vana-cli 0.24.4 → 0.25.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/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +20 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +98 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/registry.d.ts +2 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +18 -2
- package/dist/connectors/registry.js.map +1 -1
- package/dist/core/cli-types.d.ts +44 -0
- package/dist/core/cli-types.d.ts.map +1 -1
- package/dist/core/cli-types.js +14 -0
- package/dist/core/cli-types.js.map +1 -1
- package/dist/core/state-store.d.ts +6 -0
- package/dist/core/state-store.d.ts.map +1 -1
- package/dist/core/state-store.js.map +1 -1
- package/dist/pdpp/host.d.ts +66 -0
- package/dist/pdpp/host.d.ts.map +1 -0
- package/dist/pdpp/host.js +283 -0
- package/dist/pdpp/host.js.map +1 -0
- package/dist/pdpp/installer.d.ts +17 -0
- package/dist/pdpp/installer.d.ts.map +1 -0
- package/dist/pdpp/installer.js +117 -0
- package/dist/pdpp/installer.js.map +1 -0
- package/dist/pdpp/local.d.ts +14 -0
- package/dist/pdpp/local.d.ts.map +1 -0
- package/dist/pdpp/local.js +76 -0
- package/dist/pdpp/local.js.map +1 -0
- package/dist/pdpp/pins.d.ts +23 -0
- package/dist/pdpp/pins.d.ts.map +1 -0
- package/dist/pdpp/pins.js +18 -0
- package/dist/pdpp/pins.js.map +1 -0
- package/dist/pdpp/profile.d.ts +49 -0
- package/dist/pdpp/profile.d.ts.map +1 -0
- package/dist/pdpp/profile.js +17 -0
- package/dist/pdpp/profile.js.map +1 -0
- package/dist/pdpp/protocol.d.ts +79 -0
- package/dist/pdpp/protocol.d.ts.map +1 -0
- package/dist/pdpp/protocol.js +372 -0
- package/dist/pdpp/protocol.js.map +1 -0
- package/dist/pdpp/runtime.d.ts +43 -0
- package/dist/pdpp/runtime.d.ts.map +1 -0
- package/dist/pdpp/runtime.js +387 -0
- package/dist/pdpp/runtime.js.map +1 -0
- package/dist/pdpp/store.d.ts +43 -0
- package/dist/pdpp/store.d.ts.map +1 -0
- package/dist/pdpp/store.js +100 -0
- package/dist/pdpp/store.js.map +1 -0
- package/dist/runtime/core/contracts.d.ts +2 -0
- package/dist/runtime/core/contracts.d.ts.map +1 -1
- package/dist/runtime/managed-playwright.d.ts +7 -0
- package/dist/runtime/managed-playwright.d.ts.map +1 -1
- package/dist/runtime/managed-playwright.js +7 -0
- package/dist/runtime/managed-playwright.js.map +1 -1
- package/dist/vendor/pdpp-connector-manager/LICENSE +202 -0
- package/dist/vendor/pdpp-connector-manager/NOTICE +2 -0
- package/dist/vendor/pdpp-connector-manager/VENDOR.md +27 -0
- package/dist/vendor/pdpp-connector-manager/catalog-schema-data.mjs +168 -0
- package/dist/vendor/pdpp-connector-manager/catalog-schema.mjs +62 -0
- package/dist/vendor/pdpp-connector-manager/index.d.mts +49 -0
- package/dist/vendor/pdpp-connector-manager/index.mjs +1646 -0
- package/dist/vendor/pdpp-connector-manager/oci-catalog.mjs +93 -0
- package/dist/vendor/pdpp-connector-manager/oci-registry.mjs +1002 -0
- package/dist/vendor/pdpp-connector-manager/oci-verify.mjs +515 -0
- package/dist/vendor/pdpp-connector-manager/package.json +51 -0
- package/dist/vendor/pdpp-connector-manager/retry.mjs +142 -0
- package/dist/vendor/pdpp-connector-manager/tar-stream.mjs +685 -0
- package/package.json +7 -2
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// Copyright The PDP-Connect Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { Readable, Transform } from "node:stream";
|
|
7
|
+
import { createGunzip } from "node:zlib";
|
|
8
|
+
|
|
9
|
+
const requireFromTarReader = (() => {
|
|
10
|
+
try {
|
|
11
|
+
return createRequire(import.meta.url);
|
|
12
|
+
} catch {
|
|
13
|
+
// The unchanged mutation control imports a transformed copy as a data URL.
|
|
14
|
+
return createRequire(join(process.cwd(), "package.json"));
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
const { Parser } = requireFromTarReader("tar");
|
|
18
|
+
|
|
19
|
+
const BLOCK_SIZE = 512;
|
|
20
|
+
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
21
|
+
const DECOMPRESSED_OVERHEAD_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const MAX_EXTENDED_HEADER_BYTES = BLOCK_SIZE * 8;
|
|
23
|
+
|
|
24
|
+
const TYPE_FILE = new Set(["0", "\0"]);
|
|
25
|
+
const TYPE_DIRECTORY = "5";
|
|
26
|
+
const TYPE_GNU_LONGNAME = "L";
|
|
27
|
+
const TYPE_GNU_LONGLINK = "K";
|
|
28
|
+
const TYPE_PAX_NEXT = "x";
|
|
29
|
+
const TYPE_PAX_GLOBAL = "g";
|
|
30
|
+
const METADATA_TYPES = new Set([
|
|
31
|
+
TYPE_GNU_LONGNAME,
|
|
32
|
+
TYPE_GNU_LONGLINK,
|
|
33
|
+
TYPE_PAX_NEXT,
|
|
34
|
+
TYPE_PAX_GLOBAL,
|
|
35
|
+
]);
|
|
36
|
+
const INFORMATIONAL_PAX_KEYS = new Set([
|
|
37
|
+
"mtime",
|
|
38
|
+
"atime",
|
|
39
|
+
"ctime",
|
|
40
|
+
"uid",
|
|
41
|
+
"gid",
|
|
42
|
+
"uname",
|
|
43
|
+
"gname",
|
|
44
|
+
"comment",
|
|
45
|
+
]);
|
|
46
|
+
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
47
|
+
|
|
48
|
+
function asError(error) {
|
|
49
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readString(block, offset, length, label) {
|
|
53
|
+
const field = block.subarray(offset, offset + length);
|
|
54
|
+
const nul = field.indexOf(0);
|
|
55
|
+
try {
|
|
56
|
+
return STRICT_UTF8.decode(nul < 0 ? field : field.subarray(0, nul));
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`archive contains a ${label} that is not valid UTF-8`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readSize(block) {
|
|
63
|
+
const offset = 124;
|
|
64
|
+
const length = 12;
|
|
65
|
+
if (block[offset] & 0x80) {
|
|
66
|
+
let value = 0n;
|
|
67
|
+
for (let index = offset + 1; index < offset + length; index += 1) {
|
|
68
|
+
value = (value << 8n) | BigInt(block[index]);
|
|
69
|
+
}
|
|
70
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
71
|
+
throw new Error("archive declares a member size too large to account for");
|
|
72
|
+
}
|
|
73
|
+
return Number(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const field = block.toString("ascii", offset, offset + length).replace(/\0/g, " ").trim();
|
|
77
|
+
if (!/^[0-7]+$/.test(field)) {
|
|
78
|
+
throw new Error("archive declares an unreadable member size");
|
|
79
|
+
}
|
|
80
|
+
const size = Number.parseInt(field, 8);
|
|
81
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
82
|
+
throw new Error("archive declares an unreadable member size");
|
|
83
|
+
}
|
|
84
|
+
return size;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readPaxSize(value) {
|
|
88
|
+
if (!/^[0-9]+$/.test(value)) {
|
|
89
|
+
throw new Error("archive declares an unreadable PAX member size");
|
|
90
|
+
}
|
|
91
|
+
const size = Number(value);
|
|
92
|
+
if (!Number.isSafeInteger(size)) {
|
|
93
|
+
throw new Error("archive declares a PAX member size too large to account for");
|
|
94
|
+
}
|
|
95
|
+
return size;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parsePaxRecords(body) {
|
|
99
|
+
const records = new Map();
|
|
100
|
+
let offset = 0;
|
|
101
|
+
|
|
102
|
+
while (offset < body.length) {
|
|
103
|
+
const space = body.indexOf(0x20, offset);
|
|
104
|
+
if (space <= offset) throw new Error("archive contains a malformed PAX record");
|
|
105
|
+
const lengthText = body.toString("latin1", offset, space);
|
|
106
|
+
if (!/^[1-9][0-9]*$/.test(lengthText)) {
|
|
107
|
+
throw new Error("archive contains a malformed PAX record");
|
|
108
|
+
}
|
|
109
|
+
const length = Number(lengthText);
|
|
110
|
+
const end = offset + length;
|
|
111
|
+
if (!Number.isSafeInteger(length) || end > body.length || body[end - 1] !== 0x0a) {
|
|
112
|
+
throw new Error("archive contains a malformed PAX record");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const equals = body.indexOf(0x3d, space + 1);
|
|
116
|
+
if (equals < 0 || equals >= end - 1) {
|
|
117
|
+
throw new Error("archive contains a malformed PAX record");
|
|
118
|
+
}
|
|
119
|
+
let key;
|
|
120
|
+
let value;
|
|
121
|
+
try {
|
|
122
|
+
key = STRICT_UTF8.decode(body.subarray(space + 1, equals));
|
|
123
|
+
value = STRICT_UTF8.decode(body.subarray(equals + 1, end - 1));
|
|
124
|
+
} catch {
|
|
125
|
+
throw new Error("archive contains a malformed PAX record");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (key === "linkpath") throw new Error("archive uses unsupported PAX linkpath");
|
|
129
|
+
if (key !== "path" && key !== "size" && !INFORMATIONAL_PAX_KEYS.has(key)) {
|
|
130
|
+
throw new Error(`archive uses unsupported PAX key "${key}"`);
|
|
131
|
+
}
|
|
132
|
+
if ((key === "path" || key === "size") && value === "") {
|
|
133
|
+
throw new Error(`archive uses unsupported empty PAX ${key}`);
|
|
134
|
+
}
|
|
135
|
+
if (key === "path" && value.includes("\0")) {
|
|
136
|
+
throw new Error("archive uses a PAX path containing a NUL byte");
|
|
137
|
+
}
|
|
138
|
+
records.set(key, value);
|
|
139
|
+
offset = end;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return records;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseGnuLongName(body) {
|
|
146
|
+
try {
|
|
147
|
+
const nul = body.indexOf(0);
|
|
148
|
+
return STRICT_UTF8.decode(nul < 0 ? body : body.subarray(0, nul));
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("archive contains a GNU long name that is not valid UTF-8");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parserHeaderBlock(block, path, type, rawSize) {
|
|
155
|
+
if (!TYPE_FILE.has(type) || rawSize === 0 || !path.endsWith("/")) return block;
|
|
156
|
+
|
|
157
|
+
const lastNameByte = block.subarray(0, 100).findLastIndex((byte) => byte !== 0);
|
|
158
|
+
if (lastNameByte < 0 || block[lastNameByte] !== 0x2f) return block;
|
|
159
|
+
|
|
160
|
+
const adjusted = Buffer.from(block);
|
|
161
|
+
adjusted[lastNameByte] = 0;
|
|
162
|
+
adjusted.fill(0x20, 148, 156);
|
|
163
|
+
let checksum = 0;
|
|
164
|
+
for (const byte of adjusted) checksum += byte;
|
|
165
|
+
adjusted.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
166
|
+
return adjusted;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function effectiveMemberMetadata(globalPax, nextPax, rawSize) {
|
|
170
|
+
const path = nextPax?.get("path") ?? globalPax.get("path") ?? null;
|
|
171
|
+
const size = nextPax?.has("size")
|
|
172
|
+
? readPaxSize(nextPax.get("size"))
|
|
173
|
+
: globalPax.has("size")
|
|
174
|
+
? readPaxSize(globalPax.get("size"))
|
|
175
|
+
: rawSize;
|
|
176
|
+
return { path, size };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizedMemberDestination(path) {
|
|
180
|
+
return path.split("/").filter((segment) => segment !== "" && segment !== ".").join("/");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isZeroBlock(block) {
|
|
184
|
+
for (const byte of block) {
|
|
185
|
+
if (byte !== 0) return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function decompressedMeter(maxDecompressedBytes, observeDecompressedChunk) {
|
|
191
|
+
let total = 0;
|
|
192
|
+
return new Transform({
|
|
193
|
+
transform(chunk, _encoding, callback) {
|
|
194
|
+
try {
|
|
195
|
+
total += chunk.length;
|
|
196
|
+
observeDecompressedChunk(chunk.length);
|
|
197
|
+
if (total > maxDecompressedBytes) {
|
|
198
|
+
callback(new Error(`decompressed input exceeds the ${maxDecompressedBytes}-byte ceiling`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
callback(null, chunk);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
callback(asError(error));
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function blockFramer() {
|
|
210
|
+
let pending = Buffer.alloc(0);
|
|
211
|
+
return new Transform({
|
|
212
|
+
transform(chunk, _encoding, callback) {
|
|
213
|
+
pending = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
|
|
214
|
+
while (pending.length >= BLOCK_SIZE) {
|
|
215
|
+
this.push(pending.subarray(0, BLOCK_SIZE));
|
|
216
|
+
pending = pending.subarray(BLOCK_SIZE);
|
|
217
|
+
}
|
|
218
|
+
callback();
|
|
219
|
+
},
|
|
220
|
+
flush(callback) {
|
|
221
|
+
if (pending.length > 0) this.push(pending);
|
|
222
|
+
callback();
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Keep only the small amount of raw framing that node-tar intentionally does
|
|
229
|
+
* not expose. Parser still owns tar header decoding, metadata application,
|
|
230
|
+
* stream ordering and entry bodies; this tap supplies exact raw sizes and
|
|
231
|
+
* metadata bytes for the installer's stricter policy.
|
|
232
|
+
*/
|
|
233
|
+
function rawArchiveTap({ onHeader, onMetadata }) {
|
|
234
|
+
let state = "header";
|
|
235
|
+
let currentHeader = null;
|
|
236
|
+
let bodyBlocksRemaining = 0;
|
|
237
|
+
let metadataBodyRemaining = 0;
|
|
238
|
+
let metadataBody = [];
|
|
239
|
+
let sawFirstEndBlock = false;
|
|
240
|
+
|
|
241
|
+
function feed(block) {
|
|
242
|
+
if (state === "body") {
|
|
243
|
+
bodyBlocksRemaining -= 1;
|
|
244
|
+
if (bodyBlocksRemaining === 0) state = "header";
|
|
245
|
+
return block;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (state === "metadata") {
|
|
249
|
+
const used = metadataBody.length * BLOCK_SIZE;
|
|
250
|
+
const take = Math.min(BLOCK_SIZE, metadataBodyRemaining - used);
|
|
251
|
+
if (take > 0) metadataBody.push(Buffer.from(block.subarray(0, take)));
|
|
252
|
+
bodyBlocksRemaining -= 1;
|
|
253
|
+
if (bodyBlocksRemaining === 0) {
|
|
254
|
+
onMetadata({
|
|
255
|
+
type: currentHeader.type,
|
|
256
|
+
body: Buffer.concat(metadataBody, metadataBodyRemaining),
|
|
257
|
+
});
|
|
258
|
+
currentHeader = null;
|
|
259
|
+
metadataBody = [];
|
|
260
|
+
metadataBodyRemaining = 0;
|
|
261
|
+
state = "header";
|
|
262
|
+
}
|
|
263
|
+
return block;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (state !== "header") {
|
|
267
|
+
throw new Error("archive parser lost track of the current member");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (isZeroBlock(block)) {
|
|
271
|
+
sawFirstEndBlock = true;
|
|
272
|
+
return block;
|
|
273
|
+
}
|
|
274
|
+
if (sawFirstEndBlock) {
|
|
275
|
+
throw new Error("archive contains a non-zero block after its first end marker");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const type = String.fromCharCode(block[156]);
|
|
279
|
+
const name = readString(block, 0, 100, "member name");
|
|
280
|
+
const prefix = readString(block, 345, 155, "member name");
|
|
281
|
+
const rawSize = readSize(block);
|
|
282
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
283
|
+
currentHeader = { block: Buffer.from(block), type, rawSize, path };
|
|
284
|
+
onHeader(currentHeader);
|
|
285
|
+
|
|
286
|
+
if (METADATA_TYPES.has(type)) {
|
|
287
|
+
if (rawSize > MAX_EXTENDED_HEADER_BYTES) {
|
|
288
|
+
throw new Error("archive declares an implausibly large extended header");
|
|
289
|
+
}
|
|
290
|
+
metadataBodyRemaining = rawSize;
|
|
291
|
+
bodyBlocksRemaining = Math.ceil(rawSize / BLOCK_SIZE);
|
|
292
|
+
if (bodyBlocksRemaining === 0) {
|
|
293
|
+
onMetadata({ type, body: Buffer.alloc(0) });
|
|
294
|
+
currentHeader = null;
|
|
295
|
+
} else {
|
|
296
|
+
state = "metadata";
|
|
297
|
+
}
|
|
298
|
+
return block;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
state = "awaiting-entry";
|
|
302
|
+
return parserHeaderBlock(block, path, type, rawSize);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
feed,
|
|
307
|
+
beginEntry(size) {
|
|
308
|
+
if (state !== "awaiting-entry" || !currentHeader) {
|
|
309
|
+
throw new Error("archive parser did not expose the current member header");
|
|
310
|
+
}
|
|
311
|
+
bodyBlocksRemaining = Math.ceil(size / BLOCK_SIZE);
|
|
312
|
+
currentHeader = null;
|
|
313
|
+
state = bodyBlocksRemaining === 0 ? "header" : "body";
|
|
314
|
+
},
|
|
315
|
+
currentHeader() {
|
|
316
|
+
return currentHeader;
|
|
317
|
+
},
|
|
318
|
+
hasPendingMetadata() {
|
|
319
|
+
return state === "metadata" || state === "awaiting-entry" && currentHeader?.type === TYPE_GNU_LONGLINK;
|
|
320
|
+
},
|
|
321
|
+
state() {
|
|
322
|
+
return state;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function nonEmptyPaxValues(pax) {
|
|
328
|
+
return Object.fromEntries(
|
|
329
|
+
Object.entries(pax ?? {}).filter(([key, value]) => key !== "global" && value !== undefined)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function assertParserMetadata(entry) {
|
|
334
|
+
const allowed = new Set([...INFORMATIONAL_PAX_KEYS, "path", "size"]);
|
|
335
|
+
for (const metadata of [entry.extended, entry.globalExtended]) {
|
|
336
|
+
for (const key of Object.keys(nonEmptyPaxValues(metadata))) {
|
|
337
|
+
if (!allowed.has(key)) {
|
|
338
|
+
throw new Error(`archive uses unsupported PAX key "${key}"`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function parserError(error, tap) {
|
|
345
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
346
|
+
if (/Truncated input/.test(message)) {
|
|
347
|
+
return new Error(
|
|
348
|
+
tap.state() === "body"
|
|
349
|
+
? "archive ends in the middle of a member"
|
|
350
|
+
: "archive ends in the middle of a header block"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return asError(error);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Gunzip and parse one archive while enforcing file, input, and entry limits.
|
|
358
|
+
* Returns regular files as `[{ path, buffer }]`, in archive order.
|
|
359
|
+
*/
|
|
360
|
+
export async function readTarGzEntries(
|
|
361
|
+
buffer,
|
|
362
|
+
{
|
|
363
|
+
maxUnpackedBytes,
|
|
364
|
+
maxDecompressedBytes = Math.min(
|
|
365
|
+
Number.MAX_SAFE_INTEGER,
|
|
366
|
+
maxUnpackedBytes + DECOMPRESSED_OVERHEAD_BYTES
|
|
367
|
+
),
|
|
368
|
+
maxEntries = DEFAULT_MAX_ENTRIES,
|
|
369
|
+
validateMemberPath = () => {},
|
|
370
|
+
observeDecompressedChunk = () => {},
|
|
371
|
+
} = {}
|
|
372
|
+
) {
|
|
373
|
+
if (!Number.isSafeInteger(maxUnpackedBytes) || maxUnpackedBytes < 0) {
|
|
374
|
+
throw new Error("a byte ceiling is required to read an archive");
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isSafeInteger(maxDecompressedBytes) || maxDecompressedBytes < 0) {
|
|
377
|
+
throw new Error("a decompressed-input byte ceiling must be a non-negative safe integer");
|
|
378
|
+
}
|
|
379
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 0) {
|
|
380
|
+
throw new Error("an archive entry ceiling must be a non-negative safe integer");
|
|
381
|
+
}
|
|
382
|
+
if (typeof observeDecompressedChunk !== "function") {
|
|
383
|
+
throw new Error("observeDecompressedChunk must be a function");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const source = Readable.from(buffer);
|
|
387
|
+
const gunzip = createGunzip();
|
|
388
|
+
const meter = decompressedMeter(maxDecompressedBytes, observeDecompressedChunk);
|
|
389
|
+
const framer = blockFramer();
|
|
390
|
+
const parser = new Parser({ strict: true, maxDecompressionRatio: Infinity });
|
|
391
|
+
|
|
392
|
+
const files = [];
|
|
393
|
+
const globalPax = new Map();
|
|
394
|
+
const pendingMetadata = [];
|
|
395
|
+
const rawMetadata = [];
|
|
396
|
+
const memberDestinations = new Set();
|
|
397
|
+
let declared = 0;
|
|
398
|
+
let entryCount = 0;
|
|
399
|
+
let sawEndOfArchive = false;
|
|
400
|
+
let sawPartialBlock = false;
|
|
401
|
+
let failure = null;
|
|
402
|
+
let stopped = false;
|
|
403
|
+
|
|
404
|
+
const tap = rawArchiveTap({
|
|
405
|
+
onHeader(header) {
|
|
406
|
+
if (header.type === TYPE_DIRECTORY && header.rawSize !== 0) {
|
|
407
|
+
throw new Error("archive declares a non-zero directory size");
|
|
408
|
+
}
|
|
409
|
+
if (header.type === TYPE_GNU_LONGLINK) {
|
|
410
|
+
throw new Error("archive uses unsupported GNU long-link metadata");
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
onMetadata(metadata) {
|
|
414
|
+
rawMetadata.push(metadata);
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const stopInput = () => {
|
|
419
|
+
if (stopped) return;
|
|
420
|
+
stopped = true;
|
|
421
|
+
source.destroy();
|
|
422
|
+
gunzip.destroy();
|
|
423
|
+
meter.destroy();
|
|
424
|
+
framer.destroy();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const fail = (error) => {
|
|
428
|
+
const next = asError(error);
|
|
429
|
+
if (failure) return;
|
|
430
|
+
failure = next;
|
|
431
|
+
stopInput();
|
|
432
|
+
if (!parser.aborted) {
|
|
433
|
+
try {
|
|
434
|
+
parser.abort(next);
|
|
435
|
+
} catch {
|
|
436
|
+
// The parser's error listener receives the original failure.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const countEntry = () => {
|
|
442
|
+
entryCount += 1;
|
|
443
|
+
if (entryCount > maxEntries) {
|
|
444
|
+
throw new Error(`archive contains more than ${maxEntries} entries`);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const processMetadata = (_metaText) => {
|
|
449
|
+
countEntry();
|
|
450
|
+
const raw = rawMetadata.shift();
|
|
451
|
+
if (!raw) throw new Error("archive parser reported metadata without raw bytes");
|
|
452
|
+
|
|
453
|
+
if (raw.type === TYPE_GNU_LONGNAME) {
|
|
454
|
+
pendingMetadata.push({ type: raw.type, path: parseGnuLongName(raw.body) });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (raw.type === TYPE_PAX_NEXT || raw.type === TYPE_PAX_GLOBAL) {
|
|
459
|
+
pendingMetadata.push({ type: raw.type, records: parsePaxRecords(raw.body) });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
throw new Error(`Artifact contains unsupported archive entry type "${raw.type}"`);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const processEntry = (entry) => {
|
|
467
|
+
countEntry();
|
|
468
|
+
const rawHeader = tap.currentHeader();
|
|
469
|
+
if (!rawHeader) throw new Error("archive parser reported an entry without a raw header");
|
|
470
|
+
const parserInferredDirectory = TYPE_FILE.has(rawHeader.type) && entry.header.typeKey === TYPE_DIRECTORY;
|
|
471
|
+
if (
|
|
472
|
+
rawHeader.type !== entry.header.typeKey &&
|
|
473
|
+
!(rawHeader.type === "\0" && entry.header.typeKey === "0") &&
|
|
474
|
+
!parserInferredDirectory
|
|
475
|
+
) {
|
|
476
|
+
throw new Error("archive parser reported an entry with an unknown header type");
|
|
477
|
+
}
|
|
478
|
+
assertParserMetadata(entry);
|
|
479
|
+
|
|
480
|
+
if (rawMetadata.length > 0) {
|
|
481
|
+
throw new Error("archive contains metadata that has no following entry");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const localMetadata = pendingMetadata.filter(({ type }) => type !== TYPE_PAX_GLOBAL);
|
|
485
|
+
if (
|
|
486
|
+
localMetadata.some(({ type }) => type === TYPE_GNU_LONGNAME) &&
|
|
487
|
+
localMetadata.some(({ type }) => type === TYPE_PAX_NEXT)
|
|
488
|
+
) {
|
|
489
|
+
throw new Error("archive combines GNU and PAX path overrides for one entry");
|
|
490
|
+
}
|
|
491
|
+
let sawLocalMetadata = false;
|
|
492
|
+
for (const metadata of pendingMetadata) {
|
|
493
|
+
if (metadata.type === TYPE_PAX_GLOBAL) {
|
|
494
|
+
if (sawLocalMetadata) {
|
|
495
|
+
throw new Error("archive uses consecutive per-entry metadata headers");
|
|
496
|
+
}
|
|
497
|
+
} else if (sawLocalMetadata) {
|
|
498
|
+
throw new Error("archive uses consecutive per-entry metadata headers");
|
|
499
|
+
} else {
|
|
500
|
+
sawLocalMetadata = true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const pendingGlobalPax = new Map(globalPax);
|
|
505
|
+
for (const metadata of pendingMetadata) {
|
|
506
|
+
if (metadata.type === TYPE_PAX_GLOBAL) {
|
|
507
|
+
for (const [key, value] of metadata.records) pendingGlobalPax.set(key, value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const local = localMetadata[0];
|
|
511
|
+
if (local?.type === TYPE_GNU_LONGNAME && pendingGlobalPax.has("path")) {
|
|
512
|
+
throw new Error("archive combines GNU and PAX path overrides for one entry");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const metadata of pendingMetadata) {
|
|
516
|
+
if (metadata.type === TYPE_PAX_GLOBAL) {
|
|
517
|
+
for (const [key, value] of metadata.records) globalPax.set(key, value);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const nextPax = local?.type === TYPE_PAX_NEXT ? local.records : null;
|
|
521
|
+
const rawSize = rawHeader.rawSize;
|
|
522
|
+
const effective = effectiveMemberMetadata(globalPax, nextPax, rawSize);
|
|
523
|
+
const path = local?.path ?? effective.path ?? rawHeader.path;
|
|
524
|
+
const size = effective.size;
|
|
525
|
+
pendingMetadata.length = 0;
|
|
526
|
+
|
|
527
|
+
if (typeof path !== "string" || path.includes("\0")) {
|
|
528
|
+
throw new Error("archive contains a member path that is not valid");
|
|
529
|
+
}
|
|
530
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
531
|
+
throw new Error("archive declares an unreadable member size");
|
|
532
|
+
}
|
|
533
|
+
if (rawHeader.type === TYPE_DIRECTORY && size !== 0) {
|
|
534
|
+
throw new Error("archive declares a non-zero directory size");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const parserExpectedPath = TYPE_FILE.has(rawHeader.type) && rawHeader.rawSize > 0
|
|
538
|
+
? path.endsWith("/")
|
|
539
|
+
? path.slice(0, -1)
|
|
540
|
+
: path
|
|
541
|
+
: path;
|
|
542
|
+
const parserEffectivePath = entry.extended?.path ?? entry.globalExtended?.path ?? entry.path;
|
|
543
|
+
const parserMissedNewlinePath =
|
|
544
|
+
effective.path?.includes("\n") &&
|
|
545
|
+
!entry.extended?.path &&
|
|
546
|
+
!entry.globalExtended?.path &&
|
|
547
|
+
parserEffectivePath === entry.path;
|
|
548
|
+
if ((parserEffectivePath !== parserExpectedPath && !parserMissedNewlinePath) || entry.size !== size) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
"archive parser metadata disagrees with raw archive"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (rawHeader.type === TYPE_DIRECTORY) {
|
|
555
|
+
validateMemberPath(path);
|
|
556
|
+
tap.beginEntry(0);
|
|
557
|
+
entry.resume();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!TYPE_FILE.has(rawHeader.type)) {
|
|
561
|
+
throw new Error(`Artifact contains unsupported archive entry type "${rawHeader.type}"`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
validateMemberPath(path);
|
|
565
|
+
const destination = normalizedMemberDestination(path);
|
|
566
|
+
if (memberDestinations.has(destination)) {
|
|
567
|
+
throw new Error(`archive contains duplicate member destination "${destination}"`);
|
|
568
|
+
}
|
|
569
|
+
memberDestinations.add(destination);
|
|
570
|
+
declared += size;
|
|
571
|
+
if (declared > maxUnpackedBytes) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`archive declares ${declared} bytes of members, over the ${maxUnpackedBytes}-byte ceiling`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const chunks = [];
|
|
578
|
+
let received = 0;
|
|
579
|
+
let finalized = false;
|
|
580
|
+
const finalize = () => {
|
|
581
|
+
if (finalized) return;
|
|
582
|
+
finalized = true;
|
|
583
|
+
if (received !== size) {
|
|
584
|
+
fail(`archive member ended after ${received} bytes, expected ${size}`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
files.push({ path, buffer: Buffer.concat(chunks, size) });
|
|
588
|
+
};
|
|
589
|
+
entry.on("data", (chunk) => {
|
|
590
|
+
received += chunk.length;
|
|
591
|
+
chunks.push(Buffer.from(chunk));
|
|
592
|
+
});
|
|
593
|
+
entry.on("end", finalize);
|
|
594
|
+
tap.beginEntry(size);
|
|
595
|
+
entry.resume();
|
|
596
|
+
if (size === 0) finalize();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const parserPromise = new Promise((resolve, reject) => {
|
|
600
|
+
const rejectOnce = (error) => {
|
|
601
|
+
const next = parserError(error, tap);
|
|
602
|
+
if (!failure) failure = next;
|
|
603
|
+
stopInput();
|
|
604
|
+
reject(failure);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
parser.on("error", rejectOnce);
|
|
608
|
+
parser.on("warn", (code, message) => rejectOnce(new Error(`${code}: ${message}`)));
|
|
609
|
+
parser.on("ignoredEntry", (entry) => {
|
|
610
|
+
try {
|
|
611
|
+
countEntry();
|
|
612
|
+
fail(new Error(`Artifact contains unsupported archive entry type "${entry.header.typeKey}"`));
|
|
613
|
+
} catch (error) {
|
|
614
|
+
fail(error);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
parser.on("meta", (metaText) => {
|
|
618
|
+
try {
|
|
619
|
+
processMetadata(metaText);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
fail(error);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
parser.on("entry", (entry) => {
|
|
625
|
+
try {
|
|
626
|
+
processEntry(entry);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
fail(error);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
parser.on("eof", () => {
|
|
632
|
+
try {
|
|
633
|
+
if (pendingMetadata.length > 0 || rawMetadata.length > 0 || tap.hasPendingMetadata()) {
|
|
634
|
+
throw new Error("archive ends with metadata that has no following entry");
|
|
635
|
+
}
|
|
636
|
+
sawEndOfArchive = true;
|
|
637
|
+
stopInput();
|
|
638
|
+
parser.end();
|
|
639
|
+
} catch (error) {
|
|
640
|
+
fail(error);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
parser.on("end", () => {
|
|
644
|
+
if (failure) return;
|
|
645
|
+
if (!sawEndOfArchive) {
|
|
646
|
+
rejectOnce(
|
|
647
|
+
new Error(
|
|
648
|
+
sawPartialBlock
|
|
649
|
+
? "archive ends in the middle of a header block"
|
|
650
|
+
: "archive ends before two zero blocks"
|
|
651
|
+
)
|
|
652
|
+
);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
resolve();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
source.on("error", rejectOnce);
|
|
659
|
+
gunzip.on("error", rejectOnce);
|
|
660
|
+
meter.on("error", rejectOnce);
|
|
661
|
+
framer.on("error", rejectOnce);
|
|
662
|
+
framer.on("end", () => {
|
|
663
|
+
if (!stopped && !failure) parser.end();
|
|
664
|
+
});
|
|
665
|
+
framer.on("data", (block) => {
|
|
666
|
+
if (stopped || failure) return;
|
|
667
|
+
try {
|
|
668
|
+
let parserBlock = block;
|
|
669
|
+
if (block.length === BLOCK_SIZE) parserBlock = tap.feed(block);
|
|
670
|
+
else sawPartialBlock = true;
|
|
671
|
+
if (!parser.write(parserBlock)) framer.pause();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
fail(error);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
parser.on("drain", () => {
|
|
677
|
+
if (!stopped && !failure) framer.resume();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
source.pipe(gunzip).pipe(meter).pipe(framer);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await parserPromise;
|
|
684
|
+
return files;
|
|
685
|
+
}
|