Viewing files at commit 1c9b819ab86bCommit detailsView latest

Browse files
scripts/oakbench/oak_sqlite_reader.py 309 lines · 15.2 KB · python
1
"""Optional independent decoder for local Oak SQLite v1 committed trees.
2
3
No Oak code or executable is imported or invoked. Optional blake3/zstandard
4
packages belong to a pinned reader environment, not the base harness. Unknown
5
formats and missing instruments are unavailable evidence; corrupt objects fail.
6
7
Evidence contract (2026-09-27.1):
8
- Scope: when scope paths are given, only entries on those paths (and the
9
  trees leading to them) are decoded. This is the same scope the Git
10
  committed-payload check reads with ``git ls-tree HEAD -- <scope>``, so the
11
  two readers can share an evidence class. Out-of-scope entries are never
12
  decoded, so repo-wide conditions the task does not own cannot fail Oak.
13
- Reader resource and time budgets are the harness's limits, not properties
14
  of Oak's stored content: exceeding one is Unavailable (exit 77), never
15
  Invalid. Invalid (exit 1) is reserved for positively identified bad bytes.
16
- Format pin: required tables/columns must exist, and the HEAD commit
17
  preimage plus root tree serialization must verify. A mismatch there is an
18
  unrecognized format (Unavailable); once the root verifies, the same
19
  serialization is proven, so deeper hash mismatches are Invalid.
20
- In-scope non-regular entries (symlinks, unknown kinds) are reported in
21
  ``nonregular_paths`` instead of being refused, so the oracle can fail them
22
  exactly as Git's mode check does.
23
"""
24
from __future__ import annotations
25
import hashlib
26
import importlib.metadata
27
import json
28
from pathlib import Path
29
import re
30
import sqlite3
31
import sys
32
import time
33
import tempfile
34
35
READER_VERSION = 'oak-sqlite-v1-2026-09-27.1'
36
MAX_OBJECT = 32 * 1024 * 1024
37
MAX_TOTAL = 128 * 1024 * 1024
38
MAX_ENTRIES = 8192
39
MAX_DATABASE = 512 * 1024 * 1024
40
41
class Unavailable(Exception):
42
    pass
43
44
class Invalid(Exception):
45
    pass
46
47
48
REQUIRED_COLUMNS = {
49
    'metadata': {'key', 'value'},
50
    'commits': {'hash', 'branch_name', 'parent_hash', 'merge_parent_hash', 'manifest_hash', 'author', 'message', 'timestamp'},
51
    'blobs': {'hash', 'content', 'size', 'codec'},
52
    'trees': {'hash', 'content'},
53
    'tree_entries': {'tree_hash', 'name', 'kind', 'hash', 'mode'},
54
}
55
56
57
def normalize_scope(scope):
58
    """Validated repo-relative POSIX scope paths, or None for the whole tree."""
59
    if scope is None:
60
        return None
61
    result = []
62
    for item in scope:
63
        parts = item.split('/')
64
        if (not item or item.startswith('/') or any(part in ('', '.', '..', '.oak', '.git') for part in parts)
65
                or any(c in item for c in '\\\x00\n\r')):
66
            raise Unavailable('invalid reader scope path')
67
        result.append(item)
68
    return tuple(sorted(set(result)))
69
70
71
def in_scope(relative: str, scope) -> tuple[bool, bool]:
72
    """(visit, materialize) for a repo-relative path under the scope."""
73
    if scope is None:
74
        return True, True
75
    for item in scope:
76
        if relative == item or relative.startswith(item + '/'):
77
            return True, True
78
        if item.startswith(relative + '/'):
79
            return True, False
80
    return False, False
81
82
83
def read_committed_tree(repo: Path, destination: Path, scope=None) -> dict:
84
    scope = normalize_scope(scope)
85
    try:
86
        import blake3
87
        import zstandard
88
    except ImportError as exc:
89
        raise Unavailable('reader dependencies unavailable: blake3 and zstandard required') from exc
90
    db_path = repo / '.oak' / 'oak.db'
91
    if (repo.is_symlink() or (repo / '.oak').is_symlink() or db_path.is_symlink()
92
            or not db_path.is_file()):
