├── npkpy ├── __init__.py ├── npk │ ├── __init__.py │ ├── cnt_flag_a.py │ ├── pck_preheader.py │ ├── cnt_null_block.py │ ├── pck_release_typ.py │ ├── pck_eckcdsa_hash.py │ ├── pck_description.py │ ├── cnt_mpls.py │ ├── cnt_architecture_tag.py │ ├── cnt_zlib_compressed_data.py │ ├── cnt_flag_c.py │ ├── pck_multicontainer_list.py │ ├── cnt_flag_b.py │ ├── pck_multicontainer_header.py │ ├── cnt_squashfs_hash_signature.py │ ├── cnt_squasfs_image.py │ ├── npk_file_basic.py │ ├── npk_constants.py │ ├── pck_header.py │ ├── cnt_basic.py │ ├── pck_requirements_header.py │ └── npk.py ├── analyse_npk.py ├── common.py └── main.py ├── tests ├── __init__.py ├── testData │ ├── 6_45_6 │ │ └── gps-6.45.6.npk │ └── 6_48_4 │ │ ├── gps-6.48.4.npk │ │ └── gps-6.48.4.result ├── cnt_mpls_test.py ├── cnt_flag_a_test.py ├── cnt_flag_b_test.py ├── cnt_flag_c_test.py ├── cnt_null_block_test.py ├── pck_preheader_test.py ├── pck_description_test.py ├── pck_release_typ_test.py ├── pck_eckcdsa_hash_test.py ├── cnt_zlib_compressed_data_test.py ├── pck_multicontainer_list_test.py ├── npk_constants_test.py ├── pck_multicontainer_header_test.py ├── cnt_squash_fs_hash_signature_test.py ├── cnt_squas_fs_image_test.py ├── npk_file_basic_test.py ├── pck_header_test.py ├── npk_test.py ├── cnt_basic_test.py ├── constants.py ├── pck_requirements_header_test.py ├── common_test.py └── npk_parsing_gps_file_test.py ├── tools ├── __init__.py ├── demo_modify_nkp_packages │ ├── __init__.py │ └── poc_modify_npk.py ├── sections_test.py ├── sections.py ├── npkModify_test.py ├── download_all_packages_test.py └── download_all_packages.py ├── tests_acceptance_test ├── __init__.py └── acceptance_test.py ├── pytest.sh ├── requirements-dev.txt ├── .pylintrc ├── bootstrap.sh ├── setup.py ├── .github └── workflows │ └── cicd.yml ├── .gitignore ├── README.md └── LICENSE /npkpy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /npkpy/npk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests_acceptance_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/demo_modify_nkp_packages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testData/6_45_6/gps-6.45.6.npk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botlabsDev/npkpy/HEAD/tests/testData/6_45_6/gps-6.45.6.npk -------------------------------------------------------------------------------- /tests/testData/6_48_4/gps-6.48.4.npk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botlabsDev/npkpy/HEAD/tests/testData/6_48_4/gps-6.48.4.npk -------------------------------------------------------------------------------- /pytest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest --cov=npkpy --cov=tests_acceptance_test -v 4 | pylint --rcfile=.pylintrc npkpy/** tests/** tests_acceptance_test/** 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pylint 3 | pytest_httpserver 4 | urlpath 5 | setuptools 6 | wheel 7 | twine 8 | pytest-cov 9 | more-itertools 10 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_flag_a.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_FLAG_A = 7 4 | 5 | 6 | class CntFlagA(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_FLAG_A 10 | -------------------------------------------------------------------------------- /npkpy/npk/pck_preheader.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_PCK_PREHEADER = 25 4 | 5 | class PckPreHeader(CntBasic): 6 | @property 7 | def _regular_cnt_id(self): 8 | return NPK_PCK_PREHEADER 9 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_null_block.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_NULL_BLOCK = 22 4 | 5 | 6 | class CntNullBlock(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_NULL_BLOCK 10 | -------------------------------------------------------------------------------- /npkpy/npk/pck_release_typ.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_RELEASE_TYP = 24 4 | 5 | 6 | class PckReleaseTyp(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_RELEASE_TYP 10 | -------------------------------------------------------------------------------- /npkpy/npk/pck_eckcdsa_hash.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_ECKCDSA_HASH = 23 4 | 5 | 6 | class PckEckcdsaHash(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_ECKCDSA_HASH 10 | -------------------------------------------------------------------------------- /npkpy/npk/pck_description.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_PCK_DESCRIPTION = 2 4 | 5 | 6 | class PckDescription(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_PCK_DESCRIPTION 10 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_mpls.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.pck_requirements_header import PckRequirementsHeader 2 | 3 | NPK_MPLS = 19 4 | 5 | 6 | class CntMpls(PckRequirementsHeader): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_MPLS 10 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_architecture_tag.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_ARCHITECTURE_TAG = 16 4 | 5 | 6 | class CntArchitectureTag(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_ARCHITECTURE_TAG 10 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_zlib_compressed_data.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_ZLIB_COMPRESSED_DATA = 4 4 | 5 | 6 | class CntZlibDompressedData(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_ZLIB_COMPRESSED_DATA 10 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_flag_c.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_FLAG_C = 17 4 | 5 | 6 | class CntFlagC(CntBasic): 7 | """ 8 | Flag typ only found in multicast-3.30-mipsbe.npk 9 | """ 10 | 11 | @property 12 | def _regular_cnt_id(self): 13 | return NPK_FLAG_C 14 | -------------------------------------------------------------------------------- /npkpy/npk/pck_multicontainer_list.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.pck_requirements_header import PckRequirementsHeader 2 | 3 | NPK_MULTICONTAINER_LIST = 20 4 | 5 | 6 | class PktMulticontainerList(PckRequirementsHeader): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_MULTICONTAINER_LIST 10 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=superfluous-parens, 3 | missing-class-docstring, 4 | missing-module-docstring, 5 | missing-function-docstring, 6 | fixme, 7 | not-callable, 8 | C0103, 9 | W0212, 10 | 11 | [MISCELLANEOUS] 12 | max-line-length=120 13 | 14 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_flag_b.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_FLAG_B = 8 4 | 5 | 6 | class CntFlagB(CntBasic): 7 | """ 8 | Flag typ found in gps-5.23-mipsbe.npk 9 | Payload contains b'\n update-console\n ' 10 | """ 11 | 12 | @property 13 | def _regular_cnt_id(self): 14 | return NPK_FLAG_B 15 | -------------------------------------------------------------------------------- /tests/cnt_mpls_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_mpls import CntMpls 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntMpls(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntMpls(get_dummy_basic_cnt(cnt_id=19), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(19, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/cnt_flag_a_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_flag_a import CntFlagA 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntFlagA(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntFlagA(get_dummy_basic_cnt(cnt_id=7), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(7, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/cnt_flag_b_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_flag_b import CntFlagB 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntFlagB(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntFlagB(get_dummy_basic_cnt(cnt_id=8), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(8, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/cnt_flag_c_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_flag_c import CntFlagC 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntFlagC(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntFlagC(get_dummy_basic_cnt(cnt_id=17), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(17, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /npkpy/npk/pck_multicontainer_header.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from npkpy.npk.pck_header import PckHeader 4 | 5 | NPK_MULTICONTAINER_HEADER: int = 18 6 | 7 | 8 | class PktMulticontainerHeader(PckHeader): 9 | @property 10 | def _regular_cnt_id(self): 11 | return NPK_MULTICONTAINER_HEADER 12 | 13 | @property 14 | def cnt_flags(self): 15 | return struct.unpack_from(b"4B", self._data, 34) 16 | -------------------------------------------------------------------------------- /tests/cnt_null_block_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_null_block import CntNullBlock 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntNullBlock(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntNullBlock(get_dummy_basic_cnt(cnt_id=22), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(22, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/pck_preheader_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_preheader import PckPreHeader 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_pckPreHeader(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = PckPreHeader(get_dummy_basic_cnt(cnt_id=25), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(25, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/pck_description_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_description import PckDescription 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_pckDescription(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = PckDescription(get_dummy_basic_cnt(cnt_id=2), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(2, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/pck_release_typ_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_release_typ import PckReleaseTyp 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_pckReleaseTyp(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = PckReleaseTyp(get_dummy_basic_cnt(cnt_id=24), offset_in_pck=0) 10 | 11 | def test_validate_cnt_id(self): 12 | self.assertEqual(24, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/pck_eckcdsa_hash_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_eckcdsa_hash import PckEckcdsaHash 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_pckEckcdsaHash(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = PckEckcdsaHash(get_dummy_basic_cnt(cnt_id=23), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(23, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_squashfs_hash_signature.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.cnt_basic import CntBasic 2 | 3 | NPK_SQUASHFS_HASH_SIGNATURE = 9 4 | 5 | 6 | class CntSquashFsHashSignature(CntBasic): 7 | @property 8 | def _regular_cnt_id(self): 9 | return NPK_SQUASHFS_HASH_SIGNATURE 10 | 11 | @property 12 | def output_cnt(self): 13 | id_name, options = super().output_cnt 14 | return id_name, options + [f"Payload[-10:]: {self.cnt_payload[-10:]}"] 15 | -------------------------------------------------------------------------------- /tests/cnt_zlib_compressed_data_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_zlib_compressed_data import CntZlibDompressedData 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntZlibCompressedData(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntZlibDompressedData(get_dummy_basic_cnt(cnt_id=4), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(4, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/pck_multicontainer_list_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_multicontainer_list import PktMulticontainerList 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cnt_MultiContainerList(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = PktMulticontainerList(get_dummy_basic_cnt(cnt_id=20), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(20, self.cnt.cnt_id) 13 | -------------------------------------------------------------------------------- /tests/npk_constants_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.npk_constants import CNT_HANDLER 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_npkConstants(unittest.TestCase): 8 | 9 | def test_validateAssignment_DictIdIsContainerId(self): 10 | for cnt_id, cnt_class in CNT_HANDLER.items(): 11 | if cnt_class != "?": 12 | cnt = cnt_class(get_dummy_basic_cnt(), 0) 13 | self.assertEqual(cnt_id, cnt._regular_cnt_id, 14 | msg=f"{cnt_id}!={cnt._regular_cnt_id}") 15 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_squasfs_image.py: -------------------------------------------------------------------------------- 1 | from npkpy.common import sha1_sum_from_binary 2 | from npkpy.npk.cnt_basic import CntBasic 3 | 4 | NPK_SQUASH_FS_IMAGE = 21 5 | 6 | 7 | class CntSquashFsImage(CntBasic): 8 | @property 9 | def _regular_cnt_id(self): 10 | return NPK_SQUASH_FS_IMAGE 11 | 12 | @property 13 | def cnt_payload_hash(self): 14 | return sha1_sum_from_binary(self.cnt_payload) 15 | 16 | @property 17 | def output_cnt(self): 18 | id_name, options = super().output_cnt 19 | return id_name, options + [f"calc Sha1Hash: {self.cnt_payload_hash}"] 20 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | venv=${1:-virtualenv} 4 | 5 | ## setup virtualenv if not already exist 6 | if [[ ! -e ${venv} ]]; then 7 | virtualenv --python=python ${venv} 8 | ${venv}/bin/pip install pip --upgrade 9 | ${venv}/bin/pip install pip -r requirements-dev.txt 10 | ${venv}/bin/pip install pip -e . 11 | fi 12 | 13 | git config user.name "botlabsDev" 14 | git config user.email "54632107+botlabsDev@users.noreply.github.com" 15 | echo "--git config--" 16 | echo -n "git user:"; git config user.name 17 | echo -n "git email:"; git config user.email 18 | echo "--------------" 19 | 20 | ## start virtualenv 21 | source ${venv}/bin/activate 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/pck_multicontainer_header_test.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import unittest 3 | 4 | from npkpy.npk.pck_multicontainer_header import PktMulticontainerHeader 5 | from tests.constants import DummyHeaderCnt 6 | 7 | 8 | class Test_pktMultiContainerHeader(unittest.TestCase): 9 | def setUp(self) -> None: 10 | dummy_cnt = DummyHeaderCnt() 11 | dummy_cnt._00_cnt_id = struct.pack("h", 18) 12 | self.cnt = PktMulticontainerHeader(dummy_cnt.get_binary, offset_in_pck=0) 13 | 14 | def test_validateCntId(self): 15 | self.assertEqual(18, self.cnt.cnt_id) 16 | 17 | def test_getCntFlags(self): 18 | self.assertEqual((0, 0, 0, 0), self.cnt.cnt_flags) 19 | -------------------------------------------------------------------------------- /tests/cnt_squash_fs_hash_signature_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_squashfs_hash_signature import CntSquashFsHashSignature 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntSquashFsHashSignature(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntSquashFsHashSignature(get_dummy_basic_cnt(cnt_id=9), offset_in_pck=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(9, self.cnt.cnt_id) 13 | 14 | def test_giveOverviewOfCnt(self): 15 | expected = "Payload[-10:]: b'Payload'" 16 | 17 | _, cntData = self.cnt.output_cnt 18 | 19 | self.assertEqual(expected, cntData[-1]) 20 | -------------------------------------------------------------------------------- /tests/cnt_squas_fs_image_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.cnt_squasfs_image import CntSquashFsImage 4 | from tests.constants import get_dummy_basic_cnt 5 | 6 | 7 | class Test_cntSquashFsImage(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = CntSquashFsImage(get_dummy_basic_cnt(cnt_id=21), offset_in_pck=0) 10 | 11 | self.expectedHash = b'\xc3\x04\x15\xea\xccjYDit\xb7\x16\xef\xf5l\xf2\x82\x19\x81]' 12 | 13 | def test_validateCntId(self): 14 | self.assertEqual(21, self.cnt.cnt_id) 15 | 16 | def test_payloadHash(self): 17 | self.assertEqual(self.expectedHash, self.cnt.cnt_payload_hash) 18 | 19 | def test_giveOverviewOfCnt(self): 20 | expected = f"calc Sha1Hash: {self.expectedHash}" 21 | 22 | _, cntData = self.cnt.output_cnt 23 | 24 | self.assertEqual(expected, cntData[-1]) 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.8 2 | import setuptools 3 | from datetime import datetime 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="npkPy", 10 | version=f"{(datetime.now()).strftime('%Y.%m.%d.%H.%M')}", 11 | description="npkPy is an unpacker tool for MikroTiks custom npk container format", 12 | author='botlabsDev', 13 | author_email='npkPy@botlabs.dev', 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/botlabsDev/npkpy", 17 | packages=setuptools.find_packages(), 18 | python_requires='>=3.6', 19 | classifiers=[ 20 | "Programming Language :: Python :: 3.6", 21 | "License :: OSI Approved :: GNU Affero General Public License v3", 22 | "Operating System :: OS Independent", 23 | ], 24 | entry_points={ 25 | 'console_scripts': [ 26 | "npkpy=npkpy.main:main", 27 | # "npkDownloader=npkpy.download:main", 28 | ], 29 | }, 30 | install_requires=[ 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /npkpy/analyse_npk.py: -------------------------------------------------------------------------------- 1 | from npkpy.common import get_full_pkt_info, extract_container 2 | from npkpy.npk.cnt_squasfs_image import NPK_SQUASH_FS_IMAGE 3 | from npkpy.npk.cnt_zlib_compressed_data import NPK_ZLIB_COMPRESSED_DATA 4 | from npkpy.npk.npk_constants import CNT_HANDLER 5 | 6 | EXPORT_FOLDER_PREFIX = "npkPyExport_" 7 | 8 | 9 | def analyse_npk(opts, npk_files): 10 | filter_container = [] 11 | 12 | if opts.show_container: 13 | for file in npk_files: 14 | print("\n".join(get_full_pkt_info(file))) 15 | 16 | if opts.export_all: 17 | print("export all!!") 18 | filter_container = CNT_HANDLER.keys() 19 | if opts.export_squashfs: 20 | filter_container = [NPK_SQUASH_FS_IMAGE] 21 | if opts.export_zlib: 22 | filter_container = [NPK_ZLIB_COMPRESSED_DATA] 23 | 24 | if filter_container: 25 | for npk_file in npk_files: 26 | export_folder = opts.dst_folder / f"{EXPORT_FOLDER_PREFIX}{npk_file.file.stem}" 27 | export_folder.mkdir(parents=True, exist_ok=True) 28 | 29 | extract_container(npk_file, export_folder, filter_container) 30 | 31 | if not next(export_folder.iterdir(), None): 32 | export_folder.rmdir() 33 | -------------------------------------------------------------------------------- /tools/sections_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from tempfile import NamedTemporaryFile 4 | 5 | from tools.sections import findDiffs 6 | 7 | 8 | class FindDiffs_Test(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.fileA = Path(NamedTemporaryFile(delete=False).name) 11 | self.fileB = Path(NamedTemporaryFile(delete=False).name) 12 | self.fileC = Path(NamedTemporaryFile(delete=False).name) 13 | 14 | def tearDown(self) -> None: 15 | self.fileA.unlink() 16 | self.fileB.unlink() 17 | self.fileC.unlink() 18 | 19 | def test_filesEqual_minimumFile(self): 20 | self.assertFileDiff("a", "a", [(0, 0, False)]) 21 | 22 | def test_filesUnequal_minimumFile(self): 23 | self.assertFileDiff("a", "b", [(0, 0, True)]) 24 | 25 | def test_filesEqual_equalSize(self): 26 | self.assertFileDiff("aabbcc", "aabbcc", [(0, 5, False)]) 27 | 28 | def test_diffInCenter_equalSize(self): 29 | self.assertFileDiff("aabbcc", "aa cc", [(0, 1, False), (2, 3, True), (4, 5, False)]) 30 | 31 | def test_differInSize(self): 32 | self.assertFileDiff("aa", "aabb", [(0, 1, False)]) 33 | 34 | def assertFileDiff(self, contentA, contentB, result): 35 | writeFile(self.fileA, contentA) 36 | writeFile(self.fileB, contentB) 37 | 38 | self.assertEqual(result, findDiffs(self.fileA, self.fileB)) 39 | 40 | 41 | def writeFile(file, content): 42 | with file.open("w") as f: 43 | f.write(content) 44 | -------------------------------------------------------------------------------- /tests/npk_file_basic_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | 4 | from npkpy.npk.npk_file_basic import FileBasic, ARCHITECTURES 5 | 6 | 7 | class FileInfo_Test(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.file = FileBasic(Path("file-name-1.2.3.npk")) 10 | self.illegal_file = FileBasic(Path("illegalFIle.abc")) 11 | 12 | def test_file(self): 13 | self.assertEqual(Path("file-name-1.2.3.npk"), self.file.file) 14 | 15 | def test_versionName(self): 16 | self.assertEqual("1.2.3", self.file.filename_version) 17 | 18 | def test_versionName_filenameDoesntMatchFormat(self): 19 | self.assertEqual("", self.illegal_file.filename_version) 20 | 21 | def test_programName(self): 22 | self.assertEqual("file-name", self.file.filename_program_name) 23 | 24 | def test_programName_filenameDoesntMatchFormat(self): 25 | self.assertEqual("", self.illegal_file.filename_program_name) 26 | 27 | def test_programSuffix(self): 28 | self.assertEqual("npk", self.file.filename_suffix) 29 | 30 | def test_programSuffix_filenameDoesntMatchFormat(self): 31 | self.assertEqual("", self.illegal_file.filename_suffix) 32 | 33 | def test_filenameArchitecture_returnDefaultIfNotMentionedInFilename(self): 34 | self.assertEqual("x86", self.file.filename_architecture) 35 | 36 | def test_filenameArchitecture_validateArchitectures(self): 37 | for arch in ARCHITECTURES: 38 | self.assertEqual(arch, FileBasic(Path(f"file-name-1.2.3-{arch}.npk")).filename_architecture) 39 | -------------------------------------------------------------------------------- /npkpy/npk/npk_file_basic.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from npkpy.common import sha1_sum_from_file 5 | 6 | ARCHITECTURES = ['arm', 'mipsbe', 'mipsle', 'mmips', 'ppc', 'smips', 'tile', 'x86'] 7 | 8 | RE_NPK_SUFFIX = '\\.(npk$)' 9 | RE_VERSION = '([\\d]+[\\.\\d]*\\d)' 10 | RE_PROGRAM_NAME = '(^[\\w-]*)-' 11 | 12 | 13 | class FileBasic: 14 | # pylint: disable=unused-private-member 15 | __data = None 16 | 17 | def __init__(self, file_path: Path): 18 | self.file = file_path 19 | 20 | @property 21 | def filename_suffix(self): 22 | suffix = re.search(RE_NPK_SUFFIX, self.file.name) 23 | return suffix.group(1) if suffix else "" 24 | 25 | @property 26 | def filename_version(self): 27 | result = re.search(RE_VERSION, self.file.name) 28 | return result.group(1) if result else "" 29 | 30 | @property 31 | def filename_architecture(self): 32 | for arch in ARCHITECTURES: 33 | if f"-{arch}.npk" in self.file.name: 34 | return arch 35 | return "x86" 36 | 37 | @property 38 | def filename_program_name(self): 39 | name = re.search(RE_PROGRAM_NAME, self.file.name) 40 | if name: 41 | return name.group(1).replace(f"_{self.filename_architecture}_", "") 42 | return "" 43 | 44 | @property 45 | def file_hash(self): 46 | return sha1_sum_from_file(self.file) 47 | 48 | def read_data_from_file(self, offset, size): 49 | with self.file.open("rb") as _file: 50 | _file.seek(offset) 51 | return bytearray(_file.read(size)) 52 | -------------------------------------------------------------------------------- /tools/sections.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from pathlib import Path 3 | from typing import Tuple 4 | 5 | from more_itertools import peekable 6 | 7 | 8 | def getBinaryFromFile(file: Path): 9 | with file.open("rb") as f: 10 | data = f.read() 11 | for d in data: 12 | yield d 13 | 14 | def findDiffs(lastFile, file): 15 | bLastFile = peekable(getBinaryFromFile(lastFile)) 16 | bfile = peekable(getBinaryFromFile(file)) 17 | 18 | sections = [] 19 | counter = -1 20 | hasChanged = (bLastFile.peek() == bfile.peek()) 21 | sectionTracker = dict({True: 0, False: 0}) 22 | 23 | while True: 24 | try: 25 | while (next(bLastFile) == next(bfile)) is hasChanged: 26 | counter += 1 27 | except StopIteration: 28 | sections.append((max(sectionTracker.values()), counter, not hasChanged)) 29 | break 30 | 31 | sectionTracker[hasChanged] = counter + 1 32 | hasChanged = not hasChanged 33 | sections.append((sectionTracker[hasChanged], counter, hasChanged)) 34 | counter += 1 35 | 36 | return sections 37 | 38 | 39 | def findSections(filesDict: dict) -> Tuple[str, Counter]: 40 | cTotal = Counter() 41 | for (program, versionFiles) in filesDict.items(): 42 | lastFile = None 43 | c = Counter() 44 | for (version, file) in versionFiles.items(): 45 | if lastFile is not None: 46 | diffs = findDiffs(lastFile.file, file.file) 47 | c.update(diffs) 48 | cTotal.update(diffs) 49 | lastFile = file 50 | yield program, c 51 | yield "__total", cTotal 52 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: 3 | release: 4 | push: 5 | schedule: 6 | - cron: '0 2 * * *' # run at 2 AM UTC 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: actions/setup-python@v1 15 | with: 16 | python-version: 3.6 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements-dev.txt 22 | pip install -e . 23 | 24 | - name: Pylint 25 | run: | 26 | pip install pytest pytest-cov codecov 27 | pylint --rcfile=.pylintrc npkpy 28 | 29 | - name: Pytest and Coverage 30 | run: | 31 | pip install pytest pytest-cov codecov 32 | pytest --cov=npkpy --cov=acceptance_test 33 | 34 | - name: Publish to codecov 35 | run: | 36 | codecov --token=${{ secrets.CODECOV_TOKEN }} 37 | 38 | release: 39 | runs-on: ubuntu-latest 40 | needs: tests 41 | if: github.event_name == 'release' || github.event_name == 'push' 42 | 43 | steps: 44 | - uses: actions/checkout@v1 45 | - uses: actions/setup-python@v1 46 | with: 47 | python-version: 3.6 48 | 49 | - name: Install dependencies 50 | run: | 51 | python -m pip install --upgrade pip 52 | pip install -r requirements-dev.txt 53 | pip install -e . 54 | 55 | - name: Build a binary wheel and a source tarball 56 | run: | 57 | python3 setup.py sdist bdist_wheel 58 | 59 | 60 | - name: Release npkPy to PyPI 61 | # if: github.event_name == 'release' 62 | uses: pypa/gh-action-pypi-publish@master 63 | with: 64 | password: ${{ secrets.PYPI_PASSWORD }} 65 | 66 | #- name: Release npkPy to test.pypi.org 67 | # uses: pypa/gh-action-pypi-publish@master 68 | # with: 69 | # password: ${{ secrets.TEST_PYPI_PASSWORD }} 70 | # repository_url: https://test.pypi.org/legacy/ 71 | 72 | -------------------------------------------------------------------------------- /npkpy/common.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from pathlib import Path 3 | from typing import List 4 | 5 | 6 | def get_all_nkp_files(path, contain_str=None): 7 | return path.glob(f"**/*{contain_str}*.npk" if contain_str else "**/*.npk") 8 | 9 | 10 | def extract_container(npk_file, export_folder, container_ids): 11 | for position, cnt in enumerate(npk_file.pck_cnt_list): 12 | if cnt.cnt_id in container_ids: 13 | file_path = export_folder / f"{position:03}_cnt_{cnt.cnt_id_name}.raw" 14 | write_to_file(file_path, cnt.cnt_payload) 15 | 16 | 17 | def write_to_file(file, payloads): 18 | payloads = [payloads] if not isinstance(payloads, list) else payloads 19 | with open(file, "wb") as _file: 20 | for payload in payloads: 21 | _file.write(payload) 22 | 23 | 24 | def get_short_pkt_info(file) -> List: 25 | return [str(file.file.name)] 26 | 27 | 28 | def get_full_pkt_info(file) -> List: 29 | output = get_short_pkt_info(file) 30 | output += get_short_cnt_info(file) 31 | for cnt in file.pck_cnt_list: 32 | output += get_full_cnt_info(cnt) 33 | return output 34 | 35 | 36 | def get_short_cnt_info(file) -> List: 37 | return [f"Cnt:{pos:3}:{c.cnt_id_name}" for pos, c in file.pck_enumerate_cnt] 38 | 39 | 40 | def get_full_cnt_info(cnt) -> List: 41 | info = [] 42 | id_name, options = cnt.output_cnt 43 | info.append(f"{id_name}") 44 | for option in options: 45 | info.append(f" {option}") 46 | return info 47 | 48 | 49 | def sha1_sum_from_file(file: Path): 50 | with file.open('rb') as _file: 51 | return sha1_sum_from_binary(_file.read()) 52 | 53 | 54 | def sha1_sum_from_binary(payloads): 55 | if len(payloads) == 0: 56 | return b"" 57 | 58 | sha1 = hashlib.sha1() 59 | for payload in [payloads] if not isinstance(payloads, list) else payloads: 60 | sha1.update(payload) 61 | 62 | return sha1.digest() 63 | 64 | 65 | class NPKIdError(BaseException): 66 | pass 67 | 68 | 69 | class NPKMagicBytesError(BaseException): 70 | pass 71 | 72 | 73 | class NPKError(BaseException): 74 | pass 75 | -------------------------------------------------------------------------------- /tools/demo_modify_nkp_packages/poc_modify_npk.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from pprint import pprint 3 | 4 | from npkpy.npk.npk import Npk 5 | 6 | 7 | ## KEEP IN MIND: MODIFICATIONS WILL INVALIDATE THE NPK PACKAGE SIGNATURE! 8 | ## THE ROUTER WON'T INSTALL THE PACKAGE. 9 | 10 | 11 | def modify_poc(): 12 | npk_file = Npk(Path("tests/testData/6_48_4/gps-6.48.4.npk")) 13 | 14 | # print overview 15 | print("----Overview--------------------") 16 | pprint([f"pos: {pos:2} - Name: {cnt.cnt_id_name} (id:{cnt.cnt_id:2})" for pos, cnt in npk_file.pck_enumerate_cnt]) 17 | print("-------------------------------") 18 | 19 | # The following code example will modify the payload section of PckDescription 20 | CNT_ID = 4 # PckDescription 21 | 22 | print("Payload original:") 23 | print_overview(npk_file, cnt_id=CNT_ID) 24 | 25 | print("overwrite payload - same size:") 26 | npk_file.pck_cnt_list[CNT_ID].cnt_payload = b"a" * 25 27 | print_overview(npk_file, cnt_id=CNT_ID) 28 | 29 | # Modifying the size of the payload can affect the whole npk package and 30 | # forces recalculations in other containers of this package 31 | print("Payload new - small size:") 32 | npk_file.pck_cnt_list[CNT_ID].cnt_payload = b"b" * 10 33 | print_overview(npk_file, cnt_id=CNT_ID) 34 | 35 | print("Payload new - increased:") 36 | npk_file.pck_cnt_list[CNT_ID].cnt_payload = b"c" * 100 37 | print_overview(npk_file, cnt_id=CNT_ID) 38 | 39 | print("Write File: modified.npk") 40 | Path("modified.npk").write_bytes(npk_file.pck_full_binary) 41 | 42 | # Parse the new npk file as shown blow: 43 | # $ npkpy --files modified.npk --show-container 44 | 45 | 46 | def print_overview(npk_file, cnt_id): 47 | cnt = npk_file.pck_cnt_list[cnt_id] 48 | print("Cnt payload: ", cnt.cnt_payload) 49 | print("Cnt payload len: ", cnt.cnt_payload_len) 50 | print("Cnt len: ", cnt.cnt_full_length) 51 | print("pkg len: ", npk_file.pck_payload_len) 52 | print("-------------------------------") 53 | 54 | 55 | if __name__ == '__main__': 56 | modify_poc() 57 | -------------------------------------------------------------------------------- /tests/pck_header_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import struct 3 | import unittest 4 | 5 | from npkpy.npk.pck_header import PckHeader 6 | from tests.constants import DummyHeaderCnt 7 | 8 | 9 | class Test_pckHeader(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.dummy_cnt = DummyHeaderCnt() 12 | self.dummy_cnt._00_cnt_id = struct.pack("h", 1) 13 | self.cnt = PckHeader(self.dummy_cnt.get_binary, offset_in_pck=0) 14 | 15 | def test_validateCntId(self): 16 | self.assertEqual(1, self.cnt.cnt_id) 17 | 18 | def test_getCntProgramName(self): 19 | self.assertEqual("01234567890abcde", self.cnt.cnt_program_name) 20 | 21 | def test_getOsVersion(self): 22 | self.assertEqual("1.2.3 - rc(?): 4", self.cnt.cnt_os_version) 23 | 24 | def test_getNullBlock(self): 25 | self.assertEqual((0, 0, 0, 0), self.cnt.cnt_null_block) 26 | 27 | def test_getBuildTime(self): 28 | self.assertEqual(datetime.datetime(1970, 1, 1, 0, 0, 1), self.cnt.cnt_built_time) 29 | 30 | def test_getOutput(self): 31 | self.assertEqual(('PckHeader', 32 | ['Cnt id: 1', 33 | 'Cnt offset: 0', 34 | 'Cnt len: 41', 35 | 'Payload len: 35', 36 | "Payload[0:10]: b'0123456789' [...] ", 37 | 'Program name: 01234567890abcde', 38 | 'Os version: 1.2.3 - rc(?): 4', 39 | 'Created at: 1970-01-01 00:00:01', 40 | 'NullBlock: (0, 0, 0, 0)', 41 | 'Flags: (0, 0, 0, 0, 0, 0, 0)']), self.cnt.output_cnt) 42 | 43 | def test_getCntFlags(self): 44 | self.assertEqual((0, 0, 0, 0, 0, 0, 0), self.cnt.cnt_flags) 45 | 46 | def test_flagsForSpecificVersion(self): 47 | # INFO: pkt with version 5.23 seems to have only four flags. 48 | cnt = PckHeader(self.dummy_cnt.get_binary_with_special_flags, offset_in_pck=0) 49 | self.assertEqual((0, 0, 0, 0), cnt.cnt_flags) 50 | -------------------------------------------------------------------------------- /npkpy/npk/npk_constants.py: -------------------------------------------------------------------------------- 1 | from npkpy.npk.pck_preheader import NPK_PCK_PREHEADER, PckPreHeader 2 | from npkpy.npk.pck_multicontainer_list import NPK_MULTICONTAINER_LIST, PktMulticontainerList 3 | from npkpy.npk.cnt_architecture_tag import NPK_ARCHITECTURE_TAG, CntArchitectureTag 4 | from npkpy.npk.cnt_null_block import NPK_NULL_BLOCK, CntNullBlock 5 | from npkpy.npk.pck_release_typ import NPK_RELEASE_TYP, PckReleaseTyp 6 | from npkpy.npk.cnt_squasfs_image import NPK_SQUASH_FS_IMAGE, CntSquashFsImage 7 | from npkpy.npk.cnt_squashfs_hash_signature import NPK_SQUASHFS_HASH_SIGNATURE, CntSquashFsHashSignature 8 | from npkpy.npk.cnt_zlib_compressed_data import NPK_ZLIB_COMPRESSED_DATA, CntZlibDompressedData 9 | from npkpy.npk.cnt_basic import NPK_CNT_BASIC, CntBasic 10 | from npkpy.npk.pck_description import NPK_PCK_DESCRIPTION, PckDescription 11 | from npkpy.npk.pck_eckcdsa_hash import NPK_ECKCDSA_HASH, PckEckcdsaHash 12 | from npkpy.npk.pck_header import NPK_PCK_HEADER, PckHeader 13 | from npkpy.npk.pck_requirements_header import NPK_REQUIREMENTS_HEADER, PckRequirementsHeader 14 | from npkpy.npk.cnt_flag_b import NPK_FLAG_B, CntFlagB 15 | from npkpy.npk.cnt_flag_c import NPK_FLAG_C, CntFlagC 16 | from npkpy.npk.cnt_flag_a import NPK_FLAG_A, CntFlagA 17 | from npkpy.npk.pck_multicontainer_header import NPK_MULTICONTAINER_HEADER, PktMulticontainerHeader 18 | from npkpy.npk.cnt_mpls import NPK_MPLS, CntMpls 19 | 20 | CNT_HANDLER = { 21 | NPK_CNT_BASIC: CntBasic, 22 | 0: "?", 23 | NPK_PCK_HEADER: PckHeader, 24 | NPK_PCK_DESCRIPTION: PckDescription, 25 | NPK_REQUIREMENTS_HEADER: PckRequirementsHeader, 26 | NPK_ZLIB_COMPRESSED_DATA: CntZlibDompressedData, 27 | 5: "?", 28 | 6: "?", 29 | NPK_FLAG_A: CntFlagA, 30 | NPK_FLAG_B: CntFlagB, 31 | NPK_SQUASHFS_HASH_SIGNATURE: CntSquashFsHashSignature, 32 | 10: "?", 33 | 11: "?", 34 | 12: "?", 35 | 13: "?", 36 | 14: "?", 37 | 15: "?", 38 | NPK_ARCHITECTURE_TAG: CntArchitectureTag, 39 | NPK_FLAG_C: CntFlagC, 40 | NPK_MULTICONTAINER_HEADER: PktMulticontainerHeader, 41 | NPK_MPLS: CntMpls, 42 | NPK_MULTICONTAINER_LIST: PktMulticontainerList, 43 | NPK_SQUASH_FS_IMAGE: CntSquashFsImage, 44 | NPK_NULL_BLOCK: CntNullBlock, 45 | NPK_ECKCDSA_HASH: PckEckcdsaHash, 46 | NPK_RELEASE_TYP: PckReleaseTyp, 47 | NPK_PCK_PREHEADER: PckPreHeader, 48 | 26: "?", 49 | 27: "?", 50 | 28: "?", 51 | 29: "?", 52 | 30: "?", 53 | } 54 | -------------------------------------------------------------------------------- /npkpy/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pathlib import Path 3 | 4 | from npkpy.analyse_npk import analyse_npk 5 | from npkpy.common import get_all_nkp_files 6 | from npkpy.npk.npk import Npk 7 | 8 | 9 | def parse_args(): 10 | parser = argparse.ArgumentParser(description='npkPy is an unpacking tool for MikroTiks custom NPK container format') 11 | 12 | input_group = parser.add_argument_group("input") 13 | input_group.add_argument("--files", 14 | action='append', type=Path, help="Select one or more files to process") 15 | 16 | input_group.add_argument("--src-folder", 17 | type=Path, default=Path("."), 18 | help="Process all NPK files found recursively in given source folder.") 19 | 20 | input_filter_group = input_group.add_mutually_exclusive_group() 21 | input_filter_group.add_argument("--glob", 22 | type=str, default=None, 23 | help="Simple glob. Filter files from --srcFolder which match the given string.") 24 | 25 | output_group = parser.add_argument_group("output") 26 | output_group.add_argument("--dst-folder", 27 | type=Path, default=Path(".") / "exportNpk", 28 | help="Extract container into given folder") 29 | 30 | action_group = parser.add_argument_group("actions") 31 | exclusive_action = action_group.add_mutually_exclusive_group(required=True) 32 | exclusive_action.add_argument("--show-container", 33 | action="store_true", help="List all container from selected NPK files") 34 | exclusive_action.add_argument("--export-all", 35 | action="store_true", help="Export all container from selected NPK files") 36 | exclusive_action.add_argument("--export-squashfs", action="store_true", 37 | help="Export all SquashFs container from selected NPK files") 38 | exclusive_action.add_argument("--export-zlib", 39 | action="store_true", 40 | help="Export all Zlib compressed container from selected NPK files") 41 | return parser.parse_args() 42 | 43 | 44 | def main(): 45 | opts = parse_args() 46 | files = (Npk(f) for f in (opts.files if opts.files else get_all_nkp_files(opts.src_folder, opts.glob))) 47 | analyse_npk(opts, files) 48 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | ## custom 133 | .idea 134 | virtualenv/ 135 | 136 | -------------------------------------------------------------------------------- /npkpy/npk/pck_header.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import struct 3 | 4 | from npkpy.npk.cnt_basic import CntBasic 5 | 6 | NPK_PCK_HEADER = 1 7 | 8 | """ 9 | 0____4____8____b____f 10 | | | | | | 11 | x0_|AABB|BBCC|CCCC|CCCC| 12 | x1_|CCCC|CCDE|FGHH|HH..| 13 | x2_|....|....|....|....| 14 | 15 | A = Container Identifier (2) 16 | B = Payload length (4) 17 | C = Program Name (16) 18 | D = Program version: revision 19 | E = Program version: rc 20 | F = Program version: minor 21 | G = Program version: major 22 | H = Build time 23 | I = NULL BLock / Flags 24 | 25 | """ 26 | 27 | 28 | class PckHeader(CntBasic): 29 | def __init__(self, data, offset_in_pck): 30 | super().__init__(data, offset_in_pck) 31 | self._offset = offset_in_pck 32 | self.flag_offset = 0 33 | 34 | @property 35 | def _regular_cnt_id(self): 36 | return NPK_PCK_HEADER 37 | 38 | @property 39 | def cnt_program_name(self): 40 | return bytes(struct.unpack_from("16B", self._data, 6)).decode().rstrip('\x00') 41 | 42 | @property 43 | def cnt_os_version(self): 44 | revision = (struct.unpack_from("B", self._data, 22))[0] 45 | unknown_subrevision = (struct.unpack_from("B", self._data, 23))[0] 46 | minor = (struct.unpack_from("B", self._data, 24))[0] 47 | major = (struct.unpack_from("B", self._data, 25))[0] 48 | return f"{major}.{minor}.{revision} - rc(?): {unknown_subrevision}" 49 | 50 | @property 51 | def cnt_built_time(self): 52 | return datetime.datetime.utcfromtimestamp(struct.unpack_from("I", self._data, 26)[0]) 53 | 54 | @property 55 | def cnt_null_block(self): 56 | return struct.unpack_from("4B", self._data, 30) 57 | 58 | @property 59 | def cnt_flags(self): 60 | try: 61 | return struct.unpack_from("7B", self._data, 34) 62 | except struct.error: 63 | # INFO: pkt with version 5.23 seems to have only four flags. 64 | return struct.unpack_from("4B", self._data, 34) 65 | 66 | @property 67 | def output_cnt(self): 68 | id_name, options = super().output_cnt 69 | return (id_name, options + [f"Program name: {self.cnt_program_name}", 70 | f"Os version: {self.cnt_os_version}", 71 | f"Created at: {self.cnt_built_time}", 72 | f"NullBlock: {self.cnt_null_block}", 73 | f"Flags: {self.cnt_flags}" 74 | ]) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/botlabsDev/npkpy/workflows/Pytest/badge.svg)](https://github.com/botlabsDev/npkpy/actions) 2 | [![codecov](https://codecov.io/gh/botlabsDev/npkpy/branch/master/graph/badge.svg?token=4ns6uIqoln)](https://codecov.io/gh/botlabsDev/npkpy) 3 | 4 | 5 | 6 | # npkPy 7 | The npkPy package module is an unpacking tool for MikroTiks custom NPK container format. The tool is capable 8 | to display the content of any NPK package and to export all container. 9 | 10 | ["NPK stands for MikroTik RouterOS upgrade package"](https://whatis.techtarget.com/fileformat/NPK-MikroTik-RouterOS-upgrade-package) 11 | and since there is no solid unpacking tool for the format available, I want to share my approach of it. 12 | The format, in general, is used by MikroTik to install and update the software on MikroTiks routerOs systems. 13 | 14 | NPK packages can be found here: [MikroTik Archive](https://mikrotik.com/download/archive) 15 | 16 | The code covers the ability to modify the container payload. Yet, this ability won't be available for cli. 17 | Please be aware, that you can't create or modify __valid__ packages [since they are signed](https://forum.mikrotik.com/viewtopic.php?t=87126). 18 | 19 | ``` 20 | All recent packages are signed with EC-KCDSA signature, 21 | and there's no way to create a valid npk file unless you know a secret key. 22 | ``` 23 | 24 | ## Installation 25 | 26 | ``` 27 | pip install npkPy 28 | ``` 29 | 30 | ## Usage 31 | 32 | ``` 33 | $ npkPy is an unpacking tool for MikroTiks custom NPK container format 34 | 35 | optional arguments: 36 | -h, --help show this help message and exit 37 | 38 | input: 39 | --files FILES Select one or more files to process 40 | --srcFolder SRCFOLDER 41 | Process all NPK files found recursively in given source folder. 42 | --glob GLOB Simple glob. Filter files from --srcFolder which match the given string. 43 | 44 | output: 45 | --dstFolder DSTFOLDER 46 | Extract container into given folder 47 | 48 | actions: 49 | --showContainer List all container from selected NPK files 50 | --exportAll Export all container from selected NPK files 51 | --exportSquashFs Export all SquashFs container from selected NPK files 52 | --exportZlib Export all Zlib compressed container from selected NPK files 53 | 54 | ``` 55 | 56 | Common understanding: A file represents an NPK package with multiple containers. 57 | Each container 'contains' payloads like descriptions, SquashFs images or Zlib compressed data. 58 | 59 | ## Other unpacking tools 60 | If npkPy does not work for you, check out older approaches of NPK unpacking tools: 61 | * [mikrotik-npk](https://github.com/kost/mikrotik-npk) 62 | * [npk-tools](https://github.com/rsa9000/npk-tools) 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/testData/6_48_4/gps-6.48.4.result: -------------------------------------------------------------------------------- 1 | gps-6.48.4.npk 2 | Cnt: 0:PckPreHeader 3 | Cnt: 1:PckHeader 4 | Cnt: 2:PckReleaseTyp 5 | Cnt: 3:CntArchitectureTag 6 | Cnt: 4:PckDescription 7 | Cnt: 5:PckEckcdsaHash 8 | Cnt: 6:PckRequirementsHeader 9 | Cnt: 7:CntNullBlock 10 | Cnt: 8:CntSquashFsImage 11 | Cnt: 9:CntSquashFsHashSignature 12 | Cnt: 10:CntArchitectureTag 13 | PckPreHeader 14 | Cnt id: 25 15 | Cnt offset: 8 16 | Cnt len: 6 17 | Payload len: 0 18 | Payload[0:0]: b'' [...] 19 | PckHeader 20 | Cnt id: 1 21 | Cnt offset: 14 22 | Cnt len: 42 23 | Payload len: 36 24 | Payload[0:10]: b'gps\x00\x00\x00\x00\x00\x00\x00' [...] 25 | Program name: gps 26 | Os version: 6.48.4 - rc(?): 102 27 | Created at: 2021-08-18 06:43:27 28 | NullBlock: (0, 0, 0, 0) 29 | Flags: (0, 0, 0, 0, 2, 0, 0) 30 | PckReleaseTyp 31 | Cnt id: 24 32 | Cnt offset: 56 33 | Cnt len: 12 34 | Payload len: 6 35 | Payload[0:6]: b'stable' [...] 36 | CntArchitectureTag 37 | Cnt id: 16 38 | Cnt offset: 68 39 | Cnt len: 10 40 | Payload len: 4 41 | Payload[0:4]: b'i386' [...] 42 | PckDescription 43 | Cnt id: 2 44 | Cnt offset: 78 45 | Cnt len: 31 46 | Payload len: 25 47 | Payload[0:10]: b'Provides s' [...] 48 | PckEckcdsaHash 49 | Cnt id: 23 50 | Cnt offset: 109 51 | Cnt len: 46 52 | Payload len: 40 53 | Payload[0:10]: b'589981d829' [...] 54 | PckRequirementsHeader 55 | Cnt id: 3 56 | Cnt offset: 155 57 | Cnt len: 40 58 | Payload len: 34 59 | Payload[0:10]: b'\x01\x00system\x00\x00' [...] 60 | StructID: 1 61 | Offset: 155 62 | Program name: system 63 | Null block: (0, 0, 0, 0) 64 | Os versionFrom: 6.48.4 - rc(?): 102 65 | Os versionTo: 6.48.4 - rc(?): 102 66 | Flags: 67 | CntNullBlock 68 | Cnt id: 22 69 | Cnt offset: 195 70 | Cnt len: 3895 71 | Payload len: 3889 72 | Payload[0:10]: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' [...] 73 | CntSquashFsImage 74 | Cnt id: 21 75 | Cnt offset: 4090 76 | Cnt len: 49158 77 | Payload len: 49152 78 | Payload[0:10]: b'hsqs\x15\x00\x00\x00\xdb\xac' [...] 79 | calc Sha1Hash: b'&1=\x9e\x05\xef\x19\xb218AVQ\xa2g&j5\x9c\x87' 80 | CntSquashFsHashSignature 81 | Cnt id: 9 82 | Cnt offset: 53248 83 | Cnt len: 138 84 | Payload len: 132 85 | Payload[0:10]: b'\xca\xdb\x02\xed,\xca\x13\xc2\x88J' [...] 86 | Payload[-10:]: b'7\x9dF\xf4\x14\xd3,\x16\x18\x06' 87 | CntArchitectureTag 88 | Cnt id: 16 89 | Cnt offset: 53386 90 | Cnt len: 7 91 | Payload len: 1 92 | Payload[0:1]: b'I' [...] 93 | -------------------------------------------------------------------------------- /tests/npk_test.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import tempfile 3 | import unittest 4 | from pathlib import Path 5 | 6 | from npkpy.common import NPKError, NPKIdError, NPKMagicBytesError 7 | from npkpy.npk.npk import Npk 8 | from npkpy.npk.pck_header import PckHeader 9 | from tests.constants import DummyHeaderCnt, MAGIC_BYTES, get_dummy_npk_binary 10 | 11 | 12 | class Test_npkClass(unittest.TestCase): 13 | 14 | def setUp(self) -> None: 15 | self.npkFile = Path(tempfile.NamedTemporaryFile(suffix=".npk").name) 16 | self.npkFile.write_bytes(get_dummy_npk_binary()) 17 | 18 | def test_fileIsNoNpkFile(self): 19 | self.npkFile.write_bytes(b"NoMagicBytesAtHeadOfFile") 20 | 21 | with self.assertRaises(NPKMagicBytesError) as e: 22 | _ = Npk(self.npkFile).pck_magic_bytes 23 | self.assertEqual(e.exception.args[0], "Magic bytes not found in Npk file") 24 | 25 | def test_npkFileIsCorrupt_fileCorruptException(self): 26 | self.npkFile.write_bytes(MAGIC_BYTES + b"CorruptFile") 27 | 28 | with self.assertRaises(NPKError) as e: 29 | _ = Npk(self.npkFile).pck_cnt_list 30 | self.assertEqual(e.exception.args[0], 31 | f"File maybe corrupted. Please download again. File: {self.npkFile.absolute()}") 32 | 33 | def test_extractMagicBytes(self): 34 | self.assertEqual(MAGIC_BYTES, Npk(self.npkFile).pck_magic_bytes) 35 | 36 | def test_extractLenOfNpkPayload_propagatedSizeIsValid(self): 37 | self.assertEqual(len(DummyHeaderCnt().get_binary), Npk(self.npkFile).pck_payload_len) 38 | 39 | def test_calculatePckFullSize_equalsFileSize(self): 40 | self.assertEqual(self.npkFile.stat().st_size, Npk(self.npkFile).pck_full_size) 41 | 42 | def test_getNpkBinary_equalsOriginalBinary(self): 43 | npkBinary = self.npkFile.read_bytes() 44 | 45 | self.assertEqual(npkBinary, Npk(self.npkFile).pck_full_binary) 46 | 47 | def test_getEnumeratedListOfCntInNpk(self): 48 | cntList = list(Npk(self.npkFile).pck_enumerate_cnt) 49 | cntId, cnt = cntList[0] 50 | 51 | self.assertEqual(1, len(cntList)) 52 | self.assertEqual(0, cntId) 53 | self.assertTrue(isinstance(cnt, PckHeader)) 54 | 55 | def test_getAllCnt_returnAsList(self): 56 | cntList = Npk(self.npkFile).pck_cnt_list 57 | 58 | self.assertEqual(1, len(cntList)) 59 | self.assertTrue(isinstance(cntList[0], PckHeader)) 60 | 61 | def test_getAllCnt_exceptionWithUnknownCntInNpk(self): 62 | unknownCnt = DummyHeaderCnt() 63 | unknownCnt._00_cnt_id = struct.pack("H", 999) 64 | self.npkFile.write_bytes(get_dummy_npk_binary(cnt=unknownCnt.get_binary)) 65 | 66 | with self.assertRaises(NPKIdError) as e: 67 | _ = Npk(self.npkFile).pck_cnt_list 68 | self.assertEqual(e.exception.args[0], f"Failed with cnt id: 999\n" 69 | f"New cnt id discovered in file: {self.npkFile.absolute()}") 70 | -------------------------------------------------------------------------------- /npkpy/npk/cnt_basic.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | 4 | from npkpy.common import NPKError 5 | 6 | BYTES_LEN_CNT_ID = 2 7 | BYTES_LEN_CNT_PAYLOAD_LEN = 4 8 | 9 | NPK_CNT_BASIC = -1 10 | 11 | 12 | class CntBasic: 13 | """ 14 | 0____4____8____b____f 15 | | | | | | 16 | x0_|AABB|BBCC|C ..... C| 17 | x1_|....|....|....|....| 18 | 19 | A = Container Identifier 20 | B = Payload length 21 | C = Payload 22 | """ 23 | 24 | def __init__(self, data, offset_in_pck): 25 | self._data = bytearray(data) 26 | self._offset_in_pck = offset_in_pck 27 | self.modified = False 28 | 29 | @property 30 | def _regular_cnt_id(self): 31 | return NPK_CNT_BASIC 32 | 33 | @property 34 | def cnt_id(self): 35 | cnt_id = struct.unpack_from(b"h", self._data, 0)[0] 36 | if cnt_id != self._regular_cnt_id: 37 | raise NPKError(f"Cnt object does not represent given container typ {self._regular_cnt_id}/{cnt_id}") 38 | return cnt_id 39 | 40 | @property 41 | def cnt_id_name(self): 42 | return str(self.__class__.__name__) 43 | 44 | @property 45 | def cnt_payload_len(self): 46 | return (struct.unpack_from(b"I", self._data, 2))[0] 47 | 48 | @cnt_payload_len.setter 49 | def cnt_payload_len(self, payload_len): 50 | logging.warning("[MODIFICATION] Please be aware that modifications can break the npk structure") 51 | self.modified = True 52 | struct.pack_into(b"I", self._data, 2, payload_len) 53 | 54 | @property 55 | def cnt_payload(self): 56 | return struct.unpack_from(f"{self.cnt_payload_len}s", self._data, 6)[0] 57 | 58 | @cnt_payload.setter 59 | def cnt_payload(self, payload): 60 | tmp_len = len(payload) 61 | tmp_head = self._data[:2 + 4] 62 | tmp_head += struct.pack(f"{tmp_len}s", payload) 63 | self._data = tmp_head 64 | self.cnt_payload_len = tmp_len 65 | 66 | @property 67 | def cnt_full_length(self): 68 | return BYTES_LEN_CNT_ID + BYTES_LEN_CNT_PAYLOAD_LEN + self.cnt_payload_len 69 | # return len(self._data) 70 | 71 | @property 72 | def output_cnt(self): 73 | view_len = min(10, self.cnt_payload_len) 74 | 75 | return (f"{self.cnt_id_name}", [f"Cnt id: {self.cnt_id}", 76 | f"Cnt offset: {self._offset_in_pck}", 77 | f"Cnt len: {self.cnt_full_length}", 78 | f"Payload len: {self.cnt_payload_len}", 79 | f"Payload[0:{view_len}]: {self.cnt_payload[0:view_len]} [...] " 80 | ]) 81 | 82 | @property 83 | def cnt_full_binary(self): 84 | cnt_id = self.cnt_id 85 | payload_len = self.cnt_payload_len 86 | 87 | payload = struct.unpack_from(f"{self.cnt_payload_len}s", 88 | buffer=self._data, 89 | offset=BYTES_LEN_CNT_ID + BYTES_LEN_CNT_PAYLOAD_LEN)[0] 90 | 91 | return struct.pack(b"=hI", cnt_id, payload_len) + payload 92 | -------------------------------------------------------------------------------- /npkpy/npk/pck_requirements_header.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from npkpy.npk.cnt_basic import CntBasic, BYTES_LEN_CNT_PAYLOAD_LEN, BYTES_LEN_CNT_ID 3 | 4 | NPK_REQUIREMENTS_HEADER = 3 5 | 6 | 7 | class PckRequirementsHeader(CntBasic): 8 | def _version_one_and_two(self): 9 | def check(obj): 10 | if obj.cnt_structure_id > 0: 11 | return self(obj) 12 | return "" 13 | 14 | return check 15 | 16 | def _version_two_only(self): 17 | def check(obj): 18 | if obj.cnt_structure_id > 1: 19 | return self(obj) 20 | return "" 21 | 22 | return check 23 | 24 | def __init__(self, data, offset_in_pck): 25 | super().__init__(data, offset_in_pck) 26 | self._offset = offset_in_pck 27 | 28 | @property 29 | def _regular_cnt_id(self): 30 | return NPK_REQUIREMENTS_HEADER 31 | 32 | @property 33 | def cnt_structure_id(self) -> object: 34 | return struct.unpack_from(b"H", self._data, 6)[0] 35 | 36 | @property 37 | @_version_one_and_two 38 | def cnt_program_name(self): 39 | return bytes(struct.unpack_from(b"16B", self._data, 8)).decode().rstrip('\x00') 40 | 41 | @property 42 | @_version_one_and_two 43 | def cnt_os_version_min(self): 44 | revision = (struct.unpack_from(b"B", self._data, 24))[0] 45 | unknown_subrevision = (struct.unpack_from(b"B", self._data, 25))[0] 46 | minor = (struct.unpack_from(b"B", self._data, 26))[0] 47 | major = (struct.unpack_from(b"B", self._data, 27))[0] 48 | return f"{major}.{minor}.{revision} - rc(?): {unknown_subrevision}" 49 | 50 | @property 51 | @_version_one_and_two 52 | def cnt_null_block(self): 53 | return struct.unpack_from(b"BBBB", self._data, 28) 54 | 55 | @property 56 | @_version_one_and_two 57 | def cnt_os_version_max(self): 58 | revision = (struct.unpack_from(b"B", self._data, 32))[0] 59 | unknown_subrevision = (struct.unpack_from(b"B", self._data, 33))[0] 60 | minor = (struct.unpack_from(b"B", self._data, 34))[0] 61 | major = (struct.unpack_from(b"B", self._data, 35))[0] 62 | return f"{major}.{minor}.{revision} - rc(?): {unknown_subrevision}" 63 | 64 | @property 65 | @_version_two_only 66 | def cnt_flags(self): 67 | return struct.unpack_from(b"4B", self._data, 36) 68 | 69 | @property 70 | def cnt_full_binary(self): 71 | cnt_id = self.cnt_id 72 | payload_len = self.cnt_payload_len 73 | payload = struct.unpack_from(f"{self.cnt_payload_len}s", self._data, 74 | offset=BYTES_LEN_CNT_ID + BYTES_LEN_CNT_PAYLOAD_LEN)[0] 75 | return struct.pack("=HI", cnt_id, payload_len) + payload 76 | 77 | @property 78 | def output_cnt(self): 79 | _, opt = super().output_cnt 80 | options = [f"StructID: {self.cnt_structure_id}", 81 | f"Offset: {self._offset}", 82 | f"Program name: {self.cnt_program_name}", 83 | f"Null block: {self.cnt_null_block}", 84 | f"Os versionFrom: {self.cnt_os_version_min}", 85 | f"Os versionTo: {self.cnt_os_version_max}", 86 | f"Flags: {self.cnt_flags}" 87 | ] 88 | 89 | return f"{self.cnt_id_name}", opt + options 90 | -------------------------------------------------------------------------------- /tests_acceptance_test/acceptance_test.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import tempfile 3 | import unittest 4 | from pathlib import Path 5 | 6 | 7 | class Test_npkpy(unittest.TestCase): 8 | 9 | def setUp(self) -> None: 10 | # TODO: create DummyPkg and replace gps-6.45.6.npk 11 | self.npk_file = Path("tests/testData/6_48_4/gps-6.48.4.npk") 12 | self.path_to_npk = str(self.npk_file.absolute()) 13 | self.npk_container_list = Path("tests/testData/6_48_4/gps-6.48.4.result").read_text() 14 | self.dst_folder = Path(tempfile.mkdtemp()) 15 | 16 | def tearDown(self) -> None: 17 | for _file in self.dst_folder.rglob("*"): 18 | if _file.is_file(): 19 | _file.unlink() 20 | for _file in self.dst_folder.rglob("*"): 21 | _file.rmdir() 22 | 23 | self.dst_folder.rmdir() 24 | 25 | def test_list_all_containers_from_npk_pkg(self): 26 | cmd = ["npkpy", "--file", self.path_to_npk, "--show-container"] 27 | output = run_command_in_terminal(cmd) 28 | self.assertEqual(self.npk_container_list, output) 29 | 30 | def test_list_in_folder(self): 31 | cmd = ["npkpy", "--src-folder", str(self.npk_file.parent), "--show-container"] 32 | output = run_command_in_terminal(cmd) 33 | self.assertEqual(self.npk_container_list, output) 34 | 35 | def test_export_all_container_from_npk(self): 36 | cmd = ["npkpy", "--file", self.path_to_npk, "--dst-folder", self.dst_folder.absolute(), "--export-all"] 37 | 38 | run_command_in_terminal(cmd) 39 | 40 | exported_container = sorted(str(_file.relative_to(self.dst_folder)) for _file in self.dst_folder.rglob('*')) 41 | self.assertEqual(['npkPyExport_gps-6.48.4', 42 | 'npkPyExport_gps-6.48.4/000_cnt_PckPreHeader.raw', 43 | 'npkPyExport_gps-6.48.4/001_cnt_PckHeader.raw', 44 | 'npkPyExport_gps-6.48.4/002_cnt_PckReleaseTyp.raw', 45 | 'npkPyExport_gps-6.48.4/003_cnt_CntArchitectureTag.raw', 46 | 'npkPyExport_gps-6.48.4/004_cnt_PckDescription.raw', 47 | 'npkPyExport_gps-6.48.4/005_cnt_PckEckcdsaHash.raw', 48 | 'npkPyExport_gps-6.48.4/006_cnt_PckRequirementsHeader.raw', 49 | 'npkPyExport_gps-6.48.4/007_cnt_CntNullBlock.raw', 50 | 'npkPyExport_gps-6.48.4/008_cnt_CntSquashFsImage.raw', 51 | 'npkPyExport_gps-6.48.4/009_cnt_CntSquashFsHashSignature.raw', 52 | 'npkPyExport_gps-6.48.4/010_cnt_CntArchitectureTag.raw'], exported_container) 53 | 54 | def test_extract_squashfs_container_from_npk(self): 55 | cmd = ["npkpy", "--file", self.path_to_npk, "--dst-folder", self.dst_folder.absolute(), "--export-squashfs"] 56 | 57 | run_command_in_terminal(cmd) 58 | 59 | self.assert_container_extracted(['npkPyExport_gps-6.48.4', 60 | 'npkPyExport_gps-6.48.4/008_cnt_CntSquashFsImage.raw']) 61 | 62 | def test_extract_zlib_container_from_npk_nonexisting_not_extracted(self): 63 | cmd = ["npkpy", "--file", self.path_to_npk, "--dst-folder", self.dst_folder.absolute(), "--export-zlib"] 64 | 65 | run_command_in_terminal(cmd) 66 | 67 | self.assert_container_extracted([]) 68 | 69 | def assert_container_extracted(self, expected_files): 70 | extracted_container = sorted(str(_file.relative_to(self.dst_folder)) for _file in self.dst_folder.rglob('*')) 71 | self.assertEqual(expected_files, extracted_container) 72 | 73 | 74 | def run_command_in_terminal(cmd): 75 | return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True).stdout.decode("UTF-8") 76 | -------------------------------------------------------------------------------- /npkpy/npk/npk.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from pathlib import Path 3 | 4 | from npkpy.common import NPKError, NPKIdError, NPKMagicBytesError 5 | from npkpy.npk.npk_constants import CNT_HANDLER 6 | from npkpy.npk.cnt_basic import BYTES_LEN_CNT_ID, BYTES_LEN_CNT_PAYLOAD_LEN 7 | from npkpy.npk.npk_file_basic import FileBasic 8 | 9 | MAGIC_BYTES = b"\x1e\xf1\xd0\xba" 10 | BYTES_LEN_MAGIC_HEADER = 4 11 | BYTES_LEN_PCK_SIZE_LEN = 4 12 | 13 | """ 14 | 0____4____8____b____f 15 | | | | | | 16 | 0_|AAAA|BBBB| C ..... | 17 | 1_|....|....|....|....| 18 | 19 | 20 | A = MAGIC BYTES (4) 21 | B = PCK SIZE (4) 22 | C = Begin of Container area 23 | 24 | """ 25 | 26 | 27 | class Npk(FileBasic): 28 | __cnt_list = None 29 | 30 | def __init__(self, file_path: Path): 31 | super().__init__(file_path) 32 | self.cnt_offset = 8 33 | self._data = self.read_data_from_file(offset=0, size=self.cnt_offset) 34 | self._check_magic_bytes(error_msg="Magic bytes not found in Npk file") 35 | self.pck_header = self.pck_cnt_list[0] 36 | 37 | @property 38 | def pck_magic_bytes(self): 39 | return struct.unpack_from("4s", self._data, 0)[0] 40 | 41 | @property 42 | def pck_payload_len(self): 43 | self.__pck_payload_size_update() 44 | payload_len = struct.unpack_from("I", self._data, 4)[0] 45 | return payload_len 46 | 47 | def __pck_payload_size_update(self): 48 | if any(cnt.modified for cnt in self.pck_cnt_list): 49 | current_size = 0 50 | for cnt in self.pck_cnt_list: 51 | current_size += cnt.cnt_full_length 52 | cnt.modified = False 53 | struct.pack_into("I", self._data, 4, current_size) 54 | 55 | @property 56 | def pck_full_size(self): 57 | return BYTES_LEN_MAGIC_HEADER + BYTES_LEN_PCK_SIZE_LEN + self.pck_payload_len 58 | 59 | @property 60 | def pck_full_binary(self): 61 | binary = MAGIC_BYTES + struct.pack("I", self.pck_payload_len) 62 | for cnt in self.pck_cnt_list: 63 | binary += cnt.cnt_full_binary 64 | return binary 65 | 66 | @property 67 | def pck_enumerate_cnt(self): 68 | for pos, cnt in enumerate(self.pck_cnt_list): 69 | yield pos, cnt 70 | 71 | @property 72 | def pck_cnt_list(self): 73 | if not self.__cnt_list: 74 | self.__cnt_list = self.__parse_all_cnt() 75 | return self.__cnt_list 76 | 77 | def __parse_all_cnt(self): 78 | lst = [] 79 | offset = self.cnt_offset 80 | while offset < self.file.stat().st_size - 1: 81 | lst.append(self.__get_cnt(offset)) 82 | offset += BYTES_LEN_CNT_ID + BYTES_LEN_CNT_PAYLOAD_LEN + lst[-1].cnt_payload_len 83 | return lst 84 | 85 | def __get_cnt(self, offset): 86 | cnt_id = struct.unpack_from("H", self.read_data_from_file(offset, 2))[0] 87 | payload_len = struct.unpack_from("I", self.read_data_from_file(offset + BYTES_LEN_CNT_ID, 4))[0] 88 | pkt_len = BYTES_LEN_CNT_ID + BYTES_LEN_CNT_PAYLOAD_LEN + payload_len 89 | 90 | data = self.read_data_from_file(offset, pkt_len) 91 | if len(data) != pkt_len: 92 | raise NPKError(f"File maybe corrupted. Please download again. File: {self.file.absolute()}") 93 | try: 94 | return CNT_HANDLER[cnt_id](data, offset) 95 | except KeyError as e: 96 | raise NPKIdError(f"Failed with cnt id: {cnt_id}\n" 97 | f"New cnt id discovered in file: {self.file.absolute()}") from e 98 | 99 | 100 | def _check_magic_bytes(self, error_msg): 101 | if not self.pck_magic_bytes == MAGIC_BYTES: 102 | raise NPKMagicBytesError(error_msg) 103 | -------------------------------------------------------------------------------- /tools/npkModify_test.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | 5 | from tests.constants import MINIMAL_NPK_PACKAGE 6 | from npkpy.npk.npk import Npk 7 | 8 | 9 | class BasicNpkTestRequirements(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.testNpk = Path(tempfile.NamedTemporaryFile().name) 12 | self.testNpk.write_bytes(MINIMAL_NPK_PACKAGE) 13 | self.npk = Npk(self.testNpk) 14 | 15 | def tearDown(self) -> None: 16 | self.testNpk.unlink() 17 | 18 | 19 | class ParseTestPackage_Test(BasicNpkTestRequirements): 20 | def test_minimalNPK_parseHeader(self): 21 | self.assertEqual(b"\x1e\xf1\xd0\xba", self.npk.pck_magic_bytes) 22 | self.assertEqual(28, self.npk.pck_payload_len) 23 | 24 | def test_minimalNPK_parseAllContainer(self): 25 | listOfCnt = self.npk.pck_cnt_list 26 | 27 | self.assertEqual(1, listOfCnt[0].cnt_id) 28 | self.assertEqual(15, listOfCnt[0].cnt_payload_len) 29 | self.assertEqual(b"NAME OF PROGRAM", listOfCnt[0].cnt_payload) 30 | self.assertEqual(1, listOfCnt[1].cnt_id) 31 | self.assertEqual(1, listOfCnt[1].cnt_payload_len) 32 | self.assertEqual(b"I", listOfCnt[1].cnt_payload) 33 | 34 | 35 | class ModifyPayload_Test(BasicNpkTestRequirements): 36 | 37 | def setUp(self) -> None: 38 | super().setUp() 39 | self.cnt = self.npk.pck_cnt_list[0] 40 | 41 | def test_emptyPayload_emptyContainer(self): 42 | self.cnt.cnt_payload = b"" 43 | 44 | self.assertEqual(1, self.cnt.cnt_id) 45 | self.assertEqual(0, self.cnt.cnt_payload_len) 46 | self.assertEqual(b"", self.cnt.cnt_payload) 47 | self.assertEqual(6, self.cnt.cnt_full_length) 48 | 49 | self.assertEqual(13, self.npk.pck_payload_len) 50 | self.assertEqual(21, self.npk.pck_full_size) 51 | 52 | def test_dontchangePayloadSize_recalculateContainerKeepSize(self): 53 | self.cnt.cnt_payload = b"PROGRAM OF NAME" 54 | 55 | self.assertEqual(1, self.cnt.cnt_id) 56 | self.assertEqual(15, self.cnt.cnt_payload_len) 57 | self.assertEqual(b"PROGRAM OF NAME", self.cnt.cnt_payload) 58 | self.assertEqual(21, self.cnt.cnt_full_length) 59 | 60 | self.assertEqual(28, self.npk.pck_payload_len) 61 | self.assertEqual(36, self.npk.pck_full_size) 62 | 63 | def test_increasePayloadLen_recalculateContainerSizeBigger(self): 64 | self.cnt.cnt_payload = b"NEW NAME OF PROGRAM" 65 | 66 | self.assertEqual(1, self.cnt.cnt_id) 67 | self.assertEqual(19, self.cnt.cnt_payload_len) 68 | self.assertEqual(b"NEW NAME OF PROGRAM", self.cnt.cnt_payload) 69 | self.assertEqual(25, self.cnt.cnt_full_length) 70 | 71 | self.assertEqual(32, self.npk.pck_payload_len) 72 | self.assertEqual(40, self.npk.pck_full_size) 73 | 74 | def test_decreasePayloadLen_recalculateContainerSmaller(self): 75 | self.cnt.cnt_payload = b"SHORT NAME" 76 | 77 | self.assertEqual(1, self.cnt.cnt_id) 78 | self.assertEqual(10, self.cnt.cnt_payload_len) 79 | self.assertEqual(b"SHORT NAME", self.cnt.cnt_payload) 80 | self.assertEqual(16, self.cnt.cnt_full_length) 81 | 82 | self.assertEqual(23, self.npk.pck_payload_len) 83 | self.assertEqual(31, self.npk.pck_full_size) 84 | 85 | 86 | class WriteModifiedFile_Test(BasicNpkTestRequirements): 87 | def setUp(self) -> None: 88 | super().setUp() 89 | self.cnt = self.npk.pck_cnt_list[0] 90 | 91 | def test_createFile_withoutModification(self): 92 | self.assertEqual(MINIMAL_NPK_PACKAGE, self.npk.pck_full_binary) 93 | 94 | def test_createFile_changePayloadTwice(self): 95 | self.cnt.cnt_payload = b"A" 96 | self.cnt.cnt_payload = b"NAME OF PROGRAM" 97 | self.assertEqual(MINIMAL_NPK_PACKAGE, self.npk.pck_full_binary) 98 | -------------------------------------------------------------------------------- /tools/download_all_packages_test.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | from zipfile import ZipFile 5 | 6 | from pytest_httpserver import HTTPServer 7 | from urlpath import URL 8 | 9 | from tools.download_all_packages import fetchWebsite, extractDownloadLinks, filterLinks, \ 10 | unpackFile 11 | 12 | 13 | class Test_MikroTikDownloader(unittest.TestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls) -> None: 17 | print("setup Class!") 18 | cls.s = HTTPServer() 19 | cls.s.start() 20 | cls.testUrl = f"http://localhost:{cls.s.port}" 21 | 22 | # cls.s.expect_request("/download/archive").respond_with_data(createArchiveWebsite(cls.testUrl)) 23 | # cls.s.expect_request("/routeros/6.36.4/all_packages-mipsbe-6.36.4.zip").respond_with_data(b"data", 24 | 25 | # content_type="application/zip") 26 | 27 | @classmethod 28 | def tearDownClass(cls) -> None: 29 | cls.s.stop() 30 | 31 | def setUp(self) -> None: 32 | self.s.clear() 33 | 34 | def test_downloadWebsite(self): 35 | self.s.expect_request("/").respond_with_data(b"testData") 36 | 37 | result = fetchWebsite(f"http://localhost:{self.s.port}") 38 | 39 | self.assertEqual("testData", result) 40 | 41 | def test_extractDownloadLinks(self): 42 | testData = "dummyTest \n" 43 | testData += "program-arch-version.zip>\n" 44 | testData += "dummyTest \n" 45 | 46 | result = extractDownloadLinks(testData, "filterString.com") 47 | 48 | self.assertEqual([URL("//filterString.com/prod/vers/programA-arch-version.zip")], list(result)) 49 | 50 | def test_filterForSpecificLinks_basedOnSuffix(self): 51 | links = [URL("//filterString.com/prod/vers/programA-arch-version.zip"), 52 | URL("//filterString.com/prod/vers/programB-arch-version.WRONGSUFFIX")] 53 | 54 | result = filterLinks(links, [("program", ".zip")]) 55 | 56 | self.assertEqual([URL("//filterString.com/prod/vers/programA-arch-version.zip")], list(result)) 57 | 58 | def test_filterForSpecificLinks_basedOnProgram(self): 59 | links = [URL("//filterString.com/prod/vers/programA-arch-version.exe"), 60 | URL("//filterString.com/prod/vers/programB-arch-version.zip")] 61 | 62 | result = filterLinks(links, ([("programB", ".zip")])) 63 | 64 | self.assertEqual([URL("//filterString.com/prod/vers/programB-arch-version.zip")], list(result)) 65 | 66 | def test_dontExtractZip_fileDontExist(self): 67 | file = Path(tempfile.NamedTemporaryFile(suffix="WRONGSUFFIX").name) 68 | 69 | unpackFile(file) 70 | 71 | def test_dontExtractZip_fileHasNoZipSuffix(self): 72 | file = Path(tempfile.NamedTemporaryFile(suffix="WRONGSUFFIX").name) 73 | file.touch() 74 | 75 | unpackFile(file) 76 | 77 | def test_extractZip_validatePayload(self): 78 | zipFile, origTxtFile = _createTxtInZipFile(txtPayload="TEST DATA") 79 | unzipedFile = Path(f"{origTxtFile.parent}/{origTxtFile.absolute()}") 80 | 81 | unpackFile(zipFile) 82 | 83 | self.assertTrue(zipFile.exists()) 84 | self.assertTrue(unzipedFile.exists()) 85 | self.assertEqual("TEST DATA", unzipedFile.read_text()) 86 | 87 | 88 | def _createTxtInZipFile(txtPayload): 89 | txtFile = Path(tempfile.NamedTemporaryFile(suffix=".txt").name) 90 | zipFile = Path(tempfile.NamedTemporaryFile(suffix=".zip").name) 91 | with txtFile.open("w") as f: 92 | f.write(txtPayload) 93 | with ZipFile(zipFile, 'w') as zipObj: 94 | zipObj.write(txtFile.absolute()) 95 | return zipFile, txtFile 96 | 97 | 98 | def createArchiveWebsite(testUrl): 99 | return f""" 100 | 101 |
102 |
all_packages-mipsbe-6.36.4> 103 | mipsbe 104 | 105 | """ 106 | -------------------------------------------------------------------------------- /tools/download_all_packages.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | import urllib.request 4 | import zipfile 5 | from datetime import datetime 6 | from pathlib import Path 7 | from time import sleep 8 | from urllib.error import HTTPError, URLError 9 | from zipfile import ZipFile 10 | 11 | from urlpath import URL 12 | 13 | 14 | class MikroTikDownloader: 15 | 16 | def __init__(self, dstPath): 17 | 18 | self.dstPath = Path(dstPath) 19 | self.sleepTime = 1 20 | socket.setdefaulttimeout(10) 21 | 22 | def downloadAll(self, urls): 23 | missingFiles = self._determineMissingFiles(urls) 24 | for link in missingFiles: 25 | unpackFile(self._downloadFile(link)) 26 | 27 | def _downloadFile(self, url): 28 | def _progress(count, block_size, total_size): 29 | progress = (float(count * block_size) / float(total_size) * 100.0) 30 | if int(progress) > 100: 31 | raise RuntimeError("AccessDenied") 32 | sys.stderr.write(self._msg(url) + ': [%.1f%%] ' % progress) 33 | sys.stderr.flush() 34 | 35 | targetFile = self._convertToLocalFilePath(url) 36 | targetFile.parent.mkdir(exist_ok=True, parents=True) 37 | 38 | while True: 39 | try: 40 | urllib.request.urlretrieve(f"http:{url.resolve()}", targetFile, _progress) 41 | self.msg(url, "[100.0%]") 42 | self.dec_sleep() 43 | break 44 | except HTTPError as e: 45 | self.msg(url, f"[failed] (Error code: {e.code})") 46 | break 47 | except socket.timeout: 48 | self.msg(url, "[failed] (Timeout)") 49 | self.inc_sleep() 50 | except RuntimeError as e: 51 | self.msg(url, f"[failed] ([{e}])") 52 | if targetFile.exists(): 53 | targetFile.unlink() 54 | break 55 | except URLError as e: 56 | self.msg(url, f"[failed] (Error code: {e})") 57 | self.inc_sleep() 58 | 59 | return targetFile 60 | 61 | def inc_sleep(self): 62 | sleep(self.sleepTime) 63 | self.sleepTime += 1 64 | 65 | def dec_sleep(self): 66 | if self.sleepTime > 1: 67 | self.sleepTime -= 1 68 | 69 | def _determineMissingFiles(self, urls): 70 | for url in urls: 71 | if not self._convertToLocalFilePath(url).exists(): 72 | yield url 73 | else: 74 | self.msg(url, "[ok]") 75 | 76 | def msg(self, url, msg): 77 | print(f"{self._msg(url)}: {msg}") 78 | 79 | def _msg(self, url): 80 | return f"\r[{datetime.now().time()}] Download {url.name:35}" 81 | 82 | def _convertToLocalFilePath(self, link: URL): 83 | pktName = arc = version = "" 84 | elements = link.stem.split("-") 85 | count = len(elements) 86 | if count == 2: 87 | pktName, version = elements 88 | if count == 3: 89 | pktName, arc, version = elements 90 | return self.dstPath / pktName / arc / version / link.name 91 | 92 | 93 | def fetchWebsite(url): 94 | with urllib.request.urlopen(url) as response: 95 | return response.read().decode("utf-8") 96 | 97 | 98 | def extractDownloadLinks(tHtml, filterString): 99 | return (URL(line.split('"')[1]) for line in tHtml.split() if filterString in line) 100 | 101 | 102 | def filterLinks(tLinks, filters): 103 | return (link for link in tLinks if 104 | any(True for name, suffix in filters 105 | if name in link.stem and 106 | suffix in link.suffix 107 | ) 108 | ) 109 | 110 | 111 | def unpackFile(file: Path): 112 | if file.exists() and file.suffix == ".zip": 113 | try: 114 | with ZipFile(file, 'r') as zipObj: 115 | zipObj.extractall(file.parent) 116 | except zipfile.BadZipFile: 117 | file.unlink() 118 | 119 | 120 | if __name__ == "__main__": 121 | html = fetchWebsite("https://mikrotik.com/download/archive") 122 | links = extractDownloadLinks(html, "download.mikrotik.com") 123 | selectedLinks = filterLinks(links, [("all_packages", ".zip"), 124 | (".", ".npk") 125 | ] 126 | ) 127 | mtd = MikroTikDownloader("./downloads") 128 | mtd.downloadAll(selectedLinks) 129 | -------------------------------------------------------------------------------- /tests/cnt_basic_test.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import unittest 3 | 4 | from npkpy.common import NPKError 5 | from npkpy.npk.cnt_basic import CntBasic 6 | from tests.constants import get_dummy_basic_cnt 7 | 8 | 9 | class Test_CntBasic(unittest.TestCase): 10 | 11 | def setUp(self) -> None: 12 | self.cnt = CntBasic(data=get_dummy_basic_cnt(), offset_in_pck=0) 13 | 14 | def test_extractCntId(self): 15 | self.assertEqual(-1, self.cnt.cnt_id) 16 | 17 | def test_failForWrongCntId(self): 18 | cnt = CntBasic(get_dummy_basic_cnt(cnt_id=999), offset_in_pck=0) 19 | with self.assertRaises(NPKError) as _exception: 20 | _ = cnt.cnt_id 21 | self.assertEqual("Cnt object does not represent given container typ -1/999", _exception.exception.args[0]) 22 | 23 | def test_getNameOfContainerType(self): 24 | self.assertEqual("CntBasic", self.cnt.cnt_id_name) 25 | 26 | def test_extractPayloadLen(self): 27 | self.assertEqual(7, self.cnt.cnt_payload_len) 28 | 29 | def test_extractPayload(self): 30 | self.assertEqual(b"Payload", self.cnt.cnt_payload) 31 | 32 | def test_extractCntFromGivenOffset(self): 33 | self.assertEqual(len(get_dummy_basic_cnt()), self.cnt.cnt_full_length) 34 | 35 | def test_giveOverviewOfCnt(self): 36 | expected_result = ('CntBasic', ['Cnt id: -1', 37 | 'Cnt offset: 0', 38 | 'Cnt len: 13', 39 | 'Payload len: 7', 40 | "Payload[0:7]: b'Payload' [...] "]) 41 | self.assertEqual(expected_result, self.cnt.output_cnt) 42 | 43 | def test_getFullBinaryOfContainer(self): 44 | self.assertEqual(get_dummy_basic_cnt(), self.cnt.cnt_full_binary) 45 | 46 | 47 | class Test_modifyNpkContainerBasic(unittest.TestCase): 48 | def setUp(self) -> None: 49 | self.cnt = CntBasic(get_dummy_basic_cnt(), offset_in_pck=0) 50 | 51 | def test_increaseCntSize(self): 52 | orig_cnt_full_length = self.cnt.cnt_full_length 53 | orig_payload_len = self.cnt.cnt_payload_len 54 | 55 | self.cnt.cnt_payload_len += 3 56 | 57 | self.assertEqual(7, orig_payload_len) 58 | self.assertEqual(10, self.cnt.cnt_payload_len) 59 | self.assertEqual(13, orig_cnt_full_length) 60 | self.assertEqual(16, self.cnt.cnt_full_length) 61 | 62 | def test_decreaseCntSize(self): 63 | orig_cnt_full_length = self.cnt.cnt_full_length 64 | orig_payload_len = self.cnt.cnt_payload_len 65 | 66 | self.cnt.cnt_payload_len -= 4 67 | 68 | self.assertEqual(7, orig_payload_len) 69 | self.assertEqual(13, orig_cnt_full_length) 70 | self.assertEqual(3, self.cnt.cnt_payload_len) 71 | self.assertEqual(9, self.cnt.cnt_full_length) 72 | 73 | def test_failAccessPayloadAfterIncreasingPayloadLenField(self): 74 | orig_payload_len = self.cnt.cnt_payload_len 75 | self.cnt.cnt_payload_len += 3 76 | 77 | self.assertEqual(orig_payload_len + 3, self.cnt.cnt_payload_len) 78 | with self.assertRaises(struct.error): 79 | _ = self.cnt.cnt_payload 80 | 81 | def test_decreasingPayloadLen_fieldDecreaseFullCntLenAndPayload(self): 82 | orig_cnt_full_length = self.cnt.cnt_full_length 83 | orig_payload_len = self.cnt.cnt_payload_len 84 | 85 | self.cnt.cnt_payload_len -= 4 86 | 87 | self.assertEqual(orig_payload_len - 4, self.cnt.cnt_payload_len) 88 | self.assertEqual(orig_cnt_full_length - 4, self.cnt.cnt_full_length) 89 | self.assertEqual(b"Pay", self.cnt.cnt_payload) 90 | 91 | def test_failDecreasingPayloadLen_fieldBelowZero(self): 92 | with self.assertRaises(struct.error) as _exception: 93 | self.cnt.cnt_payload_len -= 8 94 | self.assertEqual("argument out of range", _exception.exception.args[0]) 95 | 96 | def test_increasePayload_updatePayloadLen(self): 97 | replace_payload = b"NewTestPayload" 98 | 99 | self.cnt.cnt_payload = replace_payload 100 | 101 | self.assertEqual(replace_payload, self.cnt.cnt_payload) 102 | self.assertEqual(len(replace_payload), self.cnt.cnt_payload_len) 103 | 104 | def test_decreasePayload_updatePayloadLen(self): 105 | replace_payload = b"New" 106 | 107 | self.cnt.cnt_payload = replace_payload 108 | self.assertEqual(replace_payload, self.cnt.cnt_payload) 109 | self.assertEqual(len(replace_payload), self.cnt.cnt_payload_len) 110 | 111 | def test_nullPayloadUpdate_payloadLenToZero(self): 112 | replace_payload = b"" 113 | 114 | self.cnt.cnt_payload = replace_payload 115 | self.assertEqual(replace_payload, self.cnt.cnt_payload) 116 | self.assertEqual(len(replace_payload), self.cnt.cnt_payload_len) 117 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from npkpy.npk.pck_header import NPK_PCK_HEADER 4 | 5 | MAGIC_BYTES = b"\x1e\xf1\xd0\xba" 6 | 7 | PCKSIZE = struct.pack("I", 28) 8 | MAGIC_AND_SIZE = MAGIC_BYTES + PCKSIZE 9 | 10 | # OPENING ARCHITECTURE_TAG 11 | SET_HEADER_TAG_ID = struct.pack("H", NPK_PCK_HEADER) # b"\x01\x00" 12 | SET_HEADER_TAG_PAYLOAD = bytes("NAME OF PROGRAM".encode()) 13 | SET_HEADER_TAG_SIZE = struct.pack("I", len(SET_HEADER_TAG_PAYLOAD)) 14 | 15 | CNT_SET_ARCHITECTURE_TAG = SET_HEADER_TAG_ID + \ 16 | SET_HEADER_TAG_SIZE + \ 17 | SET_HEADER_TAG_PAYLOAD 18 | 19 | # CLOSING ARCHITECTURE_TAG 20 | CLOSING_ARCHITECTURE_TAG_ID = struct.pack("H", 1) # b"\x01\x00" 21 | CLOSING_ARCHITECTURE_TAG_PAYLOAD = struct.pack("s", b"I") 22 | CLOSING_ARCHITECTURE_TAG_SIZE = struct.pack("I", len(CLOSING_ARCHITECTURE_TAG_PAYLOAD)) 23 | 24 | CNT_CLOSING_ARCHITECTURE_TAG = CLOSING_ARCHITECTURE_TAG_ID + \ 25 | CLOSING_ARCHITECTURE_TAG_SIZE + \ 26 | CLOSING_ARCHITECTURE_TAG_PAYLOAD 27 | 28 | # MINIMAL_NPK_PAKAGE 29 | MINIMAL_NPK_PACKAGE = MAGIC_AND_SIZE + \ 30 | CNT_SET_ARCHITECTURE_TAG + \ 31 | CNT_CLOSING_ARCHITECTURE_TAG 32 | 33 | 34 | def get_dummy_npk_binary(cnt=None): 35 | if not cnt: 36 | cnt = DummyHeaderCnt().get_binary 37 | pckPayload = cnt 38 | pckLen = struct.pack("I", len(pckPayload)) 39 | npkBinary = MAGIC_BYTES + pckLen + pckPayload 40 | return npkBinary 41 | 42 | 43 | class DummyHeaderCnt: 44 | _00_cnt_id = struct.pack("H", 1) 45 | _01_cnt_payload_len = struct.pack("I", 35) 46 | _02_cnt_programName = struct.pack("16s", b"01234567890abcdef") 47 | _03_cnt_versionRevision = struct.pack("B", 3) 48 | _04_cnt_versionRc = struct.pack("B", 4) 49 | _05_cnt_versionMinor = struct.pack("B", 2) 50 | _06_cnt_versionMajor = struct.pack("B", 1) 51 | _07_cnt_buildTime = struct.pack("I", 1) 52 | _08_cnt_nullBock = struct.pack("I", 0) 53 | _09a_cnt_flagsA = struct.pack("7B", 0, 0, 0, 0, 0, 0, 0) 54 | _09b_cnt_flagsB = struct.pack("4B", 0, 0, 0, 0) 55 | 56 | _02_payload = _02_cnt_programName + \ 57 | _03_cnt_versionRevision + \ 58 | _04_cnt_versionRc + \ 59 | _05_cnt_versionMinor + \ 60 | _06_cnt_versionMajor + \ 61 | _07_cnt_buildTime + \ 62 | _08_cnt_nullBock + \ 63 | _09a_cnt_flagsA 64 | 65 | _02_payloadSpecialFlag = _02_cnt_programName + \ 66 | _03_cnt_versionRevision + \ 67 | _04_cnt_versionRc + \ 68 | _05_cnt_versionMinor + \ 69 | _06_cnt_versionMajor + \ 70 | _07_cnt_buildTime + \ 71 | _08_cnt_nullBock + \ 72 | _09b_cnt_flagsB 73 | 74 | @property 75 | def get_binary(self): 76 | return self._00_cnt_id + \ 77 | self._01_cnt_payload_len + \ 78 | self._02_payload 79 | 80 | @property 81 | def get_binary_with_special_flags(self): 82 | return self._00_cnt_id + \ 83 | self._01_cnt_payload_len + \ 84 | self._02_payloadSpecialFlag 85 | 86 | 87 | # pylint: disable=too-many-locals 88 | def get_dummy_requirements_header(structId): 89 | _00_cnt_id = struct.pack("H", 3) 90 | _01_cnt_payload_len = struct.pack("I", 35) 91 | _02_cnt_struct_id = struct.pack("H", 0) 92 | _03_cnt_program_name = struct.pack("16s", b"abcdefghijklmnop") 93 | 94 | _04_cnt_min_versionRevision = struct.pack("B", 3) 95 | _05_cnt_min_versionRc = struct.pack("B", 4) 96 | _06_cnt_min_versionMinor = struct.pack("B", 2) 97 | _07_cnt_min_versionMajor = struct.pack("B", 1) 98 | 99 | _08_cnt_nullBock = struct.pack("I", 0) 100 | 101 | _09_cnt_max_versionRevision = struct.pack("B", 7) 102 | _10_cnt_max_versionRc = struct.pack("B", 8) 103 | _11_cnt_max_versionMinor = struct.pack("B", 6) 104 | _12_cnt_max_versionMajor = struct.pack("B", 5) 105 | 106 | _13_cnt_flags = struct.pack("5B", 0, 0, 0, 0, 0) 107 | 108 | def _build_payload(): 109 | return (_02_cnt_struct_id + 110 | _03_cnt_program_name + 111 | _04_cnt_min_versionRevision + 112 | _05_cnt_min_versionRc + 113 | _06_cnt_min_versionMinor + 114 | _07_cnt_min_versionMajor + 115 | _08_cnt_nullBock + 116 | _09_cnt_max_versionRevision + 117 | _10_cnt_max_versionRc + 118 | _11_cnt_max_versionMinor + 119 | _12_cnt_max_versionMajor + 120 | _13_cnt_flags 121 | ) 122 | 123 | _02_cnt_struct_id = struct.pack(b"H", structId) 124 | return (_00_cnt_id + _01_cnt_payload_len + _build_payload()) 125 | 126 | 127 | def get_dummy_basic_cnt(cnt_id=-1): 128 | _00_cnt_id = struct.pack("h", cnt_id) 129 | _02_cnt_payload = struct.pack("7s", b"Payload") 130 | _01_cnt_payload_len = struct.pack("I", len(_02_cnt_payload)) 131 | return _00_cnt_id + _01_cnt_payload_len + _02_cnt_payload 132 | -------------------------------------------------------------------------------- /tests/pck_requirements_header_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from npkpy.npk.pck_requirements_header import PckRequirementsHeader 4 | from tests.constants import get_dummy_requirements_header 5 | 6 | 7 | class Test_pktRequirementsHeader(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.cnt = createRequirementsHeaderCnt(structId=0) 10 | 11 | def test_validateCntId(self): 12 | self.assertEqual(3, self.cnt.cnt_id) 13 | 14 | def test_getCntStructId(self): 15 | self.assertEqual(0, createRequirementsHeaderCnt(structId=0).cnt_structure_id) 16 | self.assertEqual(1, createRequirementsHeaderCnt(structId=1).cnt_structure_id) 17 | self.assertEqual(2, createRequirementsHeaderCnt(structId=2).cnt_structure_id) 18 | 19 | 20 | class Test_pktRequirementsHeader_StructIdZero(unittest.TestCase): 21 | def setUp(self) -> None: 22 | self.cnt = createRequirementsHeaderCnt(structId=0) 23 | 24 | def test_getCntFlags(self): 25 | self.assertEqual("", self.cnt.cnt_flags) 26 | 27 | def test_getProgramName_NotAvailableForVersionZero(self): 28 | self.assertEqual("", self.cnt.cnt_program_name) 29 | 30 | def test_getOsVersionMin_NotAvailableForVersionZero(self): 31 | self.assertEqual("", self.cnt.cnt_os_version_min) 32 | 33 | def test_getNullBlock_NotAvailableForVersionZero(self): 34 | self.assertEqual("", self.cnt.cnt_null_block) 35 | 36 | def test_getOsVersionMax_NotAvailableForVersionZero(self): 37 | self.assertEqual("", self.cnt.cnt_os_version_max) 38 | 39 | def test_getFlags_NotAvailableForVersionZero(self): 40 | self.assertEqual("", self.cnt.cnt_flags) 41 | 42 | def test_FullBinary(self): 43 | self.assertEqual(get_dummy_requirements_header(structId=0), self.cnt.cnt_full_binary) 44 | 45 | def test_getOutput(self): 46 | self.assertEqual(('PckRequirementsHeader', 47 | ['Cnt id: 3', 48 | 'Cnt offset: 0', 49 | 'Cnt len: 41', 50 | 'Payload len: 35', 51 | "Payload[0:10]: b'\\x00\\x00abcdefgh' [...] ", 52 | 'StructID: 0', 53 | 'Offset: 0', 54 | 'Program name: ', 55 | 'Null block: ', 56 | 'Os versionFrom: ', 57 | 'Os versionTo: ', 58 | 'Flags: ']), self.cnt.output_cnt) 59 | 60 | 61 | class Test_pktRequirementsHeader_StructIdOne(unittest.TestCase): 62 | def setUp(self) -> None: 63 | self.cnt = createRequirementsHeaderCnt(structId=1) 64 | 65 | def test_getProgramName(self): 66 | self.assertEqual("abcdefghijklmnop", self.cnt.cnt_program_name) 67 | 68 | def test_getOsVersionMin(self): 69 | self.assertEqual("1.2.3 - rc(?): 4", self.cnt.cnt_os_version_min) 70 | 71 | def test_getNullBlock(self): 72 | self.assertEqual((0, 0, 0, 0), self.cnt.cnt_null_block) 73 | 74 | def test_getOsVersionMax(self): 75 | self.assertEqual("5.6.7 - rc(?): 8", self.cnt.cnt_os_version_max) 76 | 77 | def test_getFlags(self): 78 | self.assertEqual("", self.cnt.cnt_flags) 79 | 80 | def test_FullBinary(self): 81 | self.assertEqual(get_dummy_requirements_header(structId=1), self.cnt.cnt_full_binary) 82 | 83 | def test_getOutput(self): 84 | self.assertEqual(('PckRequirementsHeader', 85 | ['Cnt id: 3', 86 | 'Cnt offset: 0', 87 | 'Cnt len: 41', 88 | 'Payload len: 35', 89 | "Payload[0:10]: b'\\x01\\x00abcdefgh' [...] ", 90 | 'StructID: 1', 91 | 'Offset: 0', 92 | 'Program name: abcdefghijklmnop', 93 | 'Null block: (0, 0, 0, 0)', 94 | 'Os versionFrom: 1.2.3 - rc(?): 4', 95 | 'Os versionTo: 5.6.7 - rc(?): 8', 96 | 'Flags: ']), self.cnt.output_cnt) 97 | 98 | 99 | class Test_pktRequirementsHeader_StructIdTwo(unittest.TestCase): 100 | def setUp(self) -> None: 101 | self.cnt = createRequirementsHeaderCnt(structId=2) 102 | 103 | def test_getProgramName(self): 104 | self.assertEqual("abcdefghijklmnop", self.cnt.cnt_program_name) 105 | 106 | def test_getOsVersionMin(self): 107 | self.assertEqual("1.2.3 - rc(?): 4", self.cnt.cnt_os_version_min) 108 | 109 | def test_getNullBlock(self): 110 | self.assertEqual((0, 0, 0, 0), self.cnt.cnt_null_block) 111 | 112 | def test_getOsVersionMax(self): 113 | self.assertEqual("5.6.7 - rc(?): 8", self.cnt.cnt_os_version_max) 114 | 115 | def test_getFlags(self): 116 | self.assertEqual((0, 0, 0, 0), self.cnt.cnt_flags) 117 | 118 | def test_FullBinary(self): 119 | self.assertEqual(get_dummy_requirements_header(structId=2), self.cnt.cnt_full_binary) 120 | 121 | def test_getOutput(self): 122 | self.assertEqual(('PckRequirementsHeader', 123 | ['Cnt id: 3', 124 | 'Cnt offset: 0', 125 | 'Cnt len: 41', 126 | 'Payload len: 35', 127 | "Payload[0:10]: b'\\x02\\x00abcdefgh' [...] ", 128 | 'StructID: 2', 129 | 'Offset: 0', 130 | 'Program name: abcdefghijklmnop', 131 | 'Null block: (0, 0, 0, 0)', 132 | 'Os versionFrom: 1.2.3 - rc(?): 4', 133 | 'Os versionTo: 5.6.7 - rc(?): 8', 134 | 'Flags: (0, 0, 0, 0)']), self.cnt.output_cnt) 135 | 136 | 137 | def createRequirementsHeaderCnt(structId): 138 | return PckRequirementsHeader(get_dummy_requirements_header(structId), offset_in_pck=0) 139 | -------------------------------------------------------------------------------- /tests/common_test.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | from unittest.mock import Mock 5 | 6 | from npkpy.common import get_short_pkt_info, get_short_cnt_info, get_all_nkp_files, write_to_file, extract_container, \ 7 | get_full_cnt_info, get_full_pkt_info, sha1_sum_from_binary, sha1_sum_from_file 8 | from npkpy.npk.cnt_basic import CntBasic 9 | from npkpy.npk.npk import Npk 10 | from npkpy.npk.pck_header import NPK_PCK_HEADER 11 | from tests.constants import get_dummy_npk_binary, DummyHeaderCnt, get_dummy_basic_cnt 12 | 13 | 14 | class Test_findNpkFiles(unittest.TestCase): 15 | 16 | def setUp(self) -> None: 17 | self.tmpPath = Path(tempfile.TemporaryDirectory().name) 18 | self.tmpPath.mkdir(parents=True) 19 | self.expectedFiles = [] 20 | 21 | def tearDown(self) -> None: 22 | for file in self.tmpPath.rglob("*"): 23 | if file.is_file(): 24 | file.unlink() 25 | for folder in sorted(self.tmpPath.rglob("*"), key=lambda _path: str(_path.absolute()).count("/"), reverse=True): 26 | folder.rmdir() 27 | self.tmpPath.rmdir() 28 | 29 | def test_findMultipleNpkFiles_inFolder(self): 30 | self.addExistingFiles(["fileA.npk", "fileB.npk", "fileC.npk"]) 31 | 32 | self.assertExistingFiles(sorted(get_all_nkp_files(self.tmpPath))) 33 | 34 | def test_findMultipleNpkFilesRecursive(self): 35 | self.addExistingFiles(["fileA.npk", "subB/fileB.npk", "subB/subC/fileC.npk"]) 36 | 37 | self.assertExistingFiles(sorted(get_all_nkp_files(self.tmpPath))) 38 | 39 | def test_selectOnlyNpkFiles(self): 40 | self.addExistingFiles(["fileA.npk", "fileB.exe", "fileC.txt"]) 41 | 42 | self.expectedFiles = [self.tmpPath / "fileA.npk"] 43 | 44 | self.assertExistingFiles(sorted(get_all_nkp_files(self.tmpPath))) 45 | 46 | def test_globOnlyNpkFilesFittingPattern(self): 47 | self.addExistingFiles(["fi__pattern__leA.npk", "fileB.npk", "fi__pattern__leC.exe", "fileD.exe"]) 48 | 49 | self.expectedFiles = [self.tmpPath / "fi__pattern__leA.npk"] 50 | 51 | self.assertExistingFiles(sorted(get_all_nkp_files(self.tmpPath, contain_str="__pattern__"))) 52 | 53 | def assertExistingFiles(self, result): 54 | self.assertEqual(self.expectedFiles, result) 55 | 56 | def addExistingFiles(self, files): 57 | for file in files: 58 | f = self.tmpPath / file 59 | f.parent.mkdir(parents=True, exist_ok=True) 60 | f.touch() 61 | self.expectedFiles.append(f) 62 | 63 | 64 | class Common_Test(unittest.TestCase): 65 | def setUp(self) -> None: 66 | self.payload = "THIS\nIS\nA\nDUMMY\nSTRING\n\n" 67 | self.file = Path(tempfile.NamedTemporaryFile().name) 68 | self.file.touch() 69 | 70 | self.output_folder = Path(tempfile.TemporaryDirectory().name) 71 | self.output_folder.mkdir() 72 | 73 | def tearDown(self) -> None: 74 | def delete_directory(folder): 75 | for _file in folder.rglob("*"): 76 | if _file.is_file(): 77 | _file.unlink() 78 | else: 79 | delete_directory(_file) 80 | _file.rmdir() 81 | 82 | self.file.unlink() 83 | delete_directory(self.output_folder) 84 | 85 | def test_writeDataToFile_storeOneDataElement(self): 86 | write_to_file(self.file, self.payload.encode()) 87 | 88 | with self.file.open("r") as _file: 89 | self.assertEqual(self.payload, _file.read()) 90 | 91 | def test_writeDataToFile_storeList(self): 92 | payload_list = [self.payload.encode(), self.payload.encode()] 93 | 94 | write_to_file(self.file, payload_list) 95 | 96 | with self.file.open("r") as _file: 97 | self.assertEqual(self.payload + self.payload, _file.read()) 98 | 99 | def test_getPktInfo_returnOnlyFileName(self): 100 | npkFile = Mock() 101 | npkFile.file = self.file 102 | 103 | self.assertEqual([self.file.name], get_short_pkt_info(npkFile)) 104 | 105 | def test_getBasicCntInfo(self): 106 | self.file.write_bytes(get_dummy_npk_binary()) 107 | 108 | self.assertEqual(['Cnt: 0:PckHeader'], get_short_cnt_info(Npk(self.file))) 109 | 110 | def test_extractPayloadFromCnt_createFilesWithPayload(self): 111 | self.file.write_bytes(get_dummy_npk_binary()) 112 | npkFile = Npk(self.file) 113 | 114 | extract_container(npkFile, self.output_folder, [NPK_PCK_HEADER]) 115 | 116 | created_files = list(self.output_folder.rglob("*")) 117 | 118 | self.assertEqual(1, len(created_files)) 119 | 120 | self.assertEqual([self.output_folder / '000_cnt_PckHeader.raw'], created_files) 121 | self.assertEqual(DummyHeaderCnt()._02_payload, created_files[0].read_bytes()) 122 | 123 | def test_getFullCntInfo_asString(self): 124 | result = get_full_cnt_info(CntBasic(get_dummy_basic_cnt(), offset_in_pck=0)) 125 | 126 | self.assertEqual(['CntBasic', 127 | ' Cnt id: -1', 128 | ' Cnt offset: 0', 129 | ' Cnt len: 13', 130 | ' Payload len: 7', 131 | " Payload[0:7]: b'Payload' [...] "], result) 132 | 133 | def test_getFullPktInfo_asString(self): 134 | self.file.write_bytes(get_dummy_npk_binary()) 135 | npkFile = Npk(self.file) 136 | 137 | result = get_full_pkt_info(npkFile) 138 | 139 | self.assertEqual([f"{self.file.name}", 140 | 'Cnt: 0:PckHeader', 141 | 'PckHeader', 142 | ' Cnt id: 1', 143 | ' Cnt offset: 8', 144 | ' Cnt len: 41', 145 | ' Payload len: 35', 146 | " Payload[0:10]: b'0123456789' [...] ", 147 | ' Program name: 01234567890abcde', 148 | ' Os version: 1.2.3 - rc(?): 4', 149 | ' Created at: 1970-01-01 00:00:01', 150 | ' NullBlock: (0, 0, 0, 0)', 151 | ' Flags: (0, 0, 0, 0, 0, 0, 0)'], result) 152 | 153 | def test_generateSha1FromFile(self): 154 | expectedHash = b'\xbb\xbc\xf2\xc5\x943\xf6\x8f"7l\xd2C\x9dl\xd3\t7\x8d\xf6' 155 | self.file.write_bytes(b"TESTDATA") 156 | 157 | self.assertEqual(expectedHash, sha1_sum_from_file(self.file)) 158 | 159 | def test_generateSha1FromHash(self): 160 | expectedHash = b'\xbb\xbc\xf2\xc5\x943\xf6\x8f"7l\xd2C\x9dl\xd3\t7\x8d\xf6' 161 | self.assertEqual(expectedHash, sha1_sum_from_binary(b"TESTDATA")) 162 | 163 | def test_generateSha1FromHash_returnEmptyIfNoData(self): 164 | self.assertEqual(b"", sha1_sum_from_binary(b"")) 165 | -------------------------------------------------------------------------------- /tests/npk_parsing_gps_file_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | from pathlib import Path 4 | 5 | from npkpy.npk.npk import Npk, MAGIC_BYTES 6 | 7 | 8 | class GpsFile_Test(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.npkFile = Path("tests/testData/6_45_6/gps-6.45.6.npk") 11 | self.npk = Npk(self.npkFile) 12 | self.cnt = self.npk.pck_cnt_list 13 | 14 | 15 | class ParseGpsNpkFile_Test(GpsFile_Test): 16 | 17 | def test_fileInfos(self): 18 | self.assertEqual('gps', self.npk.filename_program_name) 19 | self.assertEqual('6.45.6', self.npk.filename_version) 20 | self.assertEqual('npk', self.npk.filename_suffix) 21 | self.assertEqual('x86', self.npk.filename_architecture) 22 | self.assertEqual(b'\xc6\x16\xf0\x9d~lS\xa7z\xba}.\xe5\xa6w=\xe9\xb4S\xe7', self.npk.file_hash) 23 | 24 | def test_NpkHeader(self): 25 | self.assertEqual(MAGIC_BYTES, self.npk.pck_magic_bytes) 26 | self.assertEqual(53321, self.npk.pck_payload_len) 27 | self.assertEqual(53329, self.npk.pck_full_size) 28 | 29 | def test_PckHeader(self): 30 | self.assertEqual(1, self.npk.pck_header.cnt_id) 31 | self.assertEqual("PckHeader", self.npk.pck_header.cnt_id_name) 32 | self.assertEqual(36, self.npk.pck_header.cnt_payload_len) 33 | self.assertEqual(b'gps\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06f-\x06\x97gw]' 34 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00', self.npk.pck_header.cnt_payload) 35 | 36 | self.assertEqual("gps", self.npk.pck_header.cnt_program_name) 37 | self.assertEqual("6.45.6 - rc(?): 102", self.npk.pck_header.cnt_os_version) 38 | self.assertEqual(datetime.datetime(2019, 9, 10, 9, 6, 31), self.npk.pck_header.cnt_built_time) 39 | self.assertEqual((0, 0, 0, 0), self.npk.pck_header.cnt_null_block) 40 | self.assertEqual((0, 0, 0, 0, 2, 0, 0), self.npk.pck_header.cnt_flags) 41 | 42 | def test_ReleaseTyp(self): 43 | cnt = self.cnt[1] 44 | 45 | self.assertEqual(24, cnt.cnt_id) 46 | self.assertEqual(50, cnt._offset_in_pck) 47 | self.assertEqual("PckReleaseTyp", cnt.cnt_id_name) 48 | self.assertEqual(6, cnt.cnt_payload_len) 49 | self.assertEqual(b"stable", cnt.cnt_payload) 50 | self.assertEqual(12, cnt.cnt_full_length) 51 | 52 | def test_PckArchitectureTag(self): 53 | cnt = self.cnt[2] 54 | 55 | self.assertEqual(16, cnt.cnt_id) 56 | self.assertEqual(62, cnt._offset_in_pck) 57 | self.assertEqual("CntArchitectureTag", cnt.cnt_id_name) 58 | self.assertEqual(4, cnt.cnt_payload_len) 59 | self.assertEqual(b"i386", cnt.cnt_payload) 60 | self.assertEqual(10, cnt.cnt_full_length) 61 | 62 | def test_PckDescription(self): 63 | cnt = self.cnt[3] 64 | 65 | self.assertEqual(2, cnt.cnt_id) 66 | self.assertEqual(72, cnt._offset_in_pck) 67 | self.assertEqual("PckDescription", cnt.cnt_id_name) 68 | self.assertEqual(25, cnt.cnt_payload_len) 69 | self.assertEqual(b'Provides support for GPS.', cnt.cnt_payload) 70 | self.assertEqual(31, cnt.cnt_full_length) 71 | 72 | def test_PckHash(self): 73 | cnt = self.cnt[4] 74 | 75 | self.assertEqual(23, cnt.cnt_id) 76 | self.assertEqual(103, cnt._offset_in_pck) 77 | self.assertEqual("PckEckcdsaHash", cnt.cnt_id_name) 78 | self.assertEqual(40, cnt.cnt_payload_len) 79 | self.assertEqual(b'1a7d206bbfe626c55aa6d2d2caabb6a5a990f13d', cnt.cnt_payload) 80 | self.assertEqual(46, cnt.cnt_full_length) 81 | 82 | def test_PckRequirementsHeader(self): 83 | cnt = self.cnt[5] 84 | 85 | self.assertEqual(3, cnt.cnt_id) 86 | self.assertEqual(149, cnt._offset_in_pck) 87 | self.assertEqual("PckRequirementsHeader", cnt.cnt_id_name) 88 | self.assertEqual(1, cnt.cnt_structure_id) 89 | self.assertEqual(34, cnt.cnt_payload_len) 90 | self.assertEqual('system', cnt.cnt_program_name) 91 | self.assertEqual((0, 0, 0, 0), cnt.cnt_null_block) 92 | self.assertEqual("6.45.6 - rc(?): 102", cnt.cnt_os_version_min) 93 | self.assertEqual("6.45.6 - rc(?): 102", cnt.cnt_os_version_max) 94 | self.assertEqual("", cnt.cnt_flags) 95 | self.assertEqual(40, cnt.cnt_full_length) 96 | 97 | def test_PckNullBlock(self): 98 | cnt = self.cnt[6] 99 | 100 | self.assertEqual(22, cnt.cnt_id) 101 | self.assertEqual(189, cnt._offset_in_pck) 102 | self.assertEqual("CntNullBlock", cnt.cnt_id_name) 103 | self.assertEqual(3895, cnt.cnt_payload_len) 104 | self.assertEqual(b'\x00' * 3895, cnt.cnt_payload) 105 | self.assertEqual(3901, cnt.cnt_full_length) 106 | 107 | def test_PckSquashFsImage(self): 108 | cnt = self.cnt[7] 109 | 110 | self.assertEqual(21, cnt.cnt_id) 111 | self.assertEqual(4090, cnt._offset_in_pck) 112 | self.assertEqual("CntSquashFsImage", cnt.cnt_id_name) 113 | self.assertEqual(49152, cnt.cnt_payload_len) 114 | self.assertEqual(b'hsqs', cnt.cnt_payload[0:4]) 115 | self.assertEqual(49158, cnt.cnt_full_length) 116 | 117 | def test_PckSquashFsHashSignature(self): 118 | cnt = self.cnt[8] 119 | 120 | self.assertEqual(9, cnt.cnt_id) 121 | self.assertEqual(53248, cnt._offset_in_pck) 122 | self.assertEqual("CntSquashFsHashSignature", cnt.cnt_id_name) 123 | self.assertEqual(68, cnt.cnt_payload_len) 124 | self.assertEqual(b'\x8e\xa2\xb1\x8e\xf7n\xef355', cnt.cnt_payload[0:10]) 125 | self.assertEqual(74, cnt.cnt_full_length) 126 | 127 | def test_parseGpsFilxe_PckArchitectureTag_Closing(self): 128 | cnt = self.cnt[9] 129 | 130 | self.assertEqual(16, cnt.cnt_id) 131 | self.assertEqual(53322, cnt._offset_in_pck) 132 | self.assertEqual("CntArchitectureTag", cnt.cnt_id_name) 133 | self.assertEqual(1, cnt.cnt_payload_len) 134 | self.assertEqual(b'I', cnt.cnt_payload[0:10]) 135 | self.assertEqual(7, cnt.cnt_full_length) 136 | 137 | def test_checkStructure(self): 138 | self.assertEqual(10, len(self.npk.pck_cnt_list)) 139 | self.assertEqual([1, 24, 16, 2, 23, 3, 22, 21, 9, 16], list(cnt.cnt_id for cnt in self.npk.pck_cnt_list)) 140 | 141 | 142 | class WriteModifiedGpsFile_Test(GpsFile_Test): 143 | def test_modify_PckRequirementsHeader(self): 144 | expected_binary = b"\x03\x00\x22\x00\x00\x00\x01\x00\x73\x79\x73\x74\x65\x6d\x00\x00" + \ 145 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x06\x66\x2d\x06\x00\x00\x00\x00" + \ 146 | b"\x06\x66\x2d\x06\x00\x00\x00\x00" 147 | 148 | cnt = None 149 | for c in self.cnt: 150 | if c.cnt_id == 3: 151 | cnt = c 152 | break 153 | 154 | self.assertEqual(expected_binary, cnt.cnt_full_binary) 155 | 156 | def test_createFile_changePayloadTwice(self): 157 | oldPayload = self.npk.pck_header.cnt_payload 158 | 159 | self.npk.pck_header.cnt_payload = b"A" 160 | self.npk.pck_header.cnt_payload = oldPayload 161 | 162 | self.assertEqual(Npk(self.npkFile).file.read_bytes(), self.npk.pck_full_binary) 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------