3.5. Actions#

Actions are a powerful feature that allow you to perform custom operations on the struct’s state during parsing. Instead of directly parsing or storing a field’s value, you can define actions to modify or interact with the data processing at different stages.

There are two types of actions you can define:

  • Pack Actions: These actions are executed before packing data into the struct. They are typically used for operations such as checksum calculation, logging, or any other operation that must occur before serializing the data.

  • Unpack Actions: These actions are executed before unpacking the data from the struct. They are commonly used for validation, verification, or any operation that needs to happen before deserialization.

An action can be as simple as executing a function during packing or unpacking. For example:

@struct
class Format:
    _: Action(pack=lambda ctx: print("Hello, World!"))
    a: uint8

In this case, when the struct is packed, it will print "Hello, World!" to the console. Actions like this can be used for logging, validation, or other side effects that are not tied to the direct data of the struct.

3.5.1. Advanced Usage: Message Digests#

Actions can also be used to perform more complex operations, such as calculating checksums or cryptographic hash values before or after processing fields. For example, you can use an action to automatically wrap a sequence of fields with a specialized message digest like SHA256 (see Digest):

@struct
class Format:
    a: uint8
    with Sha2_256("hash", verify=True):
        user_data: Bytes(50)
    # the 'hash' attribute will be set automatically

In this example, the user_data field is wrapped with the Sha2_256 digest action. When the struct is packed, a SHA256 hash is computed for the user_data and stored in the hash attribute. If the struct is unpacked and the data has been tampered with, the verification step will raise an error.

The resulting struct includes the following fields:

>>> Format.__struct__.fields
[
    Field('key', struct=<ConstBytes>, ...),
    (Action(Digest.begin), None),
    Field('user_data', struct=<Bytes>, ...),
    (Action(Digest.end_pack, Digest.end_unpack), None),
    Field('hash', struct=<Bytes>, ...),
    (UnpackAction(Digest.verfiy), None)
]

Here, you can see that:

  • The Digest.begin and Digest.[end,begin]_pack actions are executed around the user_data field during packing.

  • The hash field is automatically calculated and added to the struct.

  • During unpacking, the Digest.verify action ensures that the hash matches the expected value.