93
        raise Unavailable('local Oak SQLite database missing or linked')
94
    if destination.exists() or destination.is_symlink():
95
        raise Unavailable('reader destination must not exist')
96
    deadline = time.monotonic() + 60
97
    destination.mkdir(parents=True)
98
99
    def hashed(raw, expected):
100
        if not isinstance(expected, str) or not re.fullmatch('[0-9a-f]{64}', expected):
101
            raise Invalid('invalid object hash')
102
        if blake3.blake3(raw).hexdigest() != expected:
103
            raise Invalid('object hash mismatch')
104
        return raw
105
106
    def decode(content, codec):
107
        if not isinstance(content, bytes):
108
            raise Invalid('stored object has invalid type')
109
        if len(content) > MAX_OBJECT:
110
            raise Unavailable('stored object exceeds reader bound')
111
        if codec == 0:
112
            return content
113
        if codec != 1:
114
            raise Unavailable('unsupported object codec')
115
        try:
116
            size = zstandard.frame_content_size(content)
117
            if zstandard.get_frame_parameters(content).window_size > MAX_OBJECT:
118
                raise Unavailable('decompression window exceeds reader bound')
119
            if size not in (-1, zstandard.CONTENTSIZE_UNKNOWN) and not 0 <= size <= MAX_OBJECT:
120
                raise Unavailable('decompressed object exceeds reader bound')
121
            raw = zstandard.ZstdDecompressor(max_window_size=MAX_OBJECT).decompress(
122
                content, max_output_size=MAX_OBJECT, allow_extra_data=False)
123
        except zstandard.ZstdError as exc:
124
            raise Invalid('invalid compressed object') from exc
125
        if len(raw) > MAX_OBJECT:
126
            raise Unavailable('decompressed object exceeds reader bound')
127
        return raw
128
129
    def database_snapshot():
130
        if any(Path(str(db_path) + suffix).exists() for suffix in ('-wal', '-journal')):
131
            raise Unavailable('active SQLite journal; quiescent database required')
132
        with db_path.open('rb') as stream:
133
            content = stream.read(MAX_DATABASE + 1)
134
        if len(content) > MAX_DATABASE:
135
            raise Unavailable('database exceeds independent reader bound')
136
        return content
137
138
    # Snapshot only quiescent databases. immutable=1 is safe on this private copy,
139
    # never on a live WAL database; the original bytes are checked again below.
140
    snapshot = database_snapshot()
141
    database_sha256 = hashlib.sha256(snapshot).hexdigest()
142
    with tempfile.TemporaryDirectory(prefix='oakbench-reader-db-') as temp:
143
        snapshot_path = Path(temp) / 'snapshot.db'
144
        snapshot_path.write_bytes(snapshot)
145
        del snapshot
146
        return _read_snapshot(snapshot_path, database_sha256, database_snapshot,
147
                              destination, deadline, hashed, decode, blake3, scope)
148
149
150
def _read_snapshot(snapshot_path, database_sha256, database_snapshot, destination,
151
                   deadline, hashed, decode, blake3, scope=None):
152
    total = 0
153
    entries_seen = 0
154
    nonregular = []
155
    with sqlite3.connect(snapshot_path.as_uri() + '?mode=ro&immutable=1', uri=True, timeout=5) as db:
156
        db.execute('PRAGMA query_only=ON')
157
        db.execute('PRAGMA trusted_schema=OFF')
158
        db.set_progress_handler(lambda: int(time.monotonic() > deadline), 1000)
159
        db.execute('BEGIN')
160
        for table, columns in REQUIRED_COLUMNS.items():
161
            present = {row[1] for row in db.execute(f'PRAGMA table_info({table})')}
162
            if not columns <= present:
163
                raise Unavailable('unrecognized Oak storage schema: ' + table)
164
        migrations = None
165
        if db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'").fetchone():
166
            row = db.execute("SELECT max(version) FROM schema_migrations WHERE length(version)<=128").fetchone()
167
            migrations = row[0] if row else None
168
169
        def metadata(key):
