@openclaw/fs-safe 0.8.3 → 0.8.4

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/absolute-path.d.ts.map +1 -1
  3. package/dist/absolute-path.js +8 -7
  4. package/dist/archive-native.d.ts +1 -0
  5. package/dist/archive-native.d.ts.map +1 -1
  6. package/dist/archive-native.js +15 -68
  7. package/dist/archive-plan.d.ts +20 -0
  8. package/dist/archive-plan.d.ts.map +1 -0
  9. package/dist/archive-plan.js +55 -0
  10. package/dist/archive-tar-inspect.d.ts +7 -0
  11. package/dist/archive-tar-inspect.d.ts.map +1 -0
  12. package/dist/archive-tar-inspect.js +59 -0
  13. package/dist/archive-tar.d.ts +3 -1
  14. package/dist/archive-tar.d.ts.map +1 -1
  15. package/dist/archive-tar.js +12 -49
  16. package/dist/archive.d.ts +1 -0
  17. package/dist/archive.d.ts.map +1 -1
  18. package/dist/archive.js +20 -36
  19. package/dist/bounded-read.d.ts.map +1 -1
  20. package/dist/bounded-read.js +31 -4
  21. package/dist/directory-durability.js +5 -5
  22. package/dist/directory-guard.d.ts.map +1 -1
  23. package/dist/directory-guard.js +5 -6
  24. package/dist/file-store-boundary.js +1 -1
  25. package/dist/file-store-prune.js +17 -8
  26. package/dist/file-store.js +2 -2
  27. package/dist/guarded-mkdir.d.ts.map +1 -1
  28. package/dist/guarded-mkdir.js +5 -4
  29. package/dist/native-binding.d.ts +2 -1
  30. package/dist/native-binding.d.ts.map +1 -1
  31. package/dist/native-pinned-write-windows.js +6 -3
  32. package/dist/native-pinned-write.js +3 -3
  33. package/dist/native-staged-file.d.ts +2 -2
  34. package/dist/native-staged-file.d.ts.map +1 -1
  35. package/dist/native-staged-file.js +12 -8
  36. package/dist/opened-realpath.d.ts.map +1 -1
  37. package/dist/opened-realpath.js +10 -9
  38. package/dist/path-policy.js +2 -2
  39. package/dist/pinned-write.d.ts +1 -0
  40. package/dist/pinned-write.d.ts.map +1 -1
  41. package/dist/pinned-write.js +15 -11
  42. package/dist/regular-file.js +8 -8
  43. package/dist/replace-file-descriptor.d.ts.map +1 -1
  44. package/dist/replace-file-descriptor.js +8 -4
  45. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  46. package/dist/replace-file-temp-owner.js +13 -7
  47. package/dist/root-context.js +5 -5
  48. package/dist/root-impl.d.ts +2 -1
  49. package/dist/root-impl.d.ts.map +1 -1
  50. package/dist/root-impl.js +35 -26
  51. package/dist/root-path-existing.d.ts.map +1 -1
  52. package/dist/root-path-existing.js +2 -3
  53. package/dist/root-path-symlink.d.ts.map +1 -1
  54. package/dist/root-path-symlink.js +2 -3
  55. package/dist/root-path.d.ts.map +1 -1
  56. package/dist/root-path.js +3 -4
  57. package/dist/root-paths.d.ts.map +1 -1
  58. package/dist/root-paths.js +16 -15
  59. package/dist/root-write-mode.d.ts.map +1 -1
  60. package/dist/root-write-mode.js +5 -4
  61. package/dist/root-write-verification.js +6 -6
  62. package/dist/secret-file.js +2 -2
  63. package/dist/strict-file-identity.d.ts +1 -1
  64. package/dist/strict-file-identity.d.ts.map +1 -1
  65. package/dist/symlink-parents.d.ts.map +1 -1
  66. package/dist/symlink-parents.js +1 -2
  67. package/docs/archive.md +57 -0
  68. package/docs/reading.md +2 -0
  69. package/docs/root.md +10 -1
  70. package/docs/types.md +2 -1
  71. package/docs/writing.md +26 -3
  72. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.4 - 2026-09-07
