@stigmer/zip-structure 3.13.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Policy-free structural ZIP parsing: end-of-central-directory (EOCD)
3
+ * location plus central-directory walk, producing one record per entry.
4
+ *
5
+ * Lived in the runner (src/shared/zip-structure.ts) until the TS server's
6
+ * skill push gate became its second consumer; extracted to backend/libs/ts/
7
+ * per the program's shared-library rule (a library moves here when a second
8
+ * consumer exists — @stigmer/temporal-codecs is the precedent).
9
+ *
10
+ * Parsing is *central-directory-based* — never a front-to-back walk of
11
+ * local file headers. This is a correctness decision, not a style choice
12
+ * (issues #450 and #567): local headers of streaming entries
13
+ * (general-purpose flag bit 3) carry zeroed sizes, and the only
14
+ * local-only way to recover them is scanning the payload for the
15
+ * data-descriptor signature — which silently truncates any stored entry
16
+ * whose *content* happens to contain those four bytes, and
17
+ * desynchronizes every entry after it. The central directory always
18
+ * carries the real sizes, and every consumer here holds the complete
19
+ * archive bytes, so nothing a local-header walk could offer is needed.
20
+ *
21
+ * This layer maps bytes to entry records and nothing else. Policy — what
22
+ * a structural failure means, which entries are acceptable, how payloads
23
+ * are decoded — belongs to the consumers, and they differ on purpose:
24
+ *
25
+ * - runner shared/zip-extract.ts (skill artifacts): structural failure
26
+ * is NON-FATAL — both editions' push gates validated every artifact
27
+ * with a central-directory-based reader before storage, so a defect
28
+ * here can only be a truncated or corrupted download.
29
+ * - runner execute-deep-agent/attachment-injector.ts (user
30
+ * attachments): structural failure is FAIL-HARD — the input is an
31
+ * untrusted upload and nothing upstream vouched for it.
32
+ * - stigmer-server src/domain/skill/storage/ (the push gate):
33
+ * FAIL-HARD, plus a safearchive-parity entry pre-filter decoded from
34
+ * the raw creator/attribute fields exposed below.
35
+ *
36
+ * ZIP64 is deliberately unsupported: every consumer caps input far below
37
+ * every ZIP64 threshold (skill push gates: 100MB / 10,000 files;
38
+ * attachment uploads: 10MB).
39
+ *
40
+ * Throws plain `Error`s on structural defects (no valid EOCD, a central
41
+ * directory or local header record that does not parse, a payload slice
42
+ * that runs past the end of the buffer); consumers translate those into
43
+ * their own error models.
44
+ */
45
+ // ─── Format constants ────────────────────────────────────────────────────
46
+ const LOCAL_FILE_HEADER_SIG = 0x04034b50;
47
+ const CENTRAL_DIRECTORY_SIG = 0x02014b50;
48
+ const EOCD_SIG = 0x06054b50;
49
+ /** Fixed size of the EOCD record, excluding the variable-length comment. */
50
+ export const EOCD_MIN_SIZE = 22;
51
+ /** The archive comment length is a u16, so the EOCD sits within the last 64KB + 22 bytes. */
52
+ const EOCD_MAX_COMMENT = 0xffff;
53
+ const LOCAL_HEADER_SIZE = 30;
54
+ const CD_RECORD_SIZE = 46;
55
+ // ─── Parsing ─────────────────────────────────────────────────────────────
56
+ /**
57
+ * Enumerate the archive's entries from its central directory.
58
+ *
59
+ * Throws on any structural defect: no valid EOCD, a central directory or
60
+ * local header record that doesn't parse, or a payload slice that runs
61
+ * past the end of the buffer.
62
+ */
63
+ export function parseZipStructure(data) {
64
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
65
+ const eocd = findEndOfCentralDirectory(data, view);
66
+ const entries = [];
67
+ let offset = eocd.centralDirectoryOffset;
68
+ for (let i = 0; i < eocd.entryCount; i++) {
69
+ if (offset + CD_RECORD_SIZE > data.length || view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIG) {
70
+ throw new Error(`invalid central directory record at offset ${offset}`);
71
+ }
72
+ const versionMadeBy = view.getUint16(offset + 4, true);
73
+ const compressionMethod = view.getUint16(offset + 10, true);
74
+ const crc32 = view.getUint32(offset + 16, true);
75
+ const compressedSize = view.getUint32(offset + 20, true);
76
+ const uncompressedSize = view.getUint32(offset + 24, true);
77
+ const fileNameLength = view.getUint16(offset + 28, true);
78
+ const extraFieldLength = view.getUint16(offset + 30, true);
79
+ const commentLength = view.getUint16(offset + 32, true);
80
+ const externalAttributes = view.getUint32(offset + 38, true);
81
+ const localHeaderOffset = view.getUint32(offset + 42, true);
82
+ const fileName = new TextDecoder().decode(data.subarray(offset + CD_RECORD_SIZE, offset + CD_RECORD_SIZE + fileNameLength));
83
+ // The local header is consulted for one thing only: the payload start.
84
+ // Its name/extra field lengths can legitimately differ from the central
85
+ // directory's (streaming writers pad the extra field), so the offset
86
+ // cannot be derived from CD fields alone.
87
+ const dataStart = payloadStart(data, view, localHeaderOffset, fileName);
88
+ if (dataStart + compressedSize > data.length) {
89
+ throw new Error(`entry "${fileName}" payload runs past end of archive`);
90
+ }
91
+ entries.push({
92
+ name: fileName,
93
+ isDirectory: fileName.endsWith("/"),
94
+ compressionMethod,
95
+ uncompressedSize,
96
+ compressedSize,
97
+ crc32,
98
+ versionMadeBy,
99
+ externalAttributes,
100
+ compressedData: data.subarray(dataStart, dataStart + compressedSize),
101
+ });
102
+ offset += CD_RECORD_SIZE + fileNameLength + extraFieldLength + commentLength;
103
+ }
104
+ return entries;
105
+ }
106
+ /**
107
+ * Locate and validate the end-of-central-directory record.
108
+ *
109
+ * Scans backward from the end of the archive across the maximum comment
110
+ * span. A signature match alone is not trusted (the four bytes can occur
111
+ * inside a trailing comment): the record must also point at an offset
112
+ * that actually holds a central directory record, or be a genuinely
113
+ * empty archive.
114
+ */
115
+ function findEndOfCentralDirectory(data, view) {
116
+ const scanFloor = Math.max(0, data.length - EOCD_MIN_SIZE - EOCD_MAX_COMMENT);
117
+ for (let pos = data.length - EOCD_MIN_SIZE; pos >= scanFloor; pos--) {
118
+ if (view.getUint32(pos, true) !== EOCD_SIG)
119
+ continue;
120
+ const entryCount = view.getUint16(pos + 10, true);
121
+ const centralDirectorySize = view.getUint32(pos + 12, true);
122
+ const centralDirectoryOffset = view.getUint32(pos + 16, true);
123
+ const directoryEndsAtRecord = centralDirectoryOffset + centralDirectorySize <= pos;
124
+ const directoryLooksReal = entryCount === 0 ||
125
+ (centralDirectoryOffset + 4 <= data.length &&
126
+ view.getUint32(centralDirectoryOffset, true) === CENTRAL_DIRECTORY_SIG);
127
+ if (directoryEndsAtRecord && directoryLooksReal) {
128
+ return { centralDirectoryOffset, entryCount };
129
+ }
130
+ }
131
+ throw new Error("no end-of-central-directory record found");
132
+ }
133
+ // ─── CRC-32 ──────────────────────────────────────────────────────────────
134
+ // Standard CRC-32 (IEEE 802.3, the ZIP checksum), table-driven so callers
135
+ // hashing payloads at the consumers' 100 MB caps stay cheap.
136
+ const CRC_TABLE = (() => {
137
+ const table = new Uint32Array(256);
138
+ for (let n = 0; n < 256; n++) {
139
+ let c = n;
140
+ for (let bit = 0; bit < 8; bit++) {
141
+ c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
142
+ }
143
+ table[n] = c;
144
+ }
145
+ return table;
146
+ })();
147
+ /**
148
+ * CRC-32 of `data` — the ZIP checksum. Exported for consumers that verify
149
+ * decompressed payloads against an entry's declared `crc32` (Go's
150
+ * archive/zip does this on every read) and for the fixture builder.
151
+ */
152
+ export function crc32(data) {
153
+ let crc = 0xffffffff;
154
+ for (let i = 0; i < data.length; i++) {
155
+ crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ data[i]) & 0xff];
156
+ }
157
+ return (crc ^ 0xffffffff) >>> 0;
158
+ }
159
+ /** Resolve where an entry's payload begins, from its local file header. */
160
+ function payloadStart(data, view, localHeaderOffset, fileName) {
161
+ if (localHeaderOffset + LOCAL_HEADER_SIZE > data.length ||
162
+ view.getUint32(localHeaderOffset, true) !== LOCAL_FILE_HEADER_SIG) {
163
+ throw new Error(`entry "${fileName}" has no local file header at offset ${localHeaderOffset}`);
164
+ }
165
+ const localNameLength = view.getUint16(localHeaderOffset + 26, true);
166
+ const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
167
+ return localHeaderOffset + LOCAL_HEADER_SIZE + localNameLength + localExtraLength;
168
+ }
169
+ //# sourceMappingURL=zip-structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-structure.js","sourceRoot":"","sources":["../src/zip-structure.ts"],"names":[],"mappings":"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"}