150 lines
7.2 KiB
Python
Executable File
150 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Offline self-test of the unixnu build driver: defconfig parsing and module precedence."""
|
|
import argparse
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import io
|
|
import threading
|
|
import zipfile
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
from unixnu import cli, feeds, ipsw, payload
|
|
from unixnu.config import Context, parse_config
|
|
from unixnu.util import Error
|
|
|
|
|
|
def expect(condition, message):
|
|
if not condition:
|
|
raise SystemExit(f'FAIL: {message}')
|
|
|
|
|
|
class RangeHandler(BaseHTTPRequestHandler):
|
|
body = b''
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
def do_HEAD(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', str(len(self.body)))
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
start, end = self.headers['Range'].split('=')[1].split('-')
|
|
chunk = self.body[int(start):int(end) + 1]
|
|
self.send_response(206)
|
|
self.send_header('Content-Length', str(len(chunk)))
|
|
self.end_headers()
|
|
self.wfile.write(chunk)
|
|
|
|
|
|
def der(tag, body):
|
|
length = len(body)
|
|
head = bytes([tag, length]) if length < 128 else bytes([tag, 0x82]) + length.to_bytes(2, 'big')
|
|
return head + body
|
|
|
|
|
|
def main():
|
|
config = parse_config('# c\nCONFIG_A=y\nCONFIG_B=n\n# CONFIG_C is not set\nCONFIG_D="x y"\nCONFIG_E=0x10\n')
|
|
expect(config == {'A': True, 'B': False, 'C': False, 'D': 'x y', 'E': 16}, 'defconfig values')
|
|
for raw, value in (
|
|
('0', 0), ('-0', 0), ('00', 0), ('-00', 0),
|
|
('8', 8), ('-8', -8), ('08', 8), ('-08', -8),
|
|
('010', 10), ('-010', -10), ('00042', 42), ('-00042', -42),
|
|
('0x10', 16), ('-0x10', -16), ('0x00FF', 255), ('-0x00FF', -255),
|
|
):
|
|
expect(parse_config(f'CONFIG_NUMBER={raw}\n') == {'NUMBER': value}, f'defconfig number: {raw}')
|
|
try:
|
|
parse_config('CONFIG_A=maybe\n')
|
|
expect(False, 'bad defconfig value accepted')
|
|
except Error:
|
|
pass
|
|
with tempfile.TemporaryDirectory() as name:
|
|
root = Path(name)
|
|
for path in ('dev/drivers/Both', 'dev/drivers/DevOnly', 'loc/drivers/Both', 'loc/drivers/LocOnly', 'kd/Both', 'one/Both'):
|
|
(root / path).mkdir(parents=True)
|
|
(root / 'dev/board.toml').write_text('[board]\nname = "t"\n')
|
|
parsed = cli.parser().parse_args(['status', '-D', str(root / 'dev'), '--out', str(root / 'out')])
|
|
ctx = Context(parsed)
|
|
found = feeds.resolve(ctx)
|
|
expect(found[('driver', 'Both')].origin == 'device', 'device beats base')
|
|
expect(('driver', 'ArmCpu') in found and found[('driver', 'ArmCpu')].origin == 'base', 'base modules found')
|
|
parsed = cli.parser().parse_args(['status', '-D', str(root / 'dev'), '--out', str(root / 'out'), '--local', str(root / 'loc')])
|
|
found = feeds.resolve(Context(parsed))
|
|
expect(found[('driver', 'Both')].origin == 'local (--local)' and ('driver', 'LocOnly') in found, '--local beats device')
|
|
parsed = cli.parser().parse_args(['status', '-D', str(root / 'dev'), '--out', str(root / 'out'), '--local', str(root / 'loc'),
|
|
'--local-drivers', str(root / 'kd')])
|
|
found = feeds.resolve(Context(parsed))
|
|
expect(found[('driver', 'Both')].origin == 'local (--local-drivers)', '--local-drivers beats --local')
|
|
parsed = cli.parser().parse_args(['status', '-D', str(root / 'dev'), '--out', str(root / 'out'), '--local', str(root / 'loc'),
|
|
'--local-drivers', str(root / 'kd'), '--local-module', f'Both={root / "one"}'])
|
|
found = feeds.resolve(Context(parsed))
|
|
expect(found[('driver', 'Both')].origin == 'local (--local-module)', '--local-module wins')
|
|
try:
|
|
feeds.resolve(Context(cli.parser().parse_args(['status', '-D', str(root / 'dev'), '--out', str(root / 'out'),
|
|
'--local', str(root / 'missing')])))
|
|
expect(False, 'missing --local directory accepted')
|
|
except Error:
|
|
pass
|
|
expect(payload.check_args(['-v', 'a=1', 'b=x,y']) == '-v a=1 b=x,y', 'kernel arguments')
|
|
with tempfile.TemporaryDirectory() as name:
|
|
root = Path(name)
|
|
(root / 'configs').mkdir()
|
|
(root / 'configs/boot_defconfig').write_text('CONFIG_XNU_OWNER_DATA_MOUNT=y\n')
|
|
ctx = argparse.Namespace(config={}, config_path=root / '.config', config_name='boot_defconfig', device=root)
|
|
for configured in (False, True):
|
|
if configured:
|
|
ctx.config_path.write_text('CONFIG_XNU_OWNER_DATA_MOUNT=y\n')
|
|
ctx.config = parse_config(ctx.config_path.read_text())
|
|
try:
|
|
payload.check_data_input(ctx, None)
|
|
expect(False, 'owner Data payload accepted without its seed')
|
|
except Error as error:
|
|
expect('--data-ramdisk' in str(error), 'missing seed diagnostic')
|
|
payload.check_data_input(ctx, root / 'seed.dbr')
|
|
ctx.config = {'XNU_OWNER_DATA_MOUNT': False}
|
|
payload.check_data_input(ctx, None)
|
|
for bad in (['a b'], ['x' * 1100], ['a;b']):
|
|
try:
|
|
payload.check_args(bad)
|
|
expect(False, f'bad kernel argument accepted: {bad[0][:10]}')
|
|
except Error:
|
|
pass
|
|
with tempfile.TemporaryDirectory() as name:
|
|
root = Path(name)
|
|
stage = root / 'stage/EFI/UniXNU'
|
|
stage.mkdir(parents=True)
|
|
(stage / 'unixnu.toml').write_text('version = 1\n')
|
|
import os
|
|
for path in (root / 'stage', *(root / 'stage').rglob('*')):
|
|
os.utime(path, (payload.EPOCH, payload.EPOCH))
|
|
payload.write_volume(root / 'stage', root / 'a.img', 'TEST', 64)
|
|
payload.write_volume(root / 'stage', root / 'b.img', 'TEST', 64)
|
|
expect((root / 'a.img').read_bytes() == (root / 'b.img').read_bytes(), 'payload volume is reproducible')
|
|
# An IPSW is read member by member over HTTP range requests, and an IM4P payload is found by its DER structure.
|
|
archive = io.BytesIO()
|
|
with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as z:
|
|
z.writestr('BuildManifest.plist', b'manifest' * 1000)
|
|
z.writestr('big.bin', bytes(range(256)) * 40000)
|
|
RangeHandler.body = archive.getvalue()
|
|
server = HTTPServer(('127.0.0.1', 0), RangeHandler)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
remote = zipfile.ZipFile(ipsw.RangeFile(f'http://127.0.0.1:{server.server_port}/x.ipsw', block=4096))
|
|
expect(remote.read('big.bin') == bytes(range(256)) * 40000 and remote.read('BuildManifest.plist') == b'manifest' * 1000, 'remote zip members')
|
|
server.shutdown()
|
|
im4p = der(0x30, der(0x16, b'IM4P') + der(0x16, b'krnl') + der(0x16, b'ver') + der(0x04, b'bvx2payload') + der(0x30, b'\x02\x01\x01'))
|
|
expect(ipsw.im4p_payload(im4p) == b'bvx2payload', 'IM4P payload')
|
|
try:
|
|
ipsw.im4p_payload(der(0x30, der(0x16, b'XXXX') + der(0x16, b'krnl') + der(0x16, b'v') + der(0x04, b'z')))
|
|
expect(False, 'bad IM4P accepted')
|
|
except Error:
|
|
pass
|
|
print('PASS: unixnu defconfig parsing, module precedence, payload helpers and IPSW access')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|