Skip to content

Using pycrate core objects

mitshell edited this page Feb 27, 2024 · 1 revision

How to use pycrate core objects:

  • the Charpy object: a bit string handler and consumer
  • the pack_val function: a bit string packer
  • Element and Atom objects: parent classes for representing structures
  • the Envelope object: sequence of Elements
  • the Array object: repeated list of an immutable Element
  • the Sequence object: repeated list of a mutable Element

Charpy

The Charpy class enables to deal with bit strings in Python ; more specifically, it exposes a simple API to consume a given bytes' buffer into signed or unsigned, big or little endian, integral values, or shorter bytes' buffers, providing a length in bits.

The class is defined in pycrate_core/charpy.py.

>>> from pycrate_core.charpy import Charpy
>>> help(Charpy) # a doc string is provided
[...]
>>> c = Charpy(b'this is a buffer')
>>> c
charpy(b'this is a buffer')

The representation of a Charpy instance is customizable thanks to the 2 class attributes: _REPR (type of representation) and _REPR_MAX (maximum length for the representation). All possible representations are listed in the _REPR_POS class attribute. A Charpy instance also provides direct methods to get the corresponding bit-string and hex-string:

>>> c.bin()
'011101000110100001101001011100110010000[...]0110010101110010'
>>> c.hex()
'74686973206973206120627566666572'

It saves its initial bytes' buffer into the _buf attribute and its corresponding length in bits in the _len_bit attribute. Then, it uses a cursor to go forward (and backward) in the buffer to extract values at the given offset. The cursor value is stored in the _cur attribute.

>>> c._cur # current position of the cursor in the Charpy instance
0
>>> c.len_bit() # current length in bits of the remaining buffer
128

It provides methods to extract values from the buffer given a length in bits as argument, moving the cursor forward (all methods starting with get_) or not (all methods starting with to_).

>>> c.get_int(12) # returns the value of a 12 bits integer
1862
>>> c
charpy(b"\x86\x972\x06\x972\x06\x12\x06'VffW ")
>>> c.get_int_le(12) # returns the value of a little endian 12 bits integer
-122
>>> c.get_uint(27) # returns the value of a 27 bits unsigned integer
79269940
>>> c.len_bit() # current length in bits of the remaining buffer (128-12-12-27)
81
>>> c._cur # 12 + 12 + 27
47
>>> c.get_bytes(38) # returns the buffer corresponding to the next 38 bits (zeroing last bits of the last byte)
'\xb9\x900\x900'
>>> c.get_bitlist(6) # returns the list of next 0 or 1
[0, 1, 0, 0, 1, 1]
>>> c.get_bytelist(22) # returns the list of next 22 bits unsigned byte value (zeroing last bits of the last byte)
[171, 51, 48]
>>> c.len_bit()
15
>>> c._cur
113

All the values that can be extracted are:

  • get_bytes() / to_bytes(): to get a bytes' buffer
  • to_int() / get_int(): to get a big endian signed integral value
  • to_uint() / get_uint(): to get a big endian unsigned integral value
  • to_int_le() / get_int_le(): to get a little endian signed integral value
  • to_uint_le() / get_uint_le(): to get a little endian unsigned integral value
  • to_bitlist() / get_bitlist(): to get a list of 0 and 1 integral values
  • to_bytelist() / get_bytelist(): to get a list of uint8 values

When the buffer and cursor do not provide long enough data for the extraction of a given length in bits, a CharpyErr exception is raised.

>>> c.get_uint(32) # this raises as there is only 15 remaining bits available from the original buffer

Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    c.get_uint(32)
  File "C:\Python27\lib\site-packages\pycrate-0.1.0-py2.7.egg\pycrate_core\charpy.py", line 797, in get_uint
    .format(bitlen, self._len_bit-self._cur)))
CharpyErr: bitlen overflow: 32, max 15
>>> c.rewind(17) # if no args are passed, i.e. c.rewind(), this sets the cursor to 0, reinitializing the original buffer
>>> c._cur
96
>>> c.len_bit()
32
>>> c.get_uint(32)
1717986674

Some more methods are defined in the Charpy object, e.g. to concatenate some more data at the end of the buffer stored in the _buf attribute, and to overwrite some Python built-ins. Give a look at the source code for this.

pack_val

The pack_val() function allows to pack a list of composite values: signed or unsigned, big or little endian, integral values, or bytes buffers, specifying their length in bits. The function then returns the resulting bytes buffer after concatenating all those composite values. It is the revert part of the Charpy class.

