aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs/inflate_check.py
blob: c3f37b4c01cff964996035f2289d7356fbcf4c08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3
"""Research 140: inflate (RFC 1951) written in aup (aup's examples/inflate.aup).

Builds it with aup (AUP, default the sibling checkout) and inflates raw deflate streams zlib made (stored, fixed and
dynamic blocks: levels 0, 1, 6 and 9 with each of zlib's five strategies, on random bytes, words, short runs and far
repeats, 0 to 70,000 bytes) on Pure's three paths (batches, rounds, all compiled), each output against the original;
then damaged streams (cut short, a bit flipped) against inflate.py, which reads them the same way: the same bytes when
it takes one, a failed event when it refuses it. Then counts lines (code lines: neither blank nor a comment; brace
lines: a closing brace alone) of inflate.aup and of inflate.py's algorithm (above its main). One JSON line."""
import json, os, random, subprocess, sys, tempfile, zlib

here = os.path.dirname(os.path.abspath(__file__))
pure = os.path.normpath(os.path.join(here, '../../..'))
aup = os.environ.get('AUP', os.path.join(pure, '..', 'aup'))
sys.path.insert(0, here)
import inflate  # noqa: E402


def lines(path, stop=None):
    code = braces = 0
    for line in open(path):
        if stop and line.startswith(stop): break
        text = line.strip()
        if not text or text.startswith('#'): continue
        code += 1
        braces += text == '}'
    return code, braces


def corpus(chooser):
    words = [bytes(chooser.choices(b'abcdefghijklmnopqrstuvwxyz', k=chooser.randrange(2, 9))) for _ in range(400)]
    streams = []
    for size in (0, 1, 2, 3, 100, 1000, 10000, 70000):
        far = bytes(chooser.randrange(256) for _ in range(min(size, 33000)))
        kinds = {'random': bytes(chooser.randrange(256) for _ in range(size)),
                 'words': b' '.join(chooser.choice(words) for _ in range(size))[:size],
                 'runs': (bytes(chooser.choices(b'ab', k=7)) * size)[:size],
                 'far': (far + far[:32768] * 3)[:size]}  # repeats 32,768 bytes back at most
        for data in kinds.values():
            for level in (0, 1, 6, 9):
                for strategy in (zlib.Z_DEFAULT_STRATEGY, zlib.Z_FILTERED, zlib.Z_HUFFMAN_ONLY, zlib.Z_RLE, zlib.Z_FIXED):
                    packer = zlib.compressobj(level, zlib.DEFLATED, -15, 9, strategy)
                    streams.append((data, packer.compress(data) + packer.flush()))
    return streams


def damaged(chooser, streams):
    """Streams cut short or with a bit flipped, each with what inflate.py makes of it (None: refused)."""
    out = []
    small = [packed for data, packed in streams if 0 < len(packed) < 3000]
    for packed in chooser.sample(small, 120):
        cut = packed[:chooser.randrange(len(packed))]
        flipped = bytearray(packed)
        flipped[chooser.randrange(len(packed))] ^= 1 << chooser.randrange(8)
        for stream in (cut, bytes(flipped)):
            try:
                out.append((stream, inflate.inflate(stream)))
            except (ValueError, IndexError):
                out.append((stream, None))
    return out


def run(module, packed_streams, env, work):
    given = []
    for n, packed in enumerate(packed_streams):
        path = os.path.join(work, f'{n}.deflate')
        with open(path, 'wb') as f:
            f.write(packed)
        given.append(f'message inflate @{path}\n')
    out = subprocess.run([os.path.join(pure, 'build', 'pure-run'), module], input=''.join(given), capture_output=True,
                         text=True, env=dict(os.environ, **env), timeout=1800).stdout.splitlines()
    got = []
    for line in out:
        state = json.loads(line)
        items = state['states']['inflated']
        got.append(bytes.fromhex(items[-1]['data']) if state['fault'] == 'none' and items else None)
    return out, got


chooser = random.Random(140)
streams = corpus(chooser)
broken = damaged(chooser, streams)
paths = {}
with tempfile.TemporaryDirectory() as work:
    module = os.path.join(work, 'inflate.pmod')
    subprocess.run([os.path.join(aup, 'build', 'aup'), 'build', os.path.join(aup, 'examples', 'inflate.aup'), '-o', module],
                   check=True)
    outputs = {}
    for name, env in (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}),
                      ('all', {'PURE_JIT': 'all'})):
        raw, got = run(module, [packed for _, packed in streams], env, work)
        paths[name] = sum(g == data for (data, _), g in zip(streams, got)) if len(got) == len(streams) else 0
        outputs[name] = raw
    same_paths = outputs['rounds'] == outputs['batches'] and outputs['all'] == outputs['batches']
    # A damaged stream after a good one: a refused one fails its event and leaves the good one's bytes.
    raw, got = run(module, [stream for stream, _ in broken], {}, work)
    agreed = sum(g == want or (want is None and g is None) for (_, want), g in zip(broken, got)) if len(got) == len(broken) else 0
    refused = sum(want is None for _, want in broken)
aup_code, aup_braces = lines(os.path.join(aup, 'examples', 'inflate.aup'))
python_code, _ = lines(os.path.join(here, 'inflate.py'), stop='if __name__')
ok = all(n == len(streams) for n in paths.values()) and same_paths and agreed == len(broken)
print(json.dumps({'streams': len(streams), 'bytes': sum(len(d) for d, _ in streams), 'right': paths, 'paths_same': same_paths,
                  'damaged': len(broken), 'damaged_refused': refused, 'damaged_agreed': agreed, 'aup_lines': aup_code,
                  'aup_brace_lines': aup_braces, 'python_lines': python_code}))
sys.exit(0 if ok else 1)