Metadata-Version: 2.4
Name: debin
Version: 0.1.0
Summary: A declarative binary parser for Python, built on dataclasses
Author: maxcabd
Project-URL: Repository, https://github.com/maxcabd/debin
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# debin

A binary parser for Python. You describe a file format as a dataclass with
type annotations, and debin reads a buffer into it.

```python
from typing import List
from debin import *

@debin(magic="HDR ")
class Header:
    version: uint32
    count: uint32
    values: List[uint32] = field(metadata={"count": "count"})

with open("file.bin", "rb") as f:
    buffer = f.read()

header = Header().read_le(buffer)
print(header)
# Header(version=1, count=3, values=[10, 20, 30])
```

## Install

Requires Python 3.10 or newer. No third party dependencies.

### uv

```
uv add git+https://github.com/maxcabd/debin.git
```

Or clone it and install locally:

```
git clone https://github.com/maxcabd/debin.git
cd debin
uv pip install -e .
```

### pip

```
pip install git+https://github.com/maxcabd/debin.git
```

Or clone it and install locally:

```
git clone https://github.com/maxcabd/debin.git
cd debin
pip install -e .
```

## Quick start

A struct is a dataclass decorated with `@debin`. Fields are read in
declaration order.

```python
from debin import *

@debin
class Point:
    x: int32
    y: int32

buffer = bytearray((1).to_bytes(4, "little") + (2).to_bytes(4, "little"))
p = Point().read_le(buffer)
print(p)  # Point(x=1, y=2)
```

Call `.read_le(buffer)` or `.read_be(buffer)` to pick the byte order for that
read. `.read(buffer)` uses whatever endian the struct was declared with
(`@debin(endian="big")`), or little endian if none was given.

Nested structs, lists, and strings work the same way:

```python
from typing import List
from debin import *

@debin
class Entry:
    id: uint16
    name: nullstr

@debin(magic="TBL ")
class Table:
    entry_count: uint32
    entries: List[Entry] = field(metadata={"count": "entry_count"})
```

## Types

| Type | Size | Notes |
| --- | --- | --- |
| `bool` | 1 byte | |
| `uint8`, `int8` | 1 byte | |
| `uint16`, `int16` | 2 bytes | |
| `uint32`, `int32` | 4 bytes | |
| `uint64`, `int64` | 8 bytes | |
| `float16`, `float32`, `float64` | 2, 4, 8 bytes | |
| `nullstr` | variable | ASCII string, reads until a `0x00` byte |
| `List[T]` | variable | any of the above, an enum, or another struct |

An enum works as a field type if it inherits `IntEnum` or `IntFlag` and is
decorated with `@debin(repr=<some integer type>)`, which is the type actually
read from the buffer:

```python
from enum import IntFlag
from debin import *

@debin(repr=uint8)
class Flags(IntFlag):
    READ = 0x1
    WRITE = 0x2
    EXEC = 0x4
```

## Directives

Directives are passed as `field(metadata={...})` on a field. They control how
that field gets read, beyond just its type.

### magic

Struct level, not a field directive. Checks the first bytes of the struct
against a fixed value and raises `MagicError` if they don't match.

```python
@debin(magic="PNG ")
class Header:
    ...
```

`magic` also works on a single field, for a tag that shows up partway through
a struct rather than at the very start. A magic field is fully consumed by
the check, it isn't a prefix glued onto a separately typed value:

```python
@debin
class Entry:
    tag: bytes = field(metadata={"magic": b"ENT1"})
    value: uint32
```

### endian

Struct level (`@debin(endian="big")`) or field level
(`field(metadata={"endian": "big"})`). A field level override wins over
whatever endian the surrounding struct is being read with.

```python
@debin
class Packet:
    length: uint16              # follows the struct's own endian
    checksum: uint16 = field(metadata={"endian": "big"})  # always big endian
```

### count

How many elements to read into a `List[T]` field. Can be a fixed number, the
name of another field, or an expression built with `this` (see below).

```python
@debin
class Blob:
    length: uint32
    data: List[uint8] = field(metadata={"count": "length"})
```

### if

Only parse this field when the condition is true. When it's false the field
is set to `None` and nothing is read from the buffer. Conditions are written
with `this`.

```python
@debin
class Chunk:
    has_extra: uint8
    extra: uint32 = field(metadata={"if": this.has_extra != 0})
```

### assert / pre_assert

Check a condition and raise `ValidationError` if it's false, instead of
silently continuing on a file that doesn't match what you expected. `assert`
runs after the field is parsed, so the condition can reference its own value.
`pre_assert` runs before, so it can only reference earlier fields.

```python
@debin
class Header:
    version: uint8 = field(metadata={"assert": this.version <= 3})

@debin
class Entry:
    kind: uint8
    value: uint32 = field(metadata={"pre_assert": this.kind == 1})
```

### calc

A field that isn't read from the buffer at all. Its value is computed from
other fields once they've already been parsed.