The function is defined in pycrate_core/utils_py2.py and pycrate_core/utils_py3.py, and is made available in an uniform way by importing the utils.py module.

>>> from pycrate_core.utils import pack_val
>>> help(pack_val) # a doc string is provided
[...]

The function gets a serie of 3-tuples as argument, each 3-tuple indicating the type, value and length in bits of the element to be packed. the type must be one of the following: TYPE_BYTES, TYPE_UINT, TYPE_UINT_LE, TYPE_INT or TYPE_INT_LE. When using TYPE_BYTES, the value must be a bytes' buffer, for all other types, it must be a (signed or unsigned) integral value. For little endian integral value, the length in bits must be byte-aligned.

It returns a 2-tuple with a bytes' buffer and its length in bits. In case of error, a PycrateErr exception is raised.

>>> pack_val((TYPE_BYTES, b'AAAA', 32), (TYPE_UINT, 256, 12), (TYPE_INT, -256, 12))
(b'AAAA\x10\x0f\x00', 56)
>>> pack_val((TYPE_BYTES, b'AAAA', 31), (TYPE_UINT, 256, 11), (TYPE_INT, -256, 11))
(b'AAA@@8\x00', 53)

Element parent class

The pycrate library provides a set of classes and routines to define simple and complex structures, and automate then the packing of values according to those structures to return bytes' buffer, and unpacking bytes' buffer to return the values according to them.

The Element class is the master parent class for defining structures with Pycrate. It is defined in pycrate_core/elt.py. It defines common methods for atomic (Atom, ...) and composite (Envelope, ...) classes. It must not be used directly when defining structures.

Atom basic classes

The Atom class defined in the elt.py file is also a master parent class for all atomic elements. It must not be used directly, unless you want to define a new atomic element. The most useful atomic elements are alreay defined in pycrate_core/base.py.

In particular, each of this element handles a specific type of value, and defines two methods:

  • _to_pack() for packing the value thanks to the pack_val() function,
  • _from_char() for unpacking the value from a Charpy instance.

Here is the list of already defined atomic elements, from the base.py file:

  • Buf, simply used to handle bytes' buffer,
  • NullTermStr, subclass of Buf, used to handle null terminated bytes' buffer,
  • Uint, used to handle an unsigned integer value,
  • Uint8, Uint16, Uint24, Uint32, Uint48, Uint64, all subclasses of Uint but with a fixed length in bits,
  • Int, used to handle a signed integer value,
  • Int8, Int16, Int24, Int32, Int48, Int64, all subclasses of Int but with a fixed length in bits,
  • UintLE, Uint8LE, Uint16LE, Uint24LE, Uint32LE, Uint48LE, Uint64LE, IntLE, Int8LE, Int16LE, Int24LE, Int32LE, Int48LE, Int64LE, for little endian integers.

All those atomic elements will be used in composite elements, such as Envelope, to define more complex structure with potential automation in the way they must be packed and unpacked.

Envelope class

definition

The Envelope class defines a list of internal elements. It is implemented in pycrate_core/elt.py An envelope can be defined in two different ways, by defining a tuple of elements in the _GEN class attribute:

from pycrate_core.elt  import Envelope
from pycrate_core.base import *

class MyEnvelope(Envelope):
    _GEN = (
        Uint8('U1'),
        Uint16('U2'),
        Int24('I1'),
        Int('I2', bl=36),
        Buf('B', bl=50)
        )

test1 = MyEnvelope()

Or by passing a tuple of elements with the GEN keyword during the instance initialization:

from pycrate_core.elt  import Envelope
from pycrate_core.base import *

test2 = Envelope('MyEnvelope', GEN=(
        Uint8('U1'),
        Uint16('U2'),
        Int24('I1'),
        Int('I2', bl=36),
        Buf('B', bl=50)
        ))

setting and packing

Envelope instance come with many methods ready to be used for setting values and packing them into a bytes' buffer:

