@openclaw/fs-safe 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +4 -2
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +4 -3
  8. package/dist/archive-gzip-tail.d.ts.map +1 -1
  9. package/dist/archive-gzip-tail.js +13 -9
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +37 -33
  12. package/dist/archive-zip-names.d.ts.map +1 -1
  13. package/dist/archive-zip-names.js +17 -5
  14. package/dist/bounded-read.js +2 -2
  15. package/dist/device-path.d.ts.map +1 -1
  16. package/dist/device-path.js +5 -3
  17. package/dist/file-hash.d.ts.map +1 -1
  18. package/dist/file-hash.js +9 -2
  19. package/dist/file-store-prune.d.ts.map +1 -1
  20. package/dist/file-store-prune.js +9 -1
  21. package/dist/filename.d.ts +1 -0
  22. package/dist/filename.d.ts.map +1 -1
  23. package/dist/filename.js +30 -13
  24. package/dist/guarded-mkdir.d.ts +2 -0
  25. package/dist/guarded-mkdir.d.ts.map +1 -1
  26. package/dist/guarded-mkdir.js +50 -15
  27. package/dist/guest.d.ts.map +1 -1
  28. package/dist/guest.js +12 -4
  29. package/dist/install-path.d.ts +6 -0
  30. package/dist/install-path.d.ts.map +1 -1
  31. package/dist/install-path.js +13 -0
  32. package/dist/json-durable-queue-ownership.d.ts +2 -0
  33. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  34. package/dist/json-durable-queue-ownership.js +47 -3
  35. package/dist/json-durable-queue-read.d.ts +6 -0
  36. package/dist/json-durable-queue-read.d.ts.map +1 -0
  37. package/dist/json-durable-queue-read.js +59 -0
  38. package/dist/json-durable-queue.d.ts +1 -1
  39. package/dist/json-durable-queue.d.ts.map +1 -1
  40. package/dist/json-durable-queue.js +37 -80
  41. package/dist/native-binding.d.ts +7 -0
  42. package/dist/native-binding.d.ts.map +1 -1
  43. package/dist/path-scope-lexical.d.ts +14 -0
  44. package/dist/path-scope-lexical.d.ts.map +1 -0
  45. package/dist/path-scope-lexical.js +27 -0
  46. package/dist/path.d.ts.map +1 -1
  47. package/dist/path.js +20 -2
  48. package/dist/pinned-write.js +1 -0
  49. package/dist/root-boundary.d.ts +25 -0
  50. package/dist/root-boundary.d.ts.map +1 -0
  51. package/dist/root-boundary.js +177 -0
  52. package/dist/root-context.d.ts.map +1 -1
  53. package/dist/root-context.js +32 -5
  54. package/dist/root-directory-list.d.ts.map +1 -1
  55. package/dist/root-directory-list.js +10 -2
  56. package/dist/root-impl.d.ts.map +1 -1
  57. package/dist/root-impl.js +53 -34
  58. package/dist/root-move-preflight.d.ts +8 -0
  59. package/dist/root-move-preflight.d.ts.map +1 -0
  60. package/dist/root-move-preflight.js +16 -0
  61. package/dist/root-path-existing.d.ts +2 -0
  62. package/dist/root-path-existing.d.ts.map +1 -1
  63. package/dist/root-path-existing.js +10 -2
  64. package/dist/root-path.d.ts +2 -0
  65. package/dist/root-path.d.ts.map +1 -1
  66. package/dist/root-path.js +54 -26
  67. package/dist/root-paths.d.ts +2 -6
  68. package/dist/root-paths.d.ts.map +1 -1
  69. package/dist/root-paths.js +14 -26
  70. package/dist/root-walk.d.ts.map +1 -1
  71. package/dist/root-walk.js +2 -1
  72. package/dist/root-write-mode.d.ts +2 -0
  73. package/dist/root-write-mode.d.ts.map +1 -1
  74. package/dist/root-write-mode.js +20 -7
  75. package/dist/root-write-verification.d.ts.map +1 -1
  76. package/dist/root-write-verification.js +12 -3
  77. package/dist/safe-path-segment.d.ts.map +1 -1
  78. package/dist/safe-path-segment.js +3 -1
  79. package/dist/secure-file-windows.d.ts +10 -0
  80. package/dist/secure-file-windows.d.ts.map +1 -0
  81. package/dist/secure-file-windows.js +186 -0
  82. package/dist/secure-file.d.ts.map +1 -1
  83. package/dist/secure-file.js +6 -3
  84. package/dist/sibling-staged-file.d.ts +1 -0
  85. package/dist/sibling-staged-file.d.ts.map +1 -1
  86. package/dist/sibling-staged-file.js +7 -0
  87. package/dist/sibling-temp.d.ts.map +1 -1
  88. package/dist/sibling-temp.js +2 -1
  89. package/dist/sidecar-lock-acquire.js +1 -1
  90. package/dist/sidecar-lock.d.ts.map +1 -1
  91. package/dist/sidecar-lock.js +2 -3
  92. package/dist/temp-target.d.ts.map +1 -1
  93. package/dist/temp-target.js +3 -1
  94. package/dist/walk.d.ts.map +1 -1
  95. package/dist/walk.js +11 -10
  96. package/docs/advanced.md +3 -1
  97. package/docs/archive.md +6 -0
  98. package/docs/contributing.md +4 -1
  99. package/docs/copy.md +1 -1
  100. package/docs/durability.md +4 -2
  101. package/docs/file-store.md +6 -0
  102. package/docs/filename.md +9 -2
  103. package/docs/guest.md +5 -0
  104. package/docs/install-path.md +59 -13
  105. package/docs/install.md +4 -1
  106. package/docs/native-helper.md +4 -2
  107. package/docs/native.md +14 -2
  108. package/docs/path.md +1 -1
  109. package/docs/permissions.md +26 -1
  110. package/docs/public-api.md +5 -0
  111. package/docs/root.md +3 -1
  112. package/docs/secure-file.md +15 -14
  113. package/docs/security-model.md +1 -1
  114. package/docs/store.md +22 -1
  115. package/docs/temp.md +4 -0
  116. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.12.0 - 2026-09-15
