from __future__ import annotations
import logging
logger = logging.getLogger( __name__ )
from typing import TYPE_CHECKING, Any
from mrcrowbar import common
from mrcrowbar.refs import Ref, property_get, property_set
if TYPE_CHECKING:
from mrcrowbar.blocks import Block
from mrcrowbar.fields import Field
[docs]class CheckException( Exception ):
pass
[docs]class Check:
def __init__( self, raise_exception: bool = False ):
"""Base class for Checks.
raise_exception
Whether to raise an exception if the check fails.
"""
self._position_hint = next( common.next_position_hint )
self.raise_exception = raise_exception
[docs] def check_buffer( self, buffer: common.BytesReadType, parent: Block | None = None ):
"""Check if the import buffer passes the check.
Throws CheckException if raise_exception = True and the buffer doesn't match.
"""
pass
[docs] def update_deps( self, parent: Block | None = None ):
"""Update all dependent variables derived from this Check."""
pass
def __repr__( self ):
desc = f"0x{id( self ):016x}"
if hasattr( self, "repr" ) and isinstance( self.repr, str ):
desc = self.repr
return f"<{self.__class__.__name__}: {desc}>"
[docs] def get_fields( self ):
"""Return None, a single field, or a dictionary of Fields embedded within the Check."""
return None
[docs] def get_start_offset( self, parent: Block | None = None ):
"""Return the start offset of where the Check inspects the Block."""
return 0
[docs] def get_size( self, parent: Block | None = None ):
"""Return the size of the checked data (in bytes)."""
return 0
[docs] def get_end_offset( self, parent: Block | None = None ):
"""Return the end offset of where the Check inspects the Block."""
return self.get_start_offset( parent ) + self.get_size( parent )
repr = None
[docs]class Const( Check ):
def __init__(
self, field: Field, target: Any | Ref[Any], raise_exception: bool = False
):
"""Check for ensuring a Field matches a particular constant.
On import, the value is tested. On export, the value is copied
from the target.
field
Field instance to wrap.
target
Target to copy from on export.
raise_exception
Whether to raise an exception if the check fails.
"""
super().__init__( raise_exception=raise_exception )
self.field = field
self.target = target
[docs] def get_fields( self ) -> Field:
return self.field
[docs] def check_buffer( self, buffer, parent=None ):
test = self.field.get_from_buffer( buffer, parent )
value = property_get( self.target, parent )
if test != value:
mismatch = f"{self}:{value}, found {test}!"
if self.raise_exception:
raise CheckException( mismatch )
logger.warning( mismatch )
[docs] def update_deps( self, parent=None ):
if parent:
name = parent.get_field_name_by_obj( self.field )
value = property_get( self.target, parent )
property_set( Ref( name ), parent, value )
return
[docs] def get_start_offset( self, parent=None ):
value = property_get( self.target, parent )
return self.field.get_start_offset( value, parent )
[docs] def get_size( self, parent=None ):
value = property_get( self.target, parent )
return self.field.get_size( value, parent )
@property
def repr( self ):
return f"{self.field} == {self.value}"
[docs]class Pointer( Check ):
def __init__( self, field, target, *args, **kwargs ):
"""Check for loading an offset-type pointer into a Field.
On import, the value is returned as-is. On export, the value is
copied from the target; in most cases you'd use an EndOffset for
another Field in the Block class. This allows for expansion and
contraction of data.
field
Field instance to wrap.
target
Target to copy from on export.
"""
super().__init__( *args, **kwargs )
self.field = field
self.target = target
[docs] def get_fields( self ):
return self.field
[docs] def update_deps( self, parent=None ):
if parent:
name = parent.get_field_name_by_obj( self.field )
value = property_get( self.target, parent )
property_set( Ref( name ), parent, value )
return
[docs] def get_start_offset( self, parent=None ):
value = property_get( self.target, parent )
return self.field.get_start_offset( value, parent )
[docs] def get_size( self, parent=None ):
value = property_get( self.target, parent )
return self.field.get_size( value, parent )
@property
def repr( self ):
return f"{self.field} -> {self.value}"
[docs]class Updater( Check ):
def __init__( self, source, target, *args, **kwargs ):
assert isinstance( source, Ref )
assert isinstance( target, Ref )
super().__init__( *args, **kwargs )
self.source = source
self.target = target
[docs] def update_buffer( self, buffer, parent=None ):
value = property_get( self.source, parent )
self.property_set( self.target, parent, value )
@property
def repr( self ):
return f"{self.target} = {self.source}"