# Copyright (C) MatrixEditor 2023-2024## This program is free software: you can redistribute it and/or modify# it under the terms of the GNU General Public License as published by# the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with this program. If not, see <https://www.gnu.org/licenses/>.from__future__importannotationsimportoperatorimportsysfromtypingimportCallable,Any,Union,SelffromtypesimportFrameTypefromdataclassesimportdataclassfromcaterpillar.abcimport_ContextLambda,_ContextLikefromcaterpillar.exceptionimportStructExceptionfromcaterpillar.registryimportto_structCTX_PARENT="_parent"CTX_OBJECT="_obj"CTX_OFFSETS="_offsets"CTX_STREAM="_io"CTX_FIELD="_field"CTX_VALUE="_value"CTX_POS="_pos"CTX_INDEX="_index"CTX_PATH="_path"CTX_SEQ="_is_seq"CTX_ARCH="_arch"
[docs]classContext(dict):"""Represents a context object with attribute-style access."""def__setattr__(self,key:str,value)->None:""" Sets an attribute in the context. :param key: The attribute key. :param value: The value to be set. """self[key]=valuedef__getattribute__(self,key:str):""" Retrieves an attribute from the context. :param key: The attribute key. :return: The value associated with the key. """try:returnobject.__getattribute__(self,key)exceptAttributeError:returnself.__context_getattr__(key)def__context_getattr__(self,path:str):""" Retrieves an attribute from the context. :param key: The attribute key. :return: The value associated with the key. """nodes=path.split(".")obj=(self[nodes[0]]ifnodes[0]inselfelseobject.__getattribute__(self,nodes[0]))foriinrange(1,len(nodes)):obj=getattr(obj,nodes[i])returnobjdef__context_setattr__(self,path:str,value:Any)->None:nodes=path.rsplit(".",1)iflen(nodes)==1:self[path]=valueelse:obj=self.__context_getattr__(nodes[0])setattr(obj,nodes[1],value)@propertydef_root(self)->_ContextLike:current=selfwhileCTX_PARENTincurrent:# dict-like access is much fasterparent=current[CTX_PARENT]ifparentisNone:breakcurrent=parentreturncurrent
[docs]classExprMixin:""" A mixin class providing methods for creating binary and unary expressions. """def__add__(self,other)->ExprMixin:returnBinaryExpression(operator.add,self,other)def__sub__(self,other)->ExprMixin:returnBinaryExpression(operator.sub,self,other)def__mul__(self,other)->ExprMixin:returnBinaryExpression(operator.mul,self,other)def__floordiv__(self,other)->ExprMixin:returnBinaryExpression(operator.floordiv,self,other)def__truediv__(self,other)->ExprMixin:returnBinaryExpression(operator.truediv,self,other)def__mod__(self,other)->ExprMixin:returnBinaryExpression(operator.mod,self,other)def__pow__(self,other)->ExprMixin:returnBinaryExpression(operator.pow,self,other)def__xor__(self,other)->ExprMixin:returnBinaryExpression(operator.xor,self,other)def__and__(self,other)->ExprMixin:returnBinaryExpression(operator.and_,self,other)def__or__(self,other)->ExprMixin:returnBinaryExpression(operator.or_,self,other)def__rshift__(self,other)->ExprMixin:returnBinaryExpression(operator.rshift,self,other)def__lshift__(self,other)->ExprMixin:returnBinaryExpression(operator.lshift,self,other)__div__=__truediv__def__radd__(self,other)->ExprMixin:returnBinaryExpression(operator.add,other,self)def__rsub__(self,other)->ExprMixin:returnBinaryExpression(operator.sub,other,self)def__rmul__(self,other)->ExprMixin:returnBinaryExpression(operator.mul,other,self)def__rfloordiv__(self,other)->ExprMixin:returnBinaryExpression(operator.floordiv,other,self)def__rtruediv__(self,other)->ExprMixin:returnBinaryExpression(operator.truediv,other,self)def__rmod__(self,other)->ExprMixin:returnBinaryExpression(operator.mod,other,self)def__rpow__(self,other)->ExprMixin:returnBinaryExpression(operator.pow,other,self)def__rxor__(self,other)->ExprMixin:returnBinaryExpression(operator.xor,other,self)def__rand__(self,other)->ExprMixin:returnBinaryExpression(operator.and_,other,self)def__ror__(self,other)->ExprMixin:returnBinaryExpression(operator.or_,other,self)def__rrshift__(self,other)->ExprMixin:returnBinaryExpression(operator.rshift,other,self)def__rlshift__(self,other)->ExprMixin:returnBinaryExpression(operator.lshift,other,self)def__neg__(self)->ExprMixin:returnUnaryExpression("neg",operator.neg,self)def__pos__(self)->ExprMixin:returnUnaryExpression("pos",operator.pos,self)def__invert__(self)->ExprMixin:returnUnaryExpression("invert",operator.not_,self)def__contains__(self,other)->ExprMixin:returnBinaryExpression(operator.contains,self,other)def__gt__(self,other)->ExprMixin:returnBinaryExpression(operator.gt,self,other)def__ge__(self,other)->ExprMixin:returnBinaryExpression(operator.ge,self,other)def__lt__(self,other)->ExprMixin:returnBinaryExpression(operator.lt,self,other)def__le__(self,other)->ExprMixin:returnBinaryExpression(operator.le,self,other)def__eq__(self,other)->ExprMixin:returnBinaryExpression(operator.eq,self,other)def__ne__(self,other)->ExprMixin:returnBinaryExpression(operator.ne,self,other)
[docs]classConditionContext:"""Class implementation of an inline condition. Use this class to automatically apply a condition to multiple field definitions. Note that this class will only work if it has access to the parent stack frame. .. code-block:: python @struct class Format: magic: b"MGK" length: uint32 with this.length > 32: # other field definitions here foo: uint8 This class will **replace** any existing fields! :param condition: a context lambda or constant boolean value :type condition: Union[_ContextLambda, bool] """__slots__="func","annotations","namelist","depth"def__init__(self,condition:Union[_ContextLambda,bool],depth=2):self.func=conditionself.annotations=Noneself.namelist=Noneself.depth=depthdefgetframe(self,num:int,msg=None)->FrameType:try:returnsys._getframe(num)exceptAttributeErrorasexc:raiseStructException(msg)fromexcdef__enter__(self)->Self:frame=self.getframe(self.depth,"Could not enter condition context!")# keep track of all annotationstry:self.annotations=frame.f_locals["__annotations__"]exceptAttributeErrorasexc:module=frame.f_locals.get("__module__")qualname=frame.f_locals.get("__qualname__")msg=f"Could not get annotations in {module} (context={qualname!r})"raiseStructException(msg)fromexc# store names before new fields are addedself.namelist=list(self.annotations)returnselfdef__exit__(self,*_)->None:# pylint: disable-next=import-outside-toplevelfromcaterpillar.fieldsimportFieldnew_names=set(self.annotations)-set(self.namelist)fornameinnew_names:# modify newly created fieldsfield=self.annotations[name]ifisinstance(field,Field):# field already defined/created -> check for conditioniffield.has_condition():# the field's condition AND this one must be truefield.condition=BinaryExpression(operator.and_,field.condition,self.func)else:field//=self.funcelse:# create a field (other attributes will be modified later)# ISSUE #15: The annotation must be converted to a _StructLike# object. In case we have struct classes, the special __struct__# attribute must be used.struct_obj=to_struct(field)ifnotisinstance(struct_obj,Field):struct_obj=Field(struct_obj)struct_obj.condition=self.funcself.annotations[name]=struct_objself.annotations=Noneself.namelist=None
[docs]@dataclass(repr=False)classBinaryExpression(ExprMixin):""" Represents a binary expression. :param operand: The binary operator function. :param left: The left operand. :param right: The right operand. """operand:Callable[[Any,Any],Any]left:Union[Any,_ContextLambda]right:Union[Any,_ContextLambda]def__call__(self,context:Context,**kwds):lhs=self.left(context,**kwds)ifcallable(self.left)elseself.leftrhs=self.right(context,**kwds)ifcallable(self.right)elseself.rightreturnself.operand(lhs,rhs)def__repr__(self)->str:returnf"{self.operand.__name__}{{{self.left!r}, {self.right!r}}}"def__enter__(self):# pylint: disable-next=attribute-defined-outside-initself._cond=ConditionContext(self,depth=3)self._cond.__enter__()returnselfdef__exit__(self,*_):self._cond.__exit__(*_)
[docs]@dataclassclassUnaryExpression:""" Represents a unary expression. :param name: The name of the unary operator. :param operand: The unary operator function. :param value: The operand. """name:stroperand:Callable[[Any],Any]value:Union[Any,_ContextLambda]def__call__(self,context:Context,**kwds):value=self.value(context,**kwds)ifcallable(self.value)elseself.valuereturnself.operand(value)def__repr__(self)->str:returnf"{self.operand.__name__}{{{self.value!r}}}"def__enter__(self):# pylint: disable-next=attribute-defined-outside-initself._cond=ConditionContext(self,depth=3)self._cond.__enter__()returnselfdef__exit__(self,*_):self._cond.__exit__(*_)
[docs]classContextPath(ExprMixin):""" Represents a lambda function for retrieving a value from a Context based on a specified path. """def__init__(self,path:str=None)->None:""" Initializes a ContextPath instance with an optional path. :param path: The path to use when retrieving a value from a Context. """self.path=pathself._ops_=[]self.call_kwargs=Noneself.getitem_args=Nonedef__call__(self,context:_ContextLike=None,**kwds):""" Calls the lambda function to retrieve a value from a Context. :param context: The Context from which to retrieve the value. :param kwds: Additional keyword arguments. :return: The value retrieved from the Context based on the path. """ifcontextisNone:self._ops_.append((operator.call,(),kwds))returnselfvalue=context.__context_getattr__(self.path)foroperation,args,kwargsinself._ops_:value=operation(value,*args,**kwargs)returnvaluedef__getitem__(self,key)->Self:self._ops_.append((operator.getitem,(key,),{}))returnselfdef__type__(self)->type:returnAnydef__getattribute__(self,key:str)->ContextPath:""" Gets an attribute from the ContextPath, creating a new instance if needed. :param key: The attribute key. :return: A new ContextPath instance with an updated path. """try:returnsuper().__getattribute__(key)exceptAttributeError:ifnotself.path:returnContextPath(key)returnContextPath(".".join([self.path,key]))def__repr__(self)->str:""" Returns a string representation of the ContextPath. :return: A string representation. """extra=[]foroperation,args,kwargsinself._ops_:data=[]iflen(args)>0:data.append(*map(repr,args))iflen(kwargs)>0:data.append(*[f"{x}={y!r}"forx,yinkwargs.items()])extra.append(f"{operation.__name__}({', '.join(data)})")iflen(extra)==0:returnf"Path({self.path!r})"returnf"Path({self.path!r}, {', '.join(extra)})"def__str__(self)->str:""" Returns a string representation of the path. :return: A string representation of the path. """returnself.path@propertydefparent(self)->ContextPath:path=f"{CTX_PARENT}.{CTX_OBJECT}"ifnotself.path:returnContextPath(path)returnContextPath(".".join([self.path,path]))
[docs]classContextLength(ExprMixin):def__init__(self,path:ContextPath)->None:self.path=pathdef__call__(self,context:Context=None,**kwds):""" Calls the lambda function to retrieve a value from a Context. :param context: The Context from which to retrieve the value. :param kwds: Additional keyword arguments (ignored in this implementation). :return: The value retrieved from the Context based on the path. """returnlen(self.path(context))def__repr__(self)->str:returnf"len({self.path!r})"