Reference

Sequences of Packets

Until now we created a fixed count of fields and packets.

>>> from bisturi.packet import Packet
>>> from bisturi.field  import Data, Int, Ref

>>> class TypeLenValue(Packet):
...    type = Int(1)
...    length = Int(1)
...    value = Data(length)

Now imagine that we need a list of them:

>>> class Attributes(Packet):
...    count = Int(1)
...    attributes = Ref(TypeLenValue).repeated(count)

attributes is a TypeLenValue packet repeated count times.

bisturi will represent that as a list as you may expect:

>>> s = b'\x02\x01\x02ab\x04\x03abc'
>>> p = Attributes.unpack(s)    # 2 attributes: '1,2,ab' and '4,3,abc'
>>> p.count
2

>>> for attr in p.attributes:
...     print(attr)
TypeLenValue:
  type: 1
  length: 2
  value: b'ab'
TypeLenValue:
  type: 4
  length: 3
  value: b'abc'

>>> p.pack() == s
True

The field is always represented as a list which may contain zero, one or more elements.

>>> s = b'\x01\x01\x02ab'
>>> p = Attributes.unpack(s)
>>> p.count
1
>>> len(p.attributes)
1
>>> p.attributes[0]
TypeLenValue:
  type: 1
  length: 2
  value: b'ab'

>>> p.pack() == s
True

>>> s = b'\x00'
>>> p = Attributes.unpack(s)
>>> p.count
0
>>> len(p.attributes)
0

>>> p.pack() == s
True

Repeat until

With the repeated method we can repeat not only a fixed amount of times a referenced packet but the count can be dynamic as well.

In the previous example the count was determinate by the count field; in the following example we use a callable.

Imagine that Attributes does not have a count. Instead the end of the attributes list is marked by the attribute which type is zero.

We can write the following

>>> class Attributes(Packet):
...    attributes = Ref(TypeLenValue).repeated(until=lambda pkt, **k: pkt.attributes[-1].type == 0)

The callback receives, among other things, the current packet. Accessing to attributes[-1] gives us the latest attribute unpacked so far.

So pkt.attributes[-1].type == 0 means unpack the list of attributes until the last attribute’s type is zero.

>>> s =  b'\x01\x02ab\x04\x03abc\x00\x00'
>>> p = Attributes.unpack(s)
>>> for attr in p.attributes:
...     print(attr)
TypeLenValue:
  type: 1
  length: 2
  value: b'ab'
TypeLenValue:
  type: 4
  length: 3
  value: b'abc'
TypeLenValue:
  type: 0
  length: 0
  value: b''

>>> p.pack() == s
True

Note how the attributes field is created at the begin of the parsing and updated later during the parsing.

The until callback is then evaluated in each cycle so you can ask for the last attribute created with attributes[-1].

Repeat when

By definition then, when you use until you have a one or more semantics. (The list will always have at least one element).

To support zero or more constructions we need the when condition.

Imagine that Attributes has a list of attributes like before but also a flag that says if the list is empty or not.

>>> class Attributes(Packet):
...    has_attributes = Int(1)
...    attributes = Ref(TypeLenValue).repeated(
...                 when=lambda pkt, **k: pkt.has_attributes,
...                 until=lambda pkt, **k: pkt.attributes[-1].type == 0
...                 )

The until is like the previous example. The new thing is the when condition. This one is evaluated once before parsing any attribute.

>>> s = b'\x00'
>>> p = Attributes.unpack(s)
>>> p.has_attributes
0
>>> p.attributes
[]
>>> len(p.attributes)
0

>>> p.pack() == s
True

The when condition can be combined with a fixed count, but you cannot mix a fixed count with the until condition.

>>> class Attributes(Packet):
...    has_attributes = Int(1)
...    attributes = Ref(TypeLenValue).repeated(
...                 count=2,
...                 when=lambda pkt, **k: pkt.has_attributes
...                 )

>>> s = b'\x01\x01\x02ab\x04\x03abc'
>>> p = Attributes.unpack(s)
>>> for attr in p.attributes:
...     print(attr)
TypeLenValue:
  type: 1
  length: 2
  value: b'ab'
TypeLenValue:
  type: 4
  length: 3
  value: b'abc'

>>> p.pack() == s
True

>>> s = b'\x00'
>>> p = Attributes.unpack(s)
>>> p.has_attributes
0
>>> p.attributes
[]

>>> p.pack() == s
True

Here is another example with until: collect all the attributes until you run out of data to unpack except the last 4 bytes.

This is how you would do it:

>>> class Attributes(Packet):
...    attributes = Ref(TypeLenValue).repeated(
...                 until=lambda raw, offset, **k: offset >= (len(raw) - 4)
...                 )
...    checksum = Int(4)

>>> s = b'\x01\x02ab\x04\x03abc\xff\xff\xff\xff'
>>> p = Attributes.unpack(s)
>>> for attr in p.attributes:
...     print(attr)
TypeLenValue:
  type: 1
  length: 2
  value: b'ab'
TypeLenValue:
  type: 4
  length: 3
  value: b'abc'

>>> p.checksum
4294967295

>>> p.pack() == s
True

raw is the full raw string to be parsed and offset is the position in the string where the parsing is at the moment.

Optional fields

Final case, what if we want the semantics of zero or one? That’s it we want to have a field or subpacket optional.

We use the when method:

>>> class Option(Packet):
...    type = Int(1)
...    num  = Int(4).when(lambda pkt, **k: pkt.type != 0)

>>> s = b'\x01\x00\x00\x00\x04'
>>> p = Option.unpack(s)
>>> p.num
4

>>> p.pack() == s
True

>>> s = b'\x00'
>>> p = Option.unpack(s)
>>> p.num is None
True

>>> p.pack() == s
True