Files
2026-09-21 22:29:23 +08:00

151 lines
7.0 KiB
Python

#!/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('<Q', table, 40, last)
elif case == 'internal_label':
table[56:128] = 'FDS_INTERNAL'.encode('utf-16le').ljust(72, b'\0')
for start in (1024, (last-32)*512):
data[start:start+16384] = table
for sector in (1, last):
header = bytearray(data[sector*512:(sector+1)*512])
struct.pack_into('<I', header, 88, zlib.crc32(table))
if case == 'bad_backup' and sector == last:
header[56] ^= 1
struct.pack_into('<I', header, 16, 0)
struct.pack_into('<I', header, 16, zlib.crc32(header[:92]))
data[sector*512:(sector+1)*512] = header
target.write_bytes(data)
arm_inspect(target, False)
print('PASS: malformed image rejection with recomputed valid GPT CRCs', flush=True)
print(f'PASS: M9 image construction evidence: {work}')