>>> test1
<MyEnvelope : <U1 : 0><U2 : 0><I1 : 0><I2 : 0><B : b'\x00\x00\x00\x00\x00\x00'>>
>>> test1.get_bl() # to get the total length in bits
134
>>> test1.get_val() # to get the list of values
[0, 0, 0, 0, b'\x00\x00\x00\x00\x00\x00']
>>> test1() # same as .get_val()
[0, 0, 0, 0, b'\x00\x00\x00\x00\x00\x00']
>>> test1.set_val([53, 1234, -9999, 1, b'ABCDEFG']) # to set the whole content with new values
>>> test1()
[53, 1234, -9999, 1, b'ABCDEFG']
>>> test1.to_bytes() # to pack into a bytes' buffer
b'5\x04\xd2\xff\xd8\xf1\x00\x00\x00\x00\x14\x14$4DTd'
>>> test1.set_val({'I2': -20000000}) # we can set also just few values by their name
>>> test1()
[53, 1234, -9999, -20000000, b'ABCDEFG']
>>> test1.to_bytes()
b'5\x04\xd2\xff\xd8\xf1\xff\xec\xed0\x04\x14$4DTd'

unpacking

Methods also exist to unpack a bytes' buffer into the list of values:

>>> test1.from_bytes(17*b'B')
>>> test1()
[66, 16962, 4342338, 17786217508, b'$$$$$$\x00']

accessing the content

After instantiation, all elements are placed in a list in the _content attribute of the envelope. It is possible to access each individual element within the envelope thanks to its name, or its position:

>>> test1._content
[<U1 : 66>, <U2 : 16962>, <I1 : 4342338>, <I2 : 17786217508>, <B : b'$$$$$$\x00'>]
>>> test1['B']
<B : b'$$$$$$\x00'>
>>> test1['B'].get_bl()
50
>>> test1['B']()
b'$$$$$$\x00'
>>> test1[2] # 3rd element of the envelope
<I1 : 4342338>
>>> test1[2]()
4342338

manipulating the content

It is possible to add or remove internal elements of an envelope during its lifetime. Many methods that are used normally to handle lists in Python, are defined here to manage the content of the envelope:

>>> del test1[2]
>>> test1
<MyEnvelope : <U1 : 66><U2 : 16962><I2 : 17786217508><B : b'$$$$$$\x00'>>
>>> test1.append( Uint8('U3', val=45) )
>>> test1
<MyEnvelope : <U1 : 66><U2 : 16962><I2 : 17786217508><B : b'$$$$$$\x00'><U3 : 45>>
>>> test1()
[66, 16962, 17786217508, '$$$$$$\x00', 45]
>>> test1.remove( test1['U3'] )
>>> test1
<MyEnvelope : <U1 : 66><U2 : 16962><I2 : 17786217508><B : b'$$$$$$\x00'>>

It is possible, instead of adding or removing fields, to make them transparent. In this case, the element within the envelope is still there, with its value ; however, it is not taken into account when packing to / unpacking from bytes' buffer, neither when requesting the length in bits of the envelope.

>>> test1['U2'].set_trans(True)
>>> test1
<MyEnvelope : <U1 : 66><U2 [transparent] : 16962><I2 : 17786217508><B : b'$$$$$$\x00'>>
>>> test1.to_bytes()
b'BBBBBBBBBBB@'
>>> test1.get_bl()
94
>>> test1()
[66, 16962, 17786217508, b'$$$$$$\x00']

representation

There is a nice method show() to return a clean printable representation of any element. This comes together with different possible representations for atomic element:

  • REPR_RAW or REPR_HUM, to get the default Python representation of the value
  • REPR_BIN, to get the bit string corresponding to the value
  • REPR_HEX, to get the hex string corresponding to the value
  • REPR_HD, to get an hexdump-like representation of the value (useful for large buffer)

This representation can be changed by setting the _rep attribute of any atomic type:

>>> from pycrate_core.elt import * # this is to import all REPR_* values
>>> test1['U1']._rep = REPR_BIN
>>> test1['I2']._rep = REPR_HEX
>>> test1['B']._rep = REPR_HEX
>>> test1
<MyEnvelope : <U1 : 0b01000010><U2 [transparent] : 16962><I2 : 0x424242424><B : 0x2424242424240>>
>>> print(test1.show())
### MyEnvelope ###
 <U1 : 0b01000010>
 <U2 [transparent] : 16962>
 <I2 : 0x424242424>
 <B : 0x2424242424240>
>>> test1['B'].set_bl( 512 )
>>> test1['B'].set_val( 64*b'H' )
>>> test1['B']._rep = REPR_HD
>>> print(test1.show())
### MyEnvelope ###
 <U1 : 0b01000010>
 <U2 [transparent] : 16962>
 <I2 : 0x424242424>
 <B :
  48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 | b'HHHHHHHHHHHHHHHH'
  48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 | b'HHHHHHHHHHHHHHHH'
  48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 | b'HHHHHHHHHHHHHHHH'
  48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 | b'HHHHHHHHHHHHHHHH'>

