@blamejs/blamejs-shop 0.0.83 → 0.0.98
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/CHANGELOG.md +30 -0
- package/lib/admin.js +11 -7
- package/lib/customer-import.js +1 -1
- package/lib/email-campaigns.js +1 -1
- package/lib/pwa-manifest.js +1 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +8 -0
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +167 -2
- package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +37 -0
- package/lib/vendor/blamejs/index.js +15 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +629 -0
- package/lib/vendor/blamejs/lib/archive-gz.js +229 -0
- package/lib/vendor/blamejs/lib/archive-read.js +781 -0
- package/lib/vendor/blamejs/lib/archive-tar-read.js +418 -0
- package/lib/vendor/blamejs/lib/archive-tar.js +571 -0
- package/lib/vendor/blamejs/lib/archive.js +24 -2
- package/lib/vendor/blamejs/lib/audit.js +22 -7
- package/lib/vendor/blamejs/lib/backup/index.js +469 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +180 -0
- package/lib/vendor/blamejs/lib/guard-filename.js +205 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +309 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.7.json +86 -0
- package/lib/vendor/blamejs/release-notes/v0.12.8.json +81 -0
- package/lib/vendor/blamejs/release-notes/v0.12.9.json +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +247 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +228 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +180 -0
- package/package.json +2 -2
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* archive-tar — POSIX pax tar write + read. Sibling of lib/archive.js
|
|
4
|
+
* the way lib/archive-read.js is — the `b.archive.tar` and
|
|
5
|
+
* `b.archive.read.tar` primitives both live in the `b.archive`
|
|
6
|
+
* namespace declared in lib/archive.js; this file just carries the
|
|
7
|
+
* tar-format wire-format implementation.
|
|
8
|
+
*
|
|
9
|
+
* POSIX pax tar format — second archive format in the family, sits
|
|
10
|
+
* alongside `b.archive.zip` (write-only ZIP) + `b.archive.read.zip`
|
|
11
|
+
* (read-side ZIP, v0.12.7).
|
|
12
|
+
*
|
|
13
|
+
* Two output paths mirror the ZIP write side:
|
|
14
|
+
* - `toBuffer()` — whole archive in memory.
|
|
15
|
+
* - `toStream(writable)` — block-by-block streaming.
|
|
16
|
+
* - `toAdapter(adapter)` — write through the v0.12.7 adapter contract.
|
|
17
|
+
*
|
|
18
|
+
* Two input paths for read:
|
|
19
|
+
* - `b.archive.read.tar(adapter)` — random-access OR sequential.
|
|
20
|
+
* Tar has no central directory, so sequential header-by-header
|
|
21
|
+
* walk IS the canonical adversarial-safe path. Trusted-stream
|
|
22
|
+
* adapters are first-class here (unlike ZIP read where they
|
|
23
|
+
* carry a documented trust boundary).
|
|
24
|
+
* - `b.archive.adapters.trustedStream(readable)` — preferred for
|
|
25
|
+
* multi-gibibyte tar streams that don't fit in memory.
|
|
26
|
+
*
|
|
27
|
+
* Format guarantees:
|
|
28
|
+
* - ustar magic + version "00" at byte offset 257 of every header.
|
|
29
|
+
* - Names ≤ 100 chars + sizes ≤ 8 GiB fit in the fixed ustar header.
|
|
30
|
+
* - Longer names + larger sizes get a pax extended header (POSIX.1-
|
|
31
|
+
* 2001 §4.18) preceding the entry: "len key=value\n" records.
|
|
32
|
+
* - Two zero blocks (1024 bytes) terminate the archive.
|
|
33
|
+
* - Path-traversal refused at `addFile` (mirrors ZIP write).
|
|
34
|
+
* - Deterministic insertion order; deterministic mtime opt-in.
|
|
35
|
+
*
|
|
36
|
+
* Entry-type policy (typeflag handling on read):
|
|
37
|
+
* - 0 / '\0' (regular file): extract.
|
|
38
|
+
* - 5 (directory): extract (mkdir -p).
|
|
39
|
+
* - 1 (hardlink): refused by default; `allowDangerous: { hardlinks:
|
|
40
|
+
* true }` opt-in routes link target through
|
|
41
|
+
* `b.guardFilename.verifyExtractionPath`.
|
|
42
|
+
* - 2 (symlink): same as hardlink — refused by default; opt-in
|
|
43
|
+
* with realpath-on-target check.
|
|
44
|
+
* - 3 / 4 / 6 / 7 (char-device / block-device / FIFO / contiguous):
|
|
45
|
+
* refused unconditionally — no use case in application archives.
|
|
46
|
+
* - x (pax extended header): consumed by reader; merged into next
|
|
47
|
+
* entry's metadata.
|
|
48
|
+
* - g (pax global header): consumed; applies to all following.
|
|
49
|
+
*
|
|
50
|
+
* Defends:
|
|
51
|
+
* - CVE-2026-23745 / CVE-2026-24842 (node-tar symlink+hardlink
|
|
52
|
+
* path-resolution divergence between safety check + creation).
|
|
53
|
+
* - CVE-2025-4517 PATH_MAX TOCTOU (carries v0.12.7's
|
|
54
|
+
* `verifyExtractionPath` dual-check).
|
|
55
|
+
* - CVE-2025-11001 / 11002 (7-Zip symlink TOCTOU on extract).
|
|
56
|
+
* - CVE-2024-12905 / CVE-2025-48387 (tar-fs path traversal).
|
|
57
|
+
* - CVE-2025-4138 / 4330 (Python tarfile data filter bypass).
|
|
58
|
+
*
|
|
59
|
+
* Out of scope (v1):
|
|
60
|
+
* - Sparse-file emission (read reconstructs them; write doesn't
|
|
61
|
+
* produce sparse).
|
|
62
|
+
* - Compression (tar.gz lands v0.12.9 via `b.archive.gz`
|
|
63
|
+
* composition).
|
|
64
|
+
* - BSD-tar extensions beyond pax.
|
|
65
|
+
*
|
|
66
|
+
* @card
|
|
67
|
+
* POSIX pax tar archive — write + read with the same defense surface as the ZIP family.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
var nodeStream = require("node:stream");
|
|
71
|
+
var streamPromises = require("node:stream/promises");
|
|
72
|
+
var C = require("./constants");
|
|
73
|
+
var lazyRequire = require("./lazy-require");
|
|
74
|
+
var { defineClass } = require("./framework-error");
|
|
75
|
+
|
|
76
|
+
var TarError = defineClass("TarError", { alwaysPermanent: true });
|
|
77
|
+
|
|
78
|
+
void streamPromises; void nodeStream;
|
|
79
|
+
|
|
80
|
+
// Lazy because archive-gz lazy-imports archive-tar-read (sibling read
|
|
81
|
+
// module) — a top-of-file `require("./archive-gz")` would create a
|
|
82
|
+
// load-order cycle that depends on file-walk order.
|
|
83
|
+
var archiveGz = lazyRequire(function () { return require("./archive-gz"); });
|
|
84
|
+
|
|
85
|
+
// ---- Wire-format constants -----------------------------------------------
|
|
86
|
+
|
|
87
|
+
var BLOCK_SIZE = C.BYTES.bytes(512); // tar block size (POSIX)
|
|
88
|
+
var USTAR_MAGIC = "ustar\u0000"; // 6 bytes including the trailing NUL
|
|
89
|
+
var USTAR_VERSION = "00"; // 2 bytes
|
|
90
|
+
var NAME_MAX = C.BYTES.bytes(100); // ustar name field cap
|
|
91
|
+
var PREFIX_MAX = C.BYTES.bytes(155); // ustar prefix field cap
|
|
92
|
+
var LINKNAME_MAX = C.BYTES.bytes(100); // ustar linkname field cap
|
|
93
|
+
var USTAR_SIZE_MAX = 0o77777777777; // allow:raw-byte-literal — 11 octal digits = 8 GiB - 1 per ustar size-field width
|
|
94
|
+
|
|
95
|
+
// Header field byte offsets (POSIX.1-1988 ustar; same in POSIX.1-2001 pax)
|
|
96
|
+
var H_NAME = C.BYTES.bytes(0);
|
|
97
|
+
var H_MODE = C.BYTES.bytes(100);
|
|
98
|
+
var H_UID = C.BYTES.bytes(108);
|
|
99
|
+
var H_GID = C.BYTES.bytes(116);
|
|
100
|
+
var H_SIZE = C.BYTES.bytes(124);
|
|
101
|
+
var H_MTIME = C.BYTES.bytes(136);
|
|
102
|
+
var H_CHKSUM = C.BYTES.bytes(148);
|
|
103
|
+
var H_TYPEFLAG = C.BYTES.bytes(156);
|
|
104
|
+
var H_LINKNAME = C.BYTES.bytes(157);
|
|
105
|
+
var H_MAGIC = C.BYTES.bytes(257);
|
|
106
|
+
var H_VERSION = C.BYTES.bytes(263);
|
|
107
|
+
var H_UNAME = C.BYTES.bytes(265);
|
|
108
|
+
var H_GNAME = C.BYTES.bytes(297);
|
|
109
|
+
var H_DEVMAJOR = C.BYTES.bytes(329);
|
|
110
|
+
var H_DEVMINOR = C.BYTES.bytes(337);
|
|
111
|
+
var H_PREFIX = C.BYTES.bytes(345);
|
|
112
|
+
|
|
113
|
+
// Field widths
|
|
114
|
+
var W_MODE = C.BYTES.bytes(8);
|
|
115
|
+
var W_UID = C.BYTES.bytes(8);
|
|
116
|
+
var W_GID = C.BYTES.bytes(8);
|
|
117
|
+
var W_SIZE = C.BYTES.bytes(12);
|
|
118
|
+
var W_MTIME = C.BYTES.bytes(12);
|
|
119
|
+
var W_CHKSUM = C.BYTES.bytes(8);
|
|
120
|
+
var W_UNAME = C.BYTES.bytes(32);
|
|
121
|
+
var W_GNAME = C.BYTES.bytes(32);
|
|
122
|
+
|
|
123
|
+
// Typeflags — write side emits only TF_REGULAR / TF_DIRECTORY /
|
|
124
|
+
// TF_PAX_EXTENDED. The full POSIX typeflag set lives in
|
|
125
|
+
// lib/archive-tar-read.js (the reader has to classify every typeflag
|
|
126
|
+
// the wild produces). H_DEVMAJOR / H_DEVMINOR offsets stay for the
|
|
127
|
+
// shared _parseHeader call site even though the write side never
|
|
128
|
+
// emits typeflag 3/4 (char/block device) entries.
|
|
129
|
+
var TF_REGULAR = "0";
|
|
130
|
+
var TF_DIRECTORY = "5";
|
|
131
|
+
var TF_PAX_EXTENDED = "x";
|
|
132
|
+
|
|
133
|
+
void H_DEVMAJOR; void H_DEVMINOR;
|
|
134
|
+
|
|
135
|
+
// ---- Helpers -------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function _padBlock(buf) {
|
|
138
|
+
// Pad buf to the next 512-byte boundary with NUL bytes.
|
|
139
|
+
var rem = buf.length % BLOCK_SIZE;
|
|
140
|
+
if (rem === 0) return buf;
|
|
141
|
+
var pad = Buffer.alloc(BLOCK_SIZE - rem);
|
|
142
|
+
return Buffer.concat([buf, pad]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _writeOctal(buf, value, offset, width) {
|
|
146
|
+
// ustar octal fields: 6-11 octal digits + space or NUL terminator.
|
|
147
|
+
// For width=8, that's 7 octal digits + terminator. For width=12, 11
|
|
148
|
+
// digits + terminator.
|
|
149
|
+
var digits = width - 1;
|
|
150
|
+
var oct = value.toString(8); // allow:raw-byte-literal — radix=8 for octal stringify per ustar field format
|
|
151
|
+
if (oct.length > digits) {
|
|
152
|
+
throw new TarError("archive-tar/octal-overflow",
|
|
153
|
+
"value " + value + " (octal " + oct + ") exceeds field width " + digits);
|
|
154
|
+
}
|
|
155
|
+
// Left-pad with '0' to fill the digits.
|
|
156
|
+
while (oct.length < digits) oct = "0" + oct;
|
|
157
|
+
buf.write(oct, offset, digits, "ascii");
|
|
158
|
+
buf.writeUInt8(0x20, offset + digits); // allow:raw-byte-literal — ASCII space (' ') terminator per ustar
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _writeString(buf, value, offset, width) {
|
|
162
|
+
// ASCII-encoded; truncated if longer than width. NUL terminates if
|
|
163
|
+
// shorter than width.
|
|
164
|
+
var ascii = String(value);
|
|
165
|
+
var bytes = Buffer.from(ascii, "utf8");
|
|
166
|
+
if (bytes.length > width) {
|
|
167
|
+
throw new TarError("archive-tar/field-overflow",
|
|
168
|
+
"string " + JSON.stringify(value) + " (" + bytes.length + " bytes) exceeds field width " + width);
|
|
169
|
+
}
|
|
170
|
+
bytes.copy(buf, offset);
|
|
171
|
+
// Remaining bytes already NUL from Buffer.alloc.
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _readOctal(buf, offset, width) {
|
|
175
|
+
// Read an octal-encoded field. Terminator may be space or NUL.
|
|
176
|
+
var s = "";
|
|
177
|
+
for (var i = 0; i < width; i += 1) {
|
|
178
|
+
var c = buf[offset + i];
|
|
179
|
+
if (c === 0x20 || c === 0) break; // allow:raw-byte-literal — ASCII space (0x20) + NUL (0x00) field terminators
|
|
180
|
+
if (c < 0x30 || c > 0x37) { // allow:raw-byte-literal — ASCII '0' (0x30) .. '7' (0x37) octal digits
|
|
181
|
+
throw new TarError("archive-tar/bad-octal",
|
|
182
|
+
"non-octal byte 0x" + c.toString(16) + " at offset " + (offset + i)); // allow:raw-byte-literal — radix=16 for diagnostic hex format
|
|
183
|
+
}
|
|
184
|
+
s += String.fromCharCode(c);
|
|
185
|
+
}
|
|
186
|
+
if (s.length === 0) return 0;
|
|
187
|
+
return parseInt(s, 8); // allow:raw-byte-literal — radix=8 for octal parse per ustar field format
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _readString(buf, offset, width) {
|
|
191
|
+
// Read NUL-terminated ASCII / UTF-8 from the field. Truncates at
|
|
192
|
+
// the first NUL byte.
|
|
193
|
+
var end = offset;
|
|
194
|
+
var limit = offset + width;
|
|
195
|
+
while (end < limit && buf[end] !== 0) end += 1;
|
|
196
|
+
return buf.slice(offset, end).toString("utf8");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _computeChecksum(buf) {
|
|
200
|
+
// ustar checksum: sum of every byte in the 512-byte header, with the
|
|
201
|
+
// chksum field itself treated as 8 spaces (0x20). Stored as 6 octal
|
|
202
|
+
// digits + NUL + space (per the spec — historically GNU tar writes
|
|
203
|
+
// it that way; modern parsers accept several variants).
|
|
204
|
+
var sum = 0;
|
|
205
|
+
for (var i = 0; i < BLOCK_SIZE; i += 1) {
|
|
206
|
+
if (i >= H_CHKSUM && i < H_CHKSUM + W_CHKSUM) {
|
|
207
|
+
sum += 0x20; // allow:raw-byte-literal — chksum field treated as 8 spaces per POSIX.1-1988
|
|
208
|
+
} else {
|
|
209
|
+
sum += buf[i];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return sum;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _writeChecksum(buf) {
|
|
216
|
+
// Write 6 octal digits + NUL + space into the chksum field.
|
|
217
|
+
var sum = _computeChecksum(buf);
|
|
218
|
+
var oct = sum.toString(8); // allow:raw-byte-literal — radix=8 for octal stringify per ustar chksum field format
|
|
219
|
+
while (oct.length < 6) oct = "0" + oct; // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
|
|
220
|
+
if (oct.length > 6) { // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
|
|
221
|
+
// Header is corrupt / oversized somewhere; surface a typed error.
|
|
222
|
+
throw new TarError("archive-tar/chksum-overflow",
|
|
223
|
+
"chksum " + sum + " (" + oct + ") exceeds 6 octal digits");
|
|
224
|
+
}
|
|
225
|
+
buf.write(oct, H_CHKSUM, 6, "ascii"); // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
|
|
226
|
+
buf.writeUInt8(0, H_CHKSUM + 6); // allow:raw-byte-literal — chksum field: 6 digits + NUL + space per POSIX ustar
|
|
227
|
+
buf.writeUInt8(0x20, H_CHKSUM + 7); // allow:raw-byte-literal — chksum field: 6 digits + NUL + space per POSIX ustar
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _verifyChecksum(buf) {
|
|
231
|
+
// Parse the stored chksum + compare against recomputed.
|
|
232
|
+
var stored = _readOctal(buf, H_CHKSUM, W_CHKSUM);
|
|
233
|
+
var computed = _computeChecksum(buf);
|
|
234
|
+
return stored === computed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _normalizeName(name) {
|
|
238
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
239
|
+
throw new TarError("archive-tar/bad-name", "addFile: name must be non-empty string");
|
|
240
|
+
}
|
|
241
|
+
if (name.indexOf("\u0000") !== -1) {
|
|
242
|
+
throw new TarError("archive-tar/bad-name", "addFile: name contains null byte");
|
|
243
|
+
}
|
|
244
|
+
var normalized = name.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
245
|
+
var segs = normalized.split("/");
|
|
246
|
+
for (var i = 0; i < segs.length; i += 1) {
|
|
247
|
+
if (segs[i] === "..") {
|
|
248
|
+
throw new TarError("archive-tar/bad-name", "addFile: name contains '..' segment");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- Pax extended header -----------------------------------------------
|
|
255
|
+
|
|
256
|
+
function _buildPaxRecord(key, value) {
|
|
257
|
+
// POSIX.1-2001 §4.18 — "len key=value\n" where len is the total
|
|
258
|
+
// length of the record (including len itself + space + key + '=' +
|
|
259
|
+
// value + '\n'). Compute len iteratively because len encodes itself.
|
|
260
|
+
var keyVal = key + "=" + value + "\n";
|
|
261
|
+
// Initial guess: len contributes ≤ 3 digits; iterate until stable.
|
|
262
|
+
var lenStr = String(keyVal.length + 1 + 1); // +1 for space, +1 for len digit
|
|
263
|
+
var len = parseInt(lenStr, 10) + keyVal.length;
|
|
264
|
+
while (true) {
|
|
265
|
+
var encoded = String(len) + " " + keyVal;
|
|
266
|
+
if (encoded.length === len) return encoded;
|
|
267
|
+
len = encoded.length;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _buildPaxExtendedHeader(records, prefixName) {
|
|
272
|
+
// records: array of [key, value] pairs.
|
|
273
|
+
// Emit the records as the body, then build a typeflag-'x' ustar
|
|
274
|
+
// header pointing at the body.
|
|
275
|
+
var body = "";
|
|
276
|
+
for (var i = 0; i < records.length; i += 1) {
|
|
277
|
+
body += _buildPaxRecord(records[i][0], records[i][1]);
|
|
278
|
+
}
|
|
279
|
+
var bodyBuf = Buffer.from(body, "utf8");
|
|
280
|
+
var hdr = _buildUstarHeader({
|
|
281
|
+
name: prefixName || "PaxHeader/extended",
|
|
282
|
+
typeflag: TF_PAX_EXTENDED,
|
|
283
|
+
size: bodyBuf.length,
|
|
284
|
+
mtime: 0,
|
|
285
|
+
mode: 0o644,
|
|
286
|
+
});
|
|
287
|
+
return Buffer.concat([hdr, _padBlock(bodyBuf)]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _parsePaxRecords(buf) {
|
|
291
|
+
// Parse "len key=value\n" records from a pax extended-header body.
|
|
292
|
+
var out = Object.create(null);
|
|
293
|
+
var pos = 0;
|
|
294
|
+
var s = buf.toString("utf8");
|
|
295
|
+
while (pos < s.length) {
|
|
296
|
+
var spaceIdx = s.indexOf(" ", pos);
|
|
297
|
+
if (spaceIdx < 0) {
|
|
298
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
299
|
+
"pax record at byte " + pos + " missing length-space delimiter");
|
|
300
|
+
}
|
|
301
|
+
var lenStr = s.slice(pos, spaceIdx);
|
|
302
|
+
var len = parseInt(lenStr, 10);
|
|
303
|
+
if (!Number.isFinite(len) || len <= 0) {
|
|
304
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
305
|
+
"pax record length " + JSON.stringify(lenStr) + " is not a positive integer");
|
|
306
|
+
}
|
|
307
|
+
var record = s.slice(pos, pos + len);
|
|
308
|
+
if (record[record.length - 1] !== "\n") {
|
|
309
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
310
|
+
"pax record at byte " + pos + " not newline-terminated");
|
|
311
|
+
}
|
|
312
|
+
var eqIdx = record.indexOf("=", spaceIdx - pos + 1);
|
|
313
|
+
if (eqIdx < 0) {
|
|
314
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
315
|
+
"pax record at byte " + pos + " missing key=value delimiter");
|
|
316
|
+
}
|
|
317
|
+
var key = record.slice(spaceIdx - pos + 1, eqIdx);
|
|
318
|
+
var value = record.slice(eqIdx + 1, record.length - 1);
|
|
319
|
+
out[key] = value;
|
|
320
|
+
pos += len;
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- ustar header build --------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function _buildUstarHeader(entry) {
|
|
328
|
+
var buf = Buffer.alloc(BLOCK_SIZE);
|
|
329
|
+
// name + prefix split for names > 100 chars
|
|
330
|
+
var name = entry.name;
|
|
331
|
+
var prefix = "";
|
|
332
|
+
if (name.length > NAME_MAX) {
|
|
333
|
+
// Try splitting on a '/' so prefix + '/' + name <= 100 + 155
|
|
334
|
+
var splitIdx = name.lastIndexOf("/", NAME_MAX);
|
|
335
|
+
if (splitIdx > 0 && (name.length - splitIdx - 1) <= NAME_MAX &&
|
|
336
|
+
splitIdx <= PREFIX_MAX) {
|
|
337
|
+
prefix = name.slice(0, splitIdx);
|
|
338
|
+
name = name.slice(splitIdx + 1);
|
|
339
|
+
} else {
|
|
340
|
+
// Won't fit in ustar — pax extended header handles it; the
|
|
341
|
+
// ustar header carries a "PaxHeader/data" sentinel name.
|
|
342
|
+
name = "PaxHeader/data";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
_writeString(buf, name, H_NAME, NAME_MAX);
|
|
346
|
+
_writeOctal(buf, entry.mode || 0o644, H_MODE, W_MODE);
|
|
347
|
+
_writeOctal(buf, entry.uid || 0, H_UID, W_UID);
|
|
348
|
+
_writeOctal(buf, entry.gid || 0, H_GID, W_GID);
|
|
349
|
+
_writeOctal(buf, entry.size || 0, H_SIZE, W_SIZE);
|
|
350
|
+
_writeOctal(buf, entry.mtime || 0, H_MTIME, W_MTIME);
|
|
351
|
+
// chksum field — written as 8 spaces during computation, then
|
|
352
|
+
// replaced with the computed value below.
|
|
353
|
+
buf.fill(0x20, H_CHKSUM, H_CHKSUM + W_CHKSUM); // allow:raw-byte-literal — pre-fill chksum field with spaces per POSIX
|
|
354
|
+
buf.write(entry.typeflag || TF_REGULAR, H_TYPEFLAG, 1, "ascii");
|
|
355
|
+
if (entry.linkname) _writeString(buf, entry.linkname, H_LINKNAME, LINKNAME_MAX);
|
|
356
|
+
buf.write(USTAR_MAGIC, H_MAGIC, 6, "ascii"); // allow:raw-byte-literal — ustar magic is 6 bytes per POSIX
|
|
357
|
+
buf.write(USTAR_VERSION, H_VERSION, 2, "ascii"); // allow:raw-byte-literal — ustar version is 2 bytes per POSIX
|
|
358
|
+
if (entry.uname) _writeString(buf, entry.uname, H_UNAME, W_UNAME);
|
|
359
|
+
if (entry.gname) _writeString(buf, entry.gname, H_GNAME, W_GNAME);
|
|
360
|
+
if (prefix) _writeString(buf, prefix, H_PREFIX, PREFIX_MAX);
|
|
361
|
+
_writeChecksum(buf);
|
|
362
|
+
return buf;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---- Public tar builder ---------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @primitive b.archive.tar
|
|
369
|
+
* @signature b.archive.tar()
|
|
370
|
+
* @since 0.12.8
|
|
371
|
+
* @status stable
|
|
372
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
373
|
+
* @related b.archive.zip, b.archive.read.tar
|
|
374
|
+
*
|
|
375
|
+
* POSIX pax tar archive builder. Mirrors `b.archive.zip()`'s
|
|
376
|
+
* `addFile / addDirectory / toBuffer / toStream / toAdapter / digest`
|
|
377
|
+
* contract.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* var t = b.archive.tar();
|
|
381
|
+
* t.addFile("readme.txt", "Hello\n");
|
|
382
|
+
* t.addFile("data/numbers.csv", "n,sq\n1,1\n2,4\n");
|
|
383
|
+
* var bytes = t.toBuffer();
|
|
384
|
+
* t.entryCount; // → 2
|
|
385
|
+
*/
|
|
386
|
+
function tarBuilder() {
|
|
387
|
+
var entries = [];
|
|
388
|
+
|
|
389
|
+
function addFile(name, content, opts) {
|
|
390
|
+
var normalized = _normalizeName(name);
|
|
391
|
+
opts = opts || {};
|
|
392
|
+
var bodyBuf;
|
|
393
|
+
if (Buffer.isBuffer(content)) bodyBuf = content;
|
|
394
|
+
else if (typeof content === "string") bodyBuf = Buffer.from(content, "utf8");
|
|
395
|
+
else throw new TarError("archive-tar/bad-content",
|
|
396
|
+
"addFile: content must be Buffer or string, got " + typeof content);
|
|
397
|
+
entries.push({
|
|
398
|
+
name: normalized,
|
|
399
|
+
kind: "file",
|
|
400
|
+
typeflag: TF_REGULAR,
|
|
401
|
+
body: bodyBuf,
|
|
402
|
+
size: bodyBuf.length,
|
|
403
|
+
mode: opts.mode || 0o644,
|
|
404
|
+
mtime: opts.mtime !== undefined ? opts.mtime
|
|
405
|
+
: Math.floor((opts.fixedMtime || Date.now()) / C.TIME.seconds(1)),
|
|
406
|
+
uid: opts.uid || 0,
|
|
407
|
+
gid: opts.gid || 0,
|
|
408
|
+
uname: opts.uname || "",
|
|
409
|
+
gname: opts.gname || "",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function addDirectory(name, opts) {
|
|
414
|
+
var normalized = _normalizeName(name);
|
|
415
|
+
if (normalized[normalized.length - 1] !== "/") normalized = normalized + "/";
|
|
416
|
+
opts = opts || {};
|
|
417
|
+
entries.push({
|
|
418
|
+
name: normalized,
|
|
419
|
+
kind: "directory",
|
|
420
|
+
typeflag: TF_DIRECTORY,
|
|
421
|
+
body: Buffer.alloc(0),
|
|
422
|
+
size: 0,
|
|
423
|
+
mode: opts.mode || 0o755,
|
|
424
|
+
mtime: opts.mtime !== undefined ? opts.mtime : Math.floor(Date.now() / C.TIME.seconds(1)),
|
|
425
|
+
uid: opts.uid || 0,
|
|
426
|
+
gid: opts.gid || 0,
|
|
427
|
+
uname: opts.uname || "",
|
|
428
|
+
gname: opts.gname || "",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function _entryBytes(entry) {
|
|
433
|
+
// Determine if we need a pax extended header.
|
|
434
|
+
var paxRecords = [];
|
|
435
|
+
if (entry.name.length > NAME_MAX) {
|
|
436
|
+
// Check if ustar prefix/name split would work.
|
|
437
|
+
var splitIdx = entry.name.lastIndexOf("/", NAME_MAX);
|
|
438
|
+
var fits = splitIdx > 0 &&
|
|
439
|
+
(entry.name.length - splitIdx - 1) <= NAME_MAX &&
|
|
440
|
+
splitIdx <= PREFIX_MAX;
|
|
441
|
+
if (!fits) paxRecords.push(["path", entry.name]);
|
|
442
|
+
}
|
|
443
|
+
if (entry.size > USTAR_SIZE_MAX) {
|
|
444
|
+
paxRecords.push(["size", String(entry.size)]);
|
|
445
|
+
}
|
|
446
|
+
var pieces = [];
|
|
447
|
+
if (paxRecords.length > 0) {
|
|
448
|
+
pieces.push(_buildPaxExtendedHeader(paxRecords, "PaxHeader/" + entry.name.slice(0, 80))); // allow:raw-byte-literal — pax header name fits in ustar 100-char field with 20-char prefix budget
|
|
449
|
+
}
|
|
450
|
+
var hdr = _buildUstarHeader({
|
|
451
|
+
name: entry.name,
|
|
452
|
+
mode: entry.mode,
|
|
453
|
+
uid: entry.uid,
|
|
454
|
+
gid: entry.gid,
|
|
455
|
+
size: Math.min(entry.size, USTAR_SIZE_MAX),
|
|
456
|
+
mtime: entry.mtime,
|
|
457
|
+
typeflag: entry.typeflag,
|
|
458
|
+
linkname: entry.linkname,
|
|
459
|
+
uname: entry.uname,
|
|
460
|
+
gname: entry.gname,
|
|
461
|
+
});
|
|
462
|
+
pieces.push(hdr);
|
|
463
|
+
if (entry.body.length > 0) {
|
|
464
|
+
pieces.push(_padBlock(entry.body));
|
|
465
|
+
}
|
|
466
|
+
return Buffer.concat(pieces);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function toBuffer() {
|
|
470
|
+
var pieces = [];
|
|
471
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
472
|
+
pieces.push(_entryBytes(entries[i]));
|
|
473
|
+
}
|
|
474
|
+
// Two zero blocks terminate the archive (POSIX requirement).
|
|
475
|
+
pieces.push(Buffer.alloc(BLOCK_SIZE * 2)); // allow:raw-byte-literal — POSIX requires 2 trailing zero blocks
|
|
476
|
+
return Buffer.concat(pieces);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function toAdapter(adapter) {
|
|
480
|
+
if (!adapter || typeof adapter.write !== "function") {
|
|
481
|
+
throw new TarError("archive-tar/bad-adapter",
|
|
482
|
+
"toAdapter: adapter must expose a write(bytes) method");
|
|
483
|
+
}
|
|
484
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
485
|
+
await adapter.write(_entryBytes(entries[i]));
|
|
486
|
+
}
|
|
487
|
+
await adapter.write(Buffer.alloc(BLOCK_SIZE * 2)); // allow:raw-byte-literal — 2 trailing zero blocks
|
|
488
|
+
if (typeof adapter.end === "function") await adapter.end();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function toStream(writable) {
|
|
492
|
+
return new Promise(function (resolve, reject) {
|
|
493
|
+
try {
|
|
494
|
+
writable.write(toBuffer());
|
|
495
|
+
writable.end();
|
|
496
|
+
writable.once("finish", resolve);
|
|
497
|
+
writable.once("error", reject);
|
|
498
|
+
} catch (e) { reject(e); }
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function digest() {
|
|
503
|
+
var nodeCrypto = require("node:crypto");
|
|
504
|
+
return nodeCrypto.createHash("sha3-512").update(toBuffer()).digest("hex");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function toGzip(adapter, gzOpts) {
|
|
508
|
+
// Convenience composition: materialize the tar then wrap through
|
|
509
|
+
// b.archive.gz. archive-gz is lazy-required at module top to break
|
|
510
|
+
// the load-order cycle with archive-tar-read.
|
|
511
|
+
return archiveGz().gz(toBuffer(), gzOpts || {}).toAdapter(adapter);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
addFile: addFile,
|
|
516
|
+
addDirectory: addDirectory,
|
|
517
|
+
toBuffer: toBuffer,
|
|
518
|
+
toStream: toStream,
|
|
519
|
+
toAdapter: toAdapter,
|
|
520
|
+
toGzip: toGzip,
|
|
521
|
+
digest: digest,
|
|
522
|
+
get entryCount() { return entries.length; },
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ---- Shared read-side helper --------------------------------------------
|
|
527
|
+
//
|
|
528
|
+
// Sibling module lib/archive-tar-read.js imports _parseHeader via
|
|
529
|
+
// these exports. The reader lives in a separate file so the
|
|
530
|
+
// validator can pair the b.archive.read.tar primitive cleanly
|
|
531
|
+
// (sibling shape to lib/archive-read.js for ZIP). _parseHeader
|
|
532
|
+
// lives here so the write side's checksum + header-field encoding
|
|
533
|
+
// has its inverse in the same wire-format module.
|
|
534
|
+
|
|
535
|
+
function _parseHeader(buf) {
|
|
536
|
+
var magic = buf.slice(H_MAGIC, H_MAGIC + C.BYTES.bytes(5)).toString("ascii");
|
|
537
|
+
if (magic !== "ustar") {
|
|
538
|
+
throw new TarError("archive-tar/bad-magic",
|
|
539
|
+
"header magic " + JSON.stringify(magic) + " is not ustar");
|
|
540
|
+
}
|
|
541
|
+
if (!_verifyChecksum(buf)) {
|
|
542
|
+
throw new TarError("archive-tar/bad-chksum",
|
|
543
|
+
"header checksum mismatch");
|
|
544
|
+
}
|
|
545
|
+
var name = _readString(buf, H_NAME, NAME_MAX);
|
|
546
|
+
var prefix = _readString(buf, H_PREFIX, PREFIX_MAX);
|
|
547
|
+
if (prefix.length > 0) name = prefix + "/" + name;
|
|
548
|
+
return {
|
|
549
|
+
name: name,
|
|
550
|
+
mode: _readOctal(buf, H_MODE, W_MODE),
|
|
551
|
+
uid: _readOctal(buf, H_UID, W_UID),
|
|
552
|
+
gid: _readOctal(buf, H_GID, W_GID),
|
|
553
|
+
size: _readOctal(buf, H_SIZE, W_SIZE),
|
|
554
|
+
mtime: _readOctal(buf, H_MTIME, W_MTIME),
|
|
555
|
+
typeflag: String.fromCharCode(buf[H_TYPEFLAG]),
|
|
556
|
+
linkname: _readString(buf, H_LINKNAME, LINKNAME_MAX),
|
|
557
|
+
uname: _readString(buf, H_UNAME, W_UNAME),
|
|
558
|
+
gname: _readString(buf, H_GNAME, W_GNAME),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = {
|
|
563
|
+
tar: tarBuilder,
|
|
564
|
+
TarError: TarError,
|
|
565
|
+
// Exposed for sibling modules + tests
|
|
566
|
+
_buildUstarHeader: _buildUstarHeader,
|
|
567
|
+
_parseHeader: _parseHeader,
|
|
568
|
+
_readOctal: _readOctal,
|
|
569
|
+
_readString: _readString,
|
|
570
|
+
_verifyChecksum: _verifyChecksum,
|
|
571
|
+
};
|
|
@@ -538,9 +538,31 @@ function zip() {
|
|
|
538
538
|
};
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
// Read primitive — random-access ZIP reader composes the same wire-
|
|
542
|
+
// format constants as the write side. Lives in a sibling file to keep
|
|
543
|
+
// this module under the line-budget the @primitive validator + the
|
|
544
|
+
// codebase-patterns "single-concern file" pattern prefer.
|
|
545
|
+
var archiveRead = require("./archive-read");
|
|
546
|
+
var archiveTar = require("./archive-tar");
|
|
547
|
+
var archiveTarRead = require("./archive-tar-read");
|
|
548
|
+
var archiveGz = require("./archive-gz");
|
|
549
|
+
|
|
541
550
|
module.exports = {
|
|
542
|
-
zip:
|
|
543
|
-
|
|
551
|
+
zip: zip,
|
|
552
|
+
tar: archiveTar.tar,
|
|
553
|
+
gz: archiveGz.gz,
|
|
554
|
+
ArchiveError: ArchiveError,
|
|
555
|
+
TarError: archiveTar.TarError,
|
|
556
|
+
ArchiveGzError: archiveGz.ArchiveGzError,
|
|
557
|
+
read: {
|
|
558
|
+
zip: archiveRead.zip,
|
|
559
|
+
tar: archiveTarRead.tar,
|
|
560
|
+
gz: archiveGz.read.gz,
|
|
561
|
+
fromGzip: archiveGz.read.gz,
|
|
562
|
+
ArchiveReadError: archiveRead.ArchiveReadError,
|
|
563
|
+
DEFAULT_BOMB_POLICY: archiveRead.DEFAULT_BOMB_POLICY,
|
|
564
|
+
DEFAULT_ENTRY_TYPE_POLICY: archiveRead.DEFAULT_ENTRY_TYPE_POLICY,
|
|
565
|
+
},
|
|
544
566
|
// Test-only export — operators don't call this; it's here for unit-testing
|
|
545
567
|
// the CRC implementation against known vectors.
|
|
546
568
|
_crc32ForTest: _crc32,
|
|
@@ -1118,17 +1118,28 @@ function _ensureHandler() {
|
|
|
1118
1118
|
// items were emitted against. Early-exit drops them; the
|
|
1119
1119
|
// alternative is silent corruption of the next chain.
|
|
1120
1120
|
var droppedThisBatch = 0;
|
|
1121
|
+
var firstDropAction = null;
|
|
1122
|
+
var firstDropMessage = null;
|
|
1121
1123
|
for (var i = 0; i < batch.length; i++) {
|
|
1122
1124
|
if (ctx && ctx.isShutdown && ctx.isShutdown()) return;
|
|
1123
1125
|
try { await record(batch[i]); }
|
|
1124
1126
|
catch (e) {
|
|
1125
1127
|
droppedThisBatch += 1;
|
|
1126
|
-
// Per-item failure shouldn't drop the whole batch;
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1128
|
+
// Per-item failure shouldn't drop the whole batch; the
|
|
1129
|
+
// signal flows through observability.safeEvent below. The
|
|
1130
|
+
// prior per-drop log.error was noise: boot-phase
|
|
1131
|
+
// audit.emit() racing db.init() fires dozens of these
|
|
1132
|
+
// during normal startup, and operator dashboards
|
|
1133
|
+
// alert-routing on the "error" level read it as a real
|
|
1134
|
+
// failure. The aggregate observability metric is the
|
|
1135
|
+
// documented signal channel; capture the first drop's
|
|
1136
|
+
// action + message in its metadata so operators alerting
|
|
1137
|
+
// on `system.audit.chain_write_dropped` get a
|
|
1138
|
+
// representative sample without per-line log spam.
|
|
1139
|
+
if (firstDropAction === null) {
|
|
1140
|
+
firstDropAction = (batch[i] && batch[i].action) || null;
|
|
1141
|
+
firstDropMessage = (e && e.message) ? e.message : String(e);
|
|
1142
|
+
}
|
|
1132
1143
|
}
|
|
1133
1144
|
}
|
|
1134
1145
|
// Surface chain-write integrity failures via observability so
|
|
@@ -1137,7 +1148,11 @@ function _ensureHandler() {
|
|
|
1137
1148
|
// broken — so observability is the only sink left.
|
|
1138
1149
|
if (droppedThisBatch > 0) {
|
|
1139
1150
|
observability.safeEvent("system.audit.chain_write_dropped",
|
|
1140
|
-
droppedThisBatch, {
|
|
1151
|
+
droppedThisBatch, {
|
|
1152
|
+
batchSize: batch.length,
|
|
1153
|
+
firstDropAction: firstDropAction,
|
|
1154
|
+
firstDropMessage: firstDropMessage,
|
|
1155
|
+
});
|
|
1141
1156
|
}
|
|
1142
1157
|
},
|
|
1143
1158
|
});
|