@blamejs/core 0.12.7 → 0.12.9
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 +4 -0
- package/lib/archive-gz.js +229 -0
- package/lib/archive-tar-read.js +418 -0
- package/lib/archive-tar.js +571 -0
- package/lib/archive.js +12 -2
- package/lib/audit.js +22 -7
- package/lib/backup/index.js +237 -13
- package/lib/guard-archive.js +40 -0
- package/lib/safe-archive.js +49 -15
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/backup/index.js
CHANGED
|
@@ -65,6 +65,8 @@ var compliance = lazyRequire(function () { return require("../compliance"); });
|
|
|
65
65
|
// module graph (CLI tools, stand-alone backup runners). The db()
|
|
66
66
|
// callable resolves on first access.
|
|
67
67
|
var dbModuleLazy = lazyRequire(function () { return require("../db"); });
|
|
68
|
+
var archiveLazy = lazyRequire(function () { return require("../archive"); });
|
|
69
|
+
var archiveAdaptersLazy = lazyRequire(function () { return require("../archive-adapters"); });
|
|
68
70
|
var { defineClass } = require("../framework-error");
|
|
69
71
|
|
|
70
72
|
var BackupError = defineClass("BackupError");
|
|
@@ -993,6 +995,7 @@ module.exports = {
|
|
|
993
995
|
create: create,
|
|
994
996
|
diskStorage: diskStorage,
|
|
995
997
|
bundleAdapterStorage: bundleAdapterStorage,
|
|
998
|
+
migrate: migrate,
|
|
996
999
|
recommendedFiles: recommendedFiles,
|
|
997
1000
|
runInWorker: runInWorker,
|
|
998
1001
|
verifyManifestSignature: verifyManifestSignature,
|
|
@@ -1055,6 +1058,35 @@ function bundleAdapterStorage(opts) {
|
|
|
1055
1058
|
"bundleAdapterStorage: adapter missing method '" + required[i] + "'");
|
|
1056
1059
|
}
|
|
1057
1060
|
}
|
|
1061
|
+
// v0.12.8 — `format: "tar"` becomes the default for new bundles.
|
|
1062
|
+
// `format: "directory"` opts back into the v0.12.7 file-by-file
|
|
1063
|
+
// layout for operators with existing bundles. The format is
|
|
1064
|
+
// operator-supplied so a single backup engine can transition over
|
|
1065
|
+
// time + b.backup.migrate() handles the directory → tar conversion.
|
|
1066
|
+
var format = opts.format || "tar";
|
|
1067
|
+
if (format !== "tar" && format !== "tar.gz" && format !== "directory") {
|
|
1068
|
+
throw new BackupError("backup/bad-format",
|
|
1069
|
+
"bundleAdapterStorage: format must be \"tar\" (default) | \"tar.gz\" (v0.12.9 compressed) | \"directory\" (legacy v0.12.7)");
|
|
1070
|
+
}
|
|
1071
|
+
// Codex P2 on v0.12.8 PR #159 — tar mode builds the whole archive
|
|
1072
|
+
// in memory before adapter.writeFile because the v0.12.8 adapter
|
|
1073
|
+
// contract is bytes-in (no writeStream method). The OOM-prevention
|
|
1074
|
+
// gate is maxBundleBytes: writeBundle pre-walks the source tree,
|
|
1075
|
+
// sums file sizes, and refuses upfront if the projected uncompressed
|
|
1076
|
+
// tar would exceed the cap. Default 8 GiB — accommodates typical
|
|
1077
|
+
// db + mail-spool + log bundles while refusing pathological inputs.
|
|
1078
|
+
// Defer-with-condition for true streaming: when the adapter
|
|
1079
|
+
// contract gains writeStream(key) (slated for v0.13+ alongside
|
|
1080
|
+
// multipart S3 upload primitives), this path switches to
|
|
1081
|
+
// tarBuilder.toStream() and writes chunks as they're produced.
|
|
1082
|
+
var maxBundleBytes = opts.maxBundleBytes !== undefined
|
|
1083
|
+
? opts.maxBundleBytes
|
|
1084
|
+
: 8 * 1024 * 1024 * 1024; // allow:raw-byte-literal — 8 GiB default cap; uses C.BYTES.bytes covers numeric-literal rule
|
|
1085
|
+
if (!numericBounds.isPositiveFiniteInt(maxBundleBytes)) { // allow:inline-numeric-bounds-cascade — required-with-default opt
|
|
1086
|
+
throw new BackupError("backup/bad-arg",
|
|
1087
|
+
"bundleAdapterStorage: maxBundleBytes must be a positive finite integer; got " +
|
|
1088
|
+
numericBounds.shape(opts.maxBundleBytes));
|
|
1089
|
+
}
|
|
1058
1090
|
|
|
1059
1091
|
function _ensureBundleId(bundleId) {
|
|
1060
1092
|
if (!_isValidBundleId(bundleId)) {
|
|
@@ -1078,6 +1110,20 @@ function bundleAdapterStorage(opts) {
|
|
|
1078
1110
|
return out;
|
|
1079
1111
|
}
|
|
1080
1112
|
|
|
1113
|
+
// Tar-format bundle storage stores the whole bundle as a single
|
|
1114
|
+
// key under `<bundleId>/bundle.tar` (or `<bundleId>/bundle.tar.gz`
|
|
1115
|
+
// for the v0.12.9 compressed variant). The marker is named that
|
|
1116
|
+
// way so listBundles + hasBundle can locate either format by key
|
|
1117
|
+
// prefix walk.
|
|
1118
|
+
var TAR_KEY_SUFFIX = "/bundle.tar";
|
|
1119
|
+
var TAR_GZ_KEY_SUFFIX = "/bundle.tar.gz";
|
|
1120
|
+
|
|
1121
|
+
function _hasBundleKey(bundleId, format) {
|
|
1122
|
+
if (format === "tar") return adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
|
|
1123
|
+
if (format === "tar.gz") return adapter.hasKey(bundleId + TAR_GZ_KEY_SUFFIX);
|
|
1124
|
+
return adapter.hasKey(bundleId + "/manifest.json");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1081
1127
|
return {
|
|
1082
1128
|
name: "adapter",
|
|
1083
1129
|
async writeBundle(bundleId, sourceDir) {
|
|
@@ -1086,16 +1132,58 @@ function bundleAdapterStorage(opts) {
|
|
|
1086
1132
|
throw new BackupError("backup/no-source",
|
|
1087
1133
|
"writeBundle: sourceDir does not exist: " + sourceDir);
|
|
1088
1134
|
}
|
|
1089
|
-
var alreadyHas = await
|
|
1135
|
+
var alreadyHas = await _hasBundleKey(bundleId, format);
|
|
1090
1136
|
if (alreadyHas) {
|
|
1091
1137
|
throw new BackupError("backup/bundle-exists",
|
|
1092
1138
|
"writeBundle: bundle '" + bundleId + "' already exists in storage");
|
|
1093
1139
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1140
|
+
if (format === "tar" || format === "tar.gz") {
|
|
1141
|
+
// Pack the source-directory tree into a single tar archive +
|
|
1142
|
+
// store under one key. Composes b.archive.tar (+ b.archive.gz
|
|
1143
|
+
// for the tar.gz variant which adds gzip compression on the
|
|
1144
|
+
// wire). Bundle sizes drop ~3-5× on text-heavy backups
|
|
1145
|
+
// (databases, JSON exports, mail spools) under tar.gz.
|
|
1146
|
+
//
|
|
1147
|
+
// Codex P2 on v0.12.8 PR #159 — tar bytes are materialized in
|
|
1148
|
+
// memory because the v0.12.8 adapter contract is bytes-in
|
|
1149
|
+
// (writeFile takes a Buffer, no writeStream method). The
|
|
1150
|
+
// maxBundleBytes pre-walk computes the uncompressed payload
|
|
1151
|
+
// size (file bytes only — tar header overhead is bounded at
|
|
1152
|
+
// ~512 B per entry + 1024 B trailer) and refuses upfront so
|
|
1153
|
+
// pathological inputs throw `backup/bundle-too-large` instead
|
|
1154
|
+
// of OOM. The defer-with-condition for true streaming is
|
|
1155
|
+
// gated on the adapter contract gaining writeStream(key).
|
|
1156
|
+
var relPaths = _walkDirSync(sourceDir, []);
|
|
1157
|
+
var projectedBytes = 0;
|
|
1158
|
+
for (var pi = 0; pi < relPaths.length; pi += 1) {
|
|
1159
|
+
var stat = nodeFs.statSync(nodePath.join(sourceDir, relPaths[pi]));
|
|
1160
|
+
projectedBytes += stat.size;
|
|
1161
|
+
}
|
|
1162
|
+
if (projectedBytes > maxBundleBytes) {
|
|
1163
|
+
throw new BackupError("backup/bundle-too-large",
|
|
1164
|
+
"writeBundle: projected uncompressed bundle " + projectedBytes +
|
|
1165
|
+
" bytes exceeds maxBundleBytes=" + maxBundleBytes +
|
|
1166
|
+
" — split the source tree across multiple bundles or raise the cap");
|
|
1167
|
+
}
|
|
1168
|
+
var t = archiveLazy().tar();
|
|
1169
|
+
for (var i = 0; i < relPaths.length; i += 1) {
|
|
1170
|
+
var rel = relPaths[i];
|
|
1171
|
+
var bytes = nodeFs.readFileSync(nodePath.join(sourceDir, rel));
|
|
1172
|
+
t.addFile(rel, bytes);
|
|
1173
|
+
}
|
|
1174
|
+
var keySuffix = format === "tar.gz" ? TAR_GZ_KEY_SUFFIX : TAR_KEY_SUFFIX;
|
|
1175
|
+
var payloadBytes = format === "tar.gz"
|
|
1176
|
+
? archiveLazy().gz(t.toBuffer()).toBuffer()
|
|
1177
|
+
: t.toBuffer();
|
|
1178
|
+
await adapter.writeFile(bundleId + keySuffix, payloadBytes);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
// Directory format (v0.12.7 layout).
|
|
1182
|
+
var dirRelPaths = _walkDirSync(sourceDir, []);
|
|
1183
|
+
for (var j = 0; j < dirRelPaths.length; j += 1) {
|
|
1184
|
+
var dirRel = dirRelPaths[j];
|
|
1185
|
+
var dirBytes = nodeFs.readFileSync(nodePath.join(sourceDir, dirRel));
|
|
1186
|
+
await adapter.writeFile(bundleId + "/" + dirRel, dirBytes);
|
|
1099
1187
|
}
|
|
1100
1188
|
},
|
|
1101
1189
|
async readBundle(bundleId, destDir) {
|
|
@@ -1104,20 +1192,51 @@ function bundleAdapterStorage(opts) {
|
|
|
1104
1192
|
throw new BackupError("backup/dest-exists",
|
|
1105
1193
|
"readBundle: destDir already exists: " + destDir);
|
|
1106
1194
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1195
|
+
// Detect which format this bundle is in — operators with mixed
|
|
1196
|
+
// pre-v0.12.8 + post-v0.12.8 (+ v0.12.9 tar.gz) bundles can read
|
|
1197
|
+
// every flavor back.
|
|
1198
|
+
var hasTar = await adapter.hasKey(bundleId + TAR_KEY_SUFFIX);
|
|
1199
|
+
var hasTarGz = await adapter.hasKey(bundleId + TAR_GZ_KEY_SUFFIX);
|
|
1200
|
+
var hasManifest = await adapter.hasKey(bundleId + "/manifest.json");
|
|
1201
|
+
if (!hasTar && !hasTarGz && !hasManifest) {
|
|
1109
1202
|
throw new BackupError("backup/bundle-not-found",
|
|
1110
1203
|
"readBundle: '" + bundleId + "' not in storage");
|
|
1111
1204
|
}
|
|
1112
1205
|
atomicFile.ensureDir(destDir);
|
|
1206
|
+
if (hasTarGz) {
|
|
1207
|
+
// Codex P1/P2 on v0.12.9 PR #160 — propagate maxBundleBytes
|
|
1208
|
+
// to the gz restore path + disable the expansion-ratio cap.
|
|
1209
|
+
// archive.read.gz defaults (1 GiB output / 100× ratio) are
|
|
1210
|
+
// bomb-defense settings appropriate for adversarial input;
|
|
1211
|
+
// this is a SELF-AUTHORED bundle the writeBundle path
|
|
1212
|
+
// produced under maxBundleBytes — the restore contract is
|
|
1213
|
+
// "decompress to at most what was permitted on write." A
|
|
1214
|
+
// zero-filled DB file can easily hit >100× compression
|
|
1215
|
+
// ratio; without these opts the same primitive writes
|
|
1216
|
+
// bundles it can't read back.
|
|
1217
|
+
var gzBytes = await adapter.readFile(bundleId + TAR_GZ_KEY_SUFFIX);
|
|
1218
|
+
var gzReader = archiveLazy().read.gz(archiveAdaptersLazy().buffer(gzBytes), {
|
|
1219
|
+
maxDecompressedBytes: maxBundleBytes,
|
|
1220
|
+
maxExpansionRatio: 0,
|
|
1221
|
+
});
|
|
1222
|
+
var tarReader = gzReader.asTar();
|
|
1223
|
+
await tarReader.extract({ destination: destDir });
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (hasTar) {
|
|
1227
|
+
var tarBytes = await adapter.readFile(bundleId + TAR_KEY_SUFFIX);
|
|
1228
|
+
var reader = archiveLazy().read.tar(archiveAdaptersLazy().buffer(tarBytes));
|
|
1229
|
+
await reader.extract({ destination: destDir });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
// Directory format readback (v0.12.7 layout).
|
|
1233
|
+
var keys = await adapter.listKeys(bundleId + "/");
|
|
1113
1234
|
for (var i = 0; i < keys.length; i += 1) {
|
|
1114
1235
|
var key = keys[i];
|
|
1115
1236
|
var prefix = bundleId + "/";
|
|
1116
1237
|
if (key.indexOf(prefix) !== 0) continue;
|
|
1117
1238
|
var rel = key.slice(prefix.length);
|
|
1118
|
-
// Path-safety: rel must not escape destDir.
|
|
1119
|
-
// verifyExtractionPath dual-check primitive when the dest
|
|
1120
|
-
// already exists (writeable directory just created).
|
|
1239
|
+
// Path-safety: rel must not escape destDir.
|
|
1121
1240
|
var destPath = nodePath.join(destDir, rel);
|
|
1122
1241
|
var resolvedDest = nodePath.resolve(destPath);
|
|
1123
1242
|
var resolvedRoot = nodePath.resolve(destDir);
|
|
@@ -1129,7 +1248,13 @@ function bundleAdapterStorage(opts) {
|
|
|
1129
1248
|
}
|
|
1130
1249
|
atomicFile.ensureDir(nodePath.dirname(destPath));
|
|
1131
1250
|
var bytes = await adapter.readFile(key);
|
|
1132
|
-
|
|
1251
|
+
// Exclusive-create (wx) carries the v0.12.7 atomic-rollback
|
|
1252
|
+
// contract: readBundle refuses to overwrite pre-existing
|
|
1253
|
+
// files at destPath. Combined with the upfront destDir check
|
|
1254
|
+
// above (refuses if destDir already exists), the only way
|
|
1255
|
+
// wx fires here is a symlink swap between ensureDir and write
|
|
1256
|
+
// — which the framework refuses rather than following.
|
|
1257
|
+
nodeFs.writeFileSync(destPath, bytes, { flag: "wx", mode: 0o600 });
|
|
1133
1258
|
}
|
|
1134
1259
|
},
|
|
1135
1260
|
async listBundles() {
|
|
@@ -1175,7 +1300,18 @@ function bundleAdapterStorage(opts) {
|
|
|
1175
1300
|
},
|
|
1176
1301
|
async hasBundle(bundleId) {
|
|
1177
1302
|
_ensureBundleId(bundleId);
|
|
1178
|
-
|
|
1303
|
+
// Format-aware: check the storage layout's marker key. Tar
|
|
1304
|
+
// bundles store under <bid>/bundle.tar; tar.gz bundles store
|
|
1305
|
+
// under <bid>/bundle.tar.gz; directory bundles store under
|
|
1306
|
+
// <bid>/manifest.json. Operators with a mixed bundle set
|
|
1307
|
+
// (some tar, some tar.gz, some directory) get true for any.
|
|
1308
|
+
var tarKey = bundleId + TAR_KEY_SUFFIX;
|
|
1309
|
+
var tarGzKey = bundleId + TAR_GZ_KEY_SUFFIX;
|
|
1310
|
+
var dirKey = bundleId + "/manifest.json";
|
|
1311
|
+
if (await adapter.hasKey(tarKey)) return true;
|
|
1312
|
+
if (await adapter.hasKey(tarGzKey)) return true;
|
|
1313
|
+
if (await adapter.hasKey(dirKey)) return true;
|
|
1314
|
+
return false;
|
|
1179
1315
|
},
|
|
1180
1316
|
};
|
|
1181
1317
|
}
|
|
@@ -1244,3 +1380,91 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
|
1244
1380
|
},
|
|
1245
1381
|
};
|
|
1246
1382
|
};
|
|
1383
|
+
|
|
1384
|
+
// ---- v0.12.8: migrate ----------------------------------------------------
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* @primitive b.backup.migrate
|
|
1388
|
+
* @signature b.backup.migrate(opts)
|
|
1389
|
+
* @since 0.12.8
|
|
1390
|
+
* @status stable
|
|
1391
|
+
* @related b.backup.bundleAdapterStorage
|
|
1392
|
+
*
|
|
1393
|
+
* One-shot helper that walks an operator's directory-tree-format
|
|
1394
|
+
* bundle (v0.12.7 layout) and writes the same content as a tar-format
|
|
1395
|
+
* bundle via the v0.12.8 `bundleAdapterStorage`. Idempotent: re-
|
|
1396
|
+
* running on an already-migrated bundle is a no-op. Source stays in
|
|
1397
|
+
* place by default; operators with explicit transition windows opt
|
|
1398
|
+
* into the inline replace via `deleteSourceOnSuccess: true`.
|
|
1399
|
+
*
|
|
1400
|
+
* @opts
|
|
1401
|
+
* from: bundleAdapterStorage with format: "directory",
|
|
1402
|
+
* to: bundleAdapterStorage with format: "tar",
|
|
1403
|
+
* bundleId: string (single-bundle migrate; omit to migrate all),
|
|
1404
|
+
* deleteSourceOnSuccess: boolean (default false; explicit opt-in),
|
|
1405
|
+
*
|
|
1406
|
+
* @example
|
|
1407
|
+
* var from = b.backup.bundleAdapterStorage({
|
|
1408
|
+
* adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v7" }),
|
|
1409
|
+
* format: "directory",
|
|
1410
|
+
* });
|
|
1411
|
+
* var to = b.backup.bundleAdapterStorage({
|
|
1412
|
+
* adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v8" }),
|
|
1413
|
+
* format: "tar",
|
|
1414
|
+
* });
|
|
1415
|
+
* await b.backup.migrate({ from: from, to: to });
|
|
1416
|
+
*/
|
|
1417
|
+
async function migrate(opts) {
|
|
1418
|
+
opts = opts || {};
|
|
1419
|
+
if (!opts.from || typeof opts.from.readBundle !== "function" ||
|
|
1420
|
+
typeof opts.from.listBundles !== "function") {
|
|
1421
|
+
throw new BackupError("backup/bad-from",
|
|
1422
|
+
"migrate: opts.from must be a storage backend (got " + typeof opts.from + ")");
|
|
1423
|
+
}
|
|
1424
|
+
if (!opts.to || typeof opts.to.writeBundle !== "function" ||
|
|
1425
|
+
typeof opts.to.hasBundle !== "function") {
|
|
1426
|
+
throw new BackupError("backup/bad-to",
|
|
1427
|
+
"migrate: opts.to must be a storage backend (got " + typeof opts.to + ")");
|
|
1428
|
+
}
|
|
1429
|
+
var ids;
|
|
1430
|
+
if (opts.bundleId) {
|
|
1431
|
+
if (!_isValidBundleId(opts.bundleId)) {
|
|
1432
|
+
throw new BackupError("backup/bad-bundle-id",
|
|
1433
|
+
"migrate: bundleId must match the framework's timestamp+suffix format");
|
|
1434
|
+
}
|
|
1435
|
+
ids = [opts.bundleId];
|
|
1436
|
+
} else {
|
|
1437
|
+
var list = await opts.from.listBundles();
|
|
1438
|
+
ids = list.map(function (b) { return b.bundleId; });
|
|
1439
|
+
}
|
|
1440
|
+
var migrated = 0;
|
|
1441
|
+
var skipped = 0;
|
|
1442
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
1443
|
+
var bid = ids[i];
|
|
1444
|
+
// Idempotency: skip if destination already has the bundle.
|
|
1445
|
+
if (await opts.to.hasBundle(bid)) {
|
|
1446
|
+
skipped += 1;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
// Stage source-bundle into a tmp dir, then write via destination.
|
|
1450
|
+
var tmpDir = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(),
|
|
1451
|
+
"blamejs-backup-migrate-" + bid + "-"));
|
|
1452
|
+
var stageDir = nodePath.join(tmpDir, "bundle");
|
|
1453
|
+
try {
|
|
1454
|
+
await opts.from.readBundle(bid, stageDir);
|
|
1455
|
+
await opts.to.writeBundle(bid, stageDir);
|
|
1456
|
+
migrated += 1;
|
|
1457
|
+
if (opts.deleteSourceOnSuccess === true) {
|
|
1458
|
+
if (typeof opts.from.deleteBundle === "function") {
|
|
1459
|
+
await opts.from.deleteBundle(bid);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
} finally {
|
|
1463
|
+
try { nodeFs.rmSync(tmpDir, { recursive: true, force: true }); }
|
|
1464
|
+
catch (_e) { /* drop-silent */ }
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return { migrated: migrated, skipped: skipped, total: ids.length };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
|
package/lib/guard-archive.js
CHANGED
|
@@ -902,6 +902,7 @@ module.exports = {
|
|
|
902
902
|
inspect: inspect,
|
|
903
903
|
zipBombPolicy: zipBombPolicy,
|
|
904
904
|
entryTypePolicy: entryTypePolicy,
|
|
905
|
+
tarEntryPolicy: tarEntryPolicy,
|
|
905
906
|
};
|
|
906
907
|
|
|
907
908
|
// ---- v0.12.7 extensions ---------------------------------------------------
|
|
@@ -1039,3 +1040,42 @@ function entryTypePolicy(opts) {
|
|
|
1039
1040
|
sockets: opts.sockets === true,
|
|
1040
1041
|
});
|
|
1041
1042
|
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* @primitive b.guardArchive.tarEntryPolicy
|
|
1046
|
+
* @signature b.guardArchive.tarEntryPolicy(opts)
|
|
1047
|
+
* @since 0.12.8
|
|
1048
|
+
* @status stable
|
|
1049
|
+
* @related b.guardArchive.entryTypePolicy, b.archive.read.tar
|
|
1050
|
+
*
|
|
1051
|
+
* Tar-specific entry-type policy. Same shape as `entryTypePolicy`
|
|
1052
|
+
* but explicitly named for tar's typeflag vocabulary (1=hardlink,
|
|
1053
|
+
* 2=symlink, 3=char-device, 4=block-device, 6=FIFO, 7=contiguous-
|
|
1054
|
+
* file) so call sites read clearly when the operator's intent is
|
|
1055
|
+
* tar-specific. Defaults refuse every dangerous typeflag. Operators
|
|
1056
|
+
* opting symlinks / hardlinks in get the link target routed through
|
|
1057
|
+
* `b.guardFilename.verifyExtractionPath`'s realpath-on-target check
|
|
1058
|
+
* (defends CVE-2026-23745 / 24842 node-tar path-resolution divergence
|
|
1059
|
+
* class).
|
|
1060
|
+
*
|
|
1061
|
+
* @opts
|
|
1062
|
+
* symlinks: false,
|
|
1063
|
+
* hardlinks: false,
|
|
1064
|
+
* devices: false,
|
|
1065
|
+
* fifos: false,
|
|
1066
|
+
* sockets: false,
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* var policy = b.guardArchive.tarEntryPolicy({ symlinks: true });
|
|
1070
|
+
* await b.safeArchive.extract({
|
|
1071
|
+
* source, destination, entryTypePolicy: policy,
|
|
1072
|
+
* allowDangerous: { symlinks: true },
|
|
1073
|
+
* });
|
|
1074
|
+
*/
|
|
1075
|
+
function tarEntryPolicy(opts) {
|
|
1076
|
+
// Same shape as entryTypePolicy; aliased for tar-specific call-site
|
|
1077
|
+
// readability. The implementation is intentionally identical — the
|
|
1078
|
+
// policy-object shape is format-neutral, only the typeflag mapping
|
|
1079
|
+
// in the reader differs.
|
|
1080
|
+
return entryTypePolicy(opts);
|
|
1081
|
+
}
|
package/lib/safe-archive.js
CHANGED
|
@@ -45,6 +45,8 @@ var SafeArchiveError = defineClass("SafeArchiveError", { alwaysPermanent: true }
|
|
|
45
45
|
|
|
46
46
|
var archiveRead = lazyRequire(function () { return require("./archive-read"); });
|
|
47
47
|
var archiveAdapters = lazyRequire(function () { return require("./archive-adapters"); });
|
|
48
|
+
var archiveTarRead = lazyRequire(function () { return require("./archive-tar-read"); });
|
|
49
|
+
var archiveGz = lazyRequire(function () { return require("./archive-gz"); });
|
|
48
50
|
|
|
49
51
|
// ---- Format sniffing ----------------------------------------------------
|
|
50
52
|
|
|
@@ -178,18 +180,43 @@ async function extract(opts) {
|
|
|
178
180
|
var sniff = await _sniffMagic(source);
|
|
179
181
|
format = sniff.format;
|
|
180
182
|
}
|
|
181
|
-
|
|
183
|
+
var reader;
|
|
184
|
+
if (format === "zip") {
|
|
185
|
+
reader = archiveRead().zip(source, {
|
|
186
|
+
bombPolicy: opts.bombPolicy,
|
|
187
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
188
|
+
guardProfile: opts.guardProfile,
|
|
189
|
+
audit: opts.audit,
|
|
190
|
+
});
|
|
191
|
+
} else if (format === "tar") {
|
|
192
|
+
reader = archiveTarRead().tar(source, {
|
|
193
|
+
bombPolicy: opts.bombPolicy,
|
|
194
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
195
|
+
guardProfile: opts.guardProfile,
|
|
196
|
+
audit: opts.audit,
|
|
197
|
+
});
|
|
198
|
+
} else if (format === "tar.gz") {
|
|
199
|
+
// gzip envelope around tar — safeDecompress caps run on the gz
|
|
200
|
+
// layer before the tar walker ever sees a decompressed byte.
|
|
201
|
+
reader = archiveGz().read.gz(source, {
|
|
202
|
+
maxDecompressedBytes: opts.maxDecompressedBytes,
|
|
203
|
+
maxExpansionRatio: opts.maxExpansionRatio,
|
|
204
|
+
audit: opts.audit,
|
|
205
|
+
}).asTar({
|
|
206
|
+
bombPolicy: opts.bombPolicy,
|
|
207
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
208
|
+
guardProfile: opts.guardProfile,
|
|
209
|
+
audit: opts.audit,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
182
212
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
183
|
-
"extract: format=" + JSON.stringify(format) + " — v0.12.
|
|
184
|
-
"(
|
|
213
|
+
"extract: format=" + JSON.stringify(format) + " — v0.12.9 ships ZIP + tar + tar.gz " +
|
|
214
|
+
"(encryptPacked-wrap lands v0.12.10)");
|
|
185
215
|
}
|
|
186
|
-
var
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
guardProfile: opts.guardProfile,
|
|
190
|
-
audit: opts.audit,
|
|
216
|
+
var result = await reader.extract({
|
|
217
|
+
destination: opts.destination,
|
|
218
|
+
allowDangerous: opts.allowDangerous,
|
|
191
219
|
});
|
|
192
|
-
var result = await reader.extract({ destination: opts.destination });
|
|
193
220
|
return Object.assign({ format: format }, result);
|
|
194
221
|
} finally {
|
|
195
222
|
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
@@ -238,14 +265,21 @@ async function inspect(opts) {
|
|
|
238
265
|
var sniff = await _sniffMagic(source);
|
|
239
266
|
format = sniff.format;
|
|
240
267
|
}
|
|
241
|
-
|
|
268
|
+
var reader;
|
|
269
|
+
if (format === "zip") {
|
|
270
|
+
reader = archiveRead().zip(source, {
|
|
271
|
+
bombPolicy: opts.bombPolicy,
|
|
272
|
+
audit: opts.audit,
|
|
273
|
+
});
|
|
274
|
+
} else if (format === "tar") {
|
|
275
|
+
reader = archiveTarRead().tar(source, {
|
|
276
|
+
bombPolicy: opts.bombPolicy,
|
|
277
|
+
audit: opts.audit,
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
242
280
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
243
|
-
"inspect: format=" + JSON.stringify(format) + " — v0.12.
|
|
281
|
+
"inspect: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar");
|
|
244
282
|
}
|
|
245
|
-
var reader = archiveRead().zip(source, {
|
|
246
|
-
bombPolicy: opts.bombPolicy,
|
|
247
|
-
audit: opts.audit,
|
|
248
|
-
});
|
|
249
283
|
var entries = await reader.inspect();
|
|
250
284
|
var totalCompressed = 0;
|
|
251
285
|
var totalUncompressed = 0;
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f5a3cd7a-802d-4f55-ba59-958f474f38a0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-23T17:58:07.192Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.9",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.9",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.9",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.9",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|