@openclaw/fs-safe 0.5.5 → 0.6.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 (164) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +16 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-kind.js +2 -2
  10. package/dist/archive-native.d.ts.map +1 -1
  11. package/dist/archive-native.js +7 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +24 -5
  14. package/dist/archive-tar-meta.d.ts.map +1 -1
  15. package/dist/archive-tar-meta.js +53 -6
  16. package/dist/archive-tar-pax.d.ts +8 -0
  17. package/dist/archive-tar-pax.d.ts.map +1 -0
  18. package/dist/archive-tar-pax.js +100 -0
  19. package/dist/archive-tar-runtime.d.ts +1 -0
  20. package/dist/archive-tar-runtime.d.ts.map +1 -1
  21. package/dist/archive-zip-admission.d.ts +7 -0
  22. package/dist/archive-zip-admission.d.ts.map +1 -0
  23. package/dist/archive-zip-admission.js +60 -0
  24. package/dist/archive-zip-count.d.ts +2 -0
  25. package/dist/archive-zip-count.d.ts.map +1 -0
  26. package/dist/archive-zip-count.js +140 -0
  27. package/dist/archive-zip-directory.d.ts +8 -0
  28. package/dist/archive-zip-directory.d.ts.map +1 -0
  29. package/dist/archive-zip-directory.js +224 -0
  30. package/dist/archive-zip-integrity.d.ts.map +1 -1
  31. package/dist/archive-zip-integrity.js +1 -14
  32. package/dist/archive-zip-names.d.ts +12 -0
  33. package/dist/archive-zip-names.d.ts.map +1 -0
  34. package/dist/archive-zip-names.js +99 -0
  35. package/dist/archive-zip-preflight.d.ts +1 -1
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +8 -145
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +32 -23
  40. package/dist/file-hash.d.ts.map +1 -1
  41. package/dist/file-hash.js +25 -19
  42. package/dist/file-identity.d.ts +1 -0
  43. package/dist/file-identity.d.ts.map +1 -1
  44. package/dist/file-identity.js +10 -0
  45. package/dist/file-lock-sync.d.ts.map +1 -1
  46. package/dist/file-lock-sync.js +19 -7
  47. package/dist/move-path.js +2 -2
  48. package/dist/native-binding.d.ts +3 -0
  49. package/dist/native-binding.d.ts.map +1 -1
  50. package/dist/native-operations.d.ts +4 -1
  51. package/dist/native-operations.d.ts.map +1 -1
  52. package/dist/native-operations.js +24 -8
  53. package/dist/native-pinned-write-windows.d.ts +8 -0
  54. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  55. package/dist/native-pinned-write-windows.js +92 -0
  56. package/dist/native-pinned-write.d.ts.map +1 -1
  57. package/dist/native-pinned-write.js +136 -127
  58. package/dist/native-staged-file.d.ts +24 -0
  59. package/dist/native-staged-file.d.ts.map +1 -0
  60. package/dist/native-staged-file.js +337 -0
  61. package/dist/native.d.ts.map +1 -1
  62. package/dist/native.js +4 -4
  63. package/dist/opened-realpath.d.ts +2 -0
  64. package/dist/opened-realpath.d.ts.map +1 -1
  65. package/dist/opened-realpath.js +12 -7
  66. package/dist/output-sibling.js +3 -3
  67. package/dist/owner-dacl.d.ts.map +1 -1
  68. package/dist/owner-dacl.js +2 -1
  69. package/dist/permission-exec.d.ts +19 -0
  70. package/dist/permission-exec.d.ts.map +1 -1
  71. package/dist/permission-exec.js +57 -11
  72. package/dist/permissions-public.d.ts +1 -1
  73. package/dist/permissions-public.d.ts.map +1 -1
  74. package/dist/permissions-windows.d.ts +3 -0
  75. package/dist/permissions-windows.d.ts.map +1 -1
  76. package/dist/permissions-windows.js +16 -5
  77. package/dist/permissions.d.ts +5 -0
  78. package/dist/permissions.d.ts.map +1 -1
  79. package/dist/pinned-write.d.ts +6 -0
  80. package/dist/pinned-write.d.ts.map +1 -1
  81. package/dist/pinned-write.js +19 -29
  82. package/dist/private-directory.d.ts.map +1 -1
  83. package/dist/private-directory.js +3 -2
  84. package/dist/private-temp-workspace.d.ts.map +1 -1
  85. package/dist/private-temp-workspace.js +23 -15
  86. package/dist/publish-file.d.ts.map +1 -1
  87. package/dist/publish-file.js +2 -2
  88. package/dist/replace-file-descriptor.d.ts +3 -3
  89. package/dist/replace-file-descriptor.d.ts.map +1 -1
  90. package/dist/replace-file-descriptor.js +2 -2
  91. package/dist/root-impl.d.ts.map +1 -1
  92. package/dist/root-impl.js +84 -75
  93. package/dist/root-paths.d.ts +11 -14
  94. package/dist/root-paths.d.ts.map +1 -1
  95. package/dist/root-paths.js +36 -27
  96. package/dist/root-write-verification.d.ts +11 -0
  97. package/dist/root-write-verification.d.ts.map +1 -0
  98. package/dist/root-write-verification.js +91 -0
  99. package/dist/secret-file.d.ts +1 -6
  100. package/dist/secret-file.d.ts.map +1 -1
  101. package/dist/secret-file.js +43 -117
  102. package/dist/secret-read-async.d.ts +1 -1
  103. package/dist/secret-read-async.d.ts.map +1 -1
  104. package/dist/secret-read-async.js +45 -67
  105. package/dist/secret-read-policy.d.ts +13 -0
  106. package/dist/secret-read-policy.d.ts.map +1 -0
  107. package/dist/secret-read-policy.js +28 -0
  108. package/dist/secret.d.ts +2 -1
  109. package/dist/secret.d.ts.map +1 -1
  110. package/dist/secret.js +2 -1
  111. package/dist/secure-file.d.ts.map +1 -1
  112. package/dist/secure-file.js +25 -16
  113. package/dist/sibling-temp.js +1 -1
  114. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  115. package/dist/sidecar-lock-acquire.js +4 -1
  116. package/dist/staged-directory.d.ts +16 -0
  117. package/dist/staged-directory.d.ts.map +1 -0
  118. package/dist/staged-directory.js +60 -0
  119. package/dist/staged-file-types.d.ts +56 -0
  120. package/dist/staged-file-types.d.ts.map +1 -0
  121. package/dist/staged-file-types.js +1 -0
  122. package/dist/staged-file.d.ts +10 -0
  123. package/dist/staged-file.d.ts.map +1 -0
  124. package/dist/staged-file.js +15 -0
  125. package/dist/strict-file-identity.d.ts +6 -0
  126. package/dist/strict-file-identity.d.ts.map +1 -0
  127. package/dist/strict-file-identity.js +48 -0
  128. package/dist/temp-cleanup.d.ts.map +1 -1
  129. package/dist/temp-cleanup.js +3 -3
  130. package/dist/temp-target.d.ts.map +1 -1
  131. package/dist/temp-target.js +9 -7
  132. package/dist/windows-owner.d.ts +3 -0
  133. package/dist/windows-owner.d.ts.map +1 -1
  134. package/dist/windows-owner.js +10 -2
  135. package/docs/advanced.md +11 -1
  136. package/docs/archive.md +62 -15
  137. package/docs/config.md +7 -0
  138. package/docs/contributing.md +45 -7
  139. package/docs/durability.md +17 -4
  140. package/docs/errors.md +16 -1
  141. package/docs/index.md +3 -1
  142. package/docs/install.md +15 -7
  143. package/docs/migrating-to-0.5.md +7 -7
  144. package/docs/migrating-to-0.6.md +43 -0
  145. package/docs/native-helper.md +15 -5
  146. package/docs/native.md +40 -11
  147. package/docs/path-scope.md +28 -2
  148. package/docs/permissions.md +13 -2
  149. package/docs/root.md +1 -1
  150. package/docs/secret-file.md +8 -0
  151. package/docs/secure-file.md +17 -1
  152. package/docs/security-model.md +15 -1
  153. package/docs/sidecar-lock.md +13 -3
  154. package/docs/staged-file.md +178 -0
  155. package/docs/testing.md +1 -1
  156. package/docs/writing.md +19 -0
  157. package/package.json +16 -9
  158. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  159. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  160. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  161. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  162. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  163. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  164. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-08-29
