diff --git a/README.md b/README.md index 35a3c07..5871556 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ In all cases, online help can be retrieved by providing the `-h` or ``--help`` f ### concatenate **concatenate** combines a GP2040-CE firmware .bin file (such as from a fresh build) and a GP2040-CE board's storage -section .bin, and produces a properly-offset .bin file suitable for flashing to a board. This may be useful to ensure -the board is flashed with a particular configuration, for instances such as producing a binary to flash many boards with -a particular configuration (specific customizations, etc.), or keeping documented backups of what you're testing with -during development. +section .bin or config (with footer) .bin, and produces a properly-offset .bin file suitable for flashing to a board. +This may be useful to ensure the board is flashed with a particular configuration, for instances such as producing a +binary to flash many boards with a particular configuration (specific customizations, etc.), or keeping documented +backups of what you're testing with during development. Sample usage: diff --git a/gp2040ce_bintools/builder.py b/gp2040ce_bintools/builder.py index 313f88a..84fa44e 100644 --- a/gp2040ce_bintools/builder.py +++ b/gp2040ce_bintools/builder.py @@ -3,7 +3,7 @@ import argparse import logging from gp2040ce_bintools import core_parser -from gp2040ce_bintools.storage import STORAGE_LOCATION +from gp2040ce_bintools.storage import STORAGE_LOCATION, pad_config_to_storage_size logger = logging.getLogger(__name__) @@ -17,6 +17,18 @@ class FirmwareLengthError(ValueError): """Exception raised when the firmware is too large to fit the known storage location.""" +def combine_firmware_and_config(firmware_binary: bytearray, config_binary: bytearray) -> bytearray: + """Given firmware and config binaries, combine the two to one, with proper offsets for GP2040-CE. + + Args: + firmware_binary: binary data of the raw GP2040-CE firmware, probably but not necessarily unpadded + config_binary: binary data of board config + footer, possibly padded to be a full storage section + Returns: + the resulting correctly-offset binary suitable for a GP2040-CE board + """ + return pad_firmware_up_to_storage(firmware_binary) + pad_config_to_storage_size(config_binary) + + def concatenate_firmware_and_storage_files(firmware_filename: str, storage_filename: str, combined_filename: str): """Open the provided binary files and combine them into one combined GP2040-CE with config file. @@ -26,7 +38,7 @@ def concatenate_firmware_and_storage_files(firmware_filename: str, storage_filen combined_filename: filename of where to write the combine binary """ with open(firmware_filename, 'rb') as firmware, open(storage_filename, 'rb') as storage: - new_binary = pad_firmware_up_to_storage(firmware.read()) + bytearray(storage.read()) + new_binary = combine_firmware_and_config(bytearray(firmware.read()), bytearray(storage.read())) with open(combined_filename, 'wb') as combined: combined.write(new_binary) @@ -35,7 +47,7 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: """Provide a copy of the firmware padded with zero bytes up to the provided position. Args: - firmware: the read-in binary file to process + firmware: the firmware binary to process Returns: the resulting padded binary as a bytearray Raises: @@ -47,7 +59,7 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: raise FirmwareLengthError(f"provided firmware binary is larger than the start of " f"storage at {STORAGE_LOCATION}!") - return bytes(bytearray(firmware) + bytearray(b'\x00' * bytes_to_pad)) + return bytearray(firmware) + bytearray(b'\x00' * bytes_to_pad) ############ @@ -57,13 +69,13 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: def concatenate(): """Combine a built firmware .bin and a storage .bin.""" parser = argparse.ArgumentParser( - description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area .bin into one file " - "suitable for flashing onto a board.", + description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area or config .bin " + "into one file suitable for flashing onto a board.", parents=[core_parser], ) parser.add_argument('firmware_filename', help=".bin file of a GP2040-CE firmware, probably from a build") - parser.add_argument('storage_filename', help=".bin file of a GP2040-CE board's storage section") + parser.add_argument('config_filename', help=".bin file of a GP2040-CE board's storage section or config w/footer") parser.add_argument('new_binary_filename', help="output .bin file of the resulting firmware + storage") args, _ = parser.parse_known_args() - concatenate_firmware_and_storage_files(args.firmware_filename, args.storage_filename, args.new_binary_filename) + concatenate_firmware_and_storage_files(args.firmware_filename, args.config_filename, args.new_binary_filename) diff --git a/gp2040ce_bintools/storage.py b/gp2040ce_bintools/storage.py index 629cb15..79169e1 100644 --- a/gp2040ce_bintools/storage.py +++ b/gp2040ce_bintools/storage.py @@ -114,6 +114,25 @@ def get_storage_section(content: bytes) -> bytes: logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE)) return content[STORAGE_LOCATION:(STORAGE_LOCATION + STORAGE_SIZE)] + +def pad_config_to_storage_size(config: bytes) -> bytearray: + """Provide a copy of the config (with footer) padded with zero bytes to be the proper storage section size. + + Args: + firmware: the config section binary to process + Returns: + the resulting padded binary as a bytearray + Raises: + FirmwareLengthError: if the is larger than the storage location + """ + bytes_to_pad = STORAGE_SIZE - len(config) + logger.debug("config is length %s, padding %s bytes", len(config), bytes_to_pad) + if bytes_to_pad < 0: + raise ConfigLengthError(f"provided config binary is larger than the allowed storage of " + f"storage at {STORAGE_SIZE} bytes!") + + return bytearray(b'\x00' * bytes_to_pad) + bytearray(config) + ############ # COMMANDS # ############ diff --git a/tests/conftest.py b/tests/conftest.py index 4062aa9..22df89f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,16 @@ import pytest HERE = os.path.dirname(os.path.abspath(__file__)) +@pytest.fixture +def config_binary(): + """Read in a test GP2040-CE configuration, Protobuf serialized binary form with footer.""" + filename = os.path.join(HERE, 'test-files', 'test-config.bin') + with open(filename, 'rb') as file: + content = file.read() + + yield content + + @pytest.fixture def firmware_binary(): """Read in a test GP2040-CE firmware binary file.""" diff --git a/tests/test-files/test-config.bin b/tests/test-files/test-config.bin new file mode 100644 index 0000000..0229fd2 Binary files /dev/null and b/tests/test-files/test-config.bin differ diff --git a/tests/test_builder.py b/tests/test_builder.py index a5829a9..c50722f 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,7 +1,8 @@ """Tests for the image builder module.""" import pytest -from gp2040ce_bintools.builder import FirmwareLengthError, pad_firmware_up_to_storage +from gp2040ce_bintools.builder import FirmwareLengthError, combine_firmware_and_config, pad_firmware_up_to_storage +from gp2040ce_bintools.storage import get_config_footer, get_storage_section def test_padding_firmware(firmware_binary): @@ -10,6 +11,24 @@ def test_padding_firmware(firmware_binary): assert len(padded) == 2088960 +def test_firmware_plus_storage(firmware_binary, storage_dump): + """Test that combining firmware and storage produces a valid combined binary.""" + whole_board = combine_firmware_and_config(firmware_binary, storage_dump) + # if this is valid, we should be able to find the storage and footer again + storage = get_storage_section(whole_board) + footer_size, _, _ = get_config_footer(storage) + assert footer_size == 2032 + + +def test_firmware_plus_config_binary(firmware_binary, config_binary): + """Test that combining firmware and storage produces a valid combined binary.""" + whole_board = combine_firmware_and_config(firmware_binary, config_binary) + # if this is valid, we should be able to find the storage and footer again + storage = get_storage_section(whole_board) + footer_size, _, _ = get_config_footer(storage) + assert footer_size == 2032 + + def test_padding_firmware_too_big(firmware_binary): """Test that firmware is padded to the expected size.""" with pytest.raises(FirmwareLengthError): diff --git a/tests/test_storage.py b/tests/test_storage.py index 2d65964..7c4666b 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -95,3 +95,15 @@ def test_config_from_whole_board_parses(whole_board_dump): config = storage.get_config(storage.get_storage_section(whole_board_dump)) assert config.boardVersion == 'v0.7.2' assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1 + + +def test_pad_config_to_storage(config_binary): + """Test that we can properly pad a config section to the correct storage section size.""" + storage_section = storage.pad_config_to_storage_size(config_binary) + assert len(storage_section) == 8192 + + +def test_pad_config_to_storage_raises(config_binary): + """Test that we raise an exception if the config is bigger than the storage section.""" + with pytest.raises(storage.ConfigLengthError): + _ = storage.pad_config_to_storage_size(config_binary * 5)