6
+
7
+ ### Highlights
8
+
9
+ - **Faster archive workloads:** reuse validated ZIP names, batch native plain-TAR reads, and reduce gzip validation and payload-buffer allocations while preserving complete archive checks.
10
+ - **Less overhead in everyday filesystem work:** reduce repeated path resolution, directory-walk work, small-hash allocations, Linux copy buffers, and lock-manager setup.
11
+ - **Stronger Windows file boundaries:** verify secure-file permissions against the descriptor supplying the bytes, retain handles through private-directory creation and cleanup, and check directory identity when Root containment relies on case folding.
12
+ - **Safer queue recovery and cleanup:** prevent stale migrations from replacing newer queue entries, preserve files refreshed during expiry pruning, and keep existing destinations intact when guest symlink moves fail.
13
+ - **Versioned install identifiers:** add `safePathSegmentHashedV2`, which hashes every trimmed ID into a fixed lowercase directory segment and avoids the deterministic aliases of the legacy readable encoder.
14
+
15
+ ### Compatibility and upgrade notes
16
+
17
+ - Windows `readSecureFile()` now requires the matching current native package for descriptor-bound owner and DACL checks. Missing or stale helpers, remote handles, denied access, and unsupported or incomplete permission facts fail closed before reading; there is no pathname ACL-command fallback. Standalone permission-reporting APIs retain their documented fallbacks.
18
+ - Windows private-directory creation rejects explicit `.` and `..` components and components ending in spaces or periods before filesystem operations. Directory association requires the complete file identity; unsupported identity classes fail closed.
19
+ - Windows native descriptor operations require the host runtime's paired libuv descriptor bridge. Missing or partial bridges reject with `ENOTSUP` instead of interpreting descriptors as raw handles or using another runtime's descriptor table.
20
+ - `safePathSegmentHashedV2` is opt-in. Legacy encoder outputs and the default encoder remain unchanged. Switching to V2 changes existing paths: use a new base directory or explicitly migrate after verifying recorded IDs, and do not silently fall back to legacy paths.
21
+
22
+ ### Archive performance and reliability
23
+
24
+ - Reuse identical local and central ZIP name validation within one entry, including matching complete Unicode Path metadata. Differing fields and shared backing memory retain independent checks; CRC, traversal, collision, and interpretation validation remain intact.
25
+ - Reuse raw archive path segments during validation, preserving platform rules, normalized component limits, and error ordering.
26
+ - Speed up native plain-TAR inspection and extraction with bounded file read-ahead, retaining complete framing, payload, trailer, and cancellation checks at parser boundaries.
27
+ - Fill one owned result buffer when reading admitted gzip TAR payloads in JavaScript, reducing retained decoded buffers while preserving complete validation and independent returned bytes.
28
+ - Validate gzip container padding with bounded buffer comparisons and one lazily allocated reusable zero window, preserving complete suffix checks, short-read handling, and cancellation cadence.
29
+ - Destroy and join ZIP decoder sources before rejecting bounded member reads, preventing abandoned decoders and retained archive buffers after byte-limit failures while preserving error classifications on Node 22 and newer.
30
+
31
+ ### Paths, reads, and copying
32
+
33
+ - Reuse normalized absolute POSIX roots within bulk lexical path resolution and avoid per-segment drive-letter validation allocations, preserving path spellings, validation order, and errors.
34
+ - Reuse resolved Windows drive paths and descendant comparisons, with existing namespace, UNC, and colon-bearing component handling retained. Classify Windows device names without intermediate segment arrays.
35
+ - Reuse lexical directory prefixes while walking, preserving traversal order, followed-link spelling, budgets, callback behavior, and directory checks.
36
+ - Size small JavaScript SHA-256 scratch buffers to the file and byte budget, growing when size hints are stale while preserving complete reads, overflow detection, cancellation, and borrowed-descriptor ownership.
37
+ - Complete positive short reads from virtual files that report size zero instead of returning a truncated prefix. Byte-limit overflow detection and borrowed-descriptor cursor behavior remain intact.
38
+ - Confirm EOF when Linux `copy_file_range` reports zero after partial progress; resume guarded byte copying when readable data remains instead of publishing a truncated copy.
39
+ - Size Linux native byte-copy buffers from existing source-size hints, bounded between 4 KiB and 1 MiB, retaining read-to-EOF behavior, sparse output, and borrowed descriptors.
40
+
41
+ ### Windows security and generated names
42
+
43
+ - Bind secure-file owner and DACL verification to the open descriptor and compare its native volume serial and file-index projection with Node's bigint receipt before reading.
44
+ - Retain parent and created-directory handles through private-directory creation, protected-DACL validation, complete identity association, and failure cleanup, preserving concurrent pathname replacements.
45
+ - Require differently cased Root prefixes to match the Root directory's exact identity, then continue under its trusted spelling. Exact-case paths retain their lexical fast path.
46
+ - Prevent filename truncation and temp-file sanitization from producing Windows reserved-device aliases. Invalid completed sibling callback components reject before hooks or producers run.
47
+
48
+ ### Queues, locks, and cleanup
49
+
50
+ - Bind durable-queue migrations to their pinned processing generation, rejecting stale callbacks after acknowledgement, quarantine, or replacement. Release the verified read pin at Windows publication so migrations can replace their target.
51
+ - Resync resumed processing claims under the transfer lock so conditional migrations cannot bypass a failed publication sync on retry.
52
+ - Recheck current file type and modification time immediately before expiry pruning removes a file, preserving fresh replacements and timestamp refreshes without requiring read access.
53
+ - Stage guest cross-device symlink moves privately before atomic replacement, preserving the existing destination and source link when creation or publication fails.
54
+ - Avoid rescanning held locks during repeated lock-manager construction, and initialize legacy reference counts during acquisition and release so nested handles retain the outer lock.
55
+
56
+ ### Validation
57
+
58
+ - Expand representative method-audit coverage to 572 workloads across JavaScript/native modes on Linux, macOS, and Windows, including 1,000-entry walks, larger TAR inventories, and small synchronous hashes, with explicit platform exclusions.
59
+ - Allow focused method audits with explicit native mode and balanced A-B-B-A or B-A-A-B measurement ordering.
60
+ - Extend walker coverage for lexical aliases and callback mutation. Keep cancellation fixture setup outside the operation deadline and drain fixture writes before cleanup on slow or failing filesystems.
61
+ - Give the Windows slow physical-package-copy sidecar proof separate setup, operation, child, and teardown budgets with bounded failure diagnostics, preserving ordinary helper deadlines.
62
+
5
63
  ## 0.11.0 - 2026-09-14