recursivity

In order to support large structures or format, the Envelope type is recursive. This means that you can put an envelope within an envelope within an envelope...

>>> test1 = MyEnvelope('test1', val=[1, 0x101, -0x40000, 0x708080808, b'ABACADA'])
>>> test2 = MyEnvelope('test2', val=[0x80, 0, 0x2000, -1, b'ZZZXXXW'])
>>> test1.append(test2)
>>> test1
<test1 : <U1 : 1><U2 : 257><I1 : -262144><I1 : 30199515144><B : b'ABACADA'><test2 : <U1 : 128><U2 : 0><I1 : 8192><I1 : -1><B : b'ZZZXXXW'>>>
>>> print(test1.show())
### test1 ###
 <U1 : 1>
 <U2 : 257>
 <I1 : -262144>
 <I1 : 30199515144>
 <B : b'ABACADA'>
 ### test2 ###
  <U1 : 128>
  <U2 : 0>
  <I1 : 8192>
  <I1 : -1>
  <B : b'ZZZXXXW'>

hierarchy

It is possible to set a hierarchical level for any field. This enables the call of header and payload for any fields in the envelope, looking at the hierarchy of previous and next fields. The header is the previous field which has a hierarchy lower than the field requesting it, the payload is made of all the fields with an higher hierarchy than the field requesting it:

>>> test1.get_hier()
0
>>> test2.get_hier()
0
>>> test2.set_hier(1)
>>> print(test1.show())
### test1 ###
 <U1 : 1>
 <U2 : 257>
 <I1 : -262144>
 <I1 : 30199515144>
 <B : b'ABACADA'>
     ### test2 ###
      <U1 : 128>
      <U2 : 0>
      <I1 : 8192>
      <I1 : -1>
      <B : b'ZZZXXXW'>
>>> test2 == test1['test2']
True
>>> test1['test2'].get_header()
<B : b'ABACADA'>
>>> test1['B'].get_payload()
<slice : <test2 : <U1 : 128><U2 : 0><I1 : 8192><I1 : -1><B : b'ZZZXXXW'>>>

This kind of construction can help with formats where headers and payloads are explicitely defined, and need to be taken into account when operating on the data (e.g. formats defined at IETF).

automation

Finally, let's see how automation can be established between fields within an envelope. The principle characteristics of an Atom instance are:

  • its value, set in the _val attribute,
  • its length in bits, set in the _bl attribute,
  • its transparency, set in the _trans attribute,

Moreover, it is possible to associate a dictionnary for looking up values in it and print some more human-understandable value when representing the atom, hence it has a _dic attribute too.

Each of this attribute can be set with a fixed value thanks to the following methods:

  • set_val()
  • set_bl()
  • set_trans()
  • set_dic()

However, in case the element has a value that can be established automatically, the following methods can be used to set a callable instead of a fixed value:

  • set_valauto()
  • set_blauto()
  • set_transauto()
  • set_dicauto()

Those callable must ideally be set during the instance initialization stage. Here is an example for a typical definition of a Tag-Length-Value structure:

from pycrate_core.elt  import *
from pycrate_core.base import *

class MyTLV(Envelope):
    _GEN = (
        Uint16('T', desc='Tag', dic={1: 'Tag1', 2: 'Tag2', 3: 'Tag3'}),
        Uint16('L', desc='Length'),
        Buf('V', desc='Value', rep=REPR_HD)
        )
    def __init__(self, *args, **kwargs):
        Envelope.__init__(self, *args, **kwargs)
        self['L'].set_valauto( lambda: self['V'].get_bl()>>3 ) # L value automation, used when packing MyTLV
        self['V'].set_blauto( lambda: self['L'].get_val()<<3 ) # V length automation, used when unpacking MyTLV
        # warning: do not forget fields' length are defined in bits, hence the shiftings

When used, the field L does not need to be set explicitely before calling to_bytes(), neither the length in bits of V before calling from_bytes():

>>> test3 = MyTLV(val={'T': 2, 'V': 47*b'aBcDEf'})
>>> print(test3.show())
### MyTLV ###
 <T [Tag] : 2 (Tag2)>
 <L [Length] : 282>
 <V [Value] :
  61 42 63 44 45 66 61 42 63 44 45 66 61 42 63 44 | b'aBcDEfaBcDEfaBcD'
  45 66 61 42 63 44 45 66 61 42 63 44 45 66 61 42 | b'EfaBcDEfaBcDEfaB'
  [...]
  45 66 61 42 63 44 45 66 61 42 63 44 45 66 61 42 | b'EfaBcDEfaBcDEfaB'
  63 44 45 66 61 42 63 44 45 66                   | b'cDEfaBcDEf'>
