tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

arboricx_readers.py (11263B)


      1 #!/usr/bin/env python3
      2 """Opt-in reader rewrite checks against explicit full module bundles.
      3 
      4 Independent field/truncation oracles plus full-tree differential malformed-input
      5 and recursive-worker probes. Does not compile, publish, or prove source identity.
      6 This is a frozen historical-cohort comparator (including unit suffix loss),
      7 not the current-candidate oracle. Use bundle_batch.py for rewritten boundaries.
      8 """
      9 import argparse
     10 import hashlib
     11 import json
     12 from pathlib import Path
     13 
     14 from http_headers import LEAF, TRUE, expect, number, pair, result, tree_list
     15 from source_order import Runner
     16 from stable_bundle import byte_tree
     17 
     18 
     19 def product(xs):
     20     return xs[0] if len(xs) == 1 else pair(xs[0], product(xs[1:]))
     21 
     22 
     23 def u32(n):
     24     return n.to_bytes(4, 'big')
     25 
     26 
     27 def string(s):
     28     return u32(len(s)) + s
     29 
     30 
     31 class Failure(Exception):
     32     def __init__(self, code, rest):
     33         self.tree = result(False, number(code), byte_tree(rest))
     34 
     35 
     36 class Reader:
     37     def __init__(self, data):
     38         self.data = data
     39 
     40     def take(self, n):
     41         if len(self.data) < n:
     42             raise Failure(1, self.data)
     43         value, self.data = self.data[:n], self.data[n:]
     44         return value
     45 
     46     def magic(self, value):
     47         original = self.data
     48         for expected in value:
     49             if not self.data:
     50                 raise Failure(1, original)
     51             if self.take(1)[0] != expected:
     52                 raise Failure(2, original)
     53 
     54     def string(self):
     55         return self.take(int.from_bytes(self.take(4), 'big'))
     56 
     57     def root(self):
     58         return pair(byte_tree(self.take(4)), byte_tree(self.string()))
     59 
     60     def export(self):
     61         return product([byte_tree(self.string()), byte_tree(self.take(4)),
     62                         byte_tree(self.string()), byte_tree(self.string())])
     63 
     64     def tlv(self):
     65         return pair(byte_tree(self.take(2)), byte_tree(self.string()))
     66 
     67     def repeated(self, worker):
     68         return tree_list([worker() for _ in range(int.from_bytes(self.take(4), 'big'))])
     69 
     70     def core(self):
     71         self.magic(b'ARBMNFST')
     72         self.take(2)
     73         self.take(2)
     74         fields = [byte_tree(self.string()) for _ in range(9)]
     75         fields += [self.repeated(lambda: byte_tree(self.string())), byte_tree(self.take(1)),
     76                    self.repeated(self.root), self.repeated(self.export)]
     77         return product(fields)
     78 
     79 
     80 def cases():
     81     core = (b'ARBMNFST' + b'\x00\x01\x00\x00' + b''.join(string(x) for x in
     82             [b'schema', b'bundle', b'tree', b'hash', b'domain', b'payload', b'semantics', b'eval', b'abi'])
     83             + u32(2) + string(b'fs') + string(b'net') + b'\0'
     84             + u32(1) + u32(0) + string(b'main')
     85             + u32(1) + string(b'main') + u32(0) + string(b'tree') + string(b'abi'))
     86     specs = [
     87         ('manifest', 'readLengthPrefixedString', string(b'a\0\xff'), lambda r: byte_tree(r.string())),
     88         ('manifest', 'readRootEntry', u32(7) + string(b'role'), lambda r: r.root()),
     89         ('manifest', 'readExportEntry', string(b'name') + u32(7) + string(b'kind') + string(b'abi'), lambda r: r.export()),
     90         ('manifest', 'readTLV', b'\0\x03' + string(b'a\0\xff'), lambda r: r.tlv()),
     91         ('manifest', 'readManifestCore', core, lambda r: r.core()),
     92         ('common', 'readArboricxHeader', b'ARBORICX' + bytes(range(24)),
     93          lambda r: (r.magic(b'ARBORICX'), product([byte_tree(r.take(n)) for n in [2, 2, 4, 8, 8]]))[1]),
     94         ('common', 'readSectionRecord', bytes(range(32)),
     95          lambda r: product([byte_tree(r.take(n)) for n in [4, 2, 2, 2, 2, 8, 8, 4]])),
     96         ('nodes', 'readNodeRecord', u32(5) + b'\x01\0\0\0\0', lambda r: byte_tree(r.string())),
     97     ]
     98     for module, export, encoded, oracle in specs:
     99         inputs = [(f'cut-{i}', encoded[:i]) for i in range(len(encoded))]
    100         inputs += [('exact', encoded), ('suffix', encoded + b'\0\xffTAIL')]
    101         if export in ('readManifestCore', 'readArboricxHeader'):
    102             inputs += [('bad-magic', b'!' + encoded[1:])]
    103         for label, data in inputs:
    104             r = Reader(data)
    105             try:
    106                 value = oracle(r)
    107                 expected = result(True, value, byte_tree(r.data))
    108             except Failure as error:
    109                 expected = error.tree
    110             yield module, export + '-' + label, export, [byte_tree(data)], expected
    111     # Historical full-manifest entrypoint drops its successful suffix. Retain
    112     # this independent expectation separately from the field-reader protocol.
    113     full = core + u32(1) + b'\0\x03' + string(b'metadata') + u32(1) + b'\0\x05' + string(b'extension')
    114     expected = result(True, pair(Reader(core).core(), pair(
    115         tree_list([pair(byte_tree(b'\0\x03'), byte_tree(b'metadata'))]), LEAF)))
    116     for suffix in [b'', b'TAIL']:
    117         yield 'manifest', 'full-suffix-' + suffix.hex(), 'readManifest', [byte_tree(full + suffix)], expected
    118     # Counted workers: zero, multiple items, exact suffix and failure remainder.
    119     for module, export, one, value in [
    120         ('manifest', 'readCapabilities', string(b'x'), byte_tree(b'x')),
    121         ('manifest', 'readRoots', u32(0) + string(b'r'), pair(byte_tree(u32(0)), byte_tree(b'r'))),
    122         ('manifest', 'readExports', string(b'e') + u32(0) + string(b'k') + string(b'a'),
    123          product([byte_tree(b'e'), byte_tree(u32(0)), byte_tree(b'k'), byte_tree(b'a')])),
    124         ('manifest', 'readTLVList', b'\0\x01' + string(b'v'), pair(byte_tree(b'\0\x01'), byte_tree(b'v'))),
    125         ('common', 'readSectionDirectory', bytes(32), product([byte_tree(bytes(n)) for n in [4,2,2,2,2,8,8,4]])),
    126         ('nodes', 'readNodeRecords', u32(1) + b'\0', byte_tree(b'\0')),
    127     ]:
    128         for count in [0, 1, 3]:
    129             yield module, export + '-' + str(count), export, [number(count), byte_tree(one*count+b'TAIL')], result(True, tree_list([value]*count), byte_tree(b'TAIL'))
    130         yield module, export+'-missing', export, [number(2), byte_tree(one)], result(False, number(1))
    131     # Noncanonical byte-list spines: compatibility comparisons, not new safety
    132     # guarantees. Keep counts bounded to avoid unbounded attacker lengths.
    133     for module, export in [('nodes','readNodeRecord'), ('manifest','readLengthPrefixedString'),
    134                            ('common','readSectionRecord')]:
    135         for i, raw in enumerate([TRUE, b'\1\1\0', tree_list([number(0)], TRUE)]):
    136             yield module, export+'-malformed-'+str(i), export, [raw], None
    137     for tag in [0, 1, 2, 3]:
    138         for payload in [b'', b'\0', b'\1\0\0\0\0', b'\2'+bytes(8), b'\3']:
    139             yield 'nodes', f'tag-{tag}-{payload.hex()}', 'nodePayloadHasTag?', [number(tag), byte_tree(payload)], TRUE if payload and payload[0] == tag else LEAF
    140 
    141     for i, records in enumerate([
    142         [], [b'\0'], [b'\0', b'\0'], [b'\0', b'\1'+u32(0)],
    143         [b'\0', b'\1'+u32(0), b'\2'+u32(0)+u32(1)],
    144         [b'\1'+u32(0)], [b'\0', b'\1'+u32(1)],
    145         [b'\0', b'\2'+u32(0)+u32(2)], [b''], [b'\3'],
    146         [b'\0x'], [b'\1'+bytes(3)], [b'\2'+bytes(7)],
    147         [b'\1'+u32(99), b'\3'],
    148     ]):
    149         encoded = tree_list([byte_tree(x) for x in records])
    150         invalid = any(not (x == b'\0' or (len(x) == 5 and x[0] == 1)
    151                           or (len(x) == 9 and x[0] == 2)) for x in records)
    152         bad_index = any(int.from_bytes(x[k:k+4], 'big') >= j
    153                         for j,x in enumerate(records) for k in range(1,len(x),4))
    154         code = 8 if invalid else 9 if bad_index else None
    155         rest = byte_tree(b'TAIL')
    156         expected = result(code is None, encoded if code is None else number(code), rest)
    157         yield 'nodes', f'validate-{i}', 'validateNodeRecords', [encoded,rest], expected
    158     for name in [b'a', b'b', b'missing']:
    159         exports = [product([byte_tree(n),byte_tree(u32(i)),byte_tree(b'k'),byte_tree(b'abi')])
    160                    for i,n in enumerate([b'a',b'b',b'a'])]
    161         value = exports[0] if name == b'a' else exports[1] if name == b'b' else None
    162         yield 'manifest', 'find-'+name.hex(), 'findExportByName', [tree_list(exports),byte_tree(name)], LEAF if value is None else b'\1'+value
    163         yield 'manifest', 'names-'+name.hex(), 'getExportNames', [tree_list(exports)], tree_list([byte_tree(n) for n in [b'a',b'b',b'a']])
    164     for i, raw in enumerate([LEAF, TRUE, b'\1\1\0']):
    165         for module, export, args in [
    166             ('manifest','findExportByName',[raw,byte_tree(b'a')]),
    167             ('manifest','getExportNames',[raw]),
    168             ('manifest','lookupMetadata',[raw,byte_tree(b'xx')]),
    169             ('common','lookupSectionRecord',[byte_tree(b'xxxx'),raw]),
    170             ('nodes','nodeRecordsHaveInvalidPayload?',[raw]),
    171             ('nodes','nodeChildIndicesInPrefix?',[raw,LEAF,number(0)]),
    172         ]:
    173             yield module, f'{export}-shape-{i}', export, args, None
    174 
    175 
    176 def main():
    177     p = argparse.ArgumentParser(description=__doc__)
    178     p.add_argument('tricu', type=Path)
    179     p.add_argument('--subject', type=Path, required=True, help='candidate directory')
    180     p.add_argument('--reference', type=Path, required=True, help='retained baseline directory')
    181     p.add_argument('--directory', type=Path, required=True)
    182     args = p.parse_args()
    183     args.directory.mkdir(parents=True)
    184     runner = Runner(args.tricu, args.directory)
    185     pins, identities = {}, {}
    186     for role, directory in [('subject',args.subject),('reference',args.reference)]:
    187         for module in ['manifest','common','nodes']:
    188             path = directory / ('arboricx_'+module+'.arboricx')
    189             pins[role,module] = runner.imported(path,role+'-'+module)
    190             identities[role+'-'+module] = hashlib.sha256(path.read_bytes()).hexdigest()
    191     report = dict(complete=False, artifacts=identities, runtime_sha256=hashlib.sha256(args.tricu.read_bytes()).hexdigest(),
    192                   harness_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), cases=[])
    193     try:
    194         for module,label,export,values,expected in cases():
    195             argv=[]
    196             for i,value in enumerate(values):
    197                 path=args.directory/(label+f'-{i}.tree');path.write_bytes(value)
    198                 argv+=['--arg-tree-file',path]
    199             before=runner.invoke(label+'-reference',pins['reference',module],export,argv)
    200             after=runner.invoke(label+'-subject',pins['subject',module],export,argv)
    201             if expected is not None:
    202                 expect(before,expected,label+' reference oracle')
    203                 expect(after,expected,label+' subject oracle')
    204             expect(after,before,label+' complete comparison')
    205             report['cases'].append(dict(label=label,oracle=expected is not None,result_sha256=hashlib.sha256(after).hexdigest()))
    206         # Same arity, wrong export must not satisfy the independently specified result.
    207         raw=byte_tree(string(b'x'));path=args.directory/'wrong.tree';path.write_bytes(raw)
    208         wrong=runner.invoke('wrong-subject',pins['subject','manifest'],'readRootEntry',['--arg-tree-file',path])
    209         assert wrong != result(True,byte_tree(b'x')), 'wrong export accepted'
    210         report.update(complete=True,wrong_subject_rejected=True)
    211     finally:
    212         (args.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
    213     print(f'PASS: {len(report["cases"])} reader cases + wrong-subject control')
    214 
    215 
    216 if __name__ == '__main__':
    217     main()