151 lines
7.0 KiB
Python
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}')
|