170
            row = db.execute('SELECT value FROM metadata WHERE key=? AND length(value)<=4096', (key,)).fetchone()
171
            return row[0] if row else None
172
173
        if metadata('hash_format') not in (None, 'v1'):
174
            raise Unavailable('unsupported Oak hash format')
175
        head = metadata('head')
176
        branch = metadata('current_branch')
177
        if not head or not branch:
178
            raise Unavailable('committed HEAD unavailable')
179
        commit = db.execute('SELECT branch_name,parent_hash,merge_parent_hash,manifest_hash,author,message,timestamp FROM commits WHERE hash=? AND length(CAST(branch_name AS BLOB))<=4096 AND coalesce(length(CAST(parent_hash AS BLOB)),0)<=64 AND coalesce(length(CAST(merge_parent_hash AS BLOB)),0)<=64 AND length(CAST(manifest_hash AS BLOB))=64 AND length(CAST(author AS BLOB))<=4096 AND coalesce(length(CAST(message AS BLOB)),0)<=1048576 AND length(CAST(timestamp AS BLOB))<=128', (head,)).fetchone()
180
        if not commit or any(v is not None and (not isinstance(v, str) or len(v) > MAX_OBJECT) for v in commit):
181
            raise Invalid('committed HEAD object missing or invalid')
182
        # Oak v1 stores the same RFC3339 timestamp text used in its hash preimage.
183
        # A preimage mismatch is an unrecognized format (fail closed), never a
184
        # published Oak correctness failure.
185
        try:
186
            hashed('\n'.join(v or '' for v in commit).encode(), head)
187
        except Invalid as exc:
188
            raise Unavailable('commit preimage format unrecognized: ' + str(exc)) from exc
189
        root_hash = commit[3]
190
191
        def blob(object_hash):
192
            row = db.execute('SELECT content,size,codec FROM blobs WHERE hash=? AND length(content)<=?', (object_hash, MAX_OBJECT)).fetchone()
193
            if row is None:
194
                if db.execute('SELECT 1 FROM blobs WHERE hash=?', (object_hash,)).fetchone():
195
                    raise Unavailable('stored blob exceeds reader bound')
196
                # Chunk-only hydration is not yet qualified by this reader.
197
                raise Unavailable('complete blob unavailable; chunk-only storage unsupported')
198
            content, size, codec = row
199
            if type(size) is not int or size < 0:
200
                raise Invalid('invalid stored blob size')
201
            if size > MAX_OBJECT:
202
                raise Unavailable('blob size exceeds reader bound')
203
            raw = decode(content, codec)
204
            if len(raw) != size:
205
                raise Invalid('blob size mismatch')
206
            return hashed(raw, object_hash)
207
208
        def visit(tree_hash, relative, ancestors):
209
            nonlocal total, entries_seen
210
            if tree_hash in ancestors:
211
                raise Invalid('tree cycle')
212
            if len(ancestors) >= 32 or time.monotonic() > deadline:
213
                raise Unavailable('tree depth or time bound exceeded')
214
            if tree_hash == blake3.blake3(b'').hexdigest():
215
                return
216
            row = db.execute('SELECT content FROM trees WHERE hash=? AND (content IS NULL OR length(content)<=?)', (tree_hash, MAX_OBJECT)).fetchone()
217
            if row is None:
218
                if db.execute('SELECT 1 FROM trees WHERE hash=?', (tree_hash,)).fetchone():
219
                    raise Unavailable('committed tree exceeds reader bound')
220
                raise Invalid('committed tree missing')
221
            if row[0] is None:
222
                if db.execute('SELECT 1 FROM tree_entries WHERE tree_hash=? AND (length(CAST(name AS BLOB))>4096 OR length(CAST(hash AS BLOB))!=64 OR length(CAST(kind AS BLOB))>4 OR length(CAST(mode AS BLOB))>10) LIMIT 1', (tree_hash,)).fetchone():
223
                    raise Unavailable('stored tree entry exceeds reader bound')
224
                stored = db.execute('SELECT kind,name,hash,mode FROM tree_entries WHERE tree_hash=? ORDER BY name LIMIT ?', (tree_hash, MAX_ENTRIES + 1)).fetchall()
