@xagents_in/unmask 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +128 -2
- package/dist/cli/hosted.mjs +251 -0
- package/dist/cli/main.mjs +210 -0
- package/dist/core/acquire.mjs +283 -0
- package/dist/core/archive.mjs +485 -0
- package/dist/core/compare.mjs +230 -0
- package/dist/core/manifests.mjs +548 -0
- package/dist/core/online.mjs +310 -0
- package/dist/core/pipeline.mjs +345 -0
- package/dist/core/policy.mjs +92 -0
- package/dist/core/report.mjs +506 -0
- package/dist/core/resolve.mjs +186 -0
- package/dist/core/source.mjs +106 -0
- package/dist/core/syntax.mjs +340 -0
- package/dist/model/client.mjs +1257 -0
- package/dist/model/config.mjs +90 -0
- package/dist/server/app.mjs +299 -0
- package/dist/server/main.mjs +146 -0
- package/dist/server/queue.mjs +177 -0
- package/dist/server/runner.mjs +280 -0
- package/dist/server/store.mjs +105 -0
- package/dist/server/worker.mjs +57 -0
- package/dist/version.json +1 -0
- package/dist/viewer/render.mjs +255 -0
- package/dist/viewer/shell.mjs +73 -0
- package/package.json +39 -4
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createGunzip } from "node:zlib";
|
|
3
|
+
import { Readable, Transform } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import tar from "tar-stream";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
8
|
+
compressedBytes: 50 * 1024 * 1024,
|
|
9
|
+
expandedBytes: 250 * 1024 * 1024,
|
|
10
|
+
entries: 20000,
|
|
11
|
+
ratio: 100,
|
|
12
|
+
fileBytes: 10 * 1024 * 1024,
|
|
13
|
+
parseMs: 30000,
|
|
14
|
+
metadataBytes: 65536,
|
|
15
|
+
pathBytes: 4096,
|
|
16
|
+
pathDepth: 64,
|
|
17
|
+
pathNodes: 40000,
|
|
18
|
+
});
|
|
19
|
+
class InputFailure extends Error {
|
|
20
|
+
constructor(code, message, path) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.failure = { code, message, ...(path === undefined ? {} : { path }) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function fail(code, message, path) {
|
|
26
|
+
throw new InputFailure(code, message, path);
|
|
27
|
+
}
|
|
28
|
+
function safePath(value, limits) {
|
|
29
|
+
if (typeof value === "string" && Buffer.byteLength(value) > limits.pathBytes)
|
|
30
|
+
fail("path-cap", "Archive path byte limit exceeded");
|
|
31
|
+
if (
|
|
32
|
+
typeof value !== "string" ||
|
|
33
|
+
!value ||
|
|
34
|
+
/[\0\\]/u.test(value) ||
|
|
35
|
+
value.startsWith("/") ||
|
|
36
|
+
/^[a-z]:/iu.test(value)
|
|
37
|
+
)
|
|
38
|
+
fail("unsafe-path", "Unsafe archive path", value);
|
|
39
|
+
const parts = value.replace(/\/+$/u, "").split("/");
|
|
40
|
+
if (parts.length > limits.pathDepth)
|
|
41
|
+
fail("path-depth-cap", "Archive path depth limit exceeded", value);
|
|
42
|
+
if (parts.some((p) => !p || p === "." || p === ".."))
|
|
43
|
+
fail("unsafe-path", "Unsafe archive path component", value);
|
|
44
|
+
return parts.join("/");
|
|
45
|
+
}
|
|
46
|
+
function field(buffer, start, length) {
|
|
47
|
+
const part = buffer.subarray(start, start + length);
|
|
48
|
+
const nul = part.indexOf(0);
|
|
49
|
+
if (nul !== -1 && part.subarray(nul).some((n) => n !== 0))
|
|
50
|
+
fail("unsafe-path", "Embedded NUL in archive path");
|
|
51
|
+
return part.subarray(0, nul === -1 ? part.length : nul).toString("utf8");
|
|
52
|
+
}
|
|
53
|
+
function declaredSize(header) {
|
|
54
|
+
const value = header.subarray(124, 136);
|
|
55
|
+
let n;
|
|
56
|
+
if (value[0] === 0x80) {
|
|
57
|
+
n = 0;
|
|
58
|
+
for (const b of value.subarray(1)) n = n * 256 + b;
|
|
59
|
+
} else {
|
|
60
|
+
const text = value
|
|
61
|
+
.toString("ascii")
|
|
62
|
+
.replace(/[\0 ]+$/u, "")
|
|
63
|
+
.trim();
|
|
64
|
+
if (!/^[0-7]*$/u.test(text)) fail("invalid-metadata", "Invalid tar size");
|
|
65
|
+
n = text ? Number.parseInt(text, 8) : 0;
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
68
|
+
fail("invalid-metadata", "Invalid tar size");
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
function readPax(body, global, limits) {
|
|
72
|
+
const values = Object.create(null);
|
|
73
|
+
for (let offset = 0; offset < body.length;) {
|
|
74
|
+
const space = body.indexOf(32, offset);
|
|
75
|
+
const digits =
|
|
76
|
+
space === -1 ? "" : body.subarray(offset, space).toString("ascii");
|
|
77
|
+
if (!/^[1-9][0-9]*$/u.test(digits))
|
|
78
|
+
fail("invalid-metadata", "Invalid PAX length");
|
|
79
|
+
const length = Number(digits);
|
|
80
|
+
const end = offset + length;
|
|
81
|
+
if (
|
|
82
|
+
!Number.isSafeInteger(length) ||
|
|
83
|
+
end > body.length ||
|
|
84
|
+
end <= space + 2 ||
|
|
85
|
+
body[end - 1] !== 10
|
|
86
|
+
)
|
|
87
|
+
fail("invalid-metadata", "Invalid PAX record");
|
|
88
|
+
const record = body.subarray(space + 1, end - 1).toString("utf8");
|
|
89
|
+
const equals = record.indexOf("=");
|
|
90
|
+
if (equals <= 0 || record.includes("\0"))
|
|
91
|
+
fail("invalid-metadata", "Invalid PAX value");
|
|
92
|
+
const key = record.slice(0, equals);
|
|
93
|
+
const value = record.slice(equals + 1);
|
|
94
|
+
if (key === "path") safePath(value, limits);
|
|
95
|
+
if (/sparse/iu.test(key))
|
|
96
|
+
fail("unsupported-type", "Sparse archives are unsupported");
|
|
97
|
+
if (global && ["path", "size", "linkpath"].includes(key))
|
|
98
|
+
fail(
|
|
99
|
+
"unsupported-metadata",
|
|
100
|
+
"Global path and size overrides are unsupported",
|
|
101
|
+
);
|
|
102
|
+
if (
|
|
103
|
+
key === "size" &&
|
|
104
|
+
(!/^[0-9]+$/u.test(value) || !Number.isSafeInteger(Number(value)))
|
|
105
|
+
)
|
|
106
|
+
fail("invalid-metadata", "Invalid PAX size");
|
|
107
|
+
values[key] = value;
|
|
108
|
+
offset = end;
|
|
109
|
+
}
|
|
110
|
+
return values;
|
|
111
|
+
}
|
|
112
|
+
function framingGate(limits, compressedSize, checkTime) {
|
|
113
|
+
const header = Buffer.alloc(512);
|
|
114
|
+
let filled = 0,
|
|
115
|
+
remaining = 0,
|
|
116
|
+
padding = 0,
|
|
117
|
+
metadata = null,
|
|
118
|
+
metaOffset = 0,
|
|
119
|
+
metaType = "",
|
|
120
|
+
pendingSize;
|
|
121
|
+
let expanded = 0,
|
|
122
|
+
count = 0,
|
|
123
|
+
zeroBlocks = 0;
|
|
124
|
+
return new Transform({
|
|
125
|
+
transform(chunk, encoding, callback) {
|
|
126
|
+
try {
|
|
127
|
+
checkTime();
|
|
128
|
+
expanded += chunk.length;
|
|
129
|
+
if (expanded > limits.expandedBytes)
|
|
130
|
+
fail("expanded-cap", "Expanded archive byte limit exceeded");
|
|
131
|
+
if (expanded > compressedSize * limits.ratio)
|
|
132
|
+
fail("ratio-cap", "Archive expansion ratio exceeded");
|
|
133
|
+
let offset = 0;
|
|
134
|
+
while (offset < chunk.length) {
|
|
135
|
+
checkTime();
|
|
136
|
+
if (remaining) {
|
|
137
|
+
const n = Math.min(remaining, chunk.length - offset);
|
|
138
|
+
const part = chunk.subarray(offset, offset + n);
|
|
139
|
+
if (metadata) {
|
|
140
|
+
part.copy(metadata, metaOffset);
|
|
141
|
+
metaOffset += n;
|
|
142
|
+
} else this.push(part);
|
|
143
|
+
offset += n;
|
|
144
|
+
remaining -= n;
|
|
145
|
+
if (!remaining && metadata) {
|
|
146
|
+
if (metaType === "x" || metaType === "g") {
|
|
147
|
+
const pax = readPax(metadata, metaType === "g", limits);
|
|
148
|
+
if (metaType === "x")
|
|
149
|
+
pendingSize =
|
|
150
|
+
pax.size === undefined ? undefined : Number(pax.size);
|
|
151
|
+
} else {
|
|
152
|
+
const value = field(metadata, 0, metadata.length);
|
|
153
|
+
if (metaType === "L" || metaType === "N")
|
|
154
|
+
safePath(value, limits);
|
|
155
|
+
}
|
|
156
|
+
this.push(Buffer.from(header));
|
|
157
|
+
this.push(metadata);
|
|
158
|
+
metadata = null;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (padding) {
|
|
163
|
+
const n = Math.min(padding, chunk.length - offset);
|
|
164
|
+
this.push(chunk.subarray(offset, offset + n));
|
|
165
|
+
offset += n;
|
|
166
|
+
padding -= n;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const n = Math.min(512 - filled, chunk.length - offset);
|
|
170
|
+
chunk.copy(header, filled, offset, offset + n);
|
|
171
|
+
offset += n;
|
|
172
|
+
filled += n;
|
|
173
|
+
if (filled !== 512) continue;
|
|
174
|
+
filled = 0;
|
|
175
|
+
if (header.every((n) => n === 0)) {
|
|
176
|
+
zeroBlocks++;
|
|
177
|
+
this.push(Buffer.from(header));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (zeroBlocks)
|
|
181
|
+
fail("malformed-archive", "Nonzero data follows tar termination");
|
|
182
|
+
if (++count > limits.entries)
|
|
183
|
+
fail("entry-cap", "Archive entry limit exceeded");
|
|
184
|
+
const name = field(header, 0, 100);
|
|
185
|
+
const prefix = header
|
|
186
|
+
.subarray(257, 263)
|
|
187
|
+
.equals(Buffer.from("ustar\0"))
|
|
188
|
+
? field(header, 345, 155)
|
|
189
|
+
: "";
|
|
190
|
+
safePath(prefix ? `${prefix}/${name}` : name, limits);
|
|
191
|
+
const type = String.fromCharCode(header[156]);
|
|
192
|
+
const isMetadata = ["x", "g", "L", "K", "N"].includes(type);
|
|
193
|
+
if (!isMetadata && !["\0", "0", "1", "2", "5"].includes(type))
|
|
194
|
+
fail("unsupported-type", "Unsupported archive entry type", name);
|
|
195
|
+
if (["1", "2"].includes(type)) field(header, 157, 100);
|
|
196
|
+
let size = declaredSize(header);
|
|
197
|
+
if (!isMetadata && pendingSize !== undefined) {
|
|
198
|
+
size = pendingSize;
|
|
199
|
+
pendingSize = undefined;
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
size >
|
|
203
|
+
limits.expandedBytes - (expanded - (chunk.length - offset))
|
|
204
|
+
)
|
|
205
|
+
fail(
|
|
206
|
+
"expanded-cap",
|
|
207
|
+
"Declared entry exceeds remaining expanded budget",
|
|
208
|
+
name,
|
|
209
|
+
);
|
|
210
|
+
if (!isMetadata && !["\0", "0"].includes(type) && size !== 0)
|
|
211
|
+
fail("invalid-metadata", "Nonregular entry has a body", name);
|
|
212
|
+
remaining = size;
|
|
213
|
+
padding = (512 - (size % 512)) % 512;
|
|
214
|
+
if (isMetadata) {
|
|
215
|
+
if (size === 0 || size > limits.metadataBytes)
|
|
216
|
+
fail(
|
|
217
|
+
"metadata-cap",
|
|
218
|
+
"Archive metadata exceeds bounded parser allowance",
|
|
219
|
+
name,
|
|
220
|
+
);
|
|
221
|
+
metadata = Buffer.alloc(size);
|
|
222
|
+
metaOffset = 0;
|
|
223
|
+
metaType = type;
|
|
224
|
+
} else this.push(Buffer.from(header));
|
|
225
|
+
}
|
|
226
|
+
callback();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
callback(error);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
flush(callback) {
|
|
232
|
+
callback(
|
|
233
|
+
filled || remaining || padding || zeroBlocks < 2
|
|
234
|
+
? new InputFailure("malformed-archive", "Truncated tar container")
|
|
235
|
+
: null,
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function key(path) {
|
|
241
|
+
return path.normalize("NFC").toLowerCase();
|
|
242
|
+
}
|
|
243
|
+
function registerPath(paths, path, type, nodeBudget) {
|
|
244
|
+
if (path === null) return;
|
|
245
|
+
const parts = path.split("/");
|
|
246
|
+
let children = paths;
|
|
247
|
+
for (let i = 0; i < parts.length; i++) {
|
|
248
|
+
const component = parts[i];
|
|
249
|
+
const normalized = key(component);
|
|
250
|
+
const leaf = i === parts.length - 1;
|
|
251
|
+
let previous = children.get(normalized);
|
|
252
|
+
if (previous) {
|
|
253
|
+
if (previous.original !== component || (leaf && previous.explicit))
|
|
254
|
+
fail("path-collision", "Duplicate or ambiguous archive path", path);
|
|
255
|
+
if (
|
|
256
|
+
(!leaf && previous.type !== "directory") ||
|
|
257
|
+
(leaf && type !== "directory")
|
|
258
|
+
)
|
|
259
|
+
fail(
|
|
260
|
+
"path-collision",
|
|
261
|
+
"Non-directory path shadows archive descendants",
|
|
262
|
+
path,
|
|
263
|
+
);
|
|
264
|
+
if (leaf) previous.explicit = true;
|
|
265
|
+
} else {
|
|
266
|
+
if (nodeBudget.used >= nodeBudget.limit)
|
|
267
|
+
fail("path-node-cap", "Archive path index node limit exceeded", path);
|
|
268
|
+
nodeBudget.used++;
|
|
269
|
+
previous = {
|
|
270
|
+
original: component,
|
|
271
|
+
type: leaf ? type : "directory",
|
|
272
|
+
explicit: leaf,
|
|
273
|
+
children: new Map(),
|
|
274
|
+
};
|
|
275
|
+
children.set(normalized, previous);
|
|
276
|
+
}
|
|
277
|
+
children = previous.children;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export async function readArchive(bytes, options = {}) {
|
|
281
|
+
if (!Buffer.isBuffer(bytes))
|
|
282
|
+
throw new TypeError("Archive input must be a Buffer");
|
|
283
|
+
if (!["artifact", "source"].includes(options.kind))
|
|
284
|
+
throw new TypeError("Archive kind must be artifact or source");
|
|
285
|
+
const limits = { ...DEFAULT_LIMITS, ...options.limits };
|
|
286
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
287
|
+
if (
|
|
288
|
+
!(name in DEFAULT_LIMITS) ||
|
|
289
|
+
!Number.isSafeInteger(value) ||
|
|
290
|
+
value < 0 ||
|
|
291
|
+
value > DEFAULT_LIMITS[name]
|
|
292
|
+
)
|
|
293
|
+
throw new TypeError(`Invalid archive limit: ${name}`);
|
|
294
|
+
}
|
|
295
|
+
const budget = options.retentionBudget ?? {
|
|
296
|
+
limitBytes: 134217728,
|
|
297
|
+
usedBytes: 0,
|
|
298
|
+
};
|
|
299
|
+
if (
|
|
300
|
+
![budget.limitBytes, budget.usedBytes].every(
|
|
301
|
+
(n) => Number.isSafeInteger(n) && n >= 0,
|
|
302
|
+
) ||
|
|
303
|
+
budget.usedBytes > budget.limitBytes
|
|
304
|
+
)
|
|
305
|
+
throw new TypeError("Invalid shared retention budget");
|
|
306
|
+
const result = {
|
|
307
|
+
digest: {
|
|
308
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
309
|
+
sha512: createHash("sha512").update(bytes).digest("hex"),
|
|
310
|
+
},
|
|
311
|
+
entries: [],
|
|
312
|
+
complete: true,
|
|
313
|
+
limits: { ...limits },
|
|
314
|
+
failures: [],
|
|
315
|
+
totals: {
|
|
316
|
+
regularFiles: 0,
|
|
317
|
+
regularBytes: 0,
|
|
318
|
+
nonregularEntries: 0,
|
|
319
|
+
retainedBytes: 0,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const started = performance.now();
|
|
323
|
+
const checkTime = () => {
|
|
324
|
+
if (performance.now() - started >= limits.parseMs)
|
|
325
|
+
fail("parse-timeout", "Archive parse deadline exceeded");
|
|
326
|
+
};
|
|
327
|
+
let extract, pending, timer;
|
|
328
|
+
try {
|
|
329
|
+
checkTime();
|
|
330
|
+
if (bytes.length > limits.compressedBytes)
|
|
331
|
+
fail("compressed-cap", "Compressed archive byte limit exceeded");
|
|
332
|
+
let root;
|
|
333
|
+
if (options.kind === "source") {
|
|
334
|
+
if (typeof options.sourceRoot !== "string" || !options.sourceRoot)
|
|
335
|
+
fail(
|
|
336
|
+
"source-root-required",
|
|
337
|
+
"Local source archive requires an explicit root",
|
|
338
|
+
);
|
|
339
|
+
root = safePath(options.sourceRoot, limits);
|
|
340
|
+
}
|
|
341
|
+
const originals = new Map(),
|
|
342
|
+
mapped = new Map();
|
|
343
|
+
const nodeBudget = { limit: limits.pathNodes, used: 0 };
|
|
344
|
+
extract = tar.extract();
|
|
345
|
+
pending = pipeline(
|
|
346
|
+
Readable.from([bytes]),
|
|
347
|
+
createGunzip({ chunkSize: 16384 }),
|
|
348
|
+
framingGate(limits, bytes.length, checkTime),
|
|
349
|
+
extract,
|
|
350
|
+
);
|
|
351
|
+
pending.catch(() => {});
|
|
352
|
+
timer = setTimeout(
|
|
353
|
+
() =>
|
|
354
|
+
extract.destroy(
|
|
355
|
+
new InputFailure("parse-timeout", "Archive parse deadline exceeded"),
|
|
356
|
+
),
|
|
357
|
+
limits.parseMs,
|
|
358
|
+
);
|
|
359
|
+
for await (const stream of extract) {
|
|
360
|
+
checkTime();
|
|
361
|
+
const h = stream.header;
|
|
362
|
+
const original = safePath(h.name, limits);
|
|
363
|
+
const type = h.type === "link" ? "hardlink" : h.type;
|
|
364
|
+
if (!["file", "directory", "symlink", "hardlink"].includes(type))
|
|
365
|
+
fail("unsupported-type", "Unsupported archive entry type", h.name);
|
|
366
|
+
if (
|
|
367
|
+
!Number.isSafeInteger(h.size) ||
|
|
368
|
+
h.size < 0 ||
|
|
369
|
+
h.size > limits.expandedBytes
|
|
370
|
+
)
|
|
371
|
+
fail("invalid-metadata", "Invalid effective size", h.name);
|
|
372
|
+
let path;
|
|
373
|
+
if (root !== undefined) {
|
|
374
|
+
if (original === root) {
|
|
375
|
+
if (type !== "directory")
|
|
376
|
+
fail(
|
|
377
|
+
"source-root-invalid",
|
|
378
|
+
"Source archive root is not a directory",
|
|
379
|
+
h.name,
|
|
380
|
+
);
|
|
381
|
+
path = null;
|
|
382
|
+
} else if (original.startsWith(root + "/"))
|
|
383
|
+
path = original.slice(root.length + 1);
|
|
384
|
+
else
|
|
385
|
+
fail(
|
|
386
|
+
"source-root-mismatch",
|
|
387
|
+
"Entry is outside the declared source root",
|
|
388
|
+
h.name,
|
|
389
|
+
);
|
|
390
|
+
} else
|
|
391
|
+
path = original.includes("/")
|
|
392
|
+
? original.slice(original.indexOf("/") + 1)
|
|
393
|
+
: null;
|
|
394
|
+
if (path !== null) path = safePath(path, limits);
|
|
395
|
+
registerPath(originals, original, type, nodeBudget);
|
|
396
|
+
registerPath(mapped, path, type, nodeBudget);
|
|
397
|
+
const entry = {
|
|
398
|
+
path,
|
|
399
|
+
originalPath: h.name,
|
|
400
|
+
type,
|
|
401
|
+
size: h.size,
|
|
402
|
+
content: null,
|
|
403
|
+
inspection: "nonregular",
|
|
404
|
+
};
|
|
405
|
+
if (type === "file") {
|
|
406
|
+
const hash = createHash("sha256");
|
|
407
|
+
entry.inspection =
|
|
408
|
+
h.size > limits.fileBytes
|
|
409
|
+
? "size-cap"
|
|
410
|
+
: h.size > budget.limitBytes - budget.usedBytes
|
|
411
|
+
? "retention-cap"
|
|
412
|
+
: "retained";
|
|
413
|
+
const retained = entry.inspection === "retained";
|
|
414
|
+
let content = null,
|
|
415
|
+
offset = 0;
|
|
416
|
+
if (retained) {
|
|
417
|
+
budget.usedBytes += h.size;
|
|
418
|
+
content = Buffer.allocUnsafe(h.size);
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
for await (const chunk of stream) {
|
|
422
|
+
checkTime();
|
|
423
|
+
if (offset + chunk.length > h.size)
|
|
424
|
+
fail("invalid-metadata", "File exceeds declared size", h.name);
|
|
425
|
+
hash.update(chunk);
|
|
426
|
+
if (content) chunk.copy(content, offset);
|
|
427
|
+
offset += chunk.length;
|
|
428
|
+
}
|
|
429
|
+
if (offset !== h.size)
|
|
430
|
+
fail("malformed-archive", "Truncated file", h.name);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (retained) budget.usedBytes -= h.size;
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
entry.sha256 = hash.digest("hex");
|
|
436
|
+
entry.content = content;
|
|
437
|
+
result.totals.regularFiles++;
|
|
438
|
+
result.totals.regularBytes += h.size;
|
|
439
|
+
result.totals.retainedBytes += content?.length ?? 0;
|
|
440
|
+
} else {
|
|
441
|
+
if (["symlink", "hardlink"].includes(type))
|
|
442
|
+
entry.linkTarget = h.linkname ?? "";
|
|
443
|
+
for await (const chunk of stream) {
|
|
444
|
+
if (chunk.length)
|
|
445
|
+
fail("invalid-metadata", "Nonregular entry body", h.name);
|
|
446
|
+
}
|
|
447
|
+
result.totals.nonregularEntries++;
|
|
448
|
+
}
|
|
449
|
+
result.entries.push(entry);
|
|
450
|
+
}
|
|
451
|
+
await pending;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
extract?.destroy();
|
|
454
|
+
if (pending) await pending.catch(() => {});
|
|
455
|
+
if (
|
|
456
|
+
!(error instanceof InputFailure) &&
|
|
457
|
+
!["Z_DATA_ERROR", "Z_BUF_ERROR", "ERR_STREAM_PREMATURE_CLOSE"].includes(
|
|
458
|
+
error.code,
|
|
459
|
+
) &&
|
|
460
|
+
!/^(?:Invalid tar header[.:]|Invalid header$|Header exceeds max size$|Unexpected end of data$)/u.test(
|
|
461
|
+
error.message,
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
throw error;
|
|
465
|
+
result.complete = false;
|
|
466
|
+
result.failures.push(
|
|
467
|
+
error.failure ?? {
|
|
468
|
+
code: "malformed-archive",
|
|
469
|
+
message: "Malformed gzip or tar container",
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
} finally {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
}
|
|
475
|
+
result.entries.sort((a, b) =>
|
|
476
|
+
(a.path ?? "\uffff" + a.originalPath) <
|
|
477
|
+
(b.path ?? "\uffff" + b.originalPath)
|
|
478
|
+
? -1
|
|
479
|
+
: (a.path ?? "\uffff" + a.originalPath) >
|
|
480
|
+
(b.path ?? "\uffff" + b.originalPath)
|
|
481
|
+
? 1
|
|
482
|
+
: 0,
|
|
483
|
+
);
|
|
484
|
+
return result;
|
|
485
|
+
}
|