Parsing Binary Formats with Construct
construct is a Python library that lets you describe binary file layouts as data structures instead of writing manual struct.unpack calls. It is especially useful for reverse engineering and parsing proprietary formats like game archives, packet captures, and firmware headers.
Core idea
Instead of reading byte-by-byte and unpacking each field, you define a container that describes the layout. construct then reads bytes from the start and maps them onto named fields.
from construct import Struct, Int32ul, Bytes, Padding, Array, this
Header = Struct(
"magic" / Bytes(4),
"entry_count" / Int32ul,
Padding(8),
"entries" / Array(this.entry_count, Struct(
"entry_id" / Int32ul,
"offset" / Int32ul,
)),
)
Common field types
| Type | Meaning |
|---|---|
Int32ul |
4-byte unsigned integer, little-endian |
Int32ub |
4-byte unsigned integer, big-endian |
Bytes(n) |
Raw n bytes, returned as a bytes object |
Padding(n) |
Skip n bytes (often unknown/reserved fields) |
Array(count, subcon) |
Repeat subcon exactly count times |
this.field_name |
Reference another field read earlier |
The ul in Int32ul means unsigned, little-endian. This is the same format as struct.unpack("<I", ...).
Parsing a file
with open("_GRAPHRES.BIN", "rb") as f:
file_data = f.read()
parsed = Header.parse(file_data)
print(parsed.magic) # b'LINK'
print(parsed.entry_count) # 1030
print(parsed.entries[0]) # Container: entry_id=..., offset=...
The result is a Container object that behaves like a Python object with attributes.
Why use construct instead of struct?
- Self-documenting: the format is described declaratively.
- Easier to maintain: adding a field is a single line.
- Better for nested formats: headers, arrays, and sub-structures are straightforward.
- Endian swap is trivial: change
Int32ultoInt32ub. - Validation: you can use
Constto require exact magic bytes.
Limitations
- It reads from the start of the buffer, not searching for patterns.
- The whole structure is parsed into memory; very large files may need
parse_streamor compiled parsers. - Custom logic like size-by-next-offset or padding trimming still happens in Python after parsing.
Example: validating the magic
Use Const to make parsing fail early if the file signature is wrong:
from construct import Const
Header = Struct(
Const(b"LINK"),
"entry_count" / Int32ul,
Padding(8),
"entries" / Array(this.entry_count, Struct(
"entry_id" / Int32ul,
"offset" / Int32ul,
)),
)
Related concepts
- struct for binary packing and unpacking
- ctypes for C-style structure layouts
- Working with Binary Data and Bytes
- Structured Binary Parsing with struct and ctypes