>>> test3.to_bytes()
b'\x00\x02\x01\x1aaBcDEfaBcDEfaBcDEfaBcDEf[...]aBcDEf'
>>> test3.from_bytes(b'\x00\x03\x00\x20' + 0x20*b'A' + b' ... garbage')
>>> print(test3.show())
### MyTLV ###
 <T [Tag] : 3 (Tag3)>
 <L [Length] : 32>
 <V [Value] :
  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | b'AAAAAAAAAAAAAAAA'
  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | b'AAAAAAAAAAAAAAAA'>

After unpacking a bytes' buffer, all fields' value (in the _val attribute) are set explicitely, hence, if we want to change some of them and repack the envelope, we need to call the .reautomate() method to restore all automations and to be able to generate the correct bytes' buffer. For example:

>>> test3['V'] = b'abcdef'
>>> print(test3.show()) # here, the L field is not automated anymore
### MyTLV ###
 <T [Tag] : 3 (Tag3)>
 <L [Length] : 32>
 <V [Value] :
  61 62 63 64 65 66                               | b'abcdef'>
>>> test3.reautomate()
>>> print(test3.show())
### MyTLV ###
 <T [Tag] : 3 (Tag3)>
 <L [Length] : 6>
 <V [Value] :
  61 62 63 64 65 66                               | b'abcdef'>
>>> test3.to_bytes()
b'\x00\x03\x00\x06abcdef'

This example is very simple, but it is possible to create much more complex automation between fields, as soon as all relations between them are correctly set during the envelope initialization.

To end this part about Envelope, here is an example that combines many of the introduced concepts. Just play with the test4 instance to see how it behaves:

from pycrate_core.elt  import *
from pycrate_core.base import *

class MyOtherTLV(Envelope):
    _GEN = (
        Envelope('Hdr', hier=0, GEN=(
            Uint16('T', dic={1: 'Tag1', 2: 'Tag2', 3: 'Tag3'}),
            Uint16('L'),
            )),
        Envelope('Pay', hier=1, GEN=(
            Buf('V', rep=REPR_HEX),
            Uint16('EOS', val=0xffff, rep=REPR_HEX)
            ))
        )
    def __init__(self, *args, **kwargs):
        Envelope.__init__(self, *args, **kwargs)
        self['Hdr']['L'].set_valauto( lambda: self['Hdr'].get_payload().get_bl()>>3 )
        self['Pay']['V'].set_blauto( lambda: (self['Hdr']['L'].get_val()<<3)-2 )

test4 = MyOtherTLV(val={'Hdr': {'T': 3}, 'Pay': {'V': 12*b'T'}})

Array class

The Array class defines an array of values for an immutable element. It is implemented in pycrate_core/elt.py. The element used to generate the array is also used as a template to pack and unpack all values sequentially. Moreover, the initialization accepts the num keyword for specifying the number of records we want in our array.

This is generally used like this:

from pycrate_core.elt  import *
from pycrate_core.base import *

class TLV(Envelope):
    _GEN = (
        Uint16('T', desc='Tag', dic={1: 'Tag1', 2: 'Tag2', 3: 'Tag3'}),
        Uint16('L', desc='Length'),
        Buf('V', desc='Value', rep=REPR_HEX)
        )
    def __init__(self, *args, **kwargs):
        Envelope.__init__(self, *args, **kwargs)
        self['L'].set_valauto( lambda: self['V'].get_bl()>>3 )
        self['V'].set_blauto( lambda: self['L'].get_val()<<3 )

TLVArr = Array('MyTLVArr', GEN=TLV(), num=4)

In order to set values within the array, it is possible to give a list of values, or a dict with index(es) and corresponding value(s):

>>> TLVArr.set_val([{'T':1, 'V':b'abc'}, {'T':2, 'V':b'efg'}, {'T':8, 'V':b'AB'}, {'T':30, 'V':b'BA'}])
>>> TLVArr()
[[1, 3, b'abc'], [2, 3, b'efg'], [8, 2, b'AB'], [30, 2, b'BA']]
>>> print(TLVArr.show())
### MyTLVArr ###
 ### TLV ###
  <T [Tag] : 1 (Tag1)>
  <L [Length] : 3>
  <V [Value] : 0x616263>
 ### TLV ###
  <T [Tag] : 2 (Tag2)>
  <L [Length] : 3>
  <V [Value] : 0x656667>
 ### TLV ###
  <T [Tag] : 8>
  <L [Length] : 2>
  <V [Value] : 0x4142>
 ### TLV ###
  <T [Tag] : 30>
  <L [Length] : 2>
  <V [Value] : 0x4241>
