From 229092dd66f8f881f60af6ee12632eb21a7f6f03 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Thu, 22 Jun 2023 00:08:12 -0500 Subject: [PATCH] custom config validation exceptions might be useful when allowing for ignoring specific sanity checks --- gp2040ce_bintools/storage.py | 26 ++++++++++++++++++-------- tests/test_storage.py | 30 +++++++++++++++--------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/gp2040ce_bintools/storage.py b/gp2040ce_bintools/storage.py index d7a18e1..629cb15 100644 --- a/gp2040ce_bintools/storage.py +++ b/gp2040ce_bintools/storage.py @@ -16,9 +16,17 @@ FOOTER_SIZE = 12 FOOTER_MAGIC = b'\x65\xe3\xf1\xd2' -############### -# LIB METHODS # -############### +################# +# LIBRARY ITEMS # +################# + + +class ConfigLengthError(ValueError): + """Exception raised when a length sanity check fails.""" + + +class ConfigMagicError(ValueError): + """Exception raised when the config section does not have the magic value in its footer.""" def get_config(content: bytes) -> Message: @@ -46,17 +54,17 @@ def get_config_footer(content: bytes) -> tuple[int, int, str]: Returns: the discovered config size, config CRC, and magic from the config footer Raises: - ValueError: if the provided bytes are not a config footer + ConfigLengthError, ConfigMagicError: if the provided bytes are not a config footer """ # last 12 bytes are the footer logger.debug("length of content to look for footer in: %s", len(content)) if len(content) < FOOTER_SIZE: - raise ValueError("provided content is not large enough to have a config footer!") + raise ConfigLengthError("provided content is not large enough to have a config footer!") footer = content[-FOOTER_SIZE:] logger.debug("suspected footer magic: %s", footer[-4:]) if footer[-4:] != FOOTER_MAGIC: - raise ValueError("content's magic is not as expected!") + raise ConfigMagicError("content's magic is not as expected!") config_size = int.from_bytes(reversed(footer[:4]), 'big') config_crc = int.from_bytes(reversed(footer[4:8]), 'big') @@ -64,7 +72,7 @@ def get_config_footer(content: bytes) -> tuple[int, int, str]: # one last sanity check if len(content) < config_size + FOOTER_SIZE: - raise ValueError("provided content is not large enough according to the config footer!") + raise ConfigLengthError("provided content is not large enough according to the config footer!") logger.debug("detected footer (size:%s, crc:%s, magic:%s", config_size, config_crc, config_magic) return config_size, config_crc, config_magic @@ -95,11 +103,13 @@ def get_storage_section(content: bytes) -> bytes: content: bytes of a GP2040-CE whole board dump Returns: the presumed storage section from the binary + Raises: + ConfigLengthError: if the provided bytes don't appear to have a storage section """ # a whole board must be at least as big as the known fences logger.debug("length of content to look for storage in: %s", len(content)) if len(content) < STORAGE_LOCATION + STORAGE_SIZE: - raise ValueError("provided content is not large enough to have a storage section!") + raise ConfigLengthError("provided content is not large enough to have a storage section!") logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE)) return content[STORAGE_LOCATION:(STORAGE_LOCATION + STORAGE_SIZE)] diff --git a/tests/test_storage.py b/tests/test_storage.py index 8b2640e..b558324 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -5,7 +5,7 @@ import sys import pytest from decorator import decorator -from gp2040ce_bintools.storage import get_config, get_config_footer, get_config_from_file, get_storage_section +import gp2040ce_bintools.storage as storage HERE = os.path.dirname(os.path.abspath(__file__)) @@ -24,7 +24,7 @@ def with_pb2s(test, *args, **kwargs): def test_config_footer(storage_dump): """Test that a config footer is identified as expected.""" - size, crc, magic = get_config_footer(storage_dump) + size, crc, magic = storage.get_config_footer(storage_dump) assert size == 2032 assert crc == 3799109329 assert magic == '0x65e3f1d2' @@ -32,41 +32,41 @@ def test_config_footer(storage_dump): def test_config_footer_way_too_small(storage_dump): """Test that a config footer isn't detected if the size is way too small.""" - with pytest.raises(ValueError): - _, _, _ = get_config_footer(storage_dump[-11:]) + with pytest.raises(storage.ConfigLengthError): + _, _, _ = storage.get_config_footer(storage_dump[-11:]) def test_config_footer_too_small(storage_dump): """Test that a config footer isn't detected if the size is smaller than that found in the header.""" - with pytest.raises(ValueError): - _, _, _ = get_config_footer(storage_dump[-1000:]) + with pytest.raises(storage.ConfigLengthError): + _, _, _ = storage.get_config_footer(storage_dump[-1000:]) def test_whole_board_too_small(whole_board_dump): """Test that a storage section isn't detected if the size is too small to contain where it should be.""" - with pytest.raises(ValueError): - _, _, _ = get_storage_section(whole_board_dump[-100000:]) + with pytest.raises(storage.ConfigLengthError): + _, _, _ = storage.get_storage_section(whole_board_dump[-100000:]) def test_config_footer_bad_magic(storage_dump): """Test that a config footer isn't detected if the magic is incorrect.""" unmagical = bytearray(storage_dump) unmagical[-1] = 0 - with pytest.raises(ValueError): - _, _, _ = get_config_footer(unmagical) + with pytest.raises(storage.ConfigMagicError): + _, _, _ = storage.get_config_footer(unmagical) def test_config_fails_without_pb2s(storage_dump): """Test that we need the config_pb2 to exist/be compiled for reading the config to work.""" with pytest.raises(ModuleNotFoundError): - _ = get_config(storage_dump) + _ = storage.get_config(storage_dump) @with_pb2s def test_get_config_from_file_storage_dump(): """Test that we can open a storage dump file and find its config.""" filename = os.path.join(HERE, 'test-files', 'test-storage-area.bin') - config = get_config_from_file(filename) + config = storage.get_config_from_file(filename) assert config.boardVersion == 'v0.7.2' assert config.addonOptions.bootselButtonOptions.enabled is False @@ -75,7 +75,7 @@ def test_get_config_from_file_storage_dump(): def test_get_config_from_file_whole_board_dump(): """Test that we can open a storage dump file and find its config.""" filename = os.path.join(HERE, 'test-files', 'test-whole-board.bin') - config = get_config_from_file(filename, whole_board=True) + config = storage.get_config_from_file(filename, whole_board=True) assert config.boardVersion == 'v0.7.2' assert config.addonOptions.bootselButtonOptions.enabled is False @@ -83,7 +83,7 @@ def test_get_config_from_file_whole_board_dump(): @with_pb2s def test_config_parses(storage_dump): """Test that we need the config_pb2 to exist/be compiled for reading the config to work.""" - config = get_config(storage_dump) + config = storage.get_config(storage_dump) assert config.boardVersion == 'v0.7.2' assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1 @@ -91,6 +91,6 @@ def test_config_parses(storage_dump): @with_pb2s def test_config_from_whole_board_parses(whole_board_dump): """Test that we can read in a whole board and still find the config section.""" - config = get_config(get_storage_section(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