bytestruct 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bytestruct-0.1.0/.github/workflows/docs.yml +60 -0
- bytestruct-0.1.0/.github/workflows/python-app.yml +73 -0
- bytestruct-0.1.0/.github/workflows/python-publish.yml +29 -0
- bytestruct-0.1.0/.gitignore +13 -0
- bytestruct-0.1.0/.pre-commit-config.yaml +20 -0
- bytestruct-0.1.0/LICENSE +21 -0
- bytestruct-0.1.0/PKG-INFO +127 -0
- bytestruct-0.1.0/README.md +91 -0
- bytestruct-0.1.0/bytestruct/__init__.py +1088 -0
- bytestruct-0.1.0/bytestruct/_version.py +24 -0
- bytestruct-0.1.0/bytestruct/exceptions.py +76 -0
- bytestruct-0.1.0/bytestruct/py.typed +0 -0
- bytestruct-0.1.0/bytestruct.egg-info/PKG-INFO +127 -0
- bytestruct-0.1.0/bytestruct.egg-info/SOURCES.txt +34 -0
- bytestruct-0.1.0/bytestruct.egg-info/dependency_links.txt +1 -0
- bytestruct-0.1.0/bytestruct.egg-info/requires.txt +13 -0
- bytestruct-0.1.0/bytestruct.egg-info/scm_file_list.json +30 -0
- bytestruct-0.1.0/bytestruct.egg-info/scm_version.json +8 -0
- bytestruct-0.1.0/bytestruct.egg-info/top_level.txt +1 -0
- bytestruct-0.1.0/docs/api.md +12 -0
- bytestruct-0.1.0/docs/byte-reader.md +160 -0
- bytestruct-0.1.0/docs/codecs.md +331 -0
- bytestruct-0.1.0/docs/cookbook.md +398 -0
- bytestruct-0.1.0/docs/errors.md +217 -0
- bytestruct-0.1.0/docs/extending.md +581 -0
- bytestruct-0.1.0/docs/getting-started.md +85 -0
- bytestruct-0.1.0/docs/how-it-works.md +227 -0
- bytestruct-0.1.0/docs/index.md +78 -0
- bytestruct-0.1.0/docs/migrating-from-construct.md +208 -0
- bytestruct-0.1.0/docs/records.md +458 -0
- bytestruct-0.1.0/docs/reference.md +666 -0
- bytestruct-0.1.0/mkdocs.yml +117 -0
- bytestruct-0.1.0/pyproject.toml +148 -0
- bytestruct-0.1.0/setup.cfg +4 -0
- bytestruct-0.1.0/tests/test_bytestruct.py +593 -0
- bytestruct-0.1.0/tests/test_docs_examples.py +79 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths:
|
|
7
|
+
- "docs/**"
|
|
8
|
+
- "mkdocs.yml"
|
|
9
|
+
- "bytestruct/**"
|
|
10
|
+
- "pyproject.toml"
|
|
11
|
+
- ".github/workflows/docs.yml"
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- "docs/**"
|
|
15
|
+
- "mkdocs.yml"
|
|
16
|
+
- "bytestruct/**"
|
|
17
|
+
- "pyproject.toml"
|
|
18
|
+
- ".github/workflows/docs.yml"
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
# setuptools_scm derives the version from the tags.
|
|
31
|
+
fetch-depth: 0
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
- name: Install
|
|
36
|
+
run: |
|
|
37
|
+
python -m pip install --upgrade pip
|
|
38
|
+
python -m pip install -e ".[docs]"
|
|
39
|
+
- name: Build (strict)
|
|
40
|
+
run: python -m mkdocs build --strict
|
|
41
|
+
- name: Upload site artifact
|
|
42
|
+
if: github.ref == 'refs/heads/master'
|
|
43
|
+
uses: actions/upload-pages-artifact@v3
|
|
44
|
+
with:
|
|
45
|
+
path: site
|
|
46
|
+
|
|
47
|
+
deploy:
|
|
48
|
+
# Only deploy from master; PRs just run the strict build above as a check.
|
|
49
|
+
if: github.ref == 'refs/heads/master'
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
permissions:
|
|
53
|
+
pages: write
|
|
54
|
+
id-token: write
|
|
55
|
+
environment:
|
|
56
|
+
name: github-pages
|
|
57
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
58
|
+
steps:
|
|
59
|
+
- id: deployment
|
|
60
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Python application
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "master" ]
|
|
8
|
+
release:
|
|
9
|
+
types: [ created ]
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
if: '! github.event.pull_request.draft'
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
|
|
20
|
+
# Pin uv to the interpreter it provisioned instead of an already-installed system Python.
|
|
21
|
+
env:
|
|
22
|
+
UV_PYTHON_PREFERENCE: only-managed
|
|
23
|
+
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
# 3.15 is a prerelease (uv resolves it to the latest beta).
|
|
28
|
+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15" ]
|
|
29
|
+
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
# setuptools_scm derives the version from the tags.
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
# uv provisions the interpreter (python-build-standalone): unlike actions/setup-python,
|
|
37
|
+
# it still provides 3.9 on current runner images (ubuntu-24.04, arm64 macOS).
|
|
38
|
+
- name: Install uv and Python ${{ matrix.python-version }}
|
|
39
|
+
uses: astral-sh/setup-uv@v7
|
|
40
|
+
with:
|
|
41
|
+
python-version: ${{ matrix.python-version }}
|
|
42
|
+
enable-cache: true
|
|
43
|
+
cache-dependency-glob: |
|
|
44
|
+
pyproject.toml
|
|
45
|
+
- name: Run pre-commit hooks
|
|
46
|
+
# pyright needs the project installed; it runs in the dedicated job below.
|
|
47
|
+
env:
|
|
48
|
+
SKIP: pyright
|
|
49
|
+
run: uvx pre-commit run --all-files
|
|
50
|
+
- name: Run tests
|
|
51
|
+
run: uv run --extra test pytest
|
|
52
|
+
|
|
53
|
+
pyright:
|
|
54
|
+
if: '! github.event.pull_request.draft'
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
with:
|
|
59
|
+
fetch-depth: 0
|
|
60
|
+
- name: Install uv
|
|
61
|
+
uses: astral-sh/setup-uv@v7
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.14"
|
|
64
|
+
enable-cache: true
|
|
65
|
+
cache-dependency-glob: |
|
|
66
|
+
pyproject.toml
|
|
67
|
+
- name: Install dependencies
|
|
68
|
+
run: |
|
|
69
|
+
uv venv
|
|
70
|
+
uv pip install -e ".[test]"
|
|
71
|
+
# Runs the same pre-commit hook contributors get locally (pinned in .pre-commit-config.yaml).
|
|
72
|
+
- name: Run pyright
|
|
73
|
+
run: uvx pre-commit run pyright --all-files
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# This workflow builds and uploads the Python package to PyPI when a release is created
|
|
2
|
+
|
|
3
|
+
name: Upload Python Package
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [ created ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
deploy:
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
# setuptools_scm needs full history + tags to derive the release version
|
|
18
|
+
# from the tag; a shallow clone yields a PyPI-rejected local version
|
|
19
|
+
# like 0.1.dev1+g<hash>.
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v7
|
|
23
|
+
- name: Build distributions
|
|
24
|
+
run: uv build
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
27
|
+
with:
|
|
28
|
+
packages-dir: dist
|
|
29
|
+
verbose: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
# Ruff version.
|
|
4
|
+
rev: v0.15.4
|
|
5
|
+
hooks:
|
|
6
|
+
# Run the linter.
|
|
7
|
+
- id: ruff-check
|
|
8
|
+
# Run the formatter.
|
|
9
|
+
- id: ruff-format
|
|
10
|
+
- repo: local
|
|
11
|
+
hooks:
|
|
12
|
+
# Pinned so local hooks and CI agree; bump deliberately together with fixing any new findings.
|
|
13
|
+
# Resolves the project's dependencies from .venv (venvPath/venv in pyproject.toml); CI's test
|
|
14
|
+
# matrix skips this hook (SKIP=pyright) — the dedicated pyright CI job runs it after creating the venv.
|
|
15
|
+
- id: pyright
|
|
16
|
+
name: pyright
|
|
17
|
+
entry: uvx pyright@1.1.411
|
|
18
|
+
language: system
|
|
19
|
+
types: [python]
|
|
20
|
+
pass_filenames: false
|
bytestruct-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 doronz88
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bytestruct
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Declarative binary records and composable codecs, using only the standard library
|
|
5
|
+
Author-email: doronz88 <doron88@gmail.com>
|
|
6
|
+
Maintainer-email: doronz88 <doron88@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/doronz88/bytestruct
|
|
9
|
+
Project-URL: Documentation, https://doronz88.github.io/bytestruct/
|
|
10
|
+
Project-URL: Bug Reports, https://github.com/doronz88/bytestruct/issues
|
|
11
|
+
Keywords: binary,struct,parsing,serialization,dataclasses,construct
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: typing_extensions>=4.1.0; python_version < "3.11"
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs-material>=9.5; python_version >= "3.10" and extra == "docs"
|
|
33
|
+
Requires-Dist: mkdocstrings[python]>=0.26; python_version >= "3.10" and extra == "docs"
|
|
34
|
+
Requires-Dist: mkdocs-llmstxt>=0.5; python_version >= "3.10" and extra == "docs"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# bytestruct
|
|
38
|
+
|
|
39
|
+
[](https://github.com/doronz88/bytestruct/actions/workflows/python-app.yml "Python application action")
|
|
40
|
+
[](https://pypi.org/project/bytestruct/ "PyPi package")
|
|
41
|
+
[](https://doronz88.github.io/bytestruct/ "Documentation")
|
|
42
|
+
|
|
43
|
+
`bytestruct` parses and builds binary wire formats with the standard library alone. Fixed-size records are
|
|
44
|
+
dataclasses whose field annotations give each field's wire type; variable-length data is described by small
|
|
45
|
+
codecs that compose like [construct](https://github.com/construct/construct)'s. Everything is fully typed.
|
|
46
|
+
|
|
47
|
+
The layout of a record is compiled into `struct.Struct` objects once, when the class is defined, so parsing a
|
|
48
|
+
record is a single `unpack_from` per byte-order run.
|
|
49
|
+
|
|
50
|
+
**Documentation: <https://doronz88.github.io/bytestruct/>**, including a
|
|
51
|
+
[migration guide from construct](https://doronz88.github.io/bytestruct/migrating-from-construct/).
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
python3 -m pip install -U bytestruct
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Fixed-size records
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dataclasses import dataclass, field
|
|
63
|
+
from enum import IntEnum
|
|
64
|
+
from typing import Annotated
|
|
65
|
+
|
|
66
|
+
from bytestruct import BinaryStruct, Const, PaddedString, u16, u32
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Kind(IntEnum):
|
|
70
|
+
FILE = 1
|
|
71
|
+
DIRECTORY = 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Header(BinaryStruct, byte_order="<"):
|
|
76
|
+
magic: Annotated[u32, Const(0x1F3D5B79)] = field(default=0x1F3D5B79, init=False)
|
|
77
|
+
size: u32
|
|
78
|
+
name: Annotated[str, PaddedString(16)]
|
|
79
|
+
kind: Annotated[Kind, u16] # parsed into the enum type
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
data = Header(size=1024, name="notes.txt", kind=Kind.FILE).build() # a bad value -> BinaryBuildError
|
|
83
|
+
header = Header.parse(data) # short input -> TruncatedDataError, wrong magic -> UnexpectedConstantError
|
|
84
|
+
assert header.kind is Kind.FILE
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Wire fields are positional `__init__` parameters in wire order. A field declared
|
|
88
|
+
`= field(default=..., init=False)` is left out of `__init__`: parse skips its bytes (padding) and build packs
|
|
89
|
+
its default (on a `Const` field, the constant). Fields without a wire type may follow the wire fields and are
|
|
90
|
+
ignored by parse/build. Records cannot be subclassed.
|
|
91
|
+
|
|
92
|
+
Available field types include `u8`..`u64`, `i16`..`i64`, byte-order pinned `u32le`/`u32be`, `Bytes(n)`,
|
|
93
|
+
`PaddedString(n)`, `uuid_t`, `ipv4`/`ipv6`, `Array(element, n)`, int-enum fields and `Const` magics; custom
|
|
94
|
+
ones subclass `Format`.
|
|
95
|
+
|
|
96
|
+
## Codecs
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from bytestruct import CSTRING, U32_LE, U64_LE, Aligned, Prefixed, Switch
|
|
100
|
+
|
|
101
|
+
STRING = Aligned(4, Prefixed(U32_LE, CSTRING)) # u32 length, NUL-terminated UTF-8, padded to 4 bytes
|
|
102
|
+
OBJECT = Switch(U32_LE, {1: STRING, 2: U64_LE}) # parses to (tag, value)
|
|
103
|
+
|
|
104
|
+
assert OBJECT.parse(OBJECT.build((1, "hi"))) == (1, "hi")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A codec is anything with `read(reader)` and `build(value)`: the combinators (`Prefixed`, `Aligned`,
|
|
108
|
+
`PrefixedArray`, `Tuple`, `Switch`), the leaves (`CSTRING`, `GREEDY_STRING`, `GREEDY_BYTES`, `PASS`), the
|
|
109
|
+
scalars (`U8`, `U16_LE`/`U16_BE`, ..., `F64_BE`), `Bytes(n)`, and record classes themselves. A record field annotated with a codec
|
|
110
|
+
(`name: Annotated[str, CSTRING]`) makes the record variable-length; such fields follow the fixed-size ones.
|
|
111
|
+
Formats with no declarative shape are read by hand with `ByteReader`, whose sub-readers are windows on the same
|
|
112
|
+
buffer (nothing is copied).
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
Every failure raises a `BinaryDataError` (a `ValueError`): `BinaryParseError` (with `TruncatedDataError` and
|
|
117
|
+
`UnexpectedConstantError`) or `BinaryBuildError`. Errors carry the record, field, absolute offset and the path
|
|
118
|
+
through nested codecs, and prefix their message with them:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
Header.magic at offset 0: expected 0x1f3d5b79, got 0x0
|
|
122
|
+
DICTIONARY -> [2] -> STRING at offset 104: expected 200 bytes, found 16
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# bytestruct
|
|
2
|
+
|
|
3
|
+
[](https://github.com/doronz88/bytestruct/actions/workflows/python-app.yml "Python application action")
|
|
4
|
+
[](https://pypi.org/project/bytestruct/ "PyPi package")
|
|
5
|
+
[](https://doronz88.github.io/bytestruct/ "Documentation")
|
|
6
|
+
|
|
7
|
+
`bytestruct` parses and builds binary wire formats with the standard library alone. Fixed-size records are
|
|
8
|
+
dataclasses whose field annotations give each field's wire type; variable-length data is described by small
|
|
9
|
+
codecs that compose like [construct](https://github.com/construct/construct)'s. Everything is fully typed.
|
|
10
|
+
|
|
11
|
+
The layout of a record is compiled into `struct.Struct` objects once, when the class is defined, so parsing a
|
|
12
|
+
record is a single `unpack_from` per byte-order run.
|
|
13
|
+
|
|
14
|
+
**Documentation: <https://doronz88.github.io/bytestruct/>**, including a
|
|
15
|
+
[migration guide from construct](https://doronz88.github.io/bytestruct/migrating-from-construct/).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
python3 -m pip install -U bytestruct
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fixed-size records
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import IntEnum
|
|
28
|
+
from typing import Annotated
|
|
29
|
+
|
|
30
|
+
from bytestruct import BinaryStruct, Const, PaddedString, u16, u32
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Kind(IntEnum):
|
|
34
|
+
FILE = 1
|
|
35
|
+
DIRECTORY = 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Header(BinaryStruct, byte_order="<"):
|
|
40
|
+
magic: Annotated[u32, Const(0x1F3D5B79)] = field(default=0x1F3D5B79, init=False)
|
|
41
|
+
size: u32
|
|
42
|
+
name: Annotated[str, PaddedString(16)]
|
|
43
|
+
kind: Annotated[Kind, u16] # parsed into the enum type
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
data = Header(size=1024, name="notes.txt", kind=Kind.FILE).build() # a bad value -> BinaryBuildError
|
|
47
|
+
header = Header.parse(data) # short input -> TruncatedDataError, wrong magic -> UnexpectedConstantError
|
|
48
|
+
assert header.kind is Kind.FILE
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Wire fields are positional `__init__` parameters in wire order. A field declared
|
|
52
|
+
`= field(default=..., init=False)` is left out of `__init__`: parse skips its bytes (padding) and build packs
|
|
53
|
+
its default (on a `Const` field, the constant). Fields without a wire type may follow the wire fields and are
|
|
54
|
+
ignored by parse/build. Records cannot be subclassed.
|
|
55
|
+
|
|
56
|
+
Available field types include `u8`..`u64`, `i16`..`i64`, byte-order pinned `u32le`/`u32be`, `Bytes(n)`,
|
|
57
|
+
`PaddedString(n)`, `uuid_t`, `ipv4`/`ipv6`, `Array(element, n)`, int-enum fields and `Const` magics; custom
|
|
58
|
+
ones subclass `Format`.
|
|
59
|
+
|
|
60
|
+
## Codecs
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from bytestruct import CSTRING, U32_LE, U64_LE, Aligned, Prefixed, Switch
|
|
64
|
+
|
|
65
|
+
STRING = Aligned(4, Prefixed(U32_LE, CSTRING)) # u32 length, NUL-terminated UTF-8, padded to 4 bytes
|
|
66
|
+
OBJECT = Switch(U32_LE, {1: STRING, 2: U64_LE}) # parses to (tag, value)
|
|
67
|
+
|
|
68
|
+
assert OBJECT.parse(OBJECT.build((1, "hi"))) == (1, "hi")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A codec is anything with `read(reader)` and `build(value)`: the combinators (`Prefixed`, `Aligned`,
|
|
72
|
+
`PrefixedArray`, `Tuple`, `Switch`), the leaves (`CSTRING`, `GREEDY_STRING`, `GREEDY_BYTES`, `PASS`), the
|
|
73
|
+
scalars (`U8`, `U16_LE`/`U16_BE`, ..., `F64_BE`), `Bytes(n)`, and record classes themselves. A record field annotated with a codec
|
|
74
|
+
(`name: Annotated[str, CSTRING]`) makes the record variable-length; such fields follow the fixed-size ones.
|
|
75
|
+
Formats with no declarative shape are read by hand with `ByteReader`, whose sub-readers are windows on the same
|
|
76
|
+
buffer (nothing is copied).
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Every failure raises a `BinaryDataError` (a `ValueError`): `BinaryParseError` (with `TruncatedDataError` and
|
|
81
|
+
`UnexpectedConstantError`) or `BinaryBuildError`. Errors carry the record, field, absolute offset and the path
|
|
82
|
+
through nested codecs, and prefix their message with them:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Header.magic at offset 0: expected 0x1f3d5b79, got 0x0
|
|
86
|
+
DICTIONARY -> [2] -> STRING at offset 104: expected 200 bytes, found 16
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|