>>> TLVArr.to_bytes()
b'\x00\x01\x00\x03abc\x00\x02\x00\x03efg\x00\x08\x00\x02AB\x00\x1e\x00\x02BA'
>>> TLVArr.set_val({2: {'T':18, 'V':b'ABABABAB'}})
>>> TLVArr[2]
<TLV : <T [Tag] : 18><L [Length] : 8><V [Value] : 0x4142414241424142>>

It is also possible to automate the number of records in the array, thanks to the set_numauto() method which edits the _numauto attribute. For instance:

class MyTLVArr(Envelope):
    _GEN = (
        Uint8('Num', hier=0),
        Array('Arr', GEN=TLV(), hier=1)
        )
    def __init__(self, *args, **kwargs):
        Envelope.__init__(self, *args, **kwargs)
        self['Num'].set_valauto( self['Arr'].get_num )
        self['Arr'].set_numauto( self['Num'].get_val )

MyArr = MyTLVArr(val={'Arr': [{'T':1, 'V':b'aaa'}, {'T':2, 'V':b'bbb'}]})

There is no hard limit to the number of records we can set in an instance of a MyTLVArr, except the limit of the Num header coded on 8 bits. The Num field will be encoded automatically. When unpacking a bytes' buffer, the Num field will enforce the number of records of the array Arr to unpack.

>>> print(MyArr.show())
### MyTLVArr ###
 <Num : 2>
     ### Arr ###
      ### TLV ###
       <T [Tag] : 1 (Tag1)>
       <L [Length] : 3>
       <V [Value] : 0x616161>
      ### TLV ###
       <T [Tag] : 2 (Tag2)>
       <L [Length] : 3>
       <V [Value] : 0x626262>
>>> MyArr()
[2, [[1, 3, b'aaa'], [2, 3, b'bbb']]]
>>> MyArr.to_bytes()
b'\x02\x00\x01\x00\x03aaa\x00\x02\x00\x03bbb'
>>> from binascii import unhexlify
>>> MyArr.from_bytes( unhexlify('0d000100036161610002000362626200000000000000000004000574686973200005000369732000060003616e200007000661727261793b000700033132330000000000000000000100024f4b001b000761626364656621') )
>>> print(MyArr.show())
### MyTLVArr ###
 <Num : 13>
     ### Arr ###
      ### TLV ###
       <T [Tag] : 1 (Tag1)>
       <L [Length] : 3>
       <V [Value] : 0x616161>
      ### TLV ###
       <T [Tag] : 2 (Tag2)>
       <L [Length] : 3>
       <V [Value] : 0x626262>
      [...]
      ### TLV ###
       <T [Tag] : 27>
       <L [Length] : 7>
       <V [Value] : 0x61626364656621>
>>> MyArr()
[13, [[1, 3, b'aaa'], [2, 3, b'bbb'], [0, 0, b''], [0, 0, b''], [4, 5, b'this '], [5, 3, b'is '], [6, 3, b'an '], [7, 6, b'array;'], [7, 3, b'123'], [0, 0, b''], [0, 0, b''], [1, 2, b'OK'], [27, 7, b'abcdef!']]]
>>> MyArr['Arr'][8]
<TLV : <T [Tag] : 7><L [Length] : 3><V [Value] : 0x313233>>
>>> MyArr['Arr'][8]()
[7, 3, b'123']

Sequence class

The Sequence classes defines a sequence of elements established from a template element. For each iteration, the template is cloned and set within the sequence. This renders to template mutable. It is implemented in pycrate_core/elt.py. Like Array, the Sequence instance takes a num keyword for specifying the number of iteration of the element within the sequence, this is also automatable thanks to the numauto() method setting the _numauto attribute.

The PNG class, in the pycrate_media/PNG.py illustrates the usage of the Sequence object: it is made of a signature sig 8-bytes buffer, and a PNGBody which is a Sequence of PNGChunk instances. During the unpacking process, implemented in the _from_char() method, some PNGChunk can be replaced with more specific objects (i.e. IHDR or PLTE), hence the need to use a Sequence, and not an Array, for the PNGBody.