archive-codec 1.1.0 → 1.1.2
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 +14 -4
- package/dist/cfb/detect.cjs +18 -0
- package/dist/cfb/detect.d.cts +4 -0
- package/dist/cfb/detect.d.ts +4 -0
- package/dist/cfb/detect.js +17 -0
- package/dist/cfb/ole-package.cjs +42 -0
- package/dist/cfb/ole-package.d.cts +13 -0
- package/dist/cfb/ole-package.d.ts +13 -0
- package/dist/cfb/ole-package.js +40 -0
- package/dist/cfb/read.cjs +206 -0
- package/dist/cfb/read.d.cts +15 -0
- package/dist/cfb/read.d.ts +15 -0
- package/dist/cfb/read.js +203 -0
- package/dist/index.cjs +20 -343
- package/dist/index.d.cts +6 -61
- package/dist/index.d.ts +6 -61
- package/dist/index.js +6 -329
- package/dist/magic.cjs +9 -0
- package/dist/magic.d.cts +4 -0
- package/dist/magic.d.ts +4 -0
- package/dist/magic.js +8 -0
- package/dist/zip/container.cjs +15 -0
- package/dist/zip/container.d.cts +9 -0
- package/dist/zip/container.d.ts +9 -0
- package/dist/zip/container.js +13 -0
- package/dist/zip/detect.cjs +26 -0
- package/dist/zip/detect.d.cts +6 -0
- package/dist/zip/detect.d.ts +6 -0
- package/dist/zip/detect.js +24 -0
- package/dist/zip/walk.cjs +41 -0
- package/dist/zip/walk.d.cts +20 -0
- package/dist/zip/walk.d.ts +20 -0
- package/dist/zip/walk.js +37 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,330 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
//#endregion
|
|
9
|
-
//#region src/cfb/detect.ts
|
|
10
|
-
const COMPOUND_FILE_MAGIC = [
|
|
11
|
-
208,
|
|
12
|
-
207,
|
|
13
|
-
17,
|
|
14
|
-
224,
|
|
15
|
-
161,
|
|
16
|
-
177,
|
|
17
|
-
26,
|
|
18
|
-
225
|
|
19
|
-
];
|
|
20
|
-
function isCompoundFile(bytes) {
|
|
21
|
-
return startsWithMagic(bytes, COMPOUND_FILE_MAGIC);
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/cfb/ole-package.ts
|
|
25
|
-
var OlePackageFormatError = class extends Error {
|
|
26
|
-
constructor(message) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "OlePackageFormatError";
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
const ANSI_DECODER = new TextDecoder("windows-1252");
|
|
32
|
-
function readZeroTerminated(bytes, offset, fieldName) {
|
|
33
|
-
let end = offset;
|
|
34
|
-
while (end < bytes.length && bytes[end] !== 0) end++;
|
|
35
|
-
if (end >= bytes.length) throw new OlePackageFormatError(`Package stream ends inside its ${fieldName} string with no terminator`);
|
|
36
|
-
return {
|
|
37
|
-
value: ANSI_DECODER.decode(bytes.subarray(offset, end)),
|
|
38
|
-
next: end + 1
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function readOlePackage(bytes) {
|
|
42
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
43
|
-
let offset = 2;
|
|
44
|
-
const label = readZeroTerminated(bytes, offset, "label");
|
|
45
|
-
offset = label.next;
|
|
46
|
-
const sourcePath = readZeroTerminated(bytes, offset, "source path");
|
|
47
|
-
offset = sourcePath.next;
|
|
48
|
-
offset += 8;
|
|
49
|
-
const tempPath = readZeroTerminated(bytes, offset, "temp path");
|
|
50
|
-
offset = tempPath.next;
|
|
51
|
-
if (offset + 4 > bytes.length) throw new OlePackageFormatError("Package stream ends before its packaged file size field");
|
|
52
|
-
const fileByteCount = view.getUint32(offset, true);
|
|
53
|
-
offset += 4;
|
|
54
|
-
if (offset + fileByteCount > bytes.length) throw new OlePackageFormatError(`Package stream declares ${fileByteCount} packaged-file bytes but holds only ${bytes.length - offset}`);
|
|
55
|
-
return {
|
|
56
|
-
label: label.value,
|
|
57
|
-
sourcePath: sourcePath.value,
|
|
58
|
-
tempPath: tempPath.value,
|
|
59
|
-
fileBytes: bytes.slice(offset, offset + fileByteCount)
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/cfb/read.ts
|
|
64
|
-
const ENDOFCHAIN = 4294967294;
|
|
65
|
-
const FREESECT = 4294967295;
|
|
66
|
-
const FATSECT = 4294967293;
|
|
67
|
-
const DIFSECT = 4294967292;
|
|
68
|
-
const NOSTREAM = 4294967295;
|
|
69
|
-
const HEADER_SIZE = 512;
|
|
70
|
-
const HEADER_DIFAT_ENTRIES = 109;
|
|
71
|
-
const OBJECT_TYPE_STORAGE = 1;
|
|
72
|
-
const OBJECT_TYPE_STREAM = 2;
|
|
73
|
-
const OBJECT_TYPE_ROOT = 5;
|
|
74
|
-
const MAX_CFB_TOTAL_STREAM_BYTES = 536870912;
|
|
75
|
-
var CompoundFileFormatError = class extends Error {
|
|
76
|
-
constructor(message) {
|
|
77
|
-
super(message);
|
|
78
|
-
this.name = "CompoundFileFormatError";
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
function u32(view, offset) {
|
|
82
|
-
return view.getUint32(offset, true);
|
|
83
|
-
}
|
|
84
|
-
function u16(view, offset) {
|
|
85
|
-
return view.getUint16(offset, true);
|
|
86
|
-
}
|
|
87
|
-
function readCompoundFile(bytes, options = {}) {
|
|
88
|
-
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
89
|
-
if (!isCompoundFile(bytes)) throw new CompoundFileFormatError("readCompoundFile input does not carry the compound-file signature (leading magic bytes are not D0 CF 11 E0 A1 B1 1A E1)");
|
|
90
|
-
if (bytes.length < HEADER_SIZE) throw new CompoundFileFormatError(`compound file is ${bytes.length} bytes, shorter than the fixed ${HEADER_SIZE}-byte header`);
|
|
91
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
92
|
-
const majorVersion = u16(view, 26);
|
|
93
|
-
if (majorVersion !== 3 && majorVersion !== 4) throw new CompoundFileFormatError(`compound file major version ${majorVersion} is not 3 or 4`);
|
|
94
|
-
if (u16(view, 28) !== 65534) throw new CompoundFileFormatError("compound file byte order is not little-endian");
|
|
95
|
-
const sectorShift = u16(view, 30);
|
|
96
|
-
if (majorVersion === 3 && sectorShift !== 9 || majorVersion === 4 && sectorShift !== 12) throw new CompoundFileFormatError(`compound file sector shift 2^${sectorShift} does not match major version ${majorVersion} (version 3 requires 512-byte sectors, version 4 requires 4096-byte)`);
|
|
97
|
-
const miniSectorShift = u16(view, 32);
|
|
98
|
-
if (miniSectorShift !== 6) throw new CompoundFileFormatError(`compound file mini sector shift 2^${miniSectorShift} is not the mandated 64-byte mini sector`);
|
|
99
|
-
const sectorSize = 1 << sectorShift;
|
|
100
|
-
const miniSectorSize = 1 << miniSectorShift;
|
|
101
|
-
const miniStreamCutoff = u32(view, 56);
|
|
102
|
-
if (miniStreamCutoff < miniSectorSize) throw new CompoundFileFormatError(`compound file mini stream cutoff ${miniStreamCutoff} is smaller than the ${miniSectorSize}-byte mini sector itself`);
|
|
103
|
-
const firstDirectorySector = u32(view, 48);
|
|
104
|
-
const firstMiniFatSector = u32(view, 60);
|
|
105
|
-
const firstDifatSector = u32(view, 68);
|
|
106
|
-
const sectorCount = Math.floor(bytes.length / sectorSize) - 1;
|
|
107
|
-
if (sectorCount < 1) throw new CompoundFileFormatError(`compound file holds no complete ${sectorSize}-byte sector after its header`);
|
|
108
|
-
const sectorOffset = (sector) => (sector + 1) * sectorSize;
|
|
109
|
-
const sectorBytes = (sector) => bytes.subarray(sectorOffset(sector), sectorOffset(sector) + sectorSize);
|
|
110
|
-
const fatSectorIds = [];
|
|
111
|
-
const acceptFatSector = (sector, provenance) => {
|
|
112
|
-
if (sector === FREESECT) return;
|
|
113
|
-
if (sector >= sectorCount) throw new CompoundFileFormatError(`${provenance} names FAT sector ${sector}, which is outside the file's ${sectorCount} sectors`);
|
|
114
|
-
fatSectorIds.push(sector);
|
|
115
|
-
};
|
|
116
|
-
for (let i = 0; i < HEADER_DIFAT_ENTRIES; i++) acceptFatSector(u32(view, 76 + i * 4), "the header DIFAT array");
|
|
117
|
-
let difatSector = firstDifatSector;
|
|
118
|
-
let difatSectorsWalked = 0;
|
|
119
|
-
while (difatSector !== ENDOFCHAIN) {
|
|
120
|
-
if (difatSector >= sectorCount) throw new CompoundFileFormatError(`the DIFAT chain names sector ${difatSector}, which is outside the file's ${sectorCount} sectors`);
|
|
121
|
-
if (++difatSectorsWalked > sectorCount) throw new CompoundFileFormatError("the DIFAT chain visits more sectors than the file holds, so it must cycle");
|
|
122
|
-
const difatView = new DataView(bytes.buffer, bytes.byteOffset + sectorOffset(difatSector), sectorSize);
|
|
123
|
-
const entriesPerDifatSector = sectorSize / 4 - 1;
|
|
124
|
-
for (let i = 0; i < entriesPerDifatSector; i++) acceptFatSector(u32(difatView, i * 4), "a DIFAT sector");
|
|
125
|
-
difatSector = u32(difatView, entriesPerDifatSector * 4);
|
|
126
|
-
}
|
|
127
|
-
if (fatSectorIds.length === 0) throw new CompoundFileFormatError("compound file declares no FAT sectors, so no sector chain can be walked");
|
|
128
|
-
const fatBytes = new Uint8Array(fatSectorIds.length * sectorSize);
|
|
129
|
-
for (let i = 0; i < fatSectorIds.length; i++) fatBytes.set(sectorBytes(fatSectorIds[i] ?? 0), i * sectorSize);
|
|
130
|
-
const fat = new DataView(fatBytes.buffer);
|
|
131
|
-
const fatEntry = (sector) => {
|
|
132
|
-
const offset = sector * 4;
|
|
133
|
-
if (offset < 0 || offset + 4 > fatBytes.length) throw new CompoundFileFormatError(`FAT entry for sector ${sector} lies beyond the sectors the DIFAT named`);
|
|
134
|
-
return fat.getUint32(offset, true);
|
|
135
|
-
};
|
|
136
|
-
const chainSectorIds = (start) => {
|
|
137
|
-
const ids = [];
|
|
138
|
-
let current = start;
|
|
139
|
-
while (current !== ENDOFCHAIN) {
|
|
140
|
-
if (current >= sectorCount) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}, which is outside the file's ${sectorCount} sectors`);
|
|
141
|
-
if (ids.length >= sectorCount) throw new CompoundFileFormatError("a FAT chain visits more sectors than the file holds, so it must cycle");
|
|
142
|
-
ids.push(current);
|
|
143
|
-
const next = fatEntry(current);
|
|
144
|
-
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
145
|
-
current = next;
|
|
146
|
-
}
|
|
147
|
-
return ids;
|
|
148
|
-
};
|
|
149
|
-
const chainBytes = (start) => {
|
|
150
|
-
const ids = chainSectorIds(start);
|
|
151
|
-
const out = new Uint8Array(ids.length * sectorSize);
|
|
152
|
-
for (let i = 0; i < ids.length; i++) out.set(sectorBytes(ids[i] ?? 0), i * sectorSize);
|
|
153
|
-
return out;
|
|
154
|
-
};
|
|
155
|
-
const directoryBytes = chainBytes(firstDirectorySector);
|
|
156
|
-
if (directoryBytes.length === 0) throw new CompoundFileFormatError("compound file has an empty directory chain");
|
|
157
|
-
const directoryView = new DataView(directoryBytes.buffer);
|
|
158
|
-
const entryCount = directoryBytes.length / 128;
|
|
159
|
-
const nameDecoder = new TextDecoder("utf-16le");
|
|
160
|
-
const entries = [];
|
|
161
|
-
for (let id = 0; id < entryCount; id++) {
|
|
162
|
-
const base = id * 128;
|
|
163
|
-
const nameLength = u16(directoryView, base + 64);
|
|
164
|
-
entries.push({
|
|
165
|
-
name: nameDecoder.decode(directoryBytes.subarray(base, base + Math.max(0, nameLength - 2))),
|
|
166
|
-
nameLength,
|
|
167
|
-
objectType: directoryView.getUint8(base + 66),
|
|
168
|
-
leftSibling: u32(directoryView, base + 68),
|
|
169
|
-
rightSibling: u32(directoryView, base + 72),
|
|
170
|
-
child: u32(directoryView, base + 76),
|
|
171
|
-
startSector: u32(directoryView, base + 116),
|
|
172
|
-
size: u32(directoryView, base + 120) + u32(directoryView, base + 124) * 4294967296
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
const root = entries[0];
|
|
176
|
-
if (root === void 0 || root.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError("the first directory entry is not the root storage entry (object type 5), as [MS-CFB] 2.6.1 requires");
|
|
177
|
-
const miniStream = chainBytes(root.startSector).subarray(0, root.size);
|
|
178
|
-
const miniSectorCount = Math.floor(miniStream.length / miniSectorSize);
|
|
179
|
-
const miniFatBytes = chainBytes(firstMiniFatSector);
|
|
180
|
-
const miniFat = new DataView(miniFatBytes.buffer);
|
|
181
|
-
const miniChainSectorIds = (start) => {
|
|
182
|
-
const ids = [];
|
|
183
|
-
let current = start;
|
|
184
|
-
while (current !== ENDOFCHAIN) {
|
|
185
|
-
if (current >= miniSectorCount) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}, which is outside the mini stream's ${miniSectorCount} mini sectors`);
|
|
186
|
-
if (ids.length >= miniSectorCount) throw new CompoundFileFormatError("a mini-FAT chain visits more mini sectors than the mini stream holds, so it must cycle");
|
|
187
|
-
ids.push(current);
|
|
188
|
-
const next = miniFat.getUint32(current * 4, true);
|
|
189
|
-
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
190
|
-
current = next;
|
|
191
|
-
}
|
|
192
|
-
return ids;
|
|
193
|
-
};
|
|
194
|
-
let totalExtractedBytes = 0;
|
|
195
|
-
const extractStream = (entry) => {
|
|
196
|
-
if (entry.size === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
197
|
-
if (entry.size > Number.MAX_SAFE_INTEGER) throw new CompoundFileFormatError(`stream '${entry.name}' declares a size beyond the integer range this reader addresses`);
|
|
198
|
-
let out;
|
|
199
|
-
if (entry.size < miniStreamCutoff) {
|
|
200
|
-
const ids = miniChainSectorIds(entry.startSector);
|
|
201
|
-
const raw = new Uint8Array(ids.length * miniSectorSize);
|
|
202
|
-
for (let i = 0; i < ids.length; i++) {
|
|
203
|
-
const miniSector = ids[i] ?? 0;
|
|
204
|
-
raw.set(miniStream.subarray(miniSector * miniSectorSize, (miniSector + 1) * miniSectorSize), i * miniSectorSize);
|
|
205
|
-
}
|
|
206
|
-
out = raw;
|
|
207
|
-
} else out = chainBytes(entry.startSector);
|
|
208
|
-
if (out.length < entry.size) throw new CompoundFileFormatError(`stream '${entry.name}' declares ${entry.size} bytes but its chain holds only ${out.length}`);
|
|
209
|
-
totalExtractedBytes += entry.size;
|
|
210
|
-
if (totalExtractedBytes > maxTotalBytes) throw new CompoundFileFormatError(`cumulative extracted stream size exceeded the ${maxTotalBytes}-byte budget at '${entry.name}'`);
|
|
211
|
-
return out.slice(0, entry.size);
|
|
212
|
-
};
|
|
213
|
-
const streams = [];
|
|
214
|
-
const visited = /* @__PURE__ */ new Set();
|
|
215
|
-
const stack = [{
|
|
216
|
-
id: root.child,
|
|
217
|
-
prefix: "",
|
|
218
|
-
stage: "descend"
|
|
219
|
-
}];
|
|
220
|
-
for (let frame = stack.pop(); frame !== void 0; frame = stack.pop()) if (frame.stage === "descend") {
|
|
221
|
-
const { id, prefix } = frame;
|
|
222
|
-
if (id === NOSTREAM) continue;
|
|
223
|
-
const entry = entries[id];
|
|
224
|
-
if (id >= entryCount || entry === void 0) throw new CompoundFileFormatError(`the directory tree links to entry ${id}, which is outside the directory's ${entryCount} entries`);
|
|
225
|
-
if (visited.has(id)) throw new CompoundFileFormatError(`the directory tree reaches entry ${id} twice, so its sibling and child links cycle`);
|
|
226
|
-
visited.add(id);
|
|
227
|
-
if (entry.nameLength < 2 || entry.nameLength > 64 || entry.nameLength % 2 === 1) throw new CompoundFileFormatError(`directory entry ${id} declares name length ${entry.nameLength}, which is not an even byte count between 2 and 64`);
|
|
228
|
-
if (entry.objectType !== OBJECT_TYPE_STORAGE && entry.objectType !== OBJECT_TYPE_STREAM && entry.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError(`directory entry ${id} ('${entry.name}') carries object type ${entry.objectType}, which is not a storage (1), stream (2), or root (5) entry`);
|
|
229
|
-
stack.push({
|
|
230
|
-
entry,
|
|
231
|
-
prefix,
|
|
232
|
-
stage: "self"
|
|
233
|
-
});
|
|
234
|
-
stack.push({
|
|
235
|
-
id: entry.leftSibling,
|
|
236
|
-
prefix,
|
|
237
|
-
stage: "descend"
|
|
238
|
-
});
|
|
239
|
-
} else if (frame.entry.objectType === OBJECT_TYPE_STREAM) {
|
|
240
|
-
streams.push({
|
|
241
|
-
path: frame.prefix + frame.entry.name,
|
|
242
|
-
bytes: extractStream(frame.entry)
|
|
243
|
-
});
|
|
244
|
-
stack.push({
|
|
245
|
-
id: frame.entry.rightSibling,
|
|
246
|
-
prefix: frame.prefix,
|
|
247
|
-
stage: "descend"
|
|
248
|
-
});
|
|
249
|
-
} else if (frame.entry.objectType === OBJECT_TYPE_STORAGE) {
|
|
250
|
-
stack.push({
|
|
251
|
-
id: frame.entry.rightSibling,
|
|
252
|
-
prefix: frame.prefix,
|
|
253
|
-
stage: "descend"
|
|
254
|
-
});
|
|
255
|
-
stack.push({
|
|
256
|
-
id: frame.entry.child,
|
|
257
|
-
prefix: `${frame.prefix}${frame.entry.name}/`,
|
|
258
|
-
stage: "descend"
|
|
259
|
-
});
|
|
260
|
-
} else throw new CompoundFileFormatError(`the directory tree reaches entry ${frame.entry.name}, which is not a storage or stream entry`);
|
|
261
|
-
return streams;
|
|
262
|
-
}
|
|
263
|
-
//#endregion
|
|
264
|
-
//#region src/zip/container.ts
|
|
265
|
-
function unzipPackage(bytes) {
|
|
266
|
-
return unzipSync(bytes);
|
|
267
|
-
}
|
|
268
|
-
function zipPackage(entries) {
|
|
269
|
-
const data = {};
|
|
270
|
-
for (const [path, entry] of entries) if (entry.stored === true) data[path] = [entry.bytes, { level: 0 }];
|
|
271
|
-
else data[path] = entry.bytes;
|
|
272
|
-
return zipSync(data);
|
|
273
|
-
}
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/zip/detect.ts
|
|
276
|
-
const ZIP_LOCAL_FILE_HEADER_MAGIC = [
|
|
277
|
-
80,
|
|
278
|
-
75,
|
|
279
|
-
3,
|
|
280
|
-
4
|
|
281
|
-
];
|
|
282
|
-
const ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC = [
|
|
283
|
-
80,
|
|
284
|
-
75,
|
|
285
|
-
5,
|
|
286
|
-
6
|
|
287
|
-
];
|
|
288
|
-
function isZipArchive(bytes) {
|
|
289
|
-
return startsWithMagic(bytes, ZIP_LOCAL_FILE_HEADER_MAGIC) || startsWithMagic(bytes, ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC);
|
|
290
|
-
}
|
|
291
|
-
function detectArchiveFormat(bytes) {
|
|
292
|
-
if (isZipArchive(bytes)) return "zip";
|
|
293
|
-
return isCompoundFile(bytes) ? "cfb" : "unknown";
|
|
294
|
-
}
|
|
295
|
-
//#endregion
|
|
296
|
-
//#region src/zip/walk.ts
|
|
297
|
-
const MAX_WALK_DEPTH = 8;
|
|
298
|
-
const MAX_WALK_TOTAL_BYTES = 536870912;
|
|
299
|
-
var ArchiveWalkLimitError = class extends Error {
|
|
300
|
-
limit;
|
|
301
|
-
constructor(limit, message) {
|
|
302
|
-
super(message);
|
|
303
|
-
this.name = "ArchiveWalkLimitError";
|
|
304
|
-
this.limit = limit;
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
function walkArchive(bytes, options = {}) {
|
|
308
|
-
const maxDepth = options.maxDepth ?? 8;
|
|
309
|
-
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
310
|
-
if (!isZipArchive(bytes)) throw new Error("walkArchive input is not a ZIP archive (leading magic bytes are not PK\\x03\\x04 or PK\\x05\\x06)");
|
|
311
|
-
const entries = [];
|
|
312
|
-
let totalBytes = 0;
|
|
313
|
-
visit(bytes, []);
|
|
314
|
-
return entries;
|
|
315
|
-
function visit(archive, ancestors) {
|
|
316
|
-
if (ancestors.length + 1 > maxDepth) throw new ArchiveWalkLimitError("depth", `archive nesting exceeds the maximum walk depth of ${maxDepth} at ${ancestors.join(" > ")}`);
|
|
317
|
-
for (const [path, entryBytes] of Object.entries(unzipPackage(archive))) {
|
|
318
|
-
totalBytes += entryBytes.length;
|
|
319
|
-
if (totalBytes > maxTotalBytes) throw new ArchiveWalkLimitError("total-bytes", `cumulative decompressed size of the walk exceeded the ${maxTotalBytes}-byte budget at ${path}`);
|
|
320
|
-
entries.push({
|
|
321
|
-
path,
|
|
322
|
-
ancestors,
|
|
323
|
-
bytes: entryBytes
|
|
324
|
-
});
|
|
325
|
-
if (isZipArchive(entryBytes)) visit(entryBytes, [...ancestors, path]);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
//#endregion
|
|
1
|
+
import { isCompoundFile } from "./cfb/detect.js";
|
|
2
|
+
import { OlePackageFormatError, readOlePackage } from "./cfb/ole-package.js";
|
|
3
|
+
import { CompoundFileFormatError, MAX_CFB_TOTAL_STREAM_BYTES, readCompoundFile } from "./cfb/read.js";
|
|
4
|
+
import { unzipPackage, zipPackage } from "./zip/container.js";
|
|
5
|
+
import { detectArchiveFormat, isZipArchive } from "./zip/detect.js";
|
|
6
|
+
import { ArchiveWalkLimitError, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, walkArchive } from "./zip/walk.js";
|
|
330
7
|
export { ArchiveWalkLimitError, CompoundFileFormatError, MAX_CFB_TOTAL_STREAM_BYTES, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, OlePackageFormatError, detectArchiveFormat, isCompoundFile, isZipArchive, readCompoundFile, readOlePackage, unzipPackage, walkArchive, zipPackage };
|
package/dist/magic.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/magic.ts
|
|
3
|
+
function startsWithMagic(bytes, magic) {
|
|
4
|
+
if (bytes.length < magic.length) return false;
|
|
5
|
+
for (let i = 0; i < magic.length; i++) if (bytes[i] !== magic[i]) return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
exports.startsWithMagic = startsWithMagic;
|
package/dist/magic.d.cts
ADDED
package/dist/magic.d.ts
ADDED
package/dist/magic.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let fflate = require("fflate");
|
|
3
|
+
//#region src/zip/container.ts
|
|
4
|
+
function unzipPackage(bytes) {
|
|
5
|
+
return (0, fflate.unzipSync)(bytes);
|
|
6
|
+
}
|
|
7
|
+
function zipPackage(entries) {
|
|
8
|
+
const data = {};
|
|
9
|
+
for (const [path, entry] of entries) if (entry.stored === true) data[path] = [entry.bytes, { level: 0 }];
|
|
10
|
+
else data[path] = entry.bytes;
|
|
11
|
+
return (0, fflate.zipSync)(data);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
exports.unzipPackage = unzipPackage;
|
|
15
|
+
exports.zipPackage = zipPackage;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/zip/container.d.ts
|
|
2
|
+
interface ZipEntry {
|
|
3
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
4
|
+
readonly stored?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare function unzipPackage(bytes: Uint8Array<ArrayBuffer>): Record<string, Uint8Array<ArrayBuffer>>;
|
|
7
|
+
declare function zipPackage(entries: readonly (readonly [string, ZipEntry])[]): Uint8Array<ArrayBuffer>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ZipEntry, unzipPackage, zipPackage };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/zip/container.d.ts
|
|
2
|
+
interface ZipEntry {
|
|
3
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
4
|
+
readonly stored?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare function unzipPackage(bytes: Uint8Array<ArrayBuffer>): Record<string, Uint8Array<ArrayBuffer>>;
|
|
7
|
+
declare function zipPackage(entries: readonly (readonly [string, ZipEntry])[]): Uint8Array<ArrayBuffer>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ZipEntry, unzipPackage, zipPackage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { unzipSync, zipSync } from "fflate";
|
|
2
|
+
//#region src/zip/container.ts
|
|
3
|
+
function unzipPackage(bytes) {
|
|
4
|
+
return unzipSync(bytes);
|
|
5
|
+
}
|
|
6
|
+
function zipPackage(entries) {
|
|
7
|
+
const data = {};
|
|
8
|
+
for (const [path, entry] of entries) if (entry.stored === true) data[path] = [entry.bytes, { level: 0 }];
|
|
9
|
+
else data[path] = entry.bytes;
|
|
10
|
+
return zipSync(data);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { unzipPackage, zipPackage };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_magic = require("../magic.cjs");
|
|
3
|
+
const require_cfb_detect = require("../cfb/detect.cjs");
|
|
4
|
+
//#region src/zip/detect.ts
|
|
5
|
+
const ZIP_LOCAL_FILE_HEADER_MAGIC = [
|
|
6
|
+
80,
|
|
7
|
+
75,
|
|
8
|
+
3,
|
|
9
|
+
4
|
|
10
|
+
];
|
|
11
|
+
const ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC = [
|
|
12
|
+
80,
|
|
13
|
+
75,
|
|
14
|
+
5,
|
|
15
|
+
6
|
|
16
|
+
];
|
|
17
|
+
function isZipArchive(bytes) {
|
|
18
|
+
return require_magic.startsWithMagic(bytes, ZIP_LOCAL_FILE_HEADER_MAGIC) || require_magic.startsWithMagic(bytes, ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC);
|
|
19
|
+
}
|
|
20
|
+
function detectArchiveFormat(bytes) {
|
|
21
|
+
if (isZipArchive(bytes)) return "zip";
|
|
22
|
+
return require_cfb_detect.isCompoundFile(bytes) ? "cfb" : "unknown";
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
exports.detectArchiveFormat = detectArchiveFormat;
|
|
26
|
+
exports.isZipArchive = isZipArchive;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/zip/detect.d.ts
|
|
2
|
+
type ArchiveFormat = 'zip' | 'cfb' | 'unknown';
|
|
3
|
+
declare function isZipArchive(bytes: Uint8Array): boolean;
|
|
4
|
+
declare function detectArchiveFormat(bytes: Uint8Array): ArchiveFormat;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { ArchiveFormat, detectArchiveFormat, isZipArchive };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/zip/detect.d.ts
|
|
2
|
+
type ArchiveFormat = 'zip' | 'cfb' | 'unknown';
|
|
3
|
+
declare function isZipArchive(bytes: Uint8Array): boolean;
|
|
4
|
+
declare function detectArchiveFormat(bytes: Uint8Array): ArchiveFormat;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { ArchiveFormat, detectArchiveFormat, isZipArchive };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { startsWithMagic } from "../magic.js";
|
|
2
|
+
import { isCompoundFile } from "../cfb/detect.js";
|
|
3
|
+
//#region src/zip/detect.ts
|
|
4
|
+
const ZIP_LOCAL_FILE_HEADER_MAGIC = [
|
|
5
|
+
80,
|
|
6
|
+
75,
|
|
7
|
+
3,
|
|
8
|
+
4
|
|
9
|
+
];
|
|
10
|
+
const ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC = [
|
|
11
|
+
80,
|
|
12
|
+
75,
|
|
13
|
+
5,
|
|
14
|
+
6
|
|
15
|
+
];
|
|
16
|
+
function isZipArchive(bytes) {
|
|
17
|
+
return startsWithMagic(bytes, ZIP_LOCAL_FILE_HEADER_MAGIC) || startsWithMagic(bytes, ZIP_END_OF_CENTRAL_DIRECTORY_MAGIC);
|
|
18
|
+
}
|
|
19
|
+
function detectArchiveFormat(bytes) {
|
|
20
|
+
if (isZipArchive(bytes)) return "zip";
|
|
21
|
+
return isCompoundFile(bytes) ? "cfb" : "unknown";
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { detectArchiveFormat, isZipArchive };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_zip_container = require("./container.cjs");
|
|
3
|
+
const require_zip_detect = require("./detect.cjs");
|
|
4
|
+
//#region src/zip/walk.ts
|
|
5
|
+
const MAX_WALK_DEPTH = 8;
|
|
6
|
+
const MAX_WALK_TOTAL_BYTES = 536870912;
|
|
7
|
+
var ArchiveWalkLimitError = class extends Error {
|
|
8
|
+
limit;
|
|
9
|
+
constructor(limit, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ArchiveWalkLimitError";
|
|
12
|
+
this.limit = limit;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function walkArchive(bytes, options = {}) {
|
|
16
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
17
|
+
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
18
|
+
if (!require_zip_detect.isZipArchive(bytes)) throw new Error("walkArchive input is not a ZIP archive (leading magic bytes are not PK\\x03\\x04 or PK\\x05\\x06)");
|
|
19
|
+
const entries = [];
|
|
20
|
+
let totalBytes = 0;
|
|
21
|
+
visit(bytes, []);
|
|
22
|
+
return entries;
|
|
23
|
+
function visit(archive, ancestors) {
|
|
24
|
+
if (ancestors.length + 1 > maxDepth) throw new ArchiveWalkLimitError("depth", `archive nesting exceeds the maximum walk depth of ${maxDepth} at ${ancestors.join(" > ")}`);
|
|
25
|
+
for (const [path, entryBytes] of Object.entries(require_zip_container.unzipPackage(archive))) {
|
|
26
|
+
totalBytes += entryBytes.length;
|
|
27
|
+
if (totalBytes > maxTotalBytes) throw new ArchiveWalkLimitError("total-bytes", `cumulative decompressed size of the walk exceeded the ${maxTotalBytes}-byte budget at ${path}`);
|
|
28
|
+
entries.push({
|
|
29
|
+
path,
|
|
30
|
+
ancestors,
|
|
31
|
+
bytes: entryBytes
|
|
32
|
+
});
|
|
33
|
+
if (require_zip_detect.isZipArchive(entryBytes)) visit(entryBytes, [...ancestors, path]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
exports.ArchiveWalkLimitError = ArchiveWalkLimitError;
|
|
39
|
+
exports.MAX_WALK_DEPTH = MAX_WALK_DEPTH;
|
|
40
|
+
exports.MAX_WALK_TOTAL_BYTES = MAX_WALK_TOTAL_BYTES;
|
|
41
|
+
exports.walkArchive = walkArchive;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/zip/walk.d.ts
|
|
2
|
+
declare const MAX_WALK_DEPTH = 8;
|
|
3
|
+
declare const MAX_WALK_TOTAL_BYTES: number;
|
|
4
|
+
interface ArchiveWalkEntry {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly ancestors: readonly string[];
|
|
7
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
type ArchiveWalkLimit = 'depth' | 'total-bytes';
|
|
10
|
+
declare class ArchiveWalkLimitError extends Error {
|
|
11
|
+
readonly limit: ArchiveWalkLimit;
|
|
12
|
+
constructor(limit: ArchiveWalkLimit, message: string);
|
|
13
|
+
}
|
|
14
|
+
interface WalkArchiveOptions {
|
|
15
|
+
readonly maxDepth?: number;
|
|
16
|
+
readonly maxTotalBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function walkArchive(bytes: Uint8Array<ArrayBuffer>, options?: WalkArchiveOptions): ArchiveWalkEntry[];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ArchiveWalkEntry, ArchiveWalkLimit, ArchiveWalkLimitError, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, WalkArchiveOptions, walkArchive };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/zip/walk.d.ts
|
|
2
|
+
declare const MAX_WALK_DEPTH = 8;
|
|
3
|
+
declare const MAX_WALK_TOTAL_BYTES: number;
|
|
4
|
+
interface ArchiveWalkEntry {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly ancestors: readonly string[];
|
|
7
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
type ArchiveWalkLimit = 'depth' | 'total-bytes';
|
|
10
|
+
declare class ArchiveWalkLimitError extends Error {
|
|
11
|
+
readonly limit: ArchiveWalkLimit;
|
|
12
|
+
constructor(limit: ArchiveWalkLimit, message: string);
|
|
13
|
+
}
|
|
14
|
+
interface WalkArchiveOptions {
|
|
15
|
+
readonly maxDepth?: number;
|
|
16
|
+
readonly maxTotalBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function walkArchive(bytes: Uint8Array<ArrayBuffer>, options?: WalkArchiveOptions): ArchiveWalkEntry[];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ArchiveWalkEntry, ArchiveWalkLimit, ArchiveWalkLimitError, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, WalkArchiveOptions, walkArchive };
|
package/dist/zip/walk.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { unzipPackage } from "./container.js";
|
|
2
|
+
import { isZipArchive } from "./detect.js";
|
|
3
|
+
//#region src/zip/walk.ts
|
|
4
|
+
const MAX_WALK_DEPTH = 8;
|
|
5
|
+
const MAX_WALK_TOTAL_BYTES = 536870912;
|
|
6
|
+
var ArchiveWalkLimitError = class extends Error {
|
|
7
|
+
limit;
|
|
8
|
+
constructor(limit, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ArchiveWalkLimitError";
|
|
11
|
+
this.limit = limit;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function walkArchive(bytes, options = {}) {
|
|
15
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
16
|
+
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
17
|
+
if (!isZipArchive(bytes)) throw new Error("walkArchive input is not a ZIP archive (leading magic bytes are not PK\\x03\\x04 or PK\\x05\\x06)");
|
|
18
|
+
const entries = [];
|
|
19
|
+
let totalBytes = 0;
|
|
20
|
+
visit(bytes, []);
|
|
21
|
+
return entries;
|
|
22
|
+
function visit(archive, ancestors) {
|
|
23
|
+
if (ancestors.length + 1 > maxDepth) throw new ArchiveWalkLimitError("depth", `archive nesting exceeds the maximum walk depth of ${maxDepth} at ${ancestors.join(" > ")}`);
|
|
24
|
+
for (const [path, entryBytes] of Object.entries(unzipPackage(archive))) {
|
|
25
|
+
totalBytes += entryBytes.length;
|
|
26
|
+
if (totalBytes > maxTotalBytes) throw new ArchiveWalkLimitError("total-bytes", `cumulative decompressed size of the walk exceeded the ${maxTotalBytes}-byte budget at ${path}`);
|
|
27
|
+
entries.push({
|
|
28
|
+
path,
|
|
29
|
+
ancestors,
|
|
30
|
+
bytes: entryBytes
|
|
31
|
+
});
|
|
32
|
+
if (isZipArchive(entryBytes)) visit(entryBytes, [...ancestors, path]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { ArchiveWalkLimitError, MAX_WALK_DEPTH, MAX_WALK_TOTAL_BYTES, walkArchive };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archive-codec",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "ZIP-in-ZIP recursive walking with depth and cumulative decompressed-size guards, plus bounded classic OLE compound-file ([MS-CFB]) reading - zero document-format knowledge, the archive and container utility package for the documents.js family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -57,10 +57,12 @@
|
|
|
57
57
|
"_typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json",
|
|
58
58
|
"_typecheck:attw": "attw --pack",
|
|
59
59
|
"test": "turbo run _test",
|
|
60
|
-
"_test": "vitest run",
|
|
61
|
-
"test:watch": "vitest",
|
|
60
|
+
"_test": "vitest run --project unit",
|
|
61
|
+
"test:watch": "vitest --project unit",
|
|
62
62
|
"test:workers": "turbo run _test:workers",
|
|
63
63
|
"_test:workers": "vitest run --config vitest.workers.config.ts",
|
|
64
|
+
"test:smoke": "turbo run _test:smoke",
|
|
65
|
+
"_test:smoke": "vitest run --project smoke",
|
|
64
66
|
"prepare": "husky"
|
|
65
67
|
},
|
|
66
68
|
"packageManager": "pnpm@11.6.0",
|