├── .gitignore ├── DEVELOPER.md ├── LICENSE.txt ├── README.md ├── appveyor.yml ├── doc ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── examples │ ├── examples.rst │ ├── simple_example.c │ ├── simple_example_read.py │ └── simple_example_write.py │ ├── faq.rst │ ├── index.rst │ ├── installation.rst │ ├── limitations.rst │ ├── overview.rst │ └── reference.rst ├── pycstruct ├── __init__.py ├── cparser.py ├── instance.py └── pycstruct.py ├── setup.cfg ├── setup.py ├── tests ├── bitfield_big.dat ├── bitfield_little.dat ├── bitfield_struct.c ├── bitfield_struct.dat ├── bitfield_struct.h ├── bitfield_struct.xml ├── buildtest.py ├── embedded_struct.c ├── embedded_struct.dat ├── embedded_struct.xml ├── embedded_struct_nopack.dat ├── embedded_struct_nopack.xml ├── ndim_array_struct.c ├── ndim_array_struct.dat ├── ndim_array_struct.h ├── ndim_array_struct.xml ├── savebitfield.c ├── savestruct.c ├── savestruct.xml ├── savestruct_nopack.xml ├── special_cases.h ├── special_cases.xml ├── struct_big.dat ├── struct_big_nopack.dat ├── struct_little.dat ├── struct_little_nopack.dat ├── test_cparser.py ├── test_dtype.py ├── test_instance.py └── test_pycstruct.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | __pycache__ 3 | *.out 4 | .vscode 5 | *.pyc 6 | .tox 7 | pycstruct.egg-info 8 | doc/build 9 | htmlcov 10 | build 11 | dist 12 | /.* 13 | *.py,cover 14 | -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Upload package to PyPi.org 2 | 3 | ## Install prereqs 4 | 5 | python3 -m pip install --user --upgrade setuptools wheel 6 | 7 | ## Create package 8 | 9 | Run in pycstruct root folder: 10 | 11 | python3 setup.py sdist bdist_wheel 12 | 13 | ## Upload package to PyPi.org 14 | 15 | python3 -m twine upload dist/* 16 | 17 | Ender username and password. 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joel Midstjärna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pycstruct 2 | 3 | [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/midstar/pycstruct?svg=true)](https://ci.appveyor.com/api/projects/status/github/midstar/pycstruct) 4 | [![Coverage Status](https://coveralls.io/repos/github/midstar/pycstruct/badge.svg?branch=HEAD)](https://coveralls.io/github/midstar/pycstruct?branch=HEAD) 5 | [![Documentation](https://readthedocs.org/projects/pycstruct/badge/?version=latest)](https://pycstruct.readthedocs.io/en/latest/?badge=latest) 6 | 7 | pycstruct is a python library for converting binary data to and from ordinary 8 | python dictionaries or specific instance objects. 9 | 10 | Data is defined similar to what is done in C language structs, unions, 11 | bitfields and enums. 12 | 13 | Typical usage of this library is read/write binary files or binary data 14 | transmitted over a network. 15 | 16 | Following complex C types are supported: 17 | 18 | - Structs 19 | - Unions 20 | - Bitfields 21 | - Enums 22 | 23 | These types may consist of any traditional data types (integer, unsigned integer, 24 | boolean and float) between 1 to 8 bytes large, arrays (lists), and strings (ASCII/UTF-8). 25 | 26 | Structs, unions, bitfields and enums can be embedded inside other structs/unions 27 | in any level. 28 | 29 | Individual elements can be stored / read in any byte order and alignment. 30 | 31 | pycstruct also supports parsing of existing C language source code to 32 | automatically generate the pycstruct definitions / instances. 33 | 34 | Checkout the full documentation [here](https://pycstruct.readthedocs.io/en/latest/). 35 | 36 | ## Installation 37 | 38 | Simply install the package using pip: 39 | 40 | python3 -m pip install pycstruct 41 | 42 | ## Example 43 | 44 | Following C has a structure (person) with a set of elements 45 | that are written to a binary file. 46 | 47 | ```c 48 | #include 49 | #include 50 | #include 51 | 52 | #pragma pack(1) // To secure no padding is added in struct 53 | 54 | struct person 55 | { 56 | char name[50]; 57 | unsigned int age; 58 | float height; 59 | bool is_male; 60 | unsigned int nbr_of_children; 61 | unsigned int child_ages[10]; 62 | }; 63 | 64 | 65 | void main(void) { 66 | struct person p; 67 | memset(&p, 0, sizeof(struct person)); 68 | 69 | strcpy(p.name, "Foo Bar"); 70 | p.age = 42; 71 | p.height = 1.75; // m 72 | p.is_male = true; 73 | p.nbr_of_children = 2; 74 | p.child_ages[0] = 7; 75 | p.child_ages[1] = 9; 76 | 77 | FILE *f = fopen("simple_example.dat", "w"); 78 | fwrite(&p, sizeof(struct person), 1, f); 79 | fclose(f); 80 | } 81 | ``` 82 | 83 | To read the binary file using pycstruct following code 84 | required. 85 | 86 | ```python 87 | import pycstruct 88 | 89 | person = pycstruct.StructDef() 90 | person.add('utf-8', 'name', length=50) 91 | person.add('uint32', 'age') 92 | person.add('float32','height') 93 | person.add('bool8', 'is_male') 94 | person.add('uint32', 'nbr_of_children') 95 | person.add('uint32', 'child_ages', length=10) 96 | 97 | with open('simple_example.dat', 'rb') as f: 98 | inbytes = f.read() 99 | 100 | # Dictionary representation 101 | result = person.deserialize(inbytes) 102 | print('Dictionary object:') 103 | print(str(result)) 104 | 105 | # Alternative, Instance representation 106 | instance = person.instance(inbytes) 107 | print('\nInstance object:') 108 | print(f'name: {instance.name}') 109 | print(f'nbr_of_children: {instance.nbr_of_children}') 110 | print(f'child_ages[1]: {instance.child_ages[1]}') 111 | ``` 112 | 113 | The produced output will be:: 114 | 115 | {'name': 'Foo Bar', 'is_male': True, 'nbr_of_children': 2, 116 | 'age': 42, 'child_ages': [7, 9, 0, 0, 0, 0, 0, 0, 0, 0], 117 | 'height': 1.75} 118 | 119 | Instance object: 120 | name: Foo Bar 121 | nbr_of_children: 2 122 | child_ages[1]: 9 123 | 124 | To write a binary file from python using the same structure 125 | using pycstruct following code is required. 126 | 127 | ```python 128 | import pycstruct 129 | 130 | person = pycstruct.StructDef() 131 | person.add('utf-8', 'name', length=50) 132 | person.add('uint32', 'age') 133 | person.add('float32','height') 134 | person.add('bool8', 'is_male') 135 | person.add('uint32', 'nbr_of_children') 136 | person.add('uint32', 'child_ages', length=10) 137 | 138 | # Dictionary representation 139 | mrGreen = {} 140 | mrGreen['name'] = "MR Green" 141 | mrGreen['age'] = 50 142 | mrGreen['height'] = 1.93 143 | mrGreen['is_male'] = True 144 | mrGreen['nbr_of_children'] = 3 145 | mrGreen['child_ages'] = [13,24,12] 146 | buffer = person.serialize(mrGreen) 147 | 148 | # Alternative, Instance representation 149 | mrGreen = person.instance() 150 | mrGreen.name = "MR Green" 151 | mrGreen.age = 50 152 | mrGreen.height = 1.93 153 | mrGreen.is_male = True 154 | mrGreen.nbr_of_children = 3 155 | mrGreen.child_ages[0] = 13 156 | mrGreen.child_ages[1] = 24 157 | mrGreen.child_ages[2] = 12 158 | buffer = bytes(mrGreen) 159 | 160 | # Write to file 161 | f = open('simple_example_mr_green.dat','wb') 162 | f.write(buffer) 163 | f.close() 164 | ``` 165 | 166 | ## Parsing source files 167 | 168 | pycstruct also supports parsing C source code defined in external 169 | files or defined in strings. 170 | 171 | Assume the C code listed in the first example is named 172 | simple_example.c. Then you could parse the source 173 | code instead of manually creating the definitions: 174 | 175 | ```python 176 | import pycstruct 177 | 178 | definitions = pycstruct.parse_file('simple_example.c') 179 | 180 | with open('simple_example.dat', 'rb') as f: 181 | inbytes = f.read() 182 | 183 | # Dictionary representation 184 | result = definitions['person'].deserialize(inbytes) 185 | print(str(result)) 186 | 187 | # Alternative, Instance representation 188 | instance = definitions['person'].instance(inbytes) 189 | ``` 190 | 191 | The produced output will be the same as in the first example (the dictionary). 192 | 193 | ## Full documentation 194 | 195 | Checkout the full documentation [here](https://pycstruct.readthedocs.io/en/latest/). 196 | 197 | ## Author and license 198 | 199 | This application is written by Joel Midstjärna and is licensed under the MIT License. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.12.2.{build} 2 | 3 | image: 4 | - Visual Studio 2022 5 | 6 | environment: 7 | PYTHONUNBUFFERED: 1 8 | matrix: 9 | - PYTHON_VERSION: "38" 10 | POST_COMMAND: "coveralls" # Only export coverage in this version 11 | - PYTHON_VERSION: "311" 12 | 13 | init: 14 | - SET PYTHON=Python%PYTHON_VERSION% 15 | - SET PATH=C:\%PYTHON%-x64;C:\Users\appveyor\AppData\Roaming\Python\%PYTHON%\Scripts;%PATH% 16 | - python -V 17 | 18 | build: off 19 | 20 | install: 21 | - python -m pip install -U --user pip 22 | - python -m pip install -U --user tox 23 | 24 | test_script: 25 | - tox -e py%PYTHON_VERSION% 26 | - python tests/buildtest.py 27 | 28 | deploy_script: 29 | - ps: | 30 | if($env:PYTHON_VERSION -eq "38" -And $env:APPVEYOR_REPO_BRANCH -eq "master") { 31 | Write-Output "Deploying for this build" 32 | python -m pip install -U --user setuptools wheel twine 33 | python setup.py sdist bdist_wheel 34 | python -m twine upload dist/* 35 | } else { 36 | Write-Output "No deployment for this build" 37 | } 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pycstruct.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pycstruct.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pycstruct" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pycstruct" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pycstruct.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pycstruct.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pycstruct documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Dec 22 12:38:52 2019. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | # 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'pycstruct' 54 | copyright = '2023, Joel Midstjärna' 55 | author = 'Joel Midstjärna' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = '0.12.1' 63 | # The full version, including alpha/beta/rc tags. 64 | release = '0.12.1' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | # 76 | # today = '' 77 | # 78 | # Else, today_fmt is used as the format for a strftime call. 79 | # 80 | # today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | # This patterns also effect to html_static_path and html_extra_path 85 | exclude_patterns = [] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | # 94 | # add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | # 99 | # add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | # 104 | # show_authors = False 105 | 106 | # The name of the Pygments (syntax highlighting) style to use. 107 | pygments_style = 'sphinx' 108 | 109 | # A list of ignored prefixes for module index sorting. 110 | # modindex_common_prefix = [] 111 | 112 | # If true, keep warnings as "system message" paragraphs in the built documents. 113 | # keep_warnings = False 114 | 115 | # If true, `todo` and `todoList` produce output, else they produce nothing. 116 | todo_include_todos = False 117 | 118 | 119 | # -- Options for HTML output ---------------------------------------------- 120 | 121 | # The theme to use for HTML and HTML Help pages. See the documentation for 122 | # a list of builtin themes. 123 | # 124 | html_theme = 'alabaster' 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | # 130 | # html_theme_options = {} 131 | 132 | # Add any paths that contain custom themes here, relative to this directory. 133 | # html_theme_path = [] 134 | 135 | # The name for this set of Sphinx documents. 136 | # " v documentation" by default. 137 | # 138 | # html_title = 'pycstruct v0.1.0' 139 | 140 | # A shorter title for the navigation bar. Default is the same as html_title. 141 | # 142 | # html_short_title = None 143 | 144 | # The name of an image file (relative to this directory) to place at the top 145 | # of the sidebar. 146 | # 147 | # html_logo = None 148 | 149 | # The name of an image file (relative to this directory) to use as a favicon of 150 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | # 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = [] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # 164 | # html_extra_path = [] 165 | 166 | # If not None, a 'Last updated on:' timestamp is inserted at every page 167 | # bottom, using the given strftime format. 168 | # The empty string is equivalent to '%b %d, %Y'. 169 | # 170 | # html_last_updated_fmt = None 171 | 172 | # If true, SmartyPants will be used to convert quotes and dashes to 173 | # typographically correct entities. 174 | # 175 | # html_use_smartypants = True 176 | 177 | # Custom sidebar templates, maps document names to template names. 178 | # 179 | # html_sidebars = {} 180 | 181 | # Additional templates that should be rendered to pages, maps page names to 182 | # template names. 183 | # 184 | # html_additional_pages = {} 185 | 186 | # If false, no module index is generated. 187 | # 188 | # html_domain_indices = True 189 | 190 | # If false, no index is generated. 191 | # 192 | # html_use_index = True 193 | 194 | # If true, the index is split into individual pages for each letter. 195 | # 196 | # html_split_index = False 197 | 198 | # If true, links to the reST sources are added to the pages. 199 | # 200 | # html_show_sourcelink = True 201 | 202 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 203 | # 204 | # html_show_sphinx = True 205 | 206 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 207 | # 208 | # html_show_copyright = True 209 | 210 | # If true, an OpenSearch description file will be output, and all pages will 211 | # contain a tag referring to it. The value of this option must be the 212 | # base URL from which the finished HTML is served. 213 | # 214 | # html_use_opensearch = '' 215 | 216 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 217 | # html_file_suffix = None 218 | 219 | # Language to be used for generating the HTML full-text search index. 220 | # Sphinx supports the following languages: 221 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 222 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 223 | # 224 | # html_search_language = 'en' 225 | 226 | # A dictionary with options for the search language support, empty by default. 227 | # 'ja' uses this config value. 228 | # 'zh' user can custom change `jieba` dictionary path. 229 | # 230 | # html_search_options = {'type': 'default'} 231 | 232 | # The name of a javascript file (relative to the configuration directory) that 233 | # implements a search results scorer. If empty, the default will be used. 234 | # 235 | # html_search_scorer = 'scorer.js' 236 | 237 | # Output file base name for HTML help builder. 238 | htmlhelp_basename = 'pycstructdoc' 239 | 240 | # -- Options for LaTeX output --------------------------------------------- 241 | 242 | latex_elements = { 243 | # The paper size ('letterpaper' or 'a4paper'). 244 | # 245 | # 'papersize': 'letterpaper', 246 | 247 | # The font size ('10pt', '11pt' or '12pt'). 248 | # 249 | # 'pointsize': '10pt', 250 | 251 | # Additional stuff for the LaTeX preamble. 252 | # 253 | # 'preamble': '', 254 | 255 | # Latex figure (float) alignment 256 | # 257 | # 'figure_align': 'htbp', 258 | } 259 | 260 | # Grouping the document tree into LaTeX files. List of tuples 261 | # (source start file, target name, title, 262 | # author, documentclass [howto, manual, or own class]). 263 | latex_documents = [ 264 | (master_doc, 'pycstruct.tex', 'pycstruct Documentation', 265 | 'Joel Midstjärna', 'manual'), 266 | ] 267 | 268 | # The name of an image file (relative to this directory) to place at the top of 269 | # the title page. 270 | # 271 | # latex_logo = None 272 | 273 | # For "manual" documents, if this is true, then toplevel headings are parts, 274 | # not chapters. 275 | # 276 | # latex_use_parts = False 277 | 278 | # If true, show page references after internal links. 279 | # 280 | # latex_show_pagerefs = False 281 | 282 | # If true, show URL addresses after external links. 283 | # 284 | # latex_show_urls = False 285 | 286 | # Documents to append as an appendix to all manuals. 287 | # 288 | # latex_appendices = [] 289 | 290 | # It false, will not define \strong, \code, itleref, \crossref ... but only 291 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 292 | # packages. 293 | # 294 | # latex_keep_old_macro_names = True 295 | 296 | # If false, no module index is generated. 297 | # 298 | # latex_domain_indices = True 299 | 300 | 301 | # -- Options for manual page output --------------------------------------- 302 | 303 | # One entry per manual page. List of tuples 304 | # (source start file, name, description, authors, manual section). 305 | man_pages = [ 306 | (master_doc, 'pycstruct', 'pycstruct Documentation', 307 | [author], 1) 308 | ] 309 | 310 | # If true, show URL addresses after external links. 311 | # 312 | # man_show_urls = False 313 | 314 | 315 | # -- Options for Texinfo output ------------------------------------------- 316 | 317 | # Grouping the document tree into Texinfo files. List of tuples 318 | # (source start file, target name, title, author, 319 | # dir menu entry, description, category) 320 | texinfo_documents = [ 321 | (master_doc, 'pycstruct', 'pycstruct Documentation', 322 | author, 'pycstruct', 'One line description of project.', 323 | 'Miscellaneous'), 324 | ] 325 | 326 | # Documents to append as an appendix to all manuals. 327 | # 328 | # texinfo_appendices = [] 329 | 330 | # If false, no module index is generated. 331 | # 332 | # texinfo_domain_indices = True 333 | 334 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 335 | # 336 | # texinfo_show_urls = 'footnote' 337 | 338 | # If true, do not generate a @detailmenu in the "Top" node's menu. 339 | # 340 | # texinfo_no_detailmenu = False 341 | -------------------------------------------------------------------------------- /doc/source/examples/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Simple Example 5 | -------------- 6 | 7 | Following C has a structure (person) with a set of elements 8 | that are written to a binary file. 9 | 10 | .. literalinclude:: simple_example.c 11 | :language: c 12 | 13 | To read the binary file using pycstruct following code 14 | required. 15 | 16 | .. literalinclude:: simple_example_read.py 17 | :language: python 18 | 19 | The produced output will be:: 20 | 21 | Dictionary object: 22 | {'name': 'Foo Bar', 'is_male': True, 'nbr_of_children': 2, 23 | 'age': 42, 'child_ages': [7, 9, 0, 0, 0, 0, 0, 0, 0, 0], 24 | 'height': 1.75} 25 | 26 | Instance object: 27 | name: Foo Bar 28 | nbr_of_children: 2 29 | child_ages[1]: 9 30 | 31 | To write a binary file from python using the same structure 32 | using pycstruct following code is required. 33 | 34 | .. literalinclude:: simple_example_write.py 35 | :language: python 36 | 37 | Embedded Struct Example 38 | ------------------------ 39 | 40 | A struct can also include another struct. 41 | 42 | Following C structure: 43 | 44 | .. code-block:: c 45 | 46 | struct car_s 47 | { 48 | unsigned short year; 49 | char model[50]; 50 | char registration_number[10]; 51 | }; 52 | 53 | struct garage_s 54 | { 55 | struct car_s cars[20]; 56 | unsigned char nbr_registered_parkings; 57 | }; 58 | 59 | struct house_s { 60 | unsigned char nbr_of_levels; 61 | struct garage_s garage; 62 | }; 63 | 64 | Is defined as following: 65 | 66 | .. code-block:: python 67 | 68 | car = pycstruct.StructDef() 69 | car.add('uint16', 'year') 70 | car.add('utf-8', 'model', length=50) 71 | car.add('utf-8', 'registration_number', length=10) 72 | 73 | garage = pycstruct.StructDef() 74 | garage.add(car, 'cars', length=20) 75 | garage.add('uint8', 'nbr_registered_parkings') 76 | 77 | house = pycstruct.StructDef() 78 | house.add('uint8', 'nbr_of_levels') 79 | house.add(garage, 'garage') 80 | 81 | To print the model number of the first car: 82 | 83 | .. code-block:: python 84 | 85 | # Dictionary representation 86 | my_house = house.deserialize(databuffer) 87 | print(my_house['garage']['cars'][0]['model']) 88 | 89 | # Alternative, Instance representation 90 | my_house = house.instance(databuffer) 91 | print(my_house.garage.cars[0].model) 92 | 93 | 94 | Parsing source code 95 | ------------------- 96 | 97 | Assume the C code listed in the first example is named 98 | simple_example.c. Then you could parse the source 99 | code instead of manually creating the definitions: 100 | 101 | .. code-block:: python 102 | 103 | import pycstruct 104 | 105 | definitions = pycstruct.parse_file('simple_example.c') 106 | 107 | with open('simple_example.dat', 'rb') as f: 108 | inbytes = f.read() 109 | 110 | # Dictionary representation 111 | result = definitions['person'].deserialize(inbytes) 112 | print(str(result)) 113 | 114 | # Alternative, Instance representation 115 | instance = definitions['person'].instance(inbytes) 116 | 117 | The produced output will be the same is in the first example. 118 | 119 | 120 | Parsing source code strings 121 | --------------------------- 122 | 123 | You can also define the source in a string and parse it: 124 | 125 | .. code-block:: python 126 | 127 | c_str = ''' 128 | struct a_struct { 129 | int member_a; 130 | int member_b; 131 | }; 132 | struct a_bitfield { 133 | unsigned int bmem_a : 3; 134 | unsigned int bmem_b : 1; 135 | }; 136 | ''' 137 | 138 | result = pycstruct.parse_str(c_str) 139 | 140 | In the above example result['a_struct'] will be a pcstruct.StructDef 141 | instance and result['a_bitfield'] will be a pycstruct.BitfieldDef instance. -------------------------------------------------------------------------------- /doc/source/examples/simple_example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #pragma pack(1) // To secure no padding is added in struct 6 | 7 | struct person 8 | { 9 | char name[50]; 10 | unsigned int age; 11 | float height; 12 | bool is_male; 13 | unsigned int nbr_of_children; 14 | unsigned int child_ages[10]; 15 | }; 16 | 17 | 18 | void main(void) { 19 | struct person p; 20 | memset(&p, 0, sizeof(struct person)); 21 | 22 | strcpy(p.name, "Foo Bar"); 23 | p.age = 42; 24 | p.height = 1.75; // m 25 | p.is_male = true; 26 | p.nbr_of_children = 2; 27 | p.child_ages[0] = 7; 28 | p.child_ages[1] = 9; 29 | 30 | FILE *f = fopen("simple_example.dat", "w"); 31 | fwrite(&p, sizeof(struct person), 1, f); 32 | fclose(f); 33 | } -------------------------------------------------------------------------------- /doc/source/examples/simple_example_read.py: -------------------------------------------------------------------------------- 1 | import pycstruct 2 | 3 | person = pycstruct.StructDef() 4 | person.add('utf-8', 'name', length=50) 5 | person.add('uint32', 'age') 6 | person.add('float32','height') 7 | person.add('bool8', 'is_male') 8 | person.add('uint32', 'nbr_of_children') 9 | person.add('uint32', 'child_ages', length=10) 10 | 11 | with open('simple_example.dat', 'rb') as f: 12 | inbytes = f.read() 13 | 14 | # Dictionary representation 15 | result = person.deserialize(inbytes) 16 | print('Dictionary object:') 17 | print(str(result)) 18 | 19 | # Alternative, Instance representation 20 | instance = person.instance(inbytes) 21 | print('\nInstance object:') 22 | print(f'name: {instance.name}') 23 | print(f'nbr_of_children: {instance.nbr_of_children}') 24 | print(f'child_ages[1]: {instance.child_ages[1]}') -------------------------------------------------------------------------------- /doc/source/examples/simple_example_write.py: -------------------------------------------------------------------------------- 1 | import pycstruct 2 | 3 | person = pycstruct.StructDef() 4 | person.add('utf-8', 'name', length=50) 5 | person.add('uint32', 'age') 6 | person.add('float32','height') 7 | person.add('bool8', 'is_male') 8 | person.add('uint32', 'nbr_of_children') 9 | person.add('uint32', 'child_ages', length=10) 10 | 11 | # Dictionary representation 12 | mrGreen = {} 13 | mrGreen['name'] = "MR Green" 14 | mrGreen['age'] = 50 15 | mrGreen['height'] = 1.93 16 | mrGreen['is_male'] = True 17 | mrGreen['nbr_of_children'] = 3 18 | mrGreen['child_ages'] = [13,24,12] 19 | buffer = person.serialize(mrGreen) 20 | 21 | # Alternative, Instance representation 22 | mrGreen = person.instance() 23 | mrGreen.name = "MR Green" 24 | mrGreen.age = 50 25 | mrGreen.height = 1.93 26 | mrGreen.is_male = True 27 | mrGreen.nbr_of_children = 3 28 | mrGreen.child_ages[0] = 13 29 | mrGreen.child_ages[1] = 24 30 | mrGreen.child_ages[2] = 12 31 | buffer = bytes(mrGreen) 32 | 33 | # Write to file 34 | f = open('simple_example_mr_green.dat','wb') 35 | f.write(buffer) 36 | f.close() 37 | -------------------------------------------------------------------------------- /doc/source/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | How to disable printouts from pycstruct 5 | --------------------------------------- 6 | 7 | pycstruct utilizes the standard logging module for printing out warnings. 8 | To disable logging from the pycstruct module add following to your 9 | application 10 | 11 | .. code-block:: python 12 | 13 | import logging 14 | logging.getLogger('pycstruct').setLevel(logging.CRITICAL) -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | pycstruct 2 | ========= 3 | 4 | pycstruct is a python library for converting binary data to and from ordinary 5 | python dictionaries or specific instance objects. 6 | 7 | Data is defined similar to what is done in C language structs, unions, 8 | bitfields and enums. 9 | 10 | Typical usage of this library is read/write binary files or binary data 11 | transmitted over a network. 12 | 13 | Following complex C types are supported: 14 | 15 | - Structs 16 | - Unions 17 | - Bitfields 18 | - Enums 19 | 20 | These types may consist of any traditional data types (integer, unsigned integer, 21 | boolean and float) between 1 to 8 bytes large, arrays (lists), and strings (ASCII/UTF-8). 22 | 23 | Structs, unions, bitfields and enums can be embedded inside other structs/unions 24 | in any level. 25 | 26 | Individual elements can be stored / read in any byte order and alignment. 27 | 28 | pycstruct also supports parsing of existing C language source code to 29 | automatically generate the pycstruct definitions / instances. 30 | 31 | .. toctree:: 32 | :maxdepth: 2 33 | 34 | installation.rst 35 | overview.rst 36 | examples/examples.rst 37 | faq.rst 38 | limitations.rst 39 | reference.rst 40 | 41 | 42 | 43 | Indices and tables 44 | ================== 45 | 46 | * :ref:`genindex` 47 | * :ref:`search` 48 | 49 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Python version compatibility 5 | ---------------------------- 6 | 7 | pycstruct is only compatible with python version 3.6 or later. 8 | 9 | Basic Installation 10 | ------------------ 11 | 12 | Install pycstruct with :command:`pip`:: 13 | 14 | python3 -m pip install pycstruct 15 | 16 | Extra dependencies to parse source code 17 | ---------------------------------------- 18 | 19 | To be able to parse C source code (optional) you also need 20 | to install `castxml `_. 21 | 22 | castxml supports all the major platforms such as Linux, 23 | Windows and OS X. 24 | 25 | -------------------------------------------------------------------------------- /doc/source/limitations.rst: -------------------------------------------------------------------------------- 1 | .. _limitations: 2 | 3 | Limitations 4 | =========== 5 | 6 | Padding of bitfields 7 | -------------------- 8 | 9 | How bitfields are stored in memory is not defined by the C standard. 10 | pycstruct c parser tries to keep consecutive defined bitfields together, 11 | however gcc (for example) will split bitfields when alignment is needed. 12 | 13 | Following example will be a problem: 14 | 15 | .. code-block:: c 16 | 17 | struct a_struct { 18 | char m1; // = 8 bits 19 | int bf1 : 2; // = 2 bits 20 | int bf2 : 23; // = 23 bits 21 | }; 22 | 23 | pycstruct c parser will layout the struct like this using 32 bit alignment: 24 | 25 | +---------------+-----------------------+---------------------------+ 26 | | Size in bytes | Type | Name | 27 | +===============+=======================+===========================+ 28 | | 1 | Unsigned integer | m1 | 29 | +---------------+-----------------------+---------------------------+ 30 | | 1 | Unsigned integer | __pad_0[0] | 31 | +---------------+-----------------------+---------------------------+ 32 | | 1 | Unsigned integer | __pad_0[1] | 33 | +---------------+-----------------------+---------------------------+ 34 | | 1 | Unsigned integer | __pad_0[2] | 35 | +---------------+-----------------------+---------------------------+ 36 | | 4 | Bitfield | bf1 + bf2 | 37 | +---------------+-----------------------+---------------------------+ 38 | 39 | gcc will layout the struct like this using 32 bit alignment: 40 | 41 | +---------------+-----------------------+---------------------------+ 42 | | Size in bytes | Type | Name | 43 | +===============+=======================+===========================+ 44 | | 1 | Unsigned integer | m1 | 45 | +---------------+-----------------------+---------------------------+ 46 | | 1 | Bitfield | bf1 | 47 | +---------------+-----------------------+---------------------------+ 48 | | 1 | Unsigned integer | __pad_0[0] | 49 | +---------------+-----------------------+---------------------------+ 50 | | 1 | Unsigned integer | __pad_0[1] | 51 | +---------------+-----------------------+---------------------------+ 52 | | 3 | Bitfield | bf2 | 53 | +---------------+-----------------------+---------------------------+ 54 | 55 | One workaround is to pack the struct using compiler directives. 56 | 57 | Another workaround is to manually add padding bytes in the struct 58 | in-between the bitfield definitions. 59 | -------------------------------------------------------------------------------- /doc/source/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | Background 5 | ---------- 6 | 7 | Typically python applications don't care about memory layout of the used variables 8 | or objects. This is generally not a problem when parsing text based data such as 9 | JSON, XML data. However, when parsing binary data the Python language and standard 10 | library has limited support for this. 11 | 12 | The pycstruct library solves this problem by allowing the user to define the memory 13 | layout of an "object". Once the memory layout has been defined data can serialized 14 | or deserialized into/from simple python dictionaries or specific instance objects. 15 | 16 | Why and when does the memory layout matter? 17 | ------------------------------------------- 18 | 19 | Strict memory layout is required when reading and writing binary data, such as: 20 | 21 | * Binary file formats 22 | * Binary network data 23 | 24 | Structs 25 | ------- 26 | 27 | Memory layout of an object is defined using the :py:meth:`pycstruct.StructDef` 28 | object. For example: 29 | 30 | .. code-block:: python 31 | 32 | myStruct = pycstruct.StructDef() 33 | myStruct.add('int8', 'mySmallInteger') 34 | myStruct.add('uint32', 'myUnsignedInteger') 35 | myStruct.add('float32', 'myFloatingPointNumber') 36 | 37 | The above example corresponds to following layout: 38 | 39 | +---------------+-----------------------+---------------------------+ 40 | | Size in bytes | Type | Name | 41 | +===============+=======================+===========================+ 42 | | 1 | Signed integer | mySmallInteger | 43 | +---------------+-----------------------+---------------------------+ 44 | | 4 | Unsigned integer | myUnsignedInteger | 45 | +---------------+-----------------------+---------------------------+ 46 | | 4 | Floating point number | myFloatingPointNumber | 47 | +---------------+-----------------------+---------------------------+ 48 | 49 | Now, when the layout has been defined, you can write binary data using 50 | ordinary python dictionaries. 51 | 52 | .. code-block:: python 53 | 54 | myDict = {} 55 | myDict['mySmallInteger'] = -4 56 | myDict['myUnsignedInteger'] = 12345 57 | myDict['myFloatingPointNumber'] = 3.1415 58 | 59 | myByteArray = myStruct.serialize(myDict) 60 | 61 | myByteArray is now a byte array that can for example can be written to 62 | a file or transmitted over a network. 63 | 64 | The reverse process looks like this (assuming data is stored in the 65 | file myDataFile.dat): 66 | 67 | .. code-block:: python 68 | 69 | with open('myDataFile.dat', 'rb') as f: 70 | inbytes = f.read() 71 | 72 | myDict2 = myStruct.deserialize(inbytes) 73 | 74 | myDict2 will now be a dictionary with the fields mySmallInteger, 75 | myUnsignedInteger and myFloatingPointNumber. 76 | 77 | Arrays 78 | ------ 79 | 80 | Arrays are added like this: 81 | 82 | .. code-block:: python 83 | 84 | myStruct = pycstruct.StructDef() 85 | myStruct.add('int32', 'myArray', shape=100) 86 | 87 | Now myArray will be an array with 100 elements. 88 | 89 | .. code-block:: python 90 | 91 | myDict = {} 92 | myDict['myArray'] = [32, 11] 93 | 94 | myByteArray = myStruct.serialize(myDict) 95 | 96 | Note that you don't have to provide all elements of the array in the 97 | dictionary. Elements not defined will be set to 0 during serialization. 98 | 99 | Ndim arrays 100 | ----------- 101 | 102 | The shape can be a tuple for multi dimensional arrays. 103 | The last element of the tuple is the fastest dimension. 104 | 105 | .. code-block:: python 106 | 107 | myStruct = pycstruct.StructDef() 108 | myStruct.add('int32', 'myNdimArray', shape=(100, 50, 2)) 109 | 110 | Strings 111 | ------- 112 | 113 | Strings are always encoded as UTF-8. UTF-8 is backwards compatible with 114 | ASCII, thus ASCII strings are also supported. 115 | 116 | .. code-block:: python 117 | 118 | myStruct = pycstruct.StructDef() 119 | myStruct.add('utf-8', 'myString', length=50) 120 | 121 | Now myString will be a string of 50 bytes. Note that: 122 | 123 | * Non-ASCII characters are larger than one byte. Thus the number of characters 124 | might not be equal to the specified length (which is in bytes not characters) 125 | * The last byte is used as null-termination and should not be used for characters 126 | data. 127 | 128 | To write a string: 129 | 130 | .. code-block:: python 131 | 132 | myDict = {} 133 | myDict['myString'] = "this is a string" 134 | 135 | myByteArray = myStruct.serialize(myDict) 136 | 137 | If you need another encoding that UTF-8 or ASCII it is recommended that you 138 | define your element as an array of uint8. Then you can decode/encode the array 139 | to any format you want. 140 | 141 | Embedding Structs 142 | ----------------- 143 | 144 | Embedding structs in other structs is simple: 145 | 146 | .. code-block:: python 147 | 148 | myChildStruct = pycstruct.StructDef() 149 | myChildStruct.add('int8', 'myChildInteger') 150 | 151 | myParentStruct = pycstruct.StructDef() 152 | myParentStruct.add('int8', 'myParentInteger') 153 | myParentStruct.add(myChildStruct, 'myChild') 154 | 155 | Now myParentStruct includes myChildStruct. 156 | 157 | .. code-block:: python 158 | 159 | myChildDict = {} 160 | myChildDict['myChildInteger'] = 7 161 | 162 | myParentDict['myParentInteger'] = 45 163 | myParentDict['myChild'] = myChildDict 164 | 165 | myByteArray = myStruct.serialize(myParentDict) 166 | 167 | Note that you can also make an array of child structs by setting the length 168 | argument when adding the element. 169 | 170 | Unions 171 | ------ 172 | 173 | Unions are defined using the :py:meth:`pycstruct.StructDef` class, but the 174 | union argument in the construct shall be set to True. 175 | 176 | When deserializing a binary for a union, pycstruct tries to generate 177 | a dictionary for each member. If any of the members fails due to formatting 178 | errors these members will be ignored. 179 | 180 | When serializing a dictionary into a binary pycstruct will just pick the 181 | first member it finds in the dictionary. Therefore you should only 182 | define the member that you which to serialize in your dictionary. 183 | 184 | Bitfields 185 | --------- 186 | 187 | The struct definition requires that the size of each member is 1, 2, 4 or 8 188 | bytes. :py:meth:`pycstruct.BitfieldDef` allows you to define members that have any 189 | size between 1 to 64 bits. 190 | 191 | .. code-block:: python 192 | 193 | myBitfield = pycstruct.BitfieldDef() 194 | 195 | myBitfield.add("myBit",1) 196 | myBitfield.add("myTwoBits",2) 197 | myBitfield.add("myFourSignedBits",4 ,signed=True) 198 | 199 | The above bitfield will allocate one byte with following layout: 200 | 201 | +-------------+------------------+---------------+-------------+ 202 | | BIT index 7 | BIT index 6 - 3 | BIT index 2-1 | BIT index 0 | 203 | +=============+==================+===============+=============+ 204 | | Unused | MyFourSignedBits | myTwoBits | myBit | 205 | +-------------+------------------+---------------+-------------+ 206 | 207 | To add myBitfield to a struct def: 208 | 209 | .. code-block:: python 210 | 211 | myStruct = pycstruct.StructDef() 212 | myStruct.add(myBitfield, 'myBitfieldChild') 213 | 214 | To access myBitfield 215 | 216 | .. code-block:: python 217 | 218 | myBitfieldDict = {} 219 | myBitfieldDict['myBit'] = 0 220 | myBitfieldDict['myTwoBit'] = 3 221 | myBitfieldDict['myFourSignedBits'] = -1 222 | 223 | myDict = {} 224 | myDict['myBitfieldChild'] = myBitfieldDict 225 | 226 | myByteArray = myStruct.serialize(myDict) 227 | 228 | Enum 229 | ---- 230 | 231 | :py:meth:`pycstruct.EnumDef` allows your to define a signed integer of size 1, 2, 3, ... 232 | or 8 bytes with a defined set of values (constants): 233 | 234 | .. code-block:: python 235 | 236 | myEnum = pycstruct.EnumDef() 237 | 238 | myEnum.add('myConstantM3',-3) 239 | myEnum.add('myConstant0',0) 240 | myEnum.add('myConstant5',5) 241 | myEnum.add('myConstant44',44) 242 | 243 | To add an enum to a struct: 244 | 245 | .. code-block:: python 246 | 247 | myStruct = pycstruct.StructDef() 248 | myStruct.add(myEnum, 'myEnumChild') 249 | 250 | The constants are accessed as strings: 251 | 252 | .. code-block:: python 253 | 254 | myDict = {} 255 | myDict['myEnumChild'] = 'myConstant5' 256 | 257 | myByteArray = myStruct.serialize(myDict) 258 | 259 | Setting myEnumChild to a value not defined in the EnumDef will result 260 | in an exception. 261 | 262 | Byte order 263 | ---------- 264 | 265 | Structs, bitfields and enums are by default read and written in the 266 | native byte order. However, you can always override the default 267 | byteorder by providing the byteorder argument. 268 | 269 | .. code-block:: python 270 | 271 | myStruct = pycstruct.StructDef(default_byteorder = 'big') 272 | myStruct.add('int16', 'willBeBigEndian') 273 | myStruct.add('int32', 'willBeBigEndianAlso') 274 | myStruct.add('int32', 'willBeLittleEndian', byteorder = 'little') 275 | 276 | myBitfield = pycstruct.BitfieldDef(byteorder = 'little') 277 | 278 | myEnum = pycstruct.EnumDef(byteorder = 'big') 279 | 280 | Alignment and padding 281 | --------------------- 282 | 283 | Compilers usually add padding in-between elements in structs to secure 284 | individual elements are put on addresses that can be accessed 285 | efficiently. Also, padding is added in the end of the structs when 286 | required so that an array of the struct can be made without "memory gaps". 287 | 288 | Padding depends on the alignment of the CPU architecture (typically 32 289 | or 64 bits on modern architectures), the size of individual items in 290 | the struct and the position of the items in the struct. 291 | 292 | The padding behavior can be removed by most compilers, usually adding 293 | a compiler flag or directive such as: 294 | 295 | .. code-block:: c 296 | 297 | #pragma pack(1) 298 | 299 | pycstruct is by default not adding any padding, i.e. the structs are 300 | packed. However by providing the alignment argument padding will be 301 | added automatically. 302 | 303 | .. code-block:: python 304 | 305 | noPadding_Default = pycstruct.StructDef(alignment = 1) 306 | paddedFor16BitArchitecture = pycstruct.StructDef(alignment = 2) 307 | paddedFor32BitArchitecture = pycstruct.StructDef(alignment = 4) 308 | paddedFor64BitArchitecture = pycstruct.StructDef(alignment = 8) 309 | 310 | Lets add padding to the first example in this overview: 311 | 312 | .. code-block:: python 313 | 314 | myStruct = pycstruct.StructDef(alignment = 8) 315 | myStruct.add('int8', 'mySmallInteger') 316 | myStruct.add('uint32', 'myUnsignedInteger') 317 | myStruct.add('float32', 'myFloatingPointNumber') 318 | 319 | The above example will now have following layout: 320 | 321 | +---------------+-----------------------+---------------------------+ 322 | | Size in bytes | Type | Name | 323 | +===============+=======================+===========================+ 324 | | 1 | Signed integer | mySmallInteger | 325 | +---------------+-----------------------+---------------------------+ 326 | | 1 | Unsigned integer | __pad_0[0] | 327 | +---------------+-----------------------+---------------------------+ 328 | | 1 | Unsigned integer | __pad_0[1] | 329 | +---------------+-----------------------+---------------------------+ 330 | | 1 | Unsigned integer | __pad_0[2] | 331 | +---------------+-----------------------+---------------------------+ 332 | | 4 | Unsigned integer | myUnsignedInteger | 333 | +---------------+-----------------------+---------------------------+ 334 | | 4 | Floating point number | myFloatingPointNumber | 335 | +---------------+-----------------------+---------------------------+ 336 | 337 | Note that when parsing source code, pycstruct has some 338 | limitations regarding padding of bitfields. See :ref:`limitations`. 339 | 340 | Parsing source code 341 | ------------------- 342 | 343 | Instead of manually creating the definitions as described above, 344 | C source code files can be parsed and the definitions will be 345 | generated automatically with :func:`pycstruct.parse_file`. 346 | 347 | It is also possible to write the source code into a string and 348 | parse it with :func:`pycstruct.parse_str`. 349 | 350 | Internally pycstruct use the external tool 351 | `castxml `_ which needs to 352 | be installed and put in the current path. 353 | 354 | Instance objects 355 | ---------------- 356 | 357 | Most examples in this section are using dictionaries. An alternative 358 | of using dictionaries to represent the object is to use 359 | :py:meth:`pycstruct.Instance` objects. 360 | 361 | Instance objects has following advantages over dictionaries: 362 | 363 | - Data is only serialized/deserialized when accessed 364 | - Data is validated for each element/attribute access. I.e. you will 365 | get an exception if you try to set an element/attribute to a value 366 | that is not supported by the definition. 367 | - Data is accessed by attribute name instead of key indexing 368 | 369 | Instance objects are created from the :py:meth:`pycstruct.StructDef` 370 | or :py:meth:`pycstruct.BitfieldDef` object. 371 | 372 | .. code-block:: python 373 | 374 | myStruct = pycstruct.StructDef() 375 | #.... Add some elements to myStruct here 376 | instanceOfMyStruct = myStruct.instance() 377 | 378 | myBitfield = pycstruct.BitfieldDef() 379 | #.... Add some elements to myBitfield here 380 | instanceOfMyBitfield = myBitfield.instance() 381 | 382 | 383 | Deserialize with numpy 384 | ---------------------- 385 | 386 | The structure definitions can be used together with 387 | `numpy `_, with some restrictions. 388 | 389 | This provides an easy way to describe complex numpy dtype, 390 | especially compound dtypes. 391 | 392 | There is some restructions: 393 | 394 | - bitfields and enums are not supported 395 | - strings are not decoded (that's still bytes) 396 | 397 | This can be used for use cases requiring very fast processing, 398 | or smart indexing. 399 | 400 | The structure definitions provides a method `dtype` which 401 | can be read by numpy. 402 | 403 | .. code-block:: python 404 | 405 | import pycstruct 406 | import numpy 407 | 408 | # Define a RGBA color 409 | color_t = pycstruct.StructDef() 410 | color_t.add("uint8", "r") 411 | color_t.add("uint8", "g") 412 | color_t.add("uint8", "b") 413 | color_t.add("uint8", "a") 414 | 415 | # Define a vector of RGBA 416 | colorarray_t = pycstruct.StructDef() 417 | colorarray_t.add(color_t, "vector", shape=200) 418 | 419 | # Dummy data 420 | raw = b"\x20\x30\x40\xFF" * 200 421 | 422 | # Deserialize the raw bytes 423 | colorarray = numpy.frombuffer(raw, dtype=colorarray_t.dtype(), count=1) 424 | # numpy.frombuffer deserialize arrays. In this case there is 425 | # a single element of colorarray_t, which can be unstacked 426 | colorarray = colorarray[0] 427 | 428 | # Elements can be accessed by names 429 | # Here we can access to the whole red components is a single request 430 | red_component = colorarray["vector"]["r"] 431 | assert red_component.dtype == numpy.uint8 432 | assert red_component.shape == (200, ) 433 | 434 | Numpy also provides record array which can be used like the 435 | instance objects. 436 | 437 | .. code-block:: python 438 | 439 | colorarray = numpy.frombuffer(raw, dtype=colorarray_t.dtype())[0] 440 | 441 | # Create a record array 442 | colorarray = numpy.rec.array(colorarray) 443 | 444 | # Elements can be accessed by attributes 445 | assert colorarray.vector.r.dtype == numpy.uint8 446 | assert colorarray.vector.r.shape == (200, ) 447 | -------------------------------------------------------------------------------- /doc/source/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | StructDef (struct and union representation) 5 | ------------------------------------------- 6 | .. autoclass:: pycstruct.StructDef 7 | :members: 8 | 9 | BitfieldDef (bitfield representation) 10 | ------------------------------------- 11 | .. autoclass:: pycstruct.BitfieldDef 12 | :members: 13 | 14 | EnumDef (enum representation) 15 | ----------------------------- 16 | .. autoclass:: pycstruct.EnumDef 17 | :members: 18 | 19 | Parse source code files 20 | ----------------------- 21 | .. autofunction:: pycstruct.parse_file 22 | 23 | Parse source code strings 24 | ------------------------- 25 | .. autofunction:: pycstruct.parse_str 26 | 27 | Instance (instance of StructDef or BitfieldDef) 28 | ----------------------------------------------- 29 | .. autofunction:: pycstruct.Instance -------------------------------------------------------------------------------- /pycstruct/__init__.py: -------------------------------------------------------------------------------- 1 | """pycstruct library 2 | 3 | pycstruct is a python library for converting binary data to and from ordinary 4 | python dictionaries or specific instance objects. 5 | 6 | Data is defined similar to what is done in C language structs, unions, 7 | bitfields and enums. 8 | 9 | Typical usage of this library is read/write binary files or binary data 10 | transmitted over a network. 11 | 12 | For more information see: 13 | 14 | https://github.com/midstar/pycstruct 15 | """ 16 | 17 | from pycstruct.pycstruct import StructDef 18 | from pycstruct.pycstruct import BitfieldDef 19 | from pycstruct.pycstruct import EnumDef 20 | from pycstruct.pycstruct import ArrayDef 21 | 22 | from pycstruct.instance import Instance 23 | 24 | from pycstruct.cparser import parse_file 25 | from pycstruct.cparser import parse_str 26 | -------------------------------------------------------------------------------- /pycstruct/cparser.py: -------------------------------------------------------------------------------- 1 | """pycstruct cparser 2 | 3 | Copyright 2021 by Joel Midstjärna. 4 | All rights reserved. 5 | This file is part of the pycstruct python library and is 6 | released under the "MIT License Agreement". Please see the LICENSE 7 | file that should have been included as part of this package. 8 | """ 9 | 10 | import xml.etree.ElementTree as ET 11 | 12 | import hashlib 13 | import logging 14 | import math 15 | import os 16 | import shutil 17 | import subprocess 18 | import tempfile 19 | 20 | from pycstruct.pycstruct import StructDef, BitfieldDef, EnumDef 21 | 22 | ############################################################################### 23 | # Global constants 24 | 25 | logger = logging.getLogger("pycstruct") 26 | 27 | ############################################################################### 28 | # Internal functions 29 | 30 | 31 | def _run_castxml( 32 | input_files, 33 | xml_filename, 34 | castxml_cmd="castxml", 35 | castxml_extra_args=None, 36 | **subprocess_kwargs, 37 | ): 38 | """Run castcml as a 'shell command'""" 39 | if shutil.which(castxml_cmd) is None: 40 | raise RuntimeError( 41 | f'Executable "{castxml_cmd}" not found.\n' 42 | + "External software castxml is not installed.\n" 43 | + "You need to install it and put it in your PATH." 44 | ) 45 | if castxml_extra_args is None: 46 | castxml_extra_args = [] 47 | args = [castxml_cmd] 48 | args += castxml_extra_args 49 | args += input_files 50 | args.append("--castxml-gccxml") 51 | args.append("-o") 52 | args.append(xml_filename) 53 | 54 | # to preserve behavior before subprocess_kwargs were allowed to be passed 55 | # in, default to redirecting stderr to STDOUT. 56 | if "stderr" not in subprocess_kwargs: 57 | subprocess_kwargs["stderr"] = subprocess.STDOUT 58 | 59 | try: 60 | output = subprocess.check_output(args, **subprocess_kwargs) 61 | except subprocess.CalledProcessError as exception: 62 | raise RuntimeError( 63 | "Unable to run:\n" 64 | + f"{' '.join(args)}\n\n" 65 | + "Output:\n" 66 | + exception.output.decode() 67 | ) from exception 68 | 69 | if not os.path.isfile(xml_filename): 70 | raise RuntimeError( 71 | "castxml did not report any error but " 72 | + f"{xml_filename} was never produced.\n\n" 73 | + f"castxml output was:\n{output.decode()}" 74 | ) 75 | 76 | 77 | def _get_hash(list_of_strings): 78 | """Get a reproducible short name of a list of strings. The hash also 79 | includes the user name (from OS) to avoid name conflicts in multi 80 | user environments. 81 | """ 82 | long_string = "".join(list_of_strings) + os.getlogin() 83 | sha256 = hashlib.sha256(long_string.encode()) 84 | hexdigest = sha256.hexdigest() 85 | return hexdigest[:10] 86 | 87 | 88 | def _listify(list_or_str): 89 | """If list_or_str is a string it will be put in a list""" 90 | if isinstance(list_or_str, str): 91 | list_or_str = [list_or_str] 92 | return list_or_str 93 | 94 | 95 | ############################################################################### 96 | # CastXMLParser class (internal) 97 | 98 | 99 | class _CastXmlParser: 100 | """Parses XML:s produced by CastXML and creates a new dictionary where 101 | each key represent a type name of a defined type (struct, bitstruct, 102 | enum, union etc.). The value represents the metadata about the type, 103 | such as members etc. 104 | """ 105 | 106 | # pylint: disable=too-few-public-methods, broad-except 107 | 108 | def __init__(self, xml_filename): 109 | self._xml_filename = xml_filename 110 | self._anonymous_count = 0 111 | self._embedded_bf_count = 0 112 | self._embedded_bf = [] 113 | self.root = None 114 | 115 | def parse(self): 116 | """Parse the XML file provided in the constructor""" 117 | # pylint: disable=too-many-branches, too-many-locals 118 | self.root = ET.parse(self._xml_filename).getroot() 119 | 120 | supported_types = {} 121 | 122 | # Parse enums 123 | xml_enums = self.root.findall("Enumeration") 124 | for xml_enum in xml_enums: 125 | typeid = xml_enum.attrib["id"] 126 | supported_types[typeid] = self._parse_enum(xml_enum) 127 | 128 | # Parse unions 129 | xml_unions = self.root.findall("Union") 130 | for xml_union in xml_unions: 131 | typeid = xml_union.attrib["id"] 132 | supported_types[typeid] = self._parse_union(xml_union) 133 | 134 | # Parse structs and bitfields: 135 | xml_structs_and_bitfields = self.root.findall("Struct") 136 | for xml_struct_or_bitfield in xml_structs_and_bitfields: 137 | typeid = xml_struct_or_bitfield.attrib["id"] 138 | if self._is_bitfield(xml_struct_or_bitfield): 139 | supported_types[typeid] = self._parse_bitfield(xml_struct_or_bitfield) 140 | else: 141 | supported_types[typeid] = self._parse_struct(xml_struct_or_bitfield) 142 | 143 | # Add all embedded bitfields in supported_types 144 | for bitfield in self._embedded_bf: 145 | supported_types[bitfield["name"]] = bitfield 146 | 147 | # Change mapping from id to name 148 | type_meta = {} 149 | for _, datatype in supported_types.items(): 150 | name = datatype["name"] 151 | if name in type_meta: 152 | # Name is not unique, make it unique 153 | for i in range(1, 1000): 154 | new_name = name + str(i) 155 | if new_name not in type_meta: 156 | name = new_name 157 | break 158 | datatype["name"] = name # Change old name to new 159 | type_meta[name] = datatype 160 | 161 | # Secure all references points to names instead of id's 162 | for _, datatype in type_meta.items(): 163 | for member in datatype["members"]: 164 | if "reference" in member: 165 | typeid = member["reference"] 166 | name = supported_types[typeid]["name"] 167 | member["reference"] = name 168 | 169 | return type_meta 170 | 171 | def _is_bitfield(self, xml_struct_or_bitfield): 172 | """Returns true if this is a "true" bitfield, i.e. the 173 | struct only contains bitfield members""" 174 | for field in self._get_fields(xml_struct_or_bitfield): 175 | if "bits" not in field.attrib: 176 | # Only bitfield fields has the bits attribute set 177 | return False 178 | return True 179 | 180 | def _get_fields(self, xml_item): 181 | fields = [] 182 | for member_id in self._get_attrib(xml_item, "members", "").split(): 183 | xml_member = self._get_elem_with_id(member_id) 184 | if xml_member.tag != "Field": 185 | continue # Probably just a struct/union definition 186 | fields.append(xml_member) 187 | return fields 188 | 189 | def _parse_enum(self, xml_enum): 190 | enum = {} 191 | enum["type"] = "enum" 192 | self._set_common_meta(xml_enum, enum) 193 | enum["members"] = [] 194 | has_negative = False 195 | for xml_member in xml_enum.findall("EnumValue"): 196 | member = {} 197 | member["name"] = xml_member.attrib["name"] 198 | member["value"] = int(xml_member.attrib["init"]) 199 | if member["value"] < 0: 200 | has_negative = True 201 | enum["members"].append(member) 202 | if has_negative: 203 | enum["signed"] = True 204 | else: 205 | enum["signed"] = False 206 | return enum 207 | 208 | def _parse_union(self, xml_union): 209 | union = {} 210 | union["type"] = "union" 211 | self._set_common_meta(xml_union, union) 212 | self._set_struct_union_members(xml_union, union) 213 | return union 214 | 215 | def _parse_struct(self, xml_struct): 216 | struct = {} 217 | struct["type"] = "struct" 218 | self._set_common_meta(xml_struct, struct) 219 | self._set_struct_union_members(xml_struct, struct) 220 | return struct 221 | 222 | def _parse_bitfield_members(self, xml_bitfield_members): 223 | result = [] 224 | for field in xml_bitfield_members: 225 | member = {} 226 | member["name"] = field.attrib["name"] 227 | member["bits"] = int(field.attrib["bits"]) 228 | member["signed"] = True 229 | 230 | # Figure out if it is signed 231 | type_elem = self._get_basic_type_element(field.attrib["type"]) 232 | if type_elem.tag == "FundamentalType": 233 | # Figure out sign 234 | if "unsigned" in type_elem.attrib["name"]: 235 | member["signed"] = False 236 | else: 237 | logger.warning( 238 | "Unable to parse sign of bitfield member %s. Will be signed.", 239 | member["name"], 240 | ) 241 | 242 | result.append(member) 243 | return result 244 | 245 | def _parse_bitfield(self, xml_bitfield): 246 | bitfield = {} 247 | bitfield["type"] = "bitfield" 248 | self._set_common_meta(xml_bitfield, bitfield) 249 | bitfield["members"] = self._parse_bitfield_members( 250 | self._get_fields(xml_bitfield) 251 | ) 252 | 253 | return bitfield 254 | 255 | def _set_common_meta(self, xml_input, dict_output): 256 | """Set common metadata available for all types""" 257 | typeid = xml_input.attrib["id"] 258 | name = self._get_attrib(xml_input, "name", "") 259 | if name == "": 260 | # Does not have a name - check for TypeDef 261 | name = self._get_typedef_name(typeid) 262 | dict_output["name"] = name 263 | dict_output["size"] = int(int(self._get_attrib(xml_input, "size", "0")) / 8) 264 | dict_output["align"] = int(int(self._get_attrib(xml_input, "align", "8")) / 8) 265 | dict_output["supported"] = True 266 | 267 | def _set_struct_union_members(self, xml_input, dict_output): 268 | """Set members - common for struct and unions""" 269 | dict_output["members"] = [] 270 | fields = self._get_fields(xml_input) 271 | while len(fields) > 0: 272 | field = fields.pop(0) 273 | member = {} 274 | if "bits" in field.attrib: 275 | # This is a bitfield, we need to create our 276 | # own bitfield definition for this field and 277 | # all bitfield members directly after this 278 | # member 279 | bf_fields = [] 280 | nbr_bits = 0 281 | fields.insert(0, field) 282 | while len(fields) > 0 and "bits" in fields[0].attrib: 283 | bf_field = fields.pop(0) 284 | nbr_bits += int(self._get_attrib(bf_field, "bits", "0")) 285 | bf_fields.append(bf_field) 286 | bitfield = {} 287 | bitfield["type"] = "bitfield" 288 | bitfield["name"] = f"auto_bitfield_{self._embedded_bf_count}" 289 | self._embedded_bf_count += 1 290 | # WARNING! The size is compiler specific and not covered 291 | # by the C standard. We guess: 292 | bitfield["size"] = int(math.ceil(nbr_bits / 8.0)) 293 | bitfield["align"] = dict_output["align"] # Same as parent 294 | bitfield["supported"] = True 295 | bitfield["members"] = self._parse_bitfield_members(bf_fields) 296 | self._embedded_bf.append(bitfield) 297 | 298 | member["name"] = f"__{bitfield['name']}" 299 | member["type"] = "bitfield" 300 | member["reference"] = bitfield["name"] 301 | member["same_level"] = True 302 | else: 303 | member["name"] = field.attrib["name"] 304 | member_type = self._get_type(field.attrib["type"]) 305 | member["type"] = member_type["type_name"] 306 | if "shape" in member_type: 307 | member["shape"] = member_type["shape"] 308 | if "reference" in member_type: 309 | member["reference"] = member_type["reference"] 310 | dict_output["members"].append(member) 311 | 312 | def _get_attrib(self, elem, attrib, default): 313 | if attrib in elem.attrib: 314 | return elem.attrib[attrib] 315 | return default 316 | 317 | def _get_elem_with_id(self, typeid): 318 | elem = self.root.find(f"*[@id='{typeid}']") 319 | if elem is None: 320 | raise RuntimeError(f"No XML element with id attribute {typeid} identified") 321 | return elem 322 | 323 | def _get_elem_with_attrib(self, tag, attrib, value): 324 | elem = self.root.find(f"{tag}[@{attrib}='{value}']") 325 | if elem is None: 326 | raise RuntimeError( 327 | f"No {tag} XML element with {attrib} attribute {value} identified" 328 | ) 329 | return elem 330 | 331 | def _get_typedef_name(self, type_id): 332 | """Find out the typedef name of a type which do not have a name""" 333 | 334 | # First check if there is a connected ElaboratedType element 335 | try: 336 | type_id = self._get_elem_with_attrib( 337 | "ElaboratedType", "type", type_id 338 | ).attrib["id"] 339 | except Exception: 340 | pass 341 | 342 | # Now find the TypeDef element connected to the type or ElaboratedType element 343 | name = "" 344 | try: 345 | name = self._get_elem_with_attrib("Typedef", "type", type_id).attrib["name"] 346 | except Exception: 347 | name = f"anonymous_{self._anonymous_count}" 348 | self._anonymous_count += 1 349 | return name 350 | 351 | def _fundamental_type_to_pycstruct_type(self, elem, is_array): 352 | """Map the fundamental type to pycstruct type""" 353 | typename = elem.attrib["name"] 354 | typesize = elem.attrib["size"] 355 | pycstruct_type_name = "int" 356 | if "float" in typename or "double" in typename: 357 | pycstruct_type_name = "float" 358 | elif is_array and "char" in typename: 359 | if "unsigned" in typename: 360 | # "unsigned char[]" are considered uint8 array 361 | pycstruct_type_name = "uint" 362 | elif "signed" in typename: 363 | # "signed char[]" are considered int8 array 364 | pycstruct_type_name = "int" 365 | else: 366 | # "char[]" are considered UTF-8 data (string) 367 | pycstruct_type_name = "utf-" 368 | elif "unsigned" in typename: 369 | pycstruct_type_name = "uint" 370 | else: 371 | pycstruct_type_name = "int" 372 | 373 | return f"{pycstruct_type_name}{typesize}" 374 | 375 | def _get_basic_type_element(self, type_id): 376 | """Finds the basic type element possible hidden behind TypeDef's or ElaboratedType's""" 377 | elem = self._get_elem_with_id(type_id) 378 | while elem.tag in ("Typedef", "ElaboratedType"): 379 | elem = self._get_elem_with_id(elem.attrib["type"]) 380 | return elem 381 | 382 | def _get_type(self, type_id, is_array=False): 383 | elem = self._get_basic_type_element(type_id) 384 | 385 | member_type = {} 386 | 387 | if elem.tag == "ArrayType": 388 | size = int(elem.attrib["max"]) - int(elem.attrib["min"]) + 1 389 | subtype = self._get_type(elem.attrib["type"], is_array=True) 390 | shape = (size,) 391 | if "shape" in subtype: 392 | shape = shape + subtype["shape"] 393 | member_type["shape"] = shape 394 | member_type["type_name"] = subtype["type_name"] 395 | if "reference" in subtype: 396 | member_type["reference"] = subtype["reference"] 397 | return member_type 398 | 399 | if elem.tag == "CvQualifiedType": # volatile 400 | elem = self._get_basic_type_element(elem.attrib["type"]) 401 | 402 | if elem.tag == "FundamentalType": 403 | member_type["type_name"] = self._fundamental_type_to_pycstruct_type( 404 | elem, is_array 405 | ) 406 | elif elem.tag == "PointerType": 407 | member_type["type_name"] = f"uint{elem.attrib['size']}" 408 | elif elem.tag == "Struct": 409 | member_type["type_name"] = "struct" 410 | member_type["reference"] = elem.attrib["id"] 411 | elif elem.tag == "Union": 412 | member_type["type_name"] = "union" 413 | member_type["reference"] = elem.attrib["id"] 414 | elif elem.tag == "Enumeration": 415 | member_type["type_name"] = "enum" 416 | member_type["reference"] = elem.attrib["id"] 417 | else: 418 | raise RuntimeError(f"Member type {elem.tag} is not supported.") 419 | 420 | return member_type 421 | 422 | 423 | ############################################################################### 424 | # _TypeMetaParser class (internal) 425 | 426 | 427 | class _TypeMetaParser: 428 | """This class takes a dictionary with metadata about the types and 429 | generate pycstruct instances. 430 | """ 431 | 432 | # pylint: disable=too-few-public-methods, broad-except 433 | 434 | def __init__(self, type_meta, byteorder): 435 | self._type_meta = type_meta 436 | self._instances = {} 437 | self._byteorder = byteorder 438 | 439 | def parse(self): 440 | """Parse the type_meta file provided in the constructor""" 441 | for name, datatype in self._type_meta.items(): 442 | if datatype["supported"]: 443 | try: 444 | self._to_instance(name) 445 | except Exception as exception: 446 | logger.warning( 447 | """Unable to convert %s, type %s, to pycstruct defintion: 448 | - %s 449 | - Type will be ignored.""", 450 | name, 451 | datatype["type"], 452 | str(exception.args[0]), 453 | ) 454 | datatype["supported"] = False 455 | return self._instances 456 | 457 | def _to_instance(self, name): 458 | """Create a pycstruct instance of type with name. Will recursively 459 | create instances of referenced types. 460 | 461 | Returns the instance. 462 | """ 463 | # pylint: disable=too-many-branches 464 | if name in self._instances: 465 | return self._instances[name] # Parsed before 466 | 467 | meta = self._type_meta[name] 468 | 469 | if not meta["supported"]: 470 | return None # Not supported 471 | 472 | instance = None 473 | 474 | # Structs or union 475 | if meta["type"] == "struct" or meta["type"] == "union": 476 | is_union = meta["type"] == "union" 477 | instance = StructDef(self._byteorder, meta["align"], union=is_union) 478 | for member in meta["members"]: 479 | shape = member.get("shape", None) 480 | if "reference" in member: 481 | other_instance = self._to_instance(member["reference"]) 482 | if other_instance is None: 483 | raise RuntimeError( 484 | f"Member {member['name']} is of type {member['type']} " 485 | f"{member['reference']} that is not supported" 486 | ) 487 | same_level = False 488 | if ("same_level" in member) and member["same_level"]: 489 | same_level = True 490 | instance.add( 491 | other_instance, 492 | member["name"], 493 | shape=shape, 494 | same_level=same_level, 495 | ) 496 | else: 497 | instance.add(member["type"], member["name"], shape=shape) 498 | 499 | # Enum 500 | elif meta["type"] == "enum": 501 | instance = EnumDef(self._byteorder, meta["size"], meta["signed"]) 502 | for member in meta["members"]: 503 | instance.add(member["name"], member["value"]) 504 | 505 | # Bitfield 506 | elif meta["type"] == "bitfield": 507 | instance = BitfieldDef(self._byteorder, meta["size"]) 508 | for member in meta["members"]: 509 | instance.add(member["name"], member["bits"], member["signed"]) 510 | 511 | # Not supported 512 | else: 513 | logger.warning( 514 | "Unable to create instance for %s (type %s). Not supported.", 515 | meta["name"], 516 | meta["type"], 517 | ) 518 | meta["supported"] = False 519 | return None 520 | 521 | # Sanity check size: 522 | if meta["size"] != instance.size(): 523 | logger.warning( 524 | "%s size, %s, does match indicated size %s", 525 | meta["name"], 526 | instance.size(), 527 | meta["size"], 528 | ) 529 | 530 | self._instances[name] = instance 531 | return instance 532 | 533 | 534 | ############################################################################### 535 | # Public functions 536 | 537 | 538 | def parse_file( 539 | input_files, 540 | byteorder="native", 541 | castxml_cmd="castxml", 542 | castxml_extra_args=None, 543 | cache_path="", 544 | use_cached=False, 545 | **subprocess_kwargs, 546 | ): 547 | """Parse one or more C source files (C or C++) and generate pycstruct 548 | instances as a result. 549 | 550 | The result is a dictionary where the keys are the names of the 551 | struct, unions etc. typedef'ed names are also supported. 552 | 553 | The values of the resulting dictionary are the actual pycstruct 554 | instance connected to the name. 555 | 556 | This function requires that the external tool 557 | `castxml `_ is installed. 558 | 559 | Alignment will automatically be detected and configured for the pycstruct 560 | instances. 561 | 562 | Note that following pycstruct types will be used for char arrays: 563 | 564 | - 'unsigned char []' = uint8 array 565 | - 'signed char []' = int8 array 566 | - 'char []' = utf-8 data (string) 567 | 568 | :param input_files: Source file name or a list of file names. 569 | :type input_files: str or list 570 | :param byteorder: Byteorder of all elements Valid values are 'native', 571 | 'little' and 'big'. If not specified the 'native' 572 | byteorder is used. 573 | :type byteorder: str, optional 574 | :param castxml_cmd: Path to the castxml binary. If not specified 575 | castxml must be within the PATH. 576 | :type castxml_cmd: str, optional 577 | :param castxml_extra_args: Extra arguments to provide to castxml. 578 | For example definitions etc. Check 579 | castxml documentation for which 580 | arguments that are supported. 581 | :type castxml_extra_args: list, optional 582 | :param cache_path: Path where to store temporary files. If not 583 | provided, the default system temporary 584 | directory is used. 585 | :type cache_path: str, optional 586 | :param use_cached: If this is True, use previously cached 587 | output from castxml to avoid re-running 588 | castxml (since it could be time consuming). 589 | Default is False. 590 | :type use_cached: boolean, optional 591 | :param subprocess_kwargs: keyword arguments that will be passed down to the 592 | `subprocess.check_outputs()` call used to run 593 | castxml. By default, stderr will be redirected to 594 | stdout. To get the subprocess default behavior, 595 | pass `stderr=None`. 596 | :return: A dictionary keyed on names of the structs, unions 597 | etc. The values are the actual pycstruct instances. 598 | :rtype: dict 599 | """ 600 | # pylint: disable=too-many-arguments 601 | 602 | input_files = _listify(input_files) 603 | xml_filename = _get_hash(input_files) + ".xml" 604 | 605 | if castxml_extra_args is None: 606 | castxml_extra_args = [] 607 | 608 | if cache_path == "": 609 | # Use temporary path to store xml 610 | cache_path = tempfile.gettempdir() 611 | 612 | xml_path = os.path.join(cache_path, xml_filename) 613 | 614 | # Generate XML 615 | if not use_cached or not os.path.isfile(xml_path): 616 | _run_castxml( 617 | input_files, xml_path, castxml_cmd, castxml_extra_args, **subprocess_kwargs 618 | ) 619 | 620 | # Parse XML 621 | castxml_parser = _CastXmlParser(xml_path) 622 | type_meta = castxml_parser.parse() 623 | 624 | # Generate pycstruct instances 625 | type_meta_parser = _TypeMetaParser(type_meta, byteorder) 626 | pycstruct_instances = type_meta_parser.parse() 627 | 628 | return pycstruct_instances 629 | 630 | 631 | def parse_str( 632 | c_str, 633 | byteorder="native", 634 | castxml_cmd="castxml", 635 | castxml_extra_args=None, 636 | cache_path="", 637 | use_cached=False, 638 | ): 639 | """Parse a string containing C source code, such as struct or 640 | union defintions. Any valid C code is supported. 641 | 642 | The result is a dictionary where the keys are the names of the 643 | struct, unions etc. typedef'ed names are also supported. 644 | 645 | The values of the resulting dictionary are the actual pycstruct 646 | instance connected to the name. 647 | 648 | This function requires that the external tool 649 | `castxml `_ is installed. 650 | 651 | Alignment will automatically be detected and configured for the pycstruct 652 | instances. 653 | 654 | Note that following pycstruct types will be used for char arrays: 655 | 656 | - 'unsigned char []' = uint8 array 657 | - 'signed char []' = int8 array 658 | - 'char []' = utf-8 data (string) 659 | 660 | :param c_str: A string of C source code. 661 | :type c_str: str 662 | :param byteorder: Byteorder of all elements Valid values are 'native', 663 | 'little' and 'big'. If not specified the 'native' 664 | byteorder is used. 665 | :type byteorder: str, optional 666 | :param castxml_cmd: Path to the castxml binary. If not specified 667 | castxml must be within the PATH. 668 | :type castxml_cmd: str, optional 669 | :param castxml_extra_args: Extra arguments to provide to castxml. 670 | For example definitions etc. Check 671 | castxml documentation for which 672 | arguments that are supported. 673 | :type castxml_extra_args: list, optional 674 | :param cache_path: Path where to store temporary files. If not 675 | provided, the default system temporary 676 | directory is used. 677 | :type cache_path: str, optional 678 | :param use_cached: If this is True, use previously cached 679 | output from castxml to avoid re-running 680 | castxml (since it could be time consuming). 681 | Default is False. 682 | :type use_cached: boolean, optional 683 | :return: A dictionary keyed on names of the structs, unions 684 | etc. The values are the actual pycstruct instances. 685 | :rtype: dict 686 | """ 687 | # pylint: disable=too-many-arguments 688 | 689 | if castxml_extra_args is None: 690 | castxml_extra_args = [] 691 | 692 | if cache_path == "": 693 | # Use temporary path to store xml 694 | cache_path = tempfile.gettempdir() 695 | 696 | c_filename = _get_hash([c_str]) + ".c" 697 | c_path = os.path.join(cache_path, c_filename) 698 | 699 | with open(c_path, "w", encoding="utf-8") as file: 700 | file.write(c_str) 701 | 702 | return parse_file( 703 | c_path, byteorder, castxml_cmd, castxml_extra_args, cache_path, use_cached 704 | ) 705 | -------------------------------------------------------------------------------- /pycstruct/instance.py: -------------------------------------------------------------------------------- 1 | """pycstruct instance 2 | 3 | Copyright 2021 by Joel Midstjärna. 4 | All rights reserved. 5 | This file is part of the pycstruct python library and is 6 | released under the "MIT License Agreement". Please see the LICENSE 7 | file that should have been included as part of this package. 8 | """ 9 | 10 | import pycstruct.pycstruct 11 | 12 | 13 | class Instance: 14 | """This class represents an Instance of either a :meth:`StructDef` or 15 | a :meth:`BitfieldDef`. The instance object contains a bytearray 16 | buffer where 'raw data' is stored. 17 | 18 | The Instance object has following advantages over using dictionary 19 | objects: 20 | 21 | - Data is only serialized/deserialized when accessed 22 | - Data is validated for each element/attribute access. I.e. you will 23 | get an exception if you try to set an element/attribute to a value 24 | that is not supported by the definition. 25 | - Data is accessed by attribute name instead of key indexing 26 | 27 | 28 | :param type: The :meth:`StructDef` class or :meth:`BitfieldDef` class 29 | that we would like to create an instance of. 30 | :type type: :meth:`StructDef` or :meth:`BitfieldDef` 31 | :param buffer: Byte buffer where data is stored. If no buffer is 32 | provided a new byte buffer will be created and the 33 | instance will be 'empty'. 34 | :type buffer: bytearray, optional 35 | :param buffer_offset: Start offset in the buffer. This means that you 36 | can have multiple Instances (or other data) that 37 | shares the same buffer. 38 | :type buffer_offset: int, optional 39 | """ 40 | 41 | def __init__(self, datatype, buffer=None, buffer_offset=0): 42 | if not isinstance(datatype, (pycstruct.StructDef, pycstruct.BitfieldDef)): 43 | raise RuntimeError("top_class needs to be of type StructDef or BitfieldDef") 44 | 45 | if buffer is None: 46 | buffer = bytearray(datatype.size()) 47 | 48 | # All private fields needs to be defined here to avoid 49 | # recursive calls to __setattr__ and __getattr__ 50 | super().__setattr__("_Instance__type", datatype) 51 | super().__setattr__("_Instance__buffer", buffer) 52 | super().__setattr__("_Instance__buffer_offset", buffer_offset) 53 | super().__setattr__("_Instance__attributes", datatype._element_names()) 54 | super().__setattr__("_Instance__subinstances", {}) 55 | 56 | if isinstance(datatype, pycstruct.StructDef): 57 | # Create "sub-instances" for nested structs/bitfields and lists 58 | for attribute in self.__attributes: 59 | subtype = datatype._element_type(attribute) 60 | if subtype is None: 61 | # Filter attribute from fields_same_level 62 | continue 63 | offset = datatype._element_offset(attribute) 64 | if hasattr(subtype, "instance"): 65 | instance = subtype.instance(self.__buffer, buffer_offset + offset) 66 | self.__subinstances[attribute] = instance 67 | 68 | def __getattr__(self, item): 69 | if item in self.__subinstances: 70 | return self.__subinstances[item] 71 | if item in self.__attributes: 72 | return self.__type._deserialize_element( 73 | item, self.__buffer, self.__buffer_offset 74 | ) 75 | raise AttributeError(f"Instance has no element {item}") 76 | 77 | def __setattr__(self, item, value): 78 | if item in self.__subinstances: 79 | raise AttributeError(f"You are not allowed to modify {item}") 80 | if item in self.__attributes: 81 | self.__type._serialize_element( 82 | item, value, self.__buffer, self.__buffer_offset 83 | ) 84 | else: 85 | raise AttributeError(f"Instance has no element {item}") 86 | 87 | def __bytes__(self): 88 | offset = self.__buffer_offset 89 | size = self.__type.size() 90 | return bytes(self.__buffer[offset : offset + size]) 91 | 92 | def __str__(self, prefix=""): 93 | result = [] 94 | for attribute in self.__attributes: 95 | if attribute in self.__subinstances: 96 | if isinstance(self.__subinstances[attribute], _InstanceList): 97 | result.append( 98 | self.__subinstances[attribute].__str__( 99 | f"{prefix}{attribute} : " 100 | ) 101 | ) 102 | else: 103 | result.append( 104 | self.__subinstances[attribute].__str__(f"{prefix}{attribute}.") 105 | ) 106 | else: 107 | result.append(f"{prefix}{attribute} : {self.__getattr__(attribute)}") 108 | return "\n".join(result) 109 | 110 | 111 | class _InstanceList: 112 | """This class represents a list within an Instance. Note that this 113 | is a private class and shall not be created manually. It is created 114 | as a support class of Instance class. 115 | 116 | It overrides __setitem__, __getitem__ so that it is possible to use 117 | indexing with []. 118 | 119 | :param parenttype: The StructDef class which contains the list element. 120 | :type parenttype: StructDef 121 | :param name: Name of the element in parenttype that contains the list 122 | we would like to represent. 123 | :type name: str 124 | :param buffer: Byte buffer where data is stored 125 | :type buffer: bytearray 126 | :param buffer_offset: Start offset in the buffer. Note that this should 127 | be the offset to where parenttype starts, not the 128 | actual list element (name) 129 | :type buffer_offset: int 130 | """ 131 | 132 | def __init__(self, arraytype, buffer, buffer_offset): 133 | assert isinstance(arraytype, pycstruct.ArrayDef) 134 | self.__arraytype = arraytype 135 | self.__buffer = buffer 136 | assert isinstance(buffer, (bytearray, bytes)) 137 | self.__buffer_offset = buffer_offset 138 | element_type = arraytype.type 139 | self.__has_subinstances = hasattr(element_type, "instance") 140 | self.__subinstances = {} 141 | 142 | def __get_subinstance(self, key): 143 | subinstance = self.__subinstances.get(key) 144 | if subinstance is None: 145 | buffer = self.__buffer 146 | element_type = self.__arraytype.type 147 | offset = self.__buffer_offset + key * element_type.size() 148 | subinstance = element_type.instance(buffer, offset) 149 | self.__subinstances[key] = subinstance 150 | return subinstance 151 | 152 | def __check_key(self, key): 153 | if not isinstance(key, int): 154 | raise KeyError(f"Invalid index: {key} - needs to be an integer") 155 | if key < 0 or key >= self.__arraytype.length: 156 | raise KeyError( 157 | f"Invalid index: {key} - supported 0 - {self.__arraytype.length}" 158 | ) 159 | 160 | def __getitem__(self, key): 161 | self.__check_key(key) 162 | if self.__has_subinstances: 163 | return self.__get_subinstance(key) 164 | return self.__arraytype._deserialize_element( 165 | key, self.__buffer, self.__buffer_offset 166 | ) 167 | 168 | def __setitem__(self, key, value): 169 | self.__check_key(key) 170 | if self.__has_subinstances: 171 | raise AttributeError( 172 | "You are not allowed to replace object. Use object properties." 173 | ) 174 | self.__arraytype._serialize_element( 175 | key, value, self.__buffer, self.__buffer_offset 176 | ) 177 | 178 | def __len__(self): 179 | return self.__arraytype.length 180 | 181 | def __bytes__(self): 182 | offset = self.__buffer_offset 183 | element_type = self.__arraytype.type 184 | size = self.__arraytype.length * element_type.size() 185 | return bytes(self.__buffer[offset : offset + size]) 186 | 187 | def __str__(self, prefix=""): 188 | elements = [] 189 | if self.__has_subinstances: 190 | indent = " " * len(prefix) 191 | for i in range(0, self.__arraytype.length): 192 | elements.append(self.__getitem__(i).__str__(indent)) 193 | elements_str = "\n" + ("\n" + indent + ",\n").join(elements) + "\n" + indent 194 | else: 195 | for i in range(0, self.__arraytype.length): 196 | elements.append(str(self.__getitem__(i))) 197 | elements_str = ", ".join(elements) 198 | 199 | return f"{prefix}[{elements_str}]" 200 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | try: 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | except: 7 | long_description = "" 8 | 9 | setup( 10 | name="pycstruct", 11 | version="0.12.2", 12 | description="Binary data handling in Python using dictionaries", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="http://github.com/midstar/pycstruct", 16 | project_urls={ 17 | "Bug Tracker": "https://github.com/midstar/pycstruct/issues", 18 | "Documentation": "https://pycstruct.readthedocs.io/en/latest/", 19 | "Source Code": "https://github.com/midstar/pycstruct", 20 | }, 21 | author="Joel Midstjärna", 22 | author_email="joel.midstjarna@gmail.com", 23 | keywords=["struct", "enum", "bitfield", "binary", "protocol", "dict", "dictionary"], 24 | license="MIT", 25 | packages=["pycstruct"], 26 | zip_safe=False, 27 | classifiers=[ 28 | "Development Status :: 3 - Alpha", 29 | "Intended Audience :: Developers", 30 | "Topic :: Software Development :: Build Tools", 31 | "License :: OSI Approved :: MIT License", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11" 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/bitfield_big.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/bitfield_big.dat -------------------------------------------------------------------------------- /tests/bitfield_little.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/bitfield_little.dat -------------------------------------------------------------------------------- /tests/bitfield_struct.c: -------------------------------------------------------------------------------- 1 | #include "bitfield_struct.h" 2 | #include 3 | #include 4 | 5 | 6 | 7 | int main() { 8 | Data d; 9 | memset(&d, 0, sizeof(Data)); 10 | 11 | d.m1 = -11111; 12 | d.bf1a = 2; 13 | d.bf1b = 3; 14 | d.m2 = 44; 15 | d.bf2a = 5; 16 | d.bf2b = 66; 17 | d.bf3a = 7; 18 | d.bf3b = 8; 19 | d.m3 = 99; 20 | 21 | int size = sizeof(Data); 22 | 23 | printf("Saving %d bytes to bitfield_struct.dat\n", size); 24 | FILE *f = fopen("bitfield_struct.dat", "w"); 25 | fwrite(&d, size, 1, f); 26 | fclose(f); 27 | 28 | return 0; 29 | } -------------------------------------------------------------------------------- /tests/bitfield_struct.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/bitfield_struct.dat -------------------------------------------------------------------------------- /tests/bitfield_struct.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This code assures no padding will be performed 3 | */ 4 | #pragma pack(1) 5 | 6 | typedef struct { 7 | int m1; 8 | unsigned int bf1a : 3; 9 | unsigned int bf1b : 5; 10 | unsigned char m2; 11 | int bf2a : 4; 12 | int bf2b : 10; 13 | unsigned char bf3a : 3; 14 | unsigned char bf3b : 5; 15 | long m3; 16 | } Data; -------------------------------------------------------------------------------- /tests/bitfield_struct.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/buildtest.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import json 3 | 4 | appveyor_name = "appveyor.yml" 5 | setup_name = "setup.py" 6 | doc_conf_name = "doc/source/conf.py" 7 | 8 | ############################################################################### 9 | # Get version specified in appveyor.yml file 10 | appveyor_version = "appveyor?" 11 | with open(appveyor_name, "r") as f: 12 | lines = f.readlines() 13 | for line in lines: 14 | if line.startswith("version:"): 15 | v_long = line.split()[1] 16 | v_parts = v_long.split(".") 17 | appveyor_version = f"{v_parts[0]}.{v_parts[1]}.{v_parts[2]}" 18 | break 19 | 20 | ############################################################################### 21 | # Get version specified in setup.py file 22 | setup_version = "setup?" 23 | with open(setup_name, "r") as f: 24 | lines = f.readlines() 25 | for line in lines: 26 | if line.strip().startswith("version="): 27 | setup_version = line.split('"')[1] 28 | 29 | ############################################################################### 30 | # Get version specified in conf.py file 31 | doc_conf_version = "conf?" 32 | with open(doc_conf_name, "r") as f: 33 | lines = f.readlines() 34 | for line in lines: 35 | if line.strip().startswith("version ="): 36 | doc_conf_version = line.split("'")[1] 37 | 38 | ############################################################################### 39 | # Compare 40 | if appveyor_version == setup_version: 41 | print(f"PASS: Version {setup_version} match in {appveyor_name} and {setup_name}") 42 | else: 43 | print( 44 | f"FAIL: {appveyor_name} version {appveyor_version} don't " 45 | f"match {setup_name} version {setup_version}" 46 | ) 47 | exit(1) 48 | if doc_conf_version == setup_version: 49 | print(f"PASS: Doc version {doc_conf_version} match in {doc_conf_name}") 50 | else: 51 | print(f"WARN: Doc version {doc_conf_version} don't match in {doc_conf_name}") 52 | print(" If document or interace was updated please update this version.") 53 | 54 | ############################################################################### 55 | # Fetch versions from PyPI and check if it already exists 56 | with urllib.request.urlopen("https://pypi.python.org/pypi/pycstruct/json") as url: 57 | package_data = json.loads(url.read().decode("utf-8")) 58 | if setup_version in package_data["releases"]: 59 | print( 60 | f"FAIL: Version {setup_version} is already " 61 | "available in PyPI. Please update version " 62 | f"in {appveyor_name} and {setup_name}" 63 | ) 64 | exit(1) 65 | print(f"PASS: Version {setup_version} is not available in PyPi") 66 | -------------------------------------------------------------------------------- /tests/embedded_struct.c: -------------------------------------------------------------------------------- 1 | #ifndef NO_PACK 2 | 3 | /** 4 | * This code assures no padding will be performed 5 | */ 6 | #pragma pack(1) 7 | 8 | const char *out_file = "embedded_struct.dat"; 9 | 10 | #else 11 | 12 | const char *out_file = "embedded_struct_nopack.dat"; 13 | 14 | #endif // NO_PACK 15 | 16 | #include 17 | #include 18 | 19 | enum car_type { Sedan=0, Station_Wagon=5, Bus=7, Pickup=12}; 20 | 21 | struct sedan_properties_s { 22 | unsigned short sedan_code; 23 | }; 24 | 25 | struct station_wagon_properties_s { 26 | int trunk_volume; 27 | }; 28 | 29 | struct bus_properties_s { 30 | int number_of_passangers; 31 | unsigned short number_of_entries; 32 | unsigned char is_accordion_bus; 33 | }; 34 | 35 | struct pickup_properties_s { 36 | int truck_bed_volume; 37 | }; 38 | 39 | union type_specific_properties_u { 40 | struct sedan_properties_s sedan; 41 | struct station_wagon_properties_s station_wagon; 42 | struct bus_properties_s bus; 43 | struct pickup_properties_s pickup; 44 | }; 45 | 46 | 47 | struct car_properties_s { 48 | unsigned int env_class : 3; 49 | unsigned int registered : 1; 50 | unsigned int over_3500_kg : 1; 51 | }; 52 | 53 | struct car_s 54 | { 55 | unsigned short year; 56 | char model[50]; 57 | char registration_number[10]; 58 | struct car_properties_s properties; 59 | enum car_type type; 60 | union type_specific_properties_u type_properties; 61 | }; 62 | 63 | struct garage_s 64 | { 65 | struct car_s cars[20]; 66 | unsigned char nbr_registered_parkings; 67 | }; 68 | 69 | struct house_s { 70 | unsigned char nbr_of_levels; 71 | struct garage_s garage; 72 | }; 73 | 74 | int main() { 75 | struct house_s house; 76 | 77 | memset(&house, 0, sizeof(struct house_s)); 78 | house.nbr_of_levels = 5; 79 | house.garage.nbr_registered_parkings = 3; 80 | 81 | house.garage.cars[0].year = 2011; 82 | house.garage.cars[0].properties.env_class = 0; 83 | house.garage.cars[0].properties.registered = 1; 84 | house.garage.cars[0].properties.over_3500_kg = 0; 85 | house.garage.cars[0].type = Sedan; 86 | house.garage.cars[0].type_properties.sedan.sedan_code = 20; 87 | 88 | strcpy(house.garage.cars[0].registration_number, "AHF432"); 89 | strcpy(house.garage.cars[0].model, "Nissan Micra"); 90 | 91 | house.garage.cars[1].year = 2005; 92 | house.garage.cars[1].properties.env_class = 1; 93 | house.garage.cars[1].properties.registered = 1; 94 | house.garage.cars[1].properties.over_3500_kg = 1; 95 | house.garage.cars[1].type = Bus; 96 | house.garage.cars[1].type_properties.bus.number_of_passangers = 44; 97 | house.garage.cars[1].type_properties.bus.number_of_entries = 3; 98 | house.garage.cars[1].type_properties.bus.is_accordion_bus = 0; 99 | strcpy(house.garage.cars[1].registration_number, "CCO544"); 100 | strcpy(house.garage.cars[1].model, "Ford Focus"); 101 | 102 | house.garage.cars[2].year = 1998; 103 | house.garage.cars[2].properties.env_class = 3; 104 | house.garage.cars[2].properties.registered = 0; 105 | house.garage.cars[2].properties.over_3500_kg = 0; 106 | house.garage.cars[2].type = Pickup; 107 | house.garage.cars[2].type_properties.pickup.truck_bed_volume = 155; 108 | strcpy(house.garage.cars[2].registration_number, "HHT434"); 109 | strcpy(house.garage.cars[2].model, "Volkswagen Golf"); 110 | 111 | printf("Size car_type: %lu\n", sizeof(enum car_type)); 112 | printf("Size type_specific_properties_u: %lu\n", sizeof(union type_specific_properties_u)); 113 | printf("Size car_properties_s: %lu\n", sizeof(struct car_properties_s)); 114 | printf("Size car_s: %lu\n", sizeof(struct car_s)); 115 | printf("Size garage_s: %lu\n", sizeof(struct garage_s)); 116 | printf("Size house_s: %lu\n", sizeof(struct house_s)); 117 | 118 | printf("Saving %s\n", out_file); 119 | FILE *f = fopen(out_file, "w"); 120 | fwrite(&house, sizeof(struct house_s), 1, f); 121 | fclose(f); 122 | 123 | return 0; 124 | } 125 | -------------------------------------------------------------------------------- /tests/embedded_struct.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/embedded_struct.dat -------------------------------------------------------------------------------- /tests/embedded_struct_nopack.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/embedded_struct_nopack.dat -------------------------------------------------------------------------------- /tests/ndim_array_struct.c: -------------------------------------------------------------------------------- 1 | #include "ndim_array_struct.h" 2 | #include 3 | #include 4 | 5 | 6 | 7 | int main() { 8 | int x; 9 | int y; 10 | Data d; 11 | memset(&d, 0, sizeof(Data)); 12 | 13 | memset(&d, 0, sizeof(Color)); 14 | 15 | for (x = 0; x < 4; x++) { 16 | for (y = 0; y < 2; y++) { 17 | sprintf(d.array_of_strings[x][y], "%d x %d = %d", x, y, x * y); 18 | d.array_of_struct[x][y].r = x; 19 | d.array_of_struct[x][y].g = y; 20 | d.array_of_struct[x][y].b = x * 2 + y; 21 | d.array_of_struct[x][y].a = 255; 22 | } 23 | } 24 | 25 | // check the natural C order 26 | unsigned int static_array[4][2] = {1, 2, 3, 4, 5, 6, 7, 8}; 27 | memcpy(d.array_of_int, static_array, sizeof(static_array)); 28 | 29 | int size = sizeof(Data); 30 | 31 | printf("Saving %d bytes to ndim_array_struct.dat\n", size); 32 | FILE *f = fopen("ndim_array_struct.dat", "w"); 33 | fwrite(&d, size, 1, f); 34 | fclose(f); 35 | 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /tests/ndim_array_struct.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/ndim_array_struct.dat -------------------------------------------------------------------------------- /tests/ndim_array_struct.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This code assures no padding will be performed 3 | */ 4 | #pragma pack(1) 5 | 6 | typedef struct { 7 | unsigned char r; 8 | unsigned char g; 9 | unsigned char b; 10 | unsigned char a; 11 | } Color; 12 | 13 | typedef struct { 14 | char array_of_strings[4][2][16]; 15 | unsigned int array_of_int[4][2]; 16 | Color array_of_struct[4][2]; 17 | } Data; 18 | -------------------------------------------------------------------------------- /tests/ndim_array_struct.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/savebitfield.c: -------------------------------------------------------------------------------- 1 | /** 2 | * This code assures no padding will be performed 3 | */ 4 | #pragma pack(1) 5 | 6 | #include 7 | #include 8 | 9 | #define TRUE 1 10 | #define FALSE 0 11 | 12 | void SwapBytes(void *pv, size_t n) 13 | { 14 | char *p = pv; 15 | size_t lo, hi; 16 | for(lo=0, hi=n-1; hi>lo; lo++, hi--) 17 | { 18 | char tmp=p[lo]; 19 | p[lo] = p[hi]; 20 | p[hi] = tmp; 21 | } 22 | } 23 | #define SWAP(x) SwapBytes(&x, sizeof(x)); 24 | 25 | typedef struct { 26 | unsigned int onebit : 1; 27 | unsigned int twobits : 2; 28 | unsigned int threebits : 3; 29 | unsigned int fourbits : 4; 30 | signed int fivesignedbits : 5; 31 | unsigned int eightbits : 8; 32 | signed int eightsignedbits : 8; 33 | signed int onesignedbit : 1; 34 | signed int foursignedbits : 4; 35 | signed int sixteensignedbits: 16; 36 | unsigned int fivebits : 5; 37 | } Data; 38 | 39 | int main() { 40 | Data d; 41 | memset(&d, 0, sizeof(Data)); 42 | 43 | d.onebit = 1; 44 | d.twobits = 3; 45 | d.threebits = 1; 46 | d.fourbits = 3; 47 | d.fivesignedbits = -2; 48 | d.eightbits = 255; 49 | d.eightsignedbits = -128; 50 | d.onesignedbit = -1; 51 | d.foursignedbits = 5; 52 | d.sixteensignedbits = -12345; 53 | d.fivebits = 16; 54 | 55 | int size = sizeof(Data); 56 | 57 | printf("Saving %d bytes to bitfield_little.dat\n", size); 58 | FILE *f = fopen("bitfield_little.dat", "w"); 59 | fwrite(&d, size, 1, f); 60 | fclose(f); 61 | 62 | if(sizeof(unsigned long long) != size) 63 | { 64 | printf("ERROR! Big endian conversion will not work due to size error!"); 65 | return 1; 66 | } 67 | 68 | // Create a big endian version 69 | unsigned long long longvalue = *((unsigned long long *)&d); 70 | SWAP(longvalue); 71 | 72 | printf("Saving %d bytes to bitfield_big.dat\n", size); 73 | f = fopen("bitfield_big.dat", "w"); 74 | fwrite(&longvalue, size, 1, f); 75 | fclose(f); 76 | 77 | return 0; 78 | } -------------------------------------------------------------------------------- /tests/savestruct.c: -------------------------------------------------------------------------------- 1 | #ifndef NO_PACK 2 | 3 | /** 4 | * This code assures no padding will be performed 5 | */ 6 | #pragma pack(1) 7 | 8 | const char *out_file_little = "struct_little.dat"; 9 | const char *out_file_big = "struct_big.dat"; 10 | 11 | #else 12 | 13 | const char *out_file_little = "struct_little_nopack.dat"; 14 | const char *out_file_big = "struct_big_nopack.dat"; 15 | 16 | #endif // NO_PACK 17 | 18 | #include 19 | #include 20 | 21 | #define TRUE 1 22 | #define FALSE 0 23 | 24 | typedef signed char INT8; 25 | typedef unsigned char UINT8; 26 | typedef unsigned char BOOL8; 27 | 28 | typedef signed short INT16; 29 | typedef unsigned short UINT16; 30 | typedef unsigned short BOOL16; 31 | //typedef ? FLOAT16 - Not supprted 32 | 33 | typedef int INT32; 34 | typedef unsigned int UINT32; 35 | typedef unsigned int BOOL32; 36 | typedef float FLOAT32; 37 | 38 | typedef long long INT64; 39 | typedef unsigned long long UINT64; 40 | typedef unsigned long long BOOL64; 41 | typedef double FLOAT64; 42 | 43 | void SwapBytes(void *pv, size_t n) 44 | { 45 | char *p = pv; 46 | size_t lo, hi; 47 | for(lo=0, hi=n-1; hi>lo; lo++, hi--) 48 | { 49 | char tmp=p[lo]; 50 | p[lo] = p[hi]; 51 | p[hi] = tmp; 52 | } 53 | } 54 | #define SWAP(x) SwapBytes(&x, sizeof(x)); 55 | 56 | typedef struct { 57 | // 1 byte members 58 | INT8 int8_low; 59 | INT8 int8_high; 60 | UINT8 uint8_low; 61 | UINT8 uint8_high; 62 | BOOL8 bool8_false; 63 | BOOL8 bool8_true; 64 | 65 | // 2 byte members 66 | INT16 int16_low; 67 | INT16 int16_high; 68 | UINT16 uint16_low; 69 | UINT16 uint16_high; 70 | BOOL16 bool16_false; 71 | BOOL16 bool16_true; 72 | 73 | // 4 byte members 74 | INT32 int32_low; 75 | INT32 int32_high; 76 | UINT32 uint32_low; 77 | UINT32 uint32_high; 78 | BOOL32 bool32_false; 79 | BOOL32 bool32_true; 80 | FLOAT32 float32_low; 81 | FLOAT32 float32_high; 82 | 83 | // 8 byte members 84 | INT64 int64_low; 85 | INT64 int64_high; 86 | UINT64 uint64_low; 87 | UINT64 uint64_high; 88 | BOOL64 bool64_false; 89 | BOOL64 bool64_true; 90 | FLOAT64 float64_low; 91 | FLOAT64 float64_high; 92 | 93 | // Array 94 | INT32 int32_array[5]; 95 | 96 | // UTF-8 strings 97 | char utf8_ascii[100]; 98 | char utf8_nonascii[80]; 99 | char utf8_no_term[4]; 100 | } Data; 101 | 102 | int main() { 103 | Data d; 104 | memset(&d, 0, sizeof(Data)); 105 | 106 | d.int8_low = -128; 107 | d.int8_high = 127; 108 | d.uint8_low = 0; 109 | d.uint8_high = 255; 110 | d.bool8_false = FALSE; 111 | d.bool8_true = TRUE; 112 | 113 | d.int16_low = -32768; 114 | d.int16_high = 32767; 115 | d.uint16_low = 0; 116 | d.uint16_high = 65535; 117 | d.bool16_false = FALSE; 118 | d.bool16_true = TRUE; 119 | 120 | d.int32_low = -2147483648; 121 | d.int32_high = 2147483647; 122 | d.uint32_low = 0; 123 | d.uint32_high = 4294967295; 124 | d.bool32_false = FALSE; 125 | d.bool32_true = TRUE; 126 | d.float32_low = 1.23456; 127 | d.float32_high = 12345.6; 128 | 129 | d.int64_low = -9223372036854775808; 130 | d.int64_high = 9223372036854775807; 131 | d.uint64_low = 0; 132 | d.uint64_high = 18446744073709551615; 133 | d.bool64_false = FALSE; 134 | d.bool64_true = TRUE; 135 | d.float64_low = 1.23456789; 136 | d.float64_high = 12345678.9; 137 | 138 | for (int i=0 ; i < 5; i++) 139 | d.int32_array[i] = i; 140 | 141 | strcpy(d.utf8_ascii, "This is a normal ASCII string!"); 142 | strcpy(d.utf8_nonascii, "This string has special characters ÅÄÖü"); 143 | d.utf8_no_term[0] = 'A'; 144 | d.utf8_no_term[1] = 'B'; 145 | d.utf8_no_term[2] = 'C'; 146 | d.utf8_no_term[3] = 'D'; 147 | 148 | printf("Saving %s\n", out_file_little); 149 | FILE *f = fopen(out_file_little, "w"); 150 | fwrite(&d, sizeof(Data), 1, f); 151 | fclose(f); 152 | 153 | // Create a big endian version 154 | 155 | SWAP(d.int16_low); 156 | SWAP(d.int16_high); 157 | SWAP(d.uint16_low); 158 | SWAP(d.uint16_high); 159 | SWAP(d.bool16_false); 160 | SWAP(d.bool16_true); 161 | 162 | SWAP(d.int32_low); 163 | SWAP(d.int32_high); 164 | SWAP(d.uint32_low); 165 | SWAP(d.uint32_high); 166 | SWAP(d.bool32_false); 167 | SWAP(d.bool32_true); 168 | SWAP(d.float32_low); 169 | SWAP(d.float32_high); 170 | 171 | SWAP(d.int64_low); 172 | SWAP(d.int64_high); 173 | SWAP(d.uint64_low); 174 | SWAP(d.uint64_high); 175 | SWAP(d.bool64_false); 176 | SWAP(d.bool64_true); 177 | SWAP(d.float64_low); 178 | SWAP(d.float64_high); 179 | 180 | for (int i=0 ; i < 5; i++) 181 | SWAP(d.int32_array[i]); 182 | 183 | printf("Saving %s\n", out_file_big); 184 | f = fopen(out_file_big, "w"); 185 | fwrite(&d, sizeof(Data), 1, f); 186 | fclose(f); 187 | 188 | return 0; 189 | } -------------------------------------------------------------------------------- /tests/special_cases.h: -------------------------------------------------------------------------------- 1 | struct name_conflict { 2 | int member; 3 | }; 4 | 5 | typedef union { 6 | int member1; 7 | int member2; 8 | }name_conflict; 9 | 10 | struct with_volatile { 11 | volatile int volatile_member; 12 | volatile int volatile_array[2]; 13 | }; 14 | 15 | enum filled_enum { 16 | emem1 = 0, 17 | emem2 = 1, 18 | emem3 = 2, 19 | emem_fill = 0xFFFFFFFF 20 | }; 21 | 22 | enum signed_enum { 23 | semem1 = -1, 24 | semem2 = 1, 25 | semem3 = 2, 26 | semem_fill = 0xFFFFFFFF 27 | }; 28 | 29 | struct different_char_arrays { 30 | char char_array[10]; 31 | unsigned char unsigned_char_array[10]; 32 | signed char signed_char_array[10]; 33 | }; 34 | 35 | struct struct_with_struct_inside { 36 | struct struct_inside { 37 | int inside_a; 38 | int inside_b; 39 | } inside; 40 | }; -------------------------------------------------------------------------------- /tests/special_cases.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/struct_big.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/struct_big.dat -------------------------------------------------------------------------------- /tests/struct_big_nopack.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/struct_big_nopack.dat -------------------------------------------------------------------------------- /tests/struct_little.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/struct_little.dat -------------------------------------------------------------------------------- /tests/struct_little_nopack.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midstar/pycstruct/2c21f37b9f7aaaae4634f3e014e0dcc8f06850b3/tests/struct_little_nopack.dat -------------------------------------------------------------------------------- /tests/test_cparser.py: -------------------------------------------------------------------------------- 1 | import unittest, os, sys, shutil, tempfile, test_pycstruct 2 | from unittest import mock 3 | 4 | test_dir = os.path.dirname(os.path.realpath(__file__)) 5 | proj_dir = os.path.dirname(test_dir) 6 | 7 | sys.path.append(proj_dir) 8 | import pycstruct 9 | 10 | 11 | class TestCParser(unittest.TestCase): 12 | def test_run_castxml_invalid(self): 13 | _run_castxml = pycstruct.cparser._run_castxml 14 | _get_hash = pycstruct.cparser._get_hash 15 | 16 | input_files = ["dont_exist.c"] 17 | 18 | # Non existing command 19 | self.assertRaises( 20 | Exception, _run_castxml, input_files, "out.xml", castxml_cmd="dontexist" 21 | ) 22 | 23 | # Existing but failing 24 | self.assertRaises( 25 | Exception, _run_castxml, input_files, "out.xml", castxml_cmd="python3" 26 | ) 27 | 28 | # Will not fail command execution but no XML produced 29 | self.assertRaises( 30 | Exception, _run_castxml, input_files, "out.xml", castxml_cmd="echo" 31 | ) 32 | 33 | # This one will pass - fake output 34 | _ = _get_hash(input_files) 35 | 36 | def test_get_hash(self): 37 | _get_hash = pycstruct.cparser._get_hash 38 | 39 | hash = _get_hash(["one"]) 40 | self.assertEqual(len(hash), 10) 41 | 42 | hash2 = _get_hash(["one", "two"]) 43 | self.assertEqual(len(hash2), 10) 44 | 45 | self.assertNotEqual(hash, hash2) 46 | 47 | hash3 = _get_hash(["one", "two"]) 48 | self.assertEqual(hash3, hash2) 49 | 50 | def test_listify(self): 51 | _listify = pycstruct.cparser._listify 52 | 53 | alist = ["hello"] 54 | another_list = _listify(alist) 55 | self.assertEqual(alist, another_list) 56 | 57 | from_str_list = _listify("hello") 58 | self.assertEqual(from_str_list, alist) 59 | 60 | def test_xml_parse(self): 61 | _CastXmlParser = pycstruct.cparser._CastXmlParser 62 | parser = _CastXmlParser(os.path.join(test_dir, "savestruct.xml")) 63 | meta = parser.parse() 64 | self.assertTrue("Data" in meta) 65 | self.assertTrue(meta["Data"]["type"] == "struct") 66 | 67 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 68 | instance = type_meta_parser.parse() 69 | self.assertTrue("Data" in instance) 70 | self.assertTrue(isinstance(instance["Data"], pycstruct.StructDef)) 71 | 72 | test_pycstruct.check_struct(self, instance["Data"], "struct_little.dat") 73 | 74 | @mock.patch("pycstruct.cparser.logger") 75 | def test_xml_parse_exception_in_instance(self, logger): 76 | meta = {"foo": {"type": "bar", "supported": True}} 77 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 78 | type_meta_parser._to_instance = unittest.mock.Mock() 79 | type_meta_parser._to_instance.side_effect = Exception("Oups") 80 | instance = type_meta_parser.parse() 81 | logger.warning.assert_called_once() 82 | 83 | def test_xml_parse_nopack(self): 84 | _CastXmlParser = pycstruct.cparser._CastXmlParser 85 | parser = _CastXmlParser(os.path.join(test_dir, "savestruct_nopack.xml")) 86 | meta = parser.parse() 87 | self.assertTrue("Data" in meta) 88 | self.assertTrue(meta["Data"]["type"] == "struct") 89 | 90 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 91 | instance = type_meta_parser.parse() 92 | self.assertTrue("Data" in instance) 93 | self.assertTrue(isinstance(instance["Data"], pycstruct.StructDef)) 94 | 95 | test_pycstruct.check_struct(self, instance["Data"], "struct_little_nopack.dat") 96 | 97 | def test_xml_parse_special_cases(self): 98 | _CastXmlParser = pycstruct.cparser._CastXmlParser 99 | parser = _CastXmlParser(os.path.join(test_dir, "special_cases.xml")) 100 | sys.stderr.write( 101 | "\n\n>> Below test will result in a Warning. This is expected!\n\n" 102 | ) 103 | meta = parser.parse() 104 | sys.stderr.write("\n>> End of expected warning.\n") 105 | 106 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, byteorder="little") 107 | instance = type_meta_parser.parse() 108 | self.assertTrue("with_volatile" in instance) 109 | self.assertTrue("filled_enum" in instance) 110 | self.assertTrue("signed_enum" in instance) 111 | self.assertTrue("different_char_arrays" in instance) 112 | self.assertTrue("struct_with_struct_inside" in instance) 113 | self.assertTrue("struct_inside" in instance) 114 | 115 | # Check types of different_char_arrays members. 116 | # Since type cannot be read out from StructDef 117 | # for individual members we use the __str__ 118 | # representation 119 | char_array_str = str(instance["different_char_arrays"]) 120 | rows = char_array_str.splitlines() 121 | self.assertEqual(rows[1].split()[1], "utf-8") 122 | self.assertEqual(rows[2].split()[1], "uint8") 123 | self.assertEqual(rows[3].split()[1], "int8") 124 | 125 | # Check struct with struct inside 126 | s_dict = instance["struct_with_struct_inside"].create_empty_data() 127 | self.assertEqual(len(s_dict.keys()), 1) 128 | self.assertTrue("inside" in s_dict) 129 | self.assertEqual(len(s_dict["inside"].keys()), 2) 130 | self.assertTrue("inside_a" in s_dict["inside"]) 131 | self.assertTrue("inside_b" in s_dict["inside"]) 132 | 133 | # @unittest.skipIf(True, 'temporary skipped') 134 | def test_xml_parse_embedded(self): 135 | _CastXmlParser = pycstruct.cparser._CastXmlParser 136 | parser = _CastXmlParser(os.path.join(test_dir, "embedded_struct.xml")) 137 | meta = parser.parse() 138 | self.assertTrue("car_type" in meta) 139 | self.assertTrue(meta["car_type"]["type"] == "enum") 140 | self.assertTrue("sedan_properties_s" in meta) 141 | self.assertTrue(meta["sedan_properties_s"]["type"] == "struct") 142 | self.assertTrue("station_wagon_properties_s" in meta) 143 | self.assertTrue(meta["station_wagon_properties_s"]["type"] == "struct") 144 | self.assertTrue("bus_properties_s" in meta) 145 | self.assertTrue(meta["bus_properties_s"]["type"] == "struct") 146 | self.assertTrue("pickup_properties_s" in meta) 147 | self.assertTrue(meta["pickup_properties_s"]["type"] == "struct") 148 | self.assertTrue("type_specific_properties_u" in meta) 149 | self.assertTrue(meta["type_specific_properties_u"]["type"] == "union") 150 | self.assertTrue("car_properties_s" in meta) 151 | self.assertTrue(meta["car_properties_s"]["type"] == "bitfield") 152 | self.assertTrue("car_s" in meta) 153 | self.assertTrue(meta["car_s"]["type"] == "struct") 154 | self.assertTrue("garage_s" in meta) 155 | self.assertTrue(meta["garage_s"]["type"] == "struct") 156 | self.assertTrue("house_s" in meta) 157 | self.assertTrue(meta["house_s"]["type"] == "struct") 158 | 159 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 160 | instance = type_meta_parser.parse() 161 | self.assertTrue("car_type" in instance) 162 | self.assertTrue(isinstance(instance["car_type"], pycstruct.EnumDef)) 163 | self.assertTrue("type_specific_properties_u" in instance) 164 | self.assertTrue( 165 | isinstance(instance["type_specific_properties_u"], pycstruct.StructDef) 166 | ) 167 | self.assertEqual(instance["type_specific_properties_u"]._type_name(), "union") 168 | self.assertTrue("car_properties_s" in instance) 169 | self.assertTrue(isinstance(instance["car_properties_s"], pycstruct.BitfieldDef)) 170 | self.assertTrue("house_s" in instance) 171 | self.assertTrue(isinstance(instance["house_s"], pycstruct.StructDef)) 172 | self.assertEqual(instance["house_s"]._type_name(), "struct") 173 | 174 | test_pycstruct.check_embedded_struct( 175 | self, instance["house_s"], "embedded_struct.dat" 176 | ) 177 | 178 | def test_xml_parse_embedded_nopack(self): 179 | _CastXmlParser = pycstruct.cparser._CastXmlParser 180 | parser = _CastXmlParser(os.path.join(test_dir, "embedded_struct_nopack.xml")) 181 | meta = parser.parse() 182 | self.assertTrue("car_type" in meta) 183 | 184 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 185 | instance = type_meta_parser.parse() 186 | self.assertTrue("car_type" in instance) 187 | 188 | test_pycstruct.check_embedded_struct( 189 | self, instance["house_s"], "embedded_struct_nopack.dat" 190 | ) 191 | 192 | def test_xml_parse_bitfield_struct(self): 193 | _CastXmlParser = pycstruct.cparser._CastXmlParser 194 | parser = _CastXmlParser(os.path.join(test_dir, "bitfield_struct.xml")) 195 | meta = parser.parse() 196 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 197 | instance = type_meta_parser.parse() 198 | 199 | with open(os.path.join(test_dir, "bitfield_struct.dat"), "rb") as f: 200 | inbytes = f.read() 201 | result = instance["Data"].deserialize(inbytes) 202 | 203 | self.assertEqual(result["m1"], -11111) 204 | self.assertEqual(result["bf1a"], 2) 205 | self.assertEqual(result["bf1b"], 3) 206 | self.assertEqual(result["m2"], 44) 207 | self.assertEqual(result["bf2a"], 5) 208 | self.assertEqual(result["bf2b"], 66) 209 | self.assertEqual(result["bf3a"], 7) 210 | self.assertEqual(result["bf3b"], 8) 211 | self.assertEqual(result["m3"], 99) 212 | 213 | def test_xml_parse_ndim_array_struct(self): 214 | _CastXmlParser = pycstruct.cparser._CastXmlParser 215 | parser = _CastXmlParser(os.path.join(test_dir, "ndim_array_struct.xml")) 216 | meta = parser.parse() 217 | type_meta_parser = pycstruct.cparser._TypeMetaParser(meta, "little") 218 | instance = type_meta_parser.parse() 219 | 220 | with open(os.path.join(test_dir, "ndim_array_struct.dat"), "rb") as f: 221 | inbytes = f.read() 222 | result = instance["Data"].deserialize(inbytes) 223 | 224 | self.assertIn("array_of_int", result) 225 | self.assertEqual(result["array_of_int"], [[1, 2], [3, 4], [5, 6], [7, 8]]) 226 | 227 | self.assertIn("array_of_strings", result) 228 | self.assertEqual(result["array_of_strings"][0][0], "0 x 0 = 0") 229 | self.assertEqual(result["array_of_strings"][3][1], "3 x 1 = 3") 230 | 231 | self.assertIn("array_of_struct", result) 232 | self.assertEqual(result["array_of_struct"][0][0]["a"], 255) 233 | self.assertEqual(result["array_of_struct"][0][0]["b"], 0) 234 | self.assertEqual(result["array_of_struct"][1][0]["b"], 2) 235 | self.assertEqual(result["array_of_struct"][3][0]["b"], 6) 236 | self.assertEqual(result["array_of_struct"][3][1]["b"], 7) 237 | 238 | @unittest.skipIf(shutil.which("castxml") == None, "castxml is not installed") 239 | def test_run_castxml_real(self): 240 | _run_castxml = pycstruct.cparser._run_castxml 241 | input_files = [os.path.join(test_dir, "savestruct.c")] 242 | output_file = "test_output.xml" 243 | 244 | # Generate standard 245 | _run_castxml(input_files, output_file) 246 | self.assertTrue(os.path.isfile(output_file)) 247 | os.remove(output_file) 248 | 249 | # Valid extra arguments 250 | _run_castxml(input_files, output_file, castxml_extra_args=["-dI"]) 251 | self.assertTrue(os.path.isfile(output_file)) 252 | os.remove(output_file) 253 | 254 | # Invalid extra arguments 255 | self.assertRaises( 256 | Exception, 257 | _run_castxml, 258 | input_files, 259 | output_file, 260 | castxml_extra_args=["--invalid"], 261 | ) 262 | 263 | @unittest.skipIf(shutil.which("castxml") == None, "castxml is not installed") 264 | def test_run_parse_file_real(self): 265 | input_file = os.path.join(test_dir, "savestruct.c") 266 | 267 | # Use default cache 268 | result = pycstruct.parse_file(input_file) 269 | self.assertTrue("Data" in result) 270 | 271 | xml_filename = ( 272 | pycstruct.cparser._get_hash(pycstruct.cparser._listify(input_file)) + ".xml" 273 | ) 274 | xml_path = os.path.join(tempfile.gettempdir(), xml_filename) 275 | self.assertTrue(os.path.isfile(xml_path)) 276 | os.remove(xml_path) 277 | 278 | # Use custom cache path 279 | result = pycstruct.parse_file(input_file, cache_path=".") 280 | self.assertTrue("Data" in result) 281 | self.assertTrue(os.path.isfile(xml_filename)) 282 | first_timestamp = os.path.getmtime(xml_filename) 283 | 284 | # Re-run using cache path 285 | result = pycstruct.parse_file(input_file, cache_path=".", use_cached=True) 286 | self.assertTrue("Data" in result) 287 | self.assertTrue(os.path.isfile(xml_filename)) 288 | second_timestamp = os.path.getmtime(xml_filename) 289 | 290 | # Check that file was NOT updated 291 | self.assertEqual(first_timestamp, second_timestamp) 292 | 293 | # Re-run again not using cache path 294 | result = pycstruct.parse_file(input_file, cache_path=".", use_cached=False) 295 | self.assertTrue("Data" in result) 296 | self.assertTrue(os.path.isfile(xml_filename)) 297 | third_timestamp = os.path.getmtime(xml_filename) 298 | 299 | # Check that file WAS updated 300 | self.assertNotEqual(first_timestamp, third_timestamp) 301 | 302 | os.remove(xml_filename) 303 | 304 | @unittest.skipIf(shutil.which("castxml") == None, "castxml is not installed") 305 | def test_run_parse_str_real(self): 306 | c_str = """ 307 | struct a_struct { 308 | int member_a; 309 | int member_b; 310 | }; 311 | union a_union { 312 | int umember_a; 313 | int umember_b; 314 | }; 315 | """ 316 | 317 | result = pycstruct.parse_str(c_str) 318 | self.assertTrue("a_struct" in result) 319 | self.assertTrue("a_union" in result) 320 | 321 | 322 | if __name__ == "__main__": 323 | unittest.main() 324 | -------------------------------------------------------------------------------- /tests/test_dtype.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pycstruct import pycstruct 3 | 4 | try: 5 | import numpy 6 | except: 7 | numpy = None 8 | 9 | 10 | class TestDtype(unittest.TestCase): 11 | def setUp(self): 12 | if numpy is None: 13 | self.skipTest("numpy is not installed") 14 | 15 | def deserialize(self, type_t, data): 16 | r1 = type_t.deserialize(data) 17 | r2 = numpy.frombuffer(data, dtype=type_t.dtype(), count=1)[0] 18 | return r1, r2 19 | 20 | def test_uint32(self): 21 | type_t = pycstruct.BasicTypeDef("uint32", "little") 22 | data = b"\x01\x02\x03\x04" 23 | r1, r2 = self.deserialize(type_t, data) 24 | assert r1 == r2 25 | 26 | def test_float32(self): 27 | type_t = pycstruct.BasicTypeDef("float32", "little") 28 | data = b"\x01\x02\x03\x04" 29 | r1, r2 = self.deserialize(type_t, data) 30 | assert r1 == r2 31 | 32 | def test_rgba_struct(self): 33 | data = b"\x01\x02\x03\x00" 34 | type_t = pycstruct.StructDef() 35 | type_t.add("uint8", "r") 36 | type_t.add("uint8", "g") 37 | type_t.add("uint8", "b") 38 | type_t.add("uint8", "a") 39 | r1, r2 = self.deserialize(type_t, data) 40 | assert r1["r"] == r2["r"] 41 | assert r1["g"] == r2["g"] 42 | assert r1["b"] == r2["b"] 43 | assert r1["a"] == r2["a"] 44 | 45 | def test_string_in_struct(self): 46 | data = b"\x64\x64\x64\x64" 47 | type_t = pycstruct.StructDef() 48 | type_t.add("utf-8", "text", 4) 49 | r1, r2 = self.deserialize(type_t, data) 50 | assert r1["text"] == r2["text"].decode("ascii") 51 | 52 | def test_array_in_struct(self): 53 | data = b"\x01\x02\x03\x00" 54 | type_t = pycstruct.StructDef() 55 | type_t.add("uint8", "data", 4) 56 | r1, r2 = self.deserialize(type_t, data) 57 | numpy.testing.assert_array_equal(r1["data"], r2["data"]) 58 | 59 | def test_struct_in_struct(self): 60 | data = b"\x01\x02\x03\x00\x11\x12\x13\x10" 61 | rgba_t = pycstruct.StructDef() 62 | rgba_t.add("uint8", "r") 63 | rgba_t.add("uint8", "g") 64 | rgba_t.add("uint8", "b") 65 | rgba_t.add("uint8", "a") 66 | 67 | type_t = pycstruct.StructDef() 68 | type_t.add(rgba_t, "color1", 1) 69 | type_t.add(rgba_t, "color2", 1) 70 | 71 | r1, r2 = self.deserialize(type_t, data) 72 | assert r1["color1"]["r"] == r2["color1"]["r"] 73 | assert r1["color1"]["g"] == r2["color1"]["g"] 74 | assert r1["color2"]["r"] == r2["color2"]["r"] 75 | assert r1["color2"]["g"] == r2["color2"]["g"] 76 | 77 | def test_array_of_struct_in_struct(self): 78 | data = b"\x01\x02\x03\x00\x11\x12\x13\x10" 79 | rgba_t = pycstruct.StructDef() 80 | rgba_t.add("uint8", "r") 81 | rgba_t.add("uint8", "g") 82 | rgba_t.add("uint8", "b") 83 | rgba_t.add("uint8", "a") 84 | 85 | type_t = pycstruct.StructDef() 86 | type_t.add(rgba_t, "colors", 2) 87 | 88 | r1, r2 = self.deserialize(type_t, data) 89 | assert r1["colors"][0]["r"] == r2["colors"][0]["r"] 90 | assert r1["colors"][0]["g"] == r2["colors"][0]["g"] 91 | assert r1["colors"][1]["r"] == r2["colors"][1]["r"] 92 | assert r1["colors"][1]["g"] == r2["colors"][1]["g"] 93 | 94 | def test_union(self): 95 | data = b"\x01\x02\x03\x00" 96 | any_uint_t = pycstruct.StructDef(union=True) 97 | any_uint_t.add("uint8", "u1") 98 | any_uint_t.add("uint16", "u2") 99 | any_uint_t.add("uint32", "u4") 100 | 101 | r1, r2 = self.deserialize(any_uint_t, data) 102 | assert r1["u1"] == r2["u1"] 103 | assert r1["u2"] == r2["u2"] 104 | assert r1["u4"] == r2["u4"] 105 | 106 | def test_union_of_structs(self): 107 | data = b"\x01\x02\x03\x00" 108 | 109 | rgba_t = pycstruct.StructDef() 110 | rgba_t.add("uint8", "r") 111 | rgba_t.add("uint8", "g") 112 | rgba_t.add("uint8", "b") 113 | rgba_t.add("uint8", "a") 114 | 115 | color_t = pycstruct.StructDef(union=True) 116 | color_t.add("uint32", "uint32") 117 | color_t.add(rgba_t, "rgba") 118 | color_t.add("uint8", "ubyte", length=4) 119 | 120 | r1, r2 = self.deserialize(color_t, data) 121 | assert r1["uint32"] == r2["uint32"] 122 | numpy.testing.assert_array_equal(r1["ubyte"], r2["ubyte"]) 123 | assert r1["rgba"]["b"] == r2["rgba"]["b"] 124 | 125 | def test_pad_and_same_level(self): 126 | bitfield_t = pycstruct.BitfieldDef() 127 | bitfield_t.add("one_bit") 128 | type_t = pycstruct.StructDef(alignment=4) 129 | type_t.add("int8", "a") 130 | # Pad will be inserted here 131 | type_t.add("int32", "b") 132 | type_t.add(bitfield_t, "bf", same_level=True) 133 | assert len(type_t.dtype()) != 0 134 | 135 | def test_basictype(self): 136 | testdefs = [("bool8", "=b1"), ("int8", "=i1")] 137 | for testdef in testdefs: 138 | typedef, expected = testdef 139 | with self.subTest(typedef=typedef, expected=expected): 140 | type_t = pycstruct.StructDef() 141 | type_t.add(typedef, "a") 142 | d = type_t.dtype() 143 | self.assertEqual(d["formats"][0], expected) 144 | 145 | def test_unsupported_basictype(self): 146 | type_t = pycstruct.StructDef() 147 | type_t.add("bool16", "a") 148 | 149 | with self.assertRaisesRegex(Exception, "Basic type"): 150 | type_t.dtype() 151 | 152 | def test_unsupported_bitfields(self): 153 | bitfield_t = pycstruct.BitfieldDef() 154 | bitfield_t.add("one_bit") 155 | foo_t = pycstruct.StructDef() 156 | foo_t.add(bitfield_t, "thing") 157 | 158 | with self.assertRaisesRegex(Exception, "BitfieldDef"): 159 | foo_t.dtype() 160 | 161 | def test_unsupported_enum(self): 162 | enum_t = pycstruct.EnumDef() 163 | enum_t.add("first") 164 | foo_t = pycstruct.StructDef() 165 | foo_t.add(enum_t, "thing") 166 | 167 | with self.assertRaisesRegex(Exception, "EnumDef"): 168 | foo_t.dtype() 169 | -------------------------------------------------------------------------------- /tests/test_instance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pycstruct 3 | 4 | 5 | class TestInstance(unittest.TestCase): 6 | def test_instance(self): 7 | # First define a complex definition structure 8 | car_type = pycstruct.EnumDef(size=4) 9 | car_type.add("Sedan", 0) 10 | car_type.add("Station_Wagon", 5) 11 | car_type.add("Bus", 7) 12 | car_type.add("Pickup", 12) 13 | 14 | sedan_properties = pycstruct.StructDef() 15 | sedan_properties.add("uint16", "sedan_code") 16 | 17 | station_wagon_properties = pycstruct.StructDef() 18 | station_wagon_properties.add("int32", "trunk_volume") 19 | 20 | bus_properties = pycstruct.StructDef() 21 | bus_properties.add("int32", "number_of_passangers") 22 | bus_properties.add("uint16", "number_of_entries") 23 | bus_properties.add("bool8", "is_accordion_bus") 24 | 25 | pickup_properties = pycstruct.StructDef() 26 | pickup_properties.add("int32", "truck_bed_volume") 27 | 28 | type_specific_properties = pycstruct.StructDef(union=True) 29 | type_specific_properties.add(sedan_properties, "sedan") 30 | type_specific_properties.add(station_wagon_properties, "station_wagon") 31 | type_specific_properties.add(bus_properties, "bus") 32 | type_specific_properties.add(pickup_properties, "pickup") 33 | 34 | car_properties = pycstruct.BitfieldDef() 35 | car_properties.add("env_class", 3, signed=True) 36 | car_properties.add("registered", 1) 37 | car_properties.add("over_3500_kg", 1) 38 | 39 | car_properties2 = pycstruct.BitfieldDef() 40 | car_properties2.add("prop1", 7) 41 | car_properties2.add("prop2", 3) 42 | car_properties2.add("prop3", 2) 43 | 44 | car = pycstruct.StructDef() 45 | car.add("uint16", "year") 46 | car.add("utf-8", "model", length=50) 47 | car.add("utf-8", "registration_number", length=10) 48 | car.add("int32", "last_owners", length=10) 49 | car.add(car_properties, "properties") 50 | car.add(car_properties2, "properties2", same_level=True) 51 | car.add(car_type, "type") 52 | car.add(type_specific_properties, "type_properties") 53 | 54 | garage = pycstruct.StructDef() 55 | garage.add(car, "cars", length=20) 56 | garage.add("uint8", "nbr_registered_parkings") 57 | 58 | house = pycstruct.StructDef() 59 | house.add("uint8", "nbr_of_levels") 60 | house.add(garage, "garage") 61 | 62 | ##################################################################### 63 | # Create an instance of a Bitfield (car_properties) 64 | 65 | # Buffer is empty 66 | instance = car_properties.instance() # Create from BitfieldDef 67 | self.assertEqual(instance.env_class, 0) 68 | self.assertEqual(instance.registered, 0) 69 | self.assertEqual(instance.over_3500_kg, 0) 70 | 71 | instance.env_class = -2 72 | instance.registered = 0 73 | instance.over_3500_kg = 1 74 | self.assertEqual(instance.env_class, -2) 75 | self.assertEqual(instance.registered, 0) 76 | self.assertEqual(instance.over_3500_kg, 1) 77 | 78 | # Test that buffer is updated correctly 79 | bytes_instance = bytes(instance) 80 | dict_repr = car_properties.deserialize(bytes_instance) 81 | self.assertEqual(dict_repr["env_class"], -2) 82 | self.assertEqual(dict_repr["registered"], 0) 83 | self.assertEqual(dict_repr["over_3500_kg"], 1) 84 | 85 | # Create instance from buffer 86 | dict_repr["env_class"] = 1 87 | dict_repr["registered"] = 1 88 | dict_repr["over_3500_kg"] = 0 89 | bytes_struct = car_properties.serialize(dict_repr) 90 | instance = pycstruct.Instance(car_properties, bytes_struct) 91 | self.assertEqual(instance.env_class, 1) 92 | self.assertEqual(instance.registered, 1) 93 | self.assertEqual(instance.over_3500_kg, 0) 94 | 95 | # Value too large 96 | self.assertRaises(Exception, instance.env_class, 5) 97 | # Value invalid type 98 | self.assertRaises(Exception, instance.env_class, "invalid") 99 | # Non existing value 100 | try: 101 | x = instance.invalid 102 | t.assertTrue(False) 103 | except: 104 | pass 105 | try: 106 | instance.invalid = 1 107 | t.assertTrue(False) 108 | except: 109 | pass 110 | 111 | ##################################################################### 112 | # Create an instance of a simple Struct (bus_properties) 113 | 114 | # Create instance of car_properties instance 115 | instance = pycstruct.Instance(bus_properties) 116 | self.assertEqual(instance.number_of_passangers, 0) 117 | self.assertEqual(instance.number_of_entries, 0) 118 | self.assertEqual(instance.is_accordion_bus, False) 119 | 120 | instance.number_of_passangers = 55 121 | instance.number_of_entries = 3 122 | instance.is_accordion_bus = True 123 | self.assertEqual(instance.number_of_passangers, 55) 124 | self.assertEqual(instance.number_of_entries, 3) 125 | self.assertEqual(instance.is_accordion_bus, True) 126 | 127 | ##################################################################### 128 | # Create an instance of a union (type_specific_properties) 129 | instance = pycstruct.Instance(type_specific_properties) 130 | self.assertEqual(instance.sedan.sedan_code, 0) 131 | self.assertEqual(instance.station_wagon.trunk_volume, 0) 132 | self.assertEqual(instance.bus.number_of_passangers, 0) 133 | self.assertEqual(instance.bus.number_of_entries, 0) 134 | self.assertEqual(instance.bus.is_accordion_bus, 0) 135 | self.assertEqual(instance.pickup.truck_bed_volume, 0) 136 | 137 | instance.station_wagon.trunk_volume = 1234 138 | # Since this a union the truck_bed_volume should also be updated to 139 | # the same value since it has same offset and data type 140 | self.assertEqual(instance.station_wagon.trunk_volume, 1234) 141 | self.assertEqual(instance.pickup.truck_bed_volume, 1234) 142 | 143 | ##################################################################### 144 | # Create an instance of a complex Struct (house) 145 | instance = house.instance() # Create directly from StructDef 146 | instance.nbr_of_levels = 92 147 | instance.garage.cars[0].year = 2012 148 | instance.garage.cars[0].registration_number = "ABC123" 149 | instance.garage.cars[0].last_owners[0] = 1 150 | instance.garage.cars[0].properties.registered = True 151 | instance.garage.cars[0].prop2 = 3 152 | instance.garage.cars[0].type = "Station_Wagon" 153 | instance.garage.cars[0].type_properties.sedan.sedan_code = 5 154 | instance.garage.cars[10].year = 1999 155 | instance.garage.cars[10].registration_number = "CDE456" 156 | instance.garage.cars[10].last_owners[5] = 2 157 | instance.garage.cars[10].properties.registered = False 158 | instance.garage.cars[10].prop2 = 2 159 | instance.garage.cars[10].type = "Bus" 160 | instance.garage.cars[10].type_properties.station_wagon.trunk_volume = 500 161 | instance.garage.cars[19].year = 2021 162 | instance.garage.cars[19].registration_number = "EFG789" 163 | instance.garage.cars[19].last_owners[9] = 3 164 | instance.garage.cars[19].properties.registered = True 165 | instance.garage.cars[19].prop2 = 1 166 | instance.garage.cars[19].type = "Pickup" 167 | instance.garage.cars[19].type_properties.bus.number_of_entries = 3 168 | instance.garage.nbr_registered_parkings = 255 169 | 170 | # Test that buffer is updated correctly 171 | bytes_instance = bytes(instance) 172 | dict_repr = house.deserialize(bytes_instance) 173 | self.assertEqual(dict_repr["nbr_of_levels"], 92) 174 | self.assertEqual(dict_repr["garage"]["cars"][0]["year"], 2012) 175 | self.assertEqual( 176 | dict_repr["garage"]["cars"][0]["registration_number"], "ABC123" 177 | ) 178 | self.assertEqual(dict_repr["garage"]["cars"][0]["last_owners"][0], 1) 179 | self.assertEqual( 180 | dict_repr["garage"]["cars"][0]["properties"]["registered"], True 181 | ) 182 | self.assertEqual(dict_repr["garage"]["cars"][0]["prop2"], 3) 183 | self.assertEqual(dict_repr["garage"]["cars"][0]["type"], "Station_Wagon") 184 | self.assertEqual( 185 | dict_repr["garage"]["cars"][0]["type_properties"]["sedan"]["sedan_code"], 5 186 | ) 187 | self.assertEqual(dict_repr["garage"]["cars"][10]["year"], 1999) 188 | self.assertEqual( 189 | dict_repr["garage"]["cars"][10]["registration_number"], "CDE456" 190 | ) 191 | self.assertEqual(dict_repr["garage"]["cars"][10]["last_owners"][5], 2) 192 | self.assertEqual( 193 | dict_repr["garage"]["cars"][10]["properties"]["registered"], False 194 | ) 195 | self.assertEqual(dict_repr["garage"]["cars"][10]["prop2"], 2) 196 | self.assertEqual(dict_repr["garage"]["cars"][10]["type"], "Bus") 197 | self.assertEqual( 198 | dict_repr["garage"]["cars"][10]["type_properties"]["station_wagon"][ 199 | "trunk_volume" 200 | ], 201 | 500, 202 | ) 203 | self.assertEqual(dict_repr["garage"]["cars"][19]["year"], 2021) 204 | self.assertEqual( 205 | dict_repr["garage"]["cars"][19]["registration_number"], "EFG789" 206 | ) 207 | self.assertEqual(dict_repr["garage"]["cars"][19]["last_owners"][9], 3) 208 | self.assertEqual( 209 | dict_repr["garage"]["cars"][19]["properties"]["registered"], True 210 | ) 211 | self.assertEqual(dict_repr["garage"]["cars"][19]["prop2"], 1) 212 | self.assertEqual(dict_repr["garage"]["cars"][19]["type"], "Pickup") 213 | self.assertEqual( 214 | dict_repr["garage"]["cars"][19]["type_properties"]["bus"][ 215 | "number_of_entries" 216 | ], 217 | 3, 218 | ) 219 | self.assertEqual(dict_repr["garage"]["nbr_registered_parkings"], 255) 220 | 221 | ############################################# 222 | # Test to string method 223 | stringrep = str(instance) 224 | self.assertTrue("type_properties.station_wagon.trunk_volume : 5" in stringrep) 225 | self.assertTrue("last_owners : [0, 0, 0, 0, 0, 2, 0, 0, 0, 0]" in stringrep) 226 | self.assertTrue("garage.nbr_registered_parkings : 255" in stringrep) 227 | 228 | ############################################# 229 | # Invalid usage 230 | self.assertRaises(Exception, house._element_offset, "invalid") 231 | self.assertRaises(Exception, pycstruct.Instance, car_type) # Enum not possible 232 | try: 233 | instance.garage = 5 234 | t.assertTrue(False) 235 | except: 236 | pass 237 | 238 | def test_instance_list_basictype(self): 239 | struct = pycstruct.StructDef() 240 | struct.add("int16", "list", length=3) 241 | 242 | buffer = bytearray(struct.size()) 243 | struct_instance = struct.instance(buffer) 244 | instance = struct_instance.list 245 | 246 | self.assertEqual(instance[0], 0) 247 | self.assertEqual(instance[1], 0) 248 | self.assertEqual(instance[2], 0) 249 | 250 | instance[0] = 1234 251 | instance[1] = 567 252 | instance[2] = 8901 253 | 254 | self.assertEqual(instance[0], 1234) 255 | self.assertEqual(instance[1], 567) 256 | self.assertEqual(instance[2], 8901) 257 | 258 | # Test bytes on array 259 | bytes_array = bytes(instance) 260 | bytes_instance = bytes(struct_instance) 261 | self.assertEqual(bytes_array, bytes_instance) 262 | 263 | # Test that buffer is updated correctly 264 | dict_repr = struct.deserialize(bytes_instance) 265 | self.assertEqual(dict_repr["list"][0], 1234) 266 | self.assertEqual(dict_repr["list"][1], 567) 267 | self.assertEqual(dict_repr["list"][2], 8901) 268 | 269 | # Create instance from buffer 270 | dict_repr["list"][0] = 1111 271 | dict_repr["list"][1] = -2222 272 | dict_repr["list"][2] = 3333 273 | bytes_struct = struct.serialize(dict_repr) 274 | struct_instance = struct.instance(bytes_struct) 275 | instance = struct_instance.list 276 | self.assertEqual(instance[0], 1111) 277 | self.assertEqual(instance[1], -2222) 278 | self.assertEqual(instance[2], 3333) 279 | 280 | def test_instance_list_complex(self): 281 | enum = pycstruct.EnumDef(size=4) 282 | enum.add("e1", 0) 283 | enum.add("e2", 5) 284 | enum.add("e3", 7) 285 | enum.add("e4", 9) 286 | enum.add("e5", 12) 287 | 288 | bitfield = pycstruct.BitfieldDef() 289 | bitfield.add("bf1", 7) 290 | bitfield.add("bf2", 5) 291 | 292 | substruct = pycstruct.StructDef() 293 | substruct.add("uint8", "ss1") 294 | substruct.add("int32", "ss2") 295 | 296 | struct = pycstruct.StructDef() 297 | struct.add(enum, "enum", length=2) 298 | struct.add(bitfield, "bitfield", length=3) 299 | struct.add(substruct, "substruct", length=13) 300 | 301 | ##################################################################### 302 | # Enum list 303 | buffer = bytearray(struct.size()) 304 | struct_instance = struct.instance(buffer) 305 | instance = struct_instance.enum 306 | self.assertEqual(len(instance), 2) 307 | self.assertEqual(instance[0], "e1") 308 | self.assertEqual(instance[1], "e1") 309 | instance[0] = "e2" 310 | instance[1] = "e3" 311 | bytes_instance = bytes(struct_instance) 312 | dict_repr = struct.deserialize(bytes_instance) 313 | self.assertEqual(dict_repr["enum"][0], "e2") 314 | self.assertEqual(dict_repr["enum"][1], "e3") 315 | dict_repr["enum"][0] = "e5" 316 | dict_repr["enum"][1] = "e4" 317 | bytes_struct = struct.serialize(dict_repr) 318 | instance = struct.instance(bytes_struct).enum 319 | self.assertEqual(instance[0], "e5") 320 | self.assertEqual(instance[1], "e4") 321 | 322 | ##################################################################### 323 | # Bitfield list 324 | buffer = bytearray(struct.size()) 325 | struct_instance = struct.instance(buffer) 326 | instance = struct_instance.bitfield 327 | self.assertEqual(len(instance), 3) 328 | self.assertEqual(instance[0].bf1, 0) 329 | self.assertEqual(instance[0].bf2, 0) 330 | self.assertEqual(instance[1].bf1, 0) 331 | self.assertEqual(instance[1].bf2, 0) 332 | self.assertEqual(instance[2].bf1, 0) 333 | self.assertEqual(instance[2].bf2, 0) 334 | instance[0].bf1 = 1 335 | instance[0].bf2 = 2 336 | instance[1].bf1 = 3 337 | instance[1].bf2 = 4 338 | instance[2].bf1 = 5 339 | instance[2].bf2 = 6 340 | self.assertEqual(instance[0].bf1, 1) 341 | self.assertEqual(instance[0].bf2, 2) 342 | self.assertEqual(instance[1].bf1, 3) 343 | self.assertEqual(instance[1].bf2, 4) 344 | self.assertEqual(instance[2].bf1, 5) 345 | self.assertEqual(instance[2].bf2, 6) 346 | bytes_instance = bytes(struct_instance) 347 | dict_repr = struct.deserialize(bytes_instance) 348 | self.assertEqual(dict_repr["bitfield"][0]["bf1"], 1) 349 | self.assertEqual(dict_repr["bitfield"][0]["bf2"], 2) 350 | self.assertEqual(dict_repr["bitfield"][1]["bf1"], 3) 351 | self.assertEqual(dict_repr["bitfield"][1]["bf2"], 4) 352 | self.assertEqual(dict_repr["bitfield"][2]["bf1"], 5) 353 | self.assertEqual(dict_repr["bitfield"][2]["bf2"], 6) 354 | dict_repr["bitfield"][0]["bf1"] = 7 355 | dict_repr["bitfield"][0]["bf2"] = 8 356 | dict_repr["bitfield"][1]["bf1"] = 9 357 | dict_repr["bitfield"][1]["bf2"] = 10 358 | dict_repr["bitfield"][2]["bf1"] = 11 359 | dict_repr["bitfield"][2]["bf2"] = 12 360 | bytes_struct = struct.serialize(dict_repr) 361 | instance = struct.instance(bytes_struct).bitfield 362 | self.assertEqual(instance[0].bf1, 7) 363 | self.assertEqual(instance[0].bf2, 8) 364 | self.assertEqual(instance[1].bf1, 9) 365 | self.assertEqual(instance[1].bf2, 10) 366 | self.assertEqual(instance[2].bf1, 11) 367 | self.assertEqual(instance[2].bf2, 12) 368 | 369 | ##################################################################### 370 | # Struct list 371 | buffer = bytearray(struct.size()) 372 | array_type = struct.get_field_type("substruct") 373 | struct_instance = struct.instance(buffer) 374 | instance = struct_instance.substruct 375 | self.assertEqual(len(instance), 13) 376 | self.assertEqual(instance[0].ss1, 0) 377 | self.assertEqual(instance[5].ss2, 0) 378 | self.assertEqual(instance[7].ss1, 0) 379 | self.assertEqual(instance[12].ss2, 0) 380 | instance[0].ss1 = 92 381 | instance[12].ss2 = 767 382 | bytes_instance = bytes(struct_instance) 383 | dict_repr = struct.deserialize(bytes_instance) 384 | self.assertEqual(dict_repr["substruct"][0]["ss1"], 92) 385 | self.assertEqual(dict_repr["substruct"][12]["ss2"], 767) 386 | 387 | def test_invalid_accesses(self): 388 | struct = pycstruct.StructDef() 389 | struct.add("int8", "list", length=4) 390 | buffer = b"1234" 391 | instance = struct.instance(buffer).list 392 | array_of_struct = struct[1] 393 | aos_instance = array_of_struct.instance(buffer) 394 | 395 | with self.assertRaises(Exception): 396 | instance[2] = 5 397 | with self.assertRaises(Exception): 398 | x = instance["not a number"] 399 | with self.assertRaises(Exception): 400 | x = instance[999] 401 | with self.assertRaises(Exception): 402 | aos_instance[0] = 10 403 | 404 | def test_multidim_array(self): 405 | basetype = pycstruct.pycstruct.BasicTypeDef("uint8", "little") 406 | arraytype = basetype[4][3][2] 407 | buffer = b"abcd----------------uvwx" 408 | instance = arraytype.instance(buffer) 409 | self.assertEqual(chr(instance[0][0][0]), "a") 410 | self.assertEqual(chr(instance[0][0][3]), "d") 411 | self.assertEqual(chr(instance[1][2][0]), "u") 412 | self.assertEqual(chr(instance[1][2][3]), "x") 413 | 414 | 415 | if __name__ == "__main__": 416 | unittest.main() 417 | -------------------------------------------------------------------------------- /tests/test_pycstruct.py: -------------------------------------------------------------------------------- 1 | import unittest, os, sys 2 | 3 | test_dir = os.path.dirname(os.path.realpath(__file__)) 4 | proj_dir = os.path.dirname(test_dir) 5 | 6 | sys.path.append(proj_dir) 7 | import pycstruct 8 | 9 | 10 | def check_struct(t, structdef_instance, filename): 11 | ############################################# 12 | # Load pre-stored binary data and deserialize 13 | 14 | f = open(os.path.join(test_dir, filename), "rb") 15 | inbytes = f.read() 16 | result = structdef_instance.deserialize(inbytes) 17 | f.close() 18 | 19 | ############################################# 20 | # Check expected values 21 | 22 | t.assertEqual(result["int8_low"], -128) 23 | t.assertEqual(result["int8_high"], 127) 24 | t.assertEqual(result["uint8_low"], 0) 25 | t.assertEqual(result["uint8_high"], 255) 26 | t.assertEqual(result["bool8_false"], False) 27 | t.assertEqual(result["bool8_true"], True) 28 | 29 | t.assertEqual(result["int16_low"], -32768) 30 | t.assertEqual(result["int16_high"], 32767) 31 | t.assertEqual(result["uint16_low"], 0) 32 | t.assertEqual(result["uint16_high"], 65535) 33 | t.assertEqual(result["bool16_false"], False) 34 | t.assertEqual(result["bool16_true"], True) 35 | 36 | t.assertEqual(result["int32_low"], -2147483648) 37 | t.assertEqual(result["int32_high"], 2147483647) 38 | t.assertEqual(result["uint32_low"], 0) 39 | t.assertEqual(result["uint32_high"], 4294967295) 40 | t.assertEqual(result["bool32_false"], False) 41 | t.assertEqual(result["bool32_true"], True) 42 | t.assertEqual(round(result["float32_low"], 5), 1.23456) 43 | t.assertEqual(round(result["float32_high"], 1), 12345.6) 44 | 45 | t.assertEqual(result["int64_low"], -9223372036854775808) 46 | t.assertEqual(result["int64_high"], 9223372036854775807) 47 | t.assertEqual(result["uint64_low"], 0) 48 | t.assertEqual(result["uint64_high"], 18446744073709551615) 49 | t.assertEqual(result["bool64_false"], False) 50 | t.assertEqual(result["bool64_true"], True) 51 | t.assertEqual(round(result["float64_low"], 8), 1.23456789) 52 | t.assertEqual(round(result["float64_high"], 1), 12345678.9) 53 | 54 | for i in range(0, 5): 55 | t.assertEqual(result["int32_array"][i], i) 56 | 57 | t.assertEqual(result["utf8_ascii"], "This is a normal ASCII string!") 58 | t.assertEqual(result["utf8_nonascii"], "This string has special characters ÅÄÖü") 59 | t.assertEqual(result["utf8_no_term"], "ABCD") 60 | 61 | ############################################# 62 | # Serialize result into new byte array 63 | 64 | outbytes = structdef_instance.serialize(result) 65 | 66 | ############################################# 67 | # Check that generated bytes match read ones 68 | 69 | t.assertEqual(len(inbytes), len(outbytes)) 70 | for i in range(0, len(inbytes)): 71 | t.assertEqual(int(inbytes[i]), int(outbytes[i]), msg=f"Index {i}") 72 | 73 | 74 | def check_embedded_struct(t, structdef_instance, filename): 75 | ############################################# 76 | # Load pre-stored binary data and deserialize 77 | 78 | f = open(os.path.join(test_dir, filename), "rb") 79 | inbytes = f.read() 80 | result = structdef_instance.deserialize(inbytes) 81 | f.close() 82 | 83 | ############################################# 84 | # Check expected values 85 | 86 | t.assertEqual(result["nbr_of_levels"], 5) 87 | t.assertEqual(result["garage"]["nbr_registered_parkings"], 3) 88 | t.assertEqual(result["garage"]["cars"][0]["year"], 2011) 89 | t.assertEqual(result["garage"]["cars"][0]["properties"]["env_class"], 0) 90 | t.assertEqual(result["garage"]["cars"][0]["properties"]["registered"], 1) 91 | t.assertEqual(result["garage"]["cars"][0]["properties"]["over_3500_kg"], 0) 92 | t.assertEqual(result["garage"]["cars"][0]["type"], "Sedan") 93 | t.assertEqual( 94 | result["garage"]["cars"][0]["type_properties"]["sedan"]["sedan_code"], 20 95 | ) 96 | t.assertEqual(result["garage"]["cars"][0]["registration_number"], "AHF432") 97 | t.assertEqual(result["garage"]["cars"][0]["model"], "Nissan Micra") 98 | t.assertEqual(result["garage"]["cars"][1]["year"], 2005) 99 | t.assertEqual(result["garage"]["cars"][1]["properties"]["env_class"], 1) 100 | t.assertEqual(result["garage"]["cars"][1]["properties"]["registered"], 1) 101 | t.assertEqual(result["garage"]["cars"][1]["properties"]["over_3500_kg"], 1) 102 | t.assertEqual(result["garage"]["cars"][1]["type"], "Bus") 103 | t.assertEqual( 104 | result["garage"]["cars"][1]["type_properties"]["bus"]["number_of_passangers"], 105 | 44, 106 | ) 107 | t.assertEqual( 108 | result["garage"]["cars"][1]["type_properties"]["bus"]["number_of_entries"], 3 109 | ) 110 | t.assertEqual( 111 | result["garage"]["cars"][1]["type_properties"]["bus"]["is_accordion_bus"], False 112 | ) 113 | t.assertEqual(result["garage"]["cars"][1]["registration_number"], "CCO544") 114 | t.assertEqual(result["garage"]["cars"][1]["model"], "Ford Focus") 115 | t.assertEqual(result["garage"]["cars"][2]["year"], 1998) 116 | t.assertEqual(result["garage"]["cars"][2]["properties"]["env_class"], 3) 117 | t.assertEqual(result["garage"]["cars"][2]["properties"]["registered"], 0) 118 | t.assertEqual(result["garage"]["cars"][2]["properties"]["over_3500_kg"], 0) 119 | t.assertEqual(result["garage"]["cars"][2]["type"], "Pickup") 120 | t.assertEqual( 121 | result["garage"]["cars"][2]["type_properties"]["pickup"]["truck_bed_volume"], 122 | 155, 123 | ) 124 | t.assertEqual(result["garage"]["cars"][2]["registration_number"], "HHT434") 125 | t.assertEqual(result["garage"]["cars"][2]["model"], "Volkswagen Golf") 126 | 127 | ############################################# 128 | # Serialize result into new byte array 129 | 130 | outbytes = structdef_instance.serialize(result) 131 | 132 | ############################################# 133 | # Check that generated bytes match read ones 134 | bytes_equals(t, inbytes, outbytes) 135 | 136 | 137 | def bytes_equals(t, bytes1, bytes2): 138 | t.assertEqual(len(bytes1), len(bytes2)) 139 | for i in range(0, len(bytes1)): 140 | t.assertEqual(int(bytes1[i]), int(bytes2[i]), msg=f"Index {i}") 141 | 142 | 143 | class UnserializableDef(pycstruct.pycstruct._BaseDef): 144 | """Just for testing exceptions""" 145 | 146 | def size(self): 147 | return 1 148 | 149 | def serialize(self, data): 150 | raise Exception("Unable to serialize") 151 | 152 | def deserialize(self, buffer): 153 | raise Exception("Unable to deserialize") 154 | 155 | def _largest_member(self): 156 | return self.size() 157 | 158 | def _type_name(self): 159 | return "Unserializable_Test" 160 | 161 | 162 | class TestPyCStruct(unittest.TestCase): 163 | def test_invalid_baseclass(self): 164 | b = pycstruct.pycstruct._BaseDef() 165 | 166 | self.assertRaises(NotImplementedError, b.size) 167 | self.assertRaises(NotImplementedError, b.serialize, 0) 168 | self.assertRaises(NotImplementedError, b.deserialize, 0) 169 | self.assertRaises(NotImplementedError, b._largest_member) 170 | self.assertRaises(NotImplementedError, b._type_name) 171 | 172 | def test_invalid_creation(self): 173 | # Invalid byteorder on creation 174 | self.assertRaises(Exception, pycstruct.StructDef, "invalid") 175 | 176 | def test_invalid_arraytype_size(self): 177 | struct = pycstruct.pycstruct.BasicTypeDef("int8", "native") 178 | with self.assertRaises(Exception): 179 | struct["a"] 180 | 181 | def test_invalid_add(self): 182 | m = pycstruct.StructDef() 183 | 184 | # Invalid type 185 | self.assertRaises(Exception, m.add, "invalid", "aname") 186 | 187 | # Invalid length 188 | self.assertRaises(Exception, m.add, "int8", "aname", 0) 189 | 190 | # Invalid byteorder in member 191 | self.assertRaises(Exception, m.add, "int8", "aname", 1, "invalid") 192 | 193 | # Duplicaded member 194 | m.add("int8", "name1") 195 | self.assertRaises(Exception, m.add, "uint8", "name1") 196 | 197 | # same_level with length > 1 198 | self.assertRaises( 199 | Exception, 200 | m.add, 201 | pycstruct.StructDef(), 202 | "same_level_err1", 203 | length=2, 204 | same_level=True, 205 | ) 206 | 207 | # same_level with non StructDef/BitfieldDef 208 | self.assertRaises(Exception, m.add, "int8", "same_level_err1", same_level=True) 209 | 210 | def test_invalid_deserialize(self): 211 | m = pycstruct.StructDef() 212 | m.add("int8", "name1") 213 | 214 | buffer = bytearray(m.size() - 1) 215 | self.assertRaises(Exception, m.deserialize, buffer) 216 | 217 | def test_invalid_serialize(self): 218 | m = pycstruct.StructDef() 219 | m.add("utf-8", "astring", length=5) 220 | 221 | data = {} 222 | data["astring"] = 5 # no valid string 223 | self.assertRaises(Exception, m.serialize, data) 224 | 225 | data["astring"] = "too long string" 226 | self.assertRaises(Exception, m.serialize, data) 227 | 228 | m.add("int32", "alist", length=5) 229 | data["astring"] = "valid" 230 | data["alist"] = 3 # no valid list 231 | self.assertRaises(Exception, m.serialize, data) 232 | 233 | data["alist"] = [1, 2, 3, 4, 5, 6, 7] # to long 234 | self.assertRaises(Exception, m.serialize, data) 235 | 236 | def test_struct_empty_data(self): 237 | m = self.create_struct("native", 1) 238 | data = m.create_empty_data() 239 | # Check a few of the fields 240 | self.assertTrue("int8_low" in data) 241 | self.assertTrue("utf8_nonascii" in data) 242 | 243 | def test_str(self): 244 | m = self.create_struct("native", 1) 245 | m.add("int32", "int32_ndim", shape=(5, 4, 3)) 246 | a_string = str(m) 247 | # Check a few of the fields that they are listed in the result string 248 | self.assertIn("int8_low", a_string) 249 | self.assertIn("utf8_nonascii", a_string) 250 | self.assertIn("5,4,3", a_string) 251 | 252 | def test_serialize_basetype(self): 253 | basetype = pycstruct.pycstruct.BasicTypeDef("int8", "native") 254 | buffer = basetype.serialize(10) 255 | self.assertIsInstance(buffer, bytearray) 256 | self.assertEqual(buffer[0], 10) 257 | 258 | def test_serialize_string(self): 259 | stringtype = pycstruct.pycstruct.StringDef(1) 260 | buffer = stringtype.serialize("a") 261 | self.assertIsInstance(buffer, bytearray) 262 | self.assertEqual(buffer[0], 97) 263 | 264 | def test_deserialize_serialize_little(self): 265 | self.deserialize_serialize("little", 1, "struct_little.dat") 266 | 267 | def test_deserialize_serialize_little_nopack(self): 268 | self.deserialize_serialize("little", 8, "struct_little_nopack.dat") 269 | 270 | def test_deserialize_serialize_big(self): 271 | self.deserialize_serialize("big", 1, "struct_big.dat") 272 | 273 | def test_deserialize_serialize_big_nopack(self): 274 | self.deserialize_serialize("big", 8, "struct_big_nopack.dat") 275 | 276 | def create_struct(self, byteorder, alignment): 277 | m = pycstruct.StructDef(byteorder, alignment) 278 | 279 | m.add("int8", "int8_low") 280 | m.add("int8", "int8_high") 281 | m.add("uint8", "uint8_low") 282 | m.add("uint8", "uint8_high") 283 | m.add("bool8", "bool8_false") 284 | m.add("bool8", "bool8_true") 285 | 286 | m.add("int16", "int16_low") 287 | m.add("int16", "int16_high") 288 | m.add("uint16", "uint16_low") 289 | m.add("uint16", "uint16_high") 290 | m.add("bool16", "bool16_false") 291 | m.add("bool16", "bool16_true") 292 | 293 | m.add("int32", "int32_low") 294 | m.add("int32", "int32_high") 295 | m.add("uint32", "uint32_low") 296 | m.add("uint32", "uint32_high") 297 | m.add("bool32", "bool32_false") 298 | m.add("bool32", "bool32_true") 299 | m.add("float32", "float32_low") 300 | m.add("float32", "float32_high") 301 | 302 | m.add("int64", "int64_low") 303 | m.add("int64", "int64_high") 304 | m.add("uint64", "uint64_low") 305 | m.add("uint64", "uint64_high") 306 | m.add("bool64", "bool64_false") 307 | m.add("bool64", "bool64_true") 308 | m.add("float64", "float64_low") 309 | m.add("float64", "float64_high") 310 | 311 | m.add("int32", "int32_array", length=5) 312 | 313 | m.add("utf-8", "utf8_ascii", 100) 314 | m.add("utf-8", "utf8_nonascii", 80) 315 | m.add("utf-8", "utf8_no_term", 4) 316 | 317 | return m 318 | 319 | def deserialize_serialize(self, byteorder, alignment, filename): 320 | ############################################# 321 | # Define PyCStruct 322 | m = self.create_struct(byteorder, alignment) 323 | 324 | ############################################# 325 | # Load pre-stored binary data and deserialize and check 326 | check_struct(self, m, filename) 327 | 328 | def test_embedded_struct(self): 329 | self.embedded_struct("embedded_struct.dat", alignment=1) 330 | 331 | def test_embedded_struct_nopack(self): 332 | self.embedded_struct("embedded_struct_nopack.dat", alignment=8) 333 | 334 | def embedded_struct(self, filename, alignment=1): 335 | car_type = pycstruct.EnumDef(size=4) 336 | car_type.add("Sedan", 0) 337 | car_type.add("Station_Wagon", 5) 338 | car_type.add("Bus", 7) 339 | car_type.add("Pickup", 12) 340 | 341 | sedan_properties = pycstruct.StructDef(alignment=alignment) 342 | sedan_properties.add("uint16", "sedan_code") 343 | 344 | station_wagon_properties = pycstruct.StructDef(alignment=alignment) 345 | station_wagon_properties.add("int32", "trunk_volume") 346 | 347 | bus_properties = pycstruct.StructDef(alignment=alignment) 348 | bus_properties.add("int32", "number_of_passangers") 349 | bus_properties.add("uint16", "number_of_entries") 350 | bus_properties.add("bool8", "is_accordion_bus") 351 | 352 | pickup_properties = pycstruct.StructDef(alignment=alignment) 353 | pickup_properties.add("int32", "truck_bed_volume") 354 | 355 | type_specific_properties = pycstruct.StructDef(alignment=alignment, union=True) 356 | type_specific_properties.add(sedan_properties, "sedan") 357 | type_specific_properties.add(station_wagon_properties, "station_wagon") 358 | type_specific_properties.add(bus_properties, "bus") 359 | type_specific_properties.add(pickup_properties, "pickup") 360 | 361 | # gcc is setting the size of car_properties_s to 362 | # 4 bytes when no packing is added of some strange 363 | # reason. 364 | size = 1 365 | if alignment > 1: 366 | size = 4 367 | car_properties = pycstruct.BitfieldDef(size=size) 368 | car_properties.add("env_class", 3) 369 | car_properties.add("registered", 1) 370 | car_properties.add("over_3500_kg", 1) 371 | 372 | car = pycstruct.StructDef(alignment=alignment) 373 | car.add("uint16", "year") 374 | car.add("utf-8", "model", length=50) 375 | car.add("utf-8", "registration_number", length=10) 376 | car.add(car_properties, "properties") 377 | car.add(car_type, "type") 378 | car.add(type_specific_properties, "type_properties") 379 | 380 | garage = pycstruct.StructDef(alignment=alignment) 381 | garage.add(car, "cars", length=20) 382 | garage.add("uint8", "nbr_registered_parkings") 383 | 384 | house = pycstruct.StructDef(alignment=alignment) 385 | house.add("uint8", "nbr_of_levels") 386 | house.add(garage, "garage") 387 | 388 | ############################################# 389 | # Test to string method 390 | stringrep = str(car) 391 | self.assertTrue("model" in stringrep) 392 | stringrep = str(garage) 393 | self.assertTrue("nbr_registered_parkings" in stringrep) 394 | stringrep = str(house) 395 | self.assertTrue("nbr_of_levels" in stringrep) 396 | 397 | ############################################# 398 | # Load pre-stored binary data and deserialize and check 399 | check_embedded_struct(self, house, filename) 400 | 401 | def test_embedded_exception(self): 402 | unserializable = UnserializableDef() 403 | 404 | s = pycstruct.StructDef() 405 | s.add(unserializable, "unserializable") 406 | 407 | self.assertRaises(Exception, s.deserialize, bytes([0])) 408 | self.assertRaises(Exception, s.serialize, {"unserializable": "hello"}) 409 | 410 | def test_embedded_same_level(self): 411 | bitfield = pycstruct.BitfieldDef() 412 | bitfield.add("bf1", 3) 413 | bitfield.add("bf2", 1) 414 | bitfield.add("bf3", 4) 415 | 416 | parentstruct = pycstruct.StructDef() 417 | parentstruct.add("uint16", "ps1") 418 | parentstruct.add("uint32", "ps2") 419 | parentstruct.add(bitfield, "ps4", same_level=True) 420 | parentstruct.add("int8", "ps5") 421 | 422 | mydict = { 423 | "bf1": 5, 424 | "bf2": 0, 425 | "bf3": 11, 426 | "ps1": 789, 427 | "ps2": 91011, 428 | "ps4": 1213, # Should be ignored 429 | "ps5": -100, 430 | } 431 | 432 | databin = parentstruct.serialize(mydict) 433 | mydict2 = parentstruct.deserialize(databin) 434 | 435 | # Check 436 | for key, value in mydict.items(): 437 | if key != "ps4": 438 | self.assertEqual(value, mydict2[key], msg=f"Key {key}") 439 | 440 | def test_struct_remove_from(self): 441 | m = pycstruct.StructDef() 442 | m.add("int8", "e1") 443 | m.add("int8", "e2") 444 | m.add("int16", "e3") 445 | m.add("int16", "e4") 446 | m.add("int32", "e5") 447 | m.add("int32", "e6") 448 | 449 | self.assertEqual(m.size(), 14) 450 | 451 | self.assertRaises(Exception, m.remove_from, "invalid") 452 | 453 | m.remove_from("e4") 454 | self.assertEqual(m.size(), 4) 455 | 456 | d = m.create_empty_data() 457 | self.assertTrue("e1" in d) 458 | self.assertTrue("e3" in d) 459 | self.assertFalse("e4" in d) 460 | self.assertFalse("e6" in d) 461 | 462 | def test_struct_remove_to(self): 463 | m = pycstruct.StructDef() 464 | m.add("int8", "e1") 465 | m.add("int8", "e2") 466 | m.add("int16", "e3") 467 | m.add("int16", "e4") 468 | m.add("int32", "e5") 469 | m.add("int32", "e6") 470 | 471 | self.assertEqual(m.size(), 14) 472 | 473 | self.assertRaises(Exception, m.remove_to, "invalid") 474 | 475 | m.remove_to("e4") 476 | self.assertEqual(m.size(), 8) 477 | 478 | d = m.create_empty_data() 479 | self.assertFalse("e1" in d) 480 | self.assertFalse("e4" in d) 481 | self.assertTrue("e5" in d) 482 | self.assertTrue("e6" in d) 483 | 484 | def test_struct_add_invalid_shape(self): 485 | m = pycstruct.StructDef() 486 | with self.assertRaises(Exception): 487 | m.add("int32", "e6", shape=(1, "a")) 488 | 489 | def test_struct_add_int_shape(self): 490 | m = pycstruct.StructDef() 491 | m.add("int32", "field", shape=10) 492 | arraytype = m._element_type("field") 493 | self.assertIsInstance(arraytype, pycstruct.pycstruct.ArrayDef) 494 | self.assertEqual(arraytype.length, 10) 495 | 496 | def test_struct_add_string_size_1(self): 497 | m = pycstruct.StructDef() 498 | m.add("utf-8", "utf8_1", 1) 499 | utf8_type = m._element_type("utf8_1") 500 | self.assertIsInstance(utf8_type, pycstruct.pycstruct.StringDef) 501 | self.assertEqual(utf8_type.length, 1) 502 | 503 | def test_bitfield_invalid_creation(self): 504 | # Invalid byteorder on creation 505 | self.assertRaises(Exception, pycstruct.BitfieldDef, "invalid") 506 | 507 | def test_bitfield_add(self): 508 | bitfield = pycstruct.BitfieldDef() 509 | 510 | self.assertEqual(bitfield.assigned_bits(), 0) 511 | self.assertEqual(bitfield.size(), 0) 512 | 513 | bitfield.add("one_bit") 514 | self.assertEqual(bitfield.assigned_bits(), 1) 515 | bitfield.add("two_bits", 2) 516 | bitfield.add("three_bits", 3) 517 | bitfield.add("two_bits_signed", 2, signed=True) 518 | self.assertEqual(bitfield.assigned_bits(), 8) 519 | self.assertEqual(bitfield.size(), 1) 520 | 521 | bitfield.add("one_more_bit") 522 | self.assertEqual(bitfield.assigned_bits(), 9) 523 | self.assertEqual(bitfield.size(), 2) 524 | bitfield.add("seven_bits", 7) 525 | self.assertEqual(bitfield.assigned_bits(), 16) 526 | self.assertEqual(bitfield.size(), 2) 527 | 528 | bitfield.add("three_bits_signed", 3, signed=True) 529 | self.assertEqual(bitfield.assigned_bits(), 19) 530 | self.assertEqual(bitfield.size(), 3) 531 | 532 | bitfield.add("32_bits", 32) 533 | self.assertEqual(bitfield.assigned_bits(), 51) 534 | self.assertEqual(bitfield.size(), 7) 535 | 536 | bitfield.add("13_signed_bits", 13, signed=True) 537 | self.assertEqual(bitfield.assigned_bits(), 64) 538 | self.assertEqual(bitfield.size(), 8) 539 | 540 | # Should overflow 541 | self.assertRaises(Exception, bitfield.add, "this_wont_fit_in_64_bits") 542 | 543 | # Same bit field name again - forbidden 544 | self.assertRaises(Exception, bitfield.add, "three_bits") 545 | 546 | def test_bitfield_empty_data(self): 547 | b = self.create_bitfield("native") 548 | data = b.create_empty_data() 549 | # Check a few of the fields 550 | self.assertTrue("threebits" in data) 551 | self.assertTrue("onesignedbit" in data) 552 | 553 | # Test to string method 554 | stringrep = str(b) 555 | self.assertTrue("onebit" in stringrep) 556 | self.assertTrue("eightsignedbits" in stringrep) 557 | self.assertTrue("fivebits" in stringrep) 558 | 559 | def create_bitfield(self, byteorder): 560 | b = pycstruct.BitfieldDef(byteorder) 561 | 562 | b.add("onebit", 1, signed=False) 563 | b.add("twobits", 2, signed=False) 564 | b.add("threebits", 3, signed=False) 565 | b.add("fourbits", 4, signed=False) 566 | b.add("fivesignedbits", 5, signed=True) 567 | b.add("eightbits", 8, signed=False) 568 | b.add("eightsignedbits", 8, signed=True) 569 | b.add("onesignedbit", 1, signed=True) 570 | b.add("foursignedbits", 4, signed=True) 571 | b.add("sixteensignedbits", 16, signed=True) 572 | b.add("fivebits", 5, signed=False) 573 | 574 | return b 575 | 576 | def deserialize_serialize_bitfield(self, byteorder): 577 | ############################################# 578 | # Define Bitfield 579 | b = self.create_bitfield(byteorder) 580 | 581 | ############################################# 582 | # Load pre-stored binary data and deserialize 583 | 584 | f = open(os.path.join(test_dir, f"bitfield_{byteorder}.dat"), "rb") 585 | inbytes = f.read() 586 | result = b.deserialize(inbytes) 587 | f.close() 588 | 589 | ############################################# 590 | # Check expected values 591 | self.assertEqual(result["onebit"], 1) 592 | self.assertEqual(result["twobits"], 3) 593 | self.assertEqual(result["threebits"], 1) 594 | self.assertEqual(result["fourbits"], 3) 595 | self.assertEqual(result["fivesignedbits"], -2) 596 | self.assertEqual(result["eightbits"], 255) 597 | self.assertEqual(result["eightsignedbits"], -128) 598 | self.assertEqual(result["onesignedbit"], -1) 599 | self.assertEqual(result["foursignedbits"], 5) 600 | self.assertEqual(result["sixteensignedbits"], -12345) 601 | self.assertEqual(result["fivebits"], 16) 602 | 603 | ############################################# 604 | # Serialize result into new byte array 605 | 606 | outbytes = b.serialize(result) 607 | 608 | ############################################# 609 | # Check that generated bytes match read ones 610 | 611 | self.assertEqual(len(inbytes), len(outbytes)) 612 | for i in range(0, len(inbytes)): 613 | self.assertEqual(int(inbytes[i]), int(outbytes[i]), msg=f"Index {i}") 614 | 615 | def test_bitfield_deserialize_serialize_little(self): 616 | self.deserialize_serialize_bitfield("little") 617 | 618 | def test_bitfield_deserialize_serialize_big(self): 619 | self.deserialize_serialize_bitfield("big") 620 | 621 | def test_bitfield_invalid_deserialize(self): 622 | b = pycstruct.BitfieldDef() 623 | b.add("afield") 624 | 625 | buffer = bytearray(b.size() - 1) # Too small buffer 626 | self.assertRaises(Exception, b.deserialize, buffer) 627 | 628 | def test_bitfield_getsubvalue(self): 629 | bitstruct = pycstruct.BitfieldDef() 630 | value = int("0101110001010011", 2) 631 | 632 | # Unsigned tests 633 | self.assertEqual( 634 | bitstruct._get_subvalue(value, nbr_of_bits=1, start_bit=0, signed=False), 1 635 | ) 636 | self.assertEqual( 637 | bitstruct._get_subvalue(value, nbr_of_bits=4, start_bit=0, signed=False), 3 638 | ) 639 | self.assertEqual( 640 | bitstruct._get_subvalue(value, nbr_of_bits=16, start_bit=0, signed=False), 641 | 23635, 642 | ) 643 | self.assertEqual( 644 | bitstruct._get_subvalue(value, nbr_of_bits=15, start_bit=0, signed=False), 645 | 23635, 646 | ) 647 | self.assertEqual( 648 | bitstruct._get_subvalue(value, nbr_of_bits=14, start_bit=2, signed=False), 649 | 5908, 650 | ) 651 | self.assertEqual( 652 | bitstruct._get_subvalue(value, nbr_of_bits=3, start_bit=4, signed=False), 5 653 | ) 654 | 655 | # Signed tests 656 | self.assertEqual( 657 | bitstruct._get_subvalue(value, nbr_of_bits=1, start_bit=0, signed=True), -1 658 | ) 659 | self.assertEqual( 660 | bitstruct._get_subvalue(value, nbr_of_bits=4, start_bit=0, signed=True), 3 661 | ) 662 | self.assertEqual( 663 | bitstruct._get_subvalue(value, nbr_of_bits=16, start_bit=0, signed=True), 664 | 23635, 665 | ) 666 | self.assertEqual( 667 | bitstruct._get_subvalue(value, nbr_of_bits=15, start_bit=0, signed=True), 668 | -9133, 669 | ) 670 | self.assertEqual( 671 | bitstruct._get_subvalue(value, nbr_of_bits=14, start_bit=2, signed=True), 672 | 5908, 673 | ) 674 | self.assertEqual( 675 | bitstruct._get_subvalue(value, nbr_of_bits=3, start_bit=4, signed=True), -3 676 | ) 677 | 678 | def test_bitfield_setsubvalue(self): 679 | bitstruct = pycstruct.BitfieldDef() 680 | 681 | # Unsigned tests 682 | self.assertEqual( 683 | bin( 684 | bitstruct._set_subvalue(0, 1, nbr_of_bits=1, start_bit=0, signed=False) 685 | ), 686 | "0b1", 687 | ) 688 | self.assertEqual( 689 | bin( 690 | bitstruct._set_subvalue(0, 1, nbr_of_bits=1, start_bit=2, signed=False) 691 | ), 692 | "0b100", 693 | ) 694 | self.assertEqual( 695 | bin( 696 | bitstruct._set_subvalue(0, 5, nbr_of_bits=3, start_bit=5, signed=False) 697 | ), 698 | "0b10100000", 699 | ) 700 | 701 | value = int("010100001111", 2) 702 | self.assertEqual( 703 | bin( 704 | bitstruct._set_subvalue( 705 | value, 15, nbr_of_bits=4, start_bit=4, signed=False 706 | ) 707 | ), 708 | "0b10111111111", 709 | ) 710 | 711 | # Signed tests 712 | value = 0 713 | self.assertEqual( 714 | bin( 715 | bitstruct._set_subvalue(0, -1, nbr_of_bits=1, start_bit=0, signed=True) 716 | ), 717 | "0b1", 718 | ) 719 | self.assertEqual( 720 | bin( 721 | bitstruct._set_subvalue(0, -1, nbr_of_bits=1, start_bit=2, signed=True) 722 | ), 723 | "0b100", 724 | ) 725 | self.assertEqual( 726 | bin( 727 | bitstruct._set_subvalue(0, -5, nbr_of_bits=4, start_bit=5, signed=True) 728 | ), 729 | "0b101100000", 730 | ) 731 | self.assertEqual( 732 | bin(bitstruct._set_subvalue(0, 5, nbr_of_bits=4, start_bit=5, signed=True)), 733 | "0b10100000", 734 | ) 735 | 736 | # Invalid values 737 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, -1, 1, 0, False) 738 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, 2, 1, 0, False) 739 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, 8, 3, 0, False) 740 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, -2, 1, 0, True) 741 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, 2, 1, 0, True) 742 | self.assertRaises(Exception, bitstruct._set_subvalue, 0, 7, 3, 0, True) 743 | 744 | def test_enum_invalid_creation(self): 745 | # Invalid byteorder on creation 746 | self.assertRaises(Exception, pycstruct.EnumDef, "invalid") 747 | 748 | def test_enum_add(self): 749 | e = pycstruct.EnumDef() 750 | 751 | self.assertEqual(e.size(), 1) 752 | 753 | e.add("first") 754 | self.assertEqual(e.get_value("first"), 0) 755 | self.assertEqual(e.get_name(0), "first") 756 | self.assertEqual(e.size(), 1) 757 | 758 | e.add("second", 1) 759 | self.assertEqual(e.get_value("second"), 1) 760 | self.assertEqual(e.get_name(1), "second") 761 | self.assertEqual(e.size(), 1) 762 | 763 | e.add("fitbyte", 127) 764 | self.assertEqual(e.get_value("fitbyte"), 127) 765 | self.assertEqual(e.get_name(127), "fitbyte") 766 | self.assertEqual(e.size(), 1) 767 | 768 | e.add("third") 769 | self.assertEqual(e.get_value("third"), 2) 770 | self.assertEqual(e.get_name(2), "third") 771 | self.assertEqual(e.size(), 1) 772 | 773 | # Duplicate 774 | self.assertRaises(Exception, e.add, "second") 775 | 776 | # > 64 bits 777 | self.assertRaises(Exception, e.add, "too_big", 12345678901234561234567) 778 | 779 | # Get invalid value 780 | self.assertRaises(Exception, e.get_value, 33) 781 | 782 | # Get invalid name 783 | self.assertRaises(Exception, e.get_name, "invalid") 784 | 785 | def test_enum_fixed_unsigned(self): 786 | e = pycstruct.EnumDef(size=4, signed=False) 787 | 788 | self.assertEqual(e.size(), 4) 789 | 790 | e.add("first") 791 | self.assertEqual(e.get_value("first"), 0) 792 | self.assertEqual(e.get_name(0), "first") 793 | self.assertEqual(e.size(), 4) 794 | 795 | e.add("largest", 0xFFFFFFFF) 796 | self.assertEqual(e.size(), 4) 797 | 798 | # Add a too large number 799 | self.assertRaises(Exception, e.add, "too_large", 0xFFFFFFFF + 1) 800 | 801 | # Add negative number to unsigned 802 | self.assertRaises(Exception, e.add, "negative", -1) 803 | 804 | def test_enum_fixed_signed(self): 805 | e = pycstruct.EnumDef(size=4, signed=True) 806 | 807 | self.assertEqual(e.size(), 4) 808 | 809 | e.add("first") 810 | self.assertEqual(e.get_value("first"), 0) 811 | self.assertEqual(e.get_name(0), "first") 812 | self.assertEqual(e.size(), 4) 813 | 814 | e.add("negative", -1) 815 | self.assertEqual(e.size(), 4) 816 | 817 | e.add("largest_negative", 0x7FFFFFFF * -1 - 1) 818 | self.assertEqual(e.size(), 4) 819 | 820 | # Add too small value 821 | self.assertRaises(Exception, e.add, "too_small", 0x7FFFFFFF * -1 - 2) 822 | 823 | e.add("largest_positive", 0x7FFFFFFF) 824 | self.assertEqual(e.size(), 4) 825 | 826 | # Add too large value 827 | self.assertRaises(Exception, e.add, "too_large", 0x7FFFFFFF + 1) 828 | 829 | def test_enum_serialize_deserialize(self): 830 | e = pycstruct.EnumDef(size=1) 831 | e.add("zero", 0) 832 | e.add("one", 1) 833 | e.add("two", 2) 834 | e.add("three", 2) 835 | 836 | value = "two" 837 | buf = e.serialize(value) 838 | self.assertEqual(len(buf), 1) 839 | self.assertEqual(buf[0], 2) 840 | 841 | big = pycstruct.EnumDef("big", size=2) 842 | big.add("twofiftysix", 256) 843 | value = "twofiftysix" 844 | buf = big.serialize(value) 845 | self.assertEqual(len(buf), 2) 846 | self.assertEqual(buf[0], 1) 847 | self.assertEqual(buf[1], 0) 848 | outval = big.deserialize(buf) 849 | self.assertEqual(outval, "twofiftysix") 850 | 851 | little = pycstruct.EnumDef("little", size=2) 852 | value = "twofiftysix" 853 | little.add(value, 256) 854 | buf = little.serialize(value) 855 | self.assertEqual(len(buf), 2) 856 | self.assertEqual(buf[0], 0) 857 | self.assertEqual(buf[1], 1) 858 | outval = little.deserialize(buf) 859 | self.assertEqual(outval, value) 860 | 861 | value = "largest_uint16" 862 | little.add(value, 0xFFFF) 863 | buf = little.serialize(value) 864 | self.assertEqual(len(buf), 2) 865 | self.assertEqual(buf[0], 0xFF) 866 | self.assertEqual(buf[1], 0xFF) 867 | outval = little.deserialize(buf) 868 | self.assertEqual(outval, value) 869 | 870 | little_signed = pycstruct.EnumDef("little", size=2, signed=True) 871 | value = "largest_int16" 872 | little_signed.add(value, 32767) 873 | buf = little_signed.serialize(value) 874 | self.assertEqual(len(buf), 2) 875 | self.assertEqual(buf[0], 0xFF) 876 | self.assertEqual(buf[1], 0x7F) 877 | outval = little_signed.deserialize(buf) 878 | self.assertEqual(outval, value) 879 | 880 | value = "smallest_int16" 881 | little_signed.add(value, -32768) 882 | buf = little_signed.serialize(value) 883 | self.assertEqual(len(buf), 2) 884 | self.assertEqual(buf[0], 0x00) 885 | self.assertEqual(buf[1], 0x80) 886 | outval = little_signed.deserialize(buf) 887 | self.assertEqual(outval, value) 888 | 889 | # Unassigned values 890 | outval = little_signed.deserialize(bytes([0, 0])) 891 | self.assertEqual(outval, "__VALUE__0") 892 | outval = little_signed.deserialize(bytes([99, 0])) 893 | self.assertEqual(outval, "__VALUE__99") 894 | outval = little_signed.deserialize(bytes([0xFC, 0xFF])) 895 | self.assertEqual(outval, "__VALUE__-4") 896 | 897 | # Test to string method 898 | stringrep = str(e) 899 | self.assertTrue("zero" in stringrep) 900 | self.assertTrue("three" in stringrep) 901 | 902 | def test_union_no_pad(self): 903 | u = pycstruct.StructDef(union=True, default_byteorder="big") 904 | self.assertEqual(u._type_name(), "union") 905 | u.add("uint8", "small") 906 | self.assertEqual(u.size(), 1) 907 | u.add("uint16", "large") 908 | self.assertEqual(u.size(), 2) 909 | u.add("uint32", "larger") 910 | self.assertEqual(u.size(), 4) 911 | u.add("uint64", "largest") 912 | self.assertEqual(u.size(), 8) 913 | 914 | input = {} 915 | input["largest"] = 0x1122334455667788 916 | 917 | buf = u.serialize(input) 918 | self.assertEqual(len(buf), 8) 919 | 920 | output = u.deserialize(buf) 921 | self.assertEqual(output["small"], 0x11) 922 | self.assertEqual(output["large"], 0x1122) 923 | self.assertEqual(output["larger"], 0x11223344) 924 | self.assertEqual(output["largest"], 0x1122334455667788) 925 | 926 | buf2 = u.serialize(output) 927 | output2 = u.deserialize(buf2) 928 | self.assertEqual(output2["largest"], 0x1122334455667788) 929 | 930 | del output2["largest"] 931 | del output2["larger"] 932 | 933 | buf3 = u.serialize(output2) 934 | output3 = u.deserialize(buf3) 935 | self.assertEqual(output3["largest"], 0x1122000000000000) 936 | self.assertEqual(output3["larger"], 0x11220000) 937 | 938 | def test_enum_invalid_deserialize(self): 939 | e = pycstruct.EnumDef() 940 | e.add("zero") 941 | 942 | buffer = bytearray(e.size() - 1) 943 | self.assertRaises(Exception, e.deserialize, buffer) 944 | 945 | def test_get_padding(self): 946 | padding = pycstruct.pycstruct._get_padding 947 | 948 | # Alignment 1 949 | self.assertEqual(padding(1, 5, 4), 0) 950 | 951 | # Alignment 2 952 | self.assertEqual(padding(2, 0, 1), 0) 953 | self.assertEqual(padding(2, 0, 2), 0) 954 | self.assertEqual(padding(2, 0, 4), 0) 955 | self.assertEqual(padding(2, 0, 8), 0) 956 | self.assertEqual(padding(2, 1, 1), 0) 957 | self.assertEqual(padding(2, 1, 2), 1) 958 | self.assertEqual(padding(2, 1, 4), 1) 959 | self.assertEqual(padding(2, 1, 8), 1) 960 | self.assertEqual(padding(2, 2, 1), 0) 961 | self.assertEqual(padding(2, 2, 2), 0) 962 | self.assertEqual(padding(2, 2, 4), 0) 963 | self.assertEqual(padding(2, 2, 8), 0) 964 | self.assertEqual(padding(2, 3, 1), 0) 965 | self.assertEqual(padding(2, 3, 2), 1) 966 | self.assertEqual(padding(2, 3, 4), 1) 967 | self.assertEqual(padding(2, 3, 8), 1) 968 | 969 | # Alignment 4 970 | self.assertEqual(padding(4, 0, 1), 0) 971 | self.assertEqual(padding(4, 0, 2), 0) 972 | self.assertEqual(padding(4, 0, 4), 0) 973 | self.assertEqual(padding(4, 0, 8), 0) 974 | self.assertEqual(padding(4, 1, 1), 0) 975 | self.assertEqual(padding(4, 1, 2), 1) 976 | self.assertEqual(padding(4, 1, 4), 3) 977 | self.assertEqual(padding(4, 1, 8), 3) 978 | self.assertEqual(padding(4, 2, 1), 0) 979 | self.assertEqual(padding(4, 2, 2), 0) 980 | self.assertEqual(padding(4, 2, 4), 2) 981 | self.assertEqual(padding(4, 2, 8), 2) 982 | self.assertEqual(padding(4, 3, 1), 0) 983 | self.assertEqual(padding(4, 3, 2), 1) 984 | self.assertEqual(padding(4, 3, 4), 1) 985 | self.assertEqual(padding(4, 3, 8), 1) 986 | self.assertEqual(padding(4, 4, 1), 0) 987 | self.assertEqual(padding(4, 4, 2), 0) 988 | self.assertEqual(padding(4, 4, 4), 0) 989 | self.assertEqual(padding(4, 4, 8), 0) 990 | self.assertEqual(padding(4, 5, 1), 0) 991 | self.assertEqual(padding(4, 5, 2), 1) 992 | self.assertEqual(padding(4, 5, 4), 3) 993 | self.assertEqual(padding(4, 5, 8), 3) 994 | 995 | # Alignment 8 996 | self.assertEqual(padding(8, 0, 1), 0) 997 | self.assertEqual(padding(8, 0, 2), 0) 998 | self.assertEqual(padding(8, 0, 4), 0) 999 | self.assertEqual(padding(8, 0, 8), 0) 1000 | self.assertEqual(padding(8, 1, 1), 0) 1001 | self.assertEqual(padding(8, 1, 2), 1) 1002 | self.assertEqual(padding(8, 1, 4), 3) 1003 | self.assertEqual(padding(8, 1, 8), 7) 1004 | self.assertEqual(padding(8, 2, 1), 0) 1005 | self.assertEqual(padding(8, 2, 2), 0) 1006 | self.assertEqual(padding(8, 2, 4), 2) 1007 | self.assertEqual(padding(8, 2, 8), 6) 1008 | self.assertEqual(padding(8, 3, 1), 0) 1009 | self.assertEqual(padding(8, 3, 2), 1) 1010 | self.assertEqual(padding(8, 3, 4), 1) 1011 | self.assertEqual(padding(8, 3, 8), 5) 1012 | self.assertEqual(padding(8, 4, 1), 0) 1013 | self.assertEqual(padding(8, 4, 2), 0) 1014 | self.assertEqual(padding(8, 4, 4), 0) 1015 | self.assertEqual(padding(8, 4, 8), 4) 1016 | self.assertEqual(padding(8, 5, 1), 0) 1017 | self.assertEqual(padding(8, 5, 2), 1) 1018 | self.assertEqual(padding(8, 5, 4), 3) 1019 | self.assertEqual(padding(8, 5, 8), 3) 1020 | self.assertEqual(padding(8, 7, 8), 1) 1021 | self.assertEqual(padding(8, 8, 8), 0) 1022 | self.assertEqual(padding(8, 9, 8), 7) 1023 | 1024 | def test_round_pow_2(self): 1025 | round_2 = pycstruct.pycstruct._round_pow_2 1026 | 1027 | self.assertEqual(round_2(0), 0) 1028 | self.assertEqual(round_2(1), 1) 1029 | self.assertEqual(round_2(2), 2) 1030 | self.assertEqual(round_2(3), 4) 1031 | self.assertEqual(round_2(4), 4) 1032 | self.assertEqual(round_2(5), 8) 1033 | self.assertEqual(round_2(8), 8) 1034 | self.assertEqual(round_2(9), 16) 1035 | 1036 | def test_array_invalid_deserialize(self): 1037 | basetype = pycstruct.pycstruct.BasicTypeDef("uint8", "little") 1038 | arraytype = basetype[4][3][2] 1039 | buffer = b"" 1040 | with self.assertRaises(Exception): 1041 | arraytype.deserialize(buffer) 1042 | 1043 | def test_array_multidim_deserialize(self): 1044 | basetype = pycstruct.pycstruct.BasicTypeDef("uint8", "little") 1045 | arraytype = basetype[4][3][2] 1046 | buffer = b"abcd----------------uvwx" 1047 | object = arraytype.deserialize(buffer) 1048 | self.assertEqual(chr(object[0][0][0]), "a") 1049 | self.assertEqual(chr(object[0][0][3]), "d") 1050 | self.assertEqual(chr(object[1][2][0]), "u") 1051 | self.assertEqual(chr(object[1][2][3]), "x") 1052 | 1053 | def test_array_multidim_serialize(self): 1054 | basetype = pycstruct.pycstruct.BasicTypeDef("uint8", "little") 1055 | arraytype = basetype[4][3][2] 1056 | l1 = [ord("a"), ord("b"), ord("c"), ord("d")] 1057 | l2 = [ord("u"), ord("v"), ord("w"), ord("x")] 1058 | l3 = [ord("-"), ord("-"), ord("-"), ord("-")] 1059 | object = [[l1, l3, l3], [l3, l3, l2]] 1060 | expected = b"abcd----------------uvwx" 1061 | buffer = arraytype.serialize(object) 1062 | self.assertEqual(buffer, expected) 1063 | 1064 | 1065 | if __name__ == "__main__": 1066 | unittest.main() 1067 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,311} 3 | 4 | [testenv] 5 | deps = pytest-cov 6 | coveralls 7 | black 8 | pylint >= 2.14.0 9 | numpy 10 | 11 | passenv = COVERALLS_REPO_TOKEN 12 | 13 | commands = 14 | coverage run --source=./pycstruct -m unittest discover -s tests/ -v 15 | coverage report --fail-under=100 pycstruct/__init__.py pycstruct/pycstruct.py pycstruct/instance.py 16 | coverage report --fail-under=89 pycstruct/cparser.py 17 | coverage report --fail-under=95 18 | black --check pycstruct/ tests/ 19 | pylint pycstruct/ 20 | {env:POST_COMMAND:python --version} 21 | --------------------------------------------------------------------------------