option to get the config out of a whole board dump
now you don't need to fiddle with specific byte ranges of a dump, you can just dump the whole board if that's more convenient, and visualize-storage will parse that also more testing in general
This commit is contained in:
parent
3dcc0d4c59
commit
5b8dc456f1
23
README.md
23
README.md
|
@ -27,13 +27,14 @@ As above, plus also `pip install -Ur requirements/requirements-dev.txt` to get l
|
||||||
|
|
||||||
### visualize-storage
|
### visualize-storage
|
||||||
|
|
||||||
**visualize-storage** reads a dump of a GP2040-CE board's flash storage section, where the configuration lives,
|
**visualize-storage** reads a dump of a GP2040-CE board's flash storage section, where the configuration lives, and
|
||||||
and prints it out for visual inspection or diffing with other tools. Usage is simple; just pass the tool your
|
prints it out for visual inspection or diffing with other tools. It can also find the storage section from a GP2040-CE
|
||||||
binary file to analyze along with the path to the Protobuf files.
|
whole board dump, if you have that instead. Usage is simple; just pass the tool your binary file to analyze along with
|
||||||
|
the path to the Protobuf files.
|
||||||
|
|
||||||
Because Protobuf relies on .proto files to convey the serialized structure, you must supply them
|
Because Protobuf relies on .proto files to convey the serialized structure, you must supply them from the main GP2040-CE
|
||||||
from the main GP2040-CE project, e.g. pointing this tool at your clone of the core project. Something like
|
project, e.g. pointing this tool at your clone of the core project. Something like this would suffice for a working
|
||||||
this would suffice for a working invocation (note: you do not need to compile the files yourself):
|
invocation (note: you do not need to compile the files yourself):
|
||||||
|
|
||||||
```
|
```
|
||||||
% visualize-storage -P ~/proj/GP2040-CE/proto -P ~/proj/GP2040-CE/lib/nanopb/generator/proto memory.bin
|
% visualize-storage -P ~/proj/GP2040-CE/proto -P ~/proj/GP2040-CE/lib/nanopb/generator/proto memory.bin
|
||||||
|
@ -110,10 +111,16 @@ forcedSetupOptions {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dumping the storage section
|
### Dumping the GP2040-CE board
|
||||||
|
|
||||||
The storage section of a GP2040-CE board is a reserved 8 KB starting at `0x101FE000`. To dump your board's storage:
|
These tools require a dump of your GP2040-CE board, either the storage section or the whole board, depending on the
|
||||||
|
context. The storage section of a GP2040-CE board is a reserved 8 KB starting at `0x101FE000`. To dump your board's storage:
|
||||||
|
|
||||||
```
|
```
|
||||||
% picotool save -r 101FE000 10200000 memory.bin
|
% picotool save -r 101FE000 10200000 memory.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And to dump your whole board:
|
||||||
|
```
|
||||||
|
% picotool save -a whole-board.bin
|
||||||
|
```
|
||||||
|
|
|
@ -6,6 +6,9 @@ from gp2040ce_bintools import core_parser, get_config_pb2
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STORAGE_LOCATION = 0x1FE000
|
||||||
|
STORAGE_SIZE = 8192
|
||||||
|
|
||||||
FOOTER_SIZE = 12
|
FOOTER_SIZE = 12
|
||||||
FOOTER_MAGIC = b'\x65\xe3\xf1\xd2'
|
FOOTER_MAGIC = b'\x65\xe3\xf1\xd2'
|
||||||
|
|
||||||
|
@ -43,10 +46,12 @@ def get_config_footer(content: bytes) -> tuple[int, int, str]:
|
||||||
ValueError: if the provided bytes are not a config footer
|
ValueError: if the provided bytes are not a config footer
|
||||||
"""
|
"""
|
||||||
# last 12 bytes are the 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:
|
if len(content) < FOOTER_SIZE:
|
||||||
raise ValueError("provided content is not large enough to have a config footer!")
|
raise ValueError("provided content is not large enough to have a config footer!")
|
||||||
|
|
||||||
footer = content[-FOOTER_SIZE:]
|
footer = content[-FOOTER_SIZE:]
|
||||||
|
logger.debug("suspected footer magic: %s", footer[-4:])
|
||||||
if footer[-4:] != FOOTER_MAGIC:
|
if footer[-4:] != FOOTER_MAGIC:
|
||||||
raise ValueError("content's magic is not as expected!")
|
raise ValueError("content's magic is not as expected!")
|
||||||
|
|
||||||
|
@ -62,6 +67,39 @@ def get_config_footer(content: bytes) -> tuple[int, int, str]:
|
||||||
return config_size, config_crc, config_magic
|
return config_size, config_crc, config_magic
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_from_file(filename: str, whole_board: bool = False) -> dict:
|
||||||
|
"""Read the specified file (memory dump or whole board dump) and get back its config section.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: the filename of the file to open and read
|
||||||
|
Returns:
|
||||||
|
the parsed configuration
|
||||||
|
"""
|
||||||
|
with open(filename, 'rb') as dump:
|
||||||
|
content = dump.read()
|
||||||
|
|
||||||
|
if whole_board:
|
||||||
|
return get_config(get_storage_section(content))
|
||||||
|
else:
|
||||||
|
return get_config(content)
|
||||||
|
|
||||||
|
|
||||||
|
def get_storage_section(content: bytes) -> bytes:
|
||||||
|
"""Pull out what should be the GP2040-CE storage section from a whole board dump.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: bytes of a GP2040-CE whole board dump
|
||||||
|
Returns:
|
||||||
|
the presumed storage section from the binary
|
||||||
|
"""
|
||||||
|
# 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!")
|
||||||
|
|
||||||
|
logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE))
|
||||||
|
return content[STORAGE_LOCATION:(STORAGE_LOCATION + STORAGE_SIZE)]
|
||||||
|
|
||||||
############
|
############
|
||||||
# COMMANDS #
|
# COMMANDS #
|
||||||
############
|
############
|
||||||
|
@ -74,10 +112,8 @@ def visualize():
|
||||||
"its contents.",
|
"its contents.",
|
||||||
parents=[core_parser],
|
parents=[core_parser],
|
||||||
)
|
)
|
||||||
parser.add_argument('filename', help=".bin file of a GP2040-CE board's storage section, bytes 101FE000-10200000")
|
parser.add_argument('--whole-board', action='store_true', help="indicate the binary file is a whole board dump")
|
||||||
|
parser.add_argument('filename', help=".bin file of a GP2040-CE board's storage section, bytes 101FE000-10200000, "
|
||||||
|
"or of a GP2040-CE's whole board dump if --whole-board is specified")
|
||||||
args, _ = parser.parse_known_args()
|
args, _ = parser.parse_known_args()
|
||||||
with open(args.filename, 'rb') as dump:
|
print(get_config_from_file(args.filename, whole_board=args.whole_board))
|
||||||
content = dump.read()
|
|
||||||
|
|
||||||
config = get_config(content)
|
|
||||||
print(config)
|
|
||||||
|
|
|
@ -28,6 +28,9 @@ extend-ignore = "T101"
|
||||||
max-complexity = 10
|
max-complexity = 10
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
line_length = 120
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,13 @@ def storage_dump():
|
||||||
content = file.read()
|
content = file.read()
|
||||||
|
|
||||||
yield content
|
yield content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def whole_board_dump():
|
||||||
|
"""Read in a test whole board dump file of a GP2040-CE board."""
|
||||||
|
filename = os.path.join(HERE, 'test-files', 'test-whole-board.bin')
|
||||||
|
with open(filename, 'rb') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
yield content
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,17 @@
|
||||||
|
"""Test our tools themselves to make sure they adhere to certain flags."""
|
||||||
|
from subprocess import run
|
||||||
|
|
||||||
|
from gp2040ce_bintools import __version__
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_flag():
|
||||||
|
"""Test that tools report the version."""
|
||||||
|
result = run(['visualize-storage', '-v'], capture_output=True, encoding='utf8')
|
||||||
|
assert __version__ in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_flag():
|
||||||
|
"""Test that tools report the usage information."""
|
||||||
|
result = run(['visualize-storage', '-h'], capture_output=True, encoding='utf8')
|
||||||
|
assert 'usage: visualize-storage' in result.stdout
|
||||||
|
assert 'Read the configuration section from a dump of a GP2040-CE board' in result.stdout
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
import pytest
|
import pytest
|
||||||
from decorator import decorator
|
from decorator import decorator
|
||||||
|
|
||||||
from gp2040ce_bintools.storage import get_config, get_config_footer
|
from gp2040ce_bintools.storage import get_config, get_config_footer, get_config_from_file, get_storage_section
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
@ -33,13 +33,19 @@ def test_config_footer(storage_dump):
|
||||||
def test_config_footer_way_too_small(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."""
|
"""Test that a config footer isn't detected if the size is way too small."""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
size, crc, magic = get_config_footer(storage_dump[-11:])
|
_, _, _ = get_config_footer(storage_dump[-11:])
|
||||||
|
|
||||||
|
|
||||||
def test_config_footer_too_small(storage_dump):
|
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."""
|
"""Test that a config footer isn't detected if the size is smaller than that found in the header."""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
size, crc, magic = get_config_footer(storage_dump[-1000:])
|
_, _, _ = 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:])
|
||||||
|
|
||||||
|
|
||||||
def test_config_footer_bad_magic(storage_dump):
|
def test_config_footer_bad_magic(storage_dump):
|
||||||
|
@ -47,7 +53,7 @@ def test_config_footer_bad_magic(storage_dump):
|
||||||
unmagical = bytearray(storage_dump)
|
unmagical = bytearray(storage_dump)
|
||||||
unmagical[-1] = 0
|
unmagical[-1] = 0
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
size, crc, magic = get_config_footer(unmagical)
|
_, _, _ = get_config_footer(unmagical)
|
||||||
|
|
||||||
|
|
||||||
def test_config_fails_without_pb2s(storage_dump):
|
def test_config_fails_without_pb2s(storage_dump):
|
||||||
|
@ -56,9 +62,35 @@ def test_config_fails_without_pb2s(storage_dump):
|
||||||
_ = get_config(storage_dump)
|
_ = 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)
|
||||||
|
assert config.boardVersion == 'v0.7.2'
|
||||||
|
assert config.addonOptions.bootselButtonOptions.enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
@with_pb2s
|
||||||
|
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)
|
||||||
|
assert config.boardVersion == 'v0.7.2'
|
||||||
|
assert config.addonOptions.bootselButtonOptions.enabled is False
|
||||||
|
|
||||||
|
|
||||||
@with_pb2s
|
@with_pb2s
|
||||||
def test_config_parses(storage_dump):
|
def test_config_parses(storage_dump):
|
||||||
"""Test that we need the config_pb2 to exist/be compiled for reading the config to work."""
|
"""Test that we need the config_pb2 to exist/be compiled for reading the config to work."""
|
||||||
config = get_config(storage_dump)
|
config = get_config(storage_dump)
|
||||||
assert config.boardVersion == 'v0.7.2'
|
assert config.boardVersion == 'v0.7.2'
|
||||||
assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1
|
assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1
|
||||||
|
|
||||||
|
|
||||||
|
@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))
|
||||||
|
assert config.boardVersion == 'v0.7.2'
|
||||||
|
assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -37,7 +37,7 @@ commands =
|
||||||
# report on coverage runs from above
|
# report on coverage runs from above
|
||||||
skip_install = true
|
skip_install = true
|
||||||
commands =
|
commands =
|
||||||
coverage report --fail-under=80 --show-missing
|
coverage report --fail-under=90 --show-missing
|
||||||
|
|
||||||
[testenv:bandit]
|
[testenv:bandit]
|
||||||
commands =
|
commands =
|
||||||
|
|
Loading…
Reference in New Issue