225
                raw = '\n'.join('\t'.join((kind, name, obj, 'tree' if kind == 'tree' else mode)) for kind, name, obj, mode in stored).encode()
226
            else:
227
                raw = decode(row[0], 1)
228
            if not ancestors:
229
                try:
230
                    hashed(raw, tree_hash)
231
                except Invalid as exc:
232
                    raise Unavailable('root tree serialization unrecognized: ' + str(exc)) from exc
233
            else:
234
                hashed(raw, tree_hash)
235
            try:
236
                lines = raw.decode('utf-8').split('\n') if raw else []
237
            except UnicodeError as exc:
238
                raise Invalid('invalid tree encoding') from exc
239
            previous = None
240
            for line in lines:
241
                parts = line.split('\t')
242
                if len(parts) != 4:
243
                    raise Invalid('invalid tree entry')
244
                kind, name, object_hash, mode = parts
245
                if (not name or name in ('.', '..', '.oak', '.git') or any(c in name for c in '/\\\x00\n\r')
246
                        or (previous is not None and name <= previous)):
247
                    raise Invalid('unsafe or unordered tree path')
248
                previous = name
249
                path = relative / name
250
                visit_entry, _ = in_scope(path.as_posix(), scope)
251
                if not visit_entry:
252
                    continue
253
                entries_seen += 1
254
                if entries_seen > MAX_ENTRIES:
255
                    raise Unavailable('tree entry bound exceeded')
256
                if (destination / path).exists() or (destination / path).is_symlink():
257
                    raise Unavailable('filesystem path collision on reader host')
258
                if kind == 'tree' and mode == 'tree':
259
                    (destination / path).mkdir()
260
                    visit(object_hash, path, ancestors | {tree_hash})
261
                elif kind == 'blob' and mode in ('regular', 'executable'):
262
                    content = blob(object_hash)
263
                    total += len(content)
264
                    if total > MAX_TOTAL:
265
                        raise Unavailable('total payload bound exceeded')
266
                    target = destination / path
267
                    target.write_bytes(content)
268
                    target.chmod(0o755 if mode == 'executable' else 0o644)
269
                elif scope is not None:
270
                    # Git's scoped check fails any non-100644 entry; report
271
                    # it so the oracle fails the payload the same way.
272
                    nonregular.append(path.as_posix())
273
                else:
274
                    raise Unavailable('unsupported tree entry kind or mode')
275
        visit(root_hash, Path(), set())
276
        db.commit()
277
        if hashlib.sha256(database_snapshot()).hexdigest() != database_sha256:
278
            raise Unavailable('database changed during independent read')
279
    return {'head': head, 'database_sha256': database_sha256, 'reader_version': READER_VERSION,
280
            'scope': list(scope) if scope is not None else None, 'nonregular_paths': sorted(nonregular),
281
            'schema_migrations_max': migrations,
282
            'reader_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
283
            'blake3_version': importlib.metadata.version('blake3'),
284
            'zstandard_version': importlib.metadata.version('zstandard')}
285
286
287
def main():
288
    try:
289
        args = sys.argv[1:]
290
        scope = None
291
        if len(args) >= 3 and args[2] == '--scope':
292
            scope = args[3:]
293
            if not scope:
294
                raise Unavailable('empty reader scope')
295
        elif len(args) != 2:
296
            raise Unavailable('usage: oak_sqlite_reader.py REPOSITORY NEW_DESTINATION [--scope PATH...]')
297
        result = read_committed_tree(Path(args[0]), Path(args[1]), scope)
298
        code = 0
299
    except Unavailable as exc:
300
        result, code = {'unavailable': str(exc)}, 77
301
    except Invalid as exc:
302
        result, code = {'corrupt': True, 'reason': str(exc)}, 1
303
    except (OSError, ValueError, TypeError, sqlite3.Error) as exc:
304
        result, code = {'unavailable': 'reader instrument error: ' + str(exc)}, 77
305
    print(json.dumps(result, sort_keys=True))
306
    return code
307
308
if __name__ == '__main__':
309
    raise SystemExit(main())