├── requirements.txt ├── tests ├── data │ ├── u-boot.bin │ ├── imx7d-sdb.dtb │ ├── env.txt │ ├── u-boot.its │ └── script.txt ├── test_fdt_image.py ├── test_env_image.py ├── test_cli_mkenv.py ├── test_env_blob.py ├── test_cli_envimg.py ├── test_cli_mkimg.py └── test_old_image.py ├── .travis.yml ├── uboot ├── __init__.py ├── cli_envimg.py ├── cli_mkenv.py ├── env_image.py ├── env_blob.py ├── common.py ├── cli_mkimg.py ├── fdt_image.py └── old_image.py ├── .gitignore ├── setup.py ├── docs ├── mkenv.md ├── envimg.md └── mkimg.md ├── README.md └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | fdt==0.1.2 2 | click==7.0 3 | easy_enum==0.2.0 4 | -------------------------------------------------------------------------------- /tests/data/u-boot.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molejar/pyUBoot/HEAD/tests/data/u-boot.bin -------------------------------------------------------------------------------- /tests/data/imx7d-sdb.dtb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molejar/pyUBoot/HEAD/tests/data/imx7d-sdb.dtb -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | before_install: 5 | - pip install pytest pytest-console-scripts 6 | - pip install pytest-cov 7 | - pip install coveralls 8 | - pip install -r requirements.txt 9 | install: 10 | - pip install -e . 11 | script: 12 | - py.test --cov=uboot tests/* 13 | after_success: 14 | - coveralls -------------------------------------------------------------------------------- /tests/data/env.txt: -------------------------------------------------------------------------------- 1 | bootdelay=3 2 | stdin=serial 3 | stdout=serial 4 | stderr=serial 5 | baudrate=115200 6 | console=ttymxc3 7 | ethaddr=12:34:56:78:90:AB 8 | ethact=FEC 9 | mmcdev=0 10 | mmcpart=1 11 | rootdev=mmcblk2p2 12 | fdtaddr=0x18000000 13 | fdtfile=imx6q-pop-arm2.dtb 14 | loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdtaddr} ${fdtfile} 15 | imgaddr=0x12000000 16 | imgfile=zImage 17 | loadimg=fatload mmc ${mmcdev}:${mmcpart} ${imgaddr} ${imgfile} 18 | bootargs=console=${console},${baudrate} root=/dev/${rootdev} rootwait rw 19 | bootcmd=run loadfdt; run loadimg; bootz ${imgaddr} - ${fdtaddr}; 20 | -------------------------------------------------------------------------------- /tests/data/u-boot.its: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | 3 | / { 4 | description = "i.MX7D U-Boot Image"; 5 | 6 | images { 7 | uboot@1 { 8 | description = "U-Boot (32-bit)"; 9 | data = /incbin/("u-boot.bin"); 10 | type = "standalone"; 11 | arch = "arm"; 12 | compression = "none"; 13 | load = <0x40200000>; 14 | }; 15 | 16 | fdt@1 { 17 | description = "FDT i.MX7D-SDB"; 18 | data = /incbin/("imx7d-sdb.dtb"); 19 | type = "flat_dt"; 20 | compression = "none"; 21 | }; 22 | }; 23 | 24 | configurations { 25 | default = "config@1"; 26 | 27 | config@1 { 28 | description = "fsl-imx7d-sdb"; 29 | firmware = "uboot@1"; 30 | fdt = "fdt@1"; 31 | }; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /tests/data/script.txt: -------------------------------------------------------------------------------- 1 | # U-Boot Script 2 | 3 | echo '>> Run Script ...' 4 | setenv autoload 'no' 5 | dhcp 6 | setenv serverip 192.168.1.162 7 | setenv hostname 'imx7dsb' 8 | setenv netdev 'eth0' 9 | setenv nfsroot '/srv/nfs/imx7d' 10 | setenv imgfile '/imx7d/zImage' 11 | setenv fdtfile '/imx7d/imx7d-sdb.dtb' 12 | setenv fdtaddr 0x83000000 13 | setenv imgaddr 0x80800000 14 | setenv imgload 'tftp ${imgaddr} ${imgfile}' 15 | setenv fdtload 'tftp ${fdtaddr} ${fdtfile}' 16 | setenv netargs 'setenv bootargs console=${console},${baudrate} root=/dev/nfs rw nfsroot=${serverip}:${nfsroot},v3,tcp ip=dhcp' 17 | setenv netboot 'echo Booting from net ...; run netargs; run imgload; run fdtload; bootz ${imgaddr} - ${fdtaddr};' 18 | run netboot 19 | -------------------------------------------------------------------------------- /tests/test_fdt_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import pytest 17 | from uboot import parse_itb, parse_its, FdtImage 18 | 19 | 20 | # Used Directories 21 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 22 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 23 | 24 | # Test Files 25 | UBOOT_ITS = os.path.join(DATA_DIR, 'u-boot.its') 26 | UBOOT_ITB_TEMP = os.path.join(TEMP_DIR, 'u-boot.itb') 27 | 28 | 29 | def setup_module(module): 30 | # Create temp directory 31 | os.makedirs(TEMP_DIR, exist_ok=True) 32 | 33 | 34 | def teardown_module(module): 35 | # Delete created files 36 | #os.remove(UBOOT_ITB_TEMP) 37 | pass 38 | 39 | 40 | def test_01(): 41 | pass -------------------------------------------------------------------------------- /tests/test_env_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import pytest 18 | from uboot import EnvImgOld 19 | 20 | # Used Directories 21 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 22 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 23 | 24 | # Test Files 25 | UBOOT_BIN = os.path.join(DATA_DIR, 'u-boot.bin') 26 | ENV_TXT_TEMP = os.path.join(TEMP_DIR, 'u-boot_env.txt') 27 | UBOOT_BIN_TEMP = os.path.join(TEMP_DIR, 'u-boot.bin') 28 | 29 | 30 | def setup_module(module): 31 | # Create temp directory 32 | os.makedirs(TEMP_DIR, exist_ok=True) 33 | shutil.copyfile(UBOOT_BIN, UBOOT_BIN_TEMP) 34 | 35 | 36 | def teardown_module(module): 37 | # Delete created files 38 | #os.remove(ENV_TXT_TEMP) 39 | #os.remove(UBOOT_BIN_TEMP) 40 | pass 41 | 42 | 43 | def test_01(): 44 | pass 45 | 46 | 47 | def test_02(): 48 | pass 49 | 50 | 51 | def test_03(): 52 | pass -------------------------------------------------------------------------------- /uboot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .common import EnumArchType, EnumOsType, EnumImageType, EnumCompressionType 16 | from .old_image import StdImage, FwImage, ScriptImage, MultiImage, get_img_type, new_img, parse_img 17 | from .fdt_image import FdtImage, parse_its, parse_itb 18 | from .env_image import EnvImgOld 19 | from .env_blob import EnvBlob 20 | 21 | 22 | __author__ = "Martin Olejar" 23 | __contact__ = "martin.olejar@gmail.com" 24 | __version__ = "0.1.1" 25 | __license__ = "Apache 2.0" 26 | __status__ = "Development" 27 | __all__ = [ 28 | # Classes 29 | 'EnvBlob', 30 | 'EnvImgOld', 31 | 'FdtImage', 32 | 'StdImage', 33 | 'FwImage', 34 | 'ScriptImage', 35 | 'MultiImage', 36 | # Enums 37 | 'EnumOsType', 38 | 'EnumArchType', 39 | 'EnumImageType', 40 | 'EnumCompressionType', 41 | # Methods 42 | 'get_img_type', 43 | 'new_img', 44 | 'parse_img', 45 | 'parse_its', 46 | 'parse_itb' 47 | ] 48 | 49 | 50 | def parse_blob(data, offset=0): 51 | """ Universal parser for binary blob 52 | 53 | :param data: 54 | :param offset: 55 | :return: 56 | """ 57 | raise NotImplementedError() 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE 107 | .idea -------------------------------------------------------------------------------- /tests/test_cli_mkenv.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import pytest 17 | 18 | # Used Directories 19 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 20 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 21 | 22 | # Test Files 23 | ENV_TXT = os.path.join(DATA_DIR, 'env.txt') 24 | ENV_TXT_TEMP = os.path.join(TEMP_DIR, 'env.txt') 25 | ENV_BIN_TEMP = os.path.join(TEMP_DIR, 'env.bin') 26 | 27 | 28 | def setup_module(module): 29 | # Create temp directory 30 | os.makedirs(TEMP_DIR, exist_ok=True) 31 | 32 | 33 | def teardown_module(module): 34 | # Delete created files 35 | os.remove(ENV_TXT_TEMP) 36 | os.remove(ENV_BIN_TEMP) 37 | 38 | 39 | @pytest.mark.script_launch_mode('subprocess') 40 | def test_mkenv_create(script_runner): 41 | ret = script_runner.run('mkenv', 'create', ENV_TXT, ENV_BIN_TEMP) 42 | assert ret.success 43 | 44 | 45 | @pytest.mark.script_launch_mode('subprocess') 46 | def test_mkenv_info(script_runner): 47 | ret = script_runner.run('mkenv', 'info', ENV_BIN_TEMP) 48 | assert ret.success 49 | 50 | 51 | @pytest.mark.script_launch_mode('subprocess') 52 | def test_mkenv_extract(script_runner): 53 | ret = script_runner.run('mkenv', 'extract', ENV_BIN_TEMP) 54 | assert ret.success 55 | -------------------------------------------------------------------------------- /tests/test_env_blob.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import pytest 17 | from uboot import EnvBlob 18 | 19 | # Used Directories 20 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 21 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 22 | 23 | # Test Files 24 | ENV_TXT = os.path.join(DATA_DIR, 'env.txt') 25 | ENV_TXT_TEMP = os.path.join(TEMP_DIR, 'env.txt') 26 | ENV_BIN_TEMP = os.path.join(TEMP_DIR, 'env.bin') 27 | 28 | 29 | def setup_module(module): 30 | # Create temp directory 31 | os.makedirs(TEMP_DIR, exist_ok=True) 32 | 33 | 34 | def teardown_module(module): 35 | # Delete created files 36 | os.remove(ENV_TXT_TEMP) 37 | os.remove(ENV_BIN_TEMP) 38 | 39 | 40 | def test_01(): 41 | env = EnvBlob("Test") 42 | env.set("int_variable", 100) 43 | env.set("str_variable", "value") 44 | 45 | tmp = env.get("str_variable") 46 | assert tmp == "value" 47 | 48 | with pytest.raises(Exception): 49 | tmp = env.get("test") 50 | 51 | 52 | def test_02(): 53 | env = EnvBlob("Test") 54 | 55 | with open(ENV_TXT, 'r') as f: 56 | env.load(f.read()) 57 | 58 | with open(ENV_TXT_TEMP, 'w') as f: 59 | f.write(env.store()) 60 | 61 | with open(ENV_BIN_TEMP, 'wb') as f: 62 | f.write(env.export()) 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 Martin Olejar 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from os import path 19 | from setuptools import setup 20 | from uboot import __version__, __license__, __author__, __contact__ 21 | 22 | 23 | def long_description(): 24 | try: 25 | import pypandoc 26 | 27 | readme_path = path.join(path.dirname(__file__), 'README.md') 28 | return pypandoc.convert(readme_path, 'rst') 29 | except (IOError, ImportError): 30 | return ( 31 | "More on: https://github.com/molejar/pyUBoot" 32 | ) 33 | 34 | setup( 35 | name='uboot', 36 | author=__author__, 37 | version=__version__, 38 | license=__license__, 39 | author_email=__contact__, 40 | url='https://github.com/molejar/pyUBoot', 41 | description='Open Source library for manipulating with U-Boot images and environment variables', 42 | long_description=long_description(), 43 | python_requires='>=3.6', 44 | install_requires=[ 45 | 'fdt==0.1.2', 46 | 'click==7.0', 47 | 'easy_enum==0.2.0' 48 | ], 49 | packages=['uboot'], 50 | classifiers=[ 51 | 'Programming Language :: Python :: 3', 52 | 'Operating System :: OS Independent', 53 | 'License :: OSI Approved :: Apache Software License', 54 | 'Topic :: Scientific/Engineering', 55 | 'Topic :: Software Development :: Embedded Systems', 56 | 'Topic :: Utilities' 57 | ], 58 | entry_points={ 59 | 'console_scripts': [ 60 | 'envimg = uboot.cli_envimg:main', 61 | 'mkenv = uboot.cli_mkenv:main', 62 | 'mkimg = uboot.cli_mkimg:main' 63 | ], 64 | } 65 | ) 66 | -------------------------------------------------------------------------------- /tests/test_cli_envimg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import pytest 18 | 19 | # Used Directories 20 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 21 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 22 | 23 | # Test Files 24 | UBOOT_BIN = os.path.join(DATA_DIR, 'u-boot.bin') 25 | ENV_TXT_TEMP = os.path.join(TEMP_DIR, 'u-boot_env.txt') 26 | UBOOT_BIN_TEMP = os.path.join(TEMP_DIR, 'u-boot.bin') 27 | 28 | 29 | def setup_module(module): 30 | # Create temp directory and copy u-boot.bin into this directory 31 | os.makedirs(TEMP_DIR, exist_ok=True) 32 | shutil.copyfile(UBOOT_BIN, UBOOT_BIN_TEMP) 33 | 34 | 35 | def teardown_module(module): 36 | # Delete created files 37 | os.remove(ENV_TXT_TEMP) 38 | os.remove(UBOOT_BIN_TEMP) 39 | 40 | 41 | @pytest.mark.script_launch_mode('subprocess') 42 | def test_envimg_info(script_runner): 43 | ret = script_runner.run('envimg', 'info', 'bootcmd=', UBOOT_BIN) 44 | assert ret.success 45 | 46 | 47 | @pytest.mark.script_launch_mode('subprocess') 48 | def test_envimg_export(script_runner): 49 | ret = script_runner.run('envimg', 'export', 'bootcmd=', UBOOT_BIN, ENV_TXT_TEMP) 50 | assert ret.success 51 | 52 | 53 | @pytest.mark.script_launch_mode('subprocess') 54 | def test_envimg_update(script_runner): 55 | ret = script_runner.run('envimg', 'update', 'bootcmd=', UBOOT_BIN_TEMP, '-e bootdelay=0') 56 | assert ret.success 57 | 58 | 59 | @pytest.mark.script_launch_mode('subprocess') 60 | def test_envimg_replace(script_runner): 61 | ret = script_runner.run('envimg', 'replace', 'bootcmd=', UBOOT_BIN_TEMP, ENV_TXT_TEMP) 62 | assert ret.success 63 | -------------------------------------------------------------------------------- /tests/test_cli_mkimg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import pytest 18 | 19 | # Used Directories 20 | DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 21 | TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') 22 | 23 | # Test Files 24 | UBOOT_ITS = os.path.join(DATA_DIR, 'u-boot.its') 25 | SCRIPT_TXT = os.path.join(DATA_DIR, 'script.txt') 26 | UBOOT_ITB_TEMP = os.path.join(TEMP_DIR, 'u-boot.itb') 27 | SCRIPT_BIN_TEMP = os.path.join(TEMP_DIR, 'script.bin') 28 | 29 | 30 | def setup_module(module): 31 | # Create temp directory 32 | os.makedirs(TEMP_DIR, exist_ok=True) 33 | 34 | 35 | def teardown_module(module): 36 | # Delete temp directory 37 | shutil.rmtree(TEMP_DIR) 38 | 39 | 40 | @pytest.mark.script_launch_mode('subprocess') 41 | def test_mkimg_create(script_runner): 42 | ret = script_runner.run('mkimg', 'create', '-i', 'script', SCRIPT_BIN_TEMP, SCRIPT_TXT) 43 | assert ret.success 44 | 45 | 46 | @pytest.mark.script_launch_mode('subprocess') 47 | def test_mkimg_info(script_runner): 48 | ret = script_runner.run('mkimg', 'info', SCRIPT_BIN_TEMP) 49 | assert ret.success 50 | 51 | 52 | @pytest.mark.script_launch_mode('subprocess') 53 | def test_mkimg_extract(script_runner): 54 | ret = script_runner.run('mkimg', 'extract', SCRIPT_BIN_TEMP) 55 | assert ret.success 56 | 57 | 58 | @pytest.mark.script_launch_mode('subprocess') 59 | def test_mkimg_create_itb(script_runner): 60 | ret = script_runner.run('mkimg', 'createitb', '-o', UBOOT_ITB_TEMP, UBOOT_ITS) 61 | assert ret.success 62 | 63 | 64 | @pytest.mark.script_launch_mode('subprocess') 65 | def test_mkimg_info_itb(script_runner): 66 | ret = script_runner.run('mkimg', 'infoitb', UBOOT_ITB_TEMP) 67 | assert ret.success 68 | 69 | 70 | @pytest.mark.script_launch_mode('subprocess') 71 | def test_mkimg_extract_itb(script_runner): 72 | ret = script_runner.run('mkimg', 'extractitb', UBOOT_ITB_TEMP) 73 | assert ret.success 74 | -------------------------------------------------------------------------------- /docs/mkenv.md: -------------------------------------------------------------------------------- 1 | U-Boot Environment Blob Tool 2 | ============================ 3 | 4 | The `mkenv` is a tool to generate a U-Boot environment binary blob from a text file or extract U-Boot environment 5 | variables from a binary blob. It's implementing similar functionality as `mkenvimage` tool natively used in Linux. 6 | 7 | Usage 8 | ----- 9 | 10 | For printing a general info of usage this tool execute `mkenv -?`. 11 | 12 | ```sh 13 | $ Usage: mkenv [OPTIONS] COMMAND [ARGS]... 14 | 15 | The U-Boot Make Enviroment Blob Tool 16 | 17 | Options: 18 | -v, --version Show the version and exit. 19 | -?, --help Show this message and exit. 20 | 21 | Commands: 22 | create Create new image from attached file 23 | extract Extract image content 24 | info List image content 25 | ``` 26 | 27 | ## Commands 28 | 29 | #### $ mkenv info FILE 30 | 31 | Print the content of U-Boot environment blob in readable format 32 | 33 | ##### options: 34 | * **-b, --bigendian** - The target is big endian (default is little endian) 35 | * **-o, --offset** - The offset of input file (default: 0) 36 | * **-s, --size** - The environment blob size (default: 8192) 37 | * **-?, --help** - Show help message and exit 38 | 39 | ##### Example: 40 | 41 | ```sh 42 | $ mkenv info env.bin 43 | 44 | Name: None 45 | Redundant: True 46 | Endian: Little 47 | Size: 8192 Bytes 48 | EmptyValue: 0x00 49 | Variables: 50 | - bootdelay = 3 51 | - stdin = serial 52 | - stdout = serial 53 | - stderr = serial 54 | - baudrate = 115200 55 | - console = ttymxc3 56 | ... 57 | ``` 58 | 59 |
60 | 61 | #### $ mkenv extract FILE 62 | 63 | Extract the content of U-Boot environment blob and save it as readable text file. That way you can extract the U-Boot environment blob from SD card image. 64 | 65 | ##### options: 66 | * **-b, --bigendian** - The target is big endian (default is little endian) 67 | * **-o, --offset** - The offset of input file (default: 0) 68 | * **-s, --size** - The environment blob size (default: 8192) 69 | * **-?, --help** - Show help message and exit 70 | 71 | ##### Example: 72 | 73 | ```sh 74 | $ mkenv extract env.bin 75 | 76 | Successfully extracted: env.txt 77 | ``` 78 | 79 |
80 | 81 | #### $ mkenv create [OPTIONS] INFILE OUTFILE 82 | 83 | Create U-Boot environment blob from input text file and save it as binary file. 84 | 85 | ##### options: 86 | * **-r, --redundant** - The environment has multiple copies in flash (default: False) 87 | * **-b, --bigendian** - The target is big endian (default is little endian) 88 | * **-s, --size** - The environment blob size (default: 8192) 89 | * **-?, --help** - Show help message and exit 90 | 91 | ##### Example: 92 | 93 | ```sh 94 | $ mkenv create env.txt env.bin 95 | 96 | Successfully created: env.bin 97 | ``` -------------------------------------------------------------------------------- /docs/envimg.md: -------------------------------------------------------------------------------- 1 | Tool for editing environment variables inside U-Boot image 2 | ========================================================== 3 | 4 | The `envimg` is a tool for editing environment variables inside U-Boot image. 5 | 6 | Usage 7 | ----- 8 | 9 | For printing a general info of usage this tool execute `envimg -?`. 10 | 11 | ```sh 12 | Usage: envimg.py [OPTIONS] COMMAND [ARGS]... 13 | 14 | Tool for editing environment variables inside U-Boot image 15 | 16 | Options: 17 | -v, --version Show the version and exit. 18 | -?, --help Show this message and exit. 19 | 20 | Commands: 21 | info List U-Boot environment variables 22 | export Export U-Boot environment variables 23 | replace Replace U-Boot environment variables 24 | update Update U-Boot environment variables 25 | ``` 26 | 27 | ## Commands 28 | 29 | #### $ envimg info [OPTIONS] MARK FILE 30 | 31 | Print the content of U-Boot environment variables located inside image. 32 | 33 | ##### options: 34 | * **-?, --help** - Show help message and exit 35 | 36 | ##### Example: 37 | 38 | ```sh 39 | $ envimg info bootcmd= u-boot.imx 40 | 41 | Image Name: u-boot.imx 42 | Image Size: 478208 bytes 43 | EnVar Size: 2470 bytes 44 | EnVar Offset: 312273 45 | EnVar Mark: bootcmd= 46 | EnVar Content: 47 | - bootcmd = mmc dev ${mmcdev}; mmc dev ${mmcdev}; if mmc rescan; then run mmcboot; else run netboot; fi; 48 | - bootdelay = 3 49 | - ... 50 | ``` 51 | 52 |
53 | 54 | #### $ envimg export [OPTIONS] MARK FILE FENV 55 | 56 | Export environment variables from U-Boot image. 57 | 58 | ##### options: 59 | * **-?, --help** - Show help message and exit 60 | 61 | ##### Example: 62 | 63 | ```sh 64 | $ envimg export bootcmd= u-boot.imx env.txt 65 | 66 | Environment variables saved into: env.txt 67 | ``` 68 | 69 |
70 | 71 | #### $ envimg update [OPTIONS] MARK FILE 72 | 73 | Update environment variables inside U-Boot image. 74 | 75 | ##### options: 76 | * **-f, --fenv** - The file with environment variables 77 | * **-e, --env** - Environment variables 78 | * **-?, --help** - Show help message and exit 79 | 80 | ##### Example: 81 | 82 | ```sh 83 | $ envimg update bootcmd= u-boot.imx -e "bootdelay = 6" 84 | 85 | Image Name: u-boot.imx 86 | Image Size: 478208 bytes 87 | EnVar Size: 2470 bytes 88 | EnVar Offset: 312273 89 | EnVar Mark: bootcmd= 90 | EnVar Content: 91 | - bootcmd = mmc dev ${mmcdev}; mmc dev ${mmcdev}; if mmc rescan; then run mmcboot; else run netboot; fi; 92 | - bootdelay = 6 93 | - ... 94 | ``` 95 | 96 |
97 | 98 | #### $ envimg replace [OPTIONS] MARK FILE FENV 99 | 100 | Replace environment variables inside U-Boot image. 101 | 102 | ##### options: 103 | * **-?, --help** - Show help message and exit 104 | 105 | ##### Example: 106 | 107 | ```sh 108 | $ envimg update bootcmd= u-boot.imx env.txt 109 | 110 | ... 111 | ``` -------------------------------------------------------------------------------- /uboot/cli_envimg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import sys 17 | import click 18 | import uboot 19 | 20 | # Application error code 21 | ERROR_CODE = 1 22 | 23 | # The version of u-boot tools 24 | VERSION = uboot.__version__ 25 | 26 | # Short description of U-Boot envimg tool 27 | DESCRIP = ( 28 | "Tool for editing environment variables inside U-Boot image" 29 | ) 30 | 31 | 32 | # U-Boot envimg: Base options 33 | @click.group(context_settings=dict(help_option_names=['-?', '--help']), help=DESCRIP) 34 | @click.version_option(VERSION, '-v', '--version') 35 | def cli(): 36 | click.echo() 37 | 38 | 39 | # U-Boot envimg: List U-Boot environment variables 40 | @cli.command(short_help="List U-Boot environment variables") 41 | @click.argument('mark', nargs=1, type=click.STRING) 42 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 43 | def info(mark, file): 44 | """ List U-Boot environment variables """ 45 | try: 46 | envimg = uboot.EnvImgOld(start_string=mark) 47 | envimg.open_img(file) 48 | 49 | except Exception as e: 50 | click.echo(str(e) if str(e) else "Unknown Error !") 51 | sys.exit(ERROR_CODE) 52 | 53 | click.echo(str(envimg)) 54 | 55 | 56 | # U-Boot envimg: Export U-Boot environment variables 57 | @cli.command(short_help="Export U-Boot environment variables") 58 | @click.argument('mark', nargs=1, type=click.STRING) 59 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 60 | @click.argument('fenv', nargs=1, type=click.Path()) 61 | def export(mark, file, fenv): 62 | """ Export U-Boot environment variables """ 63 | try: 64 | envimg = uboot.EnvImgOld(start_string=mark) 65 | envimg.open_img(file) 66 | 67 | with open(fenv, 'w') as f: 68 | f.write(envimg.store()) 69 | 70 | except Exception as e: 71 | click.echo(str(e) if str(e) else "Unknown Error !") 72 | sys.exit(ERROR_CODE) 73 | 74 | click.secho("Environment variables saved into: %s" % fenv) 75 | 76 | 77 | # U-Boot envimg: Update U-Boot environment variables 78 | @cli.command(short_help="Update U-Boot environment variables") 79 | @click.option('-f', '--fenv', type=click.Path(exists=True), help="The file with environment variables") 80 | @click.option('-e', '--env', type=click.STRING, multiple=True, help="Environment variables") 81 | @click.argument('mark', nargs=1, type=click.STRING) 82 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 83 | def update(env, mark, file, fenv=None): 84 | """ Update U-Boot environment variables """ 85 | changed = False 86 | 87 | try: 88 | envimg = uboot.EnvImgOld(start_string=mark) 89 | envimg.open_img(file) 90 | 91 | if fenv is not None: 92 | changed = True 93 | with open(fenv, 'r') as f: 94 | envimg.load(f.read()) 95 | 96 | if env: 97 | changed = True 98 | for var in env: 99 | name, value = var.split('=', 1) 100 | envimg.set(name.strip(), value.strip()) 101 | 102 | if changed: 103 | envimg.save_img(file) 104 | 105 | except Exception as e: 106 | click.echo(str(e) if str(e) else "Unknown Error !") 107 | sys.exit(ERROR_CODE) 108 | 109 | click.echo(str(envimg)) 110 | 111 | 112 | # U-Boot envimg: Replace U-Boot environment variables 113 | @cli.command(short_help="Replace U-Boot environment variables") 114 | @click.argument('mark', nargs=1, type=click.STRING) 115 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 116 | @click.argument('fenv', nargs=1, type=click.Path(exists=True)) 117 | def replace(mark, file, fenv): 118 | """ Replace U-Boot environment variables """ 119 | try: 120 | envimg = uboot.EnvImgOld(start_string=mark) 121 | envimg.open_img(file) 122 | envimg.clear() 123 | 124 | with open(fenv, 'r') as f: 125 | envimg.load(f.read()) 126 | 127 | envimg.save_img(file) 128 | 129 | except Exception as e: 130 | click.echo(str(e) if str(e) else "Unknown Error !") 131 | sys.exit(ERROR_CODE) 132 | 133 | click.echo(str(envimg)) 134 | 135 | 136 | def main(): 137 | cli(obj={}) 138 | 139 | 140 | if __name__ == '__main__': 141 | main() 142 | -------------------------------------------------------------------------------- /docs/mkimg.md: -------------------------------------------------------------------------------- 1 | U-Boot Image Tool 2 | ================= 3 | 4 | The `mkimg` is a tool to create images for use with the U-Boot boot loader. These images can contain the linux kernel, device tree blob, root file system image, firmware images etc., either separate or combined.It's implementing similar functionality as `mkimage` tool used in Linux. 5 | 6 | Usage 7 | ----- 8 | 9 | For printing a general info of usage this tool execute `mkimg -?`. 10 | 11 | ```sh 12 | $ Usage: mkimg [OPTIONS] COMMAND [ARGS]... 13 | 14 | The U-Boot Image Tool 15 | 16 | Options: 17 | -v, --version Show the version and exit. 18 | -?, --help Show this message and exit. 19 | 20 | Commands: 21 | create Create old U-Boot image from attached files 22 | createitb Create new U-Boot image from *.its file 23 | extract Extract content from old U-Boot image 24 | extractitb Extract content from new U-Boot image 25 | info Show old image content 26 | infoitb Show new image content 27 | ``` 28 | 29 | ## Commands for old U-Boot images 30 | 31 | #### $ mkimg info FILE 32 | 33 | Print the U-Boot executable image content in readable format 34 | 35 | ##### Example: 36 | 37 | ```sh 38 | $ mkimg info script.bin 39 | 40 | Image Name: iMX7D NetBoot Script 41 | Created: Tue Apr 04 17:15:01 2017 42 | Image Type: ARM Linux Script (uncompressed) 43 | Data Size: 0.62 kB 44 | Load Address: 0x00000000 45 | Entry Address: 0x00000000 46 | Content: 16 Commands 47 | 0) echo '>> Run Script ...' 48 | 1) setenv autoload 'no' 49 | 2) dhcp 50 | 3) setenv serverip 192.168.1.162 51 | 4) setenv hostname 'imx7dsb' 52 | 5) setenv netdev 'eth0' 53 | 6) setenv nfsroot '/srv/nfs/imx7d' 54 | 7) setenv imgfile '/imx7d/zImage' 55 | 8) setenv fdtfile '/imx7d/imx7d-sdb.dtb' 56 | 9) setenv fdtaddr 0x83000000 57 | 10) setenv imgaddr 0x80800000 58 | 11) setenv imgload 'tftp ${imgaddr} ${imgfile}' 59 | 12) setenv fdtload 'tftp ${fdtaddr} ${fdtfile}' 60 | 13) setenv netargs 'setenv bootargs console=${console},${baudrate} root=/dev/nfs rw nfsroot=${serverip}:${nfsroot},v3,tcp ip=dhcp' 61 | 14) setenv netboot 'echo Booting from net ...; run netargs; run imgload; run fdtload; bootz ${imgaddr} - ${fdtaddr};' 62 | 15) run netboot 63 | ``` 64 | 65 |
66 | 67 | #### $ mkimg extract FILE 68 | 69 | Extract U-Boot executable image 70 | 71 | ##### Example: 72 | 73 | ```sh 74 | $ mkimg extract script.bin 75 | 76 | Image extracted into dir: script.bin.ex 77 | ``` 78 | 79 |
80 | 81 | #### $ mkimg create [OPTIONS] OUTFILE [INFILES] 82 | 83 | Create U-Boot executable image (uImage, Script, ...) 84 | 85 | ##### options: 86 | * **-a, --arch** - Architecture (default: arm) 87 | * **-o, --ostype** - Operating system (default: linux) 88 | * **-i, --imgtype** - Image type (default: firmware) 89 | * **-c, --compress** - Image compression (default: none) 90 | * **-l, --laddr** - Load address (default: 0) 91 | * **-e, --epaddr** - Entry point address (default: 0) 92 | * **-n, --name** - Image name (max: 32 chars) 93 | * **-?, --help** - Show help message and exit 94 | 95 | ##### Example: 96 | 97 | ```sh 98 | $ mkimg create -a arm -o linux -i script -c none script.bin script.txt 99 | 100 | Created Image: script.bin 101 | ``` 102 | 103 | ## Commands for new FDT U-Boot images 104 | 105 | #### $ mkimg infoitb FILE 106 | 107 | List new U-Boot image content in readable format 108 | 109 | ##### Example: 110 | 111 | ```sh 112 | $ mkimg infoitb image.itb 113 | 114 | FIT description: i.MX7D U-Boot Image 115 | Created: Fri May 11 21:51:14 2018 116 | Default config: config@1 117 | 118 | IMG[0] uboot@1 119 | size: 472.00 kB 120 | description: U-Boot (32-bit) 121 | type: standalone 122 | arch: arm 123 | compression: none 124 | load: 0x40200000 125 | 126 | IMG[1] fdt@1 127 | size: 46.00 kB 128 | description: FDT i.MX7D-SDB 129 | type: flat_dt 130 | compression: none 131 | 132 | CFG[0] config@1 133 | description: fsl-imx7d-sdb 134 | firmware: uboot@1 135 | fdt: fdt@1 136 | ``` 137 | 138 |
139 | 140 | #### $ mkimg createitb [OPTIONS] FILE 141 | 142 | Create new U-Boot image from *.its file 143 | 144 | ##### options: 145 | * **-o, --outfile** - Output path/file name 146 | * **-p, --padding** - Add padding to the blob of long (default: 0) 147 | * **-a, --align** - Make the blob align to the (default: 0) 148 | * **-s, --size** - Make the blob at least long (default: none) 149 | * **-?, --help** - Show help message and exit 150 | 151 | ##### Example: 152 | 153 | ```sh 154 | $ mkimg createitb image.its 155 | 156 | Created Image: image.itb 157 | ``` 158 | 159 |
160 | 161 | #### $ mkimg extractitb FILE 162 | 163 | Extract content from new U-Boot image (*.itb) 164 | 165 | ##### Example: 166 | 167 | ```sh 168 | $ mkimg extractitb image.itb 169 | 170 | Image extracted into dir: image.itb.ex 171 | ``` 172 | 173 |
174 | -------------------------------------------------------------------------------- /uboot/cli_mkenv.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import sys 17 | import click 18 | import uboot 19 | 20 | # Application error code 21 | ERROR_CODE = 1 22 | 23 | # The version of u-boot tools 24 | VERSION = uboot.__version__ 25 | 26 | # Short description of U-Boot mkenv tool 27 | DESCRIP = ( 28 | "The U-Boot Make Environment Blob Tool" 29 | ) 30 | 31 | 32 | # User defined class 33 | class UInt(click.ParamType): 34 | """ Custom argument type for UINT """ 35 | name = 'unsigned int' 36 | 37 | def __repr__(self): 38 | return 'UINT' 39 | 40 | def convert(self, value, param, ctx): 41 | try: 42 | if isinstance(value, (int,)): 43 | return value 44 | else: 45 | return int(value, 0) 46 | except: 47 | self.fail('%s is not a valid value' % value, param, ctx) 48 | 49 | 50 | # Create instances of custom argument types 51 | UINT = UInt() 52 | 53 | 54 | # U-Boot mkenv: Base options 55 | @click.group(context_settings=dict(help_option_names=['-?', '--help']), help=DESCRIP) 56 | @click.version_option(VERSION, '-v', '--version') 57 | def cli(): 58 | click.echo() 59 | 60 | 61 | # U-Boot mkenv: List image content 62 | @cli.command(short_help="List image content") 63 | @click.option('-b', '--bigendian', is_flag=True, help="The target is big endian (default is little endian)") 64 | @click.option('-o', '--offset', type=UINT, default=0, show_default=True, help="The offset of input file") 65 | @click.option('-s', '--size', type=UINT, default=8192, show_default=True, help="The environment blob size") 66 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 67 | def info(offset, size, bigendian, file): 68 | """ List image content """ 69 | try: 70 | with open(file, "rb") as f: 71 | f.seek(offset) 72 | data = f.read(size) 73 | 74 | env = uboot.EnvBlob.parse(data, 0, bigendian) 75 | env.size = size 76 | 77 | except Exception as e: 78 | click.echo(str(e) if str(e) else "Unknown Error !") 79 | sys.exit(ERROR_CODE) 80 | 81 | click.echo(str(env)) 82 | 83 | 84 | # U-Boot mkenv: Create new image from attached file 85 | @cli.command(short_help="Create new image from attached file") 86 | @click.argument('infile', nargs=1, type=click.Path(exists=True)) 87 | @click.argument('outfile', nargs=1, type=click.Path(readable=False)) 88 | @click.option('-b', '--bigendian', is_flag=True, help="The target is big endian (default is little endian)") 89 | @click.option('-r', '--redundant', is_flag=True, show_default=True, help="The environment has multiple copies in flash") 90 | @click.option('-s', '--size', type=UINT, default=8192, show_default=True, help="The environment blob size") 91 | def create(size, redundant, bigendian, infile, outfile): 92 | """ Create new image from attached file """ 93 | try: 94 | env = uboot.EnvBlob(size=size, redundant=redundant, bigendian=bigendian) 95 | 96 | with open(infile, 'r') as f: 97 | env.load(f.read()) 98 | 99 | with open(outfile, 'wb') as f: 100 | f.write(env.export()) 101 | 102 | except Exception as e: 103 | click.echo(str(e) if str(e) else "Unknown Error !") 104 | sys.exit(ERROR_CODE) 105 | 106 | click.secho(" Successfully created: %s" % outfile) 107 | 108 | 109 | # U-Boot mkenv: Extract image content 110 | @cli.command(short_help="Extract image content") 111 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 112 | @click.option('-b', '--bigendian', is_flag=True, help="The target is big endian (default is little endian)") 113 | @click.option('-o', '--offset', type=UINT, default=0, show_default=True, help="The offset of input file") 114 | @click.option('-s', '--size', type=UINT, default=8192, show_default=True, help="The environment blob size") 115 | def extract(offset, size, bigendian, file): 116 | """ Extract image content """ 117 | try: 118 | fileName, _ = os.path.splitext(file) 119 | 120 | with open(file, "rb") as f: 121 | f.seek(offset) 122 | data = f.read(size) 123 | 124 | env = uboot.EnvBlob.parse(data, 0, bigendian) 125 | env.size = size 126 | 127 | with open(fileName + '.txt', 'w') as f: 128 | f.write(env.store()) 129 | 130 | except Exception as e: 131 | click.echo(str(e) if str(e) else "Unknown Error !") 132 | sys.exit(ERROR_CODE) 133 | 134 | click.secho(" Successfully extracted: %s.txt" % fileName) 135 | 136 | 137 | def main(): 138 | cli(obj={}) 139 | 140 | 141 | if __name__ == '__main__': 142 | main() 143 | -------------------------------------------------------------------------------- /uboot/env_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import collections 17 | 18 | 19 | class EnvImgOld(object): 20 | 21 | @property 22 | def start_string(self): 23 | return self._env_mark 24 | 25 | @property 26 | def max_size(self): 27 | return self._env_max_size 28 | 29 | @property 30 | def size(self): 31 | size = 2 * len(self._env) 32 | for key, value in self._env: 33 | size += len(key) + len(value) 34 | return size 35 | 36 | def __init__(self, start_string): 37 | self._env_mark = start_string 38 | self._env_offset = -1 39 | self._env_max_size = 0 40 | self._env = collections.OrderedDict() 41 | self._img = bytearray() 42 | self._file = '' 43 | 44 | def __str__(self): 45 | return self.info() 46 | 47 | def __repr__(self): 48 | return self.info() 49 | 50 | def _parse(self): 51 | """ Parse environment variables from image """ 52 | self._env_offset = self._img.find(self._env_mark.encode()) 53 | if self._env_offset == -1: 54 | raise Exception("Searched string \"%s\" doesnt exist in image" % self._env_mark) 55 | 56 | eof = 0 57 | index = self._env_offset 58 | while index < len(self._img): 59 | if self._img[index]: 60 | eof = 0 61 | else: 62 | eof += 1 63 | 64 | if eof == 2: 65 | self._env_max_size = index - self._env_offset - 1 66 | break 67 | 68 | index += 1 69 | 70 | data = self._img[self._env_offset : self._env_offset + self._env_max_size].decode() 71 | for s in data.split('\0'): 72 | key, value = s.split('=', 1) 73 | self._env[key] = value 74 | 75 | def _update(self): 76 | """ Update environment variables inside image """ 77 | data = str() 78 | for key, val in self._env.items(): 79 | data += "{0:s}={1:s}\0".format(key, val) 80 | 81 | if len(data) - 1 > self._env_max_size: 82 | raise Exception("EnVar blob size is out of range: %d instead %d bytes" % (len(data), self._env_max_size)) 83 | 84 | if len(data) < self._env_max_size: 85 | data += "\0" * (self._env_max_size - len(data)) 86 | 87 | index = self._env_offset 88 | for c in data: 89 | self._img[index] = ord(c) 90 | index += 1 91 | 92 | def info(self): 93 | """ Get info message 94 | :return string 95 | """ 96 | msg = "" 97 | if self._file: 98 | msg += "Image Name: {}\n".format(self._file) 99 | msg += "Image Size: {} bytes\n".format(len(self._img)) 100 | msg += "EnVar Size: {} bytes\n".format(self._env_max_size) 101 | msg += "EnVar Offset: {} \n".format(self._env_offset) 102 | msg += "EnVar Mark: {}\n".format(self._env_mark) 103 | msg += "EnVar Content:\n" 104 | for key, val in self._env.items(): 105 | msg += "- {0:s} = {1:s}\n".format(key, val) 106 | 107 | return msg 108 | 109 | def get(self, name=None): 110 | """ Get the value of u-boot environment variable. If name is None, get list of all variables 111 | :param name: The variable name 112 | :return The variable value 113 | """ 114 | if name: 115 | assert isinstance(name, str), "Env name is not a string: %r" % name 116 | if not name in self._env: 117 | raise Exception("EnVar name %s doesnt exist !" % name) 118 | return self._env[name] 119 | else: 120 | return self._env.keys() 121 | 122 | def set(self, name, value): 123 | """ Set the u-boot environment variable. 124 | :param name: The variable name 125 | :param value: The variable value 126 | """ 127 | assert isinstance(name, str), "EnVar name is not a string: %r" % name 128 | if not isinstance(value, str): 129 | value = str(value) 130 | self._env[name] = value 131 | 132 | def clear(self): 133 | self._env.clear() 134 | 135 | def load(self, txt_data): 136 | """ Load the u-boot environment variables from readable string. 137 | :param txt_data: environment variables as string 138 | """ 139 | for line in txt_data.split('\n'): 140 | line = line.rstrip('\0') 141 | if not line: 142 | continue 143 | if line.startswith('#'): 144 | continue 145 | env_name, env_value = line.split('=', 1) 146 | self._env[env_name.strip()] = env_value.strip() 147 | 148 | def store(self): 149 | """ Store the u-boot environment variables into readable string. 150 | :return environment variables as string 151 | """ 152 | txt_data = "" 153 | 154 | for key, val in self._env.items(): 155 | txt_data += "{0:s} = {1:s}\n".format(key, val) 156 | 157 | return txt_data 158 | 159 | def import_img(self, data): 160 | """ Import the u-boot image and parse environment variables from it. 161 | :param data: Image data in bytes 162 | """ 163 | self._img = data if isinstance(data, bytearray) else bytearray(data) 164 | self._parse() 165 | 166 | def export_img(self): 167 | """ Export the u-boot image with updated environment variables. 168 | :return environment variables as string 169 | """ 170 | self._update() 171 | return self._img 172 | 173 | def open_img(self, file): 174 | """ Open the u-boot image and parse environment variables from it. 175 | :param file: Path to image file 176 | """ 177 | with open(file, 'rb') as f: 178 | self._img = bytearray(os.path.getsize(file)) 179 | f.readinto(self._img) 180 | 181 | self._parse() 182 | self._file = file 183 | 184 | def save_img(self, file): 185 | """ Save the u-boot image with updated environment variables. 186 | :param file: Path to image file 187 | """ 188 | self._update() 189 | with open(file, 'wb') as f: 190 | f.write(self._img) -------------------------------------------------------------------------------- /uboot/env_blob.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import struct 16 | import binascii 17 | import collections 18 | 19 | 20 | class EnvBlob(object): 21 | 22 | @property 23 | def name(self): 24 | return self._name 25 | 26 | @name.setter 27 | def name(self, value): 28 | assert type(value) is str, "name is not a string: %r" % value 29 | self._name = value 30 | 31 | @property 32 | def size(self): 33 | return self._size 34 | 35 | @size.setter 36 | def size(self, value): 37 | self._size = value 38 | 39 | @property 40 | def redundant(self): 41 | return self._redundant 42 | 43 | @redundant.setter 44 | def redundant(self, value): 45 | self._redundant = value 46 | 47 | @property 48 | def bigendian(self): 49 | return self._bigendian 50 | 51 | @bigendian.setter 52 | def bigendian(self, value): 53 | self._bigendian = value 54 | 55 | def __init__(self, name=None, size=8192, redundant=False, bigendian=False, empty_value=0x00): 56 | self._name = name 57 | self._size = size 58 | self._redundant = redundant 59 | self._bigendian = bigendian 60 | self._empty_value = empty_value 61 | self._env = collections.OrderedDict() 62 | 63 | def __str__(self): 64 | return self.info() 65 | 66 | def __repr__(self): 67 | return self.info() 68 | 69 | def __len__(self): 70 | return self._size 71 | 72 | def info(self): 73 | msg = str() 74 | msg += "Name: {}\n".format(self._name) 75 | msg += "Redundant: {}\n".format(self._redundant) 76 | msg += "Endian: {}\n".format("Big" if self._bigendian else "Little") 77 | msg += "Size: {} Bytes\n".format(self._size) 78 | msg += "EmptyValue: 0x{:02X}\n".format(self._empty_value) 79 | msg += "Variables:\n" 80 | for key, val in self._env.items(): 81 | msg += "- {0:s} = {1:s}\n".format(key, val) 82 | return msg 83 | 84 | def get(self, name=None): 85 | """ Get the value of u-boot environment variable. If name is None, get list of all variables 86 | :param name: The variable name 87 | :return The variable value 88 | """ 89 | if name: 90 | assert isinstance(name, str), "name is not a string: %r" % name 91 | if name not in self._env: 92 | raise Exception("ERROR: Env %s doesnt exist !" % name) 93 | return self._env[name] 94 | else: 95 | return self._env.keys() 96 | 97 | def set(self, name, value): 98 | """ Set the u-boot environment variable. 99 | :param name: The variable name 100 | :param value: The variable value 101 | """ 102 | assert isinstance(name, str), "Error " 103 | if not isinstance(value, str): 104 | value = str(value) 105 | self._env[name] = value 106 | 107 | def clear(self): 108 | self._env.clear() 109 | 110 | def load(self, txt_data): 111 | """ Load variables from text file 112 | :param txt_data: 113 | """ 114 | for line in txt_data.split('\n'): 115 | line = line.rstrip('\0') 116 | if not line: continue 117 | 118 | if line.startswith('#'): 119 | pass # TODO: Parse init values 120 | else: 121 | name, value = line.split('=', 1) 122 | self._env[name.strip()] = value.strip() 123 | 124 | def store(self, txt_data=None): 125 | """ Store variables into text file 126 | :param txt_data: 127 | :return: txt_data 128 | """ 129 | if txt_data is None: 130 | txt_data = "" 131 | 132 | txt_data += "# Name: {0:s}\n".format(self.name if self.name else "") 133 | txt_data += "# Size: {0:d}\n".format(self.size) 134 | txt_data += "# Redundant: {0:s}\n".format("Yes" if self.redundant else "No") 135 | txt_data += "\n" 136 | 137 | for key, val in self._env.items(): 138 | txt_data += "{0:s}={1:s}\n".format(key, val) 139 | 140 | return txt_data 141 | 142 | @classmethod 143 | def parse(cls, data, offset=0, bigendian=False): 144 | """ Parse the u-boot environment variables from bytearray. 145 | :param data: The data in bytes array 146 | :param offset: The offset of input data 147 | :param bigendian: The endian type 148 | """ 149 | env = cls(bigendian=bigendian) 150 | 151 | fmt = ">IB" if env.bigendian else " env_size: 194 | raise Exception("ERROR: ENV size out of range, extend required size !") 195 | env_blob = data.encode('utf-8') 196 | env_blob += self._empty_value.to_bytes(1,'little') * (env_size - len(data)) 197 | crc = binascii.crc32(env_blob) & 0xffffffff 198 | 199 | fmt = ">I" if self._bigendian else " Shell Scripts). 52 | 53 | # The following are exposed to uImage header. 54 | # Do not change values for backward compatibility. 55 | 56 | 57 | class EnumImageType(Enum): 58 | """ Supported UBoot Image Types """ 59 | 60 | STANDALONE = (1, "standalone", "Standalone Program") 61 | KERNEL = (2, "kernel", "Kernel Image") 62 | RAMDISK = (3, "ramdisk", "RAMDisk Image") 63 | MULTI = (4, "multi", "Multi-File Image") 64 | FIRMWARE = (5, "firmware", "Firmware Image") 65 | SCRIPT = (6, "script", "U-Boot Script Image") 66 | FILESYSTEM = (7, "filesystem", "Filesystem Image") 67 | FLATDT = (8, "flat_dt", "Flat Device Tree Image") 68 | KWB = (9, "kwbimage", "Kirkwood Boot Image") 69 | IMX = (10, "imximage", "Freescale i.MX Boot Image") 70 | UBL = (11, "ublimage", "Davinci UBL image") 71 | OMAP = (12, "omapimage", "TI OMAP SPL With GP CH") 72 | AIS = (13, "aisimage", "Davinci AIS image") 73 | 74 | KNOLOAD = (14, "kernel_noload", "Kernel Image (no loading done)") 75 | PBL = (15, "pblimage", "Freescale PBL Boot Image") 76 | MXS = (16, "mxsimage", "Freescale MXS Boot Image") 77 | GP = (17, "gpimage", "TI Keystone SPL Image") 78 | ATMEL = (18, "atmelimage", "ATMEL ROM-Boot Image") 79 | SOCFPGA = (19, "socfpgaimage", "Altera SOCFPGA preloader") 80 | X86SETUP = (20, "x86_setup", "x86 setup.bin") 81 | LPC32XX = (21, "lpc32xximage", "LPC32XX Boot Image") 82 | LOADABLE = (22, "loadable", "A list of typeless images") 83 | RKBOOT = (23, "rkimage", "Rockchip Boot Image") 84 | RKSD = (24, "rksd", "Rockchip SD Boot Image") 85 | RKSPI = (25, "rkspi", "Rockchip SPI Boot Image") 86 | ZYNQ = (26, "zynqimage", "Xilinx Zynq Boot Image") 87 | ZYNQMP = (27, "zynqmpimage", "Xilinx ZynqMP Boot Image") 88 | FPGA = (28, "fpga", "FPGA Image") 89 | VYBRID = (29, "vybridimage", "Vybrid Boot Image") 90 | TEE = (30, "tee", "Trusted Execution Environment Image") 91 | FW_IVT = (31, "firmware_ivt", "Firmware with HABv4 IVT") 92 | PMMC = (32, "pmmc", "TI Power Management Micro-Controller Firmware") 93 | 94 | 95 | # ---------------------------------------------------------------------------------------------------------------------- 96 | # Operating System Codes 97 | # ---------------------------------------------------------------------------------------------------------------------- 98 | 99 | # The following are exposed to uImage header. 100 | # Do not change values for backward compatibility. 101 | 102 | class EnumOsType(Enum): 103 | """ Supported Operating Systems """ 104 | 105 | OPENBSD = (1, "openbsd", "OpenBSD") 106 | NETBSD = (2, "netbsd", "NetBSD") 107 | FREEBSD = (3, "freebsd", "FreeBSD") 108 | BSD4 = (4, "4-4bsd", "4-4BSD") 109 | LINUX = (5, "linux", "Linux") 110 | SVR4 = (6, "svr4", "SVR4") 111 | ESIX = (7, "esix", "Esix") 112 | SOLARIS = (8, "solaris", "Solaris") 113 | IRIX = (9, "irix", "Irix") 114 | SCO = (10, "sco", "SCO") 115 | DELL = (11, "dell", "Dell") 116 | NCR = (12, "ncr", "NCR") 117 | LYNXOS = (13, "lynxos", "LynxOS") 118 | VXWORKS = (14, "vxworks", "VxWorks") 119 | PSOS = (15, "psos", "pSOS") 120 | QNX = (16, "qnx", "QNX") 121 | UBOOT = (17, "u-boot", "U-Boot Firmware") 122 | RTEMS = (18, "rtems", "RTEMS") 123 | ARTOS = (19, "artos", "ARTOS") 124 | UNITY = (20, "unity", "Unity OS") 125 | INTEGRITY = (21, "integrity", "INTEGRITY") 126 | OSE = (22, "ose", "Enea OSE") 127 | PLAN9 = (23, "plan9", "Plan 9") 128 | OPENRTOS = (24, "openrtos", "OpenRTOS") 129 | 130 | 131 | # ---------------------------------------------------------------------------------------------------------------------- 132 | # CPU Architecture Codes (supported by Linux) 133 | # ---------------------------------------------------------------------------------------------------------------------- 134 | 135 | # The following are exposed to uImage header. 136 | # Do not change values for backward compatibility. 137 | 138 | class EnumArchType(Enum): 139 | """ Supported CPU Architectures """ 140 | 141 | ALPHA = (1, "alpha", "Alpha") 142 | ARM = (2, "arm", "ARM") 143 | I386 = (3, "x86", "Intel x86") 144 | IA64 = (4, "ia64", "IA64") 145 | MIPS = (5, "mips", "MIPS") 146 | MIPS64 = (6, "mips64", "MIPS 64-Bit") 147 | PPC = (7, "powerpc", "PowerPC") 148 | S390 = (8, "s390", "IBM S390") 149 | SH = (9, "sh", "SuperH") 150 | SPARC = (10, "sparc", "SPARC") 151 | SPARC64 = (11, "sparc64", "SPARC 64 Bit") 152 | M68K = (12, "m68k", "M68K") 153 | NIOS = (13, "nios", "Nios 32") 154 | MICROBLAZE = (14, "microblaze", "MicroBlaze") 155 | NIOS2 = (15, "nios2", "NIOS II") 156 | BLACKFIN = (16, "blackfin", "Blackfin") 157 | AVR32 = (17, "avr32", "AVR32") 158 | ST200 = (18, "ST200", "STMicroelectronics ST200") 159 | SANDBOX = (19, "sandbox", "Sandbox architecture (test only)") 160 | NDS32 = (20, "nds32", "ANDES Technology - NDS32") 161 | OPENRISC = (21, "or1k", "OpenRISC 1000") 162 | ARM64 = (22, "arm64", "AArch64") 163 | ARC = (23, "arc", "Synopsis DesignWare ARC") 164 | X86_64 = (24, "x86_64", "AMD x86_64, Intel and Via") 165 | XTENSA = (25, "xtensa", "Xtensa") 166 | 167 | 168 | # ---------------------------------------------------------------------------------------------------------------------- 169 | # Compression Types 170 | # ---------------------------------------------------------------------------------------------------------------------- 171 | 172 | # The following are exposed to uImage header. 173 | # Do not change values for backward compatibility. 174 | 175 | class EnumCompressionType(Enum): 176 | """ Supported Data Compression """ 177 | 178 | NONE = (0, 'none', 'Uncompressed') 179 | GZIP = (1, 'gzip', 'Compressed with GZIP') 180 | BZIP2 = (2, 'bzip2', 'Compressed with BZIP2') 181 | LZMA = (3, 'lzma', 'Compressed with LZMA') 182 | LZO = (4, 'lzo', 'Compressed with LZO') 183 | LZ4 = (5, 'lz4', 'Compressed with LZ4') 184 | -------------------------------------------------------------------------------- /uboot/cli_mkimg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import sys 17 | import click 18 | import uboot 19 | 20 | # Application error code 21 | ERROR_CODE = 1 22 | 23 | # The version of u-boot tools 24 | VERSION = uboot.__version__ 25 | 26 | # Short description of U-Boot mkimg tool 27 | DESCRIP = ( 28 | "The U-Boot Image Tool" 29 | ) 30 | 31 | 32 | # User defined class 33 | class UInt(click.ParamType): 34 | """ Custom argument type for UINT """ 35 | name = 'unsigned int' 36 | 37 | def __repr__(self): 38 | return 'UINT' 39 | 40 | def convert(self, value, param, ctx): 41 | try: 42 | if isinstance(value, (int,)): 43 | return value 44 | else: 45 | return int(value, 0) 46 | except: 47 | self.fail('{} is not a valid value'.format(value), param, ctx) 48 | 49 | 50 | # Create instances of custom argument types 51 | UINT = UInt() 52 | 53 | # -- 54 | ARCT = [item[0] for item in uboot.EnumArchType] 55 | OST = [item[0] for item in uboot.EnumOsType] 56 | IMGT = [item[0] for item in uboot.EnumImageType] 57 | COMT = [item[0] for item in uboot.EnumCompressionType] 58 | 59 | 60 | # U-Boot mkimg: Base options 61 | @click.group(context_settings=dict(help_option_names=['-?', '--help']), help=DESCRIP) 62 | @click.version_option(VERSION, '-v', '--version') 63 | def cli(): 64 | click.echo() 65 | 66 | 67 | @cli.command(short_help="Show old image content") 68 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 69 | def info(file): 70 | """ List old image content in readable format """ 71 | try: 72 | with open(file, 'rb') as f: 73 | img = uboot.parse_img(f.read()) 74 | click.echo(img.info()) 75 | 76 | except Exception as e: 77 | click.echo(str(e) if str(e) else "Unknown Error !") 78 | sys.exit(ERROR_CODE) 79 | 80 | 81 | @cli.command(short_help="Show new image content") 82 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 83 | def infoitb(file): 84 | """ List new image content in readable format """ 85 | try: 86 | with open(file, 'rb') as f: 87 | img = uboot.parse_itb(f.read()) 88 | click.echo(img.info()) 89 | 90 | except Exception as e: 91 | click.echo(str(e) if str(e) else "Unknown Error !") 92 | sys.exit(ERROR_CODE) 93 | 94 | 95 | @cli.command(short_help="Create old U-Boot image from attached files") 96 | @click.option('-a', '--arch', type=click.Choice(ARCT), default='arm', show_default=True, help='Architecture') 97 | @click.option('-o', '--ostype', type=click.Choice(OST), default='linux', show_default=True, help='Operating system') 98 | @click.option('-i', '--imgtype', type=click.Choice(IMGT), default='firmware', show_default=True, help='Image type') 99 | @click.option('-c', '--compress', type=click.Choice(COMT), default='none', show_default=True, help='Image compression') 100 | @click.option('-l', '--laddr', type=UINT, default=0, show_default=True, help="Load address") 101 | @click.option('-e', '--epaddr', type=UINT, default=0, show_default=True, help="Entry point address") 102 | @click.option('-n', '--name', type=click.STRING, default="", help="Image name (max: 32 chars)") 103 | @click.argument('outfile', nargs=1, type=click.Path(readable=False)) 104 | @click.argument('infiles', nargs=-1, type=click.Path(exists=True)) 105 | def create(arch, ostype, imgtype, compress, laddr, epaddr, name, outfile, infiles): 106 | """ Create old U-Boot image from attached files """ 107 | try: 108 | img_type = uboot.EnumImageType[imgtype] 109 | 110 | if img_type == uboot.EnumImageType.MULTI: 111 | img = uboot.MultiImage 112 | for file in infiles: 113 | with open(file, 'rb') as f: 114 | simg = uboot.parse_img(f.read()) 115 | img.append(simg) 116 | 117 | elif img_type == uboot.EnumImageType.MULTI: 118 | img = uboot.ScriptImage() 119 | with open(infiles[0], 'r') as f: 120 | img.load(f.read()) 121 | 122 | else: 123 | img = uboot.StdImage(image=img_type) 124 | with open(infiles[0], 'rb') as f: 125 | img.data = bytearray(f.read()) 126 | 127 | img.header.arch_type = uboot.EnumArchType[arch] 128 | img.header.os_type = uboot.EnumOsType[ostype] 129 | img.header.compression = uboot.EnumCompressionType[compress] 130 | img.header.load_address = laddr 131 | img.header.entry_point = epaddr 132 | img.header.name = name 133 | 134 | click.echo(img.info()) 135 | with open(outfile, 'wb') as f: 136 | f.write(img.export()) 137 | 138 | except Exception as e: 139 | click.echo(str(e) if str(e) else "Unknown Error !") 140 | sys.exit(ERROR_CODE) 141 | 142 | click.secho("\n Created Image: %s" % outfile) 143 | 144 | 145 | @cli.command(short_help="Create new U-Boot image from *.its file") 146 | @click.option('-o', '--outfile', type=click.Path(readable=False), default=None, help="Output file") 147 | @click.option('-p', '--padding', type=UINT, default=0, help="Add padding to the blob of long") 148 | @click.option('-a', '--align', type=UINT, default=None, help="Make the blob align to the ") 149 | @click.option('-s', '--size', type=UINT, default=None, help="Make the blob at least long") 150 | @click.argument('itsfile', nargs=1, type=click.Path(exists=True)) 151 | def createitb(outfile, padding, align, size, itsfile): 152 | """ Create new U-Boot image from *.its file """ 153 | 154 | try: 155 | if outfile is None: 156 | outfile = os.path.splitext(itsfile)[0] + ".itb" 157 | 158 | with open(itsfile, 'r') as f: 159 | img = uboot.parse_its(f.read(), os.path.dirname(itsfile)) 160 | 161 | with open(outfile, 'wb') as f: 162 | f.write(img.to_itb(padding, align, size)) 163 | 164 | except Exception as e: 165 | click.echo(str(e) if str(e) else "Unknown Error !") 166 | sys.exit(ERROR_CODE) 167 | 168 | click.secho("\n Created Image: %s" % outfile) 169 | 170 | 171 | @cli.command(short_help="Extract content from old U-Boot image") 172 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 173 | def extract(file): 174 | """ Extract content from old U-Boot image """ 175 | 176 | def get_file_ext(img): 177 | ext = ('bin', 'gz', 'bz2', 'lzma', 'lzo', 'lz4') 178 | return ext[img.compression] 179 | 180 | try: 181 | with open(file, 'rb') as f: 182 | raw_data = f.read() 183 | 184 | img = uboot.parse_img(raw_data) 185 | 186 | file_path, file_name = os.path.split(file) 187 | dest_dir = os.path.normpath(os.path.join(file_path, file_name + ".ex")) 188 | os.makedirs(dest_dir, exist_ok=True) 189 | 190 | if img.header.image_type == uboot.EnumImageType.MULTI: 191 | n = 0 192 | for simg in img: 193 | with open(os.path.join(dest_dir, 'image_{0:02d}.bin'.format(n)), 'wb') as f: 194 | f.write(simg.export()) 195 | n += 1 196 | elif img.header.image_type == uboot.EnumImageType.SCRIPT: 197 | with open(os.path.join(dest_dir, 'script.txt'), 'w') as f: 198 | f.write(img.store()) 199 | 200 | else: 201 | with open(os.path.join(dest_dir, 'image.' + get_file_ext(img)), 'wb') as f: 202 | f.write(img.data) 203 | 204 | with open(os.path.join(dest_dir, 'info.txt'), 'w') as f: 205 | f.write(img.info()) 206 | 207 | except Exception as e: 208 | click.echo(str(e) if str(e) else "Unknown Error !") 209 | sys.exit(ERROR_CODE) 210 | 211 | click.secho("\n Image extracted into dir: %s" % dest_dir) 212 | 213 | 214 | @cli.command(short_help="Extract content from new U-Boot image") 215 | @click.argument('file', nargs=1, type=click.Path(exists=True)) 216 | def extractitb(file): 217 | """ Extract content from new U-Boot image """ 218 | 219 | try: 220 | with open(file, 'rb') as f: 221 | img = uboot.parse_itb(f.read()) 222 | 223 | file_path, file_name = os.path.split(file) 224 | dest_dir = os.path.normpath(os.path.join(file_path, file_name + ".ex")) 225 | os.makedirs(dest_dir, exist_ok=True) 226 | 227 | its, images = img.to_its() 228 | 229 | with open(os.path.join(dest_dir, file_name.split('.')[0] + '.its'), 'w') as f: 230 | f.write(its) 231 | 232 | for name, data in images.items(): 233 | with open(os.path.join(dest_dir, name), 'wb') as f: 234 | f.write(data) 235 | 236 | except Exception as e: 237 | click.echo(str(e) if str(e) else "Unknown Error !") 238 | sys.exit(ERROR_CODE) 239 | 240 | click.secho("\n Image extracted into dir: %s" % dest_dir) 241 | 242 | 243 | def main(): 244 | cli(obj={}) 245 | 246 | 247 | if __name__ == '__main__': 248 | main() 249 | -------------------------------------------------------------------------------- /uboot/fdt_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import fdt 17 | import time 18 | import struct 19 | 20 | from .common import EnumOsType, EnumArchType, EnumImageType, EnumCompressionType 21 | 22 | 23 | # ---------------------------------------------------------------------------------------------------------------------- 24 | # Helper methods 25 | # ---------------------------------------------------------------------------------------------------------------------- 26 | def get_value(obj, name, default=None): 27 | prop = obj.get_property(name) 28 | return default if prop is None else prop[0] 29 | 30 | 31 | def get_data(obj): 32 | prop = obj.get_property("data") 33 | if isinstance(prop, fdt.PropBytes): 34 | return prop.data 35 | if isinstance(prop, fdt.PropWords): 36 | data = bytearray() 37 | for val in prop.data: 38 | data += struct.pack(">I", val) 39 | return data 40 | 41 | raise Exception("Image data error") 42 | 43 | 44 | # ---------------------------------------------------------------------------------------------------------------------- 45 | # FDT Image Class 46 | # ---------------------------------------------------------------------------------------------------------------------- 47 | class FdtImage(object): 48 | 49 | def __init__(self): 50 | self.description = "" 51 | self.time_stamp = int(time.time()) 52 | self.def_config = None 53 | self.configs = [] 54 | self.img_info = [] 55 | self.img_data = {} 56 | 57 | def __str__(self): 58 | return self.info() 59 | 60 | def __repr__(self): 61 | return self.info() 62 | 63 | def info(self): 64 | msg = "FIT description: {}\n".format(self.description) 65 | msg += "Created: {}\n".format(time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(self.time_stamp))) 66 | msg += "Default config: {}\n".format(self.def_config) 67 | for n, img in enumerate(self.img_info): 68 | msg += "\n" 69 | msg += " IMG[{}] {}\n".format(n, img.name) 70 | msg += " size: {0:.02f} kB\n".format(len(self.img_data[img.name]) / 1024) 71 | for p in img.props: 72 | if isinstance(p, fdt.PropWords): 73 | msg += " {}: 0x{:X}\n".format(p.name, p[0]) 74 | else: 75 | msg += " {}: {}\n".format(p.name, p[0]) 76 | for n, cfg in enumerate(self.configs): 77 | msg += "\n" 78 | msg += " CFG[{}] {}\n".format(n, cfg.name) 79 | for p in cfg.props: 80 | msg += " {}: {}\n".format(p.name, p[0]) 81 | return msg 82 | 83 | def add_img(self, nfo, data): 84 | """ 85 | :param nfo: 86 | :param data: 87 | :return: 88 | """ 89 | assert isinstance(nfo, fdt.Node), "nfo type must be a fdt.Node" 90 | assert isinstance(data, (bytes, bytearray)), "data type must be a bytes or bytearray" 91 | 92 | if not nfo.exist_property("type"): 93 | raise Exception("Image type must be defined") 94 | 95 | for p in nfo.props: 96 | name, value = p.name, p[0] 97 | if name == "type" and value != "flat_dt" and value not in EnumImageType: 98 | raise Exception("Unknown IMAGE type") 99 | elif name == "os" and value not in EnumOsType: 100 | raise Exception("Unknown OS type") 101 | elif name == "arch" and value not in EnumArchType: 102 | raise Exception("Unknown ARCH type") 103 | elif name == "compression" and value not in EnumCompressionType: 104 | raise Exception("Unknown Compression type") 105 | 106 | self.img_info.append(nfo) 107 | self.img_data[nfo.name] = data 108 | 109 | def add_cfg(self, item, validate=False): 110 | """ 111 | :param item: 112 | :param validate: 113 | :return: 114 | """ 115 | assert isinstance(item, fdt.Node), "validate_cfg: item type must be a fdt.Node" 116 | for cfg in item.props: 117 | if cfg.name == "description": 118 | continue 119 | if validate and cfg[0] not in self.img_data: 120 | raise Exception("add_cfg: Config Validation Error") 121 | self.configs.append(item) 122 | 123 | def to_its(self, rpath=None, tabsize=4): 124 | """ Export to ITS format 125 | 126 | :param rpath: 127 | :param tabsize: 128 | :return: 129 | """ 130 | data = {} 131 | images = fdt.Node("images") 132 | for img in self.img_info: 133 | img_name = img.name.replace('@', '_') + '.bin' 134 | img_clone = img.copy() 135 | img_clone.append(fdt.PropIncBin("data", None, img_name, rpath)) 136 | images.append(img_clone) 137 | data[img_name] = self.img_data[img.name] 138 | 139 | 140 | # Add images and configs 141 | root_node = fdt.Node('/') 142 | root_node.append(fdt.PropStrings("description", self.description)) 143 | root_node.append(images) 144 | if len(self.configs) != 0: 145 | configs = fdt.Node("configurations", nodes=self.configs) 146 | if self.def_config is not None and self.def_config in [cnf.name for cnf in self.configs]: 147 | configs.append(fdt.PropStrings("default", self.def_config)) 148 | root_node.append(configs) 149 | 150 | # Crete ITS 151 | its = "/dts-v1/;\n" 152 | its += '\n' 153 | its += root_node.to_dts(tabsize) 154 | return its, data 155 | 156 | def to_itb(self, padding=0, align=None, size=None): 157 | """ Export to ITB format 158 | 159 | :param padding: 160 | :param align: 161 | :param size: 162 | :return: 163 | """ 164 | img_blob = bytes() 165 | img_offset = padding 166 | 167 | fdt_obj = fdt.FDT() 168 | fdt_obj.add_item(fdt.PropWords("timestamp", int(time.time()) if self.time_stamp is None else self.time_stamp)) 169 | fdt_obj.add_item(fdt.PropStrings("description", self.description)) 170 | 171 | # Add images 172 | node = fdt.Node("images") 173 | for image in self.img_info: 174 | if image.name not in self.img_data: 175 | raise Exception("export: data is None") 176 | cimg = image.copy() 177 | data = self.img_data[image.name] 178 | if padding: 179 | img_blob += data 180 | img_offset += len(data) 181 | cimg.append(fdt.PropWords("data-size", len(data))) 182 | cimg.append(fdt.PropWords("data-position", img_offset)) 183 | else: 184 | cimg.append(fdt.PropBytes("data", data=data)) 185 | node.append(cimg) 186 | fdt_obj.add_item(node) 187 | 188 | # Add configs 189 | if len(self.configs) != 0: 190 | node = fdt.Node("configurations") 191 | if self.def_config is not None and self.def_config in [cnf.name for cnf in self.configs]: 192 | node.append(fdt.PropStrings("default", self.def_config)) 193 | for cfg in self.configs: 194 | node.append(cfg) 195 | fdt_obj.add_item(node) 196 | 197 | # Generate FDT blob 198 | itb = fdt_obj.to_dtb(17) 199 | 200 | # ... 201 | if padding: 202 | itb_align = padding - len(itb) 203 | if itb_align < 0: 204 | raise Exception() 205 | if itb_align > 0: 206 | itb += bytes([0] * itb_align) 207 | itb += img_blob 208 | 209 | return itb 210 | 211 | 212 | def parse_its(text, root_dir=''): 213 | """ Parse ITS file 214 | 215 | :param text: 216 | :param root_dir: 217 | :return: 218 | """ 219 | its_obj = fdt.parse_dts(text, root_dir) 220 | # ... 221 | fim_obj = FdtImage() 222 | prop = its_obj.get_property("description") 223 | if prop is not None: 224 | fim_obj.description = prop[0] 225 | 226 | # Parse images 227 | node = its_obj.get_node("images") 228 | if node is None: 229 | raise Exception("parse_its: images not defined") 230 | for img in node.nodes: 231 | img_data = get_data(img) 232 | img.remove_property("data") 233 | fim_obj.add_img(img, img_data) 234 | 235 | # Parse configs if exists 236 | try: 237 | node = its_obj.get_node("configurations") 238 | for cfg in node.nodes: 239 | fim_obj.add_cfg(cfg, True) 240 | fim_obj.def_config = get_value(node, "default") 241 | except ValueError: 242 | pass 243 | 244 | return fim_obj 245 | 246 | 247 | def parse_itb(data, offset=0): 248 | """ Parse ITB data-blob 249 | 250 | :param data: 251 | :param offset: 252 | :return: 253 | """ 254 | fdt_obj = fdt.parse_dtb(data, offset) 255 | # ... 256 | fim_obj = FdtImage() 257 | fim_obj.time_stamp = get_value(fdt_obj, "timestamp") 258 | fim_obj.description = get_value(fdt_obj, "description", "") 259 | 260 | # Parse images 261 | node = fdt_obj.get_node("images") 262 | if node is None or not node.nodes: 263 | raise Exception("parse_itb: images not defined") 264 | for img in node.nodes: 265 | if img.exist_property("data"): 266 | img_data = get_data(img) 267 | img.remove_property("data") 268 | elif img.exist_property("data-size") and img.exist_property("data-position"): 269 | data_size = get_value(img, "data-size") 270 | data_offset = get_value(img, "data-position") 271 | img_data = data[offset + data_offset: offset + data_offset + data_size] 272 | img.remove_property("data-size") 273 | img.remove_property("data-position") 274 | else: 275 | raise Exception() 276 | fim_obj.add_img(img, img_data) 277 | 278 | # Parse configs if exists 279 | try: 280 | node = fdt_obj.get_node("configurations") 281 | for cfg in node.nodes: 282 | fim_obj.add_cfg(cfg, True) 283 | fim_obj.def_config = get_value(node, "default") 284 | except ValueError: 285 | pass 286 | 287 | return fim_obj 288 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /uboot/old_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Martin Olejar 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import time 16 | import binascii 17 | from struct import pack, unpack_from, calcsize 18 | 19 | from .common import EnumArchType, EnumOsType, EnumImageType, EnumCompressionType 20 | 21 | 22 | # ---------------------------------------------------------------------------------------------------------------------- 23 | # Helper methods 24 | # ---------------------------------------------------------------------------------------------------------------------- 25 | def CRC32(data): 26 | """ Help function for 32bit CRC calculation 27 | :param data: Tha data blob as byte array 28 | :return: CRC Value 29 | """ 30 | return binascii.crc32(data) & 0xFFFFFFFF 31 | # ---------------------------------------------------------------------------------------------------------------------- 32 | 33 | 34 | # ---------------------------------------------------------------------------------------------------------------------- 35 | # UBoot Image Header Class 36 | # ---------------------------------------------------------------------------------------------------------------------- 37 | class Header(object): 38 | MAGIC_NUMBER = 0x27051956 39 | FORMAT = '!7L4B32s' # (Big-endian, 7 ULONGS, 4 UCHARs, 32-byte string) 40 | SIZE = calcsize(FORMAT) # Should be 64-bytes 41 | 42 | @property 43 | def header_crc(self): 44 | return CRC32(pack(self.FORMAT, 45 | self.magic_number, 46 | 0, 47 | self.time_stamp, 48 | self.data_size, 49 | self.load_address, 50 | self.entry_address, 51 | self.data_crc, 52 | self.os_type, 53 | self.arch_type, 54 | self.image_type, 55 | self.compression, 56 | self.name.encode('utf-8'))) 57 | 58 | @property 59 | def os_type(self): 60 | return self._os_type 61 | 62 | @os_type.setter 63 | def os_type(self, value): 64 | assert value in EnumOsType, "HEADER: Unknown Value of OS Type: %d" % value 65 | self._os_type = value 66 | 67 | @property 68 | def arch_type(self): 69 | return self._arch_type 70 | 71 | @arch_type.setter 72 | def arch_type(self, value): 73 | assert value in EnumArchType, "HEADER: Unknown Value of Arch Type: %d" % value 74 | self._arch_type = value 75 | 76 | @property 77 | def image_type(self): 78 | return self._image_type 79 | 80 | @image_type.setter 81 | def image_type(self, value): 82 | assert value in EnumImageType, "HEADER: Unknown Value of Image Type: %d" % value 83 | self._image_type = value 84 | 85 | @property 86 | def compression(self): 87 | return self._compression 88 | 89 | @compression.setter 90 | def compression(self, value): 91 | assert value in EnumCompressionType, "HEADER: Unknown Value of Compression Type: %d" % value 92 | self._compression = value 93 | 94 | @property 95 | def name(self): 96 | return self._name 97 | 98 | @name.setter 99 | def name(self, value): 100 | assert isinstance(value, str), "HEADER: Name must be a string" 101 | assert len(value) <= 32, "HEADER: Name to long: %d char instead 32" % len(value) 102 | self._name = value 103 | 104 | @property 105 | def size(self): 106 | return self.SIZE 107 | 108 | def __init__(self, **kwargs): 109 | """ U-Boot Image Header Constructor 110 | :param laddr: Load address 111 | :param eaddr: Entry point address 112 | :param arch: Architecture (ARCHType Enum) 113 | :param os: Operating system (OSType Enum) 114 | :param image: Image type (IMGType Enum) 115 | :param compress: Image compression (COMPRESSType Enum) 116 | :param name: Image name (max: 32 chars) 117 | """ 118 | self.magic_number = self.MAGIC_NUMBER # U-Boot Default Value is 0x27051956 119 | self.time_stamp = int(time.time()) 120 | self.data_size = 0 121 | self.data_crc = 0 122 | self.load_address = 0 123 | self.entry_address = 0 124 | self._os_type = EnumOsType.LINUX 125 | self._arch_type = EnumArchType.ARM 126 | self._image_type = EnumImageType.STANDALONE 127 | self._compression = EnumCompressionType.NONE 128 | self._name = '' 129 | 130 | if 'laddr' in kwargs: 131 | try: 132 | self.load_address = int(kwargs['laddr'], 0) if isinstance(kwargs['laddr'], str) else int(kwargs['laddr']) 133 | except ValueError: 134 | raise Exception("The value of \"laddr\" is not correct !") 135 | if 'eaddr' in kwargs: 136 | try: 137 | self.entry_address = int(kwargs['eaddr'], 0) if isinstance(kwargs['eaddr'], str) else int(kwargs['eaddr']) 138 | except ValueError: 139 | raise Exception("The value of \"eaddr\" is not correct !") 140 | if 'os' in kwargs and kwargs['os'] is not None: 141 | val = kwargs['os'] 142 | self.os_type = val if isinstance(val, int) else EnumOsType[val] 143 | if 'arch' in kwargs and kwargs['arch'] is not None: 144 | val = kwargs['arch'] 145 | self.arch_type = val if isinstance(val, int) else EnumArchType[val] 146 | if 'image' in kwargs and kwargs['image'] is not None: 147 | val = kwargs['image'] 148 | self.image_type = val if isinstance(val, int) else EnumImageType[val] 149 | if 'compress' in kwargs and kwargs['compress'] is not None: 150 | val = kwargs['compress'] 151 | self.compression = val if isinstance(val, int) else EnumCompressionType[val] 152 | if 'name' in kwargs and kwargs['name'] is not None: 153 | self.name = kwargs['name'] 154 | 155 | def __len__(self): 156 | return self.size 157 | 158 | def __str__(self): 159 | return self.info() 160 | 161 | def __repr__(self): 162 | return self.info() 163 | 164 | def __ne__(self, obj): 165 | return not self.__eq__(obj) 166 | 167 | def __eq__(self, obj): 168 | if not isinstance(obj, Header): 169 | return False 170 | if self.header_crc != obj.header_crc: 171 | return False 172 | return True 173 | 174 | def info(self): 175 | msg = "Image Name: {0:s}\n".format(self.name) 176 | msg += "Created: {0:s}\n".format(time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(self.time_stamp))) 177 | msg += "Image Type: {0:s} {1:s} {2:s} ({3:s})\n".format(EnumArchType[self.arch_type], 178 | EnumOsType[self.os_type], 179 | EnumImageType[self.image_type], 180 | EnumCompressionType[self.compression]) 181 | msg += "Data Size: {0:.02f} kB\n".format(self.data_size / 1024) 182 | msg += "Load Address: 0x{0:08X}\n".format(self.load_address) 183 | msg += "Entry Address: 0x{0:08X}\n".format(self.entry_address) 184 | return msg 185 | 186 | def export(self): 187 | return pack(self.FORMAT, 188 | self.magic_number, 189 | self.header_crc, 190 | self.time_stamp, 191 | self.data_size, 192 | self.load_address, 193 | self.entry_address, 194 | self.data_crc, 195 | self.os_type, 196 | self.arch_type, 197 | self.image_type, 198 | self.compression, 199 | self.name.encode('utf-8')) 200 | 201 | @classmethod 202 | def parse(cls, data, offset=0, ignore_crc=False): 203 | if len(data) < cls.SIZE: 204 | raise Exception("Header: Too small size of input data !") 205 | 206 | val = unpack_from(cls.FORMAT, data, offset) 207 | header = cls() 208 | 209 | header.magic_number = val[0] 210 | header_crc = val[1] 211 | header.time_stamp = val[2] 212 | header.data_size = val[3] 213 | header.load_address = val[4] 214 | header.entry_address = val[5] 215 | header.data_crc = val[6] 216 | header.os_type = val[7] 217 | header.arch_type = val[8] 218 | header.image_type = val[9] 219 | header.compression = val[10] 220 | header.name = val[11].decode('utf-8', errors='ignore').strip('\0') 221 | 222 | if header.magic_number != header.MAGIC_NUMBER: 223 | raise Exception("Header: Magic number not valid !") 224 | 225 | if not ignore_crc: 226 | if header_crc != header.header_crc: 227 | raise Exception(f"Header: Uncorrect CRC of input data ! {hex(header_crc)} != {hex(header.header_crc)}") 228 | 229 | return header 230 | # ---------------------------------------------------------------------------------------------------------------------- 231 | 232 | 233 | # ---------------------------------------------------------------------------------------------------------------------- 234 | # UBoot Image Classes 235 | # ---------------------------------------------------------------------------------------------------------------------- 236 | class BaseImage(object): 237 | def __init__(self, **kwargs): 238 | self.header = Header(**kwargs) 239 | 240 | def __str__(self): 241 | return self.info() 242 | 243 | def __repr__(self): 244 | return self.info() 245 | 246 | def __ne__(self, obj): 247 | return not self.__eq__(obj) 248 | 249 | def info(self): 250 | self.export() 251 | return self.header.info() 252 | 253 | def parse(self, data, offset=0): 254 | raise NotImplementedError() 255 | 256 | def export(self): 257 | raise NotImplementedError() 258 | 259 | 260 | class StdImage(BaseImage): 261 | def __init__(self, data=None, **kwargs): 262 | """ Image Constructor 263 | :param data: Image content as byte array 264 | :param laddr: Load address 265 | :param eaddr: Entry point address 266 | :param arch: Architecture (ARCHType Enum) 267 | :param os: Operating system (OSType Enum) 268 | :param image: Image type (IMGType Enum) 269 | :param compress: Image compression (COMPRESSType Enum) 270 | :param name: Image name (max: 32 chars) 271 | """ 272 | super().__init__(**kwargs) 273 | self.data = data if data else bytearray() 274 | 275 | def __eq__(self, obj): 276 | if not isinstance(obj, StdImage): 277 | return False 278 | if self.__len__() != len(obj): 279 | return False 280 | if self.data != obj.data: 281 | return False 282 | return True 283 | 284 | def __len__(self): 285 | return len(self.data) 286 | 287 | def __iter__(self): 288 | return iter(self.data) 289 | 290 | def __getitem__(self, key): 291 | return self.data[key] 292 | 293 | def __setitem__(self, key, value): 294 | self.data[key] = value 295 | 296 | def info(self): 297 | msg = super().info() 298 | msg += "Content: Binary Blob ({0:d} Bytes)\n".format(len(self.data)) 299 | return msg 300 | 301 | def export(self): 302 | """ Export the image into byte array. """ 303 | if len(self.data) == 0: 304 | raise Exception("Image: No data to export !") 305 | 306 | self.header.data_size = len(self.data) 307 | self.header.data_crc = CRC32(self.data) 308 | 309 | return self.header.export() + self.data 310 | 311 | @classmethod 312 | def parse(cls, data, offset=0, ignore_crc=False): 313 | """ Load the image from byte array. 314 | :param data: The raw image as byte array 315 | :param offset: The offset of input data 316 | :param ignore_crc: ignore crc errors 317 | """ 318 | img = cls() 319 | img.header = Header.parse(data, offset, ignore_crc) 320 | offset += img.header.size 321 | if len(data[offset:]) < img.header.data_size: 322 | raise Exception("Image: Too small size of input data !") 323 | 324 | img.data = data[offset:offset + img.header.data_size] 325 | if CRC32(img.data) != img.header.data_crc: 326 | if not ignore_crc: 327 | raise Exception("Image: Uncorrect CRC of input data ") 328 | 329 | return img 330 | 331 | 332 | class FwImage(StdImage): 333 | def __init__(self, data=None, **kwargs): 334 | """ Image Constructor 335 | :param data: Image content as byte array 336 | :param laddr: Load address 337 | :param eaddr: Entry point address 338 | :param arch: Architecture (ARCHType Enum) 339 | :param os: Operating system (OSType Enum) 340 | :param compress: Image compression (COMPRESSType Enum) 341 | :param name: Image name (max: 32 chars) 342 | """ 343 | super().__init__(data, **kwargs) 344 | # Set The Image Type to Firmware 345 | self.header.image_type = EnumImageType.FIRMWARE 346 | 347 | def __eq__(self, obj): 348 | if not isinstance(obj, FwImage): 349 | return False 350 | if self.__len__() != len(obj): 351 | return False 352 | if self.data != obj.data: 353 | return False 354 | return True 355 | 356 | 357 | class ScriptImage(BaseImage): 358 | 359 | @property 360 | def cmds(self): 361 | return self._cmds 362 | 363 | @cmds.setter 364 | def cmds(self, value): 365 | self._cmds = value 366 | 367 | def __init__(self, cmds=None, **kwargs): 368 | """ Script Image Constructor 369 | :param cmds: List of commands, the item is in format [, ] 370 | :param laddr: Load address 371 | :param eaddr: Entry point address 372 | :param arch: Architecture (ARCHType Enum) 373 | :param os: Operating system (OSType Enum) 374 | :param compress: Image compression (COMPRESSType Enum) 375 | :param name: Image name (max: 32 chars) 376 | """ 377 | super().__init__(**kwargs) 378 | # Set The Image Type to Script 379 | self.header.image_type = EnumImageType.SCRIPT 380 | self._cmds = cmds if cmds else [] 381 | 382 | def __eq__(self, obj): 383 | if not isinstance(obj, ScriptImage): 384 | return False 385 | if self.__len__() != len(obj): 386 | return False 387 | if self.cmds != obj.cmds: 388 | return False 389 | return True 390 | 391 | def __len__(self): 392 | return len(self._cmds) 393 | 394 | def __iter__(self): 395 | return iter(self._cmds) 396 | 397 | def __getitem__(self, key): 398 | return self._cmds[key] 399 | 400 | def __setitem__(self, key, value): 401 | self._cmds[key] = value 402 | 403 | def info(self): 404 | i = 0 405 | msg = super().info() 406 | msg += 'Content: {0:d} Commands\n'.format(len(self._cmds)) 407 | for cmd in self._cmds: 408 | msg += "{0:3d}) {1:s} {2:s}\n".format(i, cmd[0], cmd[1]) 409 | i += 1 410 | return msg 411 | 412 | def append(self, cmd_name, cmd_value): 413 | assert isinstance(cmd_name, str), "ScriptImage: Command name must be a string" 414 | assert isinstance(cmd_value, str), "ScriptImage: Command value must be a string" 415 | self._cmds.append([cmd_name, cmd_value]) 416 | 417 | def pop(self, index): 418 | assert 0 <= index < len(self._cmds) 419 | return self._cmds.pop(index) 420 | 421 | def clear(self): 422 | self._cmds.clear() 423 | 424 | def load(self, txt_data): 425 | 426 | for line in txt_data.split('\n'): 427 | line = line.rstrip('\0') 428 | if not line: 429 | continue 430 | if line.startswith('#'): 431 | continue 432 | cmd = line.split(' ', 1) 433 | if len(cmd) == 1: 434 | cmd.append('') 435 | self._cmds.append([cmd[0], cmd[1]]) 436 | 437 | def store(self, txt_data=None): 438 | """ Store variables into text file 439 | :param txt_data: 440 | :return: txt_data 441 | """ 442 | if txt_data is None: 443 | txt_data = "" 444 | 445 | txt_data += '# U-Boot Script\n\n' 446 | for cmd in self._cmds: 447 | txt_data += "{0:s} {1:s}\n".format(cmd[0], cmd[1]) 448 | 449 | return txt_data 450 | 451 | def export(self): 452 | """ Export the image into byte array. """ 453 | if len(self._cmds) == 0: 454 | raise Exception("Image: No data to export !") 455 | 456 | data = b'' 457 | for cmd in self._cmds: 458 | data += "{0:s} {1:s}\n".format(cmd[0], cmd[1]).encode('utf-8') 459 | data = pack('!2L', len(data), 0) + data 460 | 461 | self.header.data_size = len(data) 462 | self.header.data_crc = CRC32(data) 463 | 464 | return self.header.export() + data 465 | 466 | @classmethod 467 | def parse(cls, data, offset=0, ignore_crc=False): 468 | """ Load the image from byte array. 469 | :param data: The raw image as byte array 470 | :param offset: The offset of input data 471 | :param ignore_crc: ignore crc errors 472 | """ 473 | img = cls() 474 | img.header = Header.parse(data, offset, ignore_crc) 475 | offset += img.header.size 476 | if len(data[offset:]) < img.header.data_size: 477 | raise Exception("Image: Too small size of input data !") 478 | 479 | data = data[offset:offset + img.header.data_size] 480 | if CRC32(data) != img.header.data_crc: 481 | if not ignore_crc: 482 | raise Exception("Image: Uncorrect CRC of input data ") 483 | 484 | # TODO: Check image format for script type 485 | size = unpack_from('!2L', data)[0] 486 | offset = 8 487 | 488 | data = data[offset:offset+size].decode('utf-8') 489 | for line in data.split('\n'): 490 | line = line.rstrip('\0') 491 | if not line: 492 | continue 493 | if line.startswith('#'): 494 | continue 495 | cmd = line.split(' ', 1) 496 | if len(cmd) == 1: 497 | cmd.append('') 498 | img.append(cmd[0], cmd[1]) 499 | 500 | return img 501 | 502 | 503 | class MultiImage(BaseImage): 504 | def __init__(self, imgs=None, **kwargs): 505 | """ Multi Image Constructor 506 | :param imgs: The list of all images 507 | :param laddr: Load address 508 | :param eaddr: Entry point address 509 | :param arch: Architecture (ARCHType Enum) 510 | :param os: Operating system (OSType Enum) 511 | :param compress: Image compression (COMPRESSType Enum) 512 | :param name: Image name (max: 32 chars) 513 | """ 514 | super().__init__(**kwargs) 515 | # Set Image Type as Multi in default 516 | self.header.image_type = EnumImageType.MULTI 517 | self._imgs = imgs if imgs else [] 518 | 519 | def __eq__(self, obj): 520 | if not isinstance(obj, MultiImage): 521 | return False 522 | if self.__len__() != len(obj): 523 | return False 524 | for img in self._imgs: 525 | if img not in obj: 526 | return False 527 | return True 528 | 529 | def __len__(self): 530 | return len(self._imgs) 531 | 532 | def __iadd__(self, value): 533 | self._imgs += value 534 | 535 | def __iter__(self): 536 | return iter(self._imgs) 537 | 538 | def __getitem__(self, key): 539 | return self._imgs[key] 540 | 541 | def __setitem__(self, key, value): 542 | self._imgs[key] = value 543 | 544 | def __delitem__(self, key): 545 | self._imgs.remove(key) 546 | 547 | def info(self): 548 | msg = super().info() 549 | msg += 'Content: {0:d} Images\n'.format(len(self._imgs)) 550 | n = 0 551 | for img in self._imgs: 552 | msg += '#IMAGE[' + str(n) + ']\n' 553 | msg += img.info() 554 | n += 1 555 | return msg 556 | 557 | def append(self, image): 558 | assert isinstance(image, BaseImage), "MultiImage: Unsupported value" 559 | self._imgs.append(image) 560 | 561 | def pop(self, index): 562 | assert 0 <= index < len(self._imgs) 563 | return self._imgs.pop(index) 564 | 565 | def cear(self): 566 | self._imgs.clear() 567 | 568 | def export(self): 569 | """ Export the image into byte array. 570 | :return 571 | """ 572 | data = bytes() 573 | dlen = [] 574 | 575 | if len(self._imgs) == 0: 576 | raise Exception("MultiImage: No data to export !") 577 | 578 | for img in self._imgs: 579 | dimg = img.export() 580 | # images must be aligned 581 | if len(dimg) % 4: 582 | dimg += b'\0' * (len(dimg) % 4) 583 | dlen.append(len(dimg)) 584 | data += dimg 585 | 586 | dlen.append(0) 587 | fmt = "!{0:d}L".format(len(dlen)) 588 | data = pack(fmt, *dlen) + data 589 | 590 | self.header.data_size = len(data) 591 | self.header.data_crc = CRC32(data) 592 | 593 | return self.header.export() + data 594 | 595 | @classmethod 596 | def parse(cls, data, offset=0, ignore_crc=False): 597 | """ Load the image from byte array. 598 | :param data: The raw image as byte array 599 | :param offset: The offset of input data 600 | :param ignore_crc: ignore crc errors 601 | """ 602 | img = cls() 603 | img.header = Header.parse(data, offset, ignore_crc) 604 | offset += img.header.size 605 | 606 | if len(data[offset:]) < img.header.data_size: 607 | raise Exception("MultiImage: Too small size of input data !") 608 | 609 | if CRC32(data[offset:offset + img.header.data_size]) != img.header.data_crc: 610 | if not ignore_crc: 611 | raise Exception("MultiImage: Uncorrect CRC of input data !") 612 | 613 | if img.header.image_type != EnumImageType.MULTI: 614 | raise Exception("MultiImage: Not a Multi Image Type !") 615 | 616 | # Parse images lengths 617 | sList = [] 618 | while True: 619 | (length,) = unpack_from('!L', data, offset) 620 | offset += 4 621 | if length == 0: break 622 | sList.append(length) 623 | 624 | # Parse images itself 625 | for size in sList: 626 | img.append(parse_img(data, offset)) 627 | offset += size 628 | 629 | return img 630 | # ---------------------------------------------------------------------------------------------------------------------- 631 | 632 | 633 | def get_img_type(data, offset=0, ignore_crc=False): 634 | """ Help function for extracting image fom raw data 635 | :param data: The raw data as byte array 636 | :param offset: The offset 637 | :return: Image type and offset where image start 638 | """ 639 | while True: 640 | if (offset + Header.SIZE) > len(data): 641 | raise Exception(f"Not an U-Boot image ! {(offset + Header.SIZE)} > {len(data)}") 642 | 643 | (header_mn, header_crc,) = unpack_from('!2L', data, offset) 644 | # Check the magic number if is U-Boot image 645 | if header_mn == Header.MAGIC_NUMBER: 646 | header = bytearray(data[offset:offset+Header.SIZE]) 647 | header[4:8] = [0]*4 648 | if not ignore_crc: 649 | if header_crc == CRC32(header): 650 | break 651 | else: 652 | break 653 | offset += 4 654 | 655 | (image_type,) = unpack_from('B', data, offset + 30) 656 | 657 | return image_type, offset 658 | 659 | 660 | def new_img(**kwargs): 661 | """ Help function for creating image 662 | :param img_type: 663 | :return: Image object 664 | """ 665 | if not 'image' in kwargs: 666 | kwargs['image'] = EnumImageType.FIRMWARE 667 | 668 | if isinstance(kwargs['image'], str): 669 | img_type = EnumImageType[kwargs['image']] 670 | else: 671 | img_type = kwargs['image'] 672 | 673 | if img_type not in EnumImageType: 674 | raise Exception("Not a valid image type") 675 | 676 | if img_type == EnumImageType.MULTI: 677 | img_obj = MultiImage(**kwargs) 678 | elif img_type == EnumImageType.FIRMWARE: 679 | img_obj = FwImage(**kwargs) 680 | elif img_type == EnumImageType.SCRIPT: 681 | img_obj = ScriptImage(**kwargs) 682 | else: 683 | img_obj = StdImage(**kwargs) 684 | 685 | return img_obj 686 | 687 | 688 | def parse_img(data, offset=0, ignore_crc=False): 689 | """ Help function for extracting image fom raw data 690 | :param data: The raw data as byte array 691 | :param offset: The offset 692 | :param ignore_crc: Ignore CRC mismatches 693 | :return: Image object 694 | """ 695 | (img_type, offset) = get_img_type(bytearray(data), offset, ignore_crc) 696 | 697 | if img_type not in EnumImageType: 698 | raise Exception("Not a valid image type") 699 | 700 | if img_type == EnumImageType.MULTI: 701 | img = MultiImage.parse(data, offset, ignore_crc) 702 | elif img_type == EnumImageType.FIRMWARE: 703 | img = FwImage.parse(data, offset, ignore_crc) 704 | elif img_type == EnumImageType.SCRIPT: 705 | img = ScriptImage.parse(data, offset, ignore_crc) 706 | else: 707 | img = StdImage.parse(data, offset, ignore_crc) 708 | img.header.image_type = img_type 709 | 710 | return img 711 | --------------------------------------------------------------------------------