@sanning/proof 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -12
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +353 -0
- package/dist/case.d.ts +217 -26
- package/dist/case.js +535 -124
- package/dist/cli.d.ts +64 -8
- package/dist/cli.js +799 -219
- package/dist/compose.js +6 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +38 -0
- package/dist/evidence.js +238 -22
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/file-match.d.ts +105 -0
- package/dist/file-match.js +374 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/index.d.ts +14 -7
- package/dist/index.js +20 -6
- package/dist/message-parts.d.ts +18 -10
- package/dist/message-parts.js +25 -15
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +63 -4
- package/dist/verifier.js +223 -15
- package/dist/witness-cache.d.ts +85 -0
- package/dist/witness-cache.js +264 -0
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +422 -0
- package/src/case.ts +671 -131
- package/src/cli.ts +932 -217
- package/src/compose.ts +8 -1
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +257 -23
- package/src/exit-codes.ts +54 -0
- package/src/file-match.ts +461 -0
- package/src/grading.ts +109 -52
- package/src/index.ts +101 -2
- package/src/message-parts.ts +25 -15
- package/src/types.ts +5 -0
- package/src/verifier.ts +225 -16
- package/src/witness-cache.ts +306 -0
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +66 -8
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +218 -9
- package/vectors/file-match.json +1808 -0
- package/vectors/jcs-reader.json +82 -3
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +20 -5
- package/vectors/witness-cache.json +1304 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one ZIP reader `case` uses, for a case file named on the command line.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript half of `src/sanning_proof/zip_reader.py`. Node has no ZIP
|
|
5
|
+
* reader, and Python's `zipfile` is not used, so the two kernels read one
|
|
6
|
+
* archive by one rule rather than by two libraries that disagree on the edges:
|
|
7
|
+
* a central directory that contradicts a local header, a name that is not
|
|
8
|
+
* UTF-8, data after the end of a deflate stream. Where either library would
|
|
9
|
+
* have made a choice, this module refuses instead.
|
|
10
|
+
*
|
|
11
|
+
* What it reads (evidence-bundle.md 5.6.1): the central directory, and each
|
|
12
|
+
* entry's data through its local header. Stored and deflate only. What it
|
|
13
|
+
* REFUSES, as a whole archive: no end-of-central-directory record, ZIP64, more
|
|
14
|
+
* than one disk, an entry the central directory places outside the archive, a
|
|
15
|
+
* local header that does not match its central entry, a size or CRC-32 that
|
|
16
|
+
* does not match the data, two entries with one name, a name that is not
|
|
17
|
+
* UTF-8, more than MAX_ZIP_ENTRIES entries or MAX_ZIP_BYTES bytes in all, and
|
|
18
|
+
* a name nested deeper than MAX_ZIP_DEPTH folders. What it REPORTS and never
|
|
19
|
+
* reads, entry by entry: an encrypted entry, a symbolic link, a method other
|
|
20
|
+
* than stored or deflate, and a name that is absolute, holds a backslash or a
|
|
21
|
+
* NUL, or has an empty, `.` or `..` segment.
|
|
22
|
+
*
|
|
23
|
+
* Pure: bytes in, entries out. The caller turns them into the tree `case`
|
|
24
|
+
* splits.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { inflateRawSync } from "node:zlib";
|
|
28
|
+
|
|
29
|
+
import { utf8Strict } from "./verifier.js";
|
|
30
|
+
|
|
31
|
+
/** More entries than this refuse the archive: the walk's own bound on names. */
|
|
32
|
+
export const MAX_ZIP_ENTRIES = 50_000;
|
|
33
|
+
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
34
|
+
export const MAX_ZIP_BYTES = 2 ** 30;
|
|
35
|
+
/** A name nested deeper than this many folders refuses the archive. */
|
|
36
|
+
export const MAX_ZIP_DEPTH = 32;
|
|
37
|
+
|
|
38
|
+
const EOCD = 0x06054b50;
|
|
39
|
+
const CENTRAL = 0x02014b50;
|
|
40
|
+
const LOCAL = 0x04034b50;
|
|
41
|
+
const S_IFMT = 0o170000;
|
|
42
|
+
const S_IFLNK = 0o120000;
|
|
43
|
+
const UNIX = 3;
|
|
44
|
+
|
|
45
|
+
/** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
|
|
46
|
+
export class ZipReadError extends Error {
|
|
47
|
+
constructor(message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "ZipReadError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One file entry. `refused` is why the entry is reported and not read, or
|
|
55
|
+
* `null`; `data` is its bytes where it was read.
|
|
56
|
+
*/
|
|
57
|
+
export interface ZipEntry {
|
|
58
|
+
name: string;
|
|
59
|
+
refused: string | null;
|
|
60
|
+
data: Uint8Array | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CRC_TABLE = (() => {
|
|
64
|
+
const table = new Uint32Array(256);
|
|
65
|
+
for (let n = 0; n < 256; n++) {
|
|
66
|
+
let c = n;
|
|
67
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
68
|
+
table[n] = c >>> 0;
|
|
69
|
+
}
|
|
70
|
+
return table;
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
function crc32(bytes: Uint8Array): number {
|
|
74
|
+
let c = 0xffffffff;
|
|
75
|
+
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
76
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function refusal(name: string): string | null {
|
|
80
|
+
if (name.startsWith("/")) return "an absolute name";
|
|
81
|
+
if (name.includes("\\")) return "a name with a backslash";
|
|
82
|
+
if (name.includes("\u0000")) return "a name with a NUL";
|
|
83
|
+
for (const segment of name.split("/")) {
|
|
84
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
85
|
+
return "a name with an empty, . or .. segment";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
92
|
+
if (a.length !== b.length) return false;
|
|
93
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every FILE entry of a ZIP archive, in central-directory order. A directory
|
|
99
|
+
* entry (a name ending in `/`) is not a file and is not returned. Throws
|
|
100
|
+
* `ZipReadError`.
|
|
101
|
+
*/
|
|
102
|
+
export function readZip(data: Uint8Array): ZipEntry[] {
|
|
103
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
104
|
+
const n = data.length;
|
|
105
|
+
const u32 = (at: number) => view.getUint32(at, true);
|
|
106
|
+
const u16 = (at: number) => view.getUint16(at, true);
|
|
107
|
+
// The end-of-central-directory record: 22 bytes plus a comment of at most
|
|
108
|
+
// 65535, searched backwards so the LAST signature wins.
|
|
109
|
+
let eocd = -1;
|
|
110
|
+
const lowest = Math.max(0, n - 22 - 0xffff);
|
|
111
|
+
for (let at = n - 22; at >= lowest; at--) {
|
|
112
|
+
if (u32(at) === EOCD && at + 22 + u16(at + 20) === n) {
|
|
113
|
+
eocd = at;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (eocd < 0) throw new ZipReadError("is not a readable .zip archive");
|
|
118
|
+
const disk = u16(eocd + 4);
|
|
119
|
+
const cdDisk = u16(eocd + 6);
|
|
120
|
+
const hereCount = u16(eocd + 8);
|
|
121
|
+
const count = u16(eocd + 10);
|
|
122
|
+
const cdSize = u32(eocd + 12);
|
|
123
|
+
const cdOffset = u32(eocd + 16);
|
|
124
|
+
if (count === 0xffff || cdSize === 0xffffffff || cdOffset === 0xffffffff) {
|
|
125
|
+
throw new ZipReadError("is a ZIP64 archive, which case does not read");
|
|
126
|
+
}
|
|
127
|
+
if (disk !== 0 || cdDisk !== 0 || hereCount !== count) {
|
|
128
|
+
throw new ZipReadError("spans more than one disk, which case does not read");
|
|
129
|
+
}
|
|
130
|
+
if (count > MAX_ZIP_ENTRIES) {
|
|
131
|
+
throw new ZipReadError(`holds more than ${MAX_ZIP_ENTRIES} entries; refusing to read it`);
|
|
132
|
+
}
|
|
133
|
+
if (cdOffset + cdSize > eocd) throw new ZipReadError("is not a readable .zip archive");
|
|
134
|
+
|
|
135
|
+
const entries: ZipEntry[] = [];
|
|
136
|
+
const names = new Set<string>();
|
|
137
|
+
let total = 0;
|
|
138
|
+
let at = cdOffset;
|
|
139
|
+
for (let e = 0; e < count; e++) {
|
|
140
|
+
if (at + 46 > eocd || u32(at) !== CENTRAL) {
|
|
141
|
+
throw new ZipReadError("is not a readable .zip archive");
|
|
142
|
+
}
|
|
143
|
+
const madeBy = u16(at + 4);
|
|
144
|
+
const flags = u16(at + 8);
|
|
145
|
+
const method = u16(at + 10);
|
|
146
|
+
const crc = u32(at + 16);
|
|
147
|
+
const csize = u32(at + 20);
|
|
148
|
+
const usize = u32(at + 24);
|
|
149
|
+
const nameLen = u16(at + 28);
|
|
150
|
+
const extraLen = u16(at + 30);
|
|
151
|
+
const commentLen = u16(at + 32);
|
|
152
|
+
const external = u32(at + 38);
|
|
153
|
+
const localAt = u32(at + 42);
|
|
154
|
+
const rawName = data.subarray(at + 46, at + 46 + nameLen);
|
|
155
|
+
at += 46 + nameLen + extraLen + commentLen;
|
|
156
|
+
if (at > eocd) throw new ZipReadError("is not a readable .zip archive");
|
|
157
|
+
let name: string;
|
|
158
|
+
try {
|
|
159
|
+
name = utf8Strict(rawName);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new ZipReadError(
|
|
162
|
+
"holds a name that is not valid UTF-8; rename it, then run case again",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (names.has(name)) throw new ZipReadError(`holds ${name} twice`);
|
|
166
|
+
names.add(name);
|
|
167
|
+
if (name.endsWith("/")) continue;
|
|
168
|
+
if (name.split("/").length - 1 > MAX_ZIP_DEPTH) {
|
|
169
|
+
throw new ZipReadError(
|
|
170
|
+
`is nested more than ${MAX_ZIP_DEPTH} folders deep; refusing to read it`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
let refused = refusal(name);
|
|
174
|
+
if (refused === null && flags & 0x1) refused = "an encrypted entry";
|
|
175
|
+
if (
|
|
176
|
+
refused === null &&
|
|
177
|
+
madeBy >> 8 === UNIX &&
|
|
178
|
+
((external >>> 16) & S_IFMT) === S_IFLNK
|
|
179
|
+
) {
|
|
180
|
+
refused = "a symbolic link";
|
|
181
|
+
}
|
|
182
|
+
if (refused === null && method !== 0 && method !== 8) {
|
|
183
|
+
refused = "a compression method other than stored or deflate";
|
|
184
|
+
}
|
|
185
|
+
if (refused !== null) {
|
|
186
|
+
entries.push({ name, refused, data: null });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
total += usize;
|
|
191
|
+
if (total > MAX_ZIP_BYTES) {
|
|
192
|
+
throw new ZipReadError(
|
|
193
|
+
`holds more than ${MAX_ZIP_BYTES} bytes uncompressed; refusing to read it`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const unreadable = () => new ZipReadError(`holds ${name}, which cannot be read`);
|
|
197
|
+
if (localAt + 30 > cdOffset || u32(localAt) !== LOCAL) throw unreadable();
|
|
198
|
+
const localNameLen = u16(localAt + 26);
|
|
199
|
+
const localExtraLen = u16(localAt + 28);
|
|
200
|
+
if (!sameBytes(data.subarray(localAt + 30, localAt + 30 + localNameLen), rawName)) {
|
|
201
|
+
throw unreadable();
|
|
202
|
+
}
|
|
203
|
+
const start = localAt + 30 + localNameLen + localExtraLen;
|
|
204
|
+
if (start + csize > cdOffset) throw unreadable();
|
|
205
|
+
const packed = data.subarray(start, start + csize);
|
|
206
|
+
let body: Uint8Array;
|
|
207
|
+
if (method === 0) {
|
|
208
|
+
body = packed;
|
|
209
|
+
} else {
|
|
210
|
+
// `info: true` hands back the engine, whose `bytesWritten` is how many
|
|
211
|
+
// input bytes the stream consumed. Anything left over is data after the
|
|
212
|
+
// end of the deflate stream, which Python's `unused_data` refuses too.
|
|
213
|
+
let result: { buffer: Buffer; engine: { bytesWritten: number } };
|
|
214
|
+
try {
|
|
215
|
+
result = inflateRawSync(packed, {
|
|
216
|
+
info: true,
|
|
217
|
+
maxOutputLength: usize + 1,
|
|
218
|
+
}) as unknown as { buffer: Buffer; engine: { bytesWritten: number } };
|
|
219
|
+
} catch {
|
|
220
|
+
throw unreadable();
|
|
221
|
+
}
|
|
222
|
+
if (result.engine.bytesWritten !== packed.length) throw unreadable();
|
|
223
|
+
body = new Uint8Array(result.buffer.buffer, result.buffer.byteOffset, result.buffer.length);
|
|
224
|
+
}
|
|
225
|
+
if (body.length !== usize || crc32(body) !== crc) throw unreadable();
|
|
226
|
+
entries.push({ name, refused: null, data: body.slice() });
|
|
227
|
+
}
|
|
228
|
+
return entries;
|
|
229
|
+
}
|
package/vectors/README.md
CHANGED
|
@@ -26,12 +26,33 @@ on a run that contacted none — a false accusation manufactured by a missing
|
|
|
26
26
|
state, and the defect this table was written after. Vendoring rules are the same
|
|
27
27
|
three steps as below, against `vectors/stamp-on-chain.json`.
|
|
28
28
|
|
|
29
|
+
## `witness-cache.json`
|
|
30
|
+
|
|
31
|
+
The rule that decides what a reader does with the witnessed-time cache, the
|
|
32
|
+
unsigned `witness-cache.json` beside a pack's `bundle.json` (`evidence-bundle.md`
|
|
33
|
+
§5.5.6 and §5.6.6, v1.25, A30). Implemented by [`../src/witness-cache.ts`](../src/witness-cache.ts)
|
|
34
|
+
and the `case` command in [`../src/cli.ts`](../src/cli.ts), and asserted against
|
|
35
|
+
this table by
|
|
36
|
+
[`../test/witness-cache-vectors.test.ts`](../test/witness-cache-vectors.test.ts)
|
|
37
|
+
**and by the Python kernel against the same bytes**, in
|
|
38
|
+
`tests/test_witness_cache_vectors.py`. Every expectation is written by hand in
|
|
39
|
+
`tools/gen-vectors/gen_witness_cache_table.py`, which writes this file.
|
|
40
|
+
|
|
41
|
+
**The one thing a consumer must not do with it: print a cached time as
|
|
42
|
+
anything but not checked.** Nobody signs the file, and anyone holding the pack
|
|
43
|
+
can write it. The only time a surface may print as the public record's is one a
|
|
44
|
+
gateway returned; a cached time the record contradicts is a finding, and one no
|
|
45
|
+
lookup settled stays labelled not checked.
|
|
46
|
+
|
|
29
47
|
## `jcs-reader.json`
|
|
30
48
|
|
|
31
49
|
The rule that decides how a kernel reads bytes that claim to be `JCS(value)`
|
|
32
|
-
(RFC 8785).
|
|
33
|
-
**strict UTF-8**, **no leading byte-order mark**, **no non-finite literal**,
|
|
34
|
-
**no
|
|
50
|
+
(RFC 8785). Five checks, none of them left to a built-in decoder's defaults:
|
|
51
|
+
**strict UTF-8**, **no leading byte-order mark**, **no non-finite literal**,
|
|
52
|
+
**no object that names one member twice** (I-JSON, RFC 7493; rule version 3,
|
|
53
|
+
proof#144), and **no unpaired surrogate in the parsed value**. A duplicate is
|
|
54
|
+
refused with the same message in both kernels, `duplicate key "<name>"`, and a
|
|
55
|
+
case that carries `error` pins that exact string. The fourth is not the first one
|
|
35
56
|
twice: a surrogate written as the ASCII escape `\ud800` survives every
|
|
36
57
|
byte-level check, because the bytes are ASCII, and the parser produces the
|
|
37
58
|
malformed code unit afterwards (proof#154).
|
|
@@ -107,17 +128,53 @@ happened, and the reason this file moved here.
|
|
|
107
128
|
|
|
108
129
|
| Key | Meaning |
|
|
109
130
|
|---|---|
|
|
110
|
-
| `ruleVersion` | Bumped when a case's expectation changes **or a case is added**. An implementation asserts the version it was written against, so a table it does not understand fails loudly rather than passing vacuously. Currently **
|
|
111
|
-
| `kernelEventClasses` | The
|
|
131
|
+
| `ruleVersion` | Bumped when a case's expectation changes **or a case is added**. An implementation asserts the version it was written against, so a table it does not understand fails loudly rather than passing vacuously. Currently **6**: `evidence-bundle.md` §5.4.5, where a record withheld by a readable declaration is counted apart and holds the pack at nothing. |
|
|
132
|
+
| `kernelEventClasses` | The six per-event outcome classes a verification result can carry, named so a case can be written as counts rather than as a hand-built bundle. The two added at rule version 6, `withheldBound` and `withheldUnbound`, carry an `eventId` and a `payloadBindingOk`; `howToUse` says how an adapter builds them. |
|
|
112
133
|
| `cases[].kernel` | The verification result to grade, as class counts. |
|
|
113
134
|
| `cases[].logs` | The disclosure account: how many records the bundle presents, and how many disclosed usable bytes. |
|
|
114
135
|
| `cases[].omissions` | Completeness notes — a checkpoint's signed `leaf_count` against the records the pack presents for it. |
|
|
115
|
-
| `cases[].expect` | The verdict, the three counts,
|
|
136
|
+
| `cases[].expect` | The verdict, the three counts, how many causes the grade must carry, and, from rule version 6, `recordsWithheld` where a case pins it. |
|
|
116
137
|
|
|
117
138
|
Nothing in a case names a transport. A directory read and a browser drop produce
|
|
118
139
|
the same account and must produce the same verdict; a table that encoded one
|
|
119
140
|
surface's mechanism would let the other one drift while staying green.
|
|
120
141
|
|
|
142
|
+
## `case-file-index.json`
|
|
143
|
+
|
|
144
|
+
How a reader compares a case file's index, `contents.json`, with the folder it
|
|
145
|
+
sits at the root of (`evidence-bundle.md` §5.6.3, v1.20 A25). One pure function
|
|
146
|
+
in both kernels, `readCaseFileIndex` in [`../src/case-file.ts`](../src/case-file.ts)
|
|
147
|
+
and `read_case_file_index` in `src/sanning_proof/case_file.py`, asserted against
|
|
148
|
+
this table by [`../test/case-file-index.test.ts`](../test/case-file-index.test.ts)
|
|
149
|
+
**and by the Python kernel against the same bytes**, in
|
|
150
|
+
`tests/test_case_file_index.py`. Each row gives the index bytes as hex (or
|
|
151
|
+
`null` for a folder with no index) and every file in the folder with its
|
|
152
|
+
SHA-256, and expects the index state, its `case_id` label and the findings in
|
|
153
|
+
the pinned order. **No finding moves a verdict**: the index is never an input
|
|
154
|
+
to the case reading, and a surface that took one from it would be trusting
|
|
155
|
+
whoever assembled the drop. Written by
|
|
156
|
+
`tools/gen-vectors/gen_case_file_index_vectors.py`, whose expectations are
|
|
157
|
+
literals.
|
|
158
|
+
|
|
159
|
+
## `file-match.json`
|
|
160
|
+
|
|
161
|
+
How a reader matches files handed in beside a pack against the commitments its
|
|
162
|
+
signed records make (`evidence-bundle.md` §5.7, v1.24 A29, proof#92). One pure
|
|
163
|
+
reading in both kernels, `matchFiles` and `matchCaseFiles` in
|
|
164
|
+
[`../src/file-match.ts`](../src/file-match.ts) and `match_files` and
|
|
165
|
+
`match_case_files` in `src/sanning_proof/file_match.py`, asserted against this
|
|
166
|
+
table by [`../test/file-match.test.ts`](../test/file-match.test.ts) **and by the
|
|
167
|
+
Python kernel against the same bytes**, in `tests/test_file_match.py`. `packs`
|
|
168
|
+
holds real signed packs, keyed by name, with any side content beside them as
|
|
169
|
+
`content_hex`; each row names the packs it reads (one for `mode: pack`, several
|
|
170
|
+
for `mode: case`), the files it offers as names and bytes, and the reading it
|
|
171
|
+
expects, with each `not_read` entry's rendered line left out. **A file is
|
|
172
|
+
matched by its SHA-256 and by nothing else**, and **no match moves a verdict**:
|
|
173
|
+
a surface that matched by name, or took a hash from a field no record's key
|
|
174
|
+
signed, would vouch for a file the pack never committed to. Written by
|
|
175
|
+
`tools/gen-vectors/gen_file_match_vectors.py`, whose expectations are written
|
|
176
|
+
by hand from what it built.
|
|
177
|
+
|
|
121
178
|
## `case-split.json`
|
|
122
179
|
|
|
123
180
|
How `case` turns the folders it was handed into packs: the verify page's
|
|
@@ -130,8 +187,9 @@ because the page's code lives in the page. Two tables and the limits:
|
|
|
130
187
|
given as bytes, or a folder filled with N empty files), the arguments, and
|
|
131
188
|
the packs, the outside paths and the paths that are not regular files, or
|
|
132
189
|
the refusal. The rows include the decision file unpacked, a symbolic link
|
|
133
|
-
under `logs/`, each side of the depth and entry limits,
|
|
134
|
-
|
|
190
|
+
under `logs/`, each side of the depth and entry limits, names that are not
|
|
191
|
+
UTF-8, and (v1.20, A25) a case file unpacked and as a `.zip` argument, with
|
|
192
|
+
the entries a reader lists and never reads and the archives it refuses.
|
|
135
193
|
- `parses`: how `bundle.json` bytes become JSON through the reader `verify`
|
|
136
194
|
and `case` share, including each side of the nesting limit.
|
|
137
195
|
- `limits`: the walk depth (32), the entry count (50,000) and the JSON nesting
|