4
+
5
+ ### Highlights
6
+
7
+ - Install only the matching native platform package; deployments using native mode `require` or native-only features must keep optional dependencies enabled. See the [0.6 migration guide](https://fs-safe.io/migrating-to-0.6.html).
8
+ - Strengthen root, secure, secret, and pathname-hash identity checks with lossless bigint comparisons and fail-closed handling of unknown Windows identities.
9
+ - Add native-required Linux/macOS `stageFileInDirectory()` with retained-directory cleanup, private staging, identity-checked publication, and async disposal.
10
+ - Validate physical ZIP metadata and names before decoder normalization or collapse, align bounded TAR/PAX and stripped-path handling, and reject ZIP symlinks disguised as directories.
11
+
12
+ ### Security and Correctness
13
+
14
+ - Validate physical ZIP local/central and Unicode name metadata before decoding, rejecting traversal, hidden duplicates, and conflicting interpretations consistently in extraction and bounded reads.
15
+
16
+ - Verify pathname SHA-256 hashing with lossless pre-open, descriptor, and current-path identities; fail closed on unknown Windows identities after one bounded retry without reopening the file.
17
+ - Preserve underlying command diagnostics (command, timing, timeout flag, exit code/signal, and sanitized stderr) and cause on Windows ACL `permission-unverified` errors without changing verification semantics.
18
+ - Verify secure and secret read identities with lossless bigint stats, reject replaced paths and retargeted aliases, and fail closed on unknown Windows identities after one bounded re-inspection; preserve the secure reader's numeric `Stats` receipt and permission options without letting them bypass identity checks.
19
+ - Apply the same exact identity checks to guarded root reads, rejecting parent-directory replacements even when distinct Windows file IDs round to the same number; retain numeric read receipts and keep Windows write reopens anchored to the writer's exact retained identity.
20
+ - Verify `Root.write()` and `create()` through retained descriptors and exact bigint identities for restrictive final modes, retaining guarded Windows path opens when pathname identity is unavailable; preserve explicit mode zero in `copyIn()`.
21
+ - Preserve filesystem failures from `ensureDirectoryWithinRoot()` and `pathScope().ensureDir()` in an optional operational `FsSafeError` diagnostic with the original cause and bounded, escaped display text, instead of misreporting them as containment violations; keep nonthrowing string results and directory safety checks.
22
+ - Accept bounded local PAX paths, sizes, and descriptive metadata, including inert binary macOS provenance xattrs, consistently in JavaScript and native TAR extraction/reads; reject ambiguous records, extension chains, and sparse semantics while retaining byte/count limits and guarded staging. Return TAR read traversal failures through the public promise instead of escaping the parser callback.
23
+ - Add native-required Linux/macOS `stageFileInDirectory()` under `advanced` with retained-directory abort cleanup, private `0600` staging until publication is identity-checked, exact identity checks, explicit publication/cleanup receipts, and async disposal; share that ownership with POSIX native streaming writes so parent moves no longer strand their unpublished temps.
24
+ - Share native writer admission and direct-child cleanup mechanisms; preserve combined POSIX coordinator operation/disposal failures with `SuppressedError` while keeping staged preparation/cleanup receipts and Windows close policy unchanged.
25
+ - Fail closed when synchronous sidecar compromise checks hit I/O errors and invoke `onCompromised` once instead of throwing from the interval. Thanks @SebTardif.
26
+ - Reject ZIP symlink-mode entries with a trailing slash or DOS directory bit before creating output, aligning JavaScript extraction with native link policy while preserving explicit filtering. Thanks @Yigtwxx.
27
+ - Make the validated stripped path authoritative during JavaScript TAR extraction, fixing `ENOENT` and native path disagreement for dot or empty components, including local PAX paths, while preserving pre-strip filter inputs. Thanks @Yigtwxx.
28
+ - Apply process-wide retry, timeout, and stale-policy defaults to synchronous file locks while preserving per-call overrides and caller-approved guarded recovery. Thanks @Yigtwxx.
29
+
30
+ ### Docs and Tooling
31
+
32
+ - Publish native bindings as platform-filtered optional packages and load only the matching package, avoiding installation of binaries for six unrelated targets; verify root-only npm/pnpm resolution and document omitted-optionals limits and native-only recovery guidance. Thanks @RomneyDa.
33
+ - Raise the Crabbox AWS root volume to 400 GiB to meet the runner image's snapshot minimum and avoid allocation failures before checks run.
34
+ - Restore six missing documentation navigation entries, remove the dangling page, correct the lock-config link, and reject missing, nonexistent, or duplicate registrations before replacing site output. Thanks @Yigtwxx.
35
+ - Fix the standalone native smoke script to use the numeric descriptor returned by `openBeneath()` for identity checks and cleanup.
36
+ - Accept npm 11 array and npm 12 package-name-keyed pack results while validating the intended package and its file metadata.
37
+
38
+ ### Dependencies and maintenance
39
+
40
+ - Refresh the Node and Rust dependency graphs, align development and CI on pnpm 11.24.0, and update the CodeQL, npm, and cargo-zigbuild pins while preserving the two-day dependency cooldown.
41
+
42
+ ## 0.5.6 - 2026-08-14
43
+
44
+ ### Security and Correctness
45
+
46
+ - Treat asynchronous sidecar compromise-check I/O failures as a lost lock and invoke `onCompromised` once, instead of leaking an unhandled rejection from the interval.
47
+ - Preserve replaced temporary paths during Windows cleanup when libuv reports a zero device or inode or a file index exceeds JavaScript's safe integer range, treating unknown or rounded identity as fail-closed instead of authorizing deletion.
48
+
49
+ ### Docs and Tooling
50
+
51
+ - Set up Node before pnpm when hydrating Crabbox runners so minimal images without a preinstalled npm can prepare the workspace.
3
52
  ## 0.5.5 - 2026-08-12
4
53
 
5
54
  ### Security and Correctness
package/README.md CHANGED
@@ -26,7 +26,7 @@ Full docs and reference at **[fs-safe.io](https://fs-safe.io)**.
26
26
 
27
27
  ## Contents
28
28
 
29
- [Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [0.5 migration](docs/migrating-to-0.5.md) · [Python migration](#migrating-from-the-python-helper) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Directory durability](#directory-durability) · [Atomic writes](#atomic-writes) · [External outputs](#external-outputs) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)
29
+ [Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [0.6 migration](docs/migrating-to-0.6.md) · [Python migration](#migrating-from-the-python-helper) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Directory durability](#directory-durability) · [Atomic writes](#atomic-writes) · [External outputs](#external-outputs) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)
30
30
 
31
31
  ## Why this exists
32
32
 
@@ -57,10 +57,10 @@ This is a **library-level guardrail**, not OS-level isolation. It does not repla
57
57
  pnpm add @openclaw/fs-safe
58
58
  ```
59
59
 
60
- Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP/TAR support; installs that omit optional dependencies can still use every non-archive subpath.
60
+ Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and non-archive fallback-capable operations work in `auto` or `off`. Native-only features remain unavailable, and operations needing the binding in `require` mode fail with `helper-unavailable`. JavaScript ZIP/TAR fallback also needs the optional `jszip`/`tar` codecs. See the [0.6 migration guide](docs/migrating-to-0.6.md).
61
61
 
62
- The package bundles prebuilt native bindings for seven supported targets. They
63
- supply fd-relative and atomic no-replace primitives that Node does not expose
62
+ The package installs one prebuilt native binding for the current supported target. It
63
+ supplies fd-relative and atomic no-replace primitives that Node does not expose
64
64
  directly. Configure the lazy loader before first use when you need a strict
65
65
  environment policy:
66
66
 
@@ -80,11 +80,11 @@ replace a writable parent between its identity check and Node's pathname
80
80
  mutation can redirect that mutation outside the root before the post-check
81
81
  reports the escape. Use `require` when hostile concurrent mutation is in scope.
82
82
 
83
- Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. All seven binaries
84
- ship inside `@openclaw/fs-safe`; there are no platform packages, postinstall
85
- steps, downloads, or consumer Rust builds. This makes the tarball larger than
86
- a per-platform package, but makes installation deterministic. On a platform
87
- without a bundled binary, `auto` silently retains lexical and canonical root
83
+ Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. The seven bindings
84
+ ship as exact-version optional packages filtered by OS, CPU, and Linux libc, so
85
+ a normal install receives only its matching binary. There are no postinstall
86
+ steps, runtime downloads, or consumer Rust builds. On a platform without a
87
+ published binding, or when optional dependencies are omitted, `auto` silently retains lexical and canonical root
88
88
  checks, no-follow opens, guarded temp+rename writes, and post-write identity
89
89
  verification. See the [native
90
90
  helper policy](docs/native-helper.md) for the exact boundary and deployment
@@ -97,7 +97,7 @@ and guarded JavaScript results are best-effort. See the [security model](docs/se
97
97
 
98
98
  ## Migrating from the Python helper
99
99
 
100
- Version 0.5 replaces the persistent Python worker with bundled prebuilt native
100
+ Version 0.5 replaces the persistent Python worker with prebuilt native
101
101
  bindings. The modes map directly: `configureFsSafePython({ mode: "auto" })`
102
102
  becomes `configureFsSafeNative({ mode: "auto" })`, and likewise for `off` and
103
103
  `require`. Replace `FS_SAFE_PYTHON_MODE` with `FS_SAFE_NATIVE_MODE`; remove
@@ -284,6 +284,12 @@ publication policy, creation callback, and platform contract.
284
284
 
285
285
  ## Atomic writes
286
286
 
287
+ For preparation that must survive a parent rename until abort cleanup, use
288
+ [`stageFileInDirectory()`](docs/staged-file.md) from `advanced`. It retains the
289
+ original directory on Linux/macOS and requires native support for this operation.
290
+ It offers atomic replace/no-replace publication, not expected-inode replacement
291
+ or a crash-durability promise; application checks and coordination remain yours.
292
+
287
293
  `replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation, pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved.
288
294
 
289
295
  ```ts
@@ -1,4 +1,5 @@
1
1
  export { createAsyncLock } from "./async-lock.js";
2
+ export { stageFileInDirectory, type StagedFile, type StagedFileReceipt, type PublishedFileReceipt, type StagedFilePublication, type StagedFileCleanupReceipt, type StagedFileFailureDetails, } from "./staged-file.js";
2
3
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
3
4
  export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, type UnsafeDeviceReadPathMatch, type UnsafeDeviceReadPathOptions, type UnsafeDeviceReadPathReason, } from "./device-path.js";
4
5
  export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, type AbsolutePathSymlinkPolicy, type EnsureAbsoluteDirectoryOptions, type EnsureAbsoluteDirectoryResult, type ResolvedAbsolutePath, type ResolvedWritableAbsolutePath, } from "./absolute-path.js";
@@ -21,5 +22,5 @@ export { resolveHomeRelativePath } from "./home-dir.js";
21
22
  export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, type AppendRegularFileOptions, type RegularFileStatResult, } from "./regular-file.js";
22
23
  export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile, withTempFile, } from "./temp-target.js";
23
24
  export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
24
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
25
+ export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionCommandFailure, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
25
26
  //# sourceMappingURL=advanced.d.ts.map
@@ -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
@@ -2,6 +2,7 @@
2
2
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
3
  // higher-level primitive.
4
4
  export { createAsyncLock } from "./async-lock.js";
5
+ export { stageFileInDirectory, } from "./staged-file.js";
5
6
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
6
7
  export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
7
8
  export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, } from "./absolute-path.js";
@@ -0,0 +1,2 @@
1
+ export declare function updateCrc32(previous: number, buffer: Buffer): number;
2
+ //# sourceMappingURL=archive-crc32.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-crc32.d.ts","sourceRoot":"","sources":["../src/archive-crc32.ts"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMpE"}
@@ -0,0 +1,14 @@
1
+ const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
2
+ let value = index;
3
+ for (let bit = 0; bit < 8; bit += 1) {
4
+ value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
5
+ }
6
+ return value >>> 0;
7
+ });
8
+ export function updateCrc32(previous, buffer) {
9
+ let crc = previous ^ -1;
10
+ for (const byte of buffer) {
11
+ crc = (crc >>> 8) ^ (CRC32_TABLE[(crc ^ byte) & 0xff] ?? 0);
12
+ }
13
+ return (crc ^ -1) >>> 0;
14
+ }
@@ -10,8 +10,8 @@ const NATIVE_TAR_SUFFIXES = [
10
10
  ];
11
11
  function requireNativeArchiveKind(kind) {
12
12
  if (!getNativeBinding()) {
13
- throw new FsSafeError("helper-unavailable", `${kind} archives require a supported bundled native binding; ` +
14
- "use FS_SAFE_NATIVE_MODE=auto or require on a supported platform");
13
+ throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
14
+ "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
15
15
  }
16
16
  return kind;
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,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,CAAC,EAAE,oBAAoB,CAAC;IAC9B,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,CAwHhB"}
1
+ {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwBjD,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,CAAC,EAAE,oBAAoB,CAAC;IAC9B,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,CA8HhB"}
@@ -7,6 +7,7 @@ import { stageArchiveFileForExtraction } from "./archive-input.js";
7
7
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
8
8
  import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
9
9
  import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
10
+ import { admitZipFile } from "./archive-zip-admission.js";
10
11
  function policyKind(kind) {
11
12
  if (kind === "file" || kind === "directory")
12
13
  return kind;
@@ -37,6 +38,9 @@ export async function extractNativeArchive(params) {
37
38
  deadline: params.deadline,
38
39
  });
39
40
  try {
41
+ const physicalCount = params.kind === "zip"
42
+ ? await admitZipFile(stagedArchive.path, limits, params.deadline)
43
+ : undefined;
40
44
  const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
41
45
  await withStagedArchiveDestination({
42
46
  destinationRealDir,
@@ -47,6 +51,9 @@ export async function extractNativeArchive(params) {
47
51
  .catch(throwMappedNativeError);
48
52
  params.deadline.check();
49
53
  assertArchiveEntryCountWithinLimit(manifest.length, limits);
54
+ if (physicalCount !== undefined && manifest.length !== physicalCount) {
55
+ throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
56
+ }
50
57
  const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
51
58
  const budget = createByteBudgetTracker(limits);
52
59
  const trackOutputPath = createArchiveOutputPathTracker();
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqMzE,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,CAmFjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA8MzE,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,CAuFjB"}
@@ -15,6 +15,8 @@ import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./arch
15
15
  import { FsSafeError } from "./errors.js";
16
16
  import { sameFileIdentity } from "./file-identity.js";
17
17
  import { getNativeBinding } from "./native.js";
18
+ import { admitZipBuffer } from "./archive-zip-admission.js";
19
+ import { resolveExtractLimits } from "./archive-limits.js";
18
20
  import { tempFile } from "./temp-target.js";
19
21
  const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
20
22
  const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
@@ -127,7 +129,15 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
127
129
  maxMetaEntrySize: DEFAULT_MAX_META_ENTRY_BYTES,
128
130
  onReadEntry(entry) {
129
131
  const info = readTarEntryInfo(entry);
130
- validateArchiveEntryPath(info.path, { escapeLabel: "archive root" });
132
+ try {
133
+ validateArchiveEntryPath(info.path, { escapeLabel: "archive root" });
134
+ }
135
+ catch (error) {
136
+ // Throws escaping node-tar's callback do not reject its promise.
137
+ entryError ??= error instanceof Error ? error : new Error(String(error));
138
+ entry.resume();
139
+ return;
140
+ }
131
141
  const normalized = normalizeArchiveEntryPath(info.path).replace(/^\.\//, "");
132
142
  if (seenPaths.has(normalized)) {
133
143
  entryError ??= new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
@@ -175,12 +185,18 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
175
185
  const requestedEntry = normalizedRequestedEntry(entryPath);
176
186
  const staged = await stageArchiveInput(archivePath);
177
187
  try {
188
+ const physicalCount = kind === "zip"
189
+ ? admitZipBuffer(staged.buffer, resolveExtractLimits())
190
+ : undefined;
178
191
  const native = getNativeBinding();
179
192
  if (native) {
180
193
  try {
181
194
  const signal = new AbortController().signal;
182
195
  const manifest = await native.inspectArchiveNative(staged.path, kind, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, signal);
183
196
  let rawEntryPath;
197
+ if (physicalCount !== undefined && manifest.length !== physicalCount) {
198
+ throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
199
+ }
184
200
  const seenPaths = new Set();
185
201
  for (const entry of manifest) {
186
202
  validateArchiveEntryPath(entry.path, { escapeLabel: "archive root" });
@@ -202,9 +218,11 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
202
218
  return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, signal);
203
219
  }
204
220
  catch (error) {
205
- if (error instanceof Error &&
206
- error.message.includes(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT)) {
207
- throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
221
+ if (error instanceof Error) {
222
+ for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
223
+ if (error.message.includes(code))
224
+ throw new ArchiveLimitError(code);
225
+ }
208
226
  }
209
227
  if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
210
228
  throw new ArchiveFormatError(error.message, { cause: error });
@@ -213,7 +231,8 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
213
231
  }
214
232
  }
215
233
  if (kind === "tar-zstd" || kind === "tar-bzip2") {
216
- throw new FsSafeError("helper-unavailable", `${kind} archives require a supported bundled native binding`);
234
+ throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
235
+ "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
217
236
  }
218
237
  return kind === "zip"
219
238
  ? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AAqJA,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
1
+ {"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AA8LA,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
@@ -4,11 +4,14 @@ import { pipeline } from "node:stream/promises";
4
4
  import { createGunzip } from "node:zlib";
5
5
  import { ArchiveFormatError } from "./archive-errors.js";
6
6
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
7
+ import { parseLocalPax, paxMemberSize } from "./archive-tar-pax.js";
7
8
  class TarMetadataMeter extends Transform {
8
9
  maxMetaEntryBytes;
9
10
  block = Buffer.alloc(512);
10
11
  blockLength = 0;
11
12
  state = { kind: "header" };
13
+ pendingPax;
14
+ pendingGnu = false;
12
15
  constructor(maxMetaEntryBytes) {
13
16
  super();
14
17
  this.maxMetaEntryBytes = maxMetaEntryBytes;
@@ -32,7 +35,10 @@ class TarMetadataMeter extends Transform {
32
35
  return Number(value);
33
36
  }
34
37
  const zero = field.indexOf(0);
35
- const text = field.subarray(0, zero < 0 ? field.length : zero).toString("ascii").trim();
38
+ const bytes = field.subarray(0, zero < 0 ? field.length : zero);
39
+ if (bytes.some((byte) => byte > 0x7f))
40
+ throw this.invalid("size is not ASCII octal");
41
+ const text = bytes.toString("ascii").trim();
36
42
  if (!text || !/^[0-7]+$/.test(text))
37
43
  throw this.invalid("size is not valid octal");
38
44
  const value = Number.parseInt(text, 8);
@@ -42,6 +48,9 @@ class TarMetadataMeter extends Transform {
42
48
  }
43
49
  finishHeader() {
44
50
  if (this.block.every((byte) => byte === 0)) {
51
+ if (this.pendingPax)
52
+ throw this.invalid("dangling PAX metadata");
53
+ this.pendingGnu = false;
45
54
  this.blockLength = 0;
46
55
  this.state = { kind: "header" };
47
56
  return;
@@ -54,13 +63,37 @@ class TarMetadataMeter extends Transform {
54
63
  if (this.block[156] !== 0x35 && name.at(-1) === 0x2f) {
55
64
  throw this.invalid("non-directory entry path ends with a separator");
56
65
  }
57
- const size = this.parseSize();
66
+ let size = this.parseSize();
58
67
  const type = this.block[156];
59
- if ([0x78, 0x67, 0x4c, 0x4b, 0x58].includes(type ?? -1) && size > this.maxMetaEntryBytes) {
68
+ if ([0x78, 0x67, 0x4c, 0x4b, 0x58, 0x4e].includes(type) && size > this.maxMetaEntryBytes) {
60
69
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
61
70
  }
62
- if (type === 0x78 || type === 0x67 || type === 0x58) {
63
- throw this.invalid("PAX metadata is unmeterable without interpreting content");
71
+ if (type === 0x67 || type === 0x58 || type === 0x4e) {
72
+ throw this.invalid("global/old PAX and old GNU metadata are not supported");
73
+ }
74
+ if (type === 0x78) {
75
+ if (this.pendingPax || this.pendingGnu || size === 0)
76
+ throw this.invalid("empty, repeated or mixed PAX metadata");
77
+ const magic = this.block.subarray(257, 265).toString("latin1");
78
+ if (magic !== "ustar\0" + "00" && magic !== "ustar \0")
79
+ throw this.invalid("unrecognized PAX header format");
80
+ const padded = Math.ceil(size / 512) * 512;
81
+ if (!Number.isSafeInteger(padded))
82
+ throw this.invalid("entry padding exceeds the safe integer range");
83
+ this.state = { kind: "pax", body: Buffer.alloc(size), used: 0, padding: padded - size };
84
+ this.blockLength = 0;
85
+ return;
86
+ }
87
+ // Sparse headers retain their metadata-limit-before-format-error ordering.
88
+ if (this.pendingPax && type !== 0x53) {
89
+ size = paxMemberSize(this.pendingPax, type, size, this.block);
90
+ this.pendingPax = undefined;
91
+ }
92
+ if (type === 0x4c || type === 0x4b) {
93
+ this.pendingGnu = true;
94
+ }
95
+ else {
96
+ this.pendingGnu = false;
64
97
  }
65
98
  const padded = Math.ceil(size / 512) * 512;
66
99
  if (!Number.isSafeInteger(padded))
@@ -98,6 +131,18 @@ class TarMetadataMeter extends Transform {
98
131
  meter(chunk) {
99
132
  let offset = 0;
100
133
  while (offset < chunk.length) {
134
+ if (this.state.kind === "pax") {
135
+ const state = this.state;
136
+ const take = Math.min(state.body.length - state.used, chunk.length - offset);
137
+ chunk.copy(state.body, state.used, offset, offset + take);
138
+ state.used += take;
139
+ offset += take;
140
+ if (state.used === state.body.length) {
141
+ this.pendingPax = parseLocalPax(state.body);
142
+ this.state = state.padding === 0 ? { kind: "header" } : { kind: "data", remaining: state.padding };
143
+ }
144
+ continue;
145
+ }
101
146
  if (this.state.kind === "data") {
102
147
  const take = Math.min(this.state.remaining, chunk.length - offset);
103
148
  offset += take;
@@ -127,7 +172,9 @@ class TarMetadataMeter extends Transform {
127
172
  }
128
173
  }
129
174
  _flush(callback) {
130
- if (this.state.kind === "header" && this.blockLength === 0)
175
+ if (this.pendingPax)
176
+ callback(this.invalid("dangling PAX metadata"));
177
+ else if (this.state.kind === "header" && this.blockLength === 0)
131
178
  callback();
132
179
  else
133
180
  callback(this.invalid(this.state.kind === "header" ? "truncated TAR header" : "truncated TAR entry"));
@@ -0,0 +1,8 @@
1
+ export type LocalPax = {
2
+ path?: string;
3
+ linkpath?: string;
4
+ size?: number;
5
+ };
6
+ export declare function parseLocalPax(body: Buffer): LocalPax;
7
+ export declare function paxMemberSize(pax: LocalPax, type: number, rawSize: number, header: Buffer): number;
8
+ //# sourceMappingURL=archive-tar-pax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-tar-pax.d.ts","sourceRoot":"","sources":["../src/archive-tar-pax.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAqB3E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAwCpD;AAQD,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAclG"}
@@ -0,0 +1,100 @@
1
+ import { ArchiveFormatError } from "./archive-errors.js";
2
+ function invalid() {
3
+ throw new ArchiveFormatError("invalid TAR header: unsupported or malformed PAX metadata");
4
+ }
5
+ function decimal(bytes) {
6
+ const text = bytes.toString("latin1");
7
+ if (!/^(0|[1-9][0-9]*)$/.test(text))
8
+ invalid();
9
+ const value = Number(text);
10
+ if (!Number.isSafeInteger(value))
11
+ invalid();
12
+ return value;
13
+ }
14
+ function ascii(bytes) {
15
+ if (bytes.length === 0 || bytes.some((byte) => byte < 0x20 || byte > 0x7e))
16
+ invalid();
17
+ return bytes.toString("ascii");
18
+ }
19
+ // Keep this grammar aligned with native/src/tar_pax.rs. Downstream parsers
20
+ // disagree on duplicates, coercions and UTF-8 chunk boundaries.
21
+ export function parseLocalPax(body) {
22
+ if (body.length === 0)
23
+ invalid();
24
+ const result = {};
25
+ const keys = new Set();
26
+ let offset = 0;
27
+ while (offset < body.length) {
28
+ const space = body.indexOf(0x20, offset);
29
+ if (space < 0)
30
+ invalid();
31
+ const length = decimal(body.subarray(offset, space));
32
+ if (length > body.length - offset || length <= space - offset + 3)
33
+ invalid();
34
+ const end = offset + length;
35
+ if (body[end - 1] !== 0x0a)
36
+ invalid();
37
+ const record = body.subarray(space + 1, end - 1);
38
+ if (record.includes(0x0a))
39
+ invalid();
40
+ const equals = record.indexOf(0x3d);
41
+ if (equals <= 0)
42
+ invalid();
43
+ const key = ascii(record.subarray(0, equals));
44
+ if (!/^[A-Za-z0-9_.-]+$/.test(key) || keys.has(key))
45
+ invalid();
46
+ keys.add(key);
47
+ const value = record.subarray(equals + 1);
48
+ if (/^(LIBARCHIVE|SCHILY)\.xattr\..+$/.test(key)) {
49
+ // Inert bytes, never restored. Newlines would corrupt Rust's subsequent
50
+ // record lookup; NUL and non-UTF8 bytes in these values are harmless.
51
+ }
52
+ else if (key === "path" || key === "linkpath") {
53
+ result[key] = ascii(value);
54
+ }
55
+ else if (key === "size") {
56
+ result.size = decimal(value);
57
+ }
58
+ else if (key === "uid" || key === "gid") {
59
+ decimal(value);
60
+ }
61
+ else if (key === "uname" || key === "gname") {
62
+ ascii(value);
63
+ }
64
+ else if (key === "mtime" || key === "atime" || key === "ctime") {
65
+ const text = ascii(value);
66
+ if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(text) || Math.abs(Number(text)) > 8_640_000_000_000)
67
+ invalid();
68
+ }
69
+ else {
70
+ invalid();
71
+ }
72
+ offset = end;
73
+ }
74
+ return result;
75
+ }
76
+ function rawText(field) {
77
+ const zero = field.indexOf(0);
78
+ const value = field.subarray(0, zero < 0 ? field.length : zero);
79
+ return value.length === 0 ? "" : ascii(value);
80
+ }
81
+ export function paxMemberSize(pax, type, rawSize, header) {
82
+ if (![0, 0x30, 0x31, 0x32, 0x35, 0x37].includes(type))
83
+ invalid();
84
+ const rawName = rawText(header.subarray(0, 100));
85
+ const rawLink = rawText(header.subarray(157, 257));
86
+ if (header.subarray(257, 265).equals(Buffer.from("ustar\0" + "00"))) {
87
+ rawText(header.subarray(345, 500));
88
+ }
89
+ const isLink = type === 0x31 || type === 0x32;
90
+ if (isLink !== (rawLink.length > 0))
91
+ invalid();
92
+ const size = pax.size ?? rawSize;
93
+ if ([0x31, 0x32, 0x35].includes(type) && (rawSize !== 0 || size !== 0))
94
+ invalid();
95
+ if (type !== 0x35 && (pax.path?.endsWith("/") || pax.path?.endsWith("\\") || rawName.endsWith("\\")))
96
+ invalid();
97
+ if (pax.linkpath !== undefined && !isLink)
98
+ invalid();
99
+ return size;
100
+ }
@@ -25,6 +25,7 @@ export type TarModule = {
25
25
  preservePaths: false;
26
26
  noChmod: true;
27
27
  preserveOwner: false;
28
+ noMtime: true;
28
29
  strict: true;
29
30
  maxMetaEntrySize: number;
30
31
  filter?(this: TarParser, entryPath: string, entry: unknown): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
1
+ {"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
@@ -0,0 +1,7 @@
1
+ import type { ExtractionDeadline } from "./archive-deadline.js";
2
+ import { type ResolvedArchiveExtractLimits } from "./archive-limits.js";
3
+ /** Admit physical records before a decoder can normalize or collapse names. */
4
+ export declare function admitZipBuffer(input: Uint8Array, limits: ResolvedArchiveExtractLimits): number;
5
+ /** The extraction input is already private and staged; read only bounded metadata. */
6
+ export declare function admitZipFile(archivePath: string, limits: ResolvedArchiveExtractLimits, deadline: ExtractionDeadline): Promise<number>;
7
+ //# sourceMappingURL=archive-zip-admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-zip-admission.d.ts","sourceRoot":"","sources":["../src/archive-zip-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAA+C,KAAK,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrH,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+B3I"}
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
4
+ import { openRootFile } from "./root-file.js";
5
+ import { scanZipDirectory } from "./archive-zip-directory.js";
6
+ import { zipFormat } from "./archive-zip-names.js";
7
+ function checkSize(size, limits) {
8
+ if (size > limits.maxArchiveBytes) {
9
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
10
+ }
11
+ if (!Number.isSafeInteger(size) || size < 0)
12
+ zipFormat("invalid archive size");
13
+ }
14
+ /** Admit physical records before a decoder can normalize or collapse names. */
15
+ export function admitZipBuffer(input, limits) {
16
+ checkSize(input.byteLength, limits);
17
+ const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
18
+ const scan = scanZipDirectory(buffer.length, limits);
19
+ let step = scan.next();
20
+ while (!step.done) {
21
+ const { offset, length } = step.value;
22
+ step = scan.next(buffer.subarray(offset, offset + length));
23
+ }
24
+ return step.value;
25
+ }
26
+ /** The extraction input is already private and staged; read only bounded metadata. */
27
+ export async function admitZipFile(archivePath, limits, deadline) {
28
+ deadline.check();
29
+ const opened = await openRootFile({
30
+ absolutePath: archivePath, rootPath: path.dirname(archivePath),
31
+ boundaryLabel: "staged ZIP archive", rejectSymlinks: true, rejectHardlinks: true,
32
+ });
33
+ if (!opened.ok)
34
+ throw opened.error ?? new Error("cannot open staged ZIP archive");
35
+ try {
36
+ checkSize(opened.stat.size, limits);
37
+ const scan = scanZipDirectory(opened.stat.size, limits);
38
+ let step = scan.next();
39
+ while (!step.done) {
40
+ const { offset, length } = step.value;
41
+ const bytes = Buffer.alloc(length);
42
+ let received = 0;
43
+ while (received < length) {
44
+ deadline.check();
45
+ const count = await new Promise((resolve, reject) => {
46
+ fs.read(opened.fd, bytes, received, length - received, offset + received, (error, count) => error ? reject(error) : resolve(count));
47
+ });
48
+ deadline.check();
49
+ if (!count)
50
+ zipFormat("truncated staged record");
51
+ received += count;
52
+ }
53
+ step = scan.next(bytes);
54
+ }
55
+ return step.value;
56
+ }
57
+ finally {
58
+ fs.closeSync(opened.fd);
59
+ }
60
+ }