Files
MeshCore/test/test_rp2040_application_capabilities.py
T

193 lines
9.3 KiB
Python

"""Actual RP2040 UF2/BIN application evidence, without PlatformIO or hardware."""
from pathlib import Path
import json
import os
import shlex
import struct
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'scripts'))
from rp2040_application import FLASH_BASE, FLASH_END, read_application
CAPABILITY = 'companion.picow.combined'
MARKER = b'meshcore.capability.companion.picow.combined.v1'
def uf2(application, padding=255):
count = (len(application) + 255) // 256
payload = application + bytes([padding]) * (count * 256 - len(application))
blocks = []
for index in range(count):
block = bytearray(512)
struct.pack_into('<8I', block, 0, 0x0A324655, 0x9E5D5157, 0x2000,
FLASH_BASE + index * 256, 256, index, count, 0xE48BFF56)
block[32:288] = payload[index * 256:(index + 1) * 256]
struct.pack_into('<I', block, 508, 0x0AB16F30)
blocks.append(bytes(block))
return b''.join(blocks)
class RP2040ApplicationCapabilityTests(unittest.TestCase):
def setUp(self):
temp = tempfile.TemporaryDirectory(prefix='rp2040-application-')
self.addCleanup(temp.cleanup)
self.path = Path(temp.name)
self.bin = self.path / 'firmware.bin'
self.uf2 = self.path / 'firmware.uf2'
self.elf = self.path / 'firmware.elf'
self.output = self.path / 'firmware.capabilities.json'
self.app = b'program-prefix\0' + MARKER + b'\0' + b'code' * 145
self.bin.write_bytes(self.app)
self.uf2.write_bytes(uf2(self.app))
self.elf.write_bytes(b'ELF metadata without combined capability')
def checker(self, platform='RP2040_PLATFORM', include_bin=True, include_uf2=True, extra=()):
command = [sys.executable, '-B', str(ROOT / 'scripts/check_firmware_capabilities.py'),
'--image', str(self.elf), '--output', str(self.output), '--target', 'PicoW_companion_radio',
'--platform', platform, '--build-profile', 'auto', '--expect-application',
CAPABILITY + '=' + MARKER.decode(), *extra]
if include_bin:
command.extend(['--firmware-bin', str(self.bin)])
if include_uf2:
command.extend(['--uf2-package', str(self.uf2)])
result = subprocess.run(command, text=True, capture_output=True, check=False)
return result, json.loads(self.output.read_text())
def test_actual_uploadable_payload_and_both_supported_padding_conventions(self):
for padding in (0, 255):
self.uf2.write_bytes(uf2(self.app, padding))
self.assertEqual(read_application(self.uf2, self.bin), self.app)
result, manifest = self.checker()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(manifest['verified'])
self.assertIn(dict(capability=CAPABILITY, evidence=MARKER.decode(), present=True,
source='packaged application'), manifest['verification'])
self.assertEqual(manifest['ota_update_methods'], [])
self.assertFalse(manifest['ota_update_verified'])
def test_elf_metadata_alone_cannot_supply_application_proof(self):
self.elf.write_bytes(b'ELF metadata\0' + MARKER)
self.bin.write_bytes(b'program without combined transports')
self.uf2.write_bytes(uf2(self.bin.read_bytes()))
result, manifest = self.checker()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertFalse(manifest['verified'])
def test_larger_flash_hardware_is_not_limited_to_the_pico_two_megabyte_size(self):
application = self.app + b'\0' * (2 * 1024 * 1024 + 17 - len(self.app))
self.bin.write_bytes(application)
self.uf2.write_bytes(uf2(application))
self.assertEqual(read_application(self.uf2, self.bin), application)
def test_unused_uf2_sector_bytes_are_never_scanned_for_capabilities(self):
self.bin.write_bytes(b'program without combined transports')
wire = bytearray(uf2(self.bin.read_bytes()))
wire[320:320+len(MARKER)] = MARKER
self.uf2.write_bytes(wire)
self.elf.write_bytes(MARKER)
result, manifest = self.checker()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertFalse(manifest['verified'])
def test_uf2_alignment_tail_cannot_supply_application_proof(self):
self.bin.write_bytes(b'program')
wire = bytearray(uf2(self.bin.read_bytes()))
wire[32+len(b'program'):32+len(b'program')+len(MARKER)] = MARKER
self.uf2.write_bytes(wire)
result, manifest = self.checker()
self.assertEqual(result.returncode, 2, result.stderr)
self.assertFalse(manifest['verified'])
self.assertIn('padding', result.stderr)
def test_raw_bin_and_uf2_disagreement_fails_before_marker_admission(self):
wire = bytearray(uf2(self.app))
wire[32] ^= 1
self.uf2.write_bytes(wire)
result, manifest = self.checker()
self.assertEqual(result.returncode, 2, result.stderr)
self.assertFalse(manifest['verified'])
self.assertIn('does not match', result.stderr)
def test_invalid_family_headers_layout_and_sector_metadata_are_rejected(self):
original = uf2(self.app)
changes = (
(0, 0), (4, 0), (508, 0), (8, 0), (8, 0x2001), (8, 0x3000),
(28, 0xADA52840), (16, 252), (20, 1), (24, 0),
(12, FLASH_BASE-256), (12, FLASH_END),
(512+12, FLASH_BASE), (512+12, FLASH_BASE+512), (512+20, 0), (512+24, 1),
)
for offset, value in changes:
wire = bytearray(original)
struct.pack_into('<I', wire, offset, value)
self.uf2.write_bytes(wire)
with self.subTest(offset=offset, value=value), self.assertRaises(ValueError):
read_application(self.uf2, self.bin)
for wire in (b'', original[:-1], original+b'\0'):
self.uf2.write_bytes(wire)
with self.subTest(size=len(wire)), self.assertRaises(ValueError):
read_application(self.uf2, self.bin)
def test_empty_oversized_or_unnecessarily_extended_application_is_rejected(self):
for size in (0, FLASH_END-FLASH_BASE+1):
with self.bin.open('wb') as stream:
stream.truncate(size)
with self.subTest(size=size), self.assertRaises(ValueError):
read_application(self.uf2, self.bin)
self.bin.write_bytes(self.app[:256])
self.uf2.write_bytes(uf2(self.app[:256] + b'\xff'*256))
with self.assertRaisesRegex(ValueError, 'does not match'):
read_application(self.uf2, self.bin)
def test_rp2040_application_proof_requires_both_exact_public_formats(self):
for include_bin, include_uf2, platform in ((True, False, 'RP2040_PLATFORM'),
(False, True, 'RP2040_PLATFORM'),
(True, True, 'ESP32_PLATFORM')):
result, manifest = self.checker(platform, include_bin, include_uf2)
with self.subTest(platform=platform, bin=include_bin, uf2=include_uf2):
self.assertEqual(result.returncode, 2, result.stderr)
self.assertFalse(manifest['verified'])
self.assertIn('require both', result.stderr)
def test_combined_transports_never_qualify_as_wireless_self_update(self):
result, manifest = self.checker(extra=('--require-ota',))
self.assertEqual(result.returncode, 1, result.stderr)
self.assertFalse(manifest['ota_update_verified'])
self.assertEqual(manifest['ota_update_methods'], [])
self.assertIn('wireless self-update', result.stderr)
@unittest.skipIf(os.name == 'nt', 'builder is POSIX shell')
def test_builder_passes_both_real_artifacts_to_the_production_checker(self):
directory = self.path / 'build/PicoW_companion_radio'
directory.mkdir(parents=True)
for source in (self.bin, self.uf2, self.elf):
(directory / source.name).write_bytes(source.read_bytes())
output = self.path / 'out'
output.mkdir()
program = r'''
set -euo pipefail
source build.sh
pio() { echo 'PlatformIO must not run' >&2; return 99; }
PIO_BUILD_DIR_OVERRIDE=@BUILD@
OUTPUT_DIR=@OUTPUT@
BUILD_PROFILE_FOR_TARGET=auto
BUILD_RECIPE_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
BUILD_CAPABILITIES=() BUILD_REDUCTIONS=() BUILD_EXPECTATIONS=()
BUILD_APPLICATION_EXPECTATIONS=('companion.picow.combined=meshcore.capability.companion.picow.combined.v1')
write_build_capability_manifest PicoW_companion_radio RP2040_PLATFORM PicoW_companion_radio fixture
'''
program = program.replace('@BUILD@', shlex.quote(str(self.path / 'build'))).replace('@OUTPUT@', shlex.quote(str(output)))
result = subprocess.run(['bash', '-c', program], cwd=ROOT, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
manifest = json.loads((output / 'fixture.capabilities.json').read_text())
self.assertTrue(manifest['verified'])
self.assertIn(CAPABILITY, manifest['capabilities'])
self.assertEqual(manifest['ota_update_methods'], [])
if __name__ == '__main__':
unittest.main()