#!/usr/bin/env python3 """Cross-check the actual ARM media builder using disposable regular files only.""" import hashlib import json from pathlib import Path import shutil import struct import subprocess import sys import tempfile import zlib project = Path(__file__).resolve().parents[2] sys.path.insert(0, str(project/'tools')) from image_formats import digest, gpt, LINUX_FILESYSTEM work = Path(tempfile.mkdtemp(prefix='m9-images.', dir=project/'out')) root = work/'root' source = (project/'out/rootfs-cli.tar').resolve().parent/'root' assert source.is_dir(), 'Build the configured rootfs first' subprocess.run(['cp', '-al', str(source), str(root)], check=True) fixture = root/'m9-test' fixture.mkdir() shutil.copy2(project/'target/aarch64-unknown-linux-musl/release/fds-burn', fixture/'fds-burn') subprocess.run(['cp', '-al', str(source), str(fixture/'system')], check=True) def manifest(kind, label, profile=''): text = f'''format = 1 [cartridge] id = "fds.test.{kind}" name = "{label}" class = "{kind}" version = "0.1.0" [media] writable = {str(kind == 'data').lower()} ''' if profile: text += f'\n[activation]\nprofile = "{profile}"\n' return text for kind in ('system', 'data', 'program', 'environment'): tree = fixture/kind (tree/'FDS').mkdir(parents=True) (tree/'FDS/CARTRIDGE.TOML').write_text(manifest(kind, f'TEST {kind.upper()}', 'windowmaker' if kind == 'environment' else '')) if kind == 'program': (tree/'app/bin').mkdir(parents=True) shutil.copy2(project/'out/fds', tree/'app/bin/fds') if kind == 'data': (tree/'read-me.txt').write_text('Created on ARM by FDS.\n') # One namespace call supplies a private ARM binfmt handler for child utilities. script = '''#!/usr/bin/bash set -euo pipefail cd /m9-test for kind in system data environment; do options=() if [[ $kind == data ]]; then options=(--size-mib 32); fi ./fds-burn create "$kind" "$kind" "$kind.img" "${options[@]}" >"$kind.build.log" 2>&1 ./fds-burn inspect "$kind.img" >"$kind.json" done if ./fds-burn create data data data.img --size-mib 32 >overwrite.log 2>&1; then exit 91; fi if ./fds-burn create system data wrong.img >wrong-class.log 2>&1; then exit 92; fi if ./fds-burn create data data data/recursive.img >recursive.log 2>&1; then exit 93; fi ln -s data.img image-link if ./fds-burn inspect image-link >symlink.log 2>&1; then exit 94; fi if ./fds-burn create program program forbidden.img >program-build-rejected.log 2>&1; then exit 95; fi printf 'PASS: actual ARM SYSTEM/DATA/ENVIRONMENT builder; PROGRAM creation requires a workstation\\n' ''' (fixture/'run').write_text(script) result = subprocess.run([str(project/'tools/in-rootfs'), str(root), '/usr/bin/bash', '/m9-test/run'], capture_output=True, text=True, timeout=600) (work/'arm.log').write_text(result.stdout+result.stderr) assert result.returncode == 0, f'ARM image checks failed: {work}/arm.log; per-class logs: {fixture}' print(result.stdout.strip(), flush=True) # Legacy PROGRAM reading/writing remains supported, but new software creation # belongs to the workstation tool. Build this compatibility fixture on the host. legacy = work / 'legacy-program.erofs' subprocess.run([str(project/'tools/in-image-tools'), 'mkfs.erofs', '--quiet', '-b', '4096', '-T', '0', '-L', 'FDS_PROGRAM', str(legacy), str(fixture/'program')], check=True) gpt(fixture/'program.img', [('FDS_PROGRAM', LINUX_FILESYSTEM, legacy)]) with (fixture/'program.json').open('w') as output: subprocess.run([str(project/'tools/in-void'), 'qemu-aarch64', str(fixture/'fds-burn'), 'inspect', str(fixture/'program.img')], check=True, stdout=output) for kind in ('system', 'data', 'program', 'environment'): image = fixture/f'{kind}.img' reported = json.loads((fixture/f'{kind}.json').read_text()) table = json.loads(subprocess.check_output(['sfdisk', '--json', str(image)]))['partitiontable'] assert table['label'] == 'gpt' and len(table['partitions']) == 1 part = table['partitions'][0] assert part['name'] == f'FDS_{kind.upper()}' assert part['start']*512 == reported['partition_start'] assert part['size']*512 == reported['partition_bytes'] assert digest(image) == reported['sha256'] assert image.stat().st_size == reported['bytes'] payload = work/f'{kind}.filesystem' with image.open('rb') as src, payload.open('wb') as dst: src.seek(part['start']*512) remaining = part['size']*512 while remaining: chunk = src.read(min(1024*1024, remaining)) assert chunk dst.write(chunk) remaining -= len(chunk) if kind == 'data': subprocess.run(['e2fsck', '-fn', str(payload)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) value = subprocess.check_output(['debugfs', '-R', 'cat /FDS/CARTRIDGE.TOML', str(payload)], stderr=subprocess.DEVNULL).decode() assert value == manifest(kind, 'TEST DATA') else: subprocess.run([str(project/'tools/in-image-tools'), 'fsck.erofs', '--extract', str(payload)], check=True, stdout=subprocess.DEVNULL) shutil.copy2(fixture/f'{kind}.json', work/f'{kind}.json') print('PASS: independent GPT geometry, Python SHA-256, EROFS and ext4 integrity checks', flush=True) # Valid CRCs are not sufficient: reject a second partition, out-of-range extents, # an internal label and disagreeing backup geometry. Use the host writer as an # independent source, then exercise the ARM parser against deliberate mutations. base = work/'negative-base.img' gpt(base, [('FDS_ENVIRONMENT', LINUX_FILESYSTEM, work/'environment.filesystem')]) def arm_inspect(path, ok): result = subprocess.run([str(project/'tools/in-void'), 'qemu-aarch64', str(fixture/'fds-burn'), 'inspect', str(path)], capture_output=True, text=True, timeout=60) assert (result.returncode == 0) == ok, (path, result.stdout, result.stderr) arm_inspect(base, True) for case in ('extra_partition', 'out_of_range', 'internal_label', 'bad_backup'): target = work/f'{case}.img' shutil.copyfile(base, target) data = bytearray(target.read_bytes()) last = len(data)//512-1 table = bytearray(data[1024:1024+16384]) if case == 'extra_partition': table[128:256] = table[:128] elif case == 'out_of_range': struct.pack_into('