6
+
7
+ - Add bounded `inspectTarArchive` with complete native/WASM admission and the same canonical extraction planner, preserving effective member identities without materializing an output tree.
8
+ - Preserve inherited and getter-backed extraction options on the WASM TAR path, matching native and ZIP handling for destinations, filters, stripping, and modes.
9
+ - Apply the existing filesystem-test worker cap locally as well as in CI, and drain archive publication fixtures before resetting hooks or cleaning restricted directories after timeouts.
10
+ - Add `durable` to Root write/create/writeJson/createJson/append options and Root defaults; `durable: false` skips file and parent fsync for reconstructible data (default unchanged: durable).
11
+ - Run metadata checks inside async operations synchronously (microseconds each), cutting event-loop round-trips per read/write while data I/O remains asynchronous and native canonical path spelling is preserved, including Windows short paths.
12
+
3
13
  ## 0.8.3 - 2026-09-06
4
14
 
5
15
  - Add optional synchronous move authority checks before renames and each copied-source removal, plus an exact bigint destination-publication receipt for caller-owned recovery after later failure; retain cross-device and Windows `EPERM` copy fallbacks and the existing `Promise<void>` contract.
@@ -1 +1 @@
1
- {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA2BvC"}
1
+ {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA2BvC"}
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
@@ -66,7 +67,7 @@ async function directoryGuardFailure(err, dir, scopeLabel) {
66
67
  return { ok: false, code: err.code, error: err };
67
68
  }
68
69
  try {
69
- const stat = await fs.lstat(dir);
70
+ const stat = fsSync.lstatSync(dir);
70
71
  const failure = classifyExistingDirectorySegment(stat, scopeLabel);
71
72
  if (failure) {
72
73
  return failure;
@@ -86,7 +87,7 @@ async function resolveTrustedDirectoryPrefix(targetPath, scopeLabel) {
86
87
  let current = root;
87
88
  let currentStat;
88
89
  try {
89
- currentStat = await fs.lstat(current);
90
+ currentStat = fsSync.lstatSync(current);
90
91
  }
91
92
  catch (err) {
92
93
  const failure = classifyDirectoryLookupError(err, scopeLabel);
@@ -110,7 +111,7 @@ async function resolveTrustedDirectoryPrefix(targetPath, scopeLabel) {
110
111
  }
111
112
  const next = path.join(current, segment);
112
113
  try {
113
- const nextStat = await fs.lstat(next);
114
+ const nextStat = fsSync.lstatSync(next);
114
115
  const segmentFailure = classifyExistingDirectorySegment(nextStat, scopeLabel);
115
116
  if (segmentFailure) {
116
117
  return segmentFailure;
@@ -155,7 +156,7 @@ async function findExistingAncestorWithStat(filePath) {
155
156
  let current = path.resolve(filePath);
156
157
  while (true) {
157
158
  try {
158
- return { path: current, stat: await fs.lstat(current) };
159
+ return { path: current, stat: fsSync.lstatSync(current) };
159
160
  }
160
161
  catch (err) {
161
162
  if (err.code !== "ENOENT") {
@@ -199,7 +200,7 @@ export async function ensureAbsoluteDirectory(dirPath, options = {}) {
199
200
  return guardResult;
200
201
  }
201
202
  try {
202
- const stat = await fs.lstat(current);
203
+ const stat = fsSync.lstatSync(current);
203
204
  if (stat.isSymbolicLink()) {
204
205
  return ensureDirectoryFailure("symlink", `directory path traverses a symlink within ${scopeLabel}`);
205
206
  }
@@ -250,7 +251,7 @@ export async function canonicalPathFromExistingAncestor(filePath) {
250
251
  }
251
252
  let canonicalAncestor = ancestor;
252
253
  try {
253
- canonicalAncestor = await fs.realpath(ancestor);
254
+ canonicalAncestor = fsSync.realpathSync.native(ancestor);
254
255
  }
255
256
  catch {
256
257
  // Keep lexical path when the existing ancestor cannot be canonicalized.
@@ -263,7 +264,7 @@ export async function resolveAbsolutePathForRead(filePath, options = {}) {
263
264
  const normalized = assertAbsolutePathInput(filePath);
264
265
  let canonicalPath;
265
266
  try {
266
- canonicalPath = await fs.realpath(normalized);
267
+ canonicalPath = fsSync.realpathSync.native(normalized);
267
268
  }
268
269
  catch (err) {
269
270
  if (err.code === "ENOENT") {
@@ -3,6 +3,7 @@ import type { ArchiveKind } from "./archive-kind.js";
3
3
  import { type ResolvedArchiveExtractLimits, type TarMeterLimits } from "./archive-limits.js";
4
4
  import type { ExtractArchiveOptions } from "./archive-options.js";
5
5
  import type { NativeBinding } from "./native.js";
6
+ export declare function throwMappedNativeArchiveError(error: unknown): never;
6
7
  export declare function extractNativeArchive(params: {
7
8
  binding: NativeBinding;
8
9
  archivePath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAML,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAQlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuBjD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CAyJhB"}
1
+ {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAYnE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
@@ -2,22 +2,14 @@ import { classifyArchiveParserError } from "./archive-parser-errors.js";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
5
- import { formatErrorDetail } from "./error-detail.js";
6
- import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
5
+ import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
6
+ import { createArchiveEntryPlanner } from "./archive-plan.js";
7
7
  import { stageArchiveFileForExtraction } from "./archive-input.js";
8
- import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, } from "./archive-limits.js";
9
- import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
8
+ import { assertArchiveEntryCountWithinLimit, } from "./archive-limits.js";
10
9
  import { prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
11
10
  import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
12
11
  import { admitZipFile } from "./archive-zip-admission.js";
13
- function policyKind(kind) {
14
- if (kind === "file" || kind === "directory")
15
- return kind;
16
- if (kind === "symlink" || kind === "hardlink")
17
- return "symlink";
18
- return "other";
19
- }
20
- function throwMappedNativeError(error) {
12
+ export function throwMappedNativeArchiveError(error) {
21
13
  if (error instanceof Error) {
22
14
  const mapped = classifyArchiveParserError(error.message, { cause: error });
23
15
  if (mapped)
@@ -50,7 +42,7 @@ export async function extractNativeArchive(params) {
50
42
  params.deadline.check();
51
43
  const manifest = await params.binding
52
44
  .inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, params.deadline.signal)
53
- .catch(throwMappedNativeError);
45
+ .catch(throwMappedNativeArchiveError);
54
46
  params.deadline.check();
55
47
  assertArchiveEntryCountWithinLimit(manifest.length, limits);
56
48
  if (physicalCount !== undefined && manifest.length !== physicalCount) {
@@ -75,69 +67,24 @@ export async function extractNativeArchive(params) {
75
67
  for (const entry of manifest)
76
68
  validateArchiveEntryPath(entry.path);
77
69
  }
78
- const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
79
- const budget = createByteBudgetTracker(limits);
80
- const trackOutputPath = createArchiveOutputPathTracker();
70
+ const planEntry = createArchiveEntryPlanner({ ...params, rootDir: stagingDir }, params.kind);
81
71
  const plan = [];
82
72
  for (const entry of manifest) {
83
73
  params.deadline.check();
84
- validateArchiveEntryPath(entry.path);
85
- const canonicalPath = stripArchivePath(entry.path, 0);
86
- if (!canonicalPath)
87
- continue;
88
- const relPath = stripArchivePath(canonicalPath, strip);
89
- if (!relPath)
90
- continue;
91
- validateArchiveEntryPath(relPath);
92
- assertArchiveEntryPathComponentsWithinLimit(relPath, limits);
93
- trackOutputPath(relPath, entry.path);
94
- resolveArchiveOutputPath({ rootDir: stagingDir, relPath, originalPath: entry.path });
95
- const kind = policyKind(entry.kind);
96
- if (!shouldExtractArchiveEntry({
97
- filter: params.entryFilter,
98
- onFiltered: params.onFiltered,
99
- entry: { path: canonicalPath, kind, size: entry.size },
100
- })) {
101
- continue;
102
- }
103
- if (entry.kind === "sparse") {
104
- throw new ArchiveFormatError(`GNU sparse archive entry is not supported: ${formatErrorDetail(entry.path)}`);
105
- }
106
- if (kind === "symlink" || entry.kind === "blocked") {
107
- const label = params.kind === "zip" ? "zip" : "tar";
108
- throw new ArchiveSecurityError("entry-link", `${label} entry is a link: ${formatErrorDetail(entry.path)}`);
109
- }
110
- if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
111
- throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
112
- }
113
- // GNUDumpDir can carry a body; charge accepted TAR sizes as JS does.
114
- if (kind === "file" || (kind === "directory" && params.kind !== "zip")) {
115
- budget.startEntry();
116
- budget.addEntrySize(entry.size);
117
- }
118
- if (kind === "file" || kind === "directory") {
119
- plan.push({
120
- index: entry.index,
121
- path: relPath,
122
- kind,
123
- size: entry.size,
124
- mode: resolveArchiveEntryMode({
125
- kind,
126
- archivedMode: params.kind === "zip"
127
- ? zipEntries[entry.index].creatorSystem === 3
128
- ? zipEntries[entry.index].externalAttributes >>> 16
129
- : undefined
130
- : entry.mode,
131
- policy: params.entryModes,
132
- }),
133
- });
134
- }
74
+ const mode = params.kind === "zip"
75
+ ? zipEntries[entry.index].creatorSystem === 3
76
+ ? zipEntries[entry.index].externalAttributes >>> 16
77
+ : undefined
78
+ : entry.mode;
79
+ const accepted = planEntry({ ...entry, mode });
80
+ if (accepted)
81
+ plan.push({ ...accepted, index: entry.index });
135
82
  }
136
83
  const directory = await fs.open(stagingDir, fsConstants.O_RDONLY |
137
84
  (typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
138
85
  try {
139
86
  params.deadline.check();
140
- await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, params.deadline.signal).catch(throwMappedNativeError);
87
+ await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, params.deadline.signal).catch(throwMappedNativeArchiveError);
141
88
  }
142
89
  finally {
143
90
  await directory.close().catch(() => undefined);
@@ -0,0 +1,20 @@
1
+ import type { ArchiveKind } from "./archive-kind.js";
2
+ import type { ExtractArchiveOptions } from "./archive-options.js";
3
+ export type ArchiveMemberKind = "file" | "directory" | "symlink" | "hardlink" | "blocked" | "sparse" | "other";
4
+ export type ArchivePlanEntry = {
5
+ path: string;
6
+ kind: "file" | "directory";
7
+ size: number;
8
+ mode: number;
9
+ };
10
+ export type ArchivePlanOptions = Pick<ExtractArchiveOptions, "stripComponents" | "limits" | "entryModes" | "entryFilter" | "onFiltered"> & {
11
+ rootDir?: string;
12
+ escapeLabel?: string;
13
+ };
14
+ export declare function createArchiveEntryPlanner(params: ArchivePlanOptions, archiveKind: ArchiveKind): (entry: {
15
+ path: string;
16
+ kind: ArchiveMemberKind;
17
+ size: number;
18
+ mode?: number;
19
+ }) => ArchivePlanEntry | null;
20
+ //# sourceMappingURL=archive-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-plan.d.ts","sourceRoot":"","sources":["../src/archive-plan.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AASrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAIlE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC/G,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACxG,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,qBAAqB,EACzD,iBAAiB,GAAG,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC,GAAG;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAIF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,GAAG,CAAC,KAAK,EAAE;IACvG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACpE,KAAK,gBAAgB,GAAG,IAAI,CA2C5B"}
@@ -0,0 +1,55 @@
1
+ import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
+ import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
3
+ import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
4
+ import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
5
+ import { formatErrorDetail } from "./error-detail.js";
6
+ // Inspection and both executors decide over the same admitted identities.
7
+ // A destination is optional only for inspection; writers still prove containment.
8
+ export function createArchiveEntryPlanner(params, archiveKind) {
9
+ const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
10
+ const limits = resolveExtractLimits(params.limits);
11
+ const budget = createByteBudgetTracker(limits);
12
+ const trackOutputPath = createArchiveOutputPathTracker();
13
+ let entryCount = 0;
14
+ return (entry) => {
15
+ assertArchiveEntryCountWithinLimit(++entryCount, limits);
16
+ validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
17
+ const canonicalPath = stripArchivePath(entry.path, 0);
18
+ if (!canonicalPath)
19
+ return null;
20
+ const relPath = stripArchivePath(canonicalPath, strip);
21
+ if (!relPath)
22
+ return null;
23
+ validateArchiveEntryPath(relPath, { escapeLabel: params.escapeLabel });
24
+ assertArchiveEntryPathComponentsWithinLimit(relPath, limits);
25
+ trackOutputPath(relPath, entry.path);
26
+ if (params.rootDir !== undefined) {
27
+ resolveArchiveOutputPath({ rootDir: params.rootDir, relPath, originalPath: entry.path,
28
+ escapeLabel: params.escapeLabel });
29
+ }
30
+ const kind = entry.kind === "file" || entry.kind === "directory" ? entry.kind
31
+ : entry.kind === "symlink" || entry.kind === "hardlink" ? "symlink" : "other";
32
+ if (!shouldExtractArchiveEntry({ filter: params.entryFilter, onFiltered: params.onFiltered,
33
+ entry: { path: canonicalPath, kind, size: entry.size } }))
34
+ return null;
35
+ if (entry.kind === "sparse") {
36
+ throw new ArchiveFormatError(`GNU sparse archive entry is not supported: ${formatErrorDetail(entry.path)}`);
37
+ }
38
+ if (kind === "symlink" || entry.kind === "blocked") {
39
+ const label = archiveKind === "zip" ? "zip" : "tar";
40
+ throw new ArchiveSecurityError("entry-link", `${label} entry is a link: ${formatErrorDetail(entry.path)}`);
41
+ }
42
+ if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
43
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
44
+ }
45
+ // Unsupported records remain visible to policy but cannot create outputs.
46
+ if (kind === "other")
47
+ return null;
48
+ if (kind === "file" || archiveKind !== "zip") {
49
+ budget.startEntry();
50
+ budget.addEntrySize(entry.size);
51
+ }
52
+ return { path: relPath, kind, size: entry.size,
53
+ mode: resolveArchiveEntryMode({ kind, archivedMode: entry.mode, policy: params.entryModes }) };
54
+ };
55
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExtractArchiveOptions } from "./archive-options.js";
2
+ import { type ArchivePlanEntry } from "./archive-plan.js";
3
+ export type InspectTarArchiveOptions = Pick<ExtractArchiveOptions, "archivePath" | "timeoutMs" | "limits" | "entryFilter" | "onFiltered">;
4
+ export type InspectedTarEntry = Readonly<Pick<ArchivePlanEntry, "path" | "kind" | "size">>;
5
+ /** Complete TAR/gzip admission and zero-strip extraction policy, without output writes. */
6
+ export declare function inspectTarArchive(params: InspectTarArchiveOptions): Promise<readonly InspectedTarEntry[]>;
7
+ //# sourceMappingURL=archive-tar-inspect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-tar-inspect.d.ts","sourceRoot":"","sources":["../src/archive-tar-inspect.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAOrF,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,qBAAqB,EAC/D,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC;AACzE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAE3F,2FAA2F;AAC3F,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CA2C/G"}
@@ -0,0 +1,59 @@
1
+ import { withExtractionDeadline } from "./archive-deadline.js";
2
+ import { validateArchiveEntryPath } from "./archive-entry.js";
3
+ import { stageArchiveFileForExtraction } from "./archive-input.js";
4
+ import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
5
+ import { throwMappedNativeArchiveError } from "./archive-native.js";
6
+ import { createArchiveEntryPlanner } from "./archive-plan.js";
7
+ import { resolveArchiveFilteredEntryPolicy } from "./archive-policy.js";
8
+ import { createTarEntryPlanner } from "./archive-tar.js";
9
+ import { inspectTar } from "./archive-tar-stream.js";
10
+ import { getNativeBinding } from "./native.js";
11
+ /** Complete TAR/gzip admission and zero-strip extraction policy, without output writes. */
12
+ export async function inspectTarArchive(params) {
13
+ const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
14
+ const limits = resolveExtractLimits(params.limits);
15
+ const tarLimits = resolveTarMeterLimits(limits);
16
+ const native = getNativeBinding();
17
+ return await withExtractionDeadline(params.timeoutMs, "inspect tar", async (deadline) => {
18
+ const staged = await stageArchiveFileForExtraction({ archivePath: params.archivePath, limits, deadline });
19
+ try {
20
+ // Staging closes descriptors asynchronously; expiry there must not start a decoder.
21
+ deadline.check();
22
+ const entries = [];
23
+ const append = (entry) => {
24
+ if (entry)
25
+ entries.push(Object.freeze({ path: entry.path, kind: entry.kind, size: entry.size }));
26
+ };
27
+ const policy = { limits, entryFilter: params.entryFilter, onFiltered };
28
+ if (native) {
29
+ const manifest = await native.inspectArchiveNative(staged.path, "tar", tarLimits, deadline.signal)
30
+ .catch(throwMappedNativeArchiveError);
31
+ deadline.check();
32
+ // Match extraction's whole-manifest validation before caller policy runs.
33
+ for (const entry of manifest)
34
+ validateArchiveEntryPath(entry.path);
35
+ const planEntry = createArchiveEntryPlanner(policy, "tar");
36
+ for (const entry of manifest) {
37
+ deadline.check();
38
+ append(planEntry(entry));
39
+ }
40
+ }
41
+ else {
42
+ const manifest = [];
43
+ await inspectTar({ archivePath: staged.path, limits: tarLimits, signal: deadline.signal,
44
+ onMember: (entry) => { manifest.push(entry); } });
45
+ const planEntry = createTarEntryPlanner(policy);
46
+ for (const entry of manifest) {
47
+ deadline.check();
48
+ append(planEntry(entry));
49
+ }
50
+ }
51
+ deadline.check();
52
+ // This is bounded evidence about the staged bytes, not a reusable write plan.
53
+ return Object.freeze(entries);
54
+ }
55
+ finally {
56
+ await staged.cleanup();
57
+ }
58
+ });
59
+ }
@@ -1,4 +1,5 @@
1
- import { type ArchiveExtractLimits } from "./archive-limits.js";
1
+ import type { ArchiveExtractLimits } from "./archive-limits.js";
2
+ import { type ArchivePlanEntry, type ArchivePlanOptions } from "./archive-plan.js";
2
3
  import { type ArchiveEntryFilter, type ArchiveFilteredEntryPolicy } from "./archive-policy.js";
3
4
  export type TarEntryInfo = {
4
5
  path: string;
@@ -6,6 +7,7 @@ export type TarEntryInfo = {
6
7
  size: number;
7
8
  mode?: number;
8
9
  };
10
+ export declare function createTarEntryPlanner(params: ArchivePlanOptions): (entry: TarEntryInfo) => ArchivePlanEntry | null;
9
11
  export declare function createTarEntryPreflightChecker(params: {
10
12
  rootDir: string;
11
13
  stripComponents?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAqDnC"}
1
+ {"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AASvF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAC9D,CAAC,KAAK,EAAE,YAAY,KAAK,gBAAgB,GAAG,IAAI,CAQjD;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAGnC"}
@@ -1,58 +1,21 @@
1
- import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
2
- import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
3
- import { archiveEntryKindFromTarType, shouldExtractArchiveEntry, } from "./archive-policy.js";
4
- import { ArchiveSecurityError } from "./archive-errors.js";
5
- import { formatErrorDetail } from "./error-detail.js";
1
+ import { createArchiveEntryPlanner, } from "./archive-plan.js";
2
+ import { archiveEntryKindFromTarType, } from "./archive-policy.js";
6
3
  const BLOCKED_TAR_ENTRY_TYPES = new Set([
7
- "SymbolicLink",
8
- "Link",
9
4
  "BlockDevice",
10
5
  "CharacterDevice",
11
6
  "FIFO",
12
7
  "Socket",
13
8
  ]);
14
- export function createTarEntryPreflightChecker(params) {
15
- const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
16
- const limits = resolveExtractLimits(params.limits);
17
- let entryCount = 0;
18
- const budget = createByteBudgetTracker(limits);
19
- const trackOutputPath = createArchiveOutputPathTracker();
9
+ export function createTarEntryPlanner(params) {
10
+ // Preserve inherited/getter-backed public options and per-entry policy reads.
11
+ const plan = createArchiveEntryPlanner(params, "tar");
20
12
  return (entry) => {
21
- entryCount += 1;
22
- assertArchiveEntryCountWithinLimit(entryCount, limits);
23
- validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
24
- const canonicalPath = stripArchivePath(entry.path, 0);
25
- if (!canonicalPath)
26
- return false;
27
- const relPath = stripArchivePath(canonicalPath, strip);
28
- if (!relPath) {
29
- return false;
30
- }
31
- validateArchiveEntryPath(relPath, { escapeLabel: params.escapeLabel });
32
- assertArchiveEntryPathComponentsWithinLimit(relPath, limits);
33
- trackOutputPath(relPath, entry.path);
34
- resolveArchiveOutputPath({
35
- rootDir: params.rootDir,
36
- relPath,
37
- originalPath: entry.path,
38
- escapeLabel: params.escapeLabel,
39
- });
40
- const kind = archiveEntryKindFromTarType(entry.type);
41
- if (!shouldExtractArchiveEntry({
42
- filter: params.entryFilter,
43
- onFiltered: params.onFiltered,
44
- entry: { path: canonicalPath, kind, size: entry.size },
45
- })) {
46
- return false;
47
- }
48
- if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) {
49
- throw new ArchiveSecurityError("entry-link", `tar entry is a link: ${formatErrorDetail(entry.path)}`);
50
- }
51
- // Accepted unsupported records remain omitted, as in the native plan.
52
- if (kind === "other")
53
- return false;
54
- budget.startEntry();
55
- budget.addEntrySize(entry.size);
56
- return true;
13
+ const kind = BLOCKED_TAR_ENTRY_TYPES.has(entry.type) ? "blocked" : archiveEntryKindFromTarType(entry.type);
14
+ // The public checker accepts structural entry objects, including class getters.
15
+ return plan({ path: entry.path, kind, size: entry.size, mode: entry.mode });
57
16
  };
58
17
  }
18
+ export function createTarEntryPreflightChecker(params) {
19
+ const plan = createTarEntryPlanner(params);
20
+ return (entry) => plan(entry) !== null;
21
+ }
package/dist/archive.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { ArchiveEntryFilter, ArchiveEntryKind, ArchiveEntryModePolicy, Arch
4
4
  export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
5
5
  export { resolveArchiveKind, resolvePackedRootDir, type ArchiveKind } from "./archive-kind.js";
6
6
  export { readArchiveEntry } from "./archive-read.js";
7
+ export { inspectTarArchive, type InspectTarArchiveOptions, type InspectedTarEntry } from "./archive-tar-inspect.js";
7
8
  export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, type ArchiveExtractLimits, type ArchiveLimitErrorCode, } from "./archive-limits.js";
8
9
  export { ArchiveFormatError, type ArchiveFormatErrorCode } from "./archive-errors.js";
9
10
  export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-staging.js";
@@ -1 +1 @@
1
- {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA6DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AA6MpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0DjF"}
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACpH,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AA6MpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CjF"}
package/dist/archive.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createTarEntryPreflightChecker } from "./archive-tar.js";
1
+ import { createTarEntryPlanner } from "./archive-tar.js";
2
2
  import { inspectTar, replayTar } from "./archive-tar-stream.js";
3
3
  import { runPinnedWriteHelper } from "./pinned-write.js";
4
- import fsSync, { constants as fsConstants } from "node:fs";
4
+ import { constants as fsConstants } from "node:fs";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { Readable } from "node:stream";
@@ -20,11 +20,12 @@ import { ArchiveSecurityError } from "./archive-errors.js";
20
20
  import { extractNativeArchive } from "./archive-native.js";
21
21
  import { stageArchiveFileForExtraction } from "./archive-input.js";
22
22
  import { getNativeBinding } from "./native.js";
23
- import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
23
+ import { resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
24
24
  import { writeSiblingTempFile } from "./sibling-temp.js";
25
25
  export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
26
26
  export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
27
27
  export { readArchiveEntry } from "./archive-read.js";
28
+ export { inspectTarArchive } from "./archive-tar-inspect.js";
28
29
  export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, } from "./archive-limits.js";
29
30
  export { ArchiveFormatError } from "./archive-errors.js";
30
31
  export { ArchiveSecurityError } from "./archive-staging.js";
@@ -207,32 +208,27 @@ export async function extractArchive(params) {
207
208
  const limits = resolveExtractLimits(params.limits);
208
209
  const tarLimits = resolveTarMeterLimits(limits);
209
210
  const native = getNativeBinding();
211
+ // Read the declared public fields before private adapters copy options: class
212
+ // getters and inherited policy must survive identically on every backend.
213
+ const options = {
214
+ archivePath: params.archivePath, destDir: params.destDir,
215
+ stripComponents: params.stripComponents, limits,
216
+ entryModes: params.entryModes, entryFilter: params.entryFilter, onFiltered,
217
+ };
210
218
  if (native) {
211
- await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
212
- binding: native,
213
- archivePath: params.archivePath,
214
- destDir: params.destDir,
215
- kind,
216
- stripComponents: params.stripComponents,
217
- limits,
218
- tarLimits,
219
- deadline,
220
- entryModes: params.entryModes,
221
- entryFilter: params.entryFilter,
222
- onFiltered,
223
- }));
219
+ await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({ ...options, binding: native, kind, tarLimits, deadline }));
224
220
  return;
225
221
  }
226
222
  assertPortableArchiveKind(kind);
227
223
  if (kind === "tar") {
228
224
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
229
225
  const stagedArchive = await stageArchiveFileForExtraction({
230
- archivePath: params.archivePath,
226
+ archivePath: options.archivePath,
231
227
  limits,
232
228
  deadline,
233
229
  });
234
230
  try {
235
- await extractWasmTar({ archivePath: stagedArchive.path, options: { ...params, onFiltered }, limits, tarLimits, deadline });
231
+ await extractWasmTar({ archivePath: stagedArchive.path, options, limits, tarLimits, deadline });
236
232
  }
237
233
  finally {
238
234
  await stagedArchive.cleanup();
@@ -240,16 +236,7 @@ export async function extractArchive(params) {
240
236
  });
241
237
  return;
242
238
  }
243
- await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractZip({
244
- archivePath: params.archivePath,
245
- destDir: params.destDir,
246
- stripComponents: params.stripComponents,
247
- limits,
248
- deadline,
249
- entryModes: params.entryModes,
250
- entryFilter: params.entryFilter,
251
- onFiltered,
252
- }));
239
+ await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractZip({ ...options, deadline }));
253
240
  }
254
241
  async function extractWasmTar(params) {
255
242
  const { options, deadline, tarLimits } = params;
@@ -260,14 +247,11 @@ async function extractWasmTar(params) {
260
247
  const destinationRealDir = await prepareArchiveDestinationDir(options.destDir);
261
248
  await withStagedArchiveDestination({ destinationRealDir, run: async (stagingPath) => {
262
249
  const stagingDir = await fs.realpath(stagingPath);
263
- const check = createTarEntryPreflightChecker({ rootDir: destinationRealDir,
264
- stripComponents: options.stripComponents, limits: params.limits,
265
- entryFilter: options.entryFilter, onFiltered: options.onFiltered });
266
- const strip = Math.max(0, Math.floor(options.stripComponents ?? 0));
267
- const accepted = manifest.filter((entry) => { deadline.check(); return check(entry); }).map((entry) => {
268
- const kind = entry.type === "Directory" || entry.type === "GNUDumpDir" ? "directory" : "file";
269
- return { ...entry, path: stripArchivePath(entry.path, strip), kind,
270
- mode: resolveArchiveEntryMode({ kind, archivedMode: entry.mode, policy: options.entryModes }) };
250
+ const planEntry = createTarEntryPlanner({ ...options, rootDir: destinationRealDir, limits: params.limits });
251
+ const accepted = manifest.flatMap((entry) => {
252
+ deadline.check();
253
+ const planned = planEntry(entry);
254
+ return planned ? [{ ...entry, ...planned }] : [];
271
255
  });
272
256
  await replayTar({ archivePath: params.archivePath, limits: tarLimits, signal: deadline.signal, members: accepted,
273
257
  async consume(member, payload) {
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAmDnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
1
+ {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AA+DnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAMjB;AAyBD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}