```python
@debin
class Name:
    first: nullstr
    last: nullstr
    full: nullstr = field(metadata={"calc": this.first + " " + this.last})
```

### map

Read the field normally, then run its value through a function before
storing it.

```python
@debin
class Version:
    raw: uint16 = field(metadata={"map": lambda v: (v >> 8, v & 0xFF)})
```

### ignore

Skip this field during normal parsing. Its value is `None` unless something
else sets it, such as `map` on a different field or `calc`.

```python
@debin
class Entry:
    raw_bytes: List[uint8] = field(metadata={"count": 8})
    text: str = field(metadata={
        "ignore": True,
        "map": lambda self: bytes(self.raw_bytes).rstrip(b"\x00").decode(),
    })
```

### pad_before / pad_after

Skip a fixed number of bytes before or after reading the field.

```python
@debin
class Entry:
    id: uint8
    value: uint32 = field(metadata={"pad_before": 3})
```

### pad_size_to

Pad this field up to a fixed total width, whatever it naturally consumed.
Different from `pad_after`, which always adds a fixed number of bytes on top.
Useful for fixed-width slots like a string that's always allotted N bytes
regardless of how long it actually is.

```python
@debin
class Entry:
    name: nullstr = field(metadata={"pad_size_to": 32})
    value: uint32
```

### align_before / align_after

Advance the offset up to the next multiple of N before or after reading the
field.

```python
@debin
class Entry:
    id: uint8
    value: uint32 = field(metadata={"align_before": 4})
```

### seek / seek_before

Jump to a specific position before reading the field, then parse it there.
The position can be an absolute offset, the name of another field, a
`(SeekFrom, offset)` pair, or a function of `(buffer, offset)`.

```python
@debin
class File:
    data_start: uint32
    payload: List[uint8] = field(metadata={"seek": "data_start", "count": 16})
    checksum: uint32 = field(metadata={"seek_before": (SeekFrom.END, -4)})
```

### restore_pos

Parse this field, but leave the offset exactly where it was before, so the
next field starts as if this one had never been read. Handy for looking
ahead at a value before deciding what to do with it.

```python
@debin
class Header:
    a: uint8
    peek_next: uint8 = field(metadata={"restore_pos": True})
    b: uint8  # reads the same byte as peek_next
```

### context

Pass values into a nested struct's field that isn't itself present in the
buffer at that point, only available from the struct doing the nesting. The
receiving field is `ignore`d, and gets set from `context` before any of that
struct's own fields are read.

```python
@debin
class Chunk:
    version: uint16 = field(metadata={"ignore": True})
    fov: float32

@debin
class File:
    version: uint16
    chunk: Chunk = field(metadata={"context": {"version": this.version}})
```

### parse_with

Read a `List[T]` field with a custom function instead of a fixed count.
debin ships three ready to use ones:

- `until_eof` reads structs until the buffer runs out
- `until_with(predicate)` reads structs up to and including the one where
  `predicate` returns true
- `until_exclusive(predicate)` reads structs up to but not including the one
  where `predicate` returns true

```python
from debin.helpers import until_eof

@debin
class Container:
    header: Header
    chunks: List[Chunk] = field(metadata={"parse_with": until_eof})
```

### try

Parse the field, and if that raises any exception, fall back to a default
value instead of failing the whole read. Real files have edge cases and
garbage data; this lets you keep going instead of hard-crashing on one bad
field. Nothing is guaranteed about the offset afterward beyond "unchanged" -
downstream fields may end up misaligned if this actually triggers.

```python
extra: uint32 = field(metadata={"try": 0})
```

### err_context

Wrap any exception raised while parsing this field with an extra message, so
failures inside deeply nested structs are easier to track down.

```python
extra: uint32 = field(metadata={"err_context": "reading Header.extra"})
```

### dbg

Print the offset and value of a field as it's read. Useful while figuring
out a format.

```python
value: uint32 = field(metadata={"dbg": True})
# [offset 0x04] value = 12345
```

## Writing expressions with `this`

`if`, `count`, and `calc` accept expressions built from `this`, which stands
for the struct being parsed. Attribute access, comparisons, and arithmetic
all work directly:

```python
texture_data: List[uint8] = field(metadata={"count": this.header.pitch})

dx10_header: DX10Header = field(metadata={
    "if": this.header.pixel_format.four_cc.apply(bytes) == b"DX10"
})
```

Plain function calls on a `this` expression need `.apply(...)` instead of
wrapping it directly, since `bytes(this.four_cc)` would call `bytes()`
immediately rather than deferring it:

```python
this.four_cc.apply(bytes) == b"DX10"     # correct
bytes(this.four_cc) == b"DX10"           # wrong, evaluates too early
```

## More examples

The `examples/` folder has full, runnable formats: BMP, DDS, WAV, TCP
packets, and a real game asset container (xfbin) that shows nested structs,
lists of structs, and `parse_with` together.
