@openclaw/fs-safe 0.10.0 → 0.11.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 (259) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +1 -0
  3. package/README.md +36 -5
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +4 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +4 -0
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +4 -5
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +17 -9
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +13 -8
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +16 -9
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/directory-durability.d.ts.map +1 -1
  58. package/dist/directory-durability.js +5 -4
  59. package/dist/directory-guard.d.ts +11 -1
  60. package/dist/directory-guard.d.ts.map +1 -1
  61. package/dist/directory-guard.js +53 -11
  62. package/dist/durability.d.ts +1 -1
  63. package/dist/durability.d.ts.map +1 -1
  64. package/dist/durability.js +1 -1
  65. package/dist/file-handle-transfer.d.ts +14 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -0
  67. package/dist/file-handle-transfer.js +64 -0
  68. package/dist/file-hash.d.ts +3 -0
  69. package/dist/file-hash.d.ts.map +1 -1
  70. package/dist/file-hash.js +91 -31
  71. package/dist/file-lock-sync.d.ts.map +1 -1
  72. package/dist/file-lock-sync.js +8 -4
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +7 -5
  75. package/dist/file-store-path.d.ts +3 -0
  76. package/dist/file-store-path.d.ts.map +1 -0
  77. package/dist/file-store-path.js +27 -0
  78. package/dist/file-store-prune.d.ts.map +1 -1
  79. package/dist/file-store-prune.js +6 -4
  80. package/dist/file-store-sync-write.d.ts.map +1 -1
  81. package/dist/file-store-sync-write.js +56 -44
  82. package/dist/file-store.d.ts.map +1 -1
  83. package/dist/file-store.js +2 -18
  84. package/dist/filename.d.ts.map +1 -1
  85. package/dist/filename.js +2 -1
  86. package/dist/guarded-mkdir.d.ts.map +1 -1
  87. package/dist/guarded-mkdir.js +3 -2
  88. package/dist/guest-dispatch-python.d.ts +2 -0
  89. package/dist/guest-dispatch-python.d.ts.map +1 -0
  90. package/dist/guest-dispatch-python.js +117 -0
  91. package/dist/guest-native-python.d.ts +4 -0
  92. package/dist/guest-native-python.d.ts.map +1 -0
  93. package/dist/guest-native-python.js +135 -0
  94. package/dist/guest.d.ts +9 -0
  95. package/dist/guest.d.ts.map +1 -0
  96. package/dist/guest.js +413 -0
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/install-path.d.ts.map +1 -1
  100. package/dist/install-path.js +3 -2
  101. package/dist/json-durable-queue-directory.js +3 -3
  102. package/dist/json-durable-queue.d.ts.map +1 -1
  103. package/dist/json-durable-queue.js +6 -4
  104. package/dist/json.d.ts.map +1 -1
  105. package/dist/json.js +2 -1
  106. package/dist/local-roots.d.ts.map +1 -1
  107. package/dist/local-roots.js +2 -1
  108. package/dist/move-path-stage.d.ts.map +1 -1
  109. package/dist/move-path-stage.js +2 -1
  110. package/dist/move-path.d.ts.map +1 -1
  111. package/dist/move-path.js +4 -3
  112. package/dist/mutation-authority.d.ts +1 -0
  113. package/dist/mutation-authority.d.ts.map +1 -1
  114. package/dist/mutation-authority.js +4 -4
  115. package/dist/native-binding.d.ts +7 -2
  116. package/dist/native-binding.d.ts.map +1 -1
  117. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  118. package/dist/native-pinned-write-windows.js +3 -2
  119. package/dist/native-pinned-write.d.ts.map +1 -1
  120. package/dist/native-pinned-write.js +2 -1
  121. package/dist/opened-realpath.d.ts.map +1 -1
  122. package/dist/opened-realpath.js +5 -4
  123. package/dist/output.d.ts +2 -0
  124. package/dist/output.d.ts.map +1 -1
  125. package/dist/output.js +2 -0
  126. package/dist/overwrite-file-handle.d.ts +8 -0
  127. package/dist/overwrite-file-handle.d.ts.map +1 -0
  128. package/dist/overwrite-file-handle.js +42 -0
  129. package/dist/path-case.d.ts +7 -0
  130. package/dist/path-case.d.ts.map +1 -0
  131. package/dist/path-case.js +136 -0
  132. package/dist/path.d.ts.map +1 -1
  133. package/dist/path.js +2 -1
  134. package/dist/permissions-windows.d.ts +1 -1
  135. package/dist/permissions-windows.d.ts.map +1 -1
  136. package/dist/permissions-windows.js +48 -6
  137. package/dist/pinned-open.d.ts.map +1 -1
  138. package/dist/pinned-open.js +3 -1
  139. package/dist/pinned-write.d.ts +2 -2
  140. package/dist/pinned-write.d.ts.map +1 -1
  141. package/dist/pinned-write.js +2 -1
  142. package/dist/private-temp-workspace.d.ts.map +1 -1
  143. package/dist/private-temp-workspace.js +6 -4
  144. package/dist/realpath.d.ts +4 -0
  145. package/dist/realpath.d.ts.map +1 -0
  146. package/dist/realpath.js +43 -0
  147. package/dist/recursive-mkdir-path.d.ts +3 -0
  148. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  149. package/dist/recursive-mkdir-path.js +8 -0
  150. package/dist/replace-directory.d.ts.map +1 -1
  151. package/dist/replace-directory.js +2 -1
  152. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  153. package/dist/replace-file-copy-fallback.js +23 -31
  154. package/dist/replace-file-copy-source.d.ts.map +1 -1
  155. package/dist/replace-file-copy-source.js +7 -12
  156. package/dist/replace-file-mode.d.ts +3 -0
  157. package/dist/replace-file-mode.d.ts.map +1 -0
  158. package/dist/replace-file-mode.js +10 -0
  159. package/dist/replace-file.d.ts +1 -0
  160. package/dist/replace-file.d.ts.map +1 -1
  161. package/dist/replace-file.js +14 -8
  162. package/dist/root-context.d.ts.map +1 -1
  163. package/dist/root-context.js +8 -8
  164. package/dist/root-create-input.d.ts +10 -0
  165. package/dist/root-create-input.d.ts.map +1 -0
  166. package/dist/root-create-input.js +80 -0
  167. package/dist/root-directory-list.d.ts +3 -1
  168. package/dist/root-directory-list.d.ts.map +1 -1
  169. package/dist/root-directory-list.js +21 -3
  170. package/dist/root-entries.d.ts +11 -0
  171. package/dist/root-entries.d.ts.map +1 -0
  172. package/dist/root-entries.js +61 -0
  173. package/dist/root-errors.d.ts +5 -5
  174. package/dist/root-errors.d.ts.map +1 -1
  175. package/dist/root-errors.js +13 -12
  176. package/dist/root-impl.d.ts +13 -3
  177. package/dist/root-impl.d.ts.map +1 -1
  178. package/dist/root-impl.js +40 -36
  179. package/dist/root-options.d.ts +12 -1
  180. package/dist/root-options.d.ts.map +1 -1
  181. package/dist/root-path-existing.d.ts.map +1 -1
  182. package/dist/root-path-existing.js +4 -3
  183. package/dist/root-path-symlink.d.ts.map +1 -1
  184. package/dist/root-path-symlink.js +3 -2
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +13 -9
  187. package/dist/root-remove.d.ts +5 -0
  188. package/dist/root-remove.d.ts.map +1 -0
  189. package/dist/root-remove.js +286 -0
  190. package/dist/root-symlink-policy.d.ts +2 -1
  191. package/dist/root-symlink-policy.d.ts.map +1 -1
  192. package/dist/root-symlink-policy.js +2 -2
  193. package/dist/root-write-mode.d.ts.map +1 -1
  194. package/dist/root-write-mode.js +2 -1
  195. package/dist/root.d.ts +2 -1
  196. package/dist/root.d.ts.map +1 -1
  197. package/dist/secret-file.d.ts.map +1 -1
  198. package/dist/secret-file.js +2 -1
  199. package/dist/secret-read-async.d.ts.map +1 -1
  200. package/dist/secret-read-async.js +2 -1
  201. package/dist/secure-file.d.ts.map +1 -1
  202. package/dist/secure-file.js +21 -4
  203. package/dist/secure-temp-dir.d.ts.map +1 -1
  204. package/dist/secure-temp-dir.js +2 -1
  205. package/dist/sibling-staged-file.d.ts +1 -0
  206. package/dist/sibling-staged-file.d.ts.map +1 -1
  207. package/dist/sibling-staged-file.js +42 -8
  208. package/dist/sibling-temp.d.ts +2 -0
  209. package/dist/sibling-temp.d.ts.map +1 -1
  210. package/dist/sibling-temp.js +6 -4
  211. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  212. package/dist/sidecar-lock-acquire.js +15 -4
  213. package/dist/sidecar-lock-policy.d.ts +2 -0
  214. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  215. package/dist/sidecar-lock-policy.js +17 -0
  216. package/dist/staged-directory.d.ts.map +1 -1
  217. package/dist/staged-directory.js +4 -3
  218. package/dist/temp-target.d.ts +14 -12
  219. package/dist/temp-target.d.ts.map +1 -1
  220. package/dist/temp-target.js +12 -6
  221. package/dist/trash.d.ts.map +1 -1
  222. package/dist/trash.js +7 -5
  223. package/dist/unicode-path.d.ts +3 -0
  224. package/dist/unicode-path.d.ts.map +1 -0
  225. package/dist/unicode-path.js +13 -0
  226. package/dist/walk.d.ts.map +1 -1
  227. package/dist/walk.js +3 -2
  228. package/dist/write-file-handle.d.ts +1 -0
  229. package/dist/write-file-handle.d.ts.map +1 -1
  230. package/dist/write-file-handle.js +3 -2
  231. package/docs/advanced.md +4 -0
  232. package/docs/archive.md +25 -5
  233. package/docs/atomic.md +17 -1
  234. package/docs/config.md +1 -0
  235. package/docs/contributing.md +29 -1
  236. package/docs/copy.md +75 -6
  237. package/docs/directory-identity.md +85 -0
  238. package/docs/durability.md +36 -1
  239. package/docs/entries.md +109 -0
  240. package/docs/errors.md +3 -3
  241. package/docs/file-store.md +15 -0
  242. package/docs/guest.md +141 -0
  243. package/docs/in-place-write.md +81 -0
  244. package/docs/index.md +2 -0
  245. package/docs/install.md +31 -0
  246. package/docs/native-helper.md +10 -3
  247. package/docs/native.md +4 -0
  248. package/docs/output.md +32 -6
  249. package/docs/path-case.md +64 -0
  250. package/docs/path-scope.md +1 -1
  251. package/docs/permissions.md +11 -2
  252. package/docs/public-api.md +31 -2
  253. package/docs/root.md +30 -2
  254. package/docs/secure-file.md +2 -0
  255. package/docs/sidecar-lock.md +12 -3
  256. package/docs/temp.md +35 -6
  257. package/docs/types.md +1 -1
  258. package/docs/writing.md +153 -3
  259. package/package.json +14 -8