6
64
 
7
65
  ### Highlights
package/README.md CHANGED
@@ -260,7 +260,7 @@ contract. Low-level helpers that OpenClaw needs to compose higher-level APIs are
260
260
  | `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
261
261
  | `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, XFS, and ZFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
262
262
  | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, native gzip/zstd/bzip2, and bounded entry reads |
263
- | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [in-place writes](docs/in-place-write.md), install paths, filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
263
+ | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [in-place writes](docs/in-place-write.md), [versioned install-ID encoding](docs/install-path.md#safepathsegmenthashedv2), filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
264
264
  | `@openclaw/fs-safe/errors` | `FsSafeError`, closed codes/categories, causes, and operation-specific details receipts |
265
265
  | `@openclaw/fs-safe/types` | shared types: `DirEntry`, `PathStat`, … |
266
266
  | `@openclaw/fs-safe/test-hooks` | hooks the test suite uses to inject races; registration requires `NODE_ENV=test` or `VITEST=true` |
@@ -460,7 +460,9 @@ Use `readSecureFile()` when the caller gives you an absolute credential path
460
460
  instead of a root-relative workspace path. It opens the file first, validates the
461
461
  same handle it will read from, checks trusted directories, owner, POSIX mode or
462
462
  Windows ACLs, hardlink count, size, and optional timeout, then reads through the
463
- pinned handle.
463
+ pinned handle. On Windows, both the bytes and the owner/DACL facts come from that
464
+ handle; secure reads require the matching current native package and do not fall
465
+ back to a pathname ACL command.
464
466
 
465
467
  ```ts
466
468
  import { readSecureFile } from "@openclaw/fs-safe/secure-file";
@@ -19,7 +19,7 @@ export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICI
19
19
  export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, type OpenRootFileParams, type OpenRootFileSyncParams, type RootFileOpenFailure, type RootFileOpenFailureReason, type RootFileOpenResult, } from "./root-file.js";
20
20
  export { ROOT_PATH_ALIAS_POLICIES, resolvePathViaExistingAncestorSync, resolveRootPath, resolveRootPathSync, type ResolvedRootPath, type RootPathAliasPolicy, } from "./root-path.js";
21
21
  export { ensureDirectoryWithinRoot, pathScope, resolveExistingPathsWithinRoot, resolvePathWithinRoot, resolvePathsWithinRoot, resolveStrictExistingPathsWithinRoot, resolveWritablePathWithinRoot, type PathScope, type PathScopeOptions, type PathScopeResolveOptions, } from "./root-paths.js";
22
- export { safeDirName, safePathSegmentHashed, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
22
+ export { safeDirName, safePathSegmentHashed, safePathSegmentHashedV2, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
23
23
  export { assertNoSymlinkParents, assertNoSymlinkParentsSync, type AssertNoSymlinkParentsOptions, } from "./symlink-parents.js";
24
24
  export { movePathToTrash, type MovePathToTrashOptions } from "./trash.js";
25
25
  export { withTimeout } from "./timing.js";
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/advanced.js CHANGED
@@ -22,7 +22,7 @@ export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICI
22
22
  export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, } from "./root-file.js";
23
23
  export { ROOT_PATH_ALIAS_POLICIES, resolvePathViaExistingAncestorSync, resolveRootPath, resolveRootPathSync, } from "./root-path.js";
24
24
  export { ensureDirectoryWithinRoot, pathScope, resolveExistingPathsWithinRoot, resolvePathWithinRoot, resolvePathsWithinRoot, resolveStrictExistingPathsWithinRoot, resolveWritablePathWithinRoot, } from "./root-paths.js";
25
- export { safeDirName, safePathSegmentHashed, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
25
+ export { safeDirName, safePathSegmentHashed, safePathSegmentHashedV2, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
26
26
  export { assertNoSymlinkParents, assertNoSymlinkParentsSync, } from "./symlink-parents.js";
27
27
  export { movePathToTrash } from "./trash.js";
28
28
  export { withTimeout } from "./timing.js";
@@ -1 +1 @@
1
- {"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAOA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CA+DN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
1
+ {"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAOA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CAgEN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
@@ -21,12 +21,13 @@ export function validateArchiveEntryPath(entryPath, params) {
21
21
  throw new ArchiveSecurityError("entry-path", `archive entry contains a NUL byte: ${formatErrorDetail(entryPath)}`);
22
22
  }
23
23
  const slashNormalized = normalizeArchiveEntryPath(entryPath);
24
+ const rawSegments = slashNormalized.split("/");
24
25
  if (process.platform === "win32" &&
25
- slashNormalized.split("/").some((segment) => segment.includes(":"))) {
26
+ rawSegments.some((segment) => segment.includes(":"))) {
26
27
  throw new ArchiveSecurityError("entry-path", `archive entry uses a Windows alternate data stream path: ${formatErrorDetail(entryPath)}`);
27
28
  }
28
29
  if (process.platform === "win32" &&
29
- slashNormalized.split("/").some((segment) => isWindowsReservedDeviceName(segment))) {
30
+ rawSegments.some((segment) => isWindowsReservedDeviceName(segment))) {
30
31
  throw new ArchiveSecurityError("entry-path", `archive entry uses a reserved device path: ${formatErrorDetail(entryPath)}`);
31
32
  }
32
33
  const normalized = path.posix.normalize(slashNormalized);
@@ -40,7 +41,7 @@ export function validateArchiveEntryPath(entryPath, params) {
40
41
  if (path.posix.isAbsolute(normalized) || normalized.startsWith("//")) {
41
42
  throw new ArchiveSecurityError("entry-path", `archive entry is absolute: ${formatErrorDetail(entryPath)}`);
42
43
  }
43
- if (slashNormalized.split("/").includes("..")) {
44
+ if (rawSegments.includes("..")) {
44
45
  throw new ArchiveSecurityError("entry-path", `archive entry contains a parent segment: ${formatErrorDetail(entryPath)}`);
45
46
  }
46
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"archive-gzip-tail.d.ts","sourceRoot":"","sources":["../src/archive-gzip-tail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEvD;AAQD,gFAAgF;AAChF,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjH;AAED;8EAC8E;AAC9E,qBAAa,SAAU,SAAQ,QAAQ;IAIzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAqB;IAExC,YAA6B,OAAO,EAAE,MAAM,EAE3C;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAcxG;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAE9D;IAEQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAGpF;CACF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvH"}
1
+ {"version":3,"file":"archive-gzip-tail.d.ts","sourceRoot":"","sources":["../src/archive-gzip-tail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAaxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEvD;AAQD,gFAAgF;AAChF,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CASjH;AAED;8EAC8E;AAC9E,qBAAa,SAAU,SAAQ,QAAQ;IAIzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAqB;IAExC,YAA6B,OAAO,EAAE,MAAM,EAE3C;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAcxG;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAE9D;IAEQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAGpF;CACF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBvH"}
@@ -2,6 +2,14 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { Writable } from "node:stream";
4
4
  import { ArchiveFormatError } from "./archive-errors.js";
5
+ const PADDING_WINDOW_BYTES = 65536;
6
+ let zeroPadding;
7
+ function assertZeroPadding(bytes) {
8
+ zeroPadding ??= Buffer.alloc(PADDING_WINDOW_BYTES);
9
+ if (!bytes.equals(zeroPadding.subarray(0, bytes.length))) {
10
+ throw new ArchiveFormatError("nonzero gzip container padding");
11
+ }
12
+ }
5
13
  export function isGzipBuffer(input) {
6
14
  return input[0] === 31 && input[1] === 139;
7
15
  }
@@ -14,12 +22,10 @@ function assertConsumedBoundary(consumed, size) {
14
22
  export async function validateGzipBufferTail(input, consumed, signal) {
15
23
  signal?.throwIfAborted();
16
24
  assertConsumedBoundary(consumed, input.length);
17
- for (let position = consumed; position < input.length; position += 65536) {
25
+ for (let position = consumed; position < input.length; position += PADDING_WINDOW_BYTES) {
18
26
  signal?.throwIfAborted();
19
- if (input.subarray(position, position + 65536).some(byte => byte !== 0)) {
20
- throw new ArchiveFormatError("nonzero gzip container padding");
21
- }
22
- if (position + 65536 < input.length)
27
+ assertZeroPadding(input.subarray(position, position + PADDING_WINDOW_BYTES));
28
+ if (position + PADDING_WINDOW_BYTES < input.length)
23
29
  await new Promise(resolve => setImmediate(resolve));
24
30
  }
25
31
  signal?.throwIfAborted();
@@ -75,7 +81,7 @@ export async function validateGzipContainerTail(filePath, consumed, signal) {
75
81
  assertConsumedBoundary(consumed, size);
76
82
  if (consumed === size)
77
83
  return;
78
- const buffer = Buffer.allocUnsafe(65536);
84
+ const buffer = Buffer.allocUnsafe(PADDING_WINDOW_BYTES);
79
85
  let position = consumed;
80
86
  while (position < size) {
81
87
  signal?.throwIfAborted();
@@ -83,9 +89,7 @@ export async function validateGzipContainerTail(filePath, consumed, signal) {
83
89
  signal?.throwIfAborted();
84
90
  if (bytesRead === 0)
85
91
  throw new ArchiveFormatError("truncated gzip container padding");
86
- if (buffer.subarray(0, bytesRead).some((byte) => byte !== 0)) {
87
- throw new ArchiveFormatError("nonzero gzip container padding");
88
- }
92
+ assertZeroPadding(buffer.subarray(0, bytesRead));
89
93
  position += bytesRead;
90
94
  }
91
95
  }
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
@@ -1,7 +1,8 @@
1
1
  import { classifyArchiveParserError } from "./archive-parser-errors.js";
2
2
  import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
- import { Readable } from "node:stream";
4
+ import { Readable, Writable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
5
6
  import { readBoundedAsync } from "./bounded-read.js";
6
7
  import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
7
8
  import { formatErrorDetail } from "./error-detail.js";
@@ -32,37 +33,21 @@ function normalizedRequestedEntry(entryPath) {
32
33
  }
33
34
  return normalized;
34
35
  }
35
- async function readStreamBounded(stream, maxBytes) {
36
- if (!(Symbol.asyncIterator in Object(stream))) {
37
- return await new Promise((resolve, reject) => {
38
- const readable = stream;
39
- const chunks = [];
40
- let total = 0;
41
- readable.on("data", (chunk) => {
42
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
- total += buffer.length;
44
- if (total > maxBytes) {
45
- readable.pause();
46
- reject(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
47
- return;
48
- }
49
- chunks.push(buffer);
50
- });
51
- readable.once("end", () => resolve(Buffer.concat(chunks, total)));
52
- readable.once("error", reject);
53
- });
54
- }
55
- const chunks = [];
36
+ async function readAdmittedTarPayload(stream, size) {
37
+ // Complete admission has bounded this exact range. Copy as it arrives so
38
+ // decoder chunks can be released before the caller receives the owned result.
39
+ const result = Buffer.allocUnsafe(size);
56
40
  let total = 0;
57
41
  for await (const chunk of stream) {
58
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
59
- total += buffer.length;
60
- if (total > maxBytes) {
61
- throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
42
+ if (chunk.length > size - total) {
43
+ throw new ArchiveFormatError("invalid admitted TAR payload size");
62
44
  }
63
- chunks.push(buffer);
45
+ chunk.copy(result, total);
46
+ total += chunk.length;
64
47
  }
65
- return Buffer.concat(chunks, total);
48
+ if (total !== size)
49
+ throw new ArchiveFormatError("truncated admitted TAR range");
50
+ return result;
66
51
  }
67
52
  async function readArchiveInput(archivePath) {
68
53
  const resolved = realpathSync.native(archivePath);
@@ -124,12 +109,31 @@ async function readZipEntry(buffer, entryPath, maxBytes, physicalCount) {
124
109
  (entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE) {
125
110
  throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
126
111
  }
112
+ const integrity = createZipIntegrityTransform(entry);
127
113
  const stream = typeof entry.nodeStream === "function"
128
114
  ? entry.nodeStream()
129
115
  : Readable.from(await entry.async("nodebuffer"));
130
- const integrity = createZipIntegrityTransform(entry);
131
- stream.once("error", (error) => integrity.destroy(normalizeZipIntegrityError(error)));
132
- return await readStreamBounded(stream.pipe(integrity), maxBytes);
116
+ const chunks = [];
117
+ let total = 0;
118
+ const destination = new Writable({
119
+ write(chunk, _encoding, callback) {
120
+ total += chunk.length;
121
+ if (total > maxBytes) {
122
+ callback(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
123
+ return;
124
+ }
125
+ chunks.push(chunk);
126
+ callback();
127
+ },
128
+ });
129
+ try {
130
+ // Iterator teardown can mask limit errors on Node 22; keep them in stream callbacks.
131
+ await pipeline(stream, integrity, destination);
132
+ return Buffer.concat(chunks, total);
133
+ }
134
+ catch (error) {
135
+ throw normalizeZipIntegrityError(error);
136
+ }
133
137
  }
134
138
  async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
135
139
  const seenPaths = new Set();
@@ -162,8 +166,8 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
162
166
  return Buffer.from(archiveBuffer.subarray(selected.offset, end));
163
167
  }
164
168
  let result;
165
- await replayTar({ archiveBuffer, limits, members: [selected], async consume(_member, payload) {
166
- result = await readStreamBounded(payload, maxBytes);
169
+ await replayTar({ archiveBuffer, limits, members: [selected], async consume(member, payload) {
170
+ result = await readAdmittedTarPayload(payload, member.size);
167
171
  } });
168
172
  return result;
169
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"archive-zip-names.d.ts","sourceRoot":"","sources":["../src/archive-zip-names.ts"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAEhD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAcjE;AA+BD,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB,GAAG,MAAM,GAAG,SAAS,CAiCrB"}
1
+ {"version":3,"file":"archive-zip-names.d.ts","sourceRoot":"","sources":["../src/archive-zip-names.ts"],"names":[],"mappings":"AAKA,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAEhD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAcjE;AA+BD,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB,GAAG,MAAM,GAAG,SAAS,CA8CrB"}
@@ -1,3 +1,4 @@
1
+ import { types } from "node:util";
1
2
  import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
3
  import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
3
4
  import { updateCrc32 } from "./archive-crc32.js";
@@ -69,19 +70,30 @@ export function admitZipNames(params) {
69
70
  const { central, local, flags, centralExtra, localExtra, seen } = params;
70
71
  if (!central.length || !local.length)
71
72
  zipFormat("empty entry name");
73
+ // Reuse only within this synchronous call; shared backing bytes can change
74
+ // concurrently even though admission does not yield while checking names.
75
+ const sameName = !types.isSharedArrayBuffer(central.buffer) &&
76
+ !types.isSharedArrayBuffer(local.buffer) && central.equals(local);
72
77
  const centralRaw = central.toString("latin1");
73
- const localRaw = local.toString("latin1");
78
+ const localRaw = sameName ? centralRaw : local.toString("latin1");
74
79
  const centralUtf8 = originalName(central, centralRaw, flags);
75
- const localUtf8 = originalName(local, localRaw, flags);
80
+ const localUtf8 = sameName ? centralUtf8 : originalName(local, localRaw, flags);
76
81
  const centralUnicode = unicodeName(central, centralExtra);
77
- const localUnicode = unicodeName(local, localExtra);
82
+ const centralField = centralExtra.get(0x7075);
83
+ const localField = localExtra.get(0x7075);
84
+ // Only immutable identical fields share their CRC-bound name admission.
85
+ const sameUnicode = (!centralField || !types.isSharedArrayBuffer(centralField.buffer)) &&
86
+ (!localField || !types.isSharedArrayBuffer(localField.buffer)) &&
87
+ (centralField === localField ||
88
+ (centralField !== undefined && localField !== undefined && centralField.equals(localField)));
89
+ const localUnicode = sameName && sameUnicode ? centralUnicode : unicodeName(local, localExtra);
78
90
  const centralKey = key(centralRaw);
79
- if (centralKey !== key(localRaw)) {
91
+ if (!sameName && centralKey !== key(localRaw)) {
80
92
  zipFormat("central and local names disagree");
81
93
  }
82
94
  const interpretations = [centralUtf8, localUtf8, centralUnicode, localUnicode].filter((value) => value !== undefined);
83
95
  const interpretationKey = interpretations.length ? key(interpretations[0]) : undefined;
84
- if (interpretations.some((value) => key(value) !== interpretationKey)) {
96
+ if (interpretations.some((value) => value !== interpretations[0] && key(value) !== interpretationKey)) {
85
97
  zipFormat("conflicting Unicode name interpretations");
86
98
  }
87
99
  // JSZip checks the central Unicode field against the local name. A slash-only
@@ -54,7 +54,7 @@ export async function readBoundedAsync(maxBytes, readChunk, options = {}) {
54
54
  // initial read; virtual files can report zero or stale sizes thereafter.
55
55
  const currentSize = bytesRead < buffer.length ? observeRegularFileSize?.() : undefined;
56
56
  total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
57
- if (currentSize !== undefined && bytesRead >= currentSize)
57
+ if (currentSize !== undefined && currentSize > 0 && bytesRead >= currentSize)
58
58
  return finishReadBuffer(buffer, total, allocate);
59
59
  }
60
60
  while (true) {
@@ -120,7 +120,7 @@ export function readBoundedSync(maxBytes, readChunk, options = {}) {
120
120
  return finishReadBuffer(buffer, 0);
121
121
  const currentSize = bytesRead < buffer.length ? observeRegularFileSize?.() : undefined;
122
122
  total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
123
- if (currentSize !== undefined && bytesRead >= currentSize)
123
+ if (currentSize !== undefined && currentSize > 0 && bytesRead >= currentSize)
124
124
  return finishReadBuffer(buffer, total);
125
125
  }
126
126
  while (true) {
@@ -1 +1 @@
1
- {"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAcF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAgC5D,CAAC;AAsEH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAcD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,2BAAgC,GACxC,yBAAyB,GAAG,SAAS,CAWvC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAON"}
1
+ {"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAcF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAgC5D,CAAC;AA2EH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAcD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,2BAAgC,GACxC,yBAAyB,GAAG,SAAS,CAWvC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAON"}
@@ -96,9 +96,11 @@ function matchPosixDeviceReadPath(filePath, cwd) {
96
96
  }
97
97
  function normalizeWindowsDeviceBaseName(filePath) {
98
98
  const normalized = trimTrailingWindowsSeparators(filePath.replace(/\//g, "\\"));
99
- const lastSegment = normalized.split("\\").filter(Boolean).at(-1) ?? normalized;
100
- const withoutStream = lastSegment.split(":")[0] ?? lastSegment;
101
- const stem = withoutStream.split(".")[0] ?? withoutStream;
99
+ const start = normalized.lastIndexOf("\\") + 1;
100
+ const stream = normalized.indexOf(":", start);
101
+ const extension = normalized.indexOf(".", start);
102
+ const end = Math.min(stream < 0 ? normalized.length : stream, extension < 0 ? normalized.length : extension);
103
+ const stem = normalized.slice(start, end);
102
104
  return trimTrailingWindowsIgnoredChars(stem).toUpperCase();
103
105
  }
104
106
  export function isWindowsReservedDeviceName(name) {
@@ -1 +1 @@
1
- {"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAClD,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,EACtD,EAAE,QAAmB,EAAE,MAAM,EAAE,GAAE,iBAAsB,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;AAyDD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAoDD,8FAA8F;AAC9F,wBAAgB,cAAc,CAC5B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAMlB"}
1
+ {"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAClD,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,EACtD,EAAE,QAAmB,EAAE,MAAM,EAAE,GAAE,iBAAsB,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAwD3B;AAyDD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAuDD,8FAA8F;AAC9F,wBAAgB,cAAc,CAC5B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAMlB"}
package/dist/file-hash.js CHANGED
@@ -38,7 +38,7 @@ export async function hashFileHandle(handle, native = getNativeBinding(), { maxB
38
38
  }
39
39
  }
40
40
  const hash = createHash("sha256");
41
- const buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(64 * 1024, stat.size)));
41
+ let buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(1, stat.size + 1), maxBytes + 1));
42
42
  let position = 0;
43
43
  while (true) {
44
44
  signal?.throwIfAborted();
@@ -53,6 +53,10 @@ export async function hashFileHandle(handle, native = getNativeBinding(), { maxB
53
53
  }
54
54
  hash.update(buffer.subarray(0, bytesRead));
55
55
  position += bytesRead;
56
+ // A full small buffer can mean growth or a virtual file with an unhelpful size.
57
+ if (bytesRead === buffer.length && buffer.length < 64 * 1024) {
58
+ buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1));
59
+ }
56
60
  }
57
61
  }
58
62
  function hashPathIdentity(filePath, afterOpen = false) {
@@ -120,7 +124,7 @@ function hashDescriptorSync(fd, { maxBytes = Infinity, signal }) {
120
124
  throw new FsSafeError("too-large", `SHA-256 input exceeds ${maxBytes} bytes`);
121
125
  }
122
126
  const hash = createHash("sha256");
123
- const buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(64 * 1024, stat.size)));
127
+ let buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(1, stat.size + 1), maxBytes + 1));
124
128
  let position = 0;
125
129
  while (true) {
126
130
  signal?.throwIfAborted();
@@ -135,6 +139,9 @@ function hashDescriptorSync(fd, { maxBytes = Infinity, signal }) {
135
139
  }
136
140
  hash.update(buffer.subarray(0, bytesRead));
137
141
  position += bytesRead;
142
+ if (bytesRead === buffer.length && buffer.length < 64 * 1024) {
143
+ buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1));
144
+ }
138
145
  }
139
146
  }
140
147
  function hashPathSync(filePath, options) {
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFhB"}
1
+ {"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8FhB"}
@@ -76,7 +76,15 @@ export async function pruneExpiredStoreEntries(params) {
76
76
  }
77
77
  if (stat.isFile() && now - stat.mtimeMs > params.options.ttlMs) {
78
78
  await assertRootGuard();
79
- await scopedRoot.remove(relativePath).catch(() => undefined);
79
+ await scopedRoot.remove(relativePath, {
80
+ assertBeforeMutation: () => {
81
+ // Removal preparation can outlive the expiry observation above.
82
+ const current = fsSync.lstatSync(fullPath);
83
+ if (!current.isFile() || !(now - current.mtimeMs > params.options.ttlMs)) {
84
+ throw new FsSafeError("path-mismatch", "store entry is no longer an expired file");
85
+ }
86
+ },
87
+ }).catch(() => undefined);
80
88
  }
81
89
  }
82
90
  if (!pruneEmptyDirs) {
@@ -1,3 +1,4 @@
1
+ export declare function suffixWindowsReservedDeviceName(fileName: string): string;
1
2
  /** Keeps short names exact and trims only the filename tail of a composite temp name. */
2
3
  export declare function fitFileNameToPortableComponent(params: {
3
4
  prefix: string;
@@ -1 +1 @@
1
- {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAmCA,yFAAyF;AACzF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CA8BT;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAoBxF"}
1
+ {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAwBA,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAexE;AAkBD,yFAAyF;AACzF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CA8BT;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAqBxF"}
package/dist/filename.js CHANGED
@@ -2,8 +2,13 @@ import path from "node:path";
2
2
  import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
3
3
  import { maxNormalizedUtf8Bytes } from "./unicode-path.js";
4
4
  const INVALID_FILE_NAME_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/g;
5
- function trimWindowsIgnoredSuffix(value) {
6
- let end = value.length;
5
+ function canStartWindowsDeviceName(character) {
6
+ const folded = character | 0x20;
7
+ return folded === 0x61 || folded === 0x63 || folded === 0x6c ||
8
+ folded === 0x6e || folded === 0x70;
9
+ }
10
+ function windowsDeviceStemEnd(value, stemEnd) {
11
+ let end = stemEnd;
7
12
  while (end > 0) {
8
13
  const character = value.charCodeAt(end - 1);
9
14
  if (character !== 0x20 && character !== 0x2e) {
@@ -11,19 +16,32 @@ function trimWindowsIgnoredSuffix(value) {
11
16
  }
12
17
  end -= 1;
13
18
  }
14
- return end === value.length ? value : value.slice(0, end);
19
+ return end;
15
20
  }
16
- function suffixWindowsReservedDeviceName(fileName) {
21
+ export function suffixWindowsReservedDeviceName(fileName) {
22
+ if (!canStartWindowsDeviceName(fileName.charCodeAt(0))) {
23
+ return fileName;
24
+ }
17
25
  const extensionIndex = fileName.indexOf(".");
18
26
  const baseNameEnd = extensionIndex < 0 ? fileName.length : extensionIndex;
19
- const baseName = fileName.slice(0, baseNameEnd);
20
- const deviceBaseName = trimWindowsIgnoredSuffix(baseName);
21
- if (!WINDOWS_RESERVED_DEVICE_NAMES.has(deviceBaseName.toUpperCase())) {
27
+ const deviceBaseNameEnd = windowsDeviceStemEnd(fileName, baseNameEnd);
28
+ if (deviceBaseNameEnd === 0 ||
29
+ deviceBaseNameEnd > 7 ||
30
+ !WINDOWS_RESERVED_DEVICE_NAMES.has(fileName.slice(0, deviceBaseNameEnd).toUpperCase())) {
22
31
  return fileName;
23
32
  }
24
- return `${baseName}_${fileName.slice(baseNameEnd)}`;
33
+ return `${fileName.slice(0, baseNameEnd)}_${fileName.slice(baseNameEnd)}`;
25
34
  }
26
35
  const PORTABLE_FILE_NAME_BYTES = 255;
36
+ const UNTRUSTED_FILE_NAME_CODE_UNITS = 200;
37
+ function truncateWithoutSplittingSurrogatePair(value, maxLength) {
38
+ let truncated = value.slice(0, maxLength);
39
+ const trailingCodeUnit = truncated.charCodeAt(truncated.length - 1);
40
+ if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
41
+ truncated = truncated.slice(0, -1);
42
+ }
43
+ return truncated;
44
+ }
27
45
  function normalizedFileNameBytes(value) {
28
46
  return maxNormalizedUtf8Bytes(value, true);
29
47
  }
@@ -71,11 +89,10 @@ export function sanitizeUntrustedFileName(fileName, fallbackName) {
71
89
  return fallbackName;
72
90
  }
73
91
  base = suffixWindowsReservedDeviceName(base);
74
- if (base.length > 200) {
75
- base = base.slice(0, 200);
76
- const trailingCodeUnit = base.charCodeAt(base.length - 1);
77
- if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
78
- base = base.slice(0, -1);
92
+ if (base.length > UNTRUSTED_FILE_NAME_CODE_UNITS) {
93
+ base = truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS);
94
+ if (suffixWindowsReservedDeviceName(base) !== base) {
95
+ base = suffixWindowsReservedDeviceName(truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS - 1));
79
96
  }
80
97
  }
81
98
  return base;
@@ -1,3 +1,4 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  /**
2
3
  * Creates each missing path component from `rootReal` down to `targetPath`,
3
4
  * guarding every step. Returns the real (symlink-resolved) path of the final
@@ -11,5 +12,6 @@ export declare function mkdirPathComponentsWithGuards(params: {
11
12
  assertBeforeMutation?: () => void;
12
13
  mode?: number;
13
14
  rejectSymlinks?: boolean;
15
+ rootIdentity?: RootBoundaryIdentity;
14
16
  }): Promise<string>;
15
17
  //# sourceMappingURL=guarded-mkdir.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDlB"}
1
+ {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAgCpF;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,GAAG,OAAO,CAAC,MAAM,CAAC,CAqFlB"}