pycrate/pycrate_core/elt.py

4740 lines
159 KiB
Python

# -*- coding: UTF-8 -*-
#/**
# * Software Name : pycrate
# * Version : 0.4
# *
# * Copyright 2016. Benoit Michau. ANSSI.
# * Copyright 2019. Benoit Michau. P1Sec.
# *
# * This library is free software; you can redistribute it and/or
# * modify it under the terms of the GNU Lesser General Public
# * License as published by the Free Software Foundation; either
# * version 2.1 of the License, or (at your option) any later version.
# *
# * This library 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
# * Lesser General Public License for more details.
# *
# * You should have received a copy of the GNU Lesser General Public
# * License along with this library; if not, write to the Free Software
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# * MA 02110-1301 USA
# *
# *--------------------------------------------------------
# * File Name : pycrate_core/elt.py
# * Created : 2016-02-20
# * Authors : Benoit Michau
# *--------------------------------------------------------
#*/
__all__ = ['EltErr', 'REPR_RAW', 'REPR_HEX', 'REPR_BIN', 'REPR_HD', 'REPR_HUM',
'Element', 'Atom', 'Envelope', 'Array', 'Sequence', 'Alt']
from binascii import hexlify
try:
from json import JSONEncoder, JSONDecoder
except ImportError:
_with_json = False
else:
_with_json = True
JsonEnc = JSONEncoder(sort_keys=True, indent=1)
JsonDec = JSONDecoder()
try:
import JSONDecodeError
except ImportError:
# it seems Python2.7 removed support for JSONDecodeError at some point
JSONDecodeError = ValueError
from .utils import *
from .charpy import Charpy, CharpyErr
#------------------------------------------------------------------------------#
# Elt specific error
#------------------------------------------------------------------------------#
class EltErr(PycrateErr):
pass
#------------------------------------------------------------------------------#
# global values for Element representation
#------------------------------------------------------------------------------#
REPR_RAW = 0
REPR_HEX = 1
REPR_BIN = 2
REPR_HD = 3
REPR_HUM = 4
# for hexdump representation
if python_version < 3:
def hview(buf, lw=16):
hv = []
for o in range(0, len(buf), lw):
l = buf[o:o+lw]
# create the hex fmt string for each iteration
hs = '%.2x ' * len(l) % tuple(map(ord, l))
hv.append( ' ' + hs + ' '*(3*lw-len(hs)) + '| %r' % l )
return hv
else:
def hview(buf, lw=16):
hv = []
for o in range(0, len(buf), lw):
l = buf[o:o+lw]
# create the hex fmt string for each iteration
hs = '%.2x ' * len(l) % tuple(l)
hv.append( ' ' + hs + ' '*(3*lw-len(hs)) + '| %r' % l )
return hv
#------------------------------------------------------------------------------#
# Element parent class
#------------------------------------------------------------------------------#
### class attributes to be inherited from Element:
#_SAFE_STAT
#_SAFE_DYN
### methods to be inherited from Element
#_log()
## envelope, hierarchy, selection routines
#set_env()
#get_env()
#get_next()
#get_prev()
#set_hier()
#inc_hier()
#dec_hier()
#get_hier()
#get_hier_abs()
#get_header()
#get_payload()
## format routines
#get_len()
#set_trans()
#set_transauto()
#get_trans()
## conversion routines
#from_bytes()
#to_bytes()
#__str__() (py2) / __bytes__() (py3)
#from_uint()
#to_uint()
#from_int()
#to_int()
## representation routines
#bin()
#__bin__()
#hex()
#__hex__()
class Element(object):
"""
Parent class for all atomic (Atom and children from base.py)
and composite (Envelope, Array, Sequence, Alt) elements
"""
# safety checks against user-provided data
# when setting static values
_SAFE_STAT = True
# when computing automatic values
_SAFE_DYN = True
# next / prev / header / payload element selection within an envelope
# select or not transparent element
ENV_SEL_TRANS = True
# hardcoded class name
CLASS = 'Element'
# default transparency
DEFAULT_TRANS = False
# default attributes value
_env = None
_hier = 0
_trans = None
_transauto = None
def _log(self, msg=''):
log('[%s] %s' % self._name, msg)
#--------------------------------------------------------------------------#
# envelope, hierarchy and selection routines
#--------------------------------------------------------------------------#
def set_env(self, env):
"""Set the envelope around self
Args:
env (element) : direct envelope of the element
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and env is not a valid envelope
"""
if env is None:
try:
del self._env
except Exception:
pass
else:
if self._SAFE_STAT and not isinstance(env, (Envelope,
Array,
Sequence,
Alt)):
raise(EltErr('{0} [set_env]: env type is {1}, expecting None, '\
'Envelope, Sequence, Array or Alt'\
.format(self._name, type(self._env).__name__)))
self._env = env
def get_env(self):
"""Returns the envelope around self
Args:
None
Returns:
env (element) : first envelope around element or None
"""
return self._env
def get_next(self, val=1):
"""Returns the next element in the envelope around self
Args:
val (int) : number of elements to go to after self
Returns:
next (element) : next element selected within the envelope
Raises:
EltErr : if self._SAFE_STAT enabled and val overflows the number of
elements within the envelope
"""
if self._env is None:
return None
# get index of self within its envelope
try:
ind = self._env.index(self)
except Exception:
return None
try:
if self.ENV_SEL_TRANS:
return self._env[ind+val]
else:
# do not count transparent element within the envelope
i = 1
while i <= val:
if not self._env[ind+i].get_trans():
i += 1
return self._env[ind+i]
except EltErr:
#raise(EltErr('{0} [get_next]: invalid index {1} within envelope {2}'\
# .format(self._name, ind+val, self._env._name)))
return None
def get_prev(self, val=1):
"""Returns the previous element in the envelope around self
Args:
val (int) : number of elements to go to before self
Returns:
next (element) : previous element selected within the envelope
Raises:
EltErr : if self._SAFE_STAT enabled and val overflows the number of
elements within the envelope
"""
if self._env is None:
return None
# get index of self within its envelope
try:
ind = self._env.index(self)
except Exception:
return None
if ind-val < 0:
return None
try:
if self.ENV_SEL_TRANS:
return self._env[ind-val]
else:
# do not count transparent element within the envelope
i = 1
while i <= val:
if not self._env[ind-i].get_trans():
i += 1
return self._env[ind-i]
except EltErr:
#raise(EltErr('{0} [get_prev]: invalid index {1} within envelope {2}'\
# .format(self._name, ind-val, self._env._name)))
return None
def set_hier(self, hier):
"""Set the hierarchical level of self, relative to the one of the
envelope around it if exists, absolute otherwise
Args:
hier (int) : hierarchical level of the element
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and hier is not an unsigned
integer
"""
if self._SAFE_STAT:
self._chk_hier(hier)
self._hier = hier
def _chk_hier(self, *args):
if args:
hier = args[0]
else:
hier = self._hier
if not isinstance(hier, integer_types):
raise(EltErr('{0} [_chk_hier]: hier type is {1}, expecting integer'\
.format(self._name, type(self._hier).__name__)))
elif hier < 0:
raise(EltErr('{0} [_chk_hier]: hier value is {1}, expecting unsigned value'\
.format(self._name, self._hier)))
def inc_hier(self, hier=1):
"""Increment the hierarchical level of self, relative to the one of the
envelope if exists, absolute otherwise
Args:
hier (int) : value to add to the hierarchical level
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and resulting hier is not an
unsigned integer
"""
if self._SAFE_STAT:
self._chk_hier(self._hier + hier)
self._hier += hier
def dec_hier(self, hier=1):
"""Decrement the hierarchical level of self, relative to the one of the
envelope if exists, absolute otherwise
Args:
hier (int) : value to substract to the hierarchical level
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and resulting hier is not an
unsigned integer
"""
if self._SAFE_STAT:
self._chk_hier(self._hier - hier)
self._hier -= hier
def get_hier(self):
"""Returns the hierarchical level of self within its envelope if exists,
absolute otherwise
Args:
None
Returns:
hier (int) : hierarchical level of self, unsigned
"""
return self._hier
def get_hier_abs(self):
"""Returns the absolute hierarchical level of self
Args:
None
Returns:
hier (int) : hierarchical level of the element, including
the hierarchy of all envelopes around it
"""
hier = self._hier
env = self._env
while env is not None:
hier += env._hier
env = env._env
return hier
def get_header(self):
"""Returns the header of self, according to their hierarchical level.
The header is the 1st element before self with a lower hierarchy
Args:
None
Returns:
hdr (element) : header of self,
or None if no header is found
"""
# go over all order of envelopes if necessary to find the 1st element
# with a lower hierarchical level than the element's one
elt = self
hier = self._hier
env = self._env
#
if self.ENV_SEL_TRANS:
while env is not None:
try:
ind = env.index(elt)
except Exception:
return None
for elt in env[ind-1::-1]:
if elt._hier < hier:
return elt
elt = env
hier = elt._hier
env = elt._env
else:
while env is not None:
try:
ind = env.index(elt)
except Exception:
return None
for elt in env[ind-1::-1]:
if not elt.get_trans() and elt._hier < hier:
return elt
elt = env
hier = elt._hier
env = elt._env
#
return None
def get_payload(self):
"""Returns a (sliced) envelope with all the payload elements of self,
according to their hierarchical level.
The payload is the list of all elements after self with a higher
hierarchy, without an element with a lower hierarchy than self in
between
Args:
None
Returns:
pay (element) : envelope with all payload elements of self,
or None if no payload is found
"""
# go over all order of envelopes if necessary to find the 1st element
# with a higher hierarchical level than the element's one
elt = self
hier = self._hier
env = self._env
#
if self.ENV_SEL_TRANS:
while env is not None:
try:
ind_start = env.index(elt)
except Exception:
return None
ind = 1+ind_start
ind_pay = [None, None]
for elt in env[1+ind_start:]:
# get the window of indexes corresponding to
# the full payload
if elt._hier > hier and ind_pay[0] is None:
ind_pay[0] = ind
elif elt._hier < hier and ind_pay[0] is not None:
ind_pay[1] = ind
return env[ind_pay[0]:ind_pay[1]]
ind += 1
if ind_pay[0] is not None:
return env[ind_pay[0]:]
else:
elt = env
hier = elt._hier
env = elt._env
else:
while env is not None:
try:
ind_start = env.index(elt)
except Exception:
return None
ind = 1+ind_start
ind_pay = [None, None]
for elt in env[1+ind_start:]:
# get the window of indexes corresponding to
# the full payload, jumping over transparent element
if not elt.get_trans():
if elt._hier > hier and ind_pay[0] is None:
ind_pay[0] = ind
elif elt._hier < hier and ind_pay[0] is not None:
ind_pay[1] = ind
return env[ind_pay[0]:ind_pay[1]]
ind += 1
if ind_pay[0] is not None:
return env[ind_pay[0]:]
else:
elt = env
hier = elt._hier
env = elt._env
#
return None
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def get_len(self):
"""Returns the length in bytes of self
Args:
None
Returns:
bytelen (int) : length in bytes computed
"""
bl = self.get_bl()
if bl%8:
return 1 + bl>>3
else:
return bl>>3
def set_trans(self, trans=None):
"""Set the raw transparency of self
Args:
trans (bool) : raw transparency, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and trans is not bool
"""
if trans is None:
try:
del self._trans
except Exception:
pass
else:
if self._SAFE_STAT:
self._chk_trans(trans)
self._trans = trans
def _chk_trans(self, *args):
if args:
trans = args[0]
else:
trans = self._trans
if not isinstance(trans, (NoneType, bool)):
raise(EltErr('{0} [_chk_trans]: trans type is {1}, expecting bool'\
.format(self._name, type(trans).__name__)))
def set_transauto(self, transauto=None):
"""Set the transparency automation of self
Args:
transauto (callable) : automate the transparency computation,
call transauto() to compute trans, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and transauto is not
callable
"""
if transauto is None:
try:
del self._transauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(transauto):
raise(EltErr('{0} [set_transauto]: transauto type is {1}, expecting callable'\
.format(self._name, type(transauto).__name__)))
self._transauto = transauto
def get_trans(self):
"""Returns the transparency of self
Args:
None
Returns:
trans (bool) : transparency computed,
default to class attribute DEFAULT_TRANS
Raises:
EltErr : if self._SAFE_DYN is enabled and the value produced
dynamically is not bool
"""
# follow the value resolution order:
# 1) raw trans
if self._trans is not None:
return self._trans
# 2) trans automation
elif self._transauto is not None:
trans = self._transauto()
if self._SAFE_DYN:
self._chk_trans(trans)
return trans
#
# 3) default transparency
else:
return self.DEFAULT_TRANS
def reautomate(self):
"""Reset all attributes of the element which have an automation
Args:
None
Returns:
None
"""
if self._transauto is not None and self._trans is not None:
del self._trans
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def from_bytes(self, char):
"""Consume a bytes buffer or Charpy instance `char' and sets the
internal value according to it
Args:
char (bytes or charpy): bytes buffer or charpy instance to be
consumed
Returns:
None
Raises:
EltErr : if `char' has not the correct type
CharpyErr
""".format(self.__class__.__name__)
if isinstance(char, bytes_types):
char = Charpy(char)
elif self._SAFE_STAT and not isinstance(char, Charpy):
raise(EltErr('{0} [from_bytes]: char type is {1}, expecting Charpy'\
.format(self._name, type(char).__name__)))
#
self._from_char(char)
def to_bytes(self):
"""Produce a bytes buffer from the internal value
Args:
None
Returns:
char (bytes) : resulting bytes buffer
""".format(self.__class__.__name__)
return pack_val(*self._to_pack())[0]
def from_uint(self, uint, bl=None):
"""Consume an unsigned integer or Charpy instance `uint' and sets the
internal value according to it
Args:
uint (int or charpy): unsigned integer or Charpy instance to be
consumed
bl: length in bits for `uint', if None, the minimum number of
bits is used
Returns:
None
Raises:
EltErr : if `uint' has not the correct type
CharpyErr
"""
if isinstance(uint, integer_types):
char = Charpy()
char.set_uint(uint, bl)
uint = char
elif self._SAFE_STAT and not isinstance(uint, Charpy):
raise(EltErr('{0} [from_uint]: uint type is {1}, expecting Charpy'\
.format(self._name, type(uint).__name__)))
#
self._from_char(uint)
def to_uint(self):
"""Produce an unsigned integer from the internal value
Args:
None
Returns:
uint (int) : unsigned integer
"""
try:
return bytes_to_uint(self.to_bytes(), self.get_bl())
except PycrateErr:
# an invalid value has been set, _SAFE_STAT / DYN is probably disabled
# for e.g. fuzzing purpose, but there is still need to not break here
b = self.to_bytes()
return bytes_to_uint(b, len(b)<<3)
def from_int(self, integ, bl=None):
"""Consume a signed integer or charpy instance `integ' and sets the
internal value according to it
Args:
integ (int or charpy): signed integer or Charpy instance to be
consumed
bl: length in bits for `integ', if None, the minimum number of
bits is used
Returns:
None
Raises:
EltErr : if `integ' has not the correct type
CharpyErr
"""
if isinstance(uint, integer_types):
char = Charpy()
char.set_int(integ, bl)
integ = char
elif self._SAFE_STAT and not isinstance(integ, Charpy):
raise(EltErr('{0} [from_int]: integ type is {1}, expecting Charpy'\
.format(self._name, type(integ).__name__)))
#
self._from_char(integ)
def to_int(self):
"""Produce a signed integer from the internal value
Args:
None
Returns:
integ (int) : signed integer
"""
try:
return bytes_to_int(self.to_bytes(), self.get_bl())
except PycrateErr:
# an invalid value has been set, _SAFE_STAT / DYN is probably disabled
# for e.g. fuzzing purpose, but there is still need to not break here
b = self.to_bytes()
return bytes_to_int(b, len(b)<<3)
if python_version < 3:
__str__ = to_bytes
else:
__bytes__ = to_bytes
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def bin(self):
bl = self.get_bl()
if bl == 0:
return ''
else:
bs = bytes_to_bitstr(self.to_bytes())
if len(bs) > bl:
return bs[:bl]
else:
return bs
def hex(self):
bl = self.get_bl()
if bl == 0:
return ''
else:
try:
return uint_to_hex(bytes_to_uint(self.to_bytes(), bl), bl)
except PycrateErr:
# an invalid value has been set, _SAFE_STAT / DYN is probably disabled
# for e.g. fuzzing purpose, but there is still need to not break here
return hexlify(self.to_bytes()).decode('ascii')
__bin__ = bin
__hex__ = hex
#--------------------------------------------------------------------------#
# json api
#--------------------------------------------------------------------------#
if _with_json:
def _from_jval(self, val):
raise(EltErr('not impemented'))
def _from_jval_wrap(self, val):
try:
val = val[self._name]
except Exception:
raise(EltErr('{0} [_from_jval]: invalid value, {1!r}'.format(self._name, val)))
else:
self._from_jval(val)
def from_json(self, txt):
if self.get_trans():
return
else:
try:
val = JsonDec.decode(txt)
except JSONDecodeError:
raise(EltErr('{0} [from_json]: invalid format, {1!r}'.format(self._name, txt)))
else:
self._from_jval_wrap(val)
def _to_jval(self):
raise(EltErr('not implemented'))
def _to_jval_wrap(self):
return {self._name: self._to_jval()}
def to_json(self):
if self.get_trans():
return ''
else:
return JsonEnc.encode(self._to_jval_wrap())
#------------------------------------------------------------------------------#
# Atom class, for base elements
#------------------------------------------------------------------------------#
class Atom(Element):
"""
Parent class for all atomic elements
universal attributes:
- name: str, custom one or class.__name__
- desc: str, more descriptive text, used for representation
- rep: int, type of representation (raw, hex, bin)
- val: type depends on Element subclasses, raw value of the atom
- bl: int, length of the atom in bits
- trans: bool, transparency of the atom
- dic: dict, for extended representation of the value
- hier: hierarchical level when placed within an envelope
automation attributes:
- valauto: callable, to automate the production of the atom's raw value
- blauto: callable, to automate the length in bits of the atom
- transauto: callable, to automate the determination of element's transparency
- dicauto: callable, to automate the production of element's dictionnary
contextual attributes:
- env: envelope, container of the atom
"""
# hardcoded class name
CLASS = 'Atom'
# tuple of types accepted as input value and used as possible returned value
# WNG: always ensure to flatten the list of types (e.g. when using
# bytes_types or integer_types)
TYPES = flatten(NoneType, )
TYPENAMES = get_typenames(*TYPES)
# representation parameters
# type of representation
REPR_TYPES = (REPR_RAW, REPR_HEX, REPR_BIN, REPR_HD, REPR_HUM)
# if > 0, object representation will be truncated at REPR_MAXLEN
REPR_MAXLEN = 0
# default value / bl / trans / dic
DEFAULT_VAL = None
DEFAULT_BL = 0
DEFAULT_TRANS = False
DEFAULT_DIC = {}
# default attributes value
_env = None
_hier = 0
_desc = ''
_rep = REPR_RAW
_bl = None
_blauto = None
_val = None
_valauto = None
_trans = None
_transauto = None
_dic = None
_dicauto = None
__attrs__ = ('_env',
'_name',
'_desc',
'_rep',
'_hier',
'_bl',
'_blauto',
'_val',
'_valauto',
'_trans',
'_transauto',
'_dic',
'_dicauto')
def __init__(self, *args, **kw):
"""Initializes an instance of Atom
Args:
*args: nothing or atom name (str)
**kw:
name (str): atom name if no args
desc (str): additional atom description
rep (int in cls.REPR_TYPES): atom representation type
hier (int): atom hierarchy level
bl (int): atom length in bits
val (see self.TYPES): atom value
trans (bool): atom transparency
dic (dict): atom dictionnary for friendly representation
"""
# element name in kw, or first args
if len(args):
self._name = str(args[0])
elif 'name' in kw:
self._name = str(kw['name'])
# if not provided, it's the class name
elif not hasattr(self, '_name'):
self._name = self.__class__.__name__
# element description customization
if 'desc' in kw:
self._desc = str(kw['desc'])
# element representation customization
if 'rep' in kw and kw['rep'] in self.REPR_TYPES:
self._rep = kw['rep']
# element hierarchy
if 'hier' in kw:
self._hier = kw['hier']
# element bit length
if 'bl' in kw:
self._bl = kw['bl']
# element value
if 'val' in kw:
self._val = kw['val']
# element transparency
if 'trans' in kw:
self._trans = kw['trans']
# element dictionnary
if 'dic' in kw:
self._dic = kw['dic']
if self._SAFE_STAT:
self._chk_hier()
self._chk_bl()
self._chk_val()
self._chk_trans()
self._chk_dic()
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def set_val(self, val=None):
"""Set the raw value of self
Args:
val (see self.TYPES) : raw value, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and val does not have the
correct type
"""
if val is None:
try:
del self._val
except Exception:
pass
else:
if self._SAFE_STAT:
self._chk_val(val)
self._val = val
def _chk_val(self, *args):
if args:
val = args[0]
else:
val = self._val
if not isinstance(val, self.TYPES + (NoneType,) ):
raise(EltErr('{0} [_chk_val]: val type is {1}, expecting {2}'\
.format(self._name, type(val).__name__, self.TYPENAMES)))
def set_valauto(self, valauto=None):
"""Set the value automation callable for self
Args:
valauto (callable) : automate the value computation,
call valauto() to compute the value, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT enabled and valauto is not a callable
"""
if valauto is None:
try:
del self._valauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(valauto):
raise(EltErr('{0} [set_valauto]: valauto type is {1}, expecting callable'\
.format(self._name, type(valauto).__name__)))
self._valauto = valauto
def get_val(self):
"""Returns the value of self
Args:
None
Returns:
value (see self.TYPES) : value computed,
default to class attribute DEFAULT_VAL
Raises:
EltErr : if self._SAFE_DYN is enabled and the value produced
dynamically has not the correct type
"""
# follow the value resolution order:
# 1) raw value
if self._val is not None:
return self._val
# 2) value automation
elif self._valauto is not None:
val = self._valauto()
if self._SAFE_DYN:
self._chk_val(val)
return val
# 3) default value
else:
return self.DEFAULT_VAL
# for atomic element, no dict to be returned, but just the standard value
get_val_d = get_val
def set_bl(self, bl=None):
"""Set the raw length in bits of self
Args:
bl (int) : raw bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and bl is not integer
"""
if bl is None:
try:
del self._bl
except Exception:
pass
else:
if self._SAFE_STAT:
self._chk_bl(bl)
self._bl = bl
def _chk_bl(self, *args):
if args:
bl = args[0]
else:
bl = self._bl
if not isinstance(bl, integer_types + (NoneType,)):
raise(EltErr('{0} [_chk_bl]: bl type is {1}, expecting integer'\
.format(self._name, type(bl).__name__)))
def set_blauto(self, blauto=None):
"""Set an automation for the length in bits of self, used only when
mapping an external buffer to it.
If bl is None, self._get_bl_from_val() is used
Args:
blauto (callable) : automate the bl computation,
call blauto() to compute the bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and blauto is not a callable
"""
if blauto is None:
try:
del self._blauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(blauto):
raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\
.format(self._name, type(blauto).__name__)))
self._blauto = blauto
def set_len(self, l=None):
"""Set the raw length in bytes of self
Args:
l (int) : raw byte length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and l is not integer
"""
if l is None:
try:
del self._bl
except Exception:
pass
else:
if self._SAFE_STAT:
# no need to multiply for this chk()
self._chk_bl(l)
self._bl = 8*l
def _get_bl_from_val(self):
# when bl is not defined at all, it is computed from the value set
return 0
def get_bl(self):
"""Returns the length in bits of self
Args:
None
Returns:
bl (int) : length in bits computed
default to class attribute DEFAULT_BL
"""
# follow the value resolution order:
# 0) transparency
if self.get_trans():
return 0
# 1) raw bl
elif self._bl is not None:
return self._bl
# 2) bl automation: only when parsing buffers
# see _from_char()
# 3) no bl defined, return the bl computed from the value set
elif self._val is not None or self._valauto is not None:
return self._get_bl_from_val()
# 4) no bl defined, no value defines, return the default one
else:
return self.DEFAULT_BL
def set_dic(self, dic=None):
"""Set a dictionnary for the value interpretation of self, used for
object representation
Args:
dic (dict) : value / interpretation lookup dict, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and dic is not dict
"""
if dic is None:
try:
del self._dic
except Exception:
pass
else:
if self._SAFE_STAT:
self._chk_dic(dic)
self._dic = dic
def _chk_dic(self, *args):
if args:
dic = args[0]
else:
dic = self._dic
if not isinstance(dic, (NoneType, dict)):
raise(EltErr('{0} [_chk_dic]: dic type is {1}, expecting dict'\
.format(self._name, type(dic).__name__)))
def set_dicauto(self, dicauto=None):
"""Set an automation for producing the dictionnary which helps in the
value interpretation of self
Args:
dicauto (callable) : automate the dic computation,
call dicauto() to compute the dictionnary, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and dicauto is not callable
"""
if dicauto is None:
try:
del self._dicauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(dicauto):
raise(EltErr('{0} [set_dicauto]: dicauto type is {1}, expecting callable'\
.format(self._name, type(dicauto).__name__)))
self._dicauto = dicauto
def get_dic(self):
"""Returns the dictionnary for the value interpretation of self
Args:
None
Returns:
dic (dict) : dictionnary computed
default to class attribute DEFAULT_DIC
Raises:
EltErr : if self._SAFE_DYN is enabled and the dic produced
dynamically is not dict
"""
# follow the value resolution order:
# 1) raw dic
if self._dic is not None:
return self._dic
# 2) dic automation
if self._dicauto is not None:
dic = self._dicauto()
if self._SAFE_DYN:
self._chk_dic(dic)
return dic
#
# 3) default dic
return self.DEFAULT_DIC
def get_val_dic(self):
"""Returns the looked-up value through the dictionnary returned by
self.get_dic()
Args:
None
Returns:
dic_val (depends of self.get_dic())
Raises:
EltErr : if self._SAFE_DYN is enabled and the dic produced
dynamically is not dict
"""
if self._dic is None and self._dicauto is None:
return self._val
else:
return self.get_dic().get(self._val, self._val)
def reautomate(self):
"""Reset all attributes of self which have an automation
Args:
None
Returns:
None
"""
# restore class attributes
if self._valauto is not None and self._val is not None:
del self._val
if self._blauto is not None and self._bl is not None:
del self._bl
if self._transauto is not None and self._trans is not None:
del self._trans
if self._dicauto is not None and self._dic is not None:
del self._dic
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def _to_pack(self):
"""Produces a tuple ready to be packed with pack_val() according to its
internal value
"""
if not self.get_trans():
return [(TYPE_BYTES, b'', 0)]
else:
return []
def _from_char(self, char):
"""Consume the charpy intance and set its internal value according to
it
"""
if not self.get_trans():
self.set_val(None)
#--------------------------------------------------------------------------#
# copy / cloning routines
#--------------------------------------------------------------------------#
def get_attrs(self):
"""Returns the dictionnary of universal attributes of self
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'rep' : self._rep,
'hier' : self._hier,
'bl' : self._bl,
'val' : self._val,
'trans': self._trans,
'dic' : self._dic}
def get_attrs_all(self):
"""Returns the dictionnary of all attributes of self
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'env' : self._env,
'name' : self._name,
'desc' : self._desc,
'rep' : self._rep,
'hier' : self._hier,
'bl' : self._bl,
'blauto' : self._blauto,
'val' : self._val,
'valauto' : self._valauto,
'trans' : self._trans,
'transauto': self._transauto,
'dic' : self._dic,
'dicauto' : self._dicauto}
def set_attrs(self, **kw):
"""Updates the attributes of self
Args:
kw (dict): dict of attributes and associated values
attributes can be name, desc, rep, hier, bl, val, trans and dic
Returns:
None
"""
if 'name' in kw and isinstance(kw['name'], str):
self._name = kw['name']
if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc:
self._desc = str(kw['desc'])
if 'rep' in kw and kw['rep'] in self.REPR_TYPES and \
kw['rep'] != self.__class__._rep:
self._rep = kw['rep']
if 'hier' in kw and kw['hier'] != self.__class__._hier:
self._hier = kw['hier']
if 'bl' in kw and kw['bl'] != self.__class__._bl:
self._bl = kw['bl']
if 'val' in kw and kw['val'] != self.__class__._val:
self._val = kw['val']
if 'trans' in kw and kw['trans'] != self.__class__._trans:
self._trans = kw['trans']
if 'dic' in kw and kw['dic'] != self.__class__._dic:
self._dic = kw['dic']
#
if self._SAFE_STAT:
self._chk_hier()
self._chk_bl()
self._chk_val()
self._chk_trans()
self._chk_dic()
def clone(self):
"""Produces an independent clone of self
Args:
None
Returns:
clone (self.__class__ instance)
"""
kw = {'rep': self._rep}
if self._desc != self.__class__._desc:
kw['desc'] = self._desc
if self._hier != self.__class__._hier:
kw['hier'] = self._hier
if self._bl != self.__class__._bl:
kw['bl'] = self._bl
if self._val != self.__class__._val:
kw['val'] = self._val
if self._trans != self.__class__._trans:
kw['trans'] = self._trans
if self._dic != self.__class__._dic:
kw['dic'] = self._dic
return self.__class__(self._name, **kw)
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def repr(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
# type of representation to be used
val = self()
if self._rep in (REPR_RAW, REPR_HUM):
val_repr = repr(val)
elif self._rep == REPR_BIN:
val_repr = '0b' + self.bin()
elif self._rep in (REPR_HEX, REPR_HD):
val_repr = '0x' + self.hex()
if self.REPR_MAXLEN > 0 and len(val_repr) > self.REPR_MAXLEN:
val_repr = val_repr[:self.REPR_MAXLEN] + '...'
# value informative addition with dict
dic = self.get_dic()
if dic and val in dic:
val_inf = ' (%s)' % dic[val]
else:
val_inf = ''
return '<%s%s%s : %s%s>' % (self._name, desc, trans, val_repr, val_inf)
def _repr_hd(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
# type of representation to be used: hview
buf = self.to_bytes()
if self.REPR_MAXLEN > 0 and len(buf) > self.REPR_MAXLEN:
val_repr = hview(buf[:self.REPR_MAXLEN])
if val_repr:
return ['<%s%s%s :' % (self._name, desc, trans)] + val_repr + [' ...>']
else:
return ['<%s%s%s : ...>' % (self._name, desc, trans)]
else:
val_repr = hview(buf)
if val_repr:
val_repr[-1] += '>'
return ['<%s%s%s :' % (self._name, desc, trans)] + val_repr
else:
return ['<%s%s%s : >' % (self._name, desc, trans)]
def show(self):
if self._rep == REPR_HD:
return '\n'.join([self.get_hier_abs() * ' ' + l for l in self._repr_hd()])
else:
return self.get_hier_abs() * ' ' + self.repr()
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__call__ = get_val
__repr__ = repr
#if python_implementation != 'PyPy':
# PyPy iterator implementation leads to an infinite loop
# __iter__() calls __len__(), but here, get_bl() calls __iter__()
# __len__ = get_bl
#------------------------------------------------------------------------------#
# Envelope parent class
#------------------------------------------------------------------------------#
class Envelope(Element):
"""
Class for envelopes: special element which acts as a container for other
elements (atom, envelope, array, sequence, alt)
class attribute:
- GEN: tuple of elements which is used to build the envelope content at
initialization
universal attributes:
- content: list of elements, cloned from the GEN tuple
- trans: bool, transparency of the envelope
- hier: hierarchical level when placed in an envelope
automation attribute:
- transauto: callable, to automate the determination of envelope's
transparency
contextual attributes:
- env: envelope, container of the current envelope
Envelope provides methods identical to Python list and dict in order to
manage elements within its content easily
"""
# hardcoded class name
CLASS = 'Envelope'
# default transparency
DEFAULT_TRANS = False
# default attributes value
_env = None
_hier = 0
_desc = ''
_blauto = None
_trans = None
_transauto = None
_GEN = tuple()
__attrs__ = ('_env',
'_name',
'_desc',
'_hier',
'_blauto',
'_trans',
'_transauto',
'_GEN',
'_content',
'_by_name',
'_by_id',
'_it'
'_it_saved')
def __init__(self, *args, **kw):
"""Initializes an instance of Envelope
Args:
*args: nothing or envelope name (str)
**kw:
name (str): envelope name if no args
desc (str): additional envelope description
hier (int): envelope hierarchy level
trans (bool): envelope transparency
GEN (tuple of elements): to override the GEN class attribute
content (dict): to broadcast settings into the elements within
the content
val (None, dict, tuple or list): to broadcast values into the
elements within the content, using self.set_val()
bl (tuple, list or dict): to broadcast bl into the elements
within the content, using self.set_bl()
"""
# iterator index initialization, required by __iter__()
# current iterator index:
self._it = 0
# saved iterator indexes, when nested iterations happen
self._it_saved = []
# envelope name in kw, or first args
if len(args):
self._name = str(args[0])
elif 'name' in kw:
self._name = str(kw['name'])
# if not provided, it's the class name
elif not hasattr(self, '_name'):
self._name = self.__class__.__name__
# envelope description customization
if 'desc' in kw:
self._desc = str(kw['desc'])
# envelope hierarchy
if 'hier' in kw:
self._hier = kw['hier']
# envelope transparency
if 'trans' in kw:
self._trans = kw['trans']
if 'GEN' in kw:
GEN, clo = kw['GEN'], False
else:
GEN, clo = self._GEN, True
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
self._chk_gen(GEN)
# content list generation
self._content, self._by_id, self._by_name = [], [], []
if clo:
self.extend([elt.clone() for elt in GEN])
else:
self.extend(GEN)
# if a content dict is passed as argument
# broadcast it to the given content items
if 'content' in kw:
#self._log('Envelope.__init__(content):', kw['content'])
for i in kw['content']:
self.__getitem__(i).set_attr(**kw['content'][i])
# if a val dict is passed as argument
# broadcast it to given content items
if 'val' in kw:
self.set_val( kw['val'] )
# if a bl dict is passed as argument
# broadcast it to given content items
if 'bl' in kw:
self.set_bl( kw['bl'] )
def _chk_gen(self, gen):
if not isinstance(gen, tuple) or \
not all([isinstance(elt, Element) for elt in gen]):
raise(EltErr('{0} [_chk_gen]: invalid envelope generator GEN'\
.format(self._name)))
#--------------------------------------------------------------------------#
# envelope, hierarchy and selection routines
#--------------------------------------------------------------------------#
# no change from Element
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def set_val(self, vals):
"""Set the raw values to the given elements of the content of self
Args:
vals (None, tuple, list or dict) :
tuple or list of all element's value
or dict of element's name, element's value
or dict of element's index, element's value
Returns:
None
Raises:
EltErr : if element's name, index or value are invalid, or value
setting raises
"""
if vals is None:
[elt.set_val(None) for elt in self.__iter__()]
elif isinstance(vals, (tuple, list)):
for ind, elt in enumerate(self.__iter__()):
elt.set_val(vals[ind])
elif isinstance(vals, dict):
# ordered values is sometimes required, depending of the structure
# -> happens in particular when an Alt() is present in the envelope
vals_ind = {self._by_name.index(k): v for (k, v) in vals.items() \
if isinstance(k, str_types)}
if vals_ind:
if len(vals_ind) == len(vals):
vals = vals_ind
else:
vals = {k: v for (k, v) in vals.items() \
if isinstance(k, integer_types)}
vals.update(vals_ind)
for k in sorted(vals.keys()):
self.__setitem__(k, vals[k])
elif self._SAFE_STAT:
raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, '\
'tuple, list or dict'.format(self._name, type(vals).__name__)))
def get_val(self):
"""Returns the list of values of all the elements of the content of self
Args:
None
Returns:
value (list) : list of values computed
Raises:
EltErr : if one element within the content raises
"""
return [elt() for elt in self.__iter__()]
def get_val_d(self):
"""Returns the dict of element names and values of the content of self
Wanrning: in case several elements have the same name, the returned value
won't be complete.
Args:
None
Returns:
value (dict) : dict of names and values
Raises:
EltErr : if one element within the content raises
"""
return {elt._name: elt.get_val_d() for elt in self.__iter__()}
def set_bl(self, bl):
"""Set the raw bit length to the given elements of the content of self
Args:
bl (tuple, list or dict) :
tuple or list of all element's bitlen
or dict of element's name, element's bitlen
or dict of element's index, element's bitlen
Returns:
None
Raises:
EltErr : if element's name, index or bl are invalid, or bit length
setting raises
"""
if isinstance(bl, (tuple, list)):
for ind, elt in enumerate(self.__iter__()):
elt.set_bl(bl[ind])
elif isinstance(bl, dict):
for key, val in bl.items():
self.__getitem__(key).set_bl(val)
elif self._SAFE_STAT:
raise(EltErr('{0} [set_bl]: bl type is {1}, expecting tuple, list '\
'or dict'.format(self._name, type(bl).__name__)))
def set_blauto(self, blauto=None):
"""Set an automation for the length in bits of self, used only when
mapping an external buffer to it.
Args:
blauto (callable) : automate the bl computation,
call blauto() to compute the bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and blauto is not a callable
"""
if blauto is None:
try:
del self._blauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(blauto):
raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\
.format(self._name, type(blauto).__name__)))
self._blauto = blauto
def get_bl(self):
"""Returns the total length in bits of self
Args:
None
Returns:
bl (int) : length in bits computed (sum of the content)
Raises:
EltErr : if one element within the content raises
"""
if self.get_trans():
return 0
else:
return sum([elt.get_bl() for elt in self.__iter__()])
def reautomate(self):
"""Reset all attributes of the element which have an automation within
the content of self
Args:
None
Returns:
None
"""
if self._transauto is not None and self._trans is not None:
del self._trans
[elt.reautomate() for elt in self._content]
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def _to_pack(self):
"""Produces a list of tuples (type, val, bl) ready to be packed with
pack_val()
"""
if not self.get_trans():
pl = []
[pl.extend(elt._to_pack()) for elt in self.__iter__()]
return pl
else:
return []
def _from_char(self, char):
"""Dispatch the consumption of a Charpy intance to the elements within
the content
"""
# TODO: in cases some ranges of elt within self have a fixed _bl
# it would be more efficient to unpack them in a single shot,
# especially if these are int / uint on 8, 16, 32, 64 bits
if self.get_trans():
return
# truncate char if length automation is set
if self._blauto is not None:
char_lb = char._len_bit
char._len_bit = char._cur + self._blauto()
if char._len_bit > char_lb:
raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name)))
#
for elt in self.__iter__():
elt._from_char(char)
#
# in case of length automation, set the original length back
if self._blauto is not None:
char._len_bit = char_lb
#--------------------------------------------------------------------------#
# copy / cloning routines
#--------------------------------------------------------------------------#
def get_attrs(self):
"""Returns the dictionnary of universal attributes of self and the
elements within its content
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'content': {elt._name: elt.get_attrs() for elt in self._content}}
def get_attrs_all(self):
"""Returns the dictionnary of all attributes of self and the elements
within its content
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'env' : self._env,
'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'transauto': self._transauto,
'content' : {elt._name: elt.get_attrs_all() for elt in self._content}}
def set_attrs(self, **kw):
"""Updates the attributes of self and the elements within its content
Args:
kw (dict): dict of attributes and associated values
attributes can be name, desc, hier, trans, bl, val and content
Returns:
None
"""
if 'name' in kw and isinstance(kw['name'], str):
self._name = kw['name']
if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc:
self._desc = str(kw['desc'])
if 'hier' in kw and kw['hier'] != self.__class__._hier:
self._hier = kw['hier']
if 'trans' in kw and kw['trans'] != self.__class__._trans:
self._trans = kw['trans']
#
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
#
if 'content' in kw:
for name, attrs in kw['content'].items():
self.__getitem__(name).set_attrs(**attrs)
if 'bl' in kw:
self.set_bl(kw['bl'])
if 'val' in kw:
self.set_val(kw['val'])
def clone(self):
"""Produces an independent clone of self
Args:
None
Returns:
clone (self.__class__ instance)
"""
kw = {}
if self._desc != self.__class__._desc:
kw['desc'] = self._desc
if self._hier != self.__class__._hier:
kw['hier'] = self._hier
if self._trans != self.__class__._trans:
kw['trans'] = self._trans
# substitute the Envelope generator with clones of the current
# envelope's content
kw['GEN'] = tuple([elt.clone() for elt in self._content])
return self.__class__(self._name, **kw)
#--------------------------------------------------------------------------#
# Python list / dict methods emulation
#--------------------------------------------------------------------------#
def append(self, elt):
"""Append the element `elt' at the end of the content of self
Args:
elt (element) : element to be appended
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and the type of `elt' is
not Element
"""
if self._SAFE_STAT and not isinstance(elt, Element):
raise(EltErr('{0} [append]: arg type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
# append elt to content
self._content.append(elt)
# populate by_id and by_name list
self._by_id.append(id(elt))
self._by_name.append(elt._name)
# inform elt of its new envelope
elt.set_env(self)
def count(self, name):
"""Count the number of elements with name `name' in the content of self
Args:
name (str) : name to be used for counting
Returns:
cnt (int): number of iteration of `name'
Raises:
EltErr : if name `name' has not the right type
"""
try:
return self._by_name.count(name)
except Exception as err:
raise(EltErr('{0} [count]: {1}'.format(self._name, err)))
def extend(self, elt_iter):
"""Append the list of elements `elt_iter' at the end of the content of
self
Args:
elt_iter (iterable of elements) : list of elements to be appended
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and the types produced by
`elt_iter' is not Element
"""
for elt in elt_iter:
if self._SAFE_STAT and not isinstance(elt, Element):
raise(EltErr('{0} [extend]: iterated arg type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
self._content.append(elt)
self._by_id.append(id(elt))
self._by_name.append(elt._name)
elt.set_env(self)
def index(self, elt):
"""Provide the index of the element `elt' within the content of self
Args:
elt (element) : element to be looked-up in the envelope
Returns:
ind (int) : index of the element within the envelope
Raises:
EltErr : element `elt' is not in the content
""".format(self.__class__.__name__)
try:
return self._by_id.index(id(elt))
except Exception as err:
raise(EltErr('{0} [index]: {1}'.format(self._name, err)))
def insert(self, index, elt):
"""Insert the element `elt' at the given index in the content of self
Args:
index (int) : index where to insert `elt'
elt (element) : element to be inserted
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and the type of `elt' is
not Element, or if insertion at the given index fails
"""
if self._SAFE_STAT and not isinstance(elt, Element):
raise(EltErr('{0} [insert]: arg type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
try:
self._content.insert(index, elt)
except Exception as err:
raise(EltErr('{0} [insert]: {1}'.format(self._name, err)))
else:
self._by_name.insert(index, elt._name)
self._by_id.insert(index, id(elt))
elt.set_env(self)
def pop(self):
"""Pop the last element of the content of self
Args:
None
Returns:
elt (element) : last element of the content
"""
try:
elt = self._content.pop()
except Exception as err:
raise(EltErr('{0} [pop]: {1}'.format(self._name, err)))
else:
# remove it from by_id and by_name lists
self._by_id.pop()
self._by_name.pop()
# elt has no envelope anymore
elt.set_env(None)
# return it
return elt
def remove(self, elt):
"""Remove the element `elt' from the content of self
Args:
elt (element) : element to be removed
Returns:
None
Raises:
EltErr : if element `elt' is not in the content
"""
try:
ind = self._by_id.index(id(elt))
except Exception as err:
raise(EltErr('{0} [remove]: {1}'.format(self._name, err)))
else:
del self._content[ind], self._by_id[ind], self._by_name[ind]
elt.set_env(None)
def replace(self, old, new):
"""Replace the element `old' with the element `new' in the content of
self
Args:
old (element) : element to be removed
new (element) : element to be inserted
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and the type of `new' is not
Element, or if element `old' is not in the content
"""
if self._SAFE_STAT and not isinstance(new, Element):
raise(EltErr('{0} [replace]: new type is {1}, expecting element'\
.format(self._name, type(new).__name__)))
#
try:
ind = self._by_id.index(id(old))
except Exception as err:
raise(EltErr('{0} [replace] error with old: {1}'.format(self._name, err)))
else:
# remove old
del self._content[ind], self._by_name[ind], self._by_id[ind]
old.set_env(None)
# insert new
self._content.insert(ind, new)
self._by_name.insert(ind, new._name)
self._by_id.insert(ind, id(new))
new.set_env(self)
new.set_hier(old.get_hier())
def clear(self):
"""Clear the content of self
Args:
None
Returns:
None
"""
if python_version < 3:
del self._content[:]
del self._by_id[:]
del self._by_name[:]
else:
self._content.clear()
self._by_id.clear()
self._by_name.clear()
def __iter__(self):
self._it_saved.append(self._it)
self._it = 0
return self
def __next__(self):
if self._it >= len(self._content):
if self._it_saved:
# in case of nested iteration
self._it = self._it_saved.pop() + 1
raise(StopIteration())
else:
it = self._it
self._it += 1
if self.ENV_SEL_TRANS:
# do not take element transparency into account
return self._content[it]
elif not self._content[it].get_trans():
# non-transparent element
return self._content[it]
else:
# transparent element, pass it and try the next one
return self.__next__()
if python_version < 3:
next = __next__
def __getitem__(self, key):
if isinstance(key, str_types):
try:
return self._content[self._by_name.index(key)]
except Exception as err:
raise(EltErr('{0} [__getitem__] str item: {1}'.format(self._name, err)))
elif isinstance(key, integer_types):
#print(self._name, self._content, len(self._content), key) #len(self), key)
try:
return self._content[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] int item: {1}'.format(self._name, err)))
elif isinstance(key, slice):
# a new `slice' envelope is produced
# _env and _hier attributes of elements in the content of self are not
# updated as the slice envelope is not supposed to become the new home
# of those
slice_env = Envelope('slice')
try:
slice_env._content = self._content[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] slice item: {1}'.format(self._name, err)))
else:
slice_env._by_name = self._by_name[key]
slice_env._by_id = self._by_id[key]
return slice_env
else:
raise(EltErr('{0} [__getitem__]: envelope item must be int, str or slice'\
.format(self._name)))
def __setitem__(self, key, val):
if isinstance(key, str_types):
try:
self._content[self._by_name.index(key)].set_val(val)
except Exception as err:
raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\
.format(self._name, key, val, err)))
elif isinstance(key, integer_types):
try:
self._content[key].set_val(val)
except Exception as err:
raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\
.format(self._name, key, val, err)))
elif isinstance(key, slice):
try:
[elt.set_val(val[i]) for (i, elt) in enumerate(self._content[key])]
except Exception as err:
raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\
.format(self._name, key, val, err)))
else:
raise(EltErr('{0} [__setitem__]: envelope item must be int, str or slice'\
.format(self._name)))
def __delitem__(self, key):
if isinstance(key, str_types):
try:
ind = self._by_name.index(key)
except Exception as err:
raise(EltErr('{0} [__delitem__] str item: {1}'.format(self._name, err)))
else:
self._content[ind]._env = None
del self._content[ind], self._by_name[ind], self._by_id[ind]
elif isinstance(key, integer_types):
try:
self._content[key]._env = None
except Exception as err:
raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err)))
else:
del self._content[key], self._by_name[key], self._by_id[key]
elif isinstance(key, slice):
try:
[elt.set_env(None) for elt in self._content[key]]
except Exception as err:
raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err)))
else:
del self._content[key], self._by_name[key], self._by_id[key]
else:
raise(EltErr('{0} [__delitem__]: envelope item must be int, str or slice'\
.format(self._name)))
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def repr(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '<%s%s%s : %s>' % \
(self._name, desc, trans, ''.join(map(repr, self._content)))
def show(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '\n '.join(
[self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \
[elt.show().replace('\n', '\n ') for elt in self.__iter__()])
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__call__ = get_val
__repr__ = repr
#if python_implementation != 'PyPy':
# PyPy iterator implementation lead to an infinite loop
# __iter__() calls __len__(), but here, get_bl() calls __iter__()
# __len__ = get_bl
#--------------------------------------------------------------------------#
# json interface
#--------------------------------------------------------------------------#
if _with_json:
def _from_jval(self, val):
if not isinstance(val, list):
raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val)))
i = 0
for e in self._content:
if not e.get_trans():
try:
e._from_jval_wrap(val[i])
except Exception:
break
else:
i += 1
# ensure all non-transparent elements were set
for e in self._content[1+self._content.index(e):]:
if not e.get_trans() and e.get_bl():
raise(EltErr('{0} [_from_jval]: missing elements, {1} ...'\
.format(self._name, e._name)))
def _to_jval(self):
return [e._to_jval_wrap() for e in self._content if not e.get_trans()]
class Array(Element):
"""
Class for arrays: special element which acts as a container for a list of
values for a given immutable element (atom, envelope, array, sequence, alt)
class attribute:
- GEN: element which is used to build the array template at initialization
universal attributes:
- tmpl: element, cloned from the GEN, used as proxy for generating an
array of elements from values in content
- tmpl_val: default value for the template to be used in the content, when
not explicitely set
- num: number of iterations of the template within the content
- content: list of values, formatted for the content
- trans: bool, transparency of the array
- hier: hierarchical level when placed within an envelope
automation attribute:
- numauto: callable, to automate the determination of the number of
template's iteration
- blauto: callable, to automate the length in bits to be decoded
- transauto: callable, to automate the determination of array's
transparency
contextual attributes:
- env: envelope, container of the array
Array provides methods identical to Python list and dict in order to
manage Element's instances within its content easily
"""
# hardcoded class name
CLASS = 'Array'
# default transparency
DEFAULT_TRANS = False
# default attributes value
_env = None
_hier = 0
_desc = ''
_trans = None
_transauto = None
_num = None
_numauto = None
_blauto = None
_GEN = Atom()
__attrs__ = ('_env',
'_name',
'_desc',
'_hier',
'_trans',
'_transauto',
'_num',
'_numauto',
'_blauto',
'_GEN',
'_tmpl',
'_tmpl_val',
'_tmpl_bl',
'_tmpl_pack',
'_val',
'_it',
'_it_saved')
def __init__(self, *args, **kw):
"""Initializes an instance of Array
Args:
*args: nothing or instance name (str)
**kw:
name (str): array name if no args
desc (str): additional array description
hier (int): array hierarchy level
trans (bool): array transparency
GEN (element): to override the GEN class attribute
tmpl_val (element's value): to set a default value for the
template generated from GEN
tmpl_bl (element's bl): to set default bit length for the
template generated from GEN
num (int): number of iteration within the array content
val (None, tuple, list or dict): values to be set in the array
"""
# iterator index initialization, required by __iter__()
# current iterator index:
self._it = 0
# saved iterator indexes, when nested iterations happen
self._it_saved = []
# array name in kw, or first args
if len(args):
self._name = str(args[0])
elif 'name' in kw:
self._name = str(kw['name'])
# if not provided, it's the class name
elif not hasattr(self, '_name'):
self._name = self.__class__.__name__
# array description customization
if 'desc' in kw:
self._desc = str(kw['desc'])
# array hierarchy
if 'hier' in kw:
self._hier = kw['hier']
# array transparency
if 'trans' in kw:
self._trans = kw['trans']
if 'GEN' in kw:
GEN, clo = kw['GEN'], False
else:
GEN, clo = self._GEN, True
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
if not isinstance(GEN, Element):
raise(EltErr('{0} [__init__]: invalid array generator GEN'\
.format(self._name)))
if clo:
self._tmpl = GEN.clone()
else:
self._tmpl = GEN
self._tmpl._env = self
# setting default values and format for the template element
if 'tmpl_val' in kw:
try:
self._tmpl.set_val(kw['tmpl_val'])
except Exception as err:
raise(EltErr('{0} [__init__] set template value error: {1}'\
.format(self._name, err)))
if 'tmpl_bl' in kw:
try:
self._tmpl.set_bl(kw['tmpl_bl'])
except Exception as err:
raise(EltErr('{0} [__init__] set template bl error: {1}'\
.format(self._name, err)))
# set default value, and values container
self._tmpl_val = self._tmpl()
self._tmpl_bl = self._tmpl.get_bl()
self._tmpl_pack = self._tmpl._to_pack()
self._val = []
# array number of content
if 'num' in kw:
self.set_num(kw['num'])
# values in the array
if 'val' in kw:
self.set_val(kw['val'])
#--------------------------------------------------------------------------#
# envelope, hierarchy and selection routines
#--------------------------------------------------------------------------#
# nothing changes from Element
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def set_val(self, vals):
"""Set an array of raw values according to the template
Args:
vals (None, tuple, list or dict) : tuple / list of values
or dict of array's index, array's value
Returns:
None
Raises:
EltErr : if array's index or value is invalid
"""
if vals is None:
self._val = []
#
elif isinstance(vals, dict):
max_ind = max(vals.keys())
if self._SAFE_STAT:
# ensure the val dict indexes are valid
if not all([isinstance(k, integer_types) for k in vals]):
raise(EltErr('{0} [set_val] vals keys are not all integers'\
.format(self._name)))
# ensure the max index does not overflow a fixed max size
if self._num is not None and max_ind >= self._num:
raise(EltErr('{0} [set_val] vals index {1} overflow (max {2})'\
.format(self._name, max_ind, self._num)))
# in case the current value self._val does not goes up to the max index
# just extend it with the default value
if len(self._val) <= max_ind:
self._val.extend( (1+max_ind-len(self._val)) * (self._tmpl_val, ) )
for i, v in vals.items():
self._tmpl.set_val(v)
self._val[i] = self._tmpl.get_val()
# reset the template's value
self._tmpl.set_val(None)
#
elif isinstance(vals, (tuple, list)):
if self._SAFE_STAT and self._num is not None and len(vals) != self._num:
# ensure vals length does not overflow a fixed number of iteration
raise(EltErr('{0} [set_val] invalid number of values: {1} instead of {2}'\
.format(self._name, len(vals), self._num)))
#
self._val = []
for v in vals:
self._tmpl.set_val(v)
self._val.append(self._tmpl())
# reset the template's value
self._tmpl.set_val(None)
#
else:
raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, tuple, list or dict'\
.format(self._name, type(vals).__name__)))
def get_val(self):
"""Returns the list of values of self according to its template
Args:
None
Returns:
value (list) : array of values
"""
return self._val
# for array element, no dict to be returned, but just the standard list of values
get_val_d = get_val
def set_num(self, num=None):
"""Set the raw number of iteration of the template in the array's value
Args:
num (int) : raw number of iteration, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and bitlen is not integer
"""
if num is None:
try:
del self._num
except Exception:
pass
else:
if self._SAFE_STAT and not isinstance(num, integer_types):
raise(EltErr('{0} [set_num]: num type is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
self._num = num
# clean up extra values if already set
if len(self._val) > num:
del self._val[num:]
# extend values if not already set
elif len(self._val) < num:
self._val.extend( (num-len(self._val)) * (self._tmpl_val, ) )
def set_numauto(self, numauto=None):
"""Set an automation for the number of iteration of the template in the
array's value, used only when mapping an external buffer to self
Args:
numauto (callable) : automate the num computation, call numauto()
to compute num, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and numauto is not a callable
"""
if numauto is None:
try:
del self._numauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(numauto):
raise(EltErr('{0} [set_numauto]: numauto type is {1}, expecting callable'\
.format(self._name, type(numauto).__name__)))
self._numauto = numauto
def get_num(self):
"""Returns the number of iterations of the template in the array's value
Args:
None
Returns:
num (int) : number of elements, default to the number of values
"""
# follow the value resolution order:
# 1) raw num
if self._num is not None:
return self._num
# 2) num automation: only when parsing buffers (in _from_char)
# see _from_char()
# 3) no num defined, return the num from the values already set
else:
return len(self._val)
def set_blauto(self, blauto=None):
"""Set an automation for the length in bits of self, used only when
mapping an external buffer to it.
Args:
blauto (callable) : automate the bl computation,
call blauto() to compute the bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and blauto is not a callable
"""
if blauto is None:
try:
del self._blauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(blauto):
raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\
.format(self._name, type(blauto).__name__)))
self._blauto = blauto
def get_bl(self):
"""Returns the length in bits of self
Args:
None
Returns:
bl (int) : length in bits computed
Raises:
EltErr : if one element within the content raises
"""
if self.get_trans():
return 0
else:
ret = []
for v in self._val:
if v == self._tmpl_val:
ret.append(self._tmpl_bl)
else:
self._tmpl.set_val(v)
ret.append(self._tmpl.get_bl())
self._tmpl.set_val(None)
return sum(ret)
def reautomate(self):
"""Reset all attributes of self and its template which have an automation
Args:
None
Returns:
None
"""
if self._transauto is not None and self._trans is not None:
del self._trans
if self._numauto is not None and self._num is not None:
del self._num
self._tmpl.reautomate()
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def _to_pack(self):
"""Produces a list of tuple ready to be packed with pack_val() from the
array's values through the template
"""
if not self.get_trans():
if self._SAFE_STAT and self._num is not None and len(self._val) != self._num:
raise(EltErr('{0} [_to_pack] invalid number of values: {1} instead of {2}'\
.format(self._name, len(self._val), self._num)))
pl = []
for v in self._val:
if v == self._tmpl_val:
pl.extend(self._tmpl_pack)
else:
self._tmpl.set_val(v)
pl.extend(self._tmpl._to_pack())
self._tmpl.set_val(None)
return pl
else:
return []
def _from_char(self, char):
"""Dispatch the consumption of a Charpy intance to the values within the
array through the template
"""
if self.get_trans():
return
# 1) determine the number of iteration of the template within the array
if self._numauto is not None:
num = self._numauto()
if self._SAFE_DYN and not isinstance(num, integer_types):
raise(EltErr('{0} [_from_char]: num type produced is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
elif self._num is not None:
num = self._num
else:
# num is None, _from_char will consume the charpy instance until
# it raises
num = None
# 2) truncate char if length automation is set
if self._blauto is not None:
char_lb = char._len_bit
char._len_bit = char._cur + self._blauto()
if char._len_bit > char_lb:
raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name)))
# 3) init value
self._val = []
# 4) consume char and fill in self._val
if num is not None:
for i in range(num):
self._tmpl._from_char(char)
self._val.append(self._tmpl())
else:
# there is no predefined limit in the number of iteration
# consume the charpy instance until its empty and raises
while True:
# remember charpy cursor position, to restore it when it raises
cur = char._cur
try:
self._tmpl._from_char(char)
except CharpyErr:
char._cur = cur
break
else:
self._val.append(self._tmpl())
self._tmpl.set_val(None)
# 5) in case of length automation, set the original length back
if self._blauto is not None:
char._len_bit = char_lb
#--------------------------------------------------------------------------#
# copy / cloning routines
#--------------------------------------------------------------------------#
def get_attrs(self):
"""Returns the dictionnary of universal attributes of self and its
template
Args:
None
Returns:
attr (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'num' : self._num,
'val' : self._val,
'tmpl' : self._tmpl.get_attrs(),
'tmpl_val': self._tmpl_val,
'tmpl_bl' : self._tmpl_bl}
def get_attrs_all(self):
"""Returns the dictionnary of all attributes of self and its template
Args:
None
Returns:
attr (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'transauto': self._transauto,
'num' : self._num,
'numauto' : self._numauto,
'val' : self._val,
'tmpl' : self._tmpl.get_attrs_all(),
'tmpl_val' : self._tmpl_val,
'tmpl_bl' : self._tmpl_bl}
def set_attrs(self, **kw):
"""Updates the attributes of self and its template
Args:
kw (dict): dict of attributes and associated values
attributes can be name, desc, hier, trans, tmpl, val and num
Returns:
None
"""
if 'name' in kw and isinstance(kw['name'], str):
self._name = kw['name']
if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc:
self._desc = kw['desc']
if 'hier' in kw and kw['hier'] != self.__class__._hier:
self._hier = kw['hier']
if 'trans' in kw and kw['trans'] != self.__class__._trans:
self._trans = kw['trans']
#
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
#
if 'tmpl' in kw:
self._tmpl.set_attrs(**kw['tmpl'])
self._tmpl_val = self._tmpl()
self._tmpl_bl = self._tmpl.get_bl()
self._tmpl_pack = self._tmpl._to_pack()
#
if 'val' in kw:
self.set_val(kw['val'])
if 'num' in kw:
self.set_num(kw['num'])
def clone(self):
"""Produces an independent clone of self
Args:
None
Returns:
clone (self.__class__ instance)
"""
kw = {}
if self._desc != self.__class__._desc:
kw['desc'] = self._desc
if self._hier != self.__class__._hier:
kw['hier'] = self._hier
if self._trans != self.__class__._trans:
kw['trans'] = self._trans
if self._num != self.__class__._num:
kw['num'] = self._num
if self._val:
kw['val'] = self._val[:]
# substitute the Array generator with the current array's template
kw['GEN'] = self._tmpl.clone()
return self.__class__(self._name, **kw)
#--------------------------------------------------------------------------#
# Python list / dict methods emulation
#--------------------------------------------------------------------------#
def append(self, val):
"""Append the value `val' at the end of the array's value
Args:
val (depends of self._tmpl) : value to be appended
Returns:
None
Raises:
EltErr
"""
if self._SAFE_STAT:
if self._num is not None and len(self._val) == self._num:
raise(EltErr('{0} [append] val length {1} overflow (num {2})'\
.format(self._name, 1+len(self._val), self._num)))
# use the template to format the value
if val != self._tmpl_val:
self._tmpl.set_val(val)
self._val.append(self._tmpl())
self._tmpl.set_val(None)
else:
self._val.append(val)
# here, .count() is the number of iteration in the array
count = get_num
def extend(self, vals):
"""Append the list of values `vals' at the end of the array's value
Args:
vals (list) : list of values to be appended
Returns:
None
Raises:
EltErr
"""
if self._SAFE_STAT:
if self._num is not None and len(vals) > (self._num-len(self._val)):
raise(EltErr('{0} [extend]: val length {1} overflow (num {2})'\
.format(self._name, len(self._val)+len(vals), self._num)))
# use the template to format the values
for val in vals:
if val != self._tmpl_val:
self._tmpl.set_val(val)
self._val.append(self._tmpl())
else:
self._val.append(val)
self._tmpl.set_val(None)
def index(self, val):
"""Provide the index of the first iteration of value `val' within the
array's value
Args:
val (depends of self._tmpl) : value to get the index of
Returns:
ind (int) : index of the value within the array
Raises:
EltErr : if value `val' is not in self._val
"""
try:
return self._val.index(val)
except Exception as err:
raise(EltErr('{0} [index]: {1}'.format(self._name, err)))
def insert(self, index, val):
"""Insert the value `val' at the given index in array's value
Args:
index (int) : index where to insert `val'
val (depends of self._tmpl) : value to be inserted
Returns:
None
Raises:
EltErr : if insertion at the given index fails
"""
if self._SAFE_STAT:
if self._num is not None and len(self._val) == self._num:
raise(EltErr('{0} [insert] val length {1} overflow (num {2})'\
.format(self._name, 1+len(self._val), self._num)))
# use the template to format the value
if val != self._tmpl_val:
self._tmpl.set_val(val)
val = self._tmpl()
self._tmpl.set_val(None)
try:
self._val.insert(index, val)
except Exception as err:
raise(EltErr('{0} [insert]: {1}'.format(self._name, err)))
def pop(self):
"""Pop the last value of the array wrapped within the its template
Args:
None
Returns:
elt (Element) : last element of the instance
"""
if self._SAFE_STAT and self._num is not None and len(self._val) == self._num:
raise(EltErr('{0} [pop] val length {1} underflow (num {2})'\
.format(self._name, len(self._val)-1, self._num)))
try:
val = self._val.pop()
except Exception as err:
raise(EltErr('{0} [pop]: {1}'.format(self._name, err)))
else:
clone = self._tmpl.clone()
clone._val = val
return clone
def remove(self, val):
"""Remove the first iteration of value `val' in the array's value
Args:
val (depends of self._tmpl) : value to be removed
Returns:
None
Raises:
EltErr : if value `val' is not in {0}
"""
if self._SAFE_STAT and self._num is not None and len(self._val) == self._num:
raise(EltErr('{0} [remove] val length {1} underflow (num {2})'\
.format(self._name, len(self._val)-1, self._num)))
try:
self._val.remove(val)
except Exception as err:
raise(EltErr('{0} [remove]: {1}'.format(self._name, err)))
def replace(self, old, new):
"""Replace the value `old' with the value `new' in the array's value
Args:
old (depends of self._tmpl) : value to be removed
new (depends of self._tmpl) : value to be inserted
Returns:
None
Raises:
EltErr : if element `old' is not in self._val
"""
try:
ind = self._val.index(old)
except Exception as err:
raise(EltErr('{0} [replace] invalid old: {1}'.format(self._name, err)))
# use the template to format the value
if new != self._tmpl_val:
self._tmpl.set_val(new)
new = self._tmpl()
self._tmpl.set_val(None)
del self._val[ind]
self._val.insert(ind, new)
def clear(self):
"""Clear the values of self
Args:
None
Returns:
None
"""
if python_version < 3:
del self._val[:]
else:
self._val.clear()
def __iter__(self):
self._it_saved.append(self._it)
self._it = 0
return self
def __next__(self):
if self._it >= len(self._val) or self._tmpl.get_trans():
if self._it_saved:
# in case of nested iteration
self._it = self._it_saved.pop() + 1
raise(StopIteration())
else:
it = self._it
self._it += 1
clone = self._tmpl.clone()
clone.set_val(self._val[it])
clone._env = self
return clone
if python_version < 3:
next = __next__
def __getitem__(self, key):
if isinstance(key, integer_types):
try:
val = self._val[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] int item: {1}'\
.format(self._name, err)))
clone = self._tmpl.clone()
clone.set_val(val)
return clone
elif isinstance(key, slice):
try:
val = self._val[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] slice item: {1}'\
.format(self._name, err)))
slice_env = Array('slice', GEN=self._tmpl.clone())
slice_env.set_val(val)
return slice_env
else:
raise(EltErr('{0} [__getitem__]: array item must be int or slice'.format(self._name)))
def __setitem__(self, key, val):
if isinstance(key, integer_types):
if val != self._tmpl_val:
self._tmpl.set_val(val)
val = self._tmpl()
self._tmpl.set_val(None)
try:
self._val[key] = val
except Exception as err:
raise(EltErr('{0} [__setitem__] int item: {1}'\
.format(self._name, err)))
elif isinstance(key, slice):
for i, k in enumerate(key):
try:
self.__setitem__(k, val[i])
except Exception as err:
raise(EltErr('{0} [__setitem__] slice item: {1}'\
.format(self._name, err)))
else:
raise(EltErr('{0} [__setitem__]: array item must be int or slice'.format(self._name)))
def __delitem__(self, key):
if self._SAFE_STAT and self._num is not None and len(self._val) == self._num:
raise(EltErr('{0} [__delitem__] val length {1} underflow (num {2})'\
.format(self._name, len(self._val)-1, self._num)))
if isinstance(key, integer_types):
try:
del self._val[key]
except Exception as err:
raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err)))
elif isinstance(key, slice):
try:
del self._val[key]
except Exception as err:
raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err)))
else:
raise(EltErr('{0} [__delitem__]: array item indices must be int or slice'\
.format(self._name)))
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def repr(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '<%s%s%s : %s>' % \
(self._name, desc, trans, ''.join(map(repr, self.__iter__())))
def show(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '\n '.join(
[self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \
[elt.show().replace('\n', '\n ') for elt in self.__iter__()])
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__call__ = get_val
__repr__ = repr
#if python_implementation != 'PyPy':
# PyPy iterator implementation lead to an infinite loop
# __iter__() calls __len__(), but here, get_bl() calls __iter__()
# __len__ = get_bl
#--------------------------------------------------------------------------#
# json interface
#--------------------------------------------------------------------------#
if _with_json:
def _from_jval(self, val):
if not isinstance(val, list):
raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val)))
# 1) determine the number of iteration of the template within the array
if self._numauto is not None:
num = self._numauto()
if self._SAFE_DYN and not isinstance(num, integer_types):
raise(EltErr('{0} [_from_jval]: num type produced is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
elif self._num is not None:
num = self._num
else:
# num is None, from_json will consume the txt until it raises
num = None
# 2) init value
self._val = []
# 3) consume val
if num is not None and len(val) != num:
raise(EltErr('{0} [_from_jval]: invalid number of values: {1} instead of {2}'\
.format(self._name, len(val), num)))
for v in val:
self._tmpl._from_jval_wrap(v)
self._val.append( self._tmpl.get_val() )
def _to_jval(self):
if self._SAFE_STAT and self._num is not None and len(self._val) != self._num:
raise(EltErr('{0} [_to_jval] invalid number of values: {1} instead of {2}'\
.format(self._name, len(self._val), self._num)))
ret = []
for v in self._val:
self._tmpl.set_val(v)
ret.append( self._tmpl._to_jval_wrap() )
return ret
class Sequence(Element):
"""
Class for sequences: special element which acts as a container for a list of
instances cloned from a mutable template element (atom, envelope, array,
sequence, alt)
class attribute:
- GEN: element which is used to build the sequence template at initialization
universal attributes:
- tmpl: element, cloned from the GEN, used as proxy for generating a
sequence of clones into content, also used as default value
- content: list of elements
- num: number of iteration of elements within the content
- trans: bool, transparency of the sequence
- hier: hierarchical level when placed within an envelope
automation attribute:
- numauto: callable, to automate the determination of the number of content's
iteration
- blauto: callable, to automate the length in bits to be decoded
- transauto: callable, to automate the determination of sequence's
transparency
contextual attributes:
- env: envelope, container of the sequence
Sequence provides methods identical to Python list and dict in order to
manage Element's instances within its content easily
Warning: transparency of certain elements within the Sequence's content is not
handled. Elements within the content must not be made transparent (e.g. with
set_trans()), but must be remove()d instead.
"""
# hardcoded class name
CLASS = 'Sequence'
# default transparency
DEFAULT_TRANS = False
# default attributes value
_env = None
_hier = 0
_desc = ''
_trans = None
_transauto = None
_num = None
_numauto = None
_blauto = None
_GEN = Atom()
__attrs__ = ('_env',
'_name',
'_desc',
'_hier',
'_trans',
'_transauto',
'_num',
'_numauto',
'_blauto',
'_tmpl',
'_content',
'_it',
'_it_saved')
def __init__(self, *args, **kw):
"""Initializes an instance of Sequence
Args:
*args: nothing or instance name (str)
**kw:
name (str): sequence name if no args
desc (str): additional sequence description
hier (int): sequence hierarchy level
trans (bool): sequence transparency
GEN (element): to override the GEN class attribute
tmpl_val (element's value): to set a default value for the
template generated from GEN
tmpl_bl (element's bl): to set default bit length for the
template generated from GEN
num (int): number of iteration within the sequence content
val (None, tuple, list or dict): values to be set in the sequence
"""
# iterator index initialization
# current iterator index:
self._it = 0
# saved iterator indexes, when nested iterations happen
self._it_saved = []
# sequence envelope
self._env = None
# sequence name in kw, or first args
if len(args):
self._name = str(args[0])
elif 'name' in kw:
self._name = str(kw['name'])
# if not provided, it's the class name
elif not hasattr(self, '_name'):
self._name = self.__class__.__name__
# sequence description customization
if 'desc' in kw:
self._desc = str(kw['desc'])
# sequence hierarchy
if 'hier' in kw:
self.set_hier(kw['hier'])
# sequence transparency
if 'trans' in kw:
self.set_trans(kw['trans'])
if 'GEN' in kw:
GEN, clo = kw['GEN'], False
else:
GEN, clo = self._GEN, True
# verifying sequence generator
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
if not isinstance(GEN, Element):
raise(EltErr('{0} [__init__]: invalid sequence generator GEN'\
.format(self._name)))
if clo:
self._tmpl = GEN.clone()
else:
self._tmpl = GEN
self._tmpl._env = self
if 'tmpl_val' in kw:
try:
self._tmpl.set_val(kw['tmpl_val'])
except Exception:
raise(EltErr('{0} [__init__] set template value error: {1}'\
.format(self._name, err)))
if 'tmpl_bl' in kw:
try:
self._tmpl.set_bl(kw['tmpl_bl'])
except Exception:
raise(EltErr('{0} [__init__] set template bl error: {1}'\
.format(self._name, err)))
# set default value, and values container
self._content = []
# sequence number of content
if 'num' in kw:
self.set_num(kw['num'])
# values in the sequence
if 'val' in kw:
self.set_val(kw['val'])
#--------------------------------------------------------------------------#
# envelope, hierarchy and selection routines
#--------------------------------------------------------------------------#
# nothing changes from Element
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def set_val(self, vals):
"""Set a sequence of template clones with the given values
Args:
vals (None, tuple, list or dict) : list of values
or dict of element's index, element's value
Returns:
None
Raises:
EltErr : if element's index or value is invalid
"""
#
if vals is None:
self._content = []
#
elif isinstance(vals, dict):
max_ind = max(vals.keys())
if self._SAFE_STAT:
# ensure the val dict indexes are valid
if not all([isinstance(k, integer_types) for k in vals]):
raise(EltErr('{0} [set_val] vals keys are not all integers'\
.format(self._name)))
# ensure the max index does not overflow a fixed max size
if self._num is not None and max_ind >= self._num:
raise(EltErr('{0} [set_val] vals index {1} overflow (max {2})'\
.format(self._name, max_ind, self._num)))
# in case the current content self._content does not goes up to the max
# val dict index, just extend it with the template element
if len(self._content) <= max_ind:
self._content.extend( (1+max_ind-len(self._content)) * (self._tmpl, ) )
# set values at given key indexes
for i, v in vals.items():
c = self._content[i]
if c == self._tmpl:
# in case of tmpl, clone it before assigning the value
c = c.clone()
c._env = self
self._content[i] = c
c.set_val(v)
#
elif isinstance(vals, (tuple, list)):
if self._SAFE_STAT and self._num is not None and len(vals) > self._num:
# ensure vals length does not overflow a fixed number of iteration
raise(EltErr('{0} [set_val] invalid number of values: {1} instead of {2}'\
.format(self._name, len(vals), self._num)))
# in case the current content self._content does not goes up to the max
# val dict index, just extend it with the template element
if len(self._content) < len(vals):
self._content.extend( (len(vals)-len(self._content)) * (self._tmpl, ) )
for i, v in enumerate(vals):
c = self._content[i]
if c == self._tmpl:
# in case of tmpl, clone it before assigning the value
c = c.clone()
c._env = self
self._content[i] = c
c.set_val(v)
#
else:
raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, tuple, list or dict'\
.format(self._name, type(vals).__name__)))
def get_val(self):
"""Returns the list of values of self
Args:
None
Returns:
value (list) : list of values computed
"""
return [elt() for elt in self._content]
def get_val_d(self):
"""Returns the list of values obtained with get_val_d() from the content of self
Wanrning: in case several elements have the same name, the returned value
won't be complete.
Args:
None
Returns:
value (list) : list of values obtained with get_val_d()
Raises:
EltErr : if one element within the content raises
"""
return [elt.get_val_d() for elt in self._content]
def set_num(self, num=None):
"""Set the raw number of iteration of the template in the sequence's
content
Args:
num (int) : raw number of iteration, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and bitlen is not integer
"""
if num is None:
try:
del self._num
except Exception:
pass
else:
if self._SAFE_STAT and not isinstance(num, integer_types):
raise(EltErr('{0} [set_num]: num type is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
self._num = num
# clean up extra content if already set
if len(self._content) > num:
del self._content[num:]
# extend content if not already set
elif len(self._content) < num:
self._content.extend( (num-len(self._content)) * (self._tmpl, ) )
def set_numauto(self, numauto=None):
"""Set an automation for the number of iteration of the template in the
sequence's content, used only when mapping an external buffer to self
Args:
numauto (callable) : automate the num computation, call numauto()
to compute num, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and numauto is not a
callable
"""
if numauto is None:
try:
del self._numauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(numauto):
raise(EltErr('{0} [set_numauto]: numauto type is {1}, expecting callable'\
.format(self._name, type(numauto).__name__)))
self._numauto = numauto
def get_num(self):
"""Returns the number of iterations of the template in the sequence's
content
Args:
None
Returns:
num (int) : number of elements, default to the number of values
"""
# follow the value resolution order:
# 1) raw num
if self._num is not None:
return self._num
# 2) num automation: only when parsing buffers (in _from_char)
# see _from_char()
# 3) no num defined, return the num from the content already set
else:
return len(self._content)
def set_blauto(self, blauto=None):
"""Set an automation for the length in bits of self, used only when
mapping an external buffer to it.
Args:
blauto (callable) : automate the bl computation,
call blauto() to compute the bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and blauto is not a callable
"""
if blauto is None:
try:
del self._blauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(blauto):
raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\
.format(self._name, type(blauto).__name__)))
self._blauto = blauto
def get_bl(self):
"""Returns the length in bits of self
Args:
None
Returns:
bl (int) : length in bits computed
Raises:
EltErr : if one element within the content raises
"""
if self.get_trans():
return 0
else:
return sum([elt.get_bl() for elt in self._content])
def reautomate(self):
"""Reset all attributes of self, its content and its template which have
an automation
Args:
None
Returns:
None
"""
if self._transauto is not None and self._trans is not None:
del self._trans
if self._numauto is not None and self._num is not None:
del self._num
[elt.reautomate() for elt in self._content if elt != self._tmpl]
self._tmpl.reautomate()
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def _to_pack(self):
"""Produces a list of tuple ready to be packed with pack_val() from the
sequence's content
"""
if not self.get_trans():
if self._SAFE_STAT and self._num is not None and len(self._content) != self._num:
raise(EltErr('{0} [_to_pack]: invalid number of repeated content: {1} instead of {2}'\
.format(self._name, len(self._content), self._num)))
pl = []
[pl.extend(elt._to_pack()) for elt in self._content]
return pl
else:
return []
def _from_char(self, char):
"""Dispatch the consumption of a Charpy intance to the elements within
the sequence's content
"""
if self.get_trans():
return
# 1) determine the number of iteration of the template within the sequence
if self._numauto is not None:
num = self._numauto()
if self._SAFE_DYN and not isinstance(num, integer_types):
raise(EltErr('{0} [_from_char]: num type produced is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
elif self._num is not None:
num = self._num
else:
# num is None, _from_char will consume the charpy instance until
# it raises
num = None
# 2) truncate char if length automation is set
if self._blauto is not None:
char_lb = char._len_bit
char._len_bit = char._cur + self._blauto()
if char._len_bit > char_lb:
raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name)))
# 3) init content
self._content = []
# 4) consume char and fill in self._content
if num is not None:
for i in range(num):
clone = self._tmpl.clone()
clone._env = self
clone._from_char(char)
self._content.append(clone)
else:
# there is no predefined limit in the number of repeated content
# consume the charpy instance until its empty and raises
while True:
# remember charpy cursor position, to restore it when it raises
cur = char._cur
clone = self._tmpl.clone()
clone._env = self
try:
clone._from_char(char)
except CharpyErr:
char._cur = cur
break
else:
self._content.append(clone)
# 5) in case of length automation, set the original length back
if self._blauto is not None:
char._len_bit = char_lb
#--------------------------------------------------------------------------#
# copy / cloning routines
#--------------------------------------------------------------------------#
def get_attrs(self):
"""Returns the dictionnary of universal attributes of self, its template
and content
Args:
None
Returns:
attr (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'num' : self._num,
'tmpl' : self._tmpl.get_attrs(),
'content': [elt.get_attrs() for elt in self._content]}
def get_attrs_all(self):
"""Returns the dictionnary of all attributes of self, its content
template and its values
Args:
None
Returns:
attr (dict) : dictionnary of attributes
"""
return {'env' : self._env,
'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'transauto': self._transauto,
'num' : self._num,
'numauto' : self._numauto,
'tmpl' : self._tmpl.get_attrs_all(),
'content' : [elt.get_attrs_all() for elt in self._content]}
def set_attrs(self, **kw):
"""Updates the attributes of self, its template and its content
Args:
kw (dict): dict of attributes and associated values
attributes can be name, desc, hier, trans, tmpl, val and num
Returns:
None
"""
if 'name' in kw and isinstance(kw['name'], str):
self._name = kw['name']
if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc:
self._desc = str(kw['desc'])
if 'hier' in kw and kw['hier'] != self.__class__._hier:
self._hier = kw['hier']
if 'trans' in kw and kw['trans'] != self.__class__._trans:
self._trans = kw['trans']
#
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
#
if 'tmpl' in kw:
try:
self._tmpl.set_attrs(**kw['tmpl'])
except Exception as err:
raise(EltErr('{0} [set_attrs] invalid tmpl: {1}'.format(self._name, err)))
#
if 'val' in kw:
self.set_val(kw['val'])
if 'num' in kw:
self.set_num(kw['num'])
def clone(self):
"""Produces an independent clone of self
Args:
None
Returns:
clone (self.__class__ instance)
"""
kw = {}
if self._desc != self.__class__._desc:
kw['desc'] = self._desc
if self._hier != self.__class__._hier:
kw['hier'] = self._hier
if self._trans != self.__class__._trans:
kw['trans'] = self._trans
if self._num != self.__class__._num:
kw['num'] = self._num
# substitute the sequence generator with the current sequence's template
kw['GEN'] = self._tmpl.clone()
clone = self.__class__(self._name, **kw)
# clone all elements within the content
clone._content = [elt.clone() for elt in self._content]
[elt.set_env(clone) for elt in clone._content]
return clone
#--------------------------------------------------------------------------#
# Python list / dict methods emulation
#--------------------------------------------------------------------------#
def append(self, elt):
"""Append the element `elt' at the end of the sequence's content
Args:
elt (element) : element to be appended
Returns:
None
Raises:
EltErr
"""
if self._SAFE_STAT:
if not isinstance(elt, Element):
raise(EltErr('{0} [append]: elt type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
if self._num is not None and len(self._content) == self._num:
raise(EltErr('{0} [append]: content length {1} overflow (max {2})'\
.format(self._name, 1+len(self._content), self._num)))
#
self._content.append(elt)
elt._env = self
# here, .count() is the number of iteration in the sequence
count = get_num
def extend(self, elts):
"""Append the list of elements `elts' at the end of sequence's content
Args:
elts (list) : iterable of elements to be appended
Returns:
None
Raises:
EltErr
"""
if self._SAFE_STAT:
if not all([isinstance(elt, Element) for elt in elts]):
raise(EltErr('{0} [extend]: elts type must be element'.format(self._name)))
elif self._num is not None and len(elts) > (self._num-len(self._content)):
raise(EltErr('{0} [extend]: content length {1} overflow (max {2})'\
.format(self._name, len(elts)+len(self._content), self._num)))
#
self._content.extend(elts)
[elt.set_env(self) for elt in elts]
def index(self, elt):
"""Provide the index of the element `elt' within the sequence's content
Args:
elt (element) : element to get the index of
Returns:
ind (int) : index of the element within the sequence
Raises:
EltErr : if `elt' is not in self._content
"""
try:
return self._content.index(elt)
except Exception as err:
raise(EltErr('{0} [index]: {1}'.format(self._name, err)))
def insert(self, index, elt):
"""Insert the element `elt' at the given index in sequence's content
Args:
index (int) : index where to insert `elt'
elt (element) : element to be inserted
Returns:
None
Raises:
EltErr : if `elt' is not an element or
if insertion at the given index fails
"""
if self._SAFE_STAT:
if not isinstance(elt, Element):
raise(EltErr('{0} [insert]: elt type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
if self._num is not None and len(self._content) == self._num:
raise(EltErr('{0} [insert]: content length {1} overflow (max {2})'\
.format(self._name, 1+len(self._content), self._num)))
try:
self._content.insert(index, elt)
except Exception as err:
raise(EltErr('{0} [insert]: {1}'.format(self._name, err)))
else:
elt._env = self
def pop(self):
"""Pop the last element of sequence's content
Args:
None
Returns:
elt (Element) : last element of the instance
Raises:
EltErr : if no element are already set in self
"""
if self._SAFE_STAT and self._num is not None and len(self._content) == self._num:
raise(EltErr('{0} [pop] content length {1} underflow (num {2})'\
.format(self._name, len(self._content)-1, self._num)))
try:
elt = self._content.pop()
except Exception as err:
raise(EltErr('{0} [pop]: {1}'.format(self._name, err)))
else:
if elt == self._tmpl:
return elt.clone()
else:
elt._env = None
return elt
def remove(self, elt):
"""Remove the element `elt' from the sequence's content
Args:
elt (element) : element to be removed
Returns:
None
Raises:
EltErr : if `elt' is not in self
"""
if self._SAFE_STAT and self._num is not None and len(self._content) == self._num:
raise(EltErr('{0} [remove] content length {1} underflow (num {2})'\
.format(self._name, len(self._content)-1, self._num)))
try:
self._content.remove(elt)
except Exception as err:
raise(EltErr('{0} [remove]: {1}'.format(self._name, err)))
else:
if elt != self._tmpl:
elt._env = None
def replace(self, old, new):
"""Replace the element `old' with the element `new' in the sequence's
content
Args:
old (element) : element to be removed,
alternatively, old can be the element's index (int)
new (element) : element to be inserted
Returns:
None
Raises:
EltErr : if element `old' is not in self or
if `new' is not an element
""".format(self.__class__.__name__)
if isinstance(old, Elt):
try:
ind = self._content.index(old)
except Exception as err:
raise(EltErr('{0} [replace] invalid old: {1}'.format(self._name, err)))
elif isinstance(old, integer_types):
ind = old
try:
old = self._content[ind]
except Exception:
raise(EltErr('{0} [replace] invalid old index: {1}'.format(self._name, ind)))
elif self._SAFE_STAT:
raise(EltErr('{0} [replace]: elt type is {1}, expecting element or index'\
.format(self._name, type(elt).__name__)))
del self._content[ind]
if old != self._tmpl:
old._env = None
self._content.insert(ind, new)
new._env = self
def clear(self):
"""Clear the content of self
Args:
None
Returns:
None
"""
if python_version < 3:
del self._content[:]
else:
self._content.clear()
def __iter__(self):
self._it_saved.append(self._it)
self._it = 0
return self
def __next__(self):
if self._it >= len(self._content) or self._tmpl.get_trans():
if self._it_saved:
# in case of nested iteration
self._it = self._it_saved.pop() + 1
raise(StopIteration())
else:
it = self._it
self._it += 1
if self.ENV_SEL_TRANS:
# do not take element transparency into account
return self._content[it]
elif not self._content[it].get_trans():
# non-transparent element
return self._content[it]
else:
# transparent element, pass it and try the next one
return self.__next__()
if python_version < 3:
next = __next__
def __getitem__(self, key):
if isinstance(key, integer_types):
try:
return self._content[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] int item: {1}'.format(self._name, err)))
elif isinstance(key, slice):
slice_env = Sequence('slice', GEN=self._tmpl.clone())
try:
slice_env._content = self._content[key]
except Exception as err:
raise(EltErr('{0} [__getitem__] slice item: {1}'.format(self._name, err)))
return slice_env
else:
raise(EltErr('{0} [__getitem__]: sequence item must be int or slice'\
.format(self._name)))
def __setitem__(self, key, val):
if isinstance(key, integer_types):
try:
elt = self._content[key]
except Exception as err:
raise(EltErr('{0} [__setitem__] int item: {1}'.format(self._name, err)))
if elt == self._tmpl:
self._content[key] = self._tmpl.clone()
self._content[key].set_val(val)
elif isinstance(key, slice):
for i, k in enumerate(key):
try:
self.__setitem__(k, val[i])
except Exception as err:
raise(EltErr('{0} [__setitem__] slice item: {1}'\
.format(self._name, err)))
else:
raise(EltErr('{0} [__setitem__]: sequence item must be int or slice'\
.format(self._name)))
def __delitem__(self, key):
if self._SAFE_STAT and self._num is not None and len(self._content) == self._num:
raise(EltErr('{0} [__delitem__] content length {1} underflow (num {2})'\
.format(self._name, len(self._content), self._num)))
if isinstance(key, integer_types):
try:
elt = self._content[key]
except Exception as err:
raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err)))
del self._content[key]
if elt != self._tmpl:
elt._env = None
elif isinstance(key, slice):
try:
elts = self._content[key]
except Exception as err:
raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err)))
del self._content[key]
[elt.set_env(None) for elt in elts if elt != self._tmpl]
else:
raise(EltErr('{0} [__delitem__]: sequence item must be int or slice'\
.format(self._name)))
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def repr(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '<%s%s%s : %s>' % \
(self._name, desc, trans, ''.join(map(repr, self._content)))
def show(self):
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '\n '.join(
[self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \
[elt.show().replace('\n', '\n ') for elt in self._content])
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__call__ = get_val
__repr__ = repr
#if python_implementation != 'PyPy':
# PyPy iterator implementation lead to an infinite loop
# __iter__() calls __len__(), but here, get_bl() calls __iter__()
# __len__ = get_bl
#--------------------------------------------------------------------------#
# json interface
#--------------------------------------------------------------------------#
if _with_json:
def _from_jval(self, val):
if not isinstance(val, list):
raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val)))
# 1) determine the number of iteration of the template within the array
if self._numauto is not None:
num = self._numauto()
if self._SAFE_DYN and not isinstance(num, integer_types):
raise(EltErr('{0} [_from_jval]: num type produced is {1}, expecting integer'\
.format(self._name, type(num).__name__)))
elif self._num is not None:
num = self._num
else:
# num is None, from_json will consume the txt until it raises
num = None
# 2) consume txt and fill in self._content
if num is not None and len(val) != num:
raise(EltErr('{0} [_from_jval]: invalid number of values: {1} instead of {2}'\
.format(self._name, len(val), num)))
# trying to keep potential mutated tmpl from the existing content
if len(self._content) < len(val):
while len(self._content) < len(val):
clone = self._tmpl.clone()
self._content.append(clone)
clone._env = self
elif len(self._content) > len(val):
del self._content[len(val):]
for i, v in enumerate(val):
self._content[i]._from_jval_wrap(v)
def _to_jval(self):
if self._SAFE_STAT and self._num is not None and len(self._content) != self._num:
raise(EltErr('{0} [_to_jval]: invalid number of repeated content: {1} instead of {2}'\
.format(self._name, len(self._content), self._num)))
return [e._to_jval_wrap() for e in self._content if not e.get_trans()]
class Alt(Element):
"""
Class for alternatives: special element which acts as a container for a list of
alternatives between several elements (atom, envelope, array, sequence, alt)
class attribute:
- GEN: dict of elements which is used to build all alternatives content at
initialization, keys are values that must correspond to the selector element
universal attributes:
- sel: selector callback to get the value which is used to select one of the
alternatives
- content: dict of selector values and elements cloned from the GEN dict
- trans: bool, transparency of the alternative
- hier: hierarchical level when placed in an envelope
automation attribute:
- transauto: callable, to automate the determination of alternative's
transparency
contextual attributes:
- env: envelope, container of the current alternative
Alt provides methods identical to Python list and dict in order to
manage elements within its content easily
"""
# hardcoded class name
CLASS = 'Alt'
# explicit representation behaviour
# if True, returns an explicit representation of the alternative including
# its selection value
# otherwise, returns directly the representation of the selected alternative
REPR_EXPL = True
# default transparency
DEFAULT_TRANS = False
# default element in case of invalid selection value
# if set to None, each invalid selection will raise an EltErr
DEFAULT = Envelope('none')
# default attributes value
_env = None
_hier = 0
_desc = ''
_blauto = None
_trans = None
_transauto = None
_GEN = {}
_sel = lambda a, b: None
# Warning:
# When setting the selection callback as class attribute _sel, prototype is
# lambda a, b: ..., both a & b being self, when instantiated
# When setting the selection callback during / after initialization, prototype is
# lambda a: ..., a being self
__attrs__ = ('_env',
'_name',
'_desc',
'_hier',
'_blauto',
'_trans',
'_transauto',
'_GEN',
'_sel',
'_content')
def __init__(self, *args, **kw):
"""Initializes an instance of Alt
Args:
*args: nothing or alt name (str)
**kw:
name (str): alt name if no args
desc (str): additional alt description
hier (int): alt hierarchy level
trans (bool): alt transparency
GEN (dict of key, elements): to override the GEN class attribute
sel (cb): callable to automate the alternative selection
warning: this cb must always have a single argument which
is self
val (None, dict, tuple or list): to broadcast values into the
element selected within the content, using self.set_val()
bl (tuple, list or dict): to broadcast bl into the element
selected within the content, using self.set_bl()
"""
# alt name in kw, or first args
if len(args):
self._name = str(args[0])
elif 'name' in kw:
self._name = str(kw['name'])
# if not provided, it's the class name
elif not hasattr(self, '_name'):
self._name = self.__class__.__name__
# alt description customization
if 'desc' in kw:
self._desc = str(kw['desc'])
# alt hierarchy
if 'hier' in kw:
self._hier = kw['hier']
# alt transparency
if 'trans' in kw:
self._trans = kw['trans']
if 'GEN' in kw:
self._GEN = kw['GEN']
# default alternative
if 'DEFAULT' in kw:
self.DEFAULT = kw['DEFAULT']
elif self.__class__.DEFAULT:
self.DEFAULT = self.__class__.DEFAULT.clone()
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
self._chk_gen(self._GEN)
if self.DEFAULT is not None and \
not isinstance(self.DEFAULT, Element):
raise(EltErr('{0} [__init__]: invalid DEFAULT element'\
.format(self._name)))
# content is populated with clones from GEN in a lazy way
# through calling get_alt()
self._content = {}
# alternative selection callback
if 'sel' in kw:
self.set_sel( kw['sel'] )
# if a val dict is passed as argument
# broadcast it to given content items
if 'val' in kw:
self.set_val( kw['val'] )
# if a bl dict is passed as argument
# broadcast it to given content items
if 'bl' in kw:
self.set_bl( kw['bl'] )
def _chk_gen(self, gen):
if not isinstance(gen, dict) or \
not all([isinstance(elt, Element) for elt in gen.values()]):
raise(EltErr('{0} [_chk_gen]: invalid alt generator or content'\
.format(self._name)))
#--------------------------------------------------------------------------#
# envelope, hierarchy and selection routines
#--------------------------------------------------------------------------#
# no change from Element
#--------------------------------------------------------------------------#
# format routines
#--------------------------------------------------------------------------#
def set_sel(self, cb):
"""Sets the alternative selection automation for self
Args:
cb (callable) : automate the alternative selection
Returns:
None
Raises:
EltErr : is self._SAFE_STAT enabled and cb is not a callable
"""
if self._SAFE_STAT and not callable(cb):
raise(EltErr('{0} [set_sel]: cb type is {1}, expecting callable'\
.format(self._name, type(cb).__name__)))
self._sel = cb
def get_sel(self):
"""Gets the key corresponding to the alternative to be selected
"""
try:
return self._sel(self)
except Exception:
return None
def get_alt(self):
"""Gets the selected alternative
Args:
None
Returns:
elt : selected alternative element
Raises:
EltErr : if the selection value is invalid and no DEFAULT is set
"""
sv = self.get_sel()
if sv in self._content:
elt = self._content[sv]
elt.set_env(self.get_env())
return elt
elif sv in self._GEN:
elt = self._GEN[sv].clone()
self.insert(sv, elt)
return elt
elif self.DEFAULT is not None:
self.DEFAULT.set_env(self.get_env())
return self.DEFAULT
else:
raise(EltErr('{0} [set_val]: invalid selection value {1!r}'\
.format(self._name, sv)))
def set_blauto(self, blauto=None):
"""Set an automation for the length in bits of self, used only when
mapping an external buffer to it.
Args:
blauto (callable) : automate the bl computation,
call blauto() to compute the bit length, default to None
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and blauto is not a callable
"""
if blauto is None:
try:
del self._blauto
except Exception:
pass
else:
if self._SAFE_STAT and not callable(blauto):
raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\
.format(self._name, type(blauto).__name__)))
self._blauto = blauto
# standard methods passthrough
def set_val(self, val=None):
self.get_alt().set_val(val)
def _chk_val(self, *args):
self.get_alt()._chk_val(*args)
#def set_valauto(self, valauto=None):
# self.get_alt().set_valauto(valauto)
def get_val(self):
return self.get_alt().get_val()
# for alt element, no dict to be returned, but just the standard list of values
def get_val_d(self):
alt = self.get_alt()
if isinstance(alt, Envelope):
return alt.get_val_d()
else:
return alt.get_val()
def set_bl(self, bl=None):
self.get_alt().set_bl(bl)
def _chk_bl(self, *args):
self.get_alt()._chk_bl(*args)
#def set_blauto(self, blauto=None):
# self.get_alt().set_blauto(blauto)
def set_len(self, l=None):
self.get_alt().set_len(l)
def _get_bl_from_val(self):
return self.get_alt()._get_bl_from_val()
def get_bl(self):
return self.get_alt().get_bl()
def set_dic(self, dic=None):
self.get_alt().set_dic(dic)
def _chk_dic(self, *args):
self.get_alt()._chk_dic(*args)
#def set_dicauto(self, dicauto=None):
# self.get_alt()._chk_dicauto(dicauto)
def get_dic(self):
return self.get_alt().get_dic()
def get_val_dic(self):
return self.get_alt().get_val_dic()
def reautomate(self):
"""Resets all attributes of the element which have an automation within
the content of self
Args:
None
Returns:
None
"""
if self._transauto is not None and self._trans is not None:
del self._trans
[elt.reautomate() for elt in self._content.values()]
#--------------------------------------------------------------------------#
# conversion routines
#--------------------------------------------------------------------------#
def _to_pack(self):
"""Produces a list of tuples (type, val, bl) ready to be packed with
pack_val()
"""
if not self.get_trans():
return self.get_alt()._to_pack()
else:
return []
def _from_char(self, char):
"""Dispatch the consumption of a Charpy intance to the selected element
within the content
"""
if not self.get_trans():
# truncate char if length automation is set
if self._blauto is not None:
char_lb = char._len_bit
char._len_bit = char._cur + self._blauto()
if char._len_bit > char_lb:
raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name)))
self.get_alt()._from_char(char)
# in case of length automation, set the original length back
if self._blauto is not None:
char._len_bit = char_lb
#--------------------------------------------------------------------------#
# copy / cloning routines
#--------------------------------------------------------------------------#
def get_attrs(self):
"""Returns the dictionnary of universal attributes of self and the
elements within its content
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'content': {sv: elt.get_attrs() for (sv, elt) in self._content.items()}}
def get_attrs_all(self):
"""Returns the dictionnary of all attributes of self and the elements
within its content
Args:
None
Returns:
attrs (dict) : dictionnary of attributes
"""
return {'env' : self._env,
'name' : self._name,
'desc' : self._desc,
'hier' : self._hier,
'trans' : self._trans,
'transauto': self._transauto,
'sel' : self._sel,
'content' : {sv: elt.get_attrs_all() for (sv, elt) in self._content.items()}}
def set_attrs(self, **kw):
"""Updates the attributes of self and the elements within its content
Args:
kw (dict): dict of attributes and associated values
attributes can be name, desc, hier, trans, bl, val and sel
Returns:
None
"""
if 'name' in kw and isinstance(kw['name'], str):
self._name = kw['name']
if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc:
self._desc = str(kw['desc'])
if 'hier' in kw and kw['hier'] != self.__class__._hier:
self._hier = kw['hier']
if 'trans' in kw and kw['trans'] != self.__class__._trans:
self._trans = kw['trans']
#
if self._SAFE_STAT:
self._chk_hier()
self._chk_trans()
#
if 'sel' in kw:
self.set_sel(kw['sel'])
if 'bl' in kw:
self.set_bl(kw['bl'])
if 'val' in kw:
self.set_val(kw['val'])
def clone(self):
"""Produces an independent clone of self
Args:
None
Returns:
clone (self.__class__ instance)
"""
kw = {
'GEN': self._GEN,
'DEFAULT': self.DEFAULT.clone(),
'sel': self._sel
}
if self._desc != self.__class__._desc:
kw['desc'] = self._desc
if self._hier != self.__class__._hier:
kw['hier'] = self._hier
if self._trans != self.__class__._trans:
kw['trans'] = self._trans
clone = self.__class__(self._name, **kw)
clone.insert(self.get_sel(), self.get_alt().clone())
return clone
#--------------------------------------------------------------------------#
# Python list / dict methods emulation
#--------------------------------------------------------------------------#
def update(self, elt_alt):
"""Updates the dict of alternatives element with the content of `elt_alt'
Args:
elt_alt (dict of {selection value: element}) : dict of alternatives
to update self._content with
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and one of the alternative has
an invalid type
"""
for sv, elt in elt_alt.items():
if self._SAFE_STAT and not isinstance(elt, Element):
raise(EltErr('{0} [update]: alternative arg type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
self._content[sv] = elt
elt.set_env(self.get_env())
def insert(self, sv, elt):
"""Insert the element `elt' with the given selection value `sv' in the
content of self
Args:
sv : selectio value
elt (element) : element to be inserted
Returns:
None
Raises:
EltErr : if self._SAFE_STAT is enabled and the type of `elt' is
not Element, or if selection value is invalid
"""
if self._SAFE_STAT and not isinstance(elt, Element):
raise(EltErr('{0} [insert]: arg type is {1}, expecting element'\
.format(self._name, type(elt).__name__)))
try:
self._content[sv] = elt
except Exception:
raise(EltErr('{0} [insert]: selection value is invalid, {1}'.format(self._name, sv)))
else:
elt.set_env(self.get_env())
def index(self, elt):
"""Provide the selection value of the element `elt' within the content
of self
Args:
elt (element) : element to be looked-up in the alt
Returns:
ind (int) : selection value of the element within the envelope
Raises:
EltErr : element `elt' is not in the content
"""
for sv, alt in self._content.items():
if elt == alt:
return sv
raise(EltErr('{0} [index]: non existent element, {1}'.format(self._name, elt)))
def clear(self):
"""Clears the content of self
Args:
None
Returns:
None
"""
self._content.clear()
# subscript methods passthrough
def __getitem__(self, key):
return self.get_alt().__getitem__(key)
def __setitem__(self, key, val):
return self.get_alt().__setitem__(key, val)
def __delitem(self, key):
return self.get_alt().__delitem__(key)
#--------------------------------------------------------------------------#
# representation routines
#--------------------------------------------------------------------------#
def repr(self):
if self.REPR_EXPL:
sv, alt = self.get_sel(), self.get_alt()
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
return '<%s%s%s : %r -> %s' % (self._name, desc, trans, sv, alt.repr()[1:])
else:
return self.get_alt().repr()
def show(self):
if self.REPR_EXPL:
sv, alt = self.get_sel(), self.get_alt()
# element transparency
if self.get_trans():
trans = ' [transparent]'
else:
trans = ''
# additional description
if self._desc:
desc = ' [%s]' % self._desc
else:
desc = ''
#
alts = alt.show()
if alts.lstrip()[:4] == '### ':
# when the alternative is a constructed element
return alts.replace('### ', '### %s%s%s : %r -> ' % (self._name, desc, trans, sv), 1)
else:
# when the alternative is a base element
spaces = self.get_hier_abs() * ' '
return '%s### %s%s%s : %r ###\n %s' % (spaces, self._name, desc, trans, sv, alts)
else:
alt = self.get_alt()
_hier = alt._hier
alt._hier = self._hier
s = alt.show()
alt._hier = _hier
return s
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__call__ = get_val
__repr__ = repr
#if python_implementation != 'PyPy':
# PyPy iterator implementation lead to an infinite loop
# __iter__() calls __len__(), but here, get_bl() calls __iter__()
# __len__ = get_bl
#--------------------------------------------------------------------------#
# json interface
#--------------------------------------------------------------------------#
if _with_json:
def _from_jval(self, val):
self.get_alt()._from_jval_wrap(val)
def _to_jval(self):
return self.get_alt()._to_jval_wrap()