@@ -8,12 +8,14 @@ import { formatErrorDetail } from "./error-detail.js";
8
8
  import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
9
9
  import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
10
10
  import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
11
+ import { isGzipBuffer } from "./archive-gzip-tail.js";
11
12
  import { inspectTar, replayTar } from "./archive-tar-stream.js";
12
- import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
13
+ import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
13
14
  import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
14
15
  import { FsSafeError } from "./errors.js";
15
16
  import { inspectFileIdentity } from "./strict-file-identity.js";
16
17
  import { resolveReadOpenFlags } from "./read-open-flags.js";
18
+ import { realpathSync } from "./realpath.js";
17
19
  import { getNativeBinding } from "./native.js";
18
20
  import { admitZipBuffer } from "./archive-zip-admission.js";
19
21
  import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
@@ -63,7 +65,7 @@ async function readStreamBounded(stream, maxBytes) {
63
65
  return Buffer.concat(chunks, total);
64
66
  }
65
67
  async function readArchiveInput(archivePath) {
66
- const resolved = fsSync.realpathSync.native(archivePath);
68
+ const resolved = realpathSync.native(archivePath);
67
69
  const before = await inspectFileIdentity(async () => {
68
70
  const stat = fsSync.lstatSync(archivePath, { bigint: true });
69
71
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -102,12 +104,8 @@ async function readArchiveInput(archivePath) {
102
104
  await handle.close().catch(() => undefined);
103
105
  }
104
106
  }
105
- async function readZipEntry(buffer, entryPath, maxBytes) {
106
- const archive = await loadZipArchiveWithPreflight(buffer, {
107
- maxArchiveBytes: DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
108
- maxEntryBytes: maxBytes,
109
- maxExtractedBytes: maxBytes,
110
- });
107
+ async function readZipEntry(buffer, entryPath, maxBytes, physicalCount) {
108
+ const archive = await loadAdmittedZipArchive(buffer, physicalCount);
111
109
  let entry;
112
110
  // JSZip keys retain some aliases and may use Unicode Path metadata. Scan the
113
111
  // effective entries once, after raw ZIP admission has rejected collisions.
@@ -153,6 +151,16 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
153
151
  }
154
152
  if (selected.size > maxBytes)
155
153
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
154
+ if (!isGzipBuffer(archiveBuffer)) {
155
+ // Complete admission already validated this private snapshot through EOF.
156
+ // Copy the admitted payload so the result cannot expose or mutate its input.
157
+ const end = selected.offset + selected.size;
158
+ if (!Number.isSafeInteger(selected.offset) || selected.offset < 0 ||
159
+ !Number.isSafeInteger(end) || end < selected.offset || end > archiveBuffer.length) {
160
+ throw new ArchiveFormatError("invalid admitted TAR range");
161
+ }
162
+ return Buffer.from(archiveBuffer.subarray(selected.offset, end));
163
+ }
156
164
  let result;
157
165
  await replayTar({ archiveBuffer, limits, members: [selected], async consume(_member, payload) {
158
166
  result = await readStreamBounded(payload, maxBytes);
@@ -222,6 +230,6 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
222
230
  if (native)
223
231
  return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, physicalCount);
224
232
  assertPortableArchiveKind(kind);
225
- return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes)
233
+ return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, physicalCount)
226
234
  : await readTarEntry(buffer, requestedEntry, options.maxBytes);
227
235
  }
@@ -1,9 +1,12 @@
1
+ import { type BigIntStats } from "node:fs";
1
2
  import { type AsyncDirectoryGuard } from "./directory-guard.js";
2
3
  import { type ExtractionDeadline } from "./archive-deadline.js";
3
4
  import { ArchiveSecurityError } from "./archive-errors.js";
5
+ export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
4
6
  export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
5
- export declare function createDirectoryIdentityGuard(dir: string): Promise<AsyncDirectoryGuard>;
6
- export declare function assertDirectoryIdentityGuard(guard: AsyncDirectoryGuard): Promise<void>;
7
+ export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
8
+ export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): Promise<void>;
9
+ export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
7
10
  export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
8
11
  export declare function assertResolvedInsideDestination(params: {
9
12
  destinationRealDir: string;
@@ -20,7 +23,7 @@ type ArchiveOutputPathParams = {
20
23
  deadline?: ExtractionDeadline;
21
24
  };
22
25
  export declare function prepareArchiveOutputPath(params: ArchiveOutputPathParams): Promise<void>;
23
- export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?: AsyncDirectoryGuard): Promise<void>;
26
+ export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?: ArchiveDirectoryGuard): Promise<void>;
24
27
  export declare function withStagedArchiveDestination<T>(params: {
25
28
  destinationRealDir: string;
26
29
  stagingDirPrefix?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAkB7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS1F,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAS5F;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5F;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA0BD,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB;AAiBD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7F;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,uBAAuB,EAAE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EACnE,gBAAgB,CAAC,EAAE,mBAAmB,GACrC,OAAO,CAAC,IAAI,CAAC,CAEf;AAkHD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,OAAO,EAAE,iCAAiC,EAAE,MAAM,oBAAoB,CAAC;AAEvE,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
1
+ {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAgB7B,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;AAMrE,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS1F,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAS9F;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY9F;AAED,wBAAsB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyCpG;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnF;AA0BD,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB;AAiBD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7F;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,uBAAuB,EAAE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EACnE,gBAAgB,CAAC,EAAE,qBAAqB,GACvC,OAAO,CAAC,IAAI,CAAC,CAEf;AAkHD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,OAAO,EAAE,iCAAiC,EAAE,MAAM,oBAAoB,CAAC;AAEvE,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
@@ -1,4 +1,4 @@
1
- import fsSync from "node:fs";
1
+ import fsSync, {} from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
@@ -13,6 +13,8 @@ import { getFsSafeTestHooks } from "./test-hooks.js";
13
13
  import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
14
14
  import { expandRelativePathWithHome } from "./root-context.js";
15
15
  import { resolveRootPath } from "./root-path.js";
16
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
17
+ import { realpathSync } from "./realpath.js";
16
18
  const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
17
19
  const ARCHIVE_STAGING_MODE = 0o700;
18
20
  function checkExtractionDeadline(deadline) {
@@ -24,7 +26,7 @@ function symlinkTraversalError(originalPath) {
24
26
  }
25
27
  export async function createDirectoryIdentityGuard(dir) {
26
28
  try {
27
- return await createAsyncDirectoryGuard(dir);
29
+ return await createAsyncDirectoryGuard(dir, { bigint: true });
28
30
  }
29
31
  catch (err) {
30
32
  if (err instanceof FsSafeError && err.code === "not-file") {
@@ -37,33 +39,51 @@ export async function assertDirectoryIdentityGuard(guard) {
37
39
  try {
38
40
  await assertAsyncDirectoryGuard(guard);
39
41
  }
42
+ catch (err) {
43
+ if (err instanceof FsSafeError || isNotFoundPathError(err)) {
44
+ throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
45
+ }
46
+ throw err;
47
+ }
48
+ }
49
+ export async function prepareArchiveDestinationGuard(destDir) {
50
+ let stat;
51
+ try {
52
+ stat = inspectFileIdentitySync(() => {
53
+ const observed = fsSync.lstatSync(destDir, { bigint: true });
54
+ if (observed.isSymbolicLink()) {
55
+ throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
56
+ }
57
+ if (!observed.isDirectory()) {
58
+ throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
59
+ }
60
+ return observed;
61
+ });
62
+ }
63
+ catch (err) {
64
+ if (err instanceof ArchiveSecurityError)
65
+ throw err;
66
+ if (err instanceof FsSafeError) {
67
+ throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
68
+ }
69
+ throw err;
70
+ }
71
+ const realPath = realpathSync.native(destDir);
72
+ const guard = { dir: destDir, realPath, stat };
73
+ try {
74
+ inspectFileIdentitySync(() => fsSync.statSync(realPath, { bigint: true }), stat);
75
+ inspectFileIdentitySync(() => fsSync.lstatSync(destDir, { bigint: true }), stat);
76
+ }
40
77
  catch (err) {
41
78
  if (err instanceof FsSafeError) {
42
79
  throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
43
80
  }
44
81
  throw err;
45
82
  }
83
+ return guard;
46
84
  }
47
85
  export async function prepareArchiveDestinationDir(destDir) {
48
- const stat = fsSync.lstatSync(destDir);
49
- if (stat.isSymbolicLink()) {
50
- throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
51
- }
52
- if (!stat.isDirectory()) {
53
- throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
54
- }
55
- const realPath = fsSync.realpathSync.native(destDir);
56
- const realStat = fsSync.statSync(realPath);
57
- const postStat = fsSync.lstatSync(destDir);
58
- if (realStat.dev !== stat.dev ||
59
- realStat.ino !== stat.ino ||
60
- postStat.isSymbolicLink() ||
61
- !postStat.isDirectory() ||
62
- postStat.dev !== stat.dev ||
63
- postStat.ino !== stat.ino) {
64
- throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
65
- }
66
- return realPath;
86
+ return (await prepareArchiveDestinationGuard(destDir)).realPath;
67
87
  }
68
88
  async function assertNoSymlinkTraversal(params) {
69
89
  const parts = params.relPath.split(/[\\/]+/).filter(Boolean);
@@ -88,7 +108,7 @@ async function assertNoSymlinkTraversal(params) {
88
108
  export async function assertResolvedInsideDestination(params) {
89
109
  let resolved;
90
110
  try {
91
- resolved = fsSync.realpathSync.native(params.targetPath);
111
+ resolved = realpathSync.native(params.targetPath);
92
112
  }
93
113
  catch (err) {
94
114
  if (isNotFoundPathError(err)) {
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAEhF,gFAAgF;AAChF,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAuDhH,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;IAClD,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;2DAC2D;AAC3D,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,QAAQ,GAAG;IAC9E,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
1
+ {"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGhF,gFAAgF;AAChF,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAuDhH,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;IAClD,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;2DAC2D;AAC3D,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,QAAQ,GAAG;IAC9E,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
@@ -2,9 +2,10 @@ import fs from "node:fs";
2
2
  import { Readable, Writable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import { createGunzip } from "node:zlib";
5
- import { GzipInput, validateGzipBufferTail, validateGzipContainerTail } from "./archive-gzip-tail.js";
5
+ import { GzipInput, isGzipBuffer, validateGzipBufferTail, validateGzipContainerTail } from "./archive-gzip-tail.js";
6
6
  import { ArchiveFormatError } from "./archive-errors.js";
7
7
  import { TarParserStream } from "./archive-tar-wasm.js";
8
+ import { readFileWindowFully } from "./positional-read.js";
8
9
  function* bufferChunks(buffer) {
9
10
  for (let offset = 0; offset < buffer.length; offset += 65536)
10
11
  yield buffer.subarray(offset, offset + 65536);
@@ -13,8 +14,8 @@ async function gzipFile(filePath) {
13
14
  const handle = await fs.promises.open(filePath, "r");
14
15
  try {
15
16
  const magic = Buffer.alloc(2);
16
- const { bytesRead } = await handle.read(magic, 0, 2, 0);
17
- return bytesRead === 2 && magic[0] === 31 && magic[1] === 139;
17
+ const bytesRead = await readFileWindowFully(handle, magic, 0);
18
+ return bytesRead === 2 && isGzipBuffer(magic);
18
19
  }
19
20
  finally {
20
21
  await handle.close();
@@ -22,13 +23,13 @@ async function gzipFile(filePath) {
22
23
  }
23
24
  async function withTarStream(params, consume) {
24
25
  const buffer = params.archiveBuffer;
25
- const gzip = buffer !== undefined ? buffer[0] === 31 && buffer[1] === 139 : await gzipFile(params.archivePath);
26
+ const gzip = buffer !== undefined ? isGzipBuffer(buffer) : await gzipFile(params.archivePath);
26
27
  const parser = new TarParserStream(params.limits, params.onMember);
27
28
  const input = buffer !== undefined
28
29
  ? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
29
30
  : fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
30
- // Match the WASM input window for buffered reads instead of emitting 16 KiB chunks.
31
- const decoder = gzip ? createGunzip(buffer === undefined ? undefined : { chunkSize: 65536 }) : undefined;
31
+ // Match the WASM input window for both staged files and buffered reads.
32
+ const decoder = gzip ? createGunzip({ chunkSize: 65536 }) : undefined;
32
33
  const gzipInput = decoder ? new GzipInput(decoder) : undefined;
33
34
  const destroy = (error) => {
34
35
  input.destroy(error);
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyClE,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAER,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAD9D,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAQjG;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAkB/F;IACQ,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAKjD;IACQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAKpF;CACF"}
1
+ {"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyClE,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAER,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAD9D,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAQjG;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAqB/F;IACQ,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAKjD;IACQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAKpF;CACF"}
@@ -53,21 +53,24 @@ export class TarParserStream extends Transform {
53
53
  _transform(chunk, _encoding, callback) {
54
54
  try {
55
55
  const abi = this.abi;
56
- for (let offset = 0; offset < chunk.length;) {
56
+ for (let offset = 0; offset < chunk.length; offset += 65536) {
57
57
  const length = Math.min(65536, chunk.length - offset);
58
58
  this.bytes(abi.input_ptr(), length).set(chunk.subarray(offset, offset + length));
59
- const used = abi.push(length);
60
- if (used < 0)
61
- throw parserError(this.text());
62
- if (used === 0 || used > length)
63
- throw new ArchiveFormatError("TAR WASM made no progress");
64
- offset += used;
65
- const type = abi.member_type();
66
- if (type >= 0)
67
- this.onMember?.({
68
- path: this.text(), type: types.get(type) ?? "Unsupported", size: abi.member_size(),
69
- mode: abi.member_mode(), offset: abi.member_offset(),
70
- });
59
+ // A member event can stop before the end of this already-copied inbox.
60
+ for (let consumed = 0; consumed < length;) {
61
+ const used = abi.push(consumed, length - consumed);
62
+ if (used < 0)
63
+ throw parserError(this.text());
64
+ if (used === 0 || used > length - consumed)
65
+ throw new ArchiveFormatError("TAR WASM made no progress");
66
+ consumed += used;
67
+ const type = abi.member_type();
68
+ if (type >= 0)
69
+ this.onMember?.({
70
+ path: this.text(), type: types.get(type) ?? "Unsupported", size: abi.member_size(),
71
+ mode: abi.member_mode(), offset: abi.member_offset(),
72
+ });
73
+ }
71
74
  }
72
75
  callback(null, chunk);
73
76
  }
@@ -3,6 +3,6 @@ import { type ResolvedArchiveExtractLimits } from "./archive-limits.js";
3
3
  import { type ZipDirectoryEntry } from "./archive-zip-directory.js";
4
4
  /** Admit physical records before a decoder can normalize or collapse names. */
5
5
  export declare function admitZipBuffer(input: Uint8Array, limits: ResolvedArchiveExtractLimits): number;
6
- /** The extraction input is already private and staged; read only bounded metadata. */
6
+ /** The extraction input is private and immutable; read bounded metadata windows. */
7
7
  export declare function admitZipFile(archivePath: string, limits: ResolvedArchiveExtractLimits, deadline: ExtractionDeadline, onEntry?: (entry: ZipDirectoryEntry) => void): Promise<number>;
8
8
  //# sourceMappingURL=archive-zip-admission.d.ts.map
@@ -1 +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;AAErH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAUtF,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,EACvF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC3C,OAAO,CAAC,MAAM,CAAC,CA+BjB"}
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;AAErH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAUtF,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAMD,oFAAoF;AACpF,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,EACvF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC3C,OAAO,CAAC,MAAM,CAAC,CA8DjB"}
@@ -23,7 +23,9 @@ export function admitZipBuffer(input, limits) {
23
23
  }
24
24
  return step.value;
25
25
  }
26
- /** The extraction input is already private and staged; read only bounded metadata. */
26
+ const METADATA_WINDOW_BYTES = 4096;
27
+ const CACHED_READS_PER_YIELD = 32;
28
+ /** The extraction input is private and immutable; read bounded metadata windows. */
27
29
  export async function admitZipFile(archivePath, limits, deadline, onEntry) {
28
30
  deadline.check();
29
31
  const opened = await openRootFile({
@@ -35,20 +37,54 @@ export async function admitZipFile(archivePath, limits, deadline, onEntry) {
35
37
  try {
36
38
  checkSize(opened.stat.size, limits);
37
39
  const scan = scanZipDirectory(opened.stat.size, limits, onEntry);
40
+ // Separate windows keep alternating central and local records hot. Never
41
+ // refill a buffer: the scanner can retain its views after cache eviction.
42
+ const windows = [];
43
+ let cachedReads = 0;
38
44
  let step = scan.next();
39
45
  while (!step.done) {
46
+ deadline.check();
40
47
  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;
48
+ const cached = windows.findIndex(window => offset >= window.offset &&
49
+ length <= window.bytes.length - (offset - window.offset));
50
+ let bytes;
51
+ if (cached !== -1) {
52
+ if (++cachedReads >= CACHED_READS_PER_YIELD) {
53
+ cachedReads = 0;
54
+ await new Promise(resolve => setImmediate(resolve));
55
+ deadline.check();
56
+ }
57
+ const window = windows[cached];
58
+ if (cached === 1)
59
+ windows.reverse();
60
+ bytes = window.bytes.subarray(offset - window.offset, offset - window.offset + length);
61
+ }
62
+ else {
63
+ const cacheable = length > 0 && length <= METADATA_WINDOW_BYTES;
64
+ const capacity = cacheable ? Math.min(METADATA_WINDOW_BYTES, opened.stat.size - offset) : length;
65
+ const buffer = Buffer.alloc(capacity);
66
+ let received = 0;
67
+ while (received < capacity) {
68
+ deadline.check();
69
+ const count = await new Promise((resolve, reject) => {
70
+ fs.read(opened.fd, buffer, received, capacity - received, offset + received, (error, count) => error ? reject(error) : resolve(count));
71
+ });
72
+ cachedReads = 0;
73
+ deadline.check();
74
+ if (!count) {
75
+ if (received < length)
76
+ zipFormat("truncated staged record");
77
+ break;
78
+ }
79
+ received += count;
80
+ }
81
+ const filled = buffer.subarray(0, received);
82
+ bytes = filled.subarray(0, length);
83
+ if (cacheable) {
84
+ windows.unshift({ offset, bytes: filled });
85
+ if (windows.length > 2)
86
+ windows.pop();
87
+ }
52
88
  }
53
89
  step = scan.next(bytes);
54
90
  }
@@ -0,0 +1,6 @@
1
+ export type ZipArchiveWithFiles = {
2
+ files: Record<string, unknown>;
3
+ };
4
+ /** Internal: the caller has admitted these unchanged bytes and their physical count. */
5
+ export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, entryCount: number): Promise<ZipArchiveWithFiles>;
6
+ //# sourceMappingURL=archive-zip-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-zip-loader.d.ts","sourceRoot":"","sources":["../src/archive-zip-loader.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF,wFAAwF;AACxF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAoB9B"}
@@ -0,0 +1,38 @@
1
+ import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
+ import { validateArchiveEntryPath } from "./archive-entry.js";
3
+ /** Internal: the caller has admitted these unchanged bytes and their physical count. */
4
+ export async function loadAdmittedZipArchive(buffer, entryCount) {
5
+ const JSZip = await importOptionalJsZip();
6
+ let archive;
7
+ try {
8
+ archive = await JSZip.loadAsync(buffer);
9
+ }
10
+ catch (error) {
11
+ throw new ArchiveFormatError(`invalid ZIP archive: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
12
+ }
13
+ const names = Object.keys(archive.files);
14
+ if (names.length !== entryCount) {
15
+ throw new ArchiveSecurityError("entry-path", "zip archive contains duplicate or colliding entry names");
16
+ }
17
+ for (const name of names)
18
+ validateArchiveEntryPath(name);
19
+ return archive;
20
+ }
21
+ async function importOptionalJsZip() {
22
+ try {
23
+ const module = await import("jszip");
24
+ const candidate = typeof module === "function" ? module : module.default;
25
+ if ((typeof candidate !== "object" && typeof candidate !== "function") ||
26
+ candidate === null ||
27
+ typeof candidate.loadAsync !== "function") {
28
+ throw new Error('Optional archive dependency "jszip" does not expose loadAsync().');
29
+ }
30
+ return candidate;
31
+ }
32
+ catch (err) {
33
+ throw missingOptionalArchiveDependencyError("jszip", err);
34
+ }
35
+ }
36
+ function missingOptionalArchiveDependencyError(packageName, cause) {
37
+ return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use ZIP archive helpers from @openclaw/fs-safe/archive.`, { cause });
38
+ }
@@ -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;AA6BD,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,CA8BrB"}
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"}
@@ -31,9 +31,10 @@ export function zipExtraFields(bytes) {
31
31
  }
32
32
  return critical;
33
33
  }
34
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
34
35
  function utf8(bytes) {
35
36
  try {
36
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
37
+ return utf8Decoder.decode(bytes);
37
38
  }
38
39
  catch {
39
40
  return zipFormat("invalid UTF-8 name");
@@ -42,10 +43,10 @@ function utf8(bytes) {
42
43
  function key(name) {
43
44
  return stripArchivePath(name, 0) ?? "";
44
45
  }
45
- function originalName(name, flags) {
46
+ function originalName(name, rawName, flags) {
46
47
  // ASCII path syntax is encoding-independent. Do not reinterpret legacy bytes
47
48
  // as UTF-8 (native ZIP uses CP437); the selected decoder still validates its name.
48
- validateArchiveEntryPath(Array.from(name, (byte) => byte < 128 ? String.fromCharCode(byte) : "_").join(""));
49
+ validateArchiveEntryPath(rawName.replace(/[\x80-\xff]/g, "_"));
49
50
  if (!(flags & 0x800))
50
51
  return undefined;
51
52
  const decoded = utf8(name);
@@ -68,15 +69,19 @@ export function admitZipNames(params) {
68
69
  const { central, local, flags, centralExtra, localExtra, seen } = params;
69
70
  if (!central.length || !local.length)
70
71
  zipFormat("empty entry name");
71
- const centralUtf8 = originalName(central, flags);
72
- const localUtf8 = originalName(local, flags);
72
+ const centralRaw = central.toString("latin1");
73
+ const localRaw = local.toString("latin1");
74
+ const centralUtf8 = originalName(central, centralRaw, flags);
75
+ const localUtf8 = originalName(local, localRaw, flags);
73
76
  const centralUnicode = unicodeName(central, centralExtra);
74
77
  const localUnicode = unicodeName(local, localExtra);
75
- if (key(central.toString("latin1")) !== key(local.toString("latin1"))) {
78
+ const centralKey = key(centralRaw);
79
+ if (centralKey !== key(localRaw)) {
76
80
  zipFormat("central and local names disagree");
77
81
  }
78
82
  const interpretations = [centralUtf8, localUtf8, centralUnicode, localUnicode].filter((value) => value !== undefined);
79
- if (interpretations.some((value) => key(value) !== key(interpretations[0]))) {
83
+ const interpretationKey = interpretations.length ? key(interpretations[0]) : undefined;
84
+ if (interpretations.some((value) => key(value) !== interpretationKey)) {
80
85
  zipFormat("conflicting Unicode name interpretations");
81
86
  }
82
87
  // JSZip checks the central Unicode field against the local name. A slash-only
@@ -88,7 +93,7 @@ export function admitZipNames(params) {
88
93
  if (!centralUnicode && localUnicode && key(localUnicode) !== key(local.toString("utf8"))) {
89
94
  zipFormat("local-only Unicode override changes the name");
90
95
  }
91
- const identities = new Set([key(central.toString("latin1"))]);
96
+ const identities = new Set([centralKey]);
92
97
  if (centralUnicode !== undefined)
93
98
  identities.add(key(Buffer.from(centralUnicode).toString("latin1")));
94
99
  if ([...identities].some((identity) => seen.has(identity))) {
@@ -1,7 +1,6 @@
1
1
  import { type ArchiveExtractLimits } from "./archive-limits.js";
2
+ import { type ZipArchiveWithFiles } from "./archive-zip-loader.js";
2
3
  export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
3
- export type ZipArchiveWithFiles = {
4
- files: Record<string, unknown>;
5
- };
4
+ export type { ZipArchiveWithFiles } from "./archive-zip-loader.js";
6
5
  export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
7
6
  //# sourceMappingURL=archive-zip-preflight.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive-zip-preflight.d.ts","sourceRoot":"","sources":["../src/archive-zip-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,iCAAiC,EAAE,MAAM,wBAAwB,CAAC;AAE3E,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAwB9B"}
1
+ {"version":3,"file":"archive-zip-preflight.d.ts","sourceRoot":"","sources":["../src/archive-zip-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC3F,OAAO,EAAE,iCAAiC,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEnE,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAO9B"}
@@ -1,7 +1,6 @@
1
1
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, resolveExtractLimits, } from "./archive-limits.js";
2
- import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
3
2
  import { admitZipBuffer } from "./archive-zip-admission.js";
4
- import { validateArchiveEntryPath } from "./archive-entry.js";
3
+ import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
5
4
  export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
6
5
  export async function loadZipArchiveWithPreflight(buffer, limits) {
7
6
  const resolvedLimits = resolveExtractLimits(limits);
@@ -9,36 +8,5 @@ export async function loadZipArchiveWithPreflight(buffer, limits) {
9
8
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
10
9
  }
11
10
  const entryCount = admitZipBuffer(buffer, resolvedLimits);
12
- const JSZip = await importOptionalJsZip();
13
- let archive;
14
- try {
15
- archive = await JSZip.loadAsync(buffer);
16
- }
17
- catch (error) {
18
- throw new ArchiveFormatError(`invalid ZIP archive: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
19
- }
20
- if (Object.keys(archive.files).length !== entryCount) {
21
- throw new ArchiveSecurityError("entry-path", "zip archive contains duplicate or colliding entry names");
22
- }
23
- for (const name of Object.keys(archive.files))
24
- validateArchiveEntryPath(name);
25
- return archive;
26
- }
27
- async function importOptionalJsZip() {
28
- try {
29
- const module = await import("jszip");
30
- const candidate = typeof module === "function" ? module : module.default;
31
- if ((typeof candidate !== "object" && typeof candidate !== "function") ||
32
- candidate === null ||
33
- typeof candidate.loadAsync !== "function") {
34
- throw new Error('Optional archive dependency "jszip" does not expose loadAsync().');
35
- }
36
- return candidate;
37
- }
38
- catch (err) {
39
- throw missingOptionalArchiveDependencyError("jszip", err);
40
- }
41
- }
42
- function missingOptionalArchiveDependencyError(packageName, cause) {
43
- return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use ZIP archive helpers from @openclaw/fs-safe/archive.`, { cause });
11
+ return await loadAdmittedZipArchive(buffer, entryCount);
44
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACpH,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAiNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CjF"}
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACpH,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAiNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CjF"}