├── .github └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── changelog.txt ├── cstruct ├── __init__.py ├── abstract.py ├── base.py ├── c_expr.py ├── c_parser.py ├── cenum.py ├── cstruct.py ├── exceptions.py ├── field.py ├── mem_cstruct.py └── native_types.py ├── docker └── i386 │ ├── Dockerfile │ └── Makefile ├── docs ├── CODE_OF_CONDUCT.md ├── api │ ├── abstract.md │ ├── base.md │ ├── c_expr.md │ ├── c_parser.md │ ├── cstruct.md │ ├── field.md │ ├── mem_cstruct.md │ ├── module.md │ └── native_types.md ├── changelog.md ├── examples │ ├── dir.md │ ├── fdisk.md │ ├── flexible_array.md │ └── who.md ├── index.md └── license.md ├── examples ├── __init__.py ├── dir.c ├── dir.py ├── dir.sh ├── fdisk.py ├── fdisk.sh ├── flexible_array.py ├── flexible_array.sh ├── who.py └── who.sh ├── mbr ├── mkdocs.yml ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_alignment.py ├── test_c_expr.py ├── test_cenum.py ├── test_cstruct.py ├── test_cstruct_var.py ├── test_define.py ├── test_flexible_array.py ├── test_get_type.py ├── test_memcstruct.py ├── test_native_types.py ├── test_nested.py ├── test_padding.py ├── test_pickle.py ├── test_typdef.py └── test_union.py └── utmp /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | 10 | wait: 11 | name: Wait for tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: fountainhead/action-wait-for-check@v1.0.0 16 | id: wait-for-tests 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | checkName: Tests done 20 | ref: ${{ github.sha }} 21 | timeoutSeconds: 3600 22 | 23 | - name: Fail the Build 24 | uses: cutenode/action-always-fail@v1 25 | if: steps.wait-for-tests.outputs.conclusion != 'success' 26 | 27 | build: 28 | name: Build package 29 | runs-on: ubuntu-latest 30 | needs: [wait] 31 | 32 | strategy: 33 | matrix: 34 | python-version: ['3.12'] 35 | 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | 45 | - name: Cache pip 46 | uses: actions/cache@v3 47 | with: 48 | path: ~/.cache/pip 49 | key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} 50 | restore-keys: | 51 | ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} 52 | ${{ runner.os }}-python-${{ matrix.python }}-pip- 53 | ${{ runner.os }}-python 54 | ${{ runner.os }}- 55 | 56 | - name: Upgrade pip and install dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | python -m pip install --upgrade setuptools wheel 60 | python -m pip install -r requirements.txt 61 | python -m pip install -r requirements-dev.txt 62 | 63 | - name: Build package 64 | run: python -m build 65 | 66 | - name: Check package 67 | run: twine check dist/* 68 | 69 | - name: Publish package 70 | env: 71 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 72 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 73 | run: | 74 | twine upload --skip-existing dist/* 75 | 76 | release: 77 | name: Release version 78 | runs-on: ubuntu-latest 79 | needs: [wait, build] 80 | 81 | steps: 82 | - name: Create release 83 | id: create_release 84 | uses: actions/create-release@v1 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | tag_name: ${{ github.ref }} 89 | release_name: ${{ github.ref }} 90 | draft: false 91 | prerelease: false 92 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: Run tests (Python ${{matrix.python}} 10 | 11 | strategy: 12 | matrix: 13 | python: 14 | - "3.8" 15 | - "3.9" 16 | - "3.10" 17 | - "3.11" 18 | - "3.12" 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python }} 30 | 31 | - name: Cache pip 32 | uses: actions/cache@v3 33 | with: 34 | path: ~/.cache/pip 35 | key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} 36 | restore-keys: | 37 | ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} 38 | ${{ runner.os }}-python-${{ matrix.python }}-pip- 39 | ${{ runner.os }}-python-${{ matrix.python }}- 40 | ${{ runner.os }}-python 41 | ${{ runner.os }}- 42 | 43 | - name: Upgrade pip and install dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | python -m pip install --upgrade setuptools wheel 47 | python -m pip install -r requirements.txt 48 | python -m pip install pytest 49 | 50 | - name: Run tests 51 | run: pytest 52 | 53 | all_done: 54 | name: Tests done 55 | runs-on: ubuntu-latest 56 | needs: [tests] 57 | 58 | steps: 59 | - name: All done 60 | run: echo 1 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | .eggs/ 38 | /share 39 | /include 40 | .mypy_cache 41 | pyvenv.cfg 42 | site/ 43 | __pycache__ 44 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | version: 2 3 | 4 | # Set the OS and Python version 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.12" 9 | 10 | mkdocs: 11 | configuration: mkdocs.yml 12 | 13 | # Declare the Python requirements required to build the documentation 14 | python: 15 | install: 16 | - requirements: requirements-dev.txt 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | andrea.bonomi@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2022 Andrea Bonomi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -e 2 | 3 | help: 4 | @echo - make black ------ Format code 5 | @echo - make isort ------ Sort imports 6 | @echo - make clean ------ Clean virtual environment 7 | @echo - make coverage --- Run tests coverage 8 | @echo - make docs ------- Make docs 9 | @echo - make lint ------- Run lint 10 | @echo - make test ------- Run test 11 | @echo - make test-32bit - Run test on 32bit architecture 12 | @echo - make typecheck -- Typecheck 13 | @echo - make venv ------- Create virtual environment 14 | 15 | .PHONY: isort 16 | codespell: 17 | @codespell -w cstruct tests examples setup.py 18 | 19 | .PHONY: isort 20 | isort: 21 | @isort --profile black cstruct tests examples setup.py 22 | 23 | .PHONY: black 24 | black: isort 25 | @black -S cstruct tests examples setup.py 26 | 27 | clean: 28 | -rm -rf build dist 29 | -rm -rf *.egg-info 30 | -rm -rf bin lib lib64 share include pyvenv.cfg 31 | 32 | coverage: 33 | @pytest --cov --cov-report=term-missing 34 | 35 | .PHONY: docs 36 | docs: 37 | @mkdocs build 38 | @mkdocs gh-deploy 39 | 40 | lint: 41 | flake8 cstruct tests 42 | 43 | test: 44 | pytest 45 | 46 | test-32bit: 47 | @make -C docker/i386 test 48 | 49 | typecheck: 50 | mypy --strict --no-warn-unused-ignores cstruct 51 | 52 | venv: 53 | python3 -m venv . || python3 -m virtualenv . 54 | . bin/activate; pip install -Ur requirements.txt 55 | . bin/activate; pip install -Ur requirements-dev.txt 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python-CStruct 2 | ============== 3 | 4 | C-style structs for Python 5 | 6 | [![Build Status](https://github.com/andreax79/python-cstruct/workflows/Tests/badge.svg)](https://github.com/andreax79/python-cstruct/actions) 7 | [![PyPI version](https://badge.fury.io/py/cstruct.svg)](https://badge.fury.io/py/cstruct) 8 | [![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct) 9 | [![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) 10 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 11 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 12 | [![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct) 13 | [![Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest)](https://python-cstruct.readthedocs.io/en/latest/) 14 | 15 | Convert C struct/union definitions into Python classes with methods for 16 | serializing/deserializing. 17 | 18 | The usage is very simple: create a class subclassing 19 | [`cstruct.MemCStruct`](https://python-cstruct.readthedocs.io/en/latest/api/mem_cstruct/) 20 | and add a C struct/union definition as a string in the `__def__` field. 21 | 22 | The C struct/union definition is parsed at runtime and the struct format string 23 | is generated. The class offers the method `unpack` for deserializing 24 | an array of bytes into a Python object and the method `pack` for 25 | serializing the values into an array of bytes. 26 | 27 | Install 28 | ------- 29 | 30 | ``` 31 | pip install cstruct 32 | ``` 33 | 34 | Examples 35 | -------- 36 | 37 | * [Read the DOS-type (MBR) partition table](https://python-cstruct.readthedocs.io/en/latest/examples/fdisk/) 38 | * [Print information about logged uses](https://python-cstruct.readthedocs.io/en/latest/examples/who/) 39 | * [Flexible Array Member (FAM)](https://python-cstruct.readthedocs.io/en/latest/examples/flexible_array/) 40 | * [libc integration (using ctypes)](https://python-cstruct.readthedocs.io/en/latest/examples/dir/) 41 | 42 | 43 | Features 44 | -------- 45 | 46 | ### Structs 47 | 48 | Struct definition subclassing `cstruct.MemCStruct`. Methods can access stuct values as instance variables. 49 | 50 | ```python 51 | class Position(cstruct.MemCStruct): 52 | __def__ = """ 53 | struct { 54 | unsigned char head; 55 | unsigned char sector; 56 | unsigned char cyl; 57 | } 58 | """ 59 | @property 60 | def lba(self): 61 | return (self.cyl * 16 + self.head) * 63 + (self.sector - 1) 62 | 63 | pos = Position(cyl=15, head=15, sector=63) 64 | print(f"head: {pos.head} sector: {pos.sector} cyl: {pos.cyl} lba: {pos.lba}") 65 | ``` 66 | 67 | Struct definition using `cstruct.parse`. 68 | 69 | ```python 70 | Partition = cstruct.parse(""" 71 | #define ACTIVE_FLAG 0x80 72 | 73 | struct Partition { 74 | unsigned char status; /* 0x80 - active */ 75 | struct Position start; 76 | unsigned char partition_type; 77 | struct Position end; 78 | unsigned int start_sect; /* starting sector counting from 0 */ 79 | unsigned int sectors; /* nr of sectors in partition */ 80 | } 81 | """) 82 | 83 | part = Partition() 84 | part.status = cstruct.getdef('ACTIVE_FLAG') 85 | ``` 86 | 87 | ### Unions 88 | 89 | Union definition subclassing `cstruct.MemCStruct`. 90 | 91 | ```python 92 | class Data(cstruct.MemCStruct): 93 | __def__ = """ 94 | union { 95 | int integer; 96 | float real; 97 | } 98 | """ 99 | 100 | data = Data() 101 | data.integer = 2 102 | data.real = 3 103 | assert data.integer != 2 104 | ``` 105 | 106 | ### Enums 107 | 108 | Enum definition subclassing `cstruct.CEnum`. 109 | 110 | ```python 111 | class HtmlFont(cstruct.CEnum): 112 | __size__ = 2 113 | __def__ = """ 114 | #define NONE 0 115 | 116 | enum htmlfont { 117 | HTMLFONT_NONE = NONE, 118 | HTMLFONT_BOLD, 119 | HTMLFONT_ITALIC 120 | } 121 | """ 122 | 123 | assert HtmlFont.HTMLFONT_NONE == 0 124 | assert HtmlFont.HTMLFONT_BOLD == 1 125 | assert HtmlFont.HTMLFONT_ITALIC == 2 126 | ``` 127 | 128 | Different enum styles are supported in struct/union definitions. 129 | 130 | ```c 131 | enum Type_A a; // externally defined using CEnum 132 | enum Type_B {A, B, C} b; 133 | enum {A, B, C} c; 134 | enum Type_D : short {A, B, C} d; // specify the underlying type 135 | enum Direction { left = 'l', right = 'r' }; 136 | ``` 137 | 138 | ### Nested structs/unions 139 | 140 | Nested stucts and unions are supported, both named and anonymous. 141 | 142 | ```python 143 | class Packet(cstruct.MemCStruct): 144 | __def__ = """ 145 | struct Packet { 146 | uint8_t packetLength; 147 | union { 148 | struct { 149 | uint16_t field1; 150 | uint16_t field2; 151 | uint16_t field3; 152 | } format1; 153 | struct { 154 | double value1; 155 | double value2; 156 | } format2; 157 | }; 158 | }; 159 | """ 160 | ``` 161 | 162 | ### Byte Order, Size, and Padding 163 | 164 | Python-CStruct supports big-endian, little-endian, and native byte orders, which you can specify using: 165 | 166 | * `cstruct.LITTLE_ENDIAN` - Little endian byte order, standard size, no padding. 167 | * `cstruct.BIG_ENDIAN` - Big endian byte order, standard size, no padding. 168 | * `cstruct.NATIVE_ORDER` - Native byte order, native size, padding. Native byte order is big-endian or little-endian, depending on the host system. 169 | 170 | Standard size depends only on the format character while native size depends on the host system. 171 | For native order, padding is automatically added to align the structure members. 172 | 173 | For more information, see the [struct - Byte Order, Size, and Padding](https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment) section. 174 | 175 | ```python 176 | class Native(cstruct.MemCStruct): 177 | __byte_order__ = cstruct.NATIVE_ORDER 178 | __def__ = """ 179 | struct { 180 | long p; 181 | char c; 182 | long x; 183 | } 184 | """ 185 | ``` 186 | 187 | ### Flexible Array Member 188 | 189 | ```python 190 | class Pkg(cstruct.MemCStruct): 191 | __byte_order__ = cstruct.LITTLE_ENDIAN 192 | __def__ = """ 193 | struct { 194 | uint16_t cmd; 195 | uint16_t length; 196 | uint8_t data[]; 197 | } 198 | """ 199 | 200 | pkg = Pkg() 201 | pkg.length = 4 202 | pkg.data = [10, 20, 30, 40] 203 | ``` 204 | 205 | ### Python object attributes 206 | 207 | In struct definition, you can access Python object attributes using `self`. 208 | The value of expression accessing class attributes is evaluated at runtime. 209 | 210 | ```python 211 | class RT11DirectoryEntry(cstruct.CStruct): 212 | 213 | __byte_order__ = cstruct.LITTLE_ENDIAN 214 | __def__ = """ 215 | struct RT11DirectoryEntry { 216 | uint8_t type; 217 | uint8_t clazz; 218 | uint16_t raw_filename1; 219 | uint16_t raw_filename2; 220 | uint16_t raw_extension; 221 | uint16_t length; 222 | uint8_t job; 223 | uint8_t channel; 224 | uint16_t raw_creation_date; 225 | uint16_t extra_bytes[self.extra_bytes_len]; /* The size of the array is determined at runtime */ 226 | }; 227 | """ 228 | 229 | extra_bytes_len: int = 0 230 | ``` 231 | 232 | ### Pack and Unpack 233 | 234 | A code example illustrating how to use 235 | [`pack`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.pack) to pack a structure into binary form. 236 | 237 | ```python 238 | class Position(cstruct.MemCStruct): 239 | __byte_order__ = cstruct.LITTLE_ENDIAN 240 | __def__ = """ 241 | struct { 242 | unsigned char head; 243 | unsigned char sector; 244 | unsigned char cyl; 245 | } 246 | """ 247 | 248 | pos = Position(head=10, sector=20, cyl=3) 249 | packed = pos.pack() 250 | ``` 251 | 252 | Binary representation can be converted into structure using 253 | [`unpack`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.unpack). 254 | 255 | ``` 256 | pos1 = Position() 257 | pos1.unpack(packed) 258 | assert pos1.head == 10 259 | assert pos1.sector == 20 260 | assert pos1.cyl == 3 261 | ``` 262 | 263 | ### Define, Sizeof, and Eval 264 | 265 | Definitions in Struct declaration: 266 | 267 | ```python 268 | class Packet(cstruct.MemCStruct): 269 | __byte_order__ = cstruct.LITTLE_ENDIAN 270 | __def__ = """ 271 | #define MaxPacket 20 272 | 273 | struct Packet { 274 | uint8_t bytes[MaxPacket]; 275 | } 276 | """ 277 | ``` 278 | 279 | Parse C definitions: 280 | 281 | ```python 282 | cstruct.parse(""" 283 | #define A1 10 284 | #define A2 10 + A1 285 | #define A3 30 286 | """) 287 | assert cstruct.getdef("A1") == 10 288 | assert cstruct.getdef('A2') == 20 289 | ``` 290 | 291 | Get structure size: 292 | 293 | ```python 294 | cstruct.sizeof(Partition) 295 | ``` 296 | 297 | Evaluate C expression using [`c_eval`](https://python-cstruct.readthedocs.io/en/latest/api/c_expr/): 298 | 299 | ```python 300 | cstruct.c_eval("A1 / 10") 301 | cstruct.c_eval("((A10 < 6) || (A10>10))") 302 | ``` 303 | 304 | C expressions are automatically evaluated during structure definitions: 305 | 306 | ```python 307 | class MBR(cstruct.MemCStruct): 308 | __byte_order__ = cstruct.LITTLE_ENDIAN 309 | __def__ = """ 310 | #define MBR_SIZE 512 311 | #define MBR_DISK_SIGNATURE_SIZE 4 312 | #define MBR_USUALY_NULLS_SIZE 2 313 | #define MBR_SIGNATURE_SIZE 2 314 | #define MBR_BOOT_SIGNATURE 0xaa55 315 | #define MBR_PARTITIONS_NUM 4 316 | #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) 317 | #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) 318 | 319 | struct { 320 | char unused[MBR_UNUSED_SIZE]; 321 | unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; 322 | unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; 323 | struct Partition partitions[MBR_PARTITIONS_NUM]; 324 | uint16 signature; 325 | } 326 | """ 327 | ``` 328 | 329 | ### Accessing Field Definitions 330 | 331 | Python-CStruct provides the `__fields_types__` attribute at the class level, which allows you to inspect the metadata of each field. 332 | The dictionary contains each field's name as a key and its metadata as a value. 333 | Each field has the following attributes: 334 | 335 | | Attribute | Description | 336 | |------------------|------------| 337 | | `kind` | Indicates whether the field is a primitive type or a nested struct. | 338 | | `c_type` | The corresponding C type. | 339 | | `ref` | If the field is a nested struct, this contains a reference to the class representing that struct. | 340 | | `vlen_ex` | The number of elements in the field. | 341 | | `flexible_array` | Indicates whether the field is a flexible array. | 342 | | `byte_order` | The byte order of the field. | 343 | | `offset` | The relative position of the field within the struct. | 344 | | `padding` | The number of bytes added before this field for alignment. | 345 | 346 | 347 | ### Ispect memory 348 | 349 | The [`inspect`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.inspect) methods displays memory contents in hexadecimal. 350 | 351 | ```python 352 | print(mbr.inspect()) 353 | ``` 354 | 355 | Output example: 356 | ``` 357 | 00000000 eb 48 90 00 00 00 00 00 00 00 00 00 00 00 00 00 |.H..............| 358 | 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 359 | 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 360 | 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 02 |................| 361 | 00000040 ff 00 00 80 61 cb 04 00 00 08 fa 80 ca 80 ea 53 |....a..........S| 362 | 00000050 7c 00 00 31 c0 8e d8 8e d0 bc 00 20 fb a0 40 7c ||..1....... ..@|| 363 | 00000060 3c ff 74 02 88 c2 52 be 79 7d e8 34 01 f6 c2 80 |<.t...R.y}.4....| 364 | 00000070 74 54 b4 41 bb aa 55 cd 13 5a 52 72 49 81 fb 55 |tT.A..U..ZRrI..U| 365 | 00000080 aa 75 43 a0 41 7c 84 c0 75 05 83 e1 01 74 37 66 |.uC.A|..u....t7f| 366 | 00000090 8b 4c 10 be 05 7c c6 44 ff 01 66 8b 1e 44 7c c7 |.L...|.D..f..D|.| 367 | 000000a0 04 10 00 c7 44 02 01 00 66 89 5c 08 c7 44 06 00 |....D...f.\..D..| 368 | 000000b0 70 66 31 c0 89 44 04 66 89 44 0c b4 42 cd 13 72 |pf1..D.f.D..B..r| 369 | 000000c0 05 bb 00 70 eb 7d b4 08 cd 13 73 0a f6 c2 80 0f |...p.}....s.....| 370 | 000000d0 84 f0 00 e9 8d 00 be 05 7c c6 44 ff 00 66 31 c0 |........|.D..f1.| 371 | 000000e0 88 f0 40 66 89 44 04 31 d2 88 ca c1 e2 02 88 e8 |..@f.D.1........| 372 | 000000f0 88 f4 40 89 44 08 31 c0 88 d0 c0 e8 02 66 89 04 |..@.D.1......f..| 373 | 00000100 66 a1 44 7c 66 31 d2 66 f7 34 88 54 0a 66 31 d2 |f.D|f1.f.4.T.f1.| 374 | 00000110 66 f7 74 04 88 54 0b 89 44 0c 3b 44 08 7d 3c 8a |f.t..T..D.;D.}<.| 375 | 00000120 54 0d c0 e2 06 8a 4c 0a fe c1 08 d1 8a 6c 0c 5a |T.....L......l.Z| 376 | 00000130 8a 74 0b bb 00 70 8e c3 31 db b8 01 02 cd 13 72 |.t...p..1......r| 377 | 00000140 2a 8c c3 8e 06 48 7c 60 1e b9 00 01 8e db 31 f6 |*....H|`......1.| 378 | 00000150 31 ff fc f3 a5 1f 61 ff 26 42 7c be 7f 7d e8 40 |1.....a.&B|..}.@| 379 | 00000160 00 eb 0e be 84 7d e8 38 00 eb 06 be 8e 7d e8 30 |.....}.8.....}.0| 380 | 00000170 00 be 93 7d e8 2a 00 eb fe 47 52 55 42 20 00 47 |...}.*...GRUB .G| 381 | 00000180 65 6f 6d 00 48 61 72 64 20 44 69 73 6b 00 52 65 |eom.Hard Disk.Re| 382 | 00000190 61 64 00 20 45 72 72 6f 72 00 bb 01 00 b4 0e cd |ad. Error.......| 383 | 000001a0 10 ac 3c 00 75 f4 c3 00 00 00 00 00 00 00 00 00 |..<.u...........| 384 | 000001b0 00 00 00 00 00 00 00 00 40 e2 01 00 00 00 80 00 |........@.......| 385 | 000001c0 02 00 83 fe 3f 86 01 00 00 00 c6 17 21 00 00 00 |....?.......!...| 386 | 000001d0 01 87 8e fe ff ff c7 17 21 00 4d d3 de 00 00 00 |........!.M.....| 387 | 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 388 | 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| 389 | ``` 390 | 391 | Links 392 | ----- 393 | * [C/C++ reference](https://en.cppreference.com/) 394 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0] - 2013-08-19 4 | 5 | ### Added 6 | 7 | - initial version 8 | 9 | ## [1.2] - 2017-05-18 10 | 11 | ### Improved 12 | 13 | - initialize all values to 0 by default 14 | - new data types 15 | 16 | ### Added 17 | 18 | - who.py example 19 | - a changelog :) 20 | 21 | ## [1.3] - 2017-05-21 22 | 23 | ### Fix 24 | 25 | - default value fix 26 | 27 | ## [1.4] - 2017-06-02 28 | 29 | ### Fix 30 | 31 | - default value fix 32 | 33 | ## [1.5] - 2017-07-22 34 | 35 | ### Fix 36 | 37 | - compatibiliy fix 38 | 39 | ## [1.6] - 2017-12-12 40 | 41 | ### Fix 42 | 43 | - fixed size of 64-bit integers, they now have 64 bits, not 32 44 | 45 | ## [1.7] - 2018-03-14 46 | 47 | ### Improved 48 | 49 | - add support for // comments 50 | 51 | ## [1.8] - 2018-10-30 52 | 53 | ### Improved 54 | 55 | - add *_t types 56 | 57 | ### Fix 58 | 59 | - fix Python 2.5 support in main module 60 | - examples fix 61 | 62 | ## [1.9] - 2019-07-09 63 | 64 | ### Improved 65 | 66 | - drop Python < 2.6 support 67 | 68 | ### Added 69 | 70 | - flexible array parsing 71 | - union initial support 72 | 73 | ## [2.0] - 2020-04-11 74 | 75 | ### Improved 76 | 77 | - drop Python 2 support 78 | 79 | ## [2.1] - 2020-10-09 80 | 81 | ### Improved 82 | 83 | - refactoring 84 | - Python 3.9 support 85 | - Github workfows 86 | 87 | ## [2.2] - 2022-08-23 88 | 89 | ### Fix 90 | 91 | - Fix empty MemCStruct size 92 | 93 | ### Improved 94 | 95 | - Python 3.10 support 96 | - pytest 97 | - black code style 98 | 99 | ## [2.3] - 2022-09-01 100 | 101 | ### Fix 102 | 103 | - Fix compare with None 104 | 105 | ## [3.0] - 2022-09-05 106 | 107 | ### Added 108 | 109 | - Flexible array support 110 | 111 | ## [3.1] - 2022-09-13 112 | 113 | ### Added 114 | 115 | - Make CStruct/MemCStruct Pickle Friendly 116 | 117 | ## [3.2] - 2022-10-23 118 | 119 | ### Fix 120 | 121 | - Fix padding tests on 32bit architectures 122 | 123 | ## [3.3] - 2022-10-24 124 | 125 | ### Added 126 | 127 | - Add 32bit test environment 128 | 129 | ### Fix 130 | 131 | - Fix padding tests on 32bit architectures 132 | 133 | ## [4.0] - 2022-11-01 134 | 135 | ### Added 136 | 137 | - Add support for nameless inline struct 138 | 139 | ### Improved 140 | 141 | - Python 3.11 support 142 | 143 | ## [5.0] - 2022-11-12 144 | 145 | ### Added 146 | 147 | - Add support for enums 148 | - Add support for multiple definition to cstruct.parse 149 | - Add inspect method 150 | 151 | ### Improved 152 | 153 | - Documentation and examples 154 | - Restructure setup 155 | 156 | ## [5.1] - 2022-11-20 157 | 158 | ### Improved 159 | 160 | - Support unpack from ctype pointers 161 | 162 | ### Added 163 | 164 | - Add support for char constants 165 | - Add native type test 166 | - dir.py example 167 | 168 | ## [5.2] - 2022-11-23 169 | 170 | ### Fix 171 | 172 | - nested struct unpack fix 173 | - nested anonymous union offset fix 174 | - inspect offset for nested struct/union fix 175 | 176 | ## [5.3] - 2024-01-08 177 | 178 | ### Fix 179 | 180 | - fix struct in struct array parsing 181 | 182 | ### Improved 183 | 184 | - Python 3.12 support 185 | 186 | ## [6.0] - 2025-01-16 187 | 188 | ### Added 189 | 190 | - access to Python class attributes in struct definition 191 | 192 | ### Improved 193 | 194 | - Python 3.13 support 195 | 196 | ## [6.1] - 2025-03-21 197 | 198 | ### Fix 199 | 200 | - fix CStruct.pack() padding 201 | -------------------------------------------------------------------------------- /cstruct/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | __author__ = "Andrea Bonomi " 26 | __license__ = "MIT" 27 | __version__ = "6.1" 28 | __date__ = "15 August 2013" 29 | 30 | from typing import Any, Dict, Optional, Type, Union 31 | 32 | from .abstract import AbstractCEnum, AbstractCStruct, CStructMeta 33 | from .base import ( 34 | BIG_ENDIAN, 35 | CHAR_ZERO, 36 | DEFINES, 37 | ENUMS, 38 | LITTLE_ENDIAN, 39 | NATIVE_ORDER, 40 | STRUCTS, 41 | TYPEDEFS, 42 | ) 43 | from .c_parser import parse_struct_def 44 | from .cenum import CEnum 45 | from .cstruct import CStruct 46 | from .mem_cstruct import MemCStruct 47 | from .native_types import get_native_type 48 | 49 | __all__ = [ 50 | "LITTLE_ENDIAN", 51 | "BIG_ENDIAN", 52 | "NATIVE_ORDER", 53 | "CHAR_ZERO", 54 | "CStruct", 55 | "MemCStruct", 56 | "CEnum", 57 | "define", 58 | "undef", 59 | "getdef", 60 | "typedef", 61 | "get_type", 62 | "sizeof", 63 | "parse", 64 | ] 65 | 66 | 67 | def define(key: str, value: Any) -> None: 68 | """ 69 | Define a constant that can be used in the C struct 70 | 71 | Examples: 72 | >>> define("INIT_THREAD_SIZE", 16384) 73 | 74 | Args: 75 | key: identifier 76 | value: value of the constant 77 | """ 78 | DEFINES[key] = value 79 | 80 | 81 | def undef(key: str) -> None: 82 | """ 83 | Undefine a symbol that was previously defined with define 84 | 85 | Examples: 86 | >>> define("INIT_THREAD_SIZE", 16384) 87 | >>> undef("INIT_THREAD_SIZE") 88 | 89 | Args: 90 | key: identifier 91 | 92 | Raises: 93 | KeyError: If key is not defined 94 | """ 95 | del DEFINES[key] 96 | 97 | 98 | def getdef(key: str) -> Any: 99 | """ 100 | Return the value for a constant 101 | 102 | Examples: 103 | >>> define("INIT_THREAD_SIZE", 16384) 104 | >>> getdef("INIT_THREAD_SIZE") 105 | 106 | Args: 107 | key: identifier 108 | 109 | Raises: 110 | KeyError: If key is not defined 111 | """ 112 | return DEFINES[key] 113 | 114 | 115 | def typedef(type_: str, alias: str) -> None: 116 | """ 117 | Define an alias name for a data type 118 | 119 | Examples: 120 | >>> typedef("int", "status") 121 | >>> sizeof("status") 122 | 4 123 | 124 | Args: 125 | type_: data type 126 | alias: new alias name 127 | """ 128 | TYPEDEFS[alias] = type_ 129 | 130 | 131 | def get_type(type_: str) -> Any: 132 | """ 133 | Get a data type (struct, union, enum) by name 134 | 135 | Examples: 136 | >>> get_type("struct Position") 137 | 138 | >>> get_type("enum htmlfont") 139 | 140 | 141 | Args: 142 | type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ'), enum or native type 143 | 144 | Returns: 145 | class: data type class 146 | 147 | Raises: 148 | KeyError: If type is not defined 149 | """ 150 | while type_ in TYPEDEFS: 151 | type_ = TYPEDEFS[type_] 152 | if isinstance(type_, CStructMeta): 153 | return type_ 154 | elif type_.startswith("struct ") or type_.startswith("union "): 155 | kind, type_ = type_.split(" ", 1) 156 | try: 157 | return STRUCTS[type_] 158 | except KeyError: 159 | raise KeyError(f"Unknown {kind} `{type_}`") 160 | elif type_.startswith("enum "): 161 | kind, type_ = type_.split(" ", 1) 162 | try: 163 | return ENUMS[type_] 164 | except KeyError: 165 | raise KeyError(f"Unknown {kind} `{type_}`") 166 | else: 167 | return get_native_type(type_) 168 | 169 | 170 | def sizeof(type_: str) -> int: 171 | """ 172 | Return the size of the type. 173 | 174 | Examples: 175 | >>> sizeof("struct Position") 176 | 16 177 | >>> sizeof('enum htmlfont') 178 | 4 179 | >>> sizeof("int") 180 | 4 181 | 182 | Args: 183 | type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ'), enum or native type 184 | 185 | Returns: 186 | size: size in bytes 187 | 188 | Raises: 189 | KeyError: If type is not defined 190 | """ 191 | while type_ in TYPEDEFS: 192 | type_ = TYPEDEFS[type_] 193 | data_type = get_type(type_) 194 | return data_type.sizeof() 195 | 196 | 197 | def parse( 198 | __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, **kargs: Dict[str, Any] 199 | ) -> Union[Type[AbstractCStruct], Type[AbstractCEnum], None]: 200 | """ 201 | Return a new class mapping a C struct/union/enum definition. 202 | If the string does not contains any definition, return None. 203 | If the string contains multiple struct/union/enum definitions, returns the last definition. 204 | 205 | Examples: 206 | >>> cstruct.parse('struct Pair { unsigned char a; unsigned char b; };') 207 | 208 | 209 | Args: 210 | __struct__ (str): definition of the struct (or union/enum) in C syntax 211 | __cls__ (type): super class - CStruct(default) or MemCStruct 212 | __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER 213 | 214 | Returns: 215 | cls: __cls__ subclass 216 | 217 | Raises: 218 | cstruct.exceptions.ParserError: Parsing exception 219 | 220 | """ 221 | if __cls__ is None: 222 | __cls__ = MemCStruct 223 | cls_def = parse_struct_def(__struct__, __cls__=__cls__, process_muliple_definition=True, **kargs) 224 | if cls_def is None: 225 | return None 226 | return cls_def["__cls__"].parse(cls_def, **kargs) 227 | -------------------------------------------------------------------------------- /cstruct/abstract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import hashlib 26 | import struct 27 | from abc import ABCMeta 28 | from collections import OrderedDict 29 | from enum import EnumMeta, IntEnum, _EnumDict 30 | from io import StringIO 31 | from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Type, Union 32 | 33 | from .base import DEFAULT_ENUM_SIZE, ENUM_SIZE_TO_C_TYPE, ENUMS, STRUCTS 34 | from .c_parser import Tokens, parse_enum, parse_enum_def, parse_struct, parse_struct_def 35 | from .exceptions import CStructException, ParserError 36 | from .field import FieldType, calculate_padding 37 | from .native_types import get_native_type 38 | 39 | __all__ = ["CStructMeta", "AbstractCStruct", "CEnumMeta", "AbstractCEnum"] 40 | 41 | 42 | class CStructMeta(ABCMeta): 43 | __size__: int = 0 44 | 45 | def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: 46 | __struct__ = namespace.get("__struct__", None) 47 | namespace["__cls__"] = bases[0] if bases else None 48 | # Parse the struct 49 | if "__struct__" in namespace: 50 | if isinstance(namespace["__struct__"], (str, Tokens)): 51 | namespace.update(parse_struct(**namespace)) 52 | __struct__ = True 53 | if "__def__" in namespace: 54 | namespace.update(parse_struct_def(**namespace)) 55 | __struct__ = True 56 | # Create the new class 57 | new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) 58 | # Register the class 59 | if __struct__ is not None and not namespace.get("__anonymous__"): 60 | STRUCTS[name] = new_class 61 | return new_class 62 | 63 | def __len__(cls) -> int: 64 | "Structure size (in bytes)" 65 | return cls.__size__ 66 | 67 | @property 68 | def size(cls) -> int: 69 | "Structure size (in bytes)" 70 | return cls.__size__ 71 | 72 | 73 | class AbstractCStruct(metaclass=CStructMeta): 74 | """ 75 | Abstract C struct to Python class 76 | """ 77 | 78 | __size__: int = 0 79 | " Size in bytes " 80 | __fields__: List[str] = [] 81 | " Struct/union fields " 82 | __fields_types__: Dict[str, FieldType] 83 | " Dictionary mapping field names to types " 84 | __byte_order__: Optional[str] = None 85 | " Byte order " 86 | __alignment__: int = 0 87 | " Alignment " 88 | __is_union__: bool = False 89 | " True if the class is an union, False if it is a struct " 90 | 91 | def __init__( 92 | self, buffer: Optional[Union[bytes, BinaryIO]] = None, flexible_array_length: Optional[int] = None, **kargs: Dict[str, Any] 93 | ) -> None: 94 | self.set_flexible_array_length(flexible_array_length) 95 | self.__fields__ = [x for x in self.__fields__] # Create a copy 96 | self.__fields_types__ = OrderedDict({k: v.copy() for k, v in self.__fields_types__.items()}) # Create a copy 97 | if buffer is not None: 98 | self.unpack(buffer) 99 | else: 100 | try: 101 | self.unpack(buffer) 102 | except Exception: 103 | pass 104 | for key, value in kargs.items(): 105 | setattr(self, key, value) 106 | 107 | @classmethod 108 | def parse( 109 | cls, 110 | __struct__: Union[str, Tokens, Dict[str, Any]], 111 | __name__: Optional[str] = None, 112 | __byte_order__: Optional[str] = None, 113 | __is_union__: Optional[bool] = False, 114 | **kargs: Dict[str, Any], 115 | ) -> Type["AbstractCStruct"]: 116 | """ 117 | Return a new class mapping a C struct/union definition. 118 | 119 | Args: 120 | __struct__: definition of the struct (or union) in C syntax 121 | __name__: name of the new class. If empty, a name based on the __struct__ hash is generated 122 | __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER 123 | __is_union__: True for union, False for struct 124 | 125 | Returns: 126 | cls: a new class mapping the definition 127 | """ 128 | cls_kargs: Dict[str, Any] = dict(kargs) 129 | if __byte_order__ is not None: 130 | cls_kargs["__byte_order__"] = __byte_order__ 131 | if __is_union__ is not None: 132 | cls_kargs["__is_union__"] = __is_union__ 133 | cls_kargs["__struct__"] = __struct__ 134 | if isinstance(__struct__, (str, Tokens)): 135 | del cls_kargs["__struct__"] 136 | cls_kargs.update(parse_struct_def(__struct__, __cls__=cls, **cls_kargs)) 137 | cls_kargs["__struct__"] = None 138 | elif isinstance(__struct__, dict): 139 | del cls_kargs["__struct__"] 140 | cls_kargs.update(__struct__) 141 | cls_kargs["__struct__"] = None 142 | __name__ = cls_kargs.get("__name__") or __name__ 143 | if __name__ is None: # Anonymous struct 144 | __name__ = cls.__name__ + "_" + hashlib.sha1(str(__struct__).encode("utf-8")).hexdigest() 145 | cls_kargs["__anonymous__"] = True 146 | cls_kargs["__name__"] = __name__ 147 | return type(__name__, (cls,), cls_kargs) 148 | 149 | def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: 150 | """ 151 | Set flexible array length (i.e. number of elements) 152 | 153 | Args: 154 | flexible_array_length: flexible array length 155 | 156 | Raises: 157 | CStructException: If flexible array is not present in the structure 158 | """ 159 | if flexible_array_length is not None: 160 | # Search for the flexible array 161 | flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] 162 | if flexible_array is None: 163 | raise CStructException("Flexible array not found in struct") 164 | flexible_array.vlen_ex = flexible_array_length 165 | 166 | def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: 167 | """ 168 | Unpack bytes containing packed C structure data 169 | 170 | Args: 171 | buffer: bytes or binary stream to be unpacked 172 | flexible_array_length: flexible array length 173 | """ 174 | self.set_flexible_array_length(flexible_array_length) 175 | if hasattr(buffer, "read"): 176 | buffer = buffer.read(self.size) # type: ignore 177 | if not buffer: 178 | return False 179 | return self.unpack_from(buffer) 180 | 181 | def unpack_from( 182 | self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None 183 | ) -> bool: # pragma: no cover 184 | """ 185 | Unpack bytes containing packed C structure data 186 | 187 | Args: 188 | buffer: bytes to be unpacked 189 | offset: optional buffer offset 190 | flexible_array_length: flexible array length 191 | """ 192 | raise NotImplementedError 193 | 194 | def pack(self) -> bytes: # pragma: no cover 195 | """ 196 | Pack the structure data into bytes 197 | 198 | Returns: 199 | bytes: The packed structure 200 | """ 201 | raise NotImplementedError 202 | 203 | def pack_into(self, buffer: bytearray, offset: int = 0) -> None: 204 | """ 205 | Pack the structure data into a buffer 206 | 207 | Args: 208 | buffer: target buffer (must be large enough to contain the packed structure) 209 | offset: optional buffer offset 210 | """ 211 | tmp = self.pack() 212 | buffer[offset : offset + len(tmp)] = tmp 213 | 214 | def clear(self) -> None: 215 | self.unpack(None) 216 | 217 | def __len__(self) -> int: 218 | "Actual structure size (in bytes)" 219 | return self.size 220 | 221 | @property 222 | def size(self) -> int: 223 | "Actual structure size (in bytes)" 224 | if not self.__fields_types__: # no fields 225 | return 0 226 | elif self.__is_union__: # C union 227 | # Calculate the sizeof union as size of its largest element 228 | return max(x.vsize for x in self.__fields_types__.values()) 229 | else: # C struct 230 | # Calculate the sizeof struct as last item's offset + size + padding 231 | last_field_type = list(self.__fields_types__.values())[-1] 232 | size = last_field_type.offset + last_field_type.vsize 233 | padding = calculate_padding(self.__byte_order__, self.__alignment__, size) 234 | return size + padding 235 | 236 | @classmethod 237 | def sizeof(cls) -> int: 238 | "Structure size in bytes (flexible array member size is omitted)" 239 | return cls.__size__ 240 | 241 | def inspect(self, start_addr: Optional[int] = None, end_addr: Optional[int] = None) -> str: 242 | """ 243 | Return memory content in hexadecimal 244 | 245 | Args: 246 | start_addr: start address 247 | end_addr: end address 248 | """ 249 | buffer = StringIO() 250 | if hasattr(self, "__mem__"): 251 | mem = self.__mem__[self.__base__ :] 252 | else: 253 | mem = self.pack() 254 | if end_addr is None: 255 | end_addr = self.size 256 | for i in range(start_addr or 0, end_addr, 16): 257 | row = mem[i : min(i + 16, end_addr)] 258 | buffer.write(f"{i:08x} ") 259 | for j, c in enumerate(row): 260 | separator = " " if j == 7 else "" 261 | buffer.write(f" {c:02x}{separator}") 262 | for j in range(len(row) - 1, 15): 263 | separator = " " if j == 7 else "" 264 | buffer.write(f" {separator}") 265 | buffer.write(" |") 266 | for c in row: 267 | buffer.write(chr(c) if c >= 32 and c < 127 else ".") 268 | for j in range(len(row) - 1, 15): 269 | buffer.write(" ") 270 | buffer.write("|") 271 | buffer.write("\n") 272 | buffer.seek(0, 0) 273 | return buffer.read() 274 | 275 | def __eq__(self, other: Any) -> bool: 276 | return other is not None and isinstance(other, self.__class__) and self.__dict__ == other.__dict__ 277 | 278 | def __ne__(self, other: Any) -> bool: 279 | return not self.__eq__(other) 280 | 281 | def __str__(self) -> str: 282 | result = [] 283 | for field in self.__fields__: 284 | result.append(field + "=" + str(getattr(self, field, None))) 285 | return type(self).__name__ + "(" + ", ".join(result) + ")" 286 | 287 | def __repr__(self) -> str: # pragma: no cover 288 | return self.__str__() 289 | 290 | def __getstate__(self) -> bytes: 291 | """ 292 | This method is called and the returned object is pickled 293 | as the contents for the instance, instead of the contents of 294 | the instance’s dictionary 295 | 296 | Returns: 297 | bytes: The packed structure 298 | """ 299 | return self.pack() 300 | 301 | def __setstate__(self, state: bytes) -> bool: 302 | """ 303 | This method it is called with the unpickled state 304 | 305 | Args: 306 | state: bytes to be unpacked 307 | """ 308 | return self.unpack(state) 309 | 310 | 311 | class CEnumMeta(EnumMeta): 312 | __size__: int 313 | __native_format__: str 314 | 315 | class WrapperDict(_EnumDict): 316 | def __setitem__(self, key: str, value: Any) -> None: 317 | env = None 318 | if key == "__enum__": 319 | env = parse_enum(value) 320 | elif key == "__def__": 321 | env = parse_enum_def(value) 322 | 323 | if env is not None: 324 | # register the enum constants in the object namespace, 325 | # using the Python Enum class Namespace dict that does the 326 | # heavy lifting 327 | for k, v in env["__constants__"].items(): 328 | super().__setitem__(k, v) 329 | else: 330 | return super().__setitem__(key, value) 331 | 332 | @classmethod 333 | def __prepare__(metacls, cls, bases, **kwds): 334 | namespace = EnumMeta.__prepare__(cls, bases, **kwds) 335 | namespace.__class__ = metacls.WrapperDict 336 | return namespace 337 | 338 | def __new__(metacls: Type["CEnumMeta"], cls: str, bases: Tuple[Type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": 339 | inst = super().__new__(metacls, cls, bases, classdict, **kwds) 340 | if len(inst) > 0: 341 | if classdict.get("__native_format__"): # data type specified 342 | inst.__size__ = struct.calcsize(classdict["__native_format__"]) 343 | elif "__size__" in classdict: # size specified 344 | try: 345 | inst.__native_format__ = get_native_type(ENUM_SIZE_TO_C_TYPE[inst.__size__]).native_format 346 | except KeyError: 347 | raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") 348 | else: # default 349 | inst.__size__ = DEFAULT_ENUM_SIZE 350 | inst.__native_format__ = get_native_type(ENUM_SIZE_TO_C_TYPE[inst.__size__]).native_format 351 | print(f"Warning: __size__ not specified for enum {cls}. Will default to {DEFAULT_ENUM_SIZE} bytes") 352 | 353 | if not classdict.get("__anonymous__", False): 354 | ENUMS[cls] = inst 355 | return inst 356 | 357 | @property 358 | def size(cls) -> int: 359 | "Enum size (in bytes)" 360 | return cls.__size__ 361 | 362 | 363 | class AbstractCEnum(IntEnum, metaclass=CEnumMeta): 364 | """ 365 | Abstract C enum to Python class 366 | """ 367 | 368 | @classmethod 369 | def parse( 370 | cls, 371 | __enum__: Union[str, Tokens, Dict[str, Any]], 372 | __name__: Optional[str] = None, 373 | __size__: Optional[int] = None, 374 | __native_format__: Optional[str] = None, 375 | **kargs: Dict[str, Any], 376 | ) -> Type["AbstractCEnum"]: 377 | """ 378 | Return a new Python Enum class mapping a C enum definition 379 | 380 | Args: 381 | __enum__: Definition of the enum in C syntax 382 | __name__: Name of the new Enum. If empty, a name based on the __enum__ hash is generated 383 | __size__: Number of bytes that the enum should be read as 384 | __native_format__: struct module format 385 | 386 | Returns: 387 | cls: A new class mapping the definition 388 | """ 389 | cls_kargs: Dict[str, Any] = dict(kargs) 390 | if __size__ is not None: 391 | cls_kargs["__size__"] = __size__ 392 | if __native_format__ is not None: 393 | cls_kargs["__native_format__"] = __native_format__ 394 | 395 | if isinstance(__enum__, (str, Tokens)): 396 | cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) 397 | elif isinstance(__enum__, dict): 398 | cls_kargs.update(__enum__) 399 | 400 | __name__ = cls_kargs.get("__name__") or __name__ 401 | if __name__ is None: 402 | __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() 403 | cls_kargs["__anonymous__"] = True 404 | 405 | cls_kargs.update(cls_kargs["__constants__"]) 406 | return cls(__name__, cls_kargs) 407 | -------------------------------------------------------------------------------- /cstruct/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | from typing import TYPE_CHECKING, Any, Dict, Type 26 | 27 | if TYPE_CHECKING: 28 | from .abstract import AbstractCEnum, AbstractCStruct 29 | 30 | __all__ = [ 31 | "LITTLE_ENDIAN", 32 | "BIG_ENDIAN", 33 | "NATIVE_ORDER", 34 | "CHAR_ZERO", 35 | "STRUCTS", 36 | "DEFINES", 37 | "TYPEDEFS", 38 | "CHAR_ZERO", 39 | "DEFAULT_ENUM_SIZE", 40 | ] 41 | 42 | LITTLE_ENDIAN = "<" 43 | "Little-endian, std. size & alignment" 44 | BIG_ENDIAN = ">" 45 | "Big-endian, std. size & alignment" 46 | NATIVE_ORDER = "@" 47 | "Native order, size & alignment" 48 | 49 | STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} 50 | 51 | ENUMS: Dict[str, Type["AbstractCEnum"]] = {} 52 | 53 | DEFINES: Dict[str, Any] = {} 54 | 55 | TYPEDEFS: Dict[str, str] = { 56 | "short int": "short", 57 | "unsigned short int": "unsigned short", 58 | "ushort": "unsigned short", 59 | "long int": "long", 60 | "unsigned long int": "unsigned long", 61 | "int8_t": "int8", 62 | "uint8_t": "uint8", 63 | "int16_t": "int16", 64 | "uint16_t": "uint16", 65 | "int32_t": "int32", 66 | "uint32_t": "uint32", 67 | "int64_t": "int64", 68 | "uint64_t": "uint64", 69 | } 70 | 71 | ENUM_SIZE_TO_C_TYPE: Dict[int, str] = {1: "int8", 2: "int16", 4: "int32", 8: "int64"} 72 | 73 | CHAR_ZERO = bytes("\0", "ascii") 74 | 75 | DEFAULT_ENUM_SIZE = 4 76 | -------------------------------------------------------------------------------- /cstruct/c_expr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import ast 26 | import inspect 27 | import operator 28 | from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union 29 | 30 | from .base import DEFINES, STRUCTS 31 | from .exceptions import ContextNotFound, EvalError 32 | 33 | if TYPE_CHECKING: 34 | from .abstract import AbstractCStruct 35 | 36 | __all__ = ["c_eval"] 37 | 38 | 39 | def c_eval(expr: str) -> Union[int, float]: 40 | """ 41 | Evaluate a C arithmetic/logic expression and return the result 42 | 43 | Examples: 44 | >>> c_eval('10 + (5 / 3)') 45 | 11 46 | >>> c_eval('!0') 47 | 1 48 | >>> c_eval('sizeof(x)') 49 | 128 50 | 51 | Args: 52 | expr: C expression 53 | 54 | Returns: 55 | result: the expression evaluation result 56 | 57 | Raises: 58 | EvalError: expression evaluation error 59 | """ 60 | try: 61 | expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") 62 | return eval_node(ast.parse(expr.strip()).body[0]) 63 | except EvalError: 64 | raise 65 | except Exception: 66 | raise EvalError 67 | 68 | 69 | def eval_attribute_node(node: ast.Attribute) -> Union[int, float]: 70 | """ 71 | Evaluate node attribute, e.g. 'self.x' 72 | Only 'self' is allowed. The attribute must be a number. 73 | 74 | Args: 75 | node: attribute node 76 | 77 | Returns: 78 | result: the attribute value 79 | 80 | Raises: 81 | EvalError: expression result is not a number, or not self attribute 82 | ContextNotFound: context is not defined 83 | """ 84 | if not node.value or node.value.id != "self": # type: ignore 85 | raise EvalError("only self is allowed") 86 | context = get_cstruct_context() 87 | if context is None: 88 | raise ContextNotFound("context is not defined") 89 | result = getattr(context, node.attr) 90 | if not isinstance(result, (int, float)): 91 | raise EvalError("expression result is not a number") 92 | return result 93 | 94 | 95 | def eval_node(node: ast.stmt) -> Union[int, float]: 96 | if isinstance(node, ast.Attribute): 97 | return eval_attribute_node(node) 98 | 99 | handler = OPS[type(node)] 100 | result = handler(node) 101 | if isinstance(result, bool): # convert bool to int 102 | return 1 if result else 0 103 | elif isinstance(result, str): # convert char to int 104 | if len(result) != 1: 105 | raise EvalError("Multi-character constant") 106 | else: 107 | return ord(result) 108 | return result 109 | 110 | 111 | def eval_get(node) -> Union[int, float, Type["AbstractCStruct"]]: 112 | "Get definition/struct by name" 113 | try: 114 | return DEFINES[node.id] 115 | except KeyError: 116 | return STRUCTS[node.id] 117 | 118 | 119 | def eval_compare(node) -> bool: 120 | "Evaluate a compare node" 121 | right = eval_node(node.left) 122 | for operation, comp in zip(node.ops, node.comparators): 123 | left = right 124 | right = eval_node(comp) 125 | if not OPS[type(operation)](left, right): 126 | return False 127 | return True 128 | 129 | 130 | def eval_div(node) -> Union[int, float]: 131 | "Evaluate div node (integer/float)" 132 | left = eval_node(node.left) 133 | right = eval_node(node.right) 134 | if isinstance(left, float) or isinstance(right, float): 135 | return operator.truediv(left, right) 136 | else: 137 | return operator.floordiv(left, right) 138 | 139 | 140 | def eval_call(node) -> Union[int, float]: 141 | from . import sizeof 142 | 143 | if node.func.id == "sizeof": 144 | args = [eval_node(x) for x in node.args] 145 | return sizeof(*args) 146 | raise KeyError(node.func.id) 147 | 148 | 149 | def get_cstruct_context() -> Optional["AbstractCStruct"]: 150 | """ 151 | Get the calling CStruct instance from the stack (if any) 152 | """ 153 | from .abstract import AbstractCStruct 154 | 155 | stack = inspect.stack() 156 | for frame in stack: 157 | caller_self = frame.frame.f_locals.get("self") 158 | if isinstance(caller_self, AbstractCStruct): 159 | return caller_self 160 | return None 161 | 162 | 163 | try: 164 | Constant = ast.Constant 165 | except AttributeError: # python < 3.8 166 | Constant = ast.NameConstant 167 | 168 | OPS: Dict[Type[ast.AST], Callable[[Any], Any]] = { 169 | ast.Expr: lambda node: eval_node(node.value), 170 | ast.Num: lambda node: node.n, 171 | ast.Name: eval_get, 172 | ast.Call: eval_call, 173 | Constant: lambda node: node.value, 174 | ast.Str: lambda node: node.s, # python < 3.8 175 | # and/or 176 | ast.BoolOp: lambda node: OPS[type(node.op)](node), # and/or operator 177 | ast.And: lambda node: all(eval_node(x) for x in node.values), # && operator 178 | ast.Or: lambda node: any(eval_node(x) for x in node.values), # || operator 179 | # binary 180 | ast.BinOp: lambda node: OPS[type(node.op)](node), # binary operators 181 | ast.Add: lambda node: operator.add(eval_node(node.left), eval_node(node.right)), # + operator 182 | ast.Sub: lambda node: operator.sub(eval_node(node.left), eval_node(node.right)), # - operator 183 | ast.Mult: lambda node: operator.mul(eval_node(node.left), eval_node(node.right)), # * operator 184 | ast.Div: eval_div, 185 | ast.Mod: lambda node: operator.mod(eval_node(node.left), eval_node(node.right)), # % operator 186 | ast.LShift: lambda node: operator.lshift(eval_node(node.left), eval_node(node.right)), # << operator 187 | ast.RShift: lambda node: operator.rshift(eval_node(node.left), eval_node(node.right)), # >> operator 188 | ast.BitOr: lambda node: operator.or_(eval_node(node.left), eval_node(node.right)), # | operator 189 | ast.BitXor: lambda node: operator.xor(eval_node(node.left), eval_node(node.right)), # ^ operator 190 | ast.BitAnd: lambda node: operator.and_(eval_node(node.left), eval_node(node.right)), # & operator 191 | # unary 192 | ast.UnaryOp: lambda node: OPS[type(node.op)](node), # unary operators 193 | ast.UAdd: lambda node: operator.pos(eval_node(node.operand)), # unary + operator 194 | ast.USub: lambda node: operator.neg(eval_node(node.operand)), # unary - operator 195 | ast.Not: lambda node: operator.not_(eval_node(node.operand)), # ! operator 196 | ast.Invert: lambda node: operator.invert(eval_node(node.operand)), # ~ operator 197 | # Compare 198 | ast.Compare: eval_compare, 199 | ast.Eq: operator.eq, 200 | ast.NotEq: operator.ne, 201 | ast.Gt: operator.gt, 202 | ast.Lt: operator.lt, 203 | ast.GtE: operator.ge, 204 | ast.LtE: operator.le, 205 | } 206 | -------------------------------------------------------------------------------- /cstruct/c_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import re 26 | from collections import OrderedDict 27 | from typing import ( 28 | TYPE_CHECKING, 29 | Any, 30 | Callable, 31 | Dict, 32 | List, 33 | Optional, 34 | Tuple, 35 | Type, 36 | Union, 37 | ) 38 | 39 | from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS 40 | from .c_expr import c_eval 41 | from .exceptions import CStructException, EvalError, ParserError 42 | from .field import FieldType, Kind, calculate_padding 43 | from .native_types import get_native_type 44 | 45 | if TYPE_CHECKING: 46 | from .abstract import AbstractCEnum, AbstractCStruct 47 | 48 | __all__ = ["parse_struct", "parse_struct_def", "parse_enum_def", "Tokens"] 49 | 50 | SEPARATORS = [" ", "\t", "\n", ";", "{", "}", ":", ",", "="] 51 | SPACES = [" ", "\t", "\n"] 52 | 53 | 54 | class Tokens: 55 | def __init__(self, text: str) -> None: 56 | # remove the comments 57 | text = re.sub(r"//.*?$|/\*.*?\*/", "", text, flags=re.S | re.MULTILINE) 58 | # c preprocessor 59 | lines = [] 60 | for line in text.split("\n"): 61 | if re.match(r"^\s*#define", line): 62 | try: 63 | _, name, value = line.strip().split(maxsplit=2) 64 | DEFINES[name] = c_eval(value) 65 | except Exception: 66 | raise ParserError(f"Parsing line `{line}`") 67 | else: 68 | lines.append(line) 69 | text = "\n".join(lines) 70 | self.tokens = self.tokenize(text) 71 | 72 | def tokenize(self, text: str) -> List[str]: 73 | tokens: List[str] = [] 74 | t: List[str] = [] 75 | for c in text: 76 | if c in SEPARATORS: 77 | if t: 78 | tokens.append("".join(t)) 79 | t.clear() 80 | if c not in SPACES: 81 | tokens.append(c) 82 | else: 83 | t.append(c) 84 | if t: 85 | tokens.append("".join(t)) 86 | return tokens 87 | 88 | def pop(self) -> str: 89 | return self.tokens.pop(0) 90 | 91 | def pop_c_type(self) -> str: 92 | c_type = self.pop() 93 | if c_type in ["signed", "unsigned"] and len(self) > 1: 94 | # short int, long int, or long long 95 | c_type = c_type + " " + self.pop() 96 | elif c_type in ["short", "long"] and len(self) > 1 and self.get() in ["int", "long"]: 97 | # short int, long int, or long long 98 | c_type = c_type + " " + self.pop() 99 | return c_type 100 | 101 | def get(self) -> str: 102 | return self.tokens[0] 103 | 104 | def push(self, value: str) -> None: 105 | return self.tokens.insert(0, value) 106 | 107 | def __len__(self) -> int: 108 | return len(self.tokens) 109 | 110 | def __str__(self) -> str: 111 | return str(self.tokens) 112 | 113 | 114 | def parse_length(tokens: Tokens, next_token: str, flexible_array: bool) -> Tuple[str, Union[int, Callable[[], int]], bool]: 115 | # Extract t_vlen 116 | t = next_token.split("[") 117 | if len(t) != 2: 118 | raise ParserError(f"Error parsing: `{next_token}`") 119 | next_token = t[0].strip() 120 | vlen_part = t[1] 121 | vlen_expr = [] 122 | while not vlen_part.endswith("]"): 123 | vlen_expr.append(vlen_part.split("]")[0].strip()) 124 | vlen_part = tokens.pop() 125 | t_vlen = vlen_part.split("]")[0].strip() 126 | vlen_expr.append(vlen_part.split("]")[0].strip()) 127 | t_vlen = " ".join(vlen_expr) 128 | # Evaluate t_vlen 129 | vlen: Union[int, Callable[[], int]] 130 | if not t_vlen: 131 | # If the length expression is empty, this is a flex array 132 | flexible_array = True 133 | vlen = 0 134 | else: 135 | # Evaluate the length expression 136 | # If the length expression is not a constant, it is evaluated at runtime 137 | try: 138 | vlen = int(c_eval(t_vlen)) 139 | except EvalError: 140 | vlen = lambda: int(c_eval(t_vlen)) 141 | return next_token, vlen, flexible_array 142 | 143 | 144 | def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Optional[str], offset: int) -> "FieldType": 145 | if len(tokens) < 2: 146 | raise ParserError("Parsing error") 147 | c_type = tokens.pop() 148 | # signed/unsigned/struct 149 | if c_type in ["signed", "unsigned", "struct", "union", "enum"] and len(tokens) > 1: 150 | c_type = c_type + " " + tokens.pop() 151 | 152 | vlen: Union[int, Callable[[], int]] = 1 153 | flexible_array = False 154 | 155 | if not c_type.endswith("{"): 156 | next_token = tokens.pop() 157 | # short int, long int, or long long 158 | if next_token in ["int", "long"]: 159 | c_type = c_type + " " + next_token 160 | next_token = tokens.pop() 161 | # void * 162 | if next_token.startswith("*"): 163 | next_token = next_token[1:] 164 | c_type = "void *" 165 | # parse length 166 | if "[" in next_token: 167 | next_token, vlen, flexible_array = parse_length(tokens, next_token, flexible_array) 168 | tokens.push(next_token) 169 | # resolve typedefs 170 | while c_type in TYPEDEFS: 171 | c_type = TYPEDEFS[c_type] 172 | 173 | # calculate fmt 174 | ref: Union[None, Type[AbstractCEnum], Type[AbstractCStruct]] 175 | if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union 176 | c_type, tail = c_type.split(" ", 1) 177 | kind = Kind.STRUCT if c_type == "struct" else Kind.UNION 178 | if tokens.get() == "{": # Named nested struct 179 | tokens.push(tail) 180 | tokens.push(c_type) 181 | ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) 182 | elif tail == "{": # Unnamed nested struct 183 | tokens.push(tail) 184 | tokens.push(c_type) 185 | ref = __cls__.parse(tokens, __byte_order__=byte_order) 186 | else: 187 | try: 188 | ref = STRUCTS[tail] 189 | except KeyError: 190 | raise ParserError(f"Unknown `{c_type} {tail}`") 191 | elif c_type.startswith("enum"): 192 | from .cenum import CEnum 193 | 194 | c_type, tail = c_type.split(" ", 1) 195 | kind = Kind.ENUM 196 | if tokens.get() == "{": # Named nested struct 197 | tokens.push(tail) 198 | tokens.push(c_type) 199 | ref = CEnum.parse(tokens, __name__=tail) 200 | elif tail == "{": # unnamed nested struct 201 | tokens.push(tail) 202 | tokens.push(c_type) 203 | ref = CEnum.parse(tokens) 204 | else: 205 | try: 206 | ref = ENUMS[tail] 207 | except KeyError: 208 | raise ParserError(f"Unknown `{c_type} {tail}`") 209 | else: # other types 210 | kind = Kind.NATIVE 211 | ref = None 212 | return FieldType(kind, c_type, ref, vlen, flexible_array, byte_order, offset) 213 | 214 | 215 | def parse_typedef(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Optional[str]) -> None: 216 | field_type = parse_type(tokens, __cls__, byte_order, 0) 217 | vname = tokens.pop() 218 | if field_type.ref is None: 219 | TYPEDEFS[vname] = field_type.c_type 220 | elif field_type.ref.__is_enum__: 221 | TYPEDEFS[vname] = f"enum {field_type.ref.__name__}" 222 | elif field_type.ref.__is_union__: 223 | TYPEDEFS[vname] = f"union {field_type.ref.__name__}" 224 | else: 225 | TYPEDEFS[vname] = f"struct {field_type.ref.__name__}" 226 | t = tokens.pop() 227 | if t != ";": 228 | raise ParserError(f"`;` expected but `{t}` found") 229 | 230 | 231 | def parse_struct_def( 232 | __def__: Union[str, Tokens], 233 | __cls__: Type["AbstractCStruct"], 234 | __byte_order__: Optional[str] = None, 235 | process_muliple_definition: bool = False, 236 | **kargs: Any, # Type['AbstractCStruct'], 237 | ) -> Optional[Dict[str, Any]]: 238 | # naive C struct parsing 239 | if isinstance(__def__, Tokens): 240 | tokens = __def__ 241 | else: 242 | tokens = Tokens(__def__) 243 | result = None 244 | while tokens and (process_muliple_definition or not result): 245 | kind = tokens.pop() 246 | if kind == ";": 247 | pass 248 | 249 | elif kind == "typedef": 250 | if result: 251 | result["__cls__"].parse(result, **kargs) 252 | parse_typedef(tokens, __cls__, __byte_order__) 253 | 254 | elif kind == "enum": 255 | if result: 256 | result["__cls__"].parse(result, **kargs) 257 | name = tokens.pop() 258 | native_format = None 259 | if tokens.get() == ":": # enumeration type declaration 260 | tokens.pop() # pop ":" 261 | type_ = get_native_type(tokens.pop_c_type()) 262 | native_format = type_.native_format 263 | if tokens.get() == "{": # named enum 264 | tokens.pop() # pop "{" 265 | result = parse_enum(tokens, __name__=name, native_format=native_format) 266 | elif name == "{": # unnamed enum 267 | result = parse_enum(tokens, native_format=native_format) 268 | else: 269 | raise ParserError(f"`{name}` definition expected") 270 | 271 | elif kind in ["struct", "union"]: 272 | if result: 273 | result["__cls__"].parse(result, **kargs) 274 | __is_union__ = kind == "union" 275 | name = tokens.pop() 276 | if name == "{": # unnamed nested struct 277 | result = parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) 278 | elif tokens.get() == "{": # Named nested struct 279 | tokens.pop() # pop "{" 280 | result = parse_struct( 281 | tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__, __name__=name 282 | ) 283 | else: 284 | raise ParserError(f"`{name}` definition expected") 285 | 286 | else: 287 | raise ParserError(f"struct, union, or enum expected - `{kind}` found") 288 | return result 289 | 290 | 291 | def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: 292 | # naive C enum parsing 293 | if isinstance(__def__, Tokens): 294 | tokens = __def__ 295 | else: 296 | tokens = Tokens(__def__) 297 | if not tokens: 298 | return None 299 | kind = tokens.pop() 300 | if kind not in ["enum"]: 301 | raise ParserError(f"enum expected - `{kind}` found") 302 | 303 | name = tokens.pop() 304 | native_format = None 305 | if tokens.get() == ":": # enumeration type declaration 306 | tokens.pop() # pop ":" 307 | type_ = get_native_type(tokens.pop_c_type()) 308 | native_format = type_.native_format 309 | if tokens.get() == "{": # named enum 310 | tokens.pop() # pop "{" 311 | return parse_enum(tokens, __name__=name, native_format=native_format) 312 | elif name == "{": # unnamed enum 313 | return parse_enum(tokens) 314 | else: 315 | raise ParserError(f"`{name}` definition expected") 316 | 317 | 318 | def parse_enum( 319 | __enum__: Union[str, Tokens], 320 | __name__: Optional[str] = None, 321 | native_format: Optional[str] = None, 322 | **kargs: Any, 323 | ) -> Optional[Dict[str, Any]]: 324 | """ 325 | Parser for C-like enum syntax. 326 | 327 | Args: 328 | __enum__: definition of the enum in C syntax 329 | __name__: enum name 330 | native_format: struct module format 331 | 332 | Returns: 333 | dict: the parsed definition 334 | """ 335 | from .cenum import CEnum 336 | 337 | constants: Dict[str, int] = OrderedDict() 338 | 339 | if isinstance(__enum__, Tokens): 340 | tokens = __enum__ 341 | else: 342 | tokens = Tokens(__enum__) 343 | 344 | while len(tokens): 345 | if tokens.get() == "}": 346 | tokens.pop() 347 | break 348 | 349 | name = tokens.pop() 350 | next_token = tokens.pop() 351 | if next_token in {",", "}"}: # enum-constant without explicit value 352 | if len(constants) == 0: 353 | value = 0 354 | else: 355 | value = next(reversed(constants.values())) + 1 356 | elif next_token == "=": # enum-constant with explicit value 357 | exp_elems = [] 358 | next_token = tokens.pop() 359 | while next_token not in {",", "}"}: 360 | exp_elems.append(next_token) 361 | if len(tokens) > 0: 362 | next_token = tokens.pop() 363 | else: 364 | break 365 | 366 | if len(exp_elems) == 0: 367 | raise ParserError("enum is missing value expression") 368 | 369 | int_expr = " ".join(exp_elems) 370 | try: 371 | value = c_eval(int_expr) 372 | except (ValueError, TypeError): 373 | value = int(int_expr) 374 | else: 375 | raise ParserError(f"`{__enum__}` is not a valid enum expression") 376 | 377 | if name in constants: 378 | raise ParserError(f"duplicate enum name `{name}`") 379 | constants[name] = value 380 | 381 | if next_token == "}": 382 | break 383 | 384 | result = { 385 | "__constants__": constants, 386 | "__is_struct__": False, 387 | "__is_union__": False, 388 | "__is_enum__": True, 389 | "__name__": __name__, 390 | "__native_format__": native_format, 391 | "__cls__": CEnum, 392 | } 393 | return result 394 | 395 | 396 | def parse_struct( 397 | __struct__: Union[str, Tokens], 398 | __cls__: Type["AbstractCStruct"], 399 | __is_union__: bool = False, 400 | __byte_order__: Optional[str] = None, 401 | __name__: Optional[str] = None, 402 | **kargs: Any, 403 | ) -> Dict[str, Any]: 404 | """ 405 | Parser for C-like struct syntax. 406 | 407 | Args: 408 | __struct__: definition of the struct/union in C syntax 409 | __cls__: base class (MemCStruct or CStruct) 410 | __is_union__: True for union, False for struct 411 | __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER 412 | __name__: struct/union name 413 | 414 | Returns: 415 | dict: the parsed definition 416 | """ 417 | # naive C struct parsing 418 | from .abstract import AbstractCStruct 419 | from .mem_cstruct import MemCStruct 420 | 421 | if __cls__ is None or __cls__ == AbstractCStruct: 422 | __cls__ = MemCStruct 423 | __is_union__ = bool(__is_union__) 424 | fields_types: Dict[str, FieldType] = OrderedDict() 425 | flexible_array: bool = False 426 | offset: int = 0 427 | max_alignment: int = 0 428 | anonymous: int = 0 429 | if isinstance(__struct__, Tokens): 430 | tokens = __struct__ 431 | else: 432 | tokens = Tokens(__struct__) 433 | while len(tokens): 434 | if tokens.get() == "}": 435 | tokens.pop() 436 | break 437 | # flexible array member must be the last member of such a struct 438 | if flexible_array: 439 | raise CStructException("Flexible array member must be the last member of such a struct") 440 | field_type = parse_type(tokens, __cls__, __byte_order__, offset) 441 | vname = tokens.pop() 442 | if vname in fields_types: 443 | raise ParserError(f"Duplicate member `{vname}`") 444 | if vname in dir(__cls__): 445 | raise ParserError(f"Invalid reserved member name `{vname}`") 446 | # parse length 447 | if "[" in vname: 448 | vname, field_type.vlen_ex, field_type.flexible_array = parse_length(tokens, vname, flexible_array) 449 | flexible_array = flexible_array or field_type.flexible_array 450 | # anonymous nested union 451 | if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): 452 | # add the anonymous struct fields to the parent 453 | for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): 454 | if nested_field_name in fields_types: 455 | raise ParserError(f"Duplicate member `{nested_field_name}`") 456 | # set the correct offset 457 | nested_field_type = nested_field_type.copy() 458 | nested_field_type.base_offset = offset + nested_field_type.base_offset 459 | nested_field_type.offset = offset + nested_field_type.offset 460 | fields_types[nested_field_name] = nested_field_type 461 | vname = f"__anonymous{anonymous}" 462 | anonymous += 1 463 | tokens.push(";") 464 | fields_types[vname] = field_type 465 | # calculate the max field size (for the alignment) 466 | max_alignment = max(max_alignment, field_type.alignment) 467 | # align struct if byte order is native 468 | if not __is_union__: # C struct 469 | field_type.align_filed_offset() 470 | offset = field_type.offset + field_type.vsize 471 | t = tokens.pop() 472 | if t != ";": 473 | raise ParserError(f"`;` expected but `{t}` found") 474 | 475 | if __is_union__: # C union 476 | # Calculate the sizeof union as size of its largest element 477 | size = max([x.vsize for x in fields_types.values()]) 478 | else: # C struct 479 | # add padding to struct if byte order is native 480 | size = offset + calculate_padding(__byte_order__, max_alignment, offset) 481 | 482 | # Prepare the result 483 | result = { 484 | "__fields__": list(fields_types.keys()), 485 | "__fields_types__": fields_types, 486 | "__size__": size, 487 | "__is_struct__": not __is_union__, 488 | "__is_union__": __is_union__, 489 | "__is_enum__": False, 490 | "__byte_order__": __byte_order__, 491 | "__alignment__": max_alignment, 492 | "__name__": __name__, 493 | "__cls__": __cls__, 494 | } 495 | return result 496 | -------------------------------------------------------------------------------- /cstruct/cenum.py: -------------------------------------------------------------------------------- 1 | from .abstract import AbstractCEnum 2 | 3 | 4 | class CEnum(AbstractCEnum): 5 | @classmethod 6 | def sizeof(cls) -> int: 7 | "Type size (in bytes)" 8 | return cls.__size__ 9 | -------------------------------------------------------------------------------- /cstruct/cstruct.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | from typing import List, Optional 26 | 27 | from .abstract import AbstractCStruct 28 | from .base import CHAR_ZERO 29 | 30 | 31 | class CStruct(AbstractCStruct): 32 | """ 33 | Convert C struct definitions into Python classes. 34 | 35 | Attributes: 36 | __struct__ (str): definition of the struct (or union) in C syntax 37 | __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER 38 | __is_union__ (bool): True for union definitions, False for struct definitions 39 | __size__ (int): size of the structure in bytes (flexible array member size is omitted) 40 | __fields__ (list): list of structure fields 41 | __fields_types__ (dict): dictionary mapping field names to types 42 | """ 43 | 44 | def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: 45 | """ 46 | Unpack bytes containing packed C structure data 47 | 48 | Args: 49 | buffer: bytes to be unpacked 50 | offset: optional buffer offset 51 | flexible_array_length: optional flexible array length (number of elements) 52 | """ 53 | self.set_flexible_array_length(flexible_array_length) 54 | if buffer is None: 55 | buffer = CHAR_ZERO * self.size 56 | for field, field_type in self.__fields_types__.items(): 57 | setattr(self, field, field_type.unpack_from(buffer, offset)) 58 | return True 59 | 60 | def pack(self) -> bytes: 61 | """ 62 | Pack the structure data into bytes 63 | 64 | Returns: 65 | bytes: The packed structure 66 | """ 67 | result: List[bytes] = [] 68 | for field, field_type in self.__fields_types__.items(): 69 | # Add padding if needed 70 | if field_type.padding: 71 | result.append(CHAR_ZERO * field_type.padding) 72 | 73 | if field_type.is_struct or field_type.is_union: 74 | if field_type.vlen == 1: # single struct 75 | v = getattr(self, field, field_type.ref()) 76 | v = v.pack() 77 | result.append(v) 78 | else: # multiple struct 79 | values = getattr(self, field, []) 80 | for j in range(0, field_type.vlen): 81 | try: 82 | v = values[j] 83 | except KeyError: 84 | v = field_type.ref() 85 | v = v.pack() 86 | result.append(v) 87 | else: 88 | data = getattr(self, field) 89 | result.append(field_type.pack(data)) 90 | return bytes().join(result) 91 | -------------------------------------------------------------------------------- /cstruct/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | __all__ = [ 26 | "CEnumException", 27 | "CStructException", 28 | "ParserError", 29 | "EvalError", 30 | "ContextNotFound", 31 | ] 32 | 33 | 34 | class CEnumException(Exception): 35 | pass 36 | 37 | 38 | class CStructException(Exception): 39 | pass 40 | 41 | 42 | class ParserError(CStructException): 43 | pass 44 | 45 | 46 | class EvalError(CStructException): 47 | pass 48 | 49 | 50 | class ContextNotFound(EvalError): 51 | pass 52 | -------------------------------------------------------------------------------- /cstruct/field.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import copy 26 | import inspect 27 | import struct 28 | from enum import Enum 29 | from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, Union 30 | 31 | from .base import NATIVE_ORDER 32 | from .exceptions import ContextNotFound, ParserError 33 | from .native_types import get_native_type 34 | 35 | if TYPE_CHECKING: 36 | from .abstract import AbstractCStruct 37 | 38 | __all__ = ["align", "calculate_padding", "Kind", "FieldType"] 39 | 40 | 41 | def align(byte_order: Optional[str]) -> bool: 42 | return byte_order is None or byte_order == NATIVE_ORDER 43 | 44 | 45 | def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> int: 46 | if align(byte_order): # calculate the padding 47 | modulo = pos % alignment 48 | if modulo: # not aligned 49 | return alignment - modulo 50 | return 0 51 | 52 | 53 | def get_cstruct_context() -> Optional["AbstractCStruct"]: 54 | """ 55 | Get the current CStruct context (instance) from the stack 56 | """ 57 | from .abstract import AbstractCStruct 58 | 59 | stack = inspect.stack() 60 | for frame in stack: 61 | caller_self = frame.frame.f_locals.get("self") 62 | if isinstance(caller_self, AbstractCStruct): 63 | return caller_self 64 | return None 65 | 66 | 67 | class Kind(Enum): 68 | """ 69 | Field type 70 | """ 71 | 72 | NATIVE = 0 73 | "Native type (e.g. int, char)" 74 | STRUCT = 1 75 | "Struct type" 76 | UNION = 2 77 | "Union type" 78 | ENUM = 3 79 | "Enum type" 80 | 81 | 82 | class FieldType: 83 | """ 84 | Struct/Union field 85 | 86 | Attributes: 87 | kind (Kind): struct/union/native 88 | c_type (str): field type 89 | ref (AbstractCStruct): struct/union class ref 90 | vlen_ex (int|callable int): number of elements 91 | flexible_array (bool): True for flexible arrays 92 | offset (int): relative memory position of the field (relative to the struct) 93 | padding (int): padding 94 | """ 95 | 96 | kind: Kind 97 | c_type: str 98 | ref: Optional[Type["AbstractCStruct"]] 99 | vlen_ex: Union[int, Callable[[], int]] 100 | flexible_array: bool 101 | byte_order: Optional[str] 102 | offset: int 103 | padding: int 104 | 105 | def __init__( 106 | self, 107 | kind: Kind, 108 | c_type: str, 109 | ref: Optional[Type["AbstractCStruct"]], 110 | vlen_ex: Union[int, Callable[[], int]], 111 | flexible_array: bool, 112 | byte_order: Optional[str], 113 | offset: int, 114 | ) -> None: 115 | """ 116 | Initialize a Struct/Union field 117 | 118 | Args: 119 | kind: struct/union/native 120 | c_type: field type 121 | ref: struct/union class ref 122 | vlen_ex: number of elements 123 | flexible_array: True for flexible arrays 124 | offset: relative memory position of the field (relative to the struct) 125 | """ 126 | self.kind = kind 127 | self.c_type = c_type 128 | self.ref = ref 129 | self.vlen_ex = vlen_ex 130 | self.flexible_array = flexible_array 131 | self.byte_order = byte_order 132 | self.offset = self.base_offset = offset 133 | self.padding = 0 134 | 135 | def unpack_from(self, buffer: bytes, offset: int = 0, context: Optional["AbstractCStruct"] = None) -> Any: 136 | """ 137 | Unpack bytes containing packed C structure data 138 | 139 | Args: 140 | buffer: bytes to be unpacked 141 | offset: optional buffer offset 142 | context: context (cstruct instance) 143 | 144 | Returns: 145 | data: The unpacked data 146 | """ 147 | if self.is_native or self.is_enum: 148 | result = struct.unpack_from(self.fmt, buffer, self.offset + offset) 149 | 150 | if self.is_enum: 151 | result = tuple(map(self.ref, result)) 152 | 153 | if self.is_array: 154 | return list(result) 155 | else: 156 | return result[0] 157 | else: # struct/union 158 | if self.vlen == 1: # single struct/union 159 | instance: AbstractCStruct = self.ref() # type: ignore 160 | instance.unpack_from(buffer, self.offset + offset) 161 | return instance 162 | else: # multiple struct/union 163 | instances: List[AbstractCStruct] = [] 164 | for j in range(0, self.vlen): 165 | instance: AbstractCStruct = self.ref() # type: ignore 166 | instance.unpack_from(buffer, self.offset + offset + j * instance.size) 167 | instances.append(instance) 168 | return instances 169 | 170 | def pack(self, data: Any) -> bytes: 171 | """ 172 | Pack the field into bytes 173 | 174 | Args: 175 | data: data to be packed 176 | 177 | Returns: 178 | bytes: The packed structure 179 | """ 180 | if self.flexible_array: 181 | self.vlen_ex = len(data) # set flexible array size 182 | return struct.pack(self.fmt, *data) 183 | elif self.is_array: 184 | if self.vlen == 0: # empty array 185 | return bytes() 186 | else: 187 | return struct.pack(self.fmt, *data) 188 | else: 189 | return struct.pack(self.fmt, data) 190 | 191 | @property 192 | def vlen(self) -> int: 193 | "Number of elements" 194 | try: 195 | return self.vlen_ex() if callable(self.vlen_ex) else self.vlen_ex 196 | except ContextNotFound: 197 | return 0 198 | 199 | @property 200 | def is_array(self) -> bool: 201 | "True if field is an array/flexible array" 202 | return self.flexible_array or (not (self.vlen == 1 or self.c_type == "char")) 203 | 204 | @property 205 | def is_native(self) -> bool: 206 | "True if the field is a native type (e.g. int, char)" 207 | return self.kind == Kind.NATIVE 208 | 209 | @property 210 | def is_enum(self) -> bool: 211 | "True if the field is an enum" 212 | return self.kind == Kind.ENUM 213 | 214 | @property 215 | def is_struct(self) -> bool: 216 | "True if the field is a struct" 217 | return self.kind == Kind.STRUCT 218 | 219 | @property 220 | def is_union(self) -> bool: 221 | "True if the field is an union" 222 | return self.kind == Kind.UNION 223 | 224 | @property 225 | def native_format(self) -> str: 226 | "Field format (struct library format)" 227 | if self.is_native: 228 | try: 229 | return get_native_type(self.c_type).native_format 230 | except KeyError: 231 | raise ParserError(f"Unknown type `{self.c_type}`") 232 | elif self.is_enum: 233 | return self.ref.__native_format__ 234 | else: 235 | return "c" 236 | 237 | @property 238 | def fmt(self) -> str: 239 | "Field format prefixed by byte order (struct library format)" 240 | if self.is_native or self.is_enum: 241 | if self.vlen == 0: 242 | fmt = "" 243 | else: 244 | fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format 245 | else: # Struct/Union 246 | fmt = str(self.vlen * self.ref.sizeof()) + self.native_format 247 | if self.byte_order: 248 | return self.byte_order + fmt 249 | else: 250 | return fmt 251 | 252 | @property 253 | def vsize(self) -> int: 254 | "Field size in bytes" 255 | return struct.calcsize(self.fmt) 256 | 257 | @property 258 | def alignment(self) -> int: 259 | "Alignment" 260 | if self.is_native or self.is_enum: 261 | if self.byte_order is not None: 262 | return struct.calcsize(self.byte_order + self.native_format) 263 | else: 264 | return struct.calcsize(self.native_format) 265 | else: # struct/union 266 | return self.ref.__alignment__ 267 | 268 | def align_filed_offset(self) -> None: 269 | "If the byte order is native, align the field" 270 | if align(self.byte_order) and self.c_type != "char": 271 | self.padding = calculate_padding(self.byte_order, self.alignment, self.base_offset) 272 | self.offset = self.base_offset + self.padding 273 | 274 | def copy(self) -> "FieldType": 275 | "Return a shallow copy of this FieldType" 276 | return copy.copy(self) 277 | 278 | def __repr__(self) -> str: # pragma: no cover 279 | return repr(self.__dict__) 280 | -------------------------------------------------------------------------------- /cstruct/mem_cstruct.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import ctypes 26 | import struct 27 | from typing import Any, List, Optional 28 | 29 | from .abstract import AbstractCStruct 30 | 31 | 32 | class CStructList(List[Any]): 33 | def __init__(self, values: List[Any], name: str, parent: Optional["MemCStruct"] = None) -> None: 34 | super().__init__(values) 35 | self.name = name 36 | self.parent = parent 37 | 38 | def __setitem__(self, key: int, value: List[Any]) -> None: # type: ignore 39 | super().__setitem__(key, value) 40 | # Notify the parent when a value is changed 41 | if self.parent is not None: 42 | self.parent.on_change_list(self.name, key, value) 43 | 44 | 45 | class MemCStruct(AbstractCStruct): 46 | """ 47 | Convert C struct definitions into Python classes. 48 | 49 | Attributes: 50 | __struct__ (str): definition of the struct (or union) in C syntax 51 | __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER 52 | __is_union__ (bool): True for union definitions, False for struct definitions 53 | __mem__: mutable character buffer 54 | __size__ (int): size of the structure in bytes (flexible array member size is omitted) 55 | __fields__ (list): list of structure fields 56 | __fields_types__ (dict): dictionary mapping field names to types 57 | """ 58 | 59 | __mem__ = None 60 | __base__ = 0 61 | 62 | def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: 63 | """ 64 | Unpack bytes containing packed C structure data 65 | 66 | Args: 67 | buffer: bytes to be unpacked 68 | offset: optional buffer offset 69 | flexible_array_length: optional flexible array length (number of elements) 70 | """ 71 | self.set_flexible_array_length(flexible_array_length) 72 | self.__base__ = offset # Base offset 73 | if buffer is None: 74 | # the buffer is one item larger than its size and the last element is NUL 75 | self.__mem__ = ctypes.create_string_buffer(self.size + 1) 76 | elif isinstance(buffer, ctypes.Array): 77 | self.__mem__ = buffer 78 | elif isinstance(buffer, int): 79 | # buffer is a pointer 80 | self.__mem__ = ctypes.cast(buffer, ctypes.POINTER(ctypes.c_char * self.size)).contents 81 | else: 82 | self.__mem__ = ctypes.create_string_buffer(buffer) 83 | for field, field_type in self.__fields_types__.items(): 84 | if field_type.is_struct or field_type.is_union: 85 | setattr(self, field, field_type.unpack_from(self.__mem__, offset)) 86 | return True 87 | 88 | def memcpy(self, destination: int, source: bytes, num: int) -> None: 89 | """ 90 | Copies the values of num bytes from source to the struct memory 91 | 92 | Args: 93 | destination: destination address 94 | source: source data to be copied 95 | num: number of bytes to copy 96 | """ 97 | ctypes.memmove(ctypes.byref(self.__mem__, destination), source, num) 98 | 99 | def pack(self) -> bytes: 100 | """ 101 | Pack the structure data into bytes 102 | 103 | Returns: 104 | bytes: The packed structure 105 | """ 106 | return self.__mem__.raw[self.__base__ : self.__base__ + self.size] 107 | 108 | def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: 109 | """ 110 | Set flexible array length (i.e. number of elements) 111 | 112 | Args: 113 | flexible_array_length: flexible array length 114 | """ 115 | super().set_flexible_array_length(flexible_array_length) 116 | if self.__mem__ is not None: 117 | try: 118 | ctypes.resize(self.__mem__, self.size + 1) 119 | except ValueError: 120 | pass 121 | 122 | def __getattr__(self, attr: str) -> Any: 123 | field_type = self.__fields_types__[attr] 124 | result = field_type.unpack_from(self.__mem__, self.__base__) 125 | if isinstance(result, list): 126 | return CStructList(result, name=attr, parent=self) 127 | else: 128 | return result 129 | 130 | def __setattr__(self, attr: str, value: Any) -> None: 131 | field_type = self.__fields_types__.get(attr) 132 | if field_type is None: 133 | object.__setattr__(self, attr, value) 134 | elif field_type.is_struct or field_type.is_union: 135 | object.__setattr__(self, attr, value) 136 | else: # native 137 | if field_type.flexible_array and len(value) != field_type.vlen: 138 | # flexible array size changed, resize the buffer 139 | field_type.vlen_ex = len(value) 140 | ctypes.resize(self.__mem__, self.size + 1) 141 | addr = field_type.offset + self.__base__ 142 | self.memcpy(addr, field_type.pack(value), field_type.vsize) 143 | 144 | def on_change_list(self, attr: str, key: int, value: Any) -> None: 145 | field_type = self.__fields_types__[attr] 146 | # Calculate the single field format and size 147 | fmt = (self.__byte_order__ + field_type.fmt[-1]) if self.__byte_order__ is not None else field_type.fmt[-1] 148 | size = struct.calcsize(fmt) 149 | # Calculate the single field memory position 150 | addr = field_type.offset + self.__base__ + size * key 151 | # Update the memory 152 | self.memcpy(addr, struct.pack(fmt, value), size) 153 | -------------------------------------------------------------------------------- /cstruct/native_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2025 Andrea Bonomi 3 | # 4 | # Published under the terms of the MIT license. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | 25 | import struct 26 | from abc import ABCMeta 27 | from typing import Any, Dict, Tuple, Type 28 | 29 | __all__ = [ 30 | "get_native_type", 31 | "AbstractNativeType", 32 | "Char", 33 | "SignedChar", 34 | "UnsignedChar", 35 | "Short", 36 | "UnsignedShort", 37 | "Int", 38 | "UnsignedInt", 39 | "Long", 40 | "UnsignedLong", 41 | "LongLong", 42 | "UnsignedLongLong", 43 | "Float", 44 | "Double", 45 | "Pointer", 46 | "Int8", 47 | "UnsignedInt8", 48 | "Int16", 49 | "UnsignedInt16", 50 | "Int32", 51 | "UnsignedInt32", 52 | "Int64", 53 | "UnsignedInt64", 54 | ] 55 | 56 | 57 | NATIVE_TYPES: Dict[str, Type["AbstractNativeType"]] = {} 58 | 59 | 60 | def get_native_type(type_: str) -> Type["AbstractNativeType"]: 61 | """ 62 | Get a base data type by name 63 | 64 | Args: 65 | type_: data type 66 | 67 | Returns: 68 | class: data type class 69 | 70 | Raises: 71 | KeyError: If type is not defined 72 | """ 73 | try: 74 | return NATIVE_TYPES[type_] 75 | except KeyError: 76 | raise KeyError(f"Unknown type `{type_}`") 77 | 78 | 79 | class NativeTypeMeta(ABCMeta): 80 | __size__: int = 0 81 | " Size in bytes " 82 | 83 | def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: 84 | if namespace.get("native_format"): 85 | native_format = namespace["native_format"] 86 | namespace["__size__"] = struct.calcsize(native_format) 87 | else: 88 | native_format = None 89 | namespace["native_format"] = None 90 | namespace["__size__"] = None 91 | new_class: Type[AbstractNativeType] = super().__new__(metacls, name, bases, namespace) # type: ignore 92 | if namespace.get("type_name"): 93 | NATIVE_TYPES[namespace["type_name"]] = new_class 94 | return new_class 95 | 96 | def __len__(cls) -> int: 97 | "Type size (in bytes)" 98 | return cls.__size__ 99 | 100 | @property 101 | def size(cls) -> int: 102 | "Type size (in bytes)" 103 | return cls.__size__ 104 | 105 | 106 | class AbstractNativeType(metaclass=NativeTypeMeta): 107 | type_name: str = "" 108 | " Type name " 109 | native_format: str = "" 110 | " Type format " 111 | 112 | def __str__(self) -> str: 113 | return self.type_name 114 | 115 | @classmethod 116 | def sizeof(cls) -> int: 117 | "Type size (in bytes)" 118 | return cls.__size__ 119 | 120 | 121 | class Char(AbstractNativeType): 122 | type_name = "char" 123 | native_format = "s" 124 | 125 | 126 | class SignedChar(AbstractNativeType): 127 | type_name = "signed char" 128 | native_format = "b" 129 | 130 | 131 | class UnsignedChar(AbstractNativeType): 132 | type_name = "unsigned char" 133 | native_format = "B" 134 | 135 | 136 | class Short(AbstractNativeType): 137 | type_name = "short" 138 | native_format = "h" 139 | 140 | 141 | class UnsignedShort(AbstractNativeType): 142 | type_name = "unsigned short" 143 | native_format = "H" 144 | 145 | 146 | class Int(AbstractNativeType): 147 | type_name = "int" 148 | native_format = "i" 149 | 150 | 151 | class UnsignedInt(AbstractNativeType): 152 | type_name = "unsigned int" 153 | native_format = "I" 154 | 155 | 156 | class Long(AbstractNativeType): 157 | type_name = "long" 158 | native_format = "l" 159 | 160 | 161 | class UnsignedLong(AbstractNativeType): 162 | type_name = "unsigned long" 163 | native_format = "L" 164 | 165 | 166 | class LongLong(AbstractNativeType): 167 | type_name = "long long" 168 | native_format = "q" 169 | 170 | 171 | class UnsignedLongLong(AbstractNativeType): 172 | type_name = "unsigned long long" 173 | native_format = "Q" 174 | 175 | 176 | class Float(AbstractNativeType): 177 | type_name = "float" 178 | native_format = "f" 179 | 180 | 181 | class Double(AbstractNativeType): 182 | type_name = "double" 183 | native_format = "d" 184 | 185 | 186 | class Pointer(AbstractNativeType): 187 | type_name = "void *" 188 | native_format = "P" 189 | 190 | 191 | class Int8(AbstractNativeType): 192 | type_name = "int8" 193 | native_format = "b" 194 | 195 | 196 | class UnsignedInt8(AbstractNativeType): 197 | type_name = "uint8" 198 | native_format = "B" 199 | 200 | 201 | class Int16(AbstractNativeType): 202 | type_name = "int16" 203 | native_format = "h" 204 | 205 | 206 | class UnsignedInt16(AbstractNativeType): 207 | type_name = "uint16" 208 | native_format = "H" 209 | 210 | 211 | class Int32(AbstractNativeType): 212 | type_name = "int32" 213 | native_format = "i" 214 | 215 | 216 | class UnsignedInt32(AbstractNativeType): 217 | type_name = "uint32" 218 | native_format = "I" 219 | 220 | 221 | class Int64(AbstractNativeType): 222 | type_name = "int64" 223 | native_format = "q" 224 | 225 | 226 | class UnsignedInt64(AbstractNativeType): 227 | type_name = "uint64" 228 | native_format = "Q" 229 | -------------------------------------------------------------------------------- /docker/i386/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM i386/ubuntu 2 | 3 | RUN apt-get update && \ 4 | apt-get -y install \ 5 | python3.6 \ 6 | python3.6-dev \ 7 | python3.6-distutils \ 8 | curl \ 9 | make && \ 10 | rm -rf /var/lib/apt/lists/* 11 | RUN curl -sSL https://bootstrap.pypa.io/pip/3.6/get-pip.py -o get-pip.py && \ 12 | python3.6 get-pip.py 13 | RUN pip install pytest 14 | WORKDIR /app 15 | -------------------------------------------------------------------------------- /docker/i386/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT=cstruct 2 | BASENAME=test-i386 3 | IMAGE_NAME=${PROJECT}-${BASENAME} 4 | 5 | .PHONY: help build push all 6 | 7 | help: 8 | @echo "- make build Build docker image" 9 | @echo "- make test Build and run tests" 10 | @echo "- make shell Run interactive shell" 11 | 12 | .DEFAULT_GOAL := help 13 | 14 | build: 15 | @DOCKER_BUILDKIT=1 docker build --tag ${IMAGE_NAME}:latest . 16 | 17 | test: build 18 | @docker run --rm -it \ 19 | --mount type=bind,source=$$PWD/../..,target=/app \ 20 | --hostname=$(BASENAME) \ 21 | ${IMAGE_NAME} \ 22 | pytest 23 | 24 | shell: 25 | @docker run --rm -it \ 26 | --mount type=bind,source=$$PWD/../..,target=/app \ 27 | --hostname=$(BASENAME) \ 28 | ${IMAGE_NAME} \ 29 | bash -i 30 | 31 | all: build 32 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | {!CODE_OF_CONDUCT.md!} 2 | -------------------------------------------------------------------------------- /docs/api/abstract.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.abstract 2 | -------------------------------------------------------------------------------- /docs/api/base.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.base 2 | -------------------------------------------------------------------------------- /docs/api/c_expr.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.c_expr 2 | -------------------------------------------------------------------------------- /docs/api/c_parser.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.c_parser 2 | -------------------------------------------------------------------------------- /docs/api/cstruct.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.cstruct 2 | -------------------------------------------------------------------------------- /docs/api/field.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.field 2 | -------------------------------------------------------------------------------- /docs/api/mem_cstruct.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.mem_cstruct 2 | -------------------------------------------------------------------------------- /docs/api/module.md: -------------------------------------------------------------------------------- 1 | ::: cstruct 2 | -------------------------------------------------------------------------------- /docs/api/native_types.md: -------------------------------------------------------------------------------- 1 | ::: cstruct.native_types 2 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | {!changelog.txt!} 2 | -------------------------------------------------------------------------------- /docs/examples/dir.md: -------------------------------------------------------------------------------- 1 | The following program prints the names of the files in a directory, 2 | calling the libc functions `getcwd`, `opendir`, `readdir`, and `closedir`: 3 | 4 | ``` 5 | {!examples/dir.py!} 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/examples/fdisk.md: -------------------------------------------------------------------------------- 1 | The following program reads the DOS-type (MBR) partition table from a disk. 2 | 3 | ``` 4 | {!examples/fdisk.py!} 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/examples/flexible_array.md: -------------------------------------------------------------------------------- 1 | [Flexible Array Member (FAM)](https://en.wikipedia.org/wiki/Flexible_array_member) example. 2 | 3 | ``` 4 | {!examples/flexible_array.py!} 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/examples/who.md: -------------------------------------------------------------------------------- 1 | The following program prints information about users who are currently logged in. 2 | 3 | ``` 4 | {!examples/who.py!} 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {!README.md!} 2 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | {!LICENSE!} 2 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /examples/dir.c: -------------------------------------------------------------------------------- 1 | /* https://www.gnu.org/software/libc/manual/html_mono/libc.html#Simple-Directory-Lister */ 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | int main (void) { 8 | DIR *dp; 9 | struct dirent *ep; 10 | 11 | dp = opendir("."); 12 | if (dp != NULL) { 13 | while (ep = readdir (dp)) { 14 | puts(ep->d_name); 15 | } 16 | closedir(dp); 17 | } else { 18 | perror("Couldn't open the directory"); 19 | } 20 | 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /examples/dir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import ctypes 3 | import sys 4 | 5 | import cstruct 6 | 7 | libc = ctypes.cdll.LoadLibrary("libc.so.6") 8 | # opendir 9 | libc.opendir.argtypes = [ctypes.c_char_p] 10 | libc.opendir.restype = ctypes.c_void_p 11 | # readdir 12 | libc.readdir.argtypes = [ctypes.c_void_p] 13 | libc.readdir.restype = ctypes.c_void_p 14 | # closedir 15 | libc.closedir.argtypes = [ctypes.c_void_p] 16 | libc.closedir.restype = ctypes.c_int 17 | 18 | 19 | class DType(cstruct.CEnum): 20 | __size__ = 1 21 | __def__ = """ 22 | enum d_type { 23 | DT_UNKNOWN = 0x0, 24 | DT_FIFO = 0x1, 25 | DT_CHR = 0x2, 26 | DT_DIR = 0x4, 27 | DT_BLK = 0x6, 28 | DT_REG = 0x8, 29 | DT_LNK = 0xa, 30 | DT_SOCK = 0xc 31 | }; 32 | """ 33 | 34 | def __str__(self): 35 | return { 36 | DType.DT_UNKNOWN: "", 37 | DType.DT_FIFO: "", 38 | DType.DT_CHR: "", 39 | DType.DT_DIR: "", 40 | DType.DT_BLK: "", 41 | DType.DT_REG: "", 42 | DType.DT_LNK: "", 43 | DType.DT_SOCK: "", 44 | }[self] 45 | 46 | 47 | class Dirent(cstruct.MemCStruct): 48 | __def__ = """ 49 | #define PATH_MAX 4096 50 | 51 | typedef long ino_t; 52 | typedef long off_t; 53 | 54 | struct dirent { 55 | ino_t d_ino; /* Inode number */ 56 | off_t d_off; /* Not an offset */ 57 | unsigned short d_reclen; /* Length of this record */ 58 | unsigned char d_type; /* Type of file; not supported 59 | by all filesystem types */ 60 | char d_name[256]; /* Null-terminated filename */ 61 | }; 62 | """ 63 | 64 | @property 65 | def name(self): 66 | return ctypes.c_char_p(self.d_name).value.decode("ascii") 67 | 68 | @property 69 | def type(self): 70 | return DType(self.d_type) 71 | 72 | 73 | def main(): 74 | if len(sys.argv) > 1: 75 | cwd = ctypes.create_string_buffer(sys.argv[1].encode("ascii")) 76 | else: 77 | # Get current dir 78 | cwd = ctypes.create_string_buffer(cstruct.getdef("PATH_MAX") + 1) 79 | assert libc.getcwd(cwd, ctypes.sizeof(cwd)) != 0 80 | # Open dir 81 | dp = libc.opendir(cwd) 82 | assert dp != 0 83 | # Read dir entries 84 | ep = libc.readdir(dp) 85 | while ep: 86 | contents = ctypes.cast(ep, ctypes.POINTER(ctypes.c_char * Dirent.size)).contents 87 | dirent = Dirent(contents) 88 | print(f"{dirent.d_ino:8} {dirent.type:10} {dirent.name}") 89 | ep = libc.readdir(dp) 90 | # Close dir 91 | libc.closedir(dp) 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /examples/dir.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd "$(dirname "$0")/.." || exit 3 | python -m examples.dir $* 4 | -------------------------------------------------------------------------------- /examples/fdisk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import sys 5 | from pathlib import Path 6 | 7 | import cstruct 8 | 9 | UNITS = ['B', 'K', 'M', 'G', 'T'] 10 | SECTOR_SIZE = 512 11 | TYPES = { 12 | 0x00: "Empty", 13 | 0x01: "FAT12", 14 | 0x05: "Extended", 15 | 0x06: "FAT16", 16 | 0x07: "HPFS/NTFS/exFAT", 17 | 0x0B: "W95 FAT32", 18 | 0x0C: "W95 FAT32 (LBA)", 19 | 0x0E: "W95 FAT16 (LBA)", 20 | 0x0F: "W95 extended (LBA)", 21 | 0x11: "Hidden FAT12", 22 | 0x14: "Hidden FAT16 <32M", 23 | 0x16: "Hidden FAT16", 24 | 0x17: "Hidden HPFS/NTFS", 25 | 0x1B: "Hidden W95 FAT32", 26 | 0x1C: "Hidden W95 FAT32 (LBA)", 27 | 0x1E: "Hidden W95 FAT16 (LBA)", 28 | 0x27: "Hidden NTFS WinRE", 29 | 0x81: "Minix / old Linux", 30 | 0x82: "Linux swap / Solaris", 31 | 0x83: "Linux", 32 | 0x85: "Linux extended", 33 | 0x86: "NTFS volume set", 34 | 0x87: "NTFS volume set", 35 | 0x88: "Linux plaintext", 36 | 0x8E: "Linux LVM", 37 | 0x9F: "BSD/OS", 38 | 0xA5: "FreeBSD", 39 | 0xA6: "OpenBSD", 40 | 0xAF: "HFS / HFS+", 41 | 0xEA: "Linux extended boot", 42 | 0xEE: "GPT", 43 | 0xEF: "EFI (FAT-12/16/32)", 44 | 0xF2: "DOS secondary", 45 | 0xFB: "VMware VMFS", 46 | 0xFC: "VMware VMKCORE", 47 | 0xFD: "Linux raid autodetect", 48 | } 49 | 50 | 51 | class Position(cstruct.MemCStruct): 52 | __byte_order__ = cstruct.LITTLE_ENDIAN 53 | __def__ = """ 54 | struct { 55 | unsigned char head; 56 | unsigned char sector; 57 | unsigned char cyl; 58 | } 59 | """ 60 | 61 | 62 | class Partition(cstruct.MemCStruct): 63 | __byte_order__ = cstruct.LITTLE_ENDIAN 64 | __def__ = """ 65 | #define ACTIVE_FLAG 0x80 66 | typedef struct Position Position; 67 | 68 | struct { 69 | unsigned char status; /* 0x80 - active */ 70 | Position start; 71 | unsigned char partition_type; 72 | Position end; 73 | unsigned int start_sect; /* starting sector counting from 0 */ 74 | unsigned int sectors; /* nr of sectors in partition */ 75 | } 76 | """ 77 | 78 | @property 79 | def bootable_str(self): 80 | return "*" if (self.status & cstruct.getdef("ACTIVE_FLAG")) else " " 81 | 82 | @property 83 | def end_sect(self): 84 | return self.start_sect + self.sectors - 1 85 | 86 | @property 87 | def part_size_str(self): 88 | val = self.sectors * SECTOR_SIZE 89 | for unit in UNITS: 90 | if val < 1000: 91 | break 92 | val = int(val / 1000) 93 | return f"{val}{unit}" 94 | 95 | @property 96 | def part_type_str(self): 97 | return TYPES.get(self.partition_type, "") 98 | 99 | def __str__(self): 100 | return f"{self.bootable_str} {self.start_sect:>10} {self.end_sect:>8} {self.sectors:>8} {self.part_size_str:>4} {self.partition_type:02x} {self.part_type_str}" 101 | 102 | 103 | class MBR(cstruct.MemCStruct): 104 | __byte_order__ = cstruct.LITTLE_ENDIAN 105 | __def__ = """ 106 | #define MBR_SIZE 512 107 | #define MBR_DISK_SIGNATURE_SIZE 4 108 | #define MBR_USUALY_NULLS_SIZE 2 109 | #define MBR_SIGNATURE_SIZE 2 110 | #define MBR_BOOT_SIGNATURE 0xaa55 111 | #define MBR_PARTITIONS_NUM 4 112 | #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) 113 | #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) 114 | 115 | typedef struct Partition Partition; 116 | 117 | struct { 118 | char unused[MBR_UNUSED_SIZE]; 119 | unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; 120 | unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; 121 | Partition partitions[MBR_PARTITIONS_NUM]; 122 | uint16 signature; 123 | } 124 | """ 125 | 126 | @property 127 | def disk_signature_str(self): 128 | return "".join(reversed([f"{x:02x}" for x in self.disk_signature])) 129 | 130 | def print_info(self): 131 | print(f"Sector size: {cstruct.getdef('MBR_SIZE')}") 132 | if self.signature != cstruct.getdef('MBR_BOOT_SIGNATURE'): 133 | print("Invalid MBR signature") 134 | 135 | print(f"Disk identifier: 0x{self.disk_signature_str}") 136 | print() 137 | print("Device Boot Start End Sectors Size Id Type") 138 | for i, partition in enumerate(self.partitions): 139 | if partition.sectors: 140 | print(f"part{i:<2} {partition}") 141 | 142 | 143 | def main(): 144 | parser = argparse.ArgumentParser(description="Display or manipulate a disk partition table.") 145 | parser.add_argument("disk") 146 | args = parser.parse_args() 147 | 148 | try: 149 | with Path(args.disk).open("rb") as f: 150 | mbr = MBR() 151 | data = f.read(len(mbr)) 152 | mbr.unpack(data) 153 | mbr.print_info() 154 | except (IOError, OSError) as ex: 155 | print(ex) 156 | sys.exit(1) 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /examples/fdisk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd "$(dirname "$0")/.." || exit 3 | python -m examples.fdisk $* 4 | -------------------------------------------------------------------------------- /examples/flexible_array.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import random 4 | from pathlib import Path 5 | 6 | from cstruct import MemCStruct 7 | 8 | 9 | class FlexArray(MemCStruct): 10 | __def__ = """ 11 | struct { 12 | int length; 13 | uint32 checksum; 14 | long data[]; 15 | } 16 | """ 17 | 18 | def set_length(self, length): 19 | self.length = length 20 | self.set_flexible_array_length(length) 21 | 22 | 23 | def write(filename, length): 24 | print("---write---") 25 | flex = FlexArray() 26 | flex.set_length(length) 27 | # Generate random data 28 | flex.data = [random.randint(0, 2**63) for _ in range(0, length)] 29 | # Calculate the checksum 30 | flex.checksum = 0 31 | for num in flex.data: 32 | flex.checksum = (flex.checksum + num) % 2**32 33 | print(f"checksum: {flex.checksum}") 34 | # Write data 35 | with Path(filename).open("wb") as f: 36 | f.write(flex.pack()) 37 | 38 | 39 | def read(filename): 40 | print("---read---") 41 | with Path(filename).open("rb") as f: 42 | # Read the header 43 | flex = FlexArray(f) 44 | print(f"length: {flex.length}, checksum: {flex.checksum}") 45 | # Read header and data 46 | f.seek(0, 0) 47 | flex.unpack(f, flexible_array_length=flex.length) 48 | if len(flex.data) == flex.length: 49 | print("length ok") 50 | # Check the checksum 51 | checksum = 0 52 | for num in flex.data: 53 | checksum = (checksum + num) % 2**32 54 | if flex.checksum == checksum: 55 | print("checksum ok") 56 | 57 | 58 | def main(): 59 | filename = "tempfile" 60 | random.seed(5) 61 | write(filename, 1000) 62 | read(filename) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /examples/flexible_array.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd "$(dirname "$0")/.." || exit 3 | python -m examples.flexible_array $* 4 | -------------------------------------------------------------------------------- /examples/who.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import sys 5 | import time 6 | from pathlib import Path 7 | 8 | from cstruct import NATIVE_ORDER, MemCStruct, getdef, parse 9 | 10 | DEFAULT_FILENAME = "/var/run/utmp" 11 | 12 | parse( 13 | """ 14 | /* Values for ut_type field, below */ 15 | 16 | #define EMPTY 0 /* Record does not contain valid info 17 | (formerly known as UT_UNKNOWN on Linux) */ 18 | #define RUN_LVL 1 /* Change in system run-level (see 19 | init(1)) */ 20 | #define BOOT_TIME 2 /* Time of system boot (in ut_tv) */ 21 | #define NEW_TIME 3 /* Time after system clock change 22 | (in ut_tv) */ 23 | #define OLD_TIME 4 /* Time before system clock change 24 | (in ut_tv) */ 25 | #define INIT_PROCESS 5 /* Process spawned by init(1) */ 26 | #define LOGIN_PROCESS 6 /* Session leader process for user login */ 27 | #define USER_PROCESS 7 /* Normal process */ 28 | #define DEAD_PROCESS 8 /* Terminated process */ 29 | #define ACCOUNTING 9 /* Not implemented */ 30 | 31 | #define UT_LINESIZE 32 32 | #define UT_NAMESIZE 32 33 | #define UT_HOSTSIZE 256 34 | 35 | typedef int pid_t; 36 | typedef long time_t; 37 | """ 38 | ) 39 | 40 | 41 | class ExitStatus(MemCStruct): 42 | __def__ = """ 43 | struct ExitStatus { 44 | short e_termination; /* Process termination status. */ 45 | short e_exit; /* Process exit status. */ 46 | } 47 | """ 48 | 49 | 50 | class Timeval(MemCStruct): 51 | __def__ = """ 52 | struct { 53 | int32_t tv_sec; /* Seconds. */ 54 | int32_t tv_usec; /* Microseconds. */ 55 | } 56 | """ 57 | 58 | 59 | def str_from_c(string): 60 | return string.decode().split("\0")[0] 61 | 62 | 63 | class Utmp(MemCStruct): 64 | __byte_order__ = NATIVE_ORDER 65 | __def__ = """ 66 | typedef struct ExitStatus ExitStatus; 67 | 68 | struct { 69 | short ut_type; /* Type of record */ 70 | pid_t ut_pid; /* PID of login process */ 71 | char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ 72 | char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ 73 | char ut_user[UT_NAMESIZE]; /* Username */ 74 | char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ 75 | ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ 76 | int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ 77 | struct { 78 | int32_t tv_sec; /* Seconds */ 79 | int32_t tv_usec; /* Microseconds */ 80 | } ut_tv; /* Time entry was made */ 81 | int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ 82 | char __unused[20]; /* Reserved for future use */ 83 | } 84 | """ 85 | 86 | @property 87 | def user(self): 88 | return str_from_c(self.ut_user) 89 | 90 | @property 91 | def line(self): 92 | return str_from_c(self.ut_line) 93 | 94 | @property 95 | def time(self): 96 | return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ut_tv.tv_sec)) 97 | 98 | @property 99 | def host(self): 100 | if str_from_c(self.ut_host): 101 | host = str_from_c(self.ut_host) 102 | return f"({host})" 103 | elif self.ut_id: 104 | ut_id = str_from_c(self.ut_id) 105 | return f"id={ut_id}" 106 | else: 107 | return "" 108 | 109 | def __str__(self): 110 | return f"{self.user:<10s} {self.line:<12s} {self.time:<15s} {self.ut_pid:>15} {self.host:<8s}" 111 | 112 | def print_info(self, show_all): 113 | if show_all or self.ut_type in (getdef('LOGIN_PROCESS'), getdef('USER_PROCESS')): 114 | print(self) 115 | 116 | 117 | def main(): 118 | parser = argparse.ArgumentParser(description="Print information about users who are currently logged in.") 119 | parser.add_argument("-a", "--all", action="store_true", dest="show_all", help="show all enties") 120 | parser.add_argument("file", nargs="?", help="if FILE is not specified use /var/run/utmp", default=DEFAULT_FILENAME) 121 | args = parser.parse_args() 122 | 123 | utmp = Utmp() 124 | try: 125 | with Path(args.file).open("rb") as f: 126 | while utmp.unpack(f): 127 | utmp.print_info(args.show_all) 128 | except (IOError, OSError) as ex: 129 | print(ex) 130 | sys.exit(1) 131 | 132 | 133 | if __name__ == "__main__": 134 | main() 135 | -------------------------------------------------------------------------------- /examples/who.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd "$(dirname "$0")/.." || exit 3 | python -m examples.who $* 4 | -------------------------------------------------------------------------------- /mbr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreax79/python-cstruct/322424e541f2b1de5eb30de5fca6c7df9570781f/mbr -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python CStruct documentation 2 | docs_dir: docs 3 | 4 | theme: 5 | name: readthedocs 6 | highlightjs: true 7 | 8 | plugins: 9 | - search 10 | - autorefs 11 | - mkdocstrings: 12 | watch: 13 | - cstruct 14 | - examples 15 | 16 | nav: 17 | - Introduction: index.md 18 | - Changelog: changelog.md 19 | - License: license.md 20 | - Code of Conduct: CODE_OF_CONDUCT.md 21 | - Source Code Repository: "https://github.com/andreax79/python-cstruct" 22 | - Examples: 23 | - "dir.py": examples/dir.md 24 | - "fdisk.py": examples/fdisk.md 25 | - "flexible_array.py": examples/flexible_array.md 26 | - "who.py": examples/who.md 27 | - API: 28 | - "cstruct": api/module.md 29 | - "cstruct.abstract": api/abstract.md 30 | - "cstruct.base": api/base.md 31 | - "cstruct.c_expr": api/c_expr.md 32 | - "cstruct.c_parser": api/c_parser.md 33 | - "cstruct.cstruct": api/cstruct.md 34 | - "cstruct.field": api/field.md 35 | - "cstruct.mem_cstruct": api/mem_cstruct.md 36 | - "cstruct.native_types": api/native_types.md 37 | 38 | markdown_extensions: 39 | - markdown_include.include: 40 | base_path: . 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 132 7 | 8 | [tool.coverage.run] 9 | source = ["cstruct"] 10 | 11 | [tool.coverage.report] 12 | exclude_lines = [ "# pragma: no cover", "if TYPE_CHECKING:" ] 13 | 14 | [tool.isort] 15 | profile = "black" 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | build 3 | codespell 4 | coverage[toml] 5 | flake8 6 | isort 7 | markdown_include 8 | mkdocs 9 | mkdocs-material 10 | mkdocstrings[python] 11 | mypy 12 | pytest 13 | pytest-cov 14 | setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability 15 | tox 16 | twine<3.4 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreax79/python-cstruct/322424e541f2b1de5eb30de5fca6c7df9570781f/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = cstruct 3 | version = attr: cstruct.__version__ 4 | keywords = struct, cstruct, enum, binary, pack, unpack 5 | description = C-style structs for Python 6 | author = Andrea Bonomi 7 | author_email = andrea.bonomi@gmail.com 8 | url = http://github.com/andreax79/python-cstruct 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | license = MIT 12 | license_files = LICENSE 13 | platforms = any 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Environment :: Console 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: MIT License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Topic :: Software Development :: Libraries :: Python Modules 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3.12 29 | Programming Language :: Python :: 3.13 30 | project_urls = 31 | Bug Tracker = http://github.com/andreax79/python-cstruct/issues 32 | Documentation = https://python-cstruct.readthedocs.io/en/latest/ 33 | Source Code = http://github.com/andreax79/python-cstruct 34 | 35 | [options] 36 | zip_safe = True 37 | include_package_data = True 38 | python_requires = >=3.6 39 | packages = find: 40 | 41 | [options.packages.find] 42 | include = cstruct* 43 | exclude = 44 | ez_setup 45 | examples 46 | tests 47 | 48 | [options.extras_require] 49 | test = pytest 50 | 51 | [aliases] 52 | test = pytest 53 | 54 | [bdist_wheel] 55 | universal = 1 56 | 57 | [flake8] 58 | max-line-length = 132 59 | extend-ignore = 60 | E203 61 | E401 62 | W504 63 | E221 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import setuptools 3 | 4 | if __name__ == "__main__": 5 | setuptools.setup() 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreax79/python-cstruct/322424e541f2b1de5eb30de5fca6c7df9570781f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_alignment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import sys 29 | 30 | from cstruct import NATIVE_ORDER, CStruct, define, sizeof, typedef 31 | 32 | IS_64BITS = sys.maxsize > 2**32 33 | 34 | define("UT_NAMESIZE", 32) 35 | define("UT_LINESIZE", 32) 36 | define("UT_HOSTSIZE", 256) 37 | 38 | typedef("int", "pid_t") 39 | typedef("long", "time_t") 40 | 41 | 42 | class ExitStatus(CStruct): 43 | __def__ = """ 44 | struct { 45 | short e_termination; /* Process termination status. */ 46 | short e_exit; /* Process exit status. */ 47 | } 48 | """ 49 | 50 | 51 | class Timeval(CStruct): 52 | __def__ = """ 53 | struct { 54 | int32_t tv_sec; /* Seconds. */ 55 | int32_t tv_usec; /* Microseconds. */ 56 | } 57 | """ 58 | 59 | 60 | class Utmp(CStruct): 61 | __byte_order__ = NATIVE_ORDER 62 | __def__ = """ 63 | struct { 64 | short ut_type; /* Type of record */ 65 | pid_t ut_pid; /* PID of login process */ 66 | char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ 67 | char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ 68 | char ut_user[UT_NAMESIZE]; /* Username */ 69 | char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ 70 | struct ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ 71 | int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ 72 | struct { 73 | int32_t tv_sec; /* Seconds */ 74 | int32_t tv_usec; /* Microseconds */ 75 | } ut_tv; /* Time entry was made */ 76 | int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ 77 | char __unused[20]; /* Reserved for future use */ 78 | } 79 | """ 80 | 81 | 82 | class AllTypes(CStruct): 83 | __byte_order__ = NATIVE_ORDER 84 | __def__ = """ 85 | struct { 86 | char x00; 87 | signed char x01; 88 | unsigned char x02; 89 | short x03; 90 | short int x04; 91 | ushort x05; 92 | unsigned short x06; 93 | unsigned short int x07; 94 | int x08; 95 | unsigned int x09; 96 | long x10; 97 | long int x11; 98 | unsigned long x12; 99 | unsigned long int x13; 100 | long long x14; 101 | unsigned long long x15; 102 | float x16; 103 | double x17; 104 | void *x18; 105 | int8 x19; 106 | int8_t x20; 107 | uint8 x21; 108 | uint8_t x22; 109 | int16 x23; 110 | int16_t x24; 111 | uint16 x25; 112 | uint16_t x26; 113 | int32 x27; 114 | int32_t x28; 115 | uint32 x29; 116 | uint32_t x30; 117 | int64 x31; 118 | int64_t x32; 119 | uint64 x33; 120 | uint64_t x34; 121 | } 122 | """ 123 | 124 | 125 | class Foo1(CStruct): 126 | __byte_order__ = NATIVE_ORDER 127 | __def__ = """ 128 | struct { 129 | long p; 130 | char c; 131 | long x; 132 | } 133 | """ 134 | 135 | 136 | class Foo2(CStruct): 137 | __byte_order__ = NATIVE_ORDER 138 | __def__ = """ 139 | struct { 140 | char c; 141 | char pad[7]; 142 | long p; 143 | long x; 144 | } 145 | """ 146 | 147 | 148 | class Foo3(CStruct): 149 | __byte_order__ = NATIVE_ORDER 150 | __def__ = """ 151 | struct { 152 | long p; 153 | char c; 154 | } 155 | """ 156 | 157 | 158 | class Foo4(CStruct): 159 | __byte_order__ = NATIVE_ORDER 160 | __def__ = """ 161 | struct { 162 | short s; 163 | char c; 164 | } 165 | """ 166 | 167 | 168 | class Foo5(CStruct): 169 | __byte_order__ = NATIVE_ORDER 170 | __def__ = """ 171 | struct { 172 | char c; 173 | struct foo5_inner { 174 | long p; 175 | short x; 176 | } inner; 177 | } 178 | """ 179 | 180 | 181 | class Foo10(CStruct): 182 | __byte_order__ = NATIVE_ORDER 183 | __def__ = """ 184 | struct { 185 | char c; 186 | long p; 187 | short s; 188 | } 189 | """ 190 | 191 | 192 | def test_utmp_sizeof(): 193 | assert Utmp.__fields_types__["ut_type"].padding == 0 194 | assert Utmp.__fields_types__["ut_pid"].padding == 2 195 | assert sizeof("struct Utmp") == 384 196 | assert Utmp().size == 384 197 | 198 | 199 | # http://www.catb.org/esr/structure-packing/ 200 | 201 | 202 | def test_foo1_sizeof(): 203 | if IS_64BITS: 204 | assert Foo1.__fields_types__["p"].padding == 0 205 | assert Foo1.__fields_types__["c"].padding == 0 206 | assert Foo1.__fields_types__["x"].padding == 7 207 | assert sizeof("struct Foo1") == 24 208 | assert Foo1().size == 24 209 | else: 210 | assert Foo1.__fields_types__["p"].padding == 0 211 | assert Foo1.__fields_types__["c"].padding == 0 212 | assert Foo1.__fields_types__["x"].padding == 3 213 | assert sizeof("struct Foo1") == 12 214 | assert Foo1().size == 12 215 | 216 | 217 | def test_foo2_sizeof(): 218 | if IS_64BITS: 219 | assert sizeof("struct Foo2") == 24 220 | assert Foo2().size == 24 221 | else: 222 | assert sizeof("struct Foo2") == 16 223 | assert Foo2().size == 16 224 | 225 | 226 | def test_foo3_sizeof(): 227 | if IS_64BITS: 228 | assert sizeof("struct Foo3") == 16 229 | assert Foo3().size == 16 230 | else: 231 | assert sizeof("struct Foo3") == 8 232 | assert Foo3().size == 8 233 | 234 | 235 | def test_foo4_sizeof(): 236 | assert sizeof("struct Foo4") == 4 237 | assert Foo4().size == 4 238 | 239 | 240 | def test_foo5_sizeof(): 241 | if IS_64BITS: 242 | assert Foo5.__fields_types__["c"].padding == 0 243 | assert Foo5.__fields_types__["inner"].padding == 7 244 | assert sizeof("struct Foo5") == 24 245 | assert Foo5().size == 24 246 | else: 247 | assert Foo5.__fields_types__["c"].padding == 0 248 | assert Foo5.__fields_types__["inner"].padding == 3 249 | assert sizeof("struct Foo5") == 12 250 | assert Foo5().size == 12 251 | 252 | 253 | def test_foo10_sizeof(): 254 | if IS_64BITS: 255 | assert Foo10.__fields_types__["c"].padding == 0 256 | assert Foo10.__fields_types__["p"].padding == 7 257 | assert Foo10.__fields_types__["s"].padding == 0 258 | assert sizeof("struct Foo10") == 24 259 | assert Foo10().size == 24 260 | else: 261 | assert Foo10.__fields_types__["c"].padding == 0 262 | assert Foo10.__fields_types__["p"].padding == 3 263 | assert Foo10.__fields_types__["s"].padding == 0 264 | assert sizeof("struct Foo10") == 12 265 | assert Foo10().size == 12 266 | -------------------------------------------------------------------------------- /tests/test_c_expr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | from cstruct import define, getdef, parse 29 | from cstruct.c_expr import c_eval 30 | 31 | 32 | def test_c_expr_def(): 33 | parse( 34 | """ 35 | #define A1 10 /* test */ 36 | #define A2 10 + A1 /* comment */ 37 | #define A3 30 38 | """ 39 | ) 40 | assert getdef("A1") == 10 41 | assert getdef("A2") == 20 # TODO 42 | assert c_eval("A1 / 10") == 1 43 | 44 | 45 | def test_c_expr_binary(): 46 | assert c_eval("6*2/( 2+1 * 2/3 +6) +8 * (8/4)") == 17 47 | assert c_eval("6*2/(2+2/3 + 6) + 8 * (8/4)") == 17 48 | assert c_eval("6*2/(2+0+6) + 8 * (8/4)") == 17 49 | assert c_eval("((1 + 2) + (3 - 4)) * 10 / 5") == 4 50 | assert c_eval("12 % 2") == 0 51 | assert c_eval("64 >> 2") == 16 52 | assert c_eval("3 & 2") == 2 53 | assert c_eval("3 | 2") == 3 54 | 55 | 56 | def test_c_expr_bool(): 57 | assert c_eval("3 && 2") == 1 58 | assert c_eval("3 && 2 && 1") == 1 59 | assert c_eval("3 || 2") == 1 60 | 61 | 62 | def test_c_expr_unary(): 63 | assert c_eval("16 << 2") == 64 64 | assert c_eval("+123") == 123 65 | assert c_eval("-123") == -123 66 | assert c_eval("!100") == -0 67 | assert c_eval("!0") == 1 68 | assert c_eval("~0") == -1 69 | assert c_eval("~1") == -2 70 | 71 | 72 | def test_c_expr_compare(): 73 | assert c_eval("1 == 2") == 0 74 | assert c_eval("5 == 5") == 1 75 | assert c_eval("1 < 2 <= 3") == 1 76 | assert c_eval("3 > 2 > 1") == 1 77 | assert c_eval("3 >= 30") == 0 78 | assert c_eval("3 <= 30") == 1 79 | define("A10", 10) 80 | assert c_eval("((A10 < 6) || (A10>10))") == 0 81 | 82 | 83 | def test_c_expr_char(): 84 | assert c_eval("'A'") == 65 85 | assert c_eval("'B'") == 66 86 | -------------------------------------------------------------------------------- /tests/test_cenum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import pytest 4 | 5 | import cstruct 6 | from cstruct import CEnum 7 | 8 | 9 | class Dummy(CEnum): 10 | __enum__ = """ 11 | #define SOME_DEFINE 7 12 | 13 | A, 14 | B, 15 | C = 2, 16 | D = 5 + SOME_DEFINE, 17 | E = 2 18 | """ 19 | 20 | 21 | class HtmlFont(CEnum): 22 | __size__ = 2 23 | __def__ = """ 24 | #define NONE 0 25 | 26 | enum HtmlFont { 27 | HTMLFONT_NONE = NONE, 28 | HTMLFONT_BOLD, 29 | HTMLFONT_ITALIC, 30 | }; 31 | """ 32 | 33 | 34 | class EnumWithType(CEnum): 35 | __def__ = """ 36 | enum EnumWithType : int { a, b, c, d}; 37 | """ 38 | 39 | 40 | class StructWithEnum(cstruct.MemCStruct): 41 | __byte_order__ = cstruct.LITTLE_ENDIAN 42 | __def__ = """ 43 | struct StructWithEnum { 44 | enum HtmlFont font; 45 | unsigned int font_size; 46 | } 47 | """ 48 | 49 | 50 | def test_dummy(): 51 | assert Dummy.A == 0 52 | assert Dummy.B == 1 53 | assert Dummy.C == 2 54 | assert Dummy.D == 12 55 | assert Dummy.E == 2 56 | 57 | 58 | def test_missing_attribute(): 59 | with pytest.raises(AttributeError): 60 | assert Dummy.F 61 | 62 | 63 | def test_set_attribute(): 64 | with pytest.raises(AttributeError): 65 | Dummy.A = 100 66 | 67 | 68 | def test_html_font(): 69 | assert HtmlFont.HTMLFONT_NONE == 0 70 | assert HtmlFont.HTMLFONT_BOLD == 1 71 | assert HtmlFont.HTMLFONT_ITALIC == 2 72 | 73 | 74 | def test_parse_enum(): 75 | reply_stat = cstruct.parse("enum reply_stat { MSG_ACCEPTED=0, MSG_DENIED=1 }") 76 | assert isinstance(reply_stat.MSG_ACCEPTED, Enum) 77 | assert isinstance(reply_stat.MSG_DENIED, Enum) 78 | assert reply_stat.MSG_ACCEPTED == 0 79 | assert reply_stat.MSG_DENIED == 1 80 | 81 | 82 | def test_struct_with_enum(): 83 | s = StructWithEnum() 84 | s.font = HtmlFont.HTMLFONT_BOLD 85 | s.font_size = 11 86 | assert s.font == HtmlFont.HTMLFONT_BOLD 87 | assert s.font_size == 11 88 | 89 | s.font = HtmlFont.HTMLFONT_NONE 90 | s.font_size = 20 91 | assert s.font == HtmlFont.HTMLFONT_NONE 92 | assert s.font_size == 20 93 | packed = s.pack() 94 | 95 | s1 = StructWithEnum() 96 | s1.unpack(packed) 97 | assert s1.font == HtmlFont.HTMLFONT_NONE 98 | 99 | 100 | def test_sizeof(): 101 | assert cstruct.sizeof("enum Dummy") == 4 102 | assert cstruct.sizeof("enum HtmlFont") == 2 103 | 104 | 105 | def test_type(): 106 | color = cstruct.parse("enum Color : unsigned short { red, green, blue };") 107 | assert color.__size__ == 2 108 | assert cstruct.sizeof("enum Color") == 2 109 | 110 | 111 | def test_char(): 112 | direction = cstruct.parse("enum Direction { left = 'l', right = 'r' };") 113 | assert direction.__size__ == 4 114 | assert cstruct.sizeof("enum Direction") == 4 115 | -------------------------------------------------------------------------------- /tests/test_cstruct.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import io 29 | import os 30 | from pathlib import Path 31 | 32 | import pytest 33 | 34 | import cstruct 35 | from cstruct import sizeof, typedef 36 | from cstruct.exceptions import ParserError 37 | 38 | MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() 39 | 40 | 41 | class Position(cstruct.CStruct): 42 | __byte_order__ = cstruct.LITTLE_ENDIAN 43 | __def__ = """ 44 | struct { 45 | unsigned char head; 46 | unsigned char sector; 47 | unsigned char cyl; 48 | } 49 | """ 50 | 51 | 52 | class Partition(cstruct.CStruct): 53 | __byte_order__ = cstruct.LITTLE_ENDIAN 54 | __def__ = """ 55 | struct { 56 | unsigned char status; /* 0x80 - active */ 57 | struct Position start; 58 | unsigned char partition_type; 59 | struct Position end; 60 | unsigned int start_sect; /* starting sector counting from 0 */ 61 | unsigned int sectors; // nr of sectors in partition 62 | } 63 | """ 64 | 65 | 66 | class MBR(cstruct.CStruct): 67 | __byte_order__ = cstruct.LITTLE_ENDIAN 68 | __def__ = """ 69 | struct { 70 | char unused[440]; 71 | unsigned char disk_signature[4]; 72 | unsigned char usualy_nulls[2]; 73 | struct Partition partitions[4]; 74 | char signature[2]; 75 | } 76 | """ 77 | 78 | 79 | class Dummy(cstruct.CStruct): 80 | __byte_order__ = cstruct.LITTLE_ENDIAN 81 | __def__ = """ 82 | struct { 83 | struct { 84 | int i; 85 | } s; 86 | char c; 87 | char vc[10]; 88 | int vi[10]; 89 | long long l; 90 | long vl[10]; 91 | float f; 92 | float vf[10]; 93 | } 94 | """ 95 | 96 | 97 | typedef("char", "BYTE") 98 | typedef("short", "WORD") 99 | typedef("int", "DWORD") 100 | 101 | 102 | class PartitionFlat(cstruct.CStruct): 103 | __byte_order__ = cstruct.LITTLE_ENDIAN 104 | __def__ = """ 105 | struct { 106 | BYTE status; // 0x80 for bootable, 0x00 for not bootable 107 | BYTE startAddrHead; // head address of start of partition 108 | WORD startAddrCylSec; 109 | BYTE partType; 110 | BYTE endAddrHead; // head address of start of partition 111 | WORD endAddrCylSec; 112 | DWORD startLBA; // address of first sector in partition 113 | DWORD endLBA; // address of last sector in partition 114 | } 115 | """ 116 | 117 | 118 | class PartitionNested(cstruct.CStruct): 119 | __byte_order__ = cstruct.LITTLE_ENDIAN 120 | __def__ = """ 121 | struct { 122 | BYTE status; // 0x80 for bootable, 0x00 for not bootable 123 | struct { 124 | BYTE addrHead; // head address of start of partition 125 | WORD addrCylSec; 126 | } start; 127 | BYTE partType; 128 | struct b { 129 | BYTE addrHead; // head address of start of partition 130 | WORD addrCylSec; 131 | } end; 132 | DWORD startLBA; // address of first sector in partition 133 | DWORD endLBA; // address of last sector in partition 134 | } 135 | """ 136 | 137 | 138 | def test_len(): 139 | mbr = MBR() 140 | assert len(mbr) == 512 141 | assert mbr.size == 512 142 | 143 | 144 | def test_pack_len(): 145 | buffer = b"\x00" * 512 146 | mbr = MBR(buffer) 147 | d = mbr.pack() 148 | assert len(d) == 512 149 | mbr = MBR() 150 | mbr.unpack(MBR_DATA) 151 | d = mbr.pack() 152 | assert len(d) == 512 153 | mbr = MBR() 154 | d = mbr.pack() 155 | assert len(d) == 512 156 | 157 | 158 | def test_sizeof(): 159 | assert sizeof("struct Partition") == sizeof("struct PartitionFlat") 160 | assert sizeof("struct Partition") == sizeof("struct PartitionNested") 161 | 162 | 163 | def test_unpack(): 164 | mbr = MBR() 165 | f = io.BytesIO(MBR_DATA) 166 | mbr.unpack(f) 167 | assert mbr.signature[0] == 0x55 168 | assert mbr.signature[1] == 0xAA 169 | assert mbr.partitions[0].start.head == 0 170 | assert mbr.partitions[0].end.head == 0xFE 171 | assert mbr.partitions[1].start_sect == 0x2117C7 172 | 173 | 174 | def test_pack(): 175 | mbr = MBR(MBR_DATA) 176 | d = mbr.pack() 177 | assert MBR_DATA == d 178 | mbr.partitions[3].start.head = 123 179 | d1 = mbr.pack() 180 | mbr1 = MBR(d1) 181 | assert mbr1.partitions[3].start.head == 123 182 | 183 | 184 | def test_init(): 185 | p = Position(head=254, sector=63, cyl=134) 186 | mbr = MBR(MBR_DATA) 187 | assert mbr.partitions[0].end.head == p.head 188 | assert mbr.partitions[0].end.sector == p.sector 189 | assert mbr.partitions[0].end.cyl == p.cyl 190 | 191 | 192 | def test_none(): 193 | mbr = MBR() 194 | assert mbr.partitions[0].end.sector == 0 195 | mbr.unpack(None) 196 | assert mbr.partitions[0].end.head == 0 197 | 198 | 199 | def test_clear(): 200 | mbr = MBR() 201 | mbr.unpack(MBR_DATA) 202 | assert mbr.partitions[0].end.head == 0xFE 203 | mbr.clear() 204 | assert mbr.partitions[0].end.head == 0x00 205 | 206 | 207 | def test_inline(): 208 | StructT1 = cstruct.parse( 209 | "struct StructT1 { unsigned char head; unsigned char sector; unsigned char cyl; }", 210 | __byte_order__=cstruct.LITTLE_ENDIAN, 211 | ) 212 | s = StructT1(head=254, sector=63, cyl=134) 213 | p = Position(head=254, sector=63, cyl=134) 214 | assert s.pack() == p.pack() 215 | 216 | 217 | def test_dummy(): 218 | dummy = Dummy() 219 | 220 | dummy.c = b"A" 221 | dummy.vc = b"ABCDEFGHIJ" 222 | dummy.i = 123456 223 | for i in range(0, 10): 224 | dummy.vi[i] = i * 10 225 | dummy.f = 123.456 226 | for i in range(0, 10): 227 | dummy.vf[i] = 10.0 / (i + 1) 228 | dummy.vl = list(range(0, 10)) 229 | data = dummy.pack() 230 | dummy1 = Dummy(data) 231 | for i in range(0, 10): 232 | dummy1.vl[i] = dummy.vl[i] 233 | assert dummy.pack() == dummy1.pack() 234 | dummy2 = Dummy(data) 235 | dummy2.vf[2] = 79 236 | assert dummy.pack() != dummy2.pack() 237 | dummy3 = Dummy(data) 238 | dummy3.vl = list(range(1, 11)) 239 | assert dummy.pack() != dummy3.pack() 240 | 241 | 242 | def test_nested(): 243 | data = os.urandom(sizeof("struct PartitionFlat")) 244 | flat = PartitionFlat(data) 245 | flat.unpack(data) 246 | nested = PartitionNested(data) 247 | nested.unpack(data) 248 | assert flat.status == nested.status 249 | assert flat.startAddrHead == nested.start.addrHead 250 | assert flat.startAddrCylSec == nested.start.addrCylSec 251 | assert flat.partType == nested.partType 252 | assert flat.endAddrHead == nested.end.addrHead 253 | assert flat.endAddrCylSec == nested.end.addrCylSec 254 | assert flat.startLBA == nested.startLBA 255 | assert flat.endLBA == nested.endLBA 256 | 257 | 258 | def test_null_compare(): 259 | c = Dummy() 260 | assert c is not None 261 | assert c != None 262 | 263 | 264 | def test_invalid_inline(): 265 | with pytest.raises(ParserError): 266 | cstruct.MemCStruct.parse("struct { unsigned char head; unsigned char head; }", __byte_order__=cstruct.LITTLE_ENDIAN) 267 | 268 | 269 | def test_invalid_inline_reserved(): 270 | with pytest.raises(ParserError): 271 | cstruct.CStruct.parse("struct { int size; }") 272 | -------------------------------------------------------------------------------- /tests/test_cstruct_var.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2025 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import cstruct 29 | from cstruct import sizeof 30 | from cstruct.c_expr import c_eval 31 | 32 | 33 | class Struct0(cstruct.CStruct): 34 | __byte_order__ = cstruct.LITTLE_ENDIAN 35 | __def__ = """ 36 | struct { 37 | uint16_t a; 38 | uint16_t b; 39 | char c[6]; 40 | } 41 | """ 42 | 43 | 44 | class Struct1(cstruct.CStruct): 45 | __byte_order__ = cstruct.LITTLE_ENDIAN 46 | __def__ = """ 47 | #define X1 6 48 | struct { 49 | uint16_t a; 50 | uint16_t b; 51 | char c[X1]; 52 | } 53 | """ 54 | 55 | 56 | class Struct2(cstruct.CStruct): 57 | c_len: int = 6 58 | 59 | __byte_order__ = cstruct.LITTLE_ENDIAN 60 | __def__ = """ 61 | struct { 62 | uint16_t a; 63 | uint16_t b; 64 | char c[self.c_len]; 65 | } 66 | """ 67 | 68 | 69 | class Struct3(cstruct.MemCStruct): 70 | c_len: int = 0 71 | 72 | __byte_order__ = cstruct.LITTLE_ENDIAN 73 | __def__ = """ 74 | struct { 75 | uint16_t a; 76 | uint16_t b; 77 | uint16_t c[self.c_len]; 78 | } 79 | """ 80 | 81 | 82 | def test_v(): 83 | assert c_eval('10') == 10 84 | 85 | s0 = Struct0() 86 | assert len(s0) == 10 87 | s1 = Struct1() 88 | assert len(s1) == 10 89 | 90 | assert sizeof(Struct2) == 4 91 | 92 | s2 = Struct2() 93 | assert len(s2) == 10 94 | s2.c_len = 10 95 | assert len(s2) == 14 96 | 97 | for i in range(10): 98 | s2.c_len = i 99 | assert len(s2) == 4 + i 100 | 101 | assert sizeof(Struct3) == 4 102 | s3 = Struct3() 103 | assert len(s3) == 4 104 | 105 | for i in range(10): 106 | s3.c_len = i 107 | assert len(s3) == 4 + i * 2 108 | -------------------------------------------------------------------------------- /tests/test_define.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import sys 29 | 30 | import pytest 31 | 32 | import cstruct 33 | from cstruct import define, sizeof, typedef, undef 34 | from cstruct.exceptions import ParserError 35 | 36 | IS_64BITS = sys.maxsize > 2**32 37 | 38 | 39 | class Position(cstruct.CStruct): 40 | __byte_order__ = cstruct.LITTLE_ENDIAN 41 | __def__ = """ 42 | struct { 43 | unsigned char head; 44 | unsigned char sector; 45 | unsigned char cyl; 46 | } 47 | """ 48 | 49 | 50 | def test_sizeof(): 51 | assert sizeof("int") == 4 52 | define("INIT_THREAD_SIZE", 2048 * sizeof("long")) 53 | if IS_64BITS: 54 | assert cstruct.DEFINES["INIT_THREAD_SIZE"] == 16384 55 | else: 56 | assert cstruct.DEFINES["INIT_THREAD_SIZE"] == 8192 57 | assert sizeof("struct Position") == 3 58 | assert sizeof("struct Position") == len(Position) 59 | assert sizeof(Position) == 3 60 | with pytest.raises(KeyError): 61 | sizeof("bla") 62 | with pytest.raises(KeyError): 63 | sizeof("struct Bla") 64 | 65 | 66 | def test_define(): 67 | define("A", 10) 68 | assert cstruct.DEFINES["A"] == 10 69 | undef("A") 70 | with pytest.raises(KeyError): 71 | cstruct.DEFINES["A"] 72 | 73 | 74 | def test_typedef(): 75 | typedef("int", "integer") 76 | assert sizeof("integer") == 4 77 | 78 | 79 | def test_invalid_type(): 80 | with pytest.raises(ParserError): 81 | 82 | class Invalid(cstruct.CStruct): 83 | __def__ = """ 84 | struct { 85 | unsigned xxx yyy; 86 | } 87 | """ 88 | 89 | 90 | def test_invalid_define(): 91 | with pytest.raises(ParserError): 92 | cstruct.parse( 93 | """ 94 | #define xxx yyy zzz 95 | """ 96 | ) 97 | 98 | 99 | def test_invalid_struct(): 100 | with pytest.raises(ParserError): 101 | cstruct.parse( 102 | """ 103 | struct { 104 | int a; 105 | int; 106 | } 107 | """ 108 | ) 109 | -------------------------------------------------------------------------------- /tests/test_flexible_array.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import cstruct 29 | from cstruct import sizeof 30 | 31 | 32 | class Pkg(cstruct.CStruct): 33 | __byte_order__ = cstruct.LITTLE_ENDIAN 34 | __def__ = """ 35 | struct { 36 | uint16_t cmd; 37 | uint16_t length; 38 | uint8_t data[]; 39 | } 40 | """ 41 | 42 | 43 | class MemPkg(cstruct.MemCStruct): 44 | __byte_order__ = cstruct.LITTLE_ENDIAN 45 | __def__ = """ 46 | struct { 47 | uint16_t cmd; 48 | uint16_t length; 49 | uint8_t data[]; 50 | } 51 | """ 52 | 53 | 54 | def test_len(): 55 | pkg = Pkg() 56 | assert len(pkg) == sizeof("uint16_t") * 2 57 | assert len(pkg.pack()) 58 | assert len(pkg) == sizeof("uint16_t") * 2 59 | assert pkg.sizeof() == sizeof("uint16_t") * 2 60 | assert pkg.__size__ == sizeof("uint16_t") * 2 61 | 62 | pkg.length = 10 63 | pkg.data = list(range(pkg.length)) 64 | assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 65 | assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 66 | assert pkg.sizeof() == sizeof("uint16_t") * 2 67 | assert pkg.__size__ == sizeof("uint16_t") * 2 68 | 69 | pkg2 = Pkg() 70 | pkg2.length = 20 71 | pkg2.data = list(range(pkg2.length)) 72 | assert len(pkg2.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) 73 | assert len(pkg2) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) 74 | assert pkg2.sizeof() == sizeof("uint16_t") * 2 75 | assert pkg2.__size__ == sizeof("uint16_t") * 2 76 | assert len(pkg) != len(pkg2) 77 | 78 | 79 | def test_pack_unpack(): 80 | pkg = Pkg() 81 | pkg.cmd = 5 82 | pkg.length = 10 83 | assert pkg.__fields_types__["data"].vlen == 0 84 | assert pkg.__fields_types__["data"].vsize == 0 85 | assert len(pkg) == sizeof("uint16_t") * 2 86 | pkg.data = list(range(pkg.length)) 87 | assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 88 | assert pkg.__fields_types__["data"].vlen == pkg.length 89 | assert pkg.__fields_types__["data"].vsize == (sizeof("uint8_t") * pkg.length) 90 | assert len(pkg.data) == pkg.length 91 | data = pkg.pack() 92 | 93 | pkg2 = Pkg() 94 | assert pkg2.__fields_types__["data"].vlen == 0 95 | pkg2.unpack(data, flexible_array_length=pkg.length) 96 | assert pkg2.__fields_types__["data"].vlen == pkg2.length 97 | assert pkg2.cmd == pkg.cmd 98 | assert pkg2.length == pkg.length 99 | assert pkg2.data == pkg.data 100 | assert len(pkg2.data) == len(pkg.data) 101 | 102 | pkg3 = Pkg(data, flexible_array_length=pkg.length) 103 | assert pkg3.cmd == pkg.cmd 104 | assert pkg3.length == pkg.length 105 | assert len(pkg3.data) == len(pkg.data) 106 | assert pkg3.data == pkg.data 107 | 108 | 109 | def test_mem_len(): 110 | pkg = MemPkg() 111 | assert len(pkg) == sizeof("uint16_t") * 2 112 | assert len(pkg.pack()) 113 | assert len(pkg) == sizeof("uint16_t") * 2 114 | assert pkg.sizeof() == sizeof("uint16_t") * 2 115 | assert pkg.__size__ == sizeof("uint16_t") * 2 116 | 117 | pkg.length = 10 118 | pkg.data = list(range(pkg.length)) 119 | assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 120 | assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 121 | assert pkg.sizeof() == sizeof("uint16_t") * 2 122 | assert pkg.__size__ == sizeof("uint16_t") * 2 123 | 124 | pkg.length = 5 125 | pkg.data = list(range(pkg.length)) 126 | assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 127 | assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 128 | assert pkg.sizeof() == sizeof("uint16_t") * 2 129 | assert pkg.__size__ == sizeof("uint16_t") * 2 130 | 131 | pkg2 = MemPkg() 132 | pkg2.length = 20 133 | pkg2.data = list(range(pkg2.length)) 134 | assert len(pkg2.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) 135 | assert len(pkg2) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) 136 | assert pkg2.sizeof() == sizeof("uint16_t") * 2 137 | assert pkg2.__size__ == sizeof("uint16_t") * 2 138 | assert len(pkg) != len(pkg2) 139 | 140 | 141 | def test_mem_pack_unpack(): 142 | pkg = MemPkg() 143 | pkg.cmd = 5 144 | pkg.length = 10 145 | assert pkg.__fields_types__["data"].vlen == 0 146 | assert pkg.__fields_types__["data"].vsize == 0 147 | assert len(pkg) == sizeof("uint16_t") * 2 148 | pkg.data = list(range(pkg.length)) 149 | assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) 150 | assert pkg.__fields_types__["data"].vlen == pkg.length 151 | assert pkg.__fields_types__["data"].vsize == (sizeof("uint8_t") * pkg.length) 152 | assert len(pkg.data) == pkg.length 153 | data = pkg.pack() 154 | 155 | pkg2 = MemPkg() 156 | assert pkg2.__fields_types__["data"].vlen == 0 157 | pkg2.unpack(data, flexible_array_length=pkg.length) 158 | assert pkg2.__fields_types__["data"].vlen == pkg2.length 159 | assert pkg2.cmd == pkg.cmd 160 | assert pkg2.length == pkg.length 161 | assert pkg2.data == pkg.data 162 | assert len(pkg2.data) == len(pkg.data) 163 | 164 | pkg3 = MemPkg(data, flexible_array_length=pkg.length) 165 | assert pkg3.cmd == pkg.cmd 166 | assert pkg3.length == pkg.length 167 | assert len(pkg3.data) == len(pkg.data) 168 | assert pkg3.data == pkg.data 169 | -------------------------------------------------------------------------------- /tests/test_get_type.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import pytest 29 | 30 | import cstruct 31 | 32 | 33 | def test_get_type(): 34 | S2 = cstruct.parse( 35 | """ 36 | #define ACTIVE_FLAG 0x80 37 | 38 | struct S1 { 39 | unsigned char head; 40 | unsigned char sector; 41 | unsigned char cyl; 42 | }; 43 | typedef struct S1 S1; 44 | 45 | union U1 { 46 | unsigned int a; 47 | float b; 48 | }; 49 | typedef union U1 U1type; 50 | 51 | #define NONE 0 52 | 53 | enum htmlfont { 54 | HTMLFONT_NONE = NONE, 55 | HTMLFONT_BOLD, 56 | HTMLFONT_ITALIC, 57 | }; 58 | 59 | struct S2 { 60 | unsigned char status; /* 0x80 - active */ 61 | struct S1 start; 62 | unsigned char partition_type; 63 | S1 end; 64 | }; 65 | 66 | typedef struct S2 S2type; 67 | """ 68 | ) 69 | assert S2 70 | assert cstruct.get_type("struct S1") 71 | assert cstruct.get_type("S1") 72 | assert cstruct.get_type("union U1") 73 | assert cstruct.get_type("U1type") 74 | assert cstruct.get_type("enum htmlfont") 75 | assert cstruct.get_type("struct S2") 76 | assert cstruct.get_type("int") 77 | assert cstruct.get_type("unsigned int") 78 | assert cstruct.get_type("long long") 79 | with pytest.raises(KeyError): 80 | cstruct.get_type("struct X") 81 | with pytest.raises(KeyError): 82 | cstruct.get_type("U1") 83 | assert cstruct.sizeof("union U1") == max(cstruct.sizeof("unsigned int"), cstruct.sizeof("float")) 84 | -------------------------------------------------------------------------------- /tests/test_memcstruct.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import os 29 | from pathlib import Path 30 | 31 | import pytest 32 | 33 | import cstruct 34 | from cstruct import sizeof, typedef 35 | from cstruct.exceptions import ParserError 36 | 37 | MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() 38 | 39 | 40 | class Position(cstruct.MemCStruct): 41 | __byte_order__ = cstruct.LITTLE_ENDIAN 42 | __def__ = """ 43 | struct { 44 | unsigned char head; 45 | unsigned char sector; 46 | unsigned char cyl; 47 | } 48 | """ 49 | 50 | 51 | class Partition(cstruct.MemCStruct): 52 | __byte_order__ = cstruct.LITTLE_ENDIAN 53 | __def__ = """ 54 | struct { 55 | unsigned char status; /* 0x80 - active */ 56 | struct Position start; 57 | unsigned char partition_type; 58 | struct Position end; 59 | unsigned int start_sect; /* starting sector counting from 0 */ 60 | unsigned int sectors; // nr of sectors in partition 61 | } 62 | """ 63 | 64 | 65 | class MBR(cstruct.MemCStruct): 66 | __byte_order__ = cstruct.LITTLE_ENDIAN 67 | __def__ = """ 68 | struct { 69 | char unused[440]; 70 | unsigned char disk_signature[4]; 71 | unsigned char usualy_nulls[2]; 72 | struct Partition partitions[4]; 73 | char signature[2]; 74 | } 75 | """ 76 | 77 | 78 | class Dummy(cstruct.MemCStruct): 79 | __byte_order__ = cstruct.LITTLE_ENDIAN 80 | __def__ = """ 81 | struct { 82 | char c; 83 | char vc[10]; 84 | int i; 85 | int vi[10]; 86 | long long l; 87 | long vl[10]; 88 | float f; 89 | float vf[10]; 90 | } 91 | """ 92 | 93 | 94 | typedef("char", "BYTE") 95 | typedef("short", "WORD") 96 | typedef("int", "DWORD") 97 | 98 | 99 | class PartitionFlat(cstruct.MemCStruct): 100 | __byte_order__ = cstruct.LITTLE_ENDIAN 101 | __def__ = """ 102 | struct { 103 | BYTE status; // 0x80 for bootable, 0x00 for not bootable 104 | BYTE startAddrHead; // head address of start of partition 105 | WORD startAddrCylSec; 106 | BYTE partType; 107 | BYTE endAddrHead; // head address of start of partition 108 | WORD endAddrCylSec; 109 | DWORD startLBA; // address of first sector in partition 110 | DWORD endLBA; // address of last sector in partition 111 | } 112 | """ 113 | 114 | 115 | class PartitionNested(cstruct.MemCStruct): 116 | __byte_order__ = cstruct.LITTLE_ENDIAN 117 | __def__ = """ 118 | struct { 119 | BYTE status; // 0x80 for bootable, 0x00 for not bootable 120 | struct { 121 | BYTE addrHead; // head address of start of partition 122 | WORD addrCylSec; 123 | } start; 124 | BYTE partType; 125 | struct b { 126 | BYTE addrHead; // head address of start of partition 127 | WORD addrCylSec; 128 | } end; 129 | DWORD startLBA; // address of first sector in partition 130 | DWORD endLBA; // address of last sector in partition 131 | } 132 | """ 133 | 134 | 135 | def test_len(): 136 | mbr = MBR() 137 | assert len(mbr) == 512 138 | assert mbr.size == 512 139 | 140 | 141 | def test_pack_len(): 142 | buffer = b"\x00" * 512 143 | mbr = MBR(buffer) 144 | d = mbr.pack() 145 | assert len(d) == 512 146 | mbr = MBR() 147 | mbr.unpack(MBR_DATA) 148 | d = mbr.pack() 149 | assert len(d) == 512 150 | mbr = MBR() 151 | d = mbr.pack() 152 | assert len(d) == 512 153 | 154 | 155 | def test_sizeof(): 156 | assert sizeof("struct Partition") == sizeof("struct PartitionFlat") 157 | assert sizeof("struct Partition") == sizeof("struct PartitionNested") 158 | 159 | 160 | def test_unpack(): 161 | mbr = MBR() 162 | mbr.unpack(MBR_DATA) 163 | assert mbr.signature[0] == 0x55 164 | assert mbr.signature[1] == 0xAA 165 | assert mbr.partitions[0].start.head == 0 166 | assert mbr.partitions[0].end.head == 0xFE 167 | assert mbr.partitions[1].start_sect == 0x2117C7 168 | 169 | 170 | def test_pack(): 171 | mbr = MBR(MBR_DATA) 172 | d = mbr.pack() 173 | assert MBR_DATA == d 174 | mbr.partitions[3].start.head = 123 175 | d1 = mbr.pack() 176 | mbr1 = MBR(d1) 177 | assert mbr1.partitions[3].start.head == 123 178 | 179 | 180 | def test_init(): 181 | p = Position(head=254, sector=63, cyl=134) 182 | mbr = MBR(MBR_DATA) 183 | assert mbr.partitions[0].end.head == p.head 184 | assert mbr.partitions[0].end.sector == p.sector 185 | assert mbr.partitions[0].end.cyl == p.cyl 186 | 187 | 188 | def test_none(): 189 | mbr = MBR() 190 | assert mbr.partitions[0].end.sector == 0 191 | mbr.unpack(None) 192 | assert mbr.partitions[0].end.head == 0 193 | 194 | 195 | def test_clear(): 196 | mbr = MBR() 197 | mbr.unpack(MBR_DATA) 198 | assert mbr.partitions[0].end.head == 0xFE 199 | mbr.clear() 200 | assert mbr.partitions[0].end.head == 0x00 201 | 202 | 203 | def test_inline(): 204 | TestStruct = cstruct.MemCStruct.parse( 205 | "struct { unsigned char head; unsigned char sector; unsigned char cyl; }", __byte_order__=cstruct.LITTLE_ENDIAN 206 | ) 207 | s = TestStruct(head=254, sector=63, cyl=134) 208 | p = Position(head=254, sector=63, cyl=134) 209 | assert s.pack() == p.pack() 210 | 211 | 212 | def test_dummy(): 213 | dummy = Dummy() 214 | dummy.c = b"A" 215 | dummy.vc = b"ABCDEFGHIJ" 216 | dummy.i = 123456 217 | for i in range(0, 10): 218 | dummy.vi[i] = i * 10 219 | dummy.f = 123.456 220 | for i in range(0, 10): 221 | dummy.vf[i] = 10.0 / (i + 1) 222 | dummy.vl = list(range(0, 10)) 223 | data = dummy.pack() 224 | dummy1 = Dummy(data) 225 | for i in range(0, 10): 226 | dummy1.vl[i] = dummy.vl[i] 227 | assert dummy.pack() == dummy1.pack() 228 | dummy2 = Dummy(data) 229 | dummy2.vf[2] = 79 230 | assert dummy.pack() != dummy2.pack() 231 | dummy3 = Dummy(data) 232 | dummy3.vl = list(range(1, 11)) 233 | assert dummy.pack() != dummy3.pack() 234 | 235 | 236 | def test_nested(): 237 | data = os.urandom(sizeof("struct PartitionFlat")) 238 | flat = PartitionFlat(data) 239 | flat.unpack(data) 240 | nested = PartitionNested(data) 241 | nested.unpack(data) 242 | assert flat.status == nested.status 243 | assert flat.startAddrHead == nested.start.addrHead 244 | assert flat.startAddrCylSec == nested.start.addrCylSec 245 | assert flat.partType == nested.partType 246 | assert flat.endAddrHead == nested.end.addrHead 247 | assert flat.endAddrCylSec == nested.end.addrCylSec 248 | assert flat.startLBA == nested.startLBA 249 | assert flat.endLBA == nested.endLBA 250 | 251 | 252 | def test_null_compare(): 253 | c = Dummy() 254 | assert c is not None 255 | assert c != None 256 | 257 | 258 | def test_invalid_inline(): 259 | with pytest.raises(ParserError): 260 | cstruct.MemCStruct.parse("struct { unsigned char head; unsigned char head; }", __byte_order__=cstruct.LITTLE_ENDIAN) 261 | 262 | 263 | def test_invalid_inline_reserved(): 264 | with pytest.raises(ParserError): 265 | cstruct.MemCStruct.parse("struct { int size; }") 266 | -------------------------------------------------------------------------------- /tests/test_native_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | from cstruct import MemCStruct, sizeof 29 | from cstruct.base import TYPEDEFS 30 | from cstruct.native_types import AbstractNativeType 31 | 32 | 33 | class SizeT(AbstractNativeType): 34 | type_name = "size_t" 35 | native_format = "Q" 36 | 37 | 38 | class StructWithSizeT(MemCStruct): 39 | __def__ = """ 40 | typedef size_t size; 41 | typedef unsigned long long ull; 42 | 43 | struct StructWithSizeT { 44 | ull a; 45 | size_t b; 46 | size c; 47 | } 48 | """ 49 | 50 | 51 | def test_typedef(): 52 | assert TYPEDEFS["size"] == "size_t" 53 | assert TYPEDEFS["ull"] == "unsigned long long" 54 | 55 | 56 | def test_sizeof_custom_native_type(): 57 | assert sizeof("size_t") == 8 58 | assert sizeof("ull") == 8 59 | assert sizeof("struct StructWithSizeT") == 8 * 3 60 | -------------------------------------------------------------------------------- /tests/test_nested.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import pytest 29 | 30 | import cstruct 31 | from cstruct import sizeof 32 | from cstruct.exceptions import ParserError 33 | 34 | INVALID_ANONYMOUS = """ 35 | struct NestedStruct { 36 | struct { 37 | int a; 38 | int b; 39 | }; 40 | int a; 41 | int b; 42 | }; 43 | """ 44 | 45 | 46 | class NestedStruct(cstruct.MemCStruct): 47 | __byte_order__ = cstruct.LITTLE_ENDIAN 48 | __def__ = """ 49 | struct NestedStruct { 50 | struct { 51 | int a; 52 | int b; 53 | } s; 54 | int a; 55 | int b; 56 | }; 57 | """ 58 | 59 | 60 | class NestedUnion(cstruct.MemCStruct): 61 | __byte_order__ = cstruct.LITTLE_ENDIAN 62 | __def__ = """ 63 | union NestedUnion { 64 | struct { 65 | int a; 66 | int b; 67 | } s; 68 | int a; 69 | }; 70 | """ 71 | 72 | 73 | class NestedStructArr(cstruct.MemCStruct): 74 | __byte_order__ = cstruct.LITTLE_ENDIAN 75 | __def__ = """ 76 | struct NestedStructArr { 77 | struct b { 78 | int c; 79 | } bval[10]; 80 | }; 81 | """ 82 | 83 | 84 | class NestedAnonymousUnion(cstruct.MemCStruct): 85 | __byte_order__ = cstruct.LITTLE_ENDIAN 86 | __def__ = """ 87 | union NestedAnonymousUnion { 88 | struct { 89 | int a; 90 | int b; 91 | }; 92 | int c; 93 | }; 94 | """ 95 | 96 | 97 | class Packet(cstruct.MemCStruct): 98 | __byte_order__ = cstruct.LITTLE_ENDIAN 99 | __def__ = """ 100 | #define MaxPacket 20 101 | 102 | struct Packet { 103 | uint8_t packetLength; 104 | union { 105 | uint8_t bytes[MaxPacket]; 106 | struct { 107 | uint16_t field1; 108 | uint16_t field2; 109 | uint16_t field3; 110 | } format1; 111 | struct { 112 | double value1; 113 | double value2; 114 | } format2; 115 | }; 116 | }; 117 | """ 118 | 119 | 120 | def test_invalid_anonymous(): 121 | with pytest.raises(ParserError): 122 | cstruct.parse(INVALID_ANONYMOUS) 123 | assert True 124 | 125 | 126 | def test_sizeof_nested_struct(): 127 | assert sizeof("struct NestedStruct") == 16 128 | o = NestedStruct() 129 | assert len(o) == 16 130 | 131 | 132 | def test_pack_unpack_nested_struct(): 133 | o = NestedStruct() 134 | o.s.a = 1 135 | o.s.b = 2 136 | o.a = 10 137 | o.b = 20 138 | b = o.pack() 139 | o1 = NestedStruct() 140 | 141 | o1.unpack(b) 142 | assert o1.s.a == 1 143 | assert o1.s.b == 2 144 | assert o1.a == 10 145 | assert o1.b == 20 146 | 147 | 148 | def test_sizeof_nested_union(): 149 | assert sizeof("struct NestedUnion") == 8 150 | o = NestedUnion() 151 | assert len(o) == 8 152 | 153 | 154 | def test_pack_unpack_nested_union(): 155 | o = NestedUnion() 156 | o.s.a = 1 157 | o.s.b = 2 158 | b = o.pack() 159 | 160 | o1 = NestedUnion() 161 | o1.unpack(b) 162 | assert o1.s.a == 1 163 | assert o1.s.b == 2 164 | 165 | o = NestedUnion() 166 | o.a = 1979 167 | b = o.pack() 168 | 169 | o1 = NestedUnion() 170 | o1.unpack(b) 171 | assert o1.a == 1979 172 | o1.s.b = 0 173 | assert o1.a == 1979 174 | o1.s.a = 0 175 | assert o1.a == 0 176 | 177 | 178 | def test_sizeof_nested_anonymous_union(): 179 | assert sizeof("struct NestedAnonymousUnion") == 8 180 | o = NestedAnonymousUnion() 181 | assert len(o) == 8 182 | 183 | 184 | def test_pack_unpack_nested_anonymous_union(): 185 | o = NestedAnonymousUnion() 186 | o.a = 1 187 | o.b = 2 188 | b = o.pack() 189 | 190 | o1 = NestedAnonymousUnion() 191 | o1.unpack(b) 192 | assert o1.a == 1 193 | assert o1.b == 2 194 | 195 | o = NestedAnonymousUnion() 196 | o.c = 1979 197 | b = o.pack() 198 | 199 | o1 = NestedAnonymousUnion() 200 | o1.unpack(b) 201 | assert o1.c == 1979 202 | o1.b = 0 203 | assert o1.c == 1979 204 | o1.a = 0 205 | assert o1.c == 0 206 | 207 | 208 | def test_nested_anonymous_union_struct(): 209 | o = Packet() 210 | assert sizeof("struct Packet") == len(o) 211 | 212 | o = Packet() 213 | o.packetLength = 10 214 | o.format1.field1 = 11 215 | o.format1.field2 = 12 216 | o.format1.field3 = 13 217 | b = o.pack() 218 | 219 | o1 = Packet() 220 | o1.unpack(b) 221 | assert o1.format1.field1 == 11 222 | assert o1.format1.field2 == 12 223 | assert o1.format1.field3 == 13 224 | 225 | 226 | def test_nested_struct_offset(): 227 | cstruct.parse( 228 | """ 229 | struct op_a { 230 | int a; 231 | }; 232 | 233 | struct op_b { 234 | char a; 235 | char b; 236 | char c; 237 | }; 238 | """, 239 | __byte_order__=cstruct.LITTLE_ENDIAN, 240 | ) 241 | 242 | Op = cstruct.parse( 243 | """ 244 | struct op { 245 | char preamble[10]; 246 | uint64_t magic; 247 | union { 248 | struct op_a a_op; 249 | struct op_b b_op; 250 | } u1; 251 | struct op_a aaa; 252 | }; 253 | """, 254 | __byte_order__=cstruct.LITTLE_ENDIAN, 255 | ) 256 | 257 | o = Op() 258 | o.preamble = b'ciao_ciao' 259 | o.magic = 3771778641802345472 260 | o.u1.a_op.a = 2022 261 | o.aaa.a = 0x33333333 262 | assert o.u1.b_op.a == b'\xe6' 263 | assert o.u1.b_op.b == b'\x07' 264 | assert o.u1.b_op.c == b'\x00' 265 | assert o.__base__ == 0 266 | assert o.u1.__base__ >= 10 267 | assert o.u1.__base__ == o.u1.a_op.__base__ 268 | assert o.u1.__base__ == o.u1.b_op.__base__ 269 | assert o.aaa.__base__ > o.u1.__base__ 270 | assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' 271 | assert o.u1.pack() == b'\xe6\x07\x00\x00' 272 | assert o.aaa.pack() == b'3333' 273 | assert o.u1.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" 274 | assert o.u1.b_op.inspect() == "00000000 e6 07 00 |... |\n" 275 | 276 | 277 | def test_nested_anonymous_struct_offset(): 278 | cstruct.parse( 279 | """ 280 | struct op_a1 { 281 | int a; 282 | }; 283 | 284 | struct op_b1 { 285 | char a; 286 | char b; 287 | char c; 288 | }; 289 | """, 290 | __byte_order__=cstruct.LITTLE_ENDIAN, 291 | ) 292 | 293 | Opu = cstruct.parse( 294 | """ 295 | struct opu { 296 | char preamble[10]; 297 | uint64_t magic; 298 | union { 299 | struct op_a1 a_op; 300 | struct op_b1 b_op; 301 | }; 302 | struct op_a aaa; 303 | }; 304 | """, 305 | __byte_order__=cstruct.LITTLE_ENDIAN, 306 | ) 307 | 308 | o = Opu() 309 | o.preamble = b'ciao_ciao' 310 | o.magic = 3771778641802345472 311 | o.__anonymous0.a_op.a = 2022 312 | o.aaa.a = 0x33333333 313 | assert o.__anonymous0.b_op.a == b'\xe6' 314 | assert o.__anonymous0.b_op.b == b'\x07' 315 | assert o.__anonymous0.b_op.c == b'\x00' 316 | assert o.__base__ == 0 317 | assert o.__anonymous0.__base__ >= 10 318 | assert o.__anonymous0.__base__ == o.__anonymous0.a_op.__base__ 319 | assert o.__anonymous0.__base__ == o.__anonymous0.b_op.__base__ 320 | assert o.aaa.__base__ > o.__anonymous0.__base__ 321 | assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' 322 | assert o.__anonymous0.pack() == b'\xe6\x07\x00\x00' 323 | assert o.aaa.pack() == b'3333' 324 | assert o.__anonymous0.inspect() == "00000000 e6 07 00 00 |.... |\n" 325 | assert o.__anonymous0.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" 326 | assert o.__anonymous0.b_op.inspect() == "00000000 e6 07 00 |... |\n" 327 | 328 | o = Opu() 329 | o.preamble = b'ciao_ciao' 330 | o.magic = 3771778641802345472 331 | o.a_op.a = 2022 332 | o.aaa.a = 0x33333333 333 | assert o.b_op.a == b'\xe6' 334 | assert o.b_op.b == b'\x07' 335 | assert o.b_op.c == b'\x00' 336 | assert o.__base__ == 0 337 | assert o.__anonymous0.__base__ >= 10 338 | assert o.__anonymous0.__base__ == o.a_op.__base__ 339 | assert o.__anonymous0.__base__ == o.b_op.__base__ 340 | assert o.aaa.__base__ > o.__base__ 341 | assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' 342 | assert o.a_op.pack() == b'\xe6\x07\x00\x00' 343 | assert o.aaa.pack() == b'3333' 344 | assert o.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" 345 | assert o.b_op.inspect() == "00000000 e6 07 00 |... |\n" 346 | 347 | 348 | def test_nested_struct_array(): 349 | Nested = cstruct.parse( 350 | """ 351 | struct Nested { 352 | struct b { 353 | int c; 354 | } bval; 355 | int a; 356 | }; 357 | """, 358 | __byte_order__=cstruct.LITTLE_ENDIAN, 359 | ) 360 | assert len(Nested) == 8 361 | t = Nested() 362 | assert "a" in t.__fields_types__ 363 | assert "bval" in t.__fields_types__ 364 | 365 | NestedArray = cstruct.parse( 366 | """ 367 | struct NestedArray { 368 | struct b { 369 | int c; 370 | } bval[2]; 371 | int a; 372 | }; 373 | """, 374 | __byte_order__=cstruct.LITTLE_ENDIAN, 375 | ) 376 | t = NestedArray() 377 | assert "a" in t.__fields_types__ 378 | assert "bval" in t.__fields_types__ 379 | assert len(NestedArray) > len(Nested) 380 | t.bval[0].c = 10 381 | t.bval[1].c = 11 382 | assert t.bval[0].c == 10 383 | assert t.bval[1].c == 11 384 | assert len(t.bval) == 2 385 | 386 | assert len(NestedStructArr) == 40 387 | t = NestedStructArr() 388 | assert "bval" in t.__fields_types__ 389 | t.bval[0].c = 10 390 | t.bval[1].c = 11 391 | assert t.bval[0].c == 10 392 | assert t.bval[1].c == 11 393 | assert len(t.bval) == 10 394 | -------------------------------------------------------------------------------- /tests/test_padding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import cstruct 4 | from cstruct import LITTLE_ENDIAN, NATIVE_ORDER, sizeof 5 | 6 | DEFINITION = """ 7 | struct struct1 { 8 | char a[2]; 9 | uint32_t b; 10 | } 11 | """ 12 | 13 | 14 | class CStructNative(cstruct.CStruct): 15 | __byte_order__ = NATIVE_ORDER 16 | __def__ = DEFINITION 17 | 18 | 19 | class MemCStructNative(cstruct.MemCStruct): 20 | __byte_order__ = NATIVE_ORDER 21 | __def__ = DEFINITION 22 | 23 | 24 | class CStructLittleEndian(cstruct.CStruct): 25 | __byte_order__ = LITTLE_ENDIAN 26 | __def__ = DEFINITION 27 | 28 | 29 | class MemCStructLittleEndian(cstruct.MemCStruct): 30 | __byte_order__ = LITTLE_ENDIAN 31 | __def__ = DEFINITION 32 | 33 | 34 | def test_memcstruct_padding(): 35 | for struct in (CStructNative, MemCStructNative): 36 | data = b"\x41\x42\x00\x00\x01\x00\x00\x00" 37 | assert sizeof(struct) == 8 38 | t = struct() 39 | t.unpack(data) 40 | assert t.a == b"AB" 41 | assert t.b == 1 42 | buf = t.pack() 43 | assert len(buf) == sizeof(struct) 44 | assert buf == data 45 | 46 | t2 = struct() 47 | t2.unpack(buf) 48 | assert t2.a == b"AB" 49 | assert t2.b == 1 50 | buf = t.pack() 51 | assert len(buf) == sizeof(struct) 52 | 53 | 54 | def test_no_padding(): 55 | for struct in (CStructLittleEndian, MemCStructLittleEndian): 56 | data = b"\x41\x42\x01\x00\x00\x00" 57 | assert sizeof(struct) == 6 58 | t = struct() 59 | t.unpack(data) 60 | assert t.a == b"AB" 61 | assert t.b == 1 62 | buf = t.pack() 63 | assert len(buf) == sizeof(struct) 64 | assert buf == data 65 | 66 | t2 = struct() 67 | t2.unpack(buf) 68 | assert t2.a == b"AB" 69 | assert t2.b == 1 70 | buf = t.pack() 71 | assert len(buf) == sizeof(struct) 72 | -------------------------------------------------------------------------------- /tests/test_pickle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pickle 4 | 5 | import cstruct 6 | 7 | 8 | class Position(cstruct.CStruct): 9 | __byte_order__ = cstruct.LITTLE_ENDIAN 10 | __def__ = """ 11 | struct { 12 | unsigned char head; 13 | unsigned char sector; 14 | unsigned char cyl; 15 | } 16 | """ 17 | 18 | 19 | class MemPosition(cstruct.MemCStruct): 20 | __byte_order__ = cstruct.LITTLE_ENDIAN 21 | __def__ = """ 22 | struct { 23 | unsigned char head; 24 | unsigned char sector; 25 | unsigned char cyl; 26 | } 27 | """ 28 | 29 | 30 | def test_pickle(): 31 | # pickle an object 32 | original_pos = Position(head=254, sector=63, cyl=134) 33 | pickled_bytes = pickle.dumps(original_pos) 34 | 35 | # reconstitute a pickled object 36 | reconstituted_pos = pickle.loads(pickled_bytes) 37 | 38 | assert reconstituted_pos.head == original_pos.head 39 | assert reconstituted_pos.sector == original_pos.sector 40 | assert reconstituted_pos.cyl == original_pos.cyl 41 | 42 | 43 | def test_mem_pickle(): 44 | # pickle an object 45 | original_pos = MemPosition(head=254, sector=63, cyl=134) 46 | pickled_bytes = pickle.dumps(original_pos) 47 | 48 | # reconstitute a pickled object 49 | reconstituted_pos = pickle.loads(pickled_bytes) 50 | 51 | assert reconstituted_pos.head == original_pos.head 52 | assert reconstituted_pos.sector == original_pos.sector 53 | assert reconstituted_pos.cyl == original_pos.cyl 54 | -------------------------------------------------------------------------------- /tests/test_typdef.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | from cstruct import NATIVE_ORDER, MemCStruct 29 | from cstruct.base import TYPEDEFS 30 | 31 | 32 | class ExitStatus(MemCStruct): 33 | __def__ = """ 34 | struct ExitStatus { 35 | short e_termination; /* Process termination status. */ 36 | short e_exit; /* Process exit status. */ 37 | } 38 | """ 39 | 40 | 41 | class Utmp(MemCStruct): 42 | __byte_order__ = NATIVE_ORDER 43 | __def__ = """ 44 | #define UT_NAMESIZE 32 45 | #define UT_LINESIZE 32 46 | #define UT_HOSTSIZE 256 47 | 48 | typedef int pid_t; 49 | typedef long time_t; 50 | typedef unsigned long int ulong; 51 | typedef struct ExitStatus ExitStatus; 52 | 53 | struct { 54 | short ut_type; /* Type of record */ 55 | pid_t ut_pid; /* PID of login process */ 56 | char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ 57 | char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ 58 | char ut_user[UT_NAMESIZE]; /* Username */ 59 | char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ 60 | ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ 61 | int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ 62 | struct { 63 | int32_t tv_sec; /* Seconds */ 64 | int32_t tv_usec; /* Microseconds */ 65 | } ut_tv; /* Time entry was made */ 66 | int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ 67 | char __unused[20]; /* Reserved for future use */ 68 | } 69 | """ 70 | 71 | 72 | def test_typedef(): 73 | assert TYPEDEFS["pid_t"] == "int" 74 | assert TYPEDEFS["time_t"] == "long" 75 | assert TYPEDEFS["ulong"] == "unsigned long" 76 | assert TYPEDEFS["ExitStatus"] == "struct ExitStatus" 77 | -------------------------------------------------------------------------------- /tests/test_union.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ***************************************************************************** 3 | # 4 | # Copyright (c) 2013-2019 Andrea Bonomi 5 | # 6 | # Published under the terms of the MIT license. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to 10 | # deal in the Software without restriction, including without limitation the 11 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | # sell copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | # 26 | # ***************************************************************************** 27 | 28 | import struct 29 | 30 | import cstruct 31 | from cstruct import sizeof 32 | 33 | 34 | class Position(cstruct.MemCStruct): 35 | __byte_order__ = cstruct.LITTLE_ENDIAN 36 | __def__ = """ 37 | struct { 38 | unsigned char head; 39 | unsigned char sector; 40 | unsigned char cyl; 41 | } 42 | """ 43 | 44 | 45 | class Partition(cstruct.MemCStruct): 46 | __byte_order__ = cstruct.LITTLE_ENDIAN 47 | __def__ = """ 48 | struct { 49 | unsigned char status; /* 0x80 - active */ 50 | struct Position start; 51 | unsigned char partition_type; 52 | struct Position end; 53 | unsigned int start_sect; /* starting sector counting from 0 */ 54 | unsigned int sectors; // nr of sectors in partition 55 | } 56 | """ 57 | 58 | 59 | class UnionT1(cstruct.MemCStruct): 60 | __byte_order__ = cstruct.LITTLE_ENDIAN 61 | __def__ = """ 62 | union { 63 | uint8 a; 64 | uint8 a1; 65 | uint16 b; 66 | uint32 c; 67 | struct Partition d; 68 | struct Partition e[4]; 69 | } 70 | """ 71 | 72 | 73 | def test_sizeof(): 74 | assert sizeof("struct UnionT1") == 64 75 | s = UnionT1() 76 | assert len(s) == 64 77 | 78 | 79 | def test_union_unpack(): 80 | union = UnionT1() 81 | union.unpack(None) 82 | assert union.a == 0 83 | assert union.a1 == 0 84 | assert union.b == 0 85 | assert union.c == 0 86 | union.unpack(struct.pack("b", 10) + cstruct.CHAR_ZERO * union.size) 87 | assert union.a == 10 88 | assert union.a1 == 10 89 | assert union.b == 10 90 | assert union.c == 10 91 | union.unpack(struct.pack("h", 1979) + cstruct.CHAR_ZERO * union.size) 92 | assert union.a == 187 93 | assert union.a1 == 187 94 | assert union.b == 1979 95 | assert union.c == 1979 96 | print(union) 97 | union2 = UnionT1(union.pack()) 98 | print(union2) 99 | assert len(union) == len(union2) 100 | assert union2.a == 187 101 | assert union2.a1 == 187 102 | assert union2.b == 1979 103 | assert union2.c == 1979 104 | -------------------------------------------------------------------------------- /utmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreax79/python-cstruct/322424e541f2b1de5eb30de5fca6c7df9570781f/utmp --------------------------------------------------------------------------------