├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── example.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── lorem.lszz ├── lorem.txt ├── reference.vbf ├── test_checksum.py ├── test_lzss.py ├── test_options.py └── test_vbf.py ├── tox.ini └── vbftool ├── __init__.py ├── checksum.py ├── lzss.py ├── options.py ├── output.py └── vbf.py /.gitignore: -------------------------------------------------------------------------------- 1 | test.vbf 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | # Pyre type checker 128 | .pyre/ 129 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | cache: pip 5 | jobs: 6 | include: 7 | - python: 3.5 8 | env: TOXENV=py35 9 | - python: 3.6 10 | env: TOXENV=py36 11 | - python: 3.7 12 | env: TOXENV=py37 13 | - python: 3.8 14 | env: TOXENV=py38 15 | 16 | install: pip install tox 17 | 18 | script: tox 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 David Schneider 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the README 2 | include *.md 3 | include *.txt 4 | 5 | # Include the license file 6 | include LICENSE 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](http://img.shields.io/pypi/v/vbftool.svg)](https://pypi.python.org/pypi/vbftool) 2 | [![Build Status](https://travis-ci.org/dsch/vbftool.svg?branch=master)](https://travis-ci.org/dsch/vbftool) 3 | [![License](http://img.shields.io/badge/license-MIT-green.svg)](https://github.com/dsch/vbftool/blob/master/LICENSE) 4 | 5 | # vbftool 6 | Versatile Binary Format Tool 7 | -------------------------------------------------------------------------------- /docs/example.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bincopy 3 | 4 | import vbftool.options as opt 5 | from vbftool import Vbf, VbfVersion 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | bin = bincopy.BinFile() 10 | bin.add_ihex_file('test.hex') 11 | 12 | vbf = Vbf(VbfVersion.VERSION_JLR3_0, 0x1000000, 0x3C0000, bin.as_binary()) 13 | vbf.add_option(opt.DataFormatIdentifier(1, 0)) 14 | 15 | with open('reference.vbf', 'wb') as fp: 16 | vbf.dump(fp) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==5.3.2 2 | flake8==3.7.9 3 | check-manifest==0.40 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | 7 | [flake8] 8 | max-line-length = 120 9 | exclude = .venv,.tox,*.egg,build 10 | select = E,W,F,C,N 11 | 12 | [check-manifest] 13 | ignore = 14 | tox.ini 15 | docs* 16 | tests* 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | # Get the long description from the README file 7 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='vbftool', 12 | 13 | # Versions should comply with PEP 440: 14 | # https://www.python.org/dev/peps/pep-0440/ 15 | version='1.0a0', 16 | 17 | description='Versatile Binary Format Tool', 18 | 19 | long_description=long_description, 20 | long_description_content_type='text/markdown', 21 | 22 | url='https://github.com/dsch/vbftool', 23 | 24 | author='David Schneider', 25 | author_email='schneidav81@gmail.com', 26 | 27 | classifiers=[ 28 | 'Development Status :: 3 - Alpha', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Embedded Systems', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | ], 38 | 39 | packages=['vbftool'], 40 | 41 | python_requires='>=3.5', 42 | 43 | # This field corresponds to the "Project-URL" metadata fields: 44 | # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use 45 | project_urls={ 46 | 'Bug Reports': 'https://github.com/dsch/vbftool/issues', 47 | 'Source': 'https://github.com/dsch/vbftool', 48 | 'Continuous Integration': 'https://travis-ci.org/dsch/vbftool', 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsch/vbftool/ef9796f0feddd10f87dccf56ec3c5b4fa6f85624/tests/__init__.py -------------------------------------------------------------------------------- /tests/lorem.lszz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsch/vbftool/ef9796f0feddd10f87dccf56ec3c5b4fa6f85624/tests/lorem.lszz -------------------------------------------------------------------------------- /tests/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacus neque, rutrum eget ultricies vitae, varius rhoncus eros. Praesent ante dui, porttitor at ante id, interdum mattis est. Proin sed risus nisi. Ut sollicitudin fringilla mi sed mattis. Fusce venenatis tincidunt felis, a placerat diam facilisis eu. In nec tortor ut arcu pretium pellentesque. Mauris id massa vitae quam consequat facilisis. Duis interdum condimentum nibh. Suspendisse vel lacus ut mauris aliquam auctor. Praesent at mauris lorem. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 2 | Cras quis magna arcu. Quisque dapibus ornare vehicula. Sed ut iaculis lectus. Etiam sed eros sem. Pellentesque varius viverra nibh eu varius. Morbi euismod pharetra ex, ac posuere eros sagittis non. Nam semper non libero in ornare. Phasellus pharetra venenatis pulvinar. Phasellus egestas sapien est, ultrices facilisis leo lobortis nec. Mauris posuere quam vel justo lobortis consequat. Nulla a lacus sit amet metus varius convallis vitae eget metus. 3 | Aenean ornare leo accumsan urna lacinia gravida. Cras eu convallis dui. Quisque pulvinar augue vitae enim euismod hendrerit. Maecenas dignissim sit amet nunc id cursus. Sed fringilla accumsan ipsum a efficitur. Nam id molestie libero. Maecenas libero ligula, posuere ut fringilla vitae, tempus id dolor. Pellentesque dapibus dolor nisi, non dapibus nibh convallis sed. Duis at quam est. Vestibulum arcu nibh, posuere nec volutpat a, vulputate facilisis risus. Vestibulum sit amet elit sem. Nunc hendrerit venenatis viverra. Nulla facilisi. Sed nulla tellus, vestibulum non nibh id, lacinia auctor ante. Aliquam dui ante, tincidunt eu sem vitae, dapibus porta magna. Ut nec eros luctus, pharetra velit nec, dictum metus. 4 | Aliquam eget feugiat mauris. Morbi tempus neque velit, ac ornare diam suscipit vitae. Nulla facilisi. Duis hendrerit leo nec tincidunt lacinia. Nulla nec urna euismod, elementum leo non, finibus eros. Sed porta vulputate dolor eu porta. Morbi consequat rhoncus lacus cras amet. -------------------------------------------------------------------------------- /tests/reference.vbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsch/vbftool/ef9796f0feddd10f87dccf56ec3c5b4fa6f85624/tests/reference.vbf -------------------------------------------------------------------------------- /tests/test_checksum.py: -------------------------------------------------------------------------------- 1 | from vbftool.checksum import crc16, crc32 2 | from struct import pack 3 | from pytest import mark 4 | 5 | 6 | @mark.parametrize("data,expected", [ 7 | ('00 00 00 00', 0x84C0), 8 | ('F2 01 83', 0xD374), 9 | ('0F AA 00 55', 0x2023), 10 | ('00 FF 55 11', 0xB8F9), 11 | ('33 22 55 AA BB CC DD EE FF', 0xF53F), 12 | ('92 6B 55', 0x0745), 13 | ('FF FF FF FF', 0x1D0F), 14 | ('31 32 33 34 35 36 37 38 39', 0x29B1), # check 15 | ('31 32 33 34 35 36 37 38 39 29 b1', 0x0000), # magic 16 | ]) 17 | def test_crc16(data, expected): 18 | assert crc16(bytes.fromhex(data)) == expected 19 | 20 | 21 | @mark.parametrize("data,expected", [ 22 | (bytes.fromhex('00 00 00 00'), 0x2144DF1C), 23 | (bytes.fromhex('F2 01 83'), 0x24AB9D77), 24 | (bytes.fromhex('0F AA 00 55'), 0xB6C9B287), 25 | (bytes.fromhex('00 FF 55 11'), 0x32A06212), 26 | (bytes.fromhex('33 22 55 AA BB CC DD EE FF'), 0xB0AE863D), 27 | (bytes.fromhex('92 6B 55'), 0x9CDEA29B), 28 | (bytes.fromhex('FF FF FF FF'), 0xffffffff), 29 | (b'123456789', 0xCBF43926), # check 30 | (b'123456789' + pack('\x17!' \ 10 | b'\x16\x00\x14-\xa2h\x04\xa0\x07Q\xb9\x83q\x0f\xa1o\x06\x00\x04\x80\x1d\x88j\x00\x12\x01\x08F0ZA' \ 11 | b'\xc0UD\xd8\\\xc0p\x03P\x05\xe0\t@\x15\x00\xbf\x0b0\xc5\x00b\x16\x91\xac\x16\x11\\\x16\x91\x80b' \ 12 | b'\xbd\x03\xd8Q\x83`X\xc5\x00]\x84\x10\x10\x10\x16\x00,\xc4Y\x00r\x01D\x17Q\x066`h706\x03\xa4.\x00' \ 13 | b'\xf8EH\t\xa1\x1b\xa0\x05\xc0\x89\x81\x1f\x01\xcc\x030E\xa0\xddG\xc0RF\xf8\x10 \xb1\x90\x90\x12"' \ 14 | b'\x04\x04&\x00\x9c\x0b\x90s\x01\x1e\x00\x0c\x04\xa0\n\xc1\xb8\x03@\xb6\x08!\x16\xc0\x11\x84\x9b' \ 15 | b'\x856\xc2\t\x80\x99\x08\xa2\x01\\7;\x99\x1b\x00\xc2r\x00\x8c\x04\xa0@\xe9\x97\xb1"BK\x18|\x05\x10' \ 16 | b'F\x01j\x11N\xc0tI0\x01\x80+\x80U\x04e\x85\x8a\xd0\x0e\x04}\x81W\x03J%\xc4\x07\xf1[\x01\xea\x94q' \ 17 | b'\x1e@Y\xce\xc2E\x80\xbd\x99\xcf\x00D\x05\x10\x03\xdd\x1c\xc7\x01\xe2\x01\x00\xc9\x85\x0c\xe2\x81q' \ 18 | b'\x02\x92\x17\x038\x04DK\xb8\r\x91\x1d\x83oA\xc0\x1e@\xec\x00(\x13\xee\x02T\x8b\x98"\x11,\x02\x82D' \ 19 | b'\xdc\x00\x98\x03\x007[\x11\'\x01\x94\x08\\\x02\xc8XDp\xa3\x01!ce@\xc0\x1eBAh4\xe4\x1f\xc0<\x8c*' \ 20 | b'\x86y\x80d\x82)\x03\xea%\xbc,\xe9P\nI\x8b\x90\xa8\xa6\x11\x08\xc2IA\x02\x81\xb0\x01\x90(\x9c\x12' \ 21 | b'\xa0\x0008\x02\x94\x016\x81\x1e\x00p\x87,Q\xc0(\xc1>\xa4\x08D\xe4\x01\x93\n*\x10\x02P#\xb8\x83' \ 22 | b'\xb9\x0c\x01?\x83P+\x12\x19\xd80\x81\x00\x0b\x92#\x13^D\xd0VJ\x961D@\x8d\xc0\x9a\ns\x0bm\xbcx\x05' \ 23 | b'\xc2\xd0\xa1\x05\x1f\n\xee\x0b\xc1\x19\x12|\x02@\x00H+0\x14\x00\xac\xd13\x02 \x02\x0cH\xec-\xcb' \ 24 | b'\x9c)\xca\xb0\xed\xa9\x0c\x98\xe8\xa1\xe1\xb10\xf1H\xc2\xa0\x15]\x8a\xf8\x05p\x03X\x13\xf0\xafw ' \ 25 | b'\x11p\n\xe0]AA\x8bA\xa0\xa4(\xf4\x12\xe8\xb1@O`\x94\xc0Z\x90f\x00\xae\x81xI\\\x15\xa0f\xc0\x03' \ 26 | b'\xc2\xc4PE\x8d\x11=\x02\xc6\xec\x1d\x8b\xd9\x9c\x12\xb5\x1b`\x12\xc0\xe7\xa0\xc0; \x17\x10\x03' \ 27 | b'\xa0W\xf0\x1cT\x02l|\x00\x12\x08_\x10\xd2\x88\xcc_\xe0\ra\x0b\xea\x14Q\xfb\x1aY\n\x0bW\x11\x0c\n' \ 28 | b'\x01\x9c\xd90@Samo\x80/\xc1\xd7\x82h\x17\xce&h\x11\xc0\xd9\xc9\x1060O \x7fE.\x12\xde\x10\x07\x02,' \ 29 | b'S|6\x06;\x83gX\xe4\x03\x90\xd7\x81~\x05\xde\xd6\x83r\'\xe8\x06(=\xc1,\xa0\xa8\x81\xac\x88`\x1bJg' \ 30 | b'\x03x"\x18_k\xa3PI\x00Z\xc9\xf4\x85\xc9\xdb\x02\xe6\x15$C\x86\x01\x8b\xdan\x03\x0c\x0e\xd8\xb70' \ 31 | b'\xb3Y\x94X1p:\x00\xc8\x10\xb6\x15\xbd\xcd\x83\xc7\x06\xfe\x0c\xc1D\xd4\xb3\x15 @\x1e\x80i\x82A' \ 32 | b'\x17j\xaaXFd\x17S$\x00\x81\xc1\x94\x02~\x11z6A\xf4\x07\x84\xd5\xf5\'\x96)\x06k\x84U\x82A\x06ZEq' \ 33 | b'\x96\xc9\x85\x88U\xe2\x13\x82\x88\x98H\xf7\x0b\xbc6\xec&\xcc*\xc3\xf0\x8b\x007\x00\x1cb\x9b\x05l' \ 34 | b'\x19 \x87P+@\nH)`\xb8\x1e\xe1#\xe3v|\x00PA\xbc\x01>\x07!.\xc0F!\x1a)}\xaeT\x00X\n\xb87\xbe\x02' \ 35 | b'\xe8D\x8b\xb5YZ*\xf7Y\xc8\xb0\xc6Ap\x08\xb4\x8e\xb84\x829\xc2{!\x96\x92\xd91\xec\x05\xb8\x01\xfb' \ 36 | b'\x07\x80T`Y\xc4\x1b\x82E\x00\x9c\x14\x10\xbc\x06`\xc3\x10\xa73\'\x823\x81\x19\x80b\x05\x0e\x01P' \ 37 | b'\x90 \x93f.\x8c\xc1\xc1\xe7\x82\xd7\x0bk\x15\x00\xe8\x06\x80*\xa2\xd8N\x08\xb0\xf35 \x9b\xc1$\x93' \ 38 | b'M\x94\x00G\x00U\x10\x8e\xa0\x1d\xa0\x95\x80\x0c2{P&ve,\x03@\x0c\xa8\xab u\xe4\xea\xc4\x13\x05\x89' \ 39 | b'B\x01>T4\x01\xac\x96\xb1\xaa \x1a\xc0\tYp\x83\xbe\x13\xcc!\x04\x1b\xf0!a\'\x04\x8a|\xc7\xa2\x0e' \ 40 | b'\x876J\xf0\x94d\x03\xf5hl\xda\x08\x1d\x18\x89U\xbe\x0b\\\t\xf8\x151\\\xa2\xd9\r\xea\x83\x95\x01' \ 41 | b'\xd2\x06\xc8J\x10\xac\x02G(\xab\x10\xe8Ad@\xaa.\xf0\xd8S\x05\x81@\x80f\x05\x01\x05\xca\xd0\xf8' \ 42 | b'\xc0`\x01\x11\x16\xee\x05\x84laD\x85\x99K\x80\x00' 43 | DATA = b'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacus neque, rutrum eget ultricies vitae, ' \ 44 | b'varius rhoncus eros. Praesent ante dui, porttitor at ante id, interdum mattis est. Proin sed risus nisi. Ut ' \ 45 | b'sollicitudin fringilla mi sed mattis. Fusce venenatis tincidunt felis, a placerat diam facilisis eu. In nec ' \ 46 | b'tortor ut arcu pretium pellentesque. Mauris id massa vitae quam consequat facilisis. Duis interdum ' \ 47 | b'condimentum nibh. Suspendisse vel lacus ut mauris aliquam auctor. Praesent at mauris lorem. Class aptent ' \ 48 | b'taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.\nCras quis magna arcu. ' \ 49 | b'Quisque dapibus ornare vehicula. Sed ut iaculis lectus. Etiam sed eros sem. Pellentesque varius viverra ' \ 50 | b'nibh eu varius. Morbi euismod pharetra ex, ac posuere eros sagittis non. Nam semper non libero in ornare. ' \ 51 | b'Phasellus pharetra venenatis pulvinar. Phasellus egestas sapien est, ultrices facilisis leo lobortis nec. ' \ 52 | b'Mauris posuere quam vel justo lobortis consequat. Nulla a lacus sit amet metus varius convallis vitae eget ' \ 53 | b'metus.\nAenean ornare leo accumsan urna lacinia gravida. Cras eu convallis dui. Quisque pulvinar augue ' \ 54 | b'vitae enim euismod hendrerit. Maecenas dignissim sit amet nunc id cursus. Sed fringilla accumsan ipsum a ' \ 55 | b'efficitur. Nam id molestie libero. Maecenas libero ligula, posuere ut fringilla vitae, tempus id dolor. ' \ 56 | b'Pellentesque dapibus dolor nisi, non dapibus nibh convallis sed. Duis at quam est. Vestibulum arcu nibh, ' \ 57 | b'posuere nec volutpat a, vulputate facilisis risus. Vestibulum sit amet elit sem. Nunc hendrerit venenatis ' \ 58 | b'viverra. Nulla facilisi. Sed nulla tellus, vestibulum non nibh id, lacinia auctor ante. Aliquam dui ante, ' \ 59 | b'tincidunt eu sem vitae, dapibus porta magna. Ut nec eros luctus, pharetra velit nec, dictum metus.\nAliquam ' \ 60 | b'eget feugiat mauris. Morbi tempus neque velit, ac ornare diam suscipit vitae. Nulla facilisi. Duis ' \ 61 | b'hendrerit leo nec tincidunt lacinia. Nulla nec urna euismod, elementum leo non, finibus eros. Sed porta ' \ 62 | b'vulputate dolor eu porta. Morbi consequat rhoncus lacus cras amet.' 63 | 64 | 65 | def test_dict(): 66 | d = Dictionary() 67 | d.append(50) 68 | assert d[0] == 50 69 | 70 | 71 | def test_dict_full(): 72 | dct = Dictionary() 73 | for d in DATA[:1024]: 74 | dct.append(d) 75 | 76 | for i in range(1024): 77 | assert dct[i] == DATA[i] 78 | 79 | 80 | def test_dict_wrap(): 81 | dct = Dictionary() 82 | for d in DATA[:1025]: 83 | dct.append(d) 84 | 85 | assert dct[0] == DATA[1024] # wrapped 86 | for i in range(1023): 87 | assert dct[i + 1] == DATA[i + 1] 88 | 89 | 90 | def test_dict_pos(): 91 | dct = Dictionary() 92 | for i, d in enumerate(DATA): 93 | dct.append(d) 94 | assert dct[i % 1024] == d 95 | 96 | 97 | def test_dict_data(): 98 | dct = Dictionary() 99 | for i, d in enumerate(DATA): 100 | dct.append(d) 101 | assert dct.data == DATA[max(0, i - 1023):i + 1] 102 | 103 | 104 | def test_decompress(): 105 | decompressor = Decompressor() 106 | assert decompressor.decompress(COMPRESSED_DATA) == DATA 107 | 108 | 109 | def test_decompress_chunked(): 110 | decompressor = Decompressor() 111 | 112 | stream = io.BytesIO(COMPRESSED_DATA) 113 | out = b'' 114 | for chunk in iter(lambda: stream.read(64), b''): 115 | out += decompressor.decompress(chunk) 116 | 117 | assert (out == DATA) 118 | 119 | 120 | def test_lookahead_init_buffer(): 121 | lab = LookAheadBuffer(DATA) 122 | assert lab.lookahead == DATA[0:17] 123 | 124 | 125 | def test_lookahead_iter_data(): 126 | lab = LookAheadBuffer(DATA) 127 | it = lab.iter() 128 | for x in DATA: 129 | assert next(it) == x 130 | 131 | 132 | def test_lookahead_iter_lookaheaed(): 133 | lab = LookAheadBuffer(DATA) 134 | it = lab.iter() 135 | for i in range(len(DATA)): 136 | next(it) 137 | assert lab.lookahead == DATA[i:i + 17] 138 | 139 | 140 | def test_lookahead_dict_init(): 141 | lab = LookAheadBuffer(DATA) 142 | assert lab.dictionary.data == b'' 143 | 144 | # from vbftool.lzss import Compressor 145 | # def test_compress(): 146 | # compressor = Compressor() 147 | # 148 | # compressed = compressor.compress(DATA) 149 | # 150 | # l = 15 151 | # for i, (d, c) in enumerate(zip(compressed[0:l], COMPRESSED_DATA[0:l])): 152 | # print(i, hex(d), hex(c)) 153 | # assert d == c 154 | # 155 | # # assert compressed == COMPRESSED_DATA 156 | # 157 | # decompressor = Decompressor() 158 | # assert decompressor.decompress(compressed) == DATA 159 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | import vbftool.options as opts 4 | from vbftool import SwPartType, Network, FrameFormat 5 | 6 | 7 | def assert_desc(desc, expected): 8 | with BytesIO() as fp: 9 | desc.dump(fp) 10 | fp.seek(0) 11 | assert fp.read() == expected 12 | 13 | 14 | def test_description_single_line(): 15 | desc = opts.Description('description') 16 | assert_desc(desc, b'\t// Description\r\n' 17 | b'\tdescription = {"description"};\r\n' 18 | b'\r\n') 19 | 20 | 21 | def test_description_multi_line(): 22 | desc = opts.Description(['description1', 'description2']) 23 | assert_desc(desc, b'\t// Description\r\n' 24 | b'\tdescription = {"description1", "description2"};\r\n' 25 | b'\r\n') 26 | 27 | 28 | def test_sw_part_number_wers_or_eniva(): 29 | desc = opts.SwPartNumber('WERS number') 30 | assert_desc(desc, b'\t// Software part number\r\n' 31 | b'\tsw_part_number = "WERS number";\r\n' 32 | b'\r\n') 33 | 34 | 35 | def test_sw_part_number_wers_and_eniva(): 36 | desc = opts.SwPartNumber(['WERS number', "ENOVIA number"]) 37 | assert_desc(desc, b'\t// Software part number\r\n' 38 | b'\tsw_part_number = {"WERS number", "ENOVIA number"};\r\n' 39 | b'\r\n') 40 | 41 | 42 | def test_sw_part_number_DID(): 43 | desc = opts.SwPartNumberDID(0xF188) 44 | assert_desc(desc, b'\t// DID to read software part number\r\n' 45 | b'\tsw_part_number_DID = 0xF188;\r\n' 46 | b'\r\n') 47 | 48 | 49 | def test_sw_part_type(): 50 | desc = opts.SwPartType(SwPartType.EXE) 51 | assert_desc(desc, b'\t// Software part type\r\n' 52 | b'\tsw_part_type = EXE;\r\n' 53 | b'\r\n') 54 | 55 | 56 | def test_data_format_identifier_none(): 57 | desc = opts.DataFormatIdentifier(0, 0) 58 | assert_desc(desc, b'\t// Format identifier\r\n' 59 | b'\tdata_format_identifier = 0x00;\r\n' 60 | b'\r\n') 61 | 62 | 63 | def test_data_format_identifier_compressed(): 64 | desc = opts.DataFormatIdentifier(1, 0) 65 | assert_desc(desc, b'\t// Format identifier\r\n' 66 | b'\tdata_format_identifier = 0x10;\r\n' 67 | b'\r\n') 68 | 69 | 70 | def test_network_single(): 71 | desc = opts.Network(Network.CAN_HS) 72 | assert_desc(desc, b'\t// Network type or list\r\n' 73 | b'\tnetwork = CAN_HS;\r\n' 74 | b'\r\n') 75 | 76 | 77 | def test_network_subnet(): 78 | desc = opts.Network([Network.CAN_HS, Network.SUB_CAN1]) 79 | assert_desc(desc, b'\t// Network type or list\r\n' 80 | b'\tnetwork = {CAN_HS, SUB_CAN1};\r\n' 81 | b'\r\n') 82 | 83 | 84 | def test_ecu_address_single(): 85 | desc = opts.EcuAddress(0x723) 86 | assert_desc(desc, b'\t// ECU address or list\r\n' 87 | b'\tecu_address = 0x723;\r\n' 88 | b'\r\n') 89 | 90 | 91 | def test_ecu_address_subnet(): 92 | desc = opts.EcuAddress([0x723, 0x740]) 93 | assert_desc(desc, b'\t// ECU address or list\r\n' 94 | b'\tecu_address = {0x723, 0x740};\r\n' 95 | b'\r\n') 96 | 97 | 98 | def test_frame_format(): 99 | desc = opts.FrameFormat(FrameFormat.CAN_STANDARD) 100 | assert_desc(desc, b'\t// Format frame\r\n' 101 | b'\tframe_format = CAN_STANDARD;\r\n' 102 | b'\r\n') 103 | 104 | 105 | def test_erase_single_block(): 106 | desc = opts.Erase([(0x10000, 0x20000)]) 107 | assert_desc(desc, b'\t// Erase block\r\n' 108 | b'\terase = {{0x10000, 0x20000}};\r\n' 109 | b'\r\n') 110 | 111 | 112 | def test_erase_multi_block(): 113 | desc = opts.Erase([(0x10000, 0x20000), (0x50000, 0x1000)]) 114 | assert_desc(desc, b'\t// Erase block\r\n' 115 | b'\terase = {{0x10000, 0x20000}, {0x50000, 0x1000}};\r\n' 116 | b'\r\n') 117 | 118 | 119 | def test_call(): 120 | desc = opts.Call(0x10000) 121 | assert_desc(desc, b'\t// Call address\r\n' 122 | b'\tcall = 0x10000;\r\n' 123 | b'\r\n') 124 | -------------------------------------------------------------------------------- /tests/test_vbf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import BytesIO 3 | from array import array 4 | 5 | from vbftool import Vbf, VbfVersion, SwPartType, Network, FrameFormat 6 | import vbftool.options as opts 7 | 8 | 9 | def test_block(): 10 | block = Vbf.create_data_block(0x1200, b'\x33\x22\x55\xAA\xBB\xCC\xDD\xEE\xFF') 11 | assert block == b'\x00\x00\x12\x00\x00\x00\x00\x09\x33\x22\x55\xaa\xbb\xcc\xdd\xee\xff\xf5\x3f' 12 | 13 | 14 | def test_reference(): 15 | data = array('B', range(1, 255)) 16 | start_addr = 64 * 1024 # 64 KB 17 | length = 128 * 1024 # 128 KB 18 | vbf = Vbf(VbfVersion.VERSION_JLR3_0) 19 | vbf.add_data(start_addr, data) 20 | vbf.add_option(opts.Description(['SOP software for X400 AWD', 21 | 'Created: 2002-03-14'])) 22 | vbf.add_option(opts.SwPartNumber('318-08832-AB')) 23 | vbf.add_option(opts.SwPartNumberDID(0xF188)) 24 | vbf.add_option(opts.SwPartType(SwPartType.EXE)) 25 | vbf.add_option(opts.DataFormatIdentifier(0, 0)) 26 | vbf.add_option(opts.Network(Network.CAN_HS)) 27 | vbf.add_option(opts.EcuAddress(0x723)) 28 | vbf.add_option(opts.FrameFormat(FrameFormat.CAN_STANDARD)) 29 | vbf.add_option(opts.Erase([(start_addr, length)])) 30 | vbf.add_option(opts.Call(start_addr)) 31 | 32 | ref_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'reference.vbf') 33 | with open(ref_filename, 'rb') as fp: 34 | reference = fp.read() 35 | 36 | with open('test.vbf', 'wb') as fp: 37 | vbf.dump(fp) 38 | 39 | with BytesIO() as fp: 40 | vbf.dump(fp) 41 | fp.seek(0) 42 | 43 | assert fp.read() == reference 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{35,36,37,38} 4 | lint 5 | 6 | [testenv] 7 | deps = pytest 8 | commands = pytest 9 | 10 | [testenv:lint] 11 | skip_install = True 12 | deps = 13 | flake8 14 | check-manifest 15 | commands = 16 | flake8 17 | check-manifest 18 | python setup.py check -m -s 19 | -------------------------------------------------------------------------------- /vbftool/__init__.py: -------------------------------------------------------------------------------- 1 | from vbftool.vbf import Vbf, VbfVersion, SwPartType, Network, FrameFormat 2 | 3 | __all__ = ['Vbf', 'VbfVersion', 'SwPartType', 'Network', 'FrameFormat'] 4 | -------------------------------------------------------------------------------- /vbftool/checksum.py: -------------------------------------------------------------------------------- 1 | from binascii import crc32 2 | 3 | __all__ = ['crc16', 'crc32'] 4 | 5 | __crctab = [ 6 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 7 | 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 8 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 9 | 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 10 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 11 | 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 12 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 13 | 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 14 | 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 15 | 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 16 | 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 17 | 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 18 | 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 19 | 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 20 | 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 21 | 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 22 | 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 23 | 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 24 | 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 25 | 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 26 | 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 27 | 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 28 | 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 29 | 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 30 | 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 31 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 32 | 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 33 | 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 34 | 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 35 | 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 36 | 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 37 | 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 38 | ] 39 | 40 | 41 | def crc16(data: bytes, crc=0xffff) -> int: 42 | for d in data: 43 | tmp = (crc >> 8) ^ d 44 | crc = ((crc & 0xff) << 8) ^ __crctab[tmp] 45 | return crc 46 | -------------------------------------------------------------------------------- /vbftool/lzss.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | class Buffer: 5 | def __init__(self): 6 | self._queue = deque() 7 | self._use = 0 8 | self._d = 0 9 | self._eof = False 10 | self._needs_input = True 11 | 12 | def __fill(self): 13 | while self._use <= 16: 14 | try: 15 | pop = self._queue.pop() 16 | self._d |= (pop << (24 - 8 - self._use)) 17 | self._use += 8 18 | except IndexError: 19 | break 20 | return self._use > 0 21 | 22 | def decode(self, data): 23 | self._queue.extendleft(data) 24 | self._needs_input = False 25 | 26 | while self.__fill(): 27 | if self._d & 0x800000: 28 | # uncompressed 29 | if self._use >= 9: 30 | data = (self._d >> (24 - 9)) & 0xFF 31 | self._d = (self._d << 9) & 0xFFFFFF 32 | self._use -= 9 33 | yield False, data 34 | else: 35 | self._needs_input = True 36 | break 37 | else: 38 | # compressed 39 | if self._use >= 11: 40 | offset = (self._d >> (24 - 11)) & 0x3FF 41 | if offset == 0: 42 | self._eof = True 43 | break 44 | if self._use >= 15: 45 | offset -= 1 # offset is one based 46 | length = ((self._d >> (24 - 15)) & 0xF) + 2 47 | self._d = (self._d << 15) & 0xFFFFFF 48 | self._use -= 15 49 | yield True, (offset, length) 50 | else: 51 | self._needs_input = True 52 | break 53 | 54 | def _flush_encoded(self): 55 | output = bytearray() 56 | while self._use > 7: 57 | output.append((self._d >> 16) & 0xFF) 58 | self._d = (self._d << 8) & 0xFFFFFF 59 | self._use -= 8 60 | return output 61 | 62 | def encode_uncompressed(self, x): 63 | self._d |= 0x1 << (24 - 1 - self._use) # uncompressed 64 | self._d |= (x & 0xFF) << (24 - 9 - self._use) 65 | self._use += 9 66 | return self._flush_encoded() 67 | 68 | def encode_compressed(self, offset, length): 69 | self._d |= 0x0 << (24 - 1 - self._use) # compressed 70 | self._d |= ((offset + 1) & 0x3FF) << (24 - 11 - self._use) # 10 bits offset 71 | self._d |= ((length - 2) & 0xF) << (24 - 15 - self._use) # 4 bits length 72 | self._use += 15 73 | return self._flush_encoded() 74 | 75 | 76 | class Dictionary: 77 | def __init__(self): 78 | self.dictionary = bytearray(1024) 79 | self.__dict_pos = 0 80 | self.__cnt = 0 81 | 82 | @staticmethod 83 | def __window(x: int) -> int: 84 | return x & 0x3FF 85 | 86 | def increment_dict_pos(self, inc: int): 87 | self.__dict_pos = self.__window(self.__dict_pos + inc) 88 | 89 | def append(self, x): 90 | self.dictionary[self.__dict_pos] = x 91 | self.increment_dict_pos(1) 92 | self.__cnt += 1 93 | 94 | def __getitem__(self, item: int) -> int: 95 | return self.dictionary[self.__window(item)] 96 | 97 | @property 98 | def data(self): 99 | first = bytearray() 100 | if self.__cnt > 1024: 101 | first = self.dictionary[self.__dict_pos:] 102 | 103 | if self.__cnt == 1024: 104 | return self.dictionary 105 | 106 | return first + self.dictionary[0:self.__dict_pos] 107 | 108 | 109 | class Decompressor: 110 | def __init__(self): 111 | self.buf = Buffer() 112 | self.dictionary = Dictionary() 113 | 114 | def decompress(self, data) -> bytearray: 115 | output = bytearray() 116 | with open('decompress.log', 'w') as f: 117 | for compressed, x in self.buf.decode(data): 118 | f.write('%s %s\n' % (compressed, x)) 119 | if compressed: 120 | offset, length = x 121 | for c in range(length): 122 | temp = self.dictionary[offset + c] 123 | self.dictionary.append(temp) 124 | output.append(temp) 125 | else: 126 | # uncompressed 127 | self.dictionary.append(x) 128 | output.append(x) 129 | return output 130 | 131 | 132 | class LookAheadBuffer: 133 | def __init__(self, data, size=17): 134 | self.__data = data 135 | self.__size = size 136 | self.lookahead = data[0:size] 137 | self.dictionary = Dictionary() 138 | 139 | def iter(self): 140 | for nxt in self.__data[self.__size:]: 141 | current = self.lookahead[0] 142 | yield current 143 | self.dictionary.append(current) 144 | self.lookahead = self.lookahead[1:] + bytes([nxt]) 145 | 146 | for current in self.lookahead: 147 | yield current 148 | self.dictionary.append(current) 149 | self.lookahead = self.lookahead[1:] 150 | 151 | 152 | class Compressor: 153 | def __init__(self): 154 | self.buf = Buffer() 155 | self.dictionary = bytearray() 156 | self.lookahead = bytearray() 157 | 158 | def xx(self, data): 159 | # pre-fill lookahead buffer 160 | self.lookahead = data[0:17] 161 | 162 | for l in data[17:]: 163 | yield l 164 | self.dictionary.append(self.lookahead[0]) 165 | self.lookahead = self.lookahead[1:] + bytes([l]) 166 | 167 | def compress(self, data: bytearray) -> bytearray: 168 | output = bytearray() 169 | 170 | with open('compress.log', 'w') as f: 171 | 172 | gen = self.xx(data) 173 | for l in gen: 174 | for i in range(17, 1, -1): 175 | index = self.dictionary.rfind(self.lookahead[:i]) 176 | if index >= 0: 177 | length = i 178 | f.write('%s %s\n' % (True, (index, length))) 179 | output += self.buf.encode_compressed(index, length) 180 | for _ in range(length - 1): 181 | next(gen) 182 | break 183 | 184 | if index < 0: 185 | f.write('%s %s\n' % (False, self.lookahead[0])) 186 | output += self.buf.encode_uncompressed(self.lookahead[0]) 187 | 188 | return output 189 | -------------------------------------------------------------------------------- /vbftool/options.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from vbftool.output import writeline, newline 3 | 4 | 5 | # Allowed identifiers: 6 | # - vbf_version 7 | # - header 8 | # - description 9 | # - sw_part_number 10 | # - sw_part_number_DID 11 | # - sw_part_type 12 | # - VBF_package_release_number 13 | # - data_format_identifier (optional) 14 | # - network 15 | # - ecu_address 16 | # - frame_format 17 | # - erase (optional) 18 | # - omit (optional) 19 | # - call (mandatory only for SBL, GBL, TEST) 20 | # - file_checksum 21 | 22 | 23 | class Option: 24 | def __init__(self, name, description, value, number_format='0x%x'): 25 | self._name = name 26 | self._desc = description 27 | self._value = value 28 | self._number_format = number_format 29 | 30 | def _format_value(self, value): 31 | if isinstance(value, Enum): 32 | value = value.value 33 | elif isinstance(value, int): 34 | value = self._number_format % value 35 | elif isinstance(value, str): 36 | value = '"%s"' % value 37 | elif isinstance(value, tuple) or isinstance(value, list): 38 | x = [self._format_value(s) for s in value] 39 | value = '{%s}' % ', '.join(x) 40 | return value 41 | 42 | def dump(self, fp): 43 | writeline(fp, '\t// %s' % self._desc) 44 | 45 | value = self._format_value(self._value) 46 | 47 | writeline(fp, '\t%s = %s;' % (self._name, value)) 48 | newline(fp) 49 | 50 | 51 | class Description(Option): 52 | def __init__(self, value): 53 | if not isinstance(value, list): 54 | value = [value] 55 | super().__init__('description', 'Description', value) 56 | 57 | 58 | class SwPartNumber(Option): 59 | def __init__(self, value): 60 | super().__init__('sw_part_number', 'Software part number', value) 61 | 62 | 63 | class SwPartNumberDID(Option): 64 | def __init__(self, value): 65 | super().__init__('sw_part_number_DID', 66 | 'DID to read software part number', value, number_format='0x%04X') 67 | 68 | 69 | class SwPartType(Option): 70 | def __init__(self, value): 71 | super().__init__('sw_part_type', 'Software part type', value) 72 | 73 | 74 | class DataFormatIdentifier(Option): 75 | def __init__(self, compression_method, encryption_method): 76 | super().__init__('data_format_identifier', 'Format identifier', 77 | compression_method << 4 | encryption_method, 78 | number_format='0x%02x') 79 | 80 | 81 | class Network(Option): 82 | def __init__(self, value): 83 | super().__init__('network', 'Network type or list', value) 84 | 85 | 86 | class EcuAddress(Option): 87 | def __init__(self, value): 88 | super().__init__('ecu_address', 'ECU address or list', value) 89 | 90 | 91 | class FrameFormat(Option): 92 | def __init__(self, value): 93 | super().__init__('frame_format', 'Format frame', value) 94 | 95 | 96 | class Erase(Option): 97 | def __init__(self, value): 98 | super().__init__('erase', 'Erase block', value) 99 | 100 | 101 | class Call(Option): 102 | def __init__(self, value): 103 | super().__init__('call', 'Call address', value) 104 | -------------------------------------------------------------------------------- /vbftool/output.py: -------------------------------------------------------------------------------- 1 | def writeline(fp, s): 2 | fp.write(b'%s\r\n' % s.encode('utf-8')) 3 | 4 | 5 | def newline(fp): 6 | fp.write(b'\r\n') 7 | -------------------------------------------------------------------------------- /vbftool/vbf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from enum import Enum 4 | 5 | from vbftool.checksum import crc16, crc32 6 | from vbftool.options import Option 7 | from vbftool.output import writeline, newline 8 | from vbftool.lzss import Compressor 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class VbfVersion(Enum): 14 | VERSION_2_2 = '2.2' 15 | VERSION_2_3 = '2.3' 16 | VERSION_2_4 = '2.4' 17 | VERSION_2_5 = '2.5' 18 | VERSION_3_0 = '3.0' 19 | VERSION_JLR3_0 = 'JLR3.0' 20 | 21 | 22 | class Network(Enum): 23 | CAN_HS = 'CAN_HS' # main network @ 500 kbit/s 24 | CAN_MS = 'CAN_MS' # main network @ 125 kbit/s 25 | FLEXRAY = 'FLEXRAY' # Main network 26 | DOIP = 'DOIP' # Main network 27 | SUB_MOST = 'SUB_MOST' # MOST sub network connected to a gateway 28 | SUB_CAN1 = 'SUB_CAN1' # CAN sub network connected to a gateway 29 | SUB_CAN2 = 'SUB_CAN2' # CAN sub network connected to a gateway 30 | SUB_LIN1 = 'SUB_LIN1' # LIN sub network connected to a gateway 31 | SUB_LIN2 = 'SUB_LIN2' # LIN sub network connected to a gateway 32 | SUB_OTHER = 'SUB_OTHER' # Other sub network connected to a gateway 33 | 34 | 35 | class FrameFormat(Enum): 36 | # standard frame format, 11-bit CAN identifiers 37 | CAN_STANDARD = 'CAN_STANDARD' 38 | # extended frame format, 29-bit CAN identifiers 39 | CAN_EXTENDED = 'CAN_EXTENDED' 40 | # 16 bit DoIP logical address or 16 bit FlexRay logical address 41 | LOGICAL_ADDRESS = '16BIT_STANDARD' 42 | 43 | 44 | class SwPartType(Enum): 45 | CARCFG = 'CARCFG' # Car configuration 46 | CUSTOM = 'CUSTOM' # Customer parameters 47 | DATA = 'DATA' # Data or parameters (i.e., calibrations) 48 | EXE = 'EXE' # Executable (i.e., strategy) 49 | GBL = 'GBL' # Gateway Bootloader 50 | SBL = 'SBL' # Secondary Bootloader 51 | SIGCFG = 'SIGCFG' # Signal configuration 52 | TEST = 'TEST' # Test program, (i.e. production test, diagnostics) 53 | 54 | 55 | class _FileChecksum(Option): 56 | def __init__(self, value): 57 | super().__init__('file_checksum', 'file checksum', value, number_format='0x%08x') 58 | 59 | 60 | class Vbf: 61 | def __init__(self, version): 62 | self.version = version 63 | self._options = [] 64 | self._data = [] 65 | 66 | def add_data(self, address, data): 67 | self._data.append((address, data)) 68 | 69 | def dump(self, fp): 70 | content = bytes() 71 | crc = 0 72 | for address, data in self._data: 73 | block_content = self.create_data_block(address, data) 74 | crc = crc32(block_content, crc) 75 | content += block_content 76 | file_checksum = _FileChecksum(crc) 77 | 78 | writeline(fp, 'vbf_version = %s;' % self.version.value) 79 | newline(fp) 80 | writeline(fp, 'header {') 81 | newline(fp) 82 | 83 | for option in self._options: 84 | option.dump(fp) 85 | 86 | file_checksum.dump(fp) 87 | 88 | fp.write(b'}') 89 | 90 | fp.write(content) 91 | 92 | @staticmethod 93 | def create_data_block(start_addr, data): 94 | data_checksum = crc16(data) # data checksum is always calculated from uncompressed data 95 | if 0: # compression enabled 96 | compressor = Compressor() 97 | data = compressor.compress(bytes(data)) 98 | data_length = len(data) 99 | log.info("block start address: 0x%08x", start_addr) 100 | log.info("block length: 0x%08x", data_length) 101 | log.info("block checksum: 0x%04x", data_checksum) 102 | content = struct.pack('>II', start_addr, data_length) 103 | content += data 104 | content += struct.pack('>H', data_checksum) 105 | return content 106 | 107 | def add_option(self, option): 108 | self._options.append(option) 109 | --------------------------------------------------------------------------------