@openclaw/fs-safe 0.8.6 → 0.10.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 (246) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +17 -0
  3. package/dist/advanced.d.ts +1 -0
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts.map +1 -1
  7. package/dist/archive-crc32.js +6 -1
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +3 -4
  10. package/dist/archive-durability.d.ts +24 -0
  11. package/dist/archive-durability.d.ts.map +1 -0
  12. package/dist/archive-durability.js +180 -0
  13. package/dist/archive-gzip-tail.d.ts +2 -0
  14. package/dist/archive-gzip-tail.d.ts.map +1 -1
  15. package/dist/archive-gzip-tail.js +22 -4
  16. package/dist/archive-input.js +4 -4
  17. package/dist/archive-kind.d.ts.map +1 -1
  18. package/dist/archive-kind.js +3 -2
  19. package/dist/archive-merge.d.ts +1 -0
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +42 -9
  22. package/dist/archive-native.d.ts +1 -0
  23. package/dist/archive-native.d.ts.map +1 -1
  24. package/dist/archive-native.js +4 -2
  25. package/dist/archive-options.d.ts +2 -0
  26. package/dist/archive-options.d.ts.map +1 -1
  27. package/dist/archive-parser.wasm +0 -0
  28. package/dist/archive-read.d.ts.map +1 -1
  29. package/dist/archive-read.js +72 -70
  30. package/dist/archive-staging.d.ts +1 -1
  31. package/dist/archive-staging.d.ts.map +1 -1
  32. package/dist/archive-staging.js +24 -19
  33. package/dist/archive-tar-stream.d.ts +11 -4
  34. package/dist/archive-tar-stream.d.ts.map +1 -1
  35. package/dist/archive-tar-stream.js +20 -8
  36. package/dist/archive-tar-wasm.d.ts.map +1 -1
  37. package/dist/archive-tar-wasm.js +2 -1
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/bounded-read.d.ts +7 -0
  41. package/dist/bounded-read.d.ts.map +1 -1
  42. package/dist/bounded-read.js +73 -43
  43. package/dist/clone-metadata.d.ts +19 -0
  44. package/dist/clone-metadata.d.ts.map +1 -0
  45. package/dist/clone-metadata.js +32 -0
  46. package/dist/copy-file-input.d.ts +23 -0
  47. package/dist/copy-file-input.d.ts.map +1 -0
  48. package/dist/copy-file-input.js +91 -0
  49. package/dist/copy-policy.d.ts +3 -0
  50. package/dist/copy-policy.d.ts.map +1 -0
  51. package/dist/copy-policy.js +8 -0
  52. package/dist/copy-publication.d.ts +15 -0
  53. package/dist/copy-publication.d.ts.map +1 -0
  54. package/dist/copy-publication.js +30 -0
  55. package/dist/copy-tree-portable.d.ts +9 -0
  56. package/dist/copy-tree-portable.d.ts.map +1 -0
  57. package/dist/copy-tree-portable.js +191 -0
  58. package/dist/copy.d.ts +16 -0
  59. package/dist/copy.d.ts.map +1 -0
  60. package/dist/copy.js +123 -0
  61. package/dist/directory-mode-node.d.ts.map +1 -1
  62. package/dist/directory-mode-node.js +4 -4
  63. package/dist/durability.d.ts +1 -1
  64. package/dist/durability.d.ts.map +1 -1
  65. package/dist/error-detail.d.ts.map +1 -1
  66. package/dist/error-detail.js +4 -1
  67. package/dist/file-hash.d.ts +6 -2
  68. package/dist/file-hash.d.ts.map +1 -1
  69. package/dist/file-hash.js +49 -12
  70. package/dist/file-store-sync-write.d.ts +1 -0
  71. package/dist/file-store-sync-write.d.ts.map +1 -1
  72. package/dist/file-store-sync-write.js +9 -6
  73. package/dist/file-store.d.ts +4 -0
  74. package/dist/file-store.d.ts.map +1 -1
  75. package/dist/file-store.js +10 -1
  76. package/dist/filename.d.ts.map +1 -1
  77. package/dist/filename.js +2 -12
  78. package/dist/fs.d.ts.map +1 -1
  79. package/dist/fs.js +1 -2
  80. package/dist/guarded-mkdir.d.ts +1 -0
  81. package/dist/guarded-mkdir.d.ts.map +1 -1
  82. package/dist/guarded-mkdir.js +1 -0
  83. package/dist/guarded-mutation.d.ts +2 -0
  84. package/dist/guarded-mutation.d.ts.map +1 -1
  85. package/dist/guarded-mutation.js +9 -5
  86. package/dist/home-dir.d.ts.map +1 -1
  87. package/dist/home-dir.js +9 -7
  88. package/dist/index.d.ts +1 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/install-path.d.ts.map +1 -1
  91. package/dist/install-path.js +9 -13
  92. package/dist/json-document-store.d.ts +3 -0
  93. package/dist/json-document-store.d.ts.map +1 -1
  94. package/dist/json-document-store.js +1 -0
  95. package/dist/json-durable-queue-directory.js +3 -3
  96. package/dist/json-durable-queue-ownership.js +3 -2
  97. package/dist/json-durable-queue.d.ts.map +1 -1
  98. package/dist/json-durable-queue.js +31 -27
  99. package/dist/local-roots.d.ts.map +1 -1
  100. package/dist/local-roots.js +22 -24
  101. package/dist/move-path-cleanup.d.ts.map +1 -1
  102. package/dist/move-path-cleanup.js +4 -3
  103. package/dist/move-path-stage.d.ts +8 -0
  104. package/dist/move-path-stage.d.ts.map +1 -0
  105. package/dist/move-path-stage.js +55 -0
  106. package/dist/move-path.d.ts.map +1 -1
  107. package/dist/move-path.js +55 -47
  108. package/dist/mutation-authority.d.ts +8 -0
  109. package/dist/mutation-authority.d.ts.map +1 -0
  110. package/dist/mutation-authority.js +36 -0
  111. package/dist/native-binding.d.ts +24 -1
  112. package/dist/native-binding.d.ts.map +1 -1
  113. package/dist/native-operations.d.ts +1 -1
  114. package/dist/native-operations.d.ts.map +1 -1
  115. package/dist/native-operations.js +11 -5
  116. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  117. package/dist/native-pinned-write-windows.js +18 -7
  118. package/dist/native-pinned-write.d.ts.map +1 -1
  119. package/dist/native-pinned-write.js +1 -0
  120. package/dist/native-staged-file.d.ts +3 -3
  121. package/dist/native-staged-file.d.ts.map +1 -1
  122. package/dist/native-staged-file.js +35 -8
  123. package/dist/opened-file-failure.d.ts +1 -1
  124. package/dist/opened-file-failure.d.ts.map +1 -1
  125. package/dist/opened-file-failure.js +3 -2
  126. package/dist/path.d.ts.map +1 -1
  127. package/dist/path.js +3 -1
  128. package/dist/permissions-windows.d.ts.map +1 -1
  129. package/dist/permissions-windows.js +38 -48
  130. package/dist/permissions.js +3 -3
  131. package/dist/pinned-operation.js +1 -1
  132. package/dist/pinned-write.d.ts +5 -1
  133. package/dist/pinned-write.d.ts.map +1 -1
  134. package/dist/pinned-write.js +50 -30
  135. package/dist/positional-read.d.ts +9 -0
  136. package/dist/positional-read.d.ts.map +1 -0
  137. package/dist/positional-read.js +36 -0
  138. package/dist/private-temp-workspace.d.ts.map +1 -1
  139. package/dist/private-temp-workspace.js +9 -3
  140. package/dist/publish-copy-stage.d.ts +13 -0
  141. package/dist/publish-copy-stage.d.ts.map +1 -0
  142. package/dist/publish-copy-stage.js +47 -0
  143. package/dist/publish-file.d.ts.map +1 -1
  144. package/dist/publish-file.js +26 -25
  145. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  146. package/dist/replace-file-copy-fallback.js +30 -17
  147. package/dist/replace-file-copy-source.d.ts.map +1 -1
  148. package/dist/replace-file-copy-source.js +10 -5
  149. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  150. package/dist/replace-file-temp-owner.js +6 -2
  151. package/dist/replace-file.d.ts.map +1 -1
  152. package/dist/replace-file.js +8 -3
  153. package/dist/root-context.d.ts +3 -0
  154. package/dist/root-context.d.ts.map +1 -1
  155. package/dist/root-context.js +51 -26
  156. package/dist/root-directory-list.d.ts +24 -0
  157. package/dist/root-directory-list.d.ts.map +1 -0
  158. package/dist/root-directory-list.js +201 -0
  159. package/dist/root-errors.d.ts +1 -0
  160. package/dist/root-errors.d.ts.map +1 -1
  161. package/dist/root-errors.js +3 -0
  162. package/dist/root-file.d.ts +2 -0
  163. package/dist/root-file.d.ts.map +1 -1
  164. package/dist/root-file.js +12 -4
  165. package/dist/root-impl.d.ts +6 -50
  166. package/dist/root-impl.d.ts.map +1 -1
  167. package/dist/root-impl.js +342 -208
  168. package/dist/root-options.d.ts +66 -0
  169. package/dist/root-options.d.ts.map +1 -0
  170. package/dist/root-options.js +18 -0
  171. package/dist/root-path-existing.d.ts +5 -0
  172. package/dist/root-path-existing.d.ts.map +1 -1
  173. package/dist/root-path-existing.js +56 -1
  174. package/dist/root-path.d.ts +1 -0
  175. package/dist/root-path.d.ts.map +1 -1
  176. package/dist/root-path.js +74 -162
  177. package/dist/root-symlink-policy.d.ts +13 -0
  178. package/dist/root-symlink-policy.d.ts.map +1 -0
  179. package/dist/root-symlink-policy.js +34 -0
  180. package/dist/root-walk.d.ts +5 -4
  181. package/dist/root-walk.d.ts.map +1 -1
  182. package/dist/root-walk.js +99 -50
  183. package/dist/root-write-mode.d.ts.map +1 -1
  184. package/dist/root-write-mode.js +1 -4
  185. package/dist/root.d.ts +3 -1
  186. package/dist/root.d.ts.map +1 -1
  187. package/dist/secret-file.d.ts +1 -0
  188. package/dist/secret-file.d.ts.map +1 -1
  189. package/dist/secret-file.js +1 -0
  190. package/dist/secret-read-async.js +5 -5
  191. package/dist/secure-file.d.ts +1 -1
  192. package/dist/secure-file.d.ts.map +1 -1
  193. package/dist/secure-file.js +23 -13
  194. package/dist/sibling-staged-file.js +7 -7
  195. package/dist/sibling-temp.d.ts.map +1 -1
  196. package/dist/sibling-temp.js +16 -7
  197. package/dist/sidecar-lock-acquire.d.ts +1 -1
  198. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  199. package/dist/sidecar-lock-acquire.js +6 -4
  200. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  201. package/dist/sidecar-lock-policy.js +5 -3
  202. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  203. package/dist/sidecar-lock-reclaim.js +16 -4
  204. package/dist/sidecar-lock.d.ts.map +1 -1
  205. package/dist/sidecar-lock.js +2 -1
  206. package/dist/temp-cleanup.d.ts.map +1 -1
  207. package/dist/temp-cleanup.js +3 -2
  208. package/dist/temp-target.d.ts.map +1 -1
  209. package/dist/temp-target.js +9 -4
  210. package/dist/timing.d.ts +1 -0
  211. package/dist/timing.d.ts.map +1 -1
  212. package/dist/timing.js +25 -6
  213. package/dist/trash.js +2 -2
  214. package/dist/walk.d.ts.map +1 -1
  215. package/dist/walk.js +7 -27
  216. package/dist/windows-owner.d.ts +9 -12
  217. package/dist/windows-owner.d.ts.map +1 -1
  218. package/dist/windows-owner.js +24 -58
  219. package/dist/write-file-handle.d.ts +6 -0
  220. package/dist/write-file-handle.d.ts.map +1 -0
  221. package/dist/write-file-handle.js +25 -0
  222. package/dist/write-open-flags.d.ts.map +1 -1
  223. package/dist/write-open-flags.js +1 -2
  224. package/docs/advanced.md +18 -4
  225. package/docs/archive.md +57 -9
  226. package/docs/atomic.md +7 -0
  227. package/docs/contributing.md +7 -0
  228. package/docs/copy.md +86 -0
  229. package/docs/durability.md +17 -2
  230. package/docs/errors.md +1 -1
  231. package/docs/file-store.md +48 -3
  232. package/docs/json-store.md +10 -0
  233. package/docs/local-roots.md +8 -1
  234. package/docs/native.md +14 -1
  235. package/docs/permissions.md +20 -10
  236. package/docs/positional-read.md +63 -0
  237. package/docs/reading.md +6 -0
  238. package/docs/root.md +168 -6
  239. package/docs/secret-file.md +6 -0
  240. package/docs/security-model.md +14 -0
  241. package/docs/sidecar-lock.md +2 -0
  242. package/docs/timing.md +2 -0
  243. package/docs/types.md +18 -11
  244. package/docs/walk.md +54 -5
  245. package/docs/writing.md +25 -5
  246. package/package.json +16 -11
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { ownExtractionDestinationMutation } from "./archive-deadline.js";
@@ -10,13 +11,18 @@ import { formatErrorDetail } from "./error-detail.js";
10
11
  import { isPathInside } from "./path.js";
11
12
  import { root } from "./root.js";
12
13
  import { getFsSafeTestHooks } from "./test-hooks.js";
14
+ import { onCopyPublication } from "./copy-publication.js";
15
+ import { syncFileBestEffortSync } from "./file-sync.js";
16
+ import { finalizeArchivePublication } from "./archive-durability.js";
13
17
  export async function mergePlannedArchiveIntoDestination(params) {
14
- await mergeTree(params, params.entries);
18
+ await mergeTree(params, params.entries, params.durable === true);
15
19
  }
16
20
  export async function mergeExtractedTreeIntoDestination(params) {
17
21
  await mergeTree(params);
18
22
  }
19
- async function mergeTree(params, publication) {
23
+ async function mergeTree(params, publication, durable = true) {
24
+ const publishedFiles = [];
25
+ const publishedDirectories = [];
20
26
  const check = () => params.deadline?.check();
21
27
  check();
22
28
  const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
@@ -29,7 +35,7 @@ async function mergeTree(params, publication) {
29
35
  for (const entry of publication ?? []) {
30
36
  // Resolve admitted spelling in private staging, preserving the volume's case
31
37
  // and Unicode behavior without assigning explicit modes to distinct parents.
32
- const stagedPath = await fs.realpath(path.join(params.sourceDir, entry.path));
38
+ const stagedPath = fsSync.realpathSync.native(path.join(params.sourceDir, entry.path));
33
39
  check();
34
40
  if (!isPathInside(sourceGuard.realPath, stagedPath) || plan.has(stagedPath)) {
35
41
  throw new FsSafeError("path-mismatch", "archive publication paths changed in staging");
@@ -59,11 +65,11 @@ async function mergeTree(params, publication) {
59
65
  const relPath = path.relative(params.sourceDir, sourcePath);
60
66
  const originalPath = relPath.split(path.sep).join("/");
61
67
  const destinationPath = path.join(params.destinationDir, relPath);
62
- const sourceStat = await fs.lstat(sourcePath);
68
+ const sourceStat = fsSync.lstatSync(sourcePath);
63
69
  check();
64
70
  if (sourceStat.isSymbolicLink())
65
71
  throw createArchiveSymlinkTraversalError(originalPath);
66
- const sourceReal = await fs.realpath(sourcePath);
72
+ const sourceReal = fsSync.realpathSync.native(sourcePath);
67
73
  check();
68
74
  if (!isPathInside(sourceGuard.realPath, sourceReal))
69
75
  throw createArchiveSymlinkTraversalError(originalPath);
@@ -78,7 +84,7 @@ async function mergeTree(params, publication) {
78
84
  const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
79
85
  await preparePrivateArchiveOutputPath({
80
86
  ...params, relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
81
- }, assertGuards);
87
+ }, assertGuards, destinationGuard);
82
88
  check();
83
89
  if (kind === "directory") {
84
90
  // Ownership spans open, descendants, finalization and close, including timeout.
@@ -98,6 +104,11 @@ async function mergeTree(params, publication) {
98
104
  ancestors.push({ guard, owner });
99
105
  try {
100
106
  await walk(sourcePath);
107
+ if (publication) {
108
+ await assertGuards();
109
+ publishedDirectories.push({ guard, mode, parents: ancestors.slice(0, -1).map((ancestor) => ancestor.guard) });
110
+ return;
111
+ }
101
112
  await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", destinationPath);
102
113
  check();
103
114
  await assertGuards();
@@ -129,21 +140,39 @@ async function mergeTree(params, publication) {
129
140
  await ownExtractionDestinationMutation(params.deadline, async () => {
130
141
  await assertGuards();
131
142
  try {
132
- await targetRoot.copyIn(relPath, sourcePath, { mkdir: true, mode });
143
+ const options = {
144
+ mkdir: true, mode, durable: publication ? false : true,
145
+ };
146
+ if (publication && durable) {
147
+ options[onCopyPublication] = async (fd, identity) => {
148
+ check();
149
+ if ((mode & 0o400) === 0) {
150
+ // Final mode may prohibit reopening. Sync the writer's still-owned
151
+ // descriptor without widening permissions or syncing its parent.
152
+ syncFileBestEffortSync(fd);
153
+ check();
154
+ }
155
+ else {
156
+ publishedFiles.push({ relativePath: relPath, identity,
157
+ guards: ancestors.map((ancestor) => ancestor.guard) });
158
+ }
159
+ };
160
+ }
161
+ await targetRoot.copyIn(relPath, sourcePath, options);
133
162
  check();
134
163
  await assertGuards();
135
164
  await assertResolvedInsideDestination({
136
165
  destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
137
166
  });
138
167
  check();
139
- const stat = await fs.lstat(destinationPath);
168
+ const stat = fsSync.lstatSync(destinationPath);
140
169
  check();
141
170
  if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink > 1)) {
142
171
  throw createArchiveSymlinkTraversalError(originalPath);
143
172
  }
144
173
  }
145
174
  catch (error) {
146
- // copyIn alone owns cleanup; no receipt permits archive-layer unlink.
175
+ // copyIn cleans unpublished stages; merge never gains rollback authority.
147
176
  if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
148
177
  throw createArchiveSymlinkTraversalError(originalPath);
149
178
  }
@@ -154,4 +183,8 @@ async function mergeTree(params, publication) {
154
183
  }
155
184
  };
156
185
  await walk(params.sourceDir);
186
+ if (publication) {
187
+ await finalizeArchivePublication({ targetRoot, destinationGuard, sourceGuard,
188
+ files: publishedFiles, directories: publishedDirectories, durable, deadline: params.deadline });
189
+ }
157
190
  }
@@ -5,6 +5,7 @@ import type { ExtractArchiveOptions } from "./archive-options.js";
5
5
  import type { NativeBinding } from "./native.js";
6
6
  export declare function throwMappedNativeArchiveError(error: unknown): never;
7
7
  export declare function extractNativeArchive(params: {
8
+ durable?: boolean;
8
9
  binding: NativeBinding;
9
10
  archivePath: string;
10
11
  destDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAYnE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
1
+ {"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAYnE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CA+FhB"}
@@ -40,8 +40,9 @@ export async function extractNativeArchive(params) {
40
40
  destinationRealDir,
41
41
  run: async (stagingDir) => {
42
42
  params.deadline.check();
43
+ // N-API retains completed task state on its signal; each pass needs its own.
43
44
  const manifest = await params.binding
44
- .inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, params.deadline.signal)
45
+ .inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, AbortSignal.any([params.deadline.signal]))
45
46
  .catch(throwMappedNativeArchiveError);
46
47
  params.deadline.check();
47
48
  assertArchiveEntryCountWithinLimit(manifest.length, limits);
@@ -84,7 +85,7 @@ export async function extractNativeArchive(params) {
84
85
  (typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
85
86
  try {
86
87
  params.deadline.check();
87
- await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, params.deadline.signal).catch(throwMappedNativeArchiveError);
88
+ await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, AbortSignal.any([params.deadline.signal])).catch(throwMappedNativeArchiveError);
88
89
  }
89
90
  finally {
90
91
  await directory.close().catch(() => undefined);
@@ -92,6 +93,7 @@ export async function extractNativeArchive(params) {
92
93
  params.deadline.check();
93
94
  await mergePlannedArchiveIntoDestination({
94
95
  entries: plan,
96
+ durable: params.durable,
95
97
  sourceDir: stagingDir,
96
98
  destinationDir: params.destDir,
97
99
  destinationRealDir,
@@ -9,6 +9,8 @@ export type ExtractArchiveOptions = {
9
9
  archivePath: string;
10
10
  destDir: string;
11
11
  timeoutMs: number;
12
+ /** Sync published files and directories before returning. Defaults to false. */
13
+ durable?: boolean;
12
14
  kind?: ArchiveKind;
13
15
  stripComponents?: number;
14
16
  tarGzip?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-options.d.ts","sourceRoot":"","sources":["../src/archive-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,CAAC"}
1
+ {"version":3,"file":"archive-options.d.ts","sourceRoot":"","sources":["../src/archive-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,CAAC"}
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkLpG,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,CA6EjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
@@ -1,7 +1,8 @@
1
1
  import { classifyArchiveParserError } from "./archive-parser-errors.js";
2
+ import fsSync from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import { Readable } from "node:stream";
4
- import { readFileHandleBounded } from "./bounded-read.js";
5
+ import { readBoundedAsync } from "./bounded-read.js";
5
6
  import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
6
7
  import { formatErrorDetail } from "./error-detail.js";
7
8
  import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
@@ -16,7 +17,6 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
16
17
  import { getNativeBinding } from "./native.js";
17
18
  import { admitZipBuffer } from "./archive-zip-admission.js";
18
19
  import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
19
- import { tempFile } from "./temp-target.js";
20
20
  const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
21
21
  const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
22
22
  function canonicalEntryPath(entryPath) {
@@ -62,37 +62,37 @@ async function readStreamBounded(stream, maxBytes) {
62
62
  }
63
63
  return Buffer.concat(chunks, total);
64
64
  }
65
- async function stageArchiveInput(archivePath) {
66
- const resolved = await fs.realpath(archivePath);
65
+ async function readArchiveInput(archivePath) {
66
+ const resolved = fsSync.realpathSync.native(archivePath);
67
67
  const before = await inspectFileIdentity(async () => {
68
- const stat = await fs.lstat(archivePath, { bigint: true });
68
+ const stat = fsSync.lstatSync(archivePath, { bigint: true });
69
69
  if (stat.isSymbolicLink() || !stat.isFile()) {
70
70
  throw new Error(`archive is not a regular file: ${archivePath}`);
71
71
  }
72
72
  return stat;
73
73
  });
74
74
  const handle = await fs.open(resolved, resolveReadOpenFlags());
75
- let staged;
76
75
  try {
77
- staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
78
76
  const opened = await inspectFileIdentity(async () => {
79
- const stat = await handle.stat({ bigint: true });
77
+ const stat = fsSync.fstatSync(handle.fd, { bigint: true });
80
78
  if (!stat.isFile())
81
79
  throw new Error("archive changed during validation");
82
80
  return stat;
83
81
  }, before);
84
82
  await inspectFileIdentity(async () => {
85
- const stat = await fs.lstat(resolved, { bigint: true });
83
+ const stat = fsSync.lstatSync(resolved, { bigint: true });
86
84
  if (stat.isSymbolicLink() || !stat.isFile())
87
85
  throw new Error("archive changed during validation");
88
86
  return stat;
89
87
  }, opened);
90
- const buffer = await readFileHandleBounded(handle, DEFAULT_MAX_ARCHIVE_BYTES_ZIP);
91
- await fs.writeFile(staged.path, buffer, { flag: "wx", mode: 0o600 });
92
- return { path: staged.path, buffer, cleanup: staged.cleanup };
88
+ // Native archive workers borrow this private buffer. A pooled allocation could
89
+ // share its ArrayBuffer with unrelated JS buffers while the worker runs.
90
+ return await readBoundedAsync(DEFAULT_MAX_ARCHIVE_BYTES_ZIP, async (buffer, length) => (await handle.read(buffer, 0, length, null)).bytesRead, { unpooled: true, observeRegularFileSize: () => {
91
+ const size = fsSync.fstatSync(handle.fd).size;
92
+ return Number.isSafeInteger(size) && size >= 0 ? size : undefined;
93
+ } });
93
94
  }
94
95
  catch (error) {
95
- await staged?.cleanup().catch(() => undefined);
96
96
  if (error instanceof FsSafeError && error.code === "path-mismatch") {
97
97
  throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
98
98
  }
@@ -133,11 +133,11 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
133
133
  stream.once("error", (error) => integrity.destroy(normalizeZipIntegrityError(error)));
134
134
  return await readStreamBounded(stream.pipe(integrity), maxBytes);
135
135
  }
136
- async function readTarEntry(archivePath, entryPath, maxBytes) {
136
+ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
137
137
  const seenPaths = new Set();
138
138
  let selected;
139
139
  const limits = resolveTarMeterLimits();
140
- await inspectTar({ archivePath, limits, onMember(info) {
140
+ await inspectTar({ archiveBuffer, limits, onMember(info) {
141
141
  const normalized = canonicalEntryPath(info.path);
142
142
  if (seenPaths.has(normalized)) {
143
143
  throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
@@ -154,11 +154,57 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
154
154
  if (selected.size > maxBytes)
155
155
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
156
156
  let result;
157
- await replayTar({ archivePath, limits, members: [selected], async consume(_member, payload) {
157
+ await replayTar({ archiveBuffer, limits, members: [selected], async consume(_member, payload) {
158
158
  result = await readStreamBounded(payload, maxBytes);
159
159
  } });
160
160
  return result;
161
161
  }
162
+ function selectNativeEntry(manifest, requested, displayPath, physicalCount) {
163
+ if (physicalCount !== undefined && manifest.length !== physicalCount) {
164
+ throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
165
+ }
166
+ const seen = new Set();
167
+ let selected;
168
+ for (const entry of manifest) {
169
+ const normalized = canonicalEntryPath(entry.path);
170
+ if (seen.has(normalized)) {
171
+ throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
172
+ }
173
+ seen.add(normalized);
174
+ if (normalized === requested) {
175
+ if (entry.kind !== "file")
176
+ throw new Error(`archive entry is not a file: ${formatErrorDetail(displayPath)}`);
177
+ selected = entry;
178
+ }
179
+ }
180
+ if (!selected)
181
+ throw new Error(`archive entry not found: ${formatErrorDetail(displayPath)}`);
182
+ return selected;
183
+ }
184
+ function throwNativeReadError(error) {
185
+ if (error instanceof Error) {
186
+ const mapped = classifyArchiveParserError(error.message, { cause: error });
187
+ if (mapped)
188
+ throw mapped;
189
+ if (isArchiveFormatErrorMessage(error.message))
190
+ throw new ArchiveFormatError(error.message, { cause: error });
191
+ }
192
+ throw error;
193
+ }
194
+ async function readNativeBufferEntry(native, buffer, kind, requested, displayPath, maxBytes, physicalCount) {
195
+ try {
196
+ const signal = new AbortController().signal;
197
+ const limits = resolveTarMeterLimits();
198
+ const reader = kind === "zip"
199
+ ? await native.openZipBufferNative(buffer, limits, AbortSignal.any([signal]))
200
+ : await native.openTarBufferNative(buffer, kind, limits, AbortSignal.any([signal]));
201
+ const selected = selectNativeEntry(reader.entries, requested, displayPath, physicalCount);
202
+ return await reader.readEntry(selected.index, maxBytes, AbortSignal.any([signal]));
203
+ }
204
+ catch (error) {
205
+ throwNativeReadError(error);
206
+ }
207
+ }
162
208
  export async function readArchiveEntry(archivePath, entryPath, options) {
163
209
  if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
164
210
  throw new RangeError("maxBytes must be a non-negative safe integer");
@@ -168,58 +214,14 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
168
214
  throw new Error(`unsupported archive: ${archivePath}`);
169
215
  }
170
216
  const requestedEntry = normalizedRequestedEntry(entryPath);
171
- const staged = await stageArchiveInput(archivePath);
172
- try {
173
- const physicalCount = kind === "zip"
174
- ? admitZipBuffer(staged.buffer, resolveExtractLimits())
175
- : undefined;
176
- const native = getNativeBinding();
177
- if (native) {
178
- try {
179
- const signal = new AbortController().signal;
180
- const limits = resolveTarMeterLimits();
181
- const manifest = await native.inspectArchiveNative(staged.path, kind, limits, signal);
182
- let rawEntryPath;
183
- if (physicalCount !== undefined && manifest.length !== physicalCount) {
184
- throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
185
- }
186
- const seenPaths = new Set();
187
- for (const entry of manifest) {
188
- const normalized = canonicalEntryPath(entry.path);
189
- if (seenPaths.has(normalized)) {
190
- throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
191
- }
192
- seenPaths.add(normalized);
193
- if (normalized === requestedEntry) {
194
- if (entry.kind !== "file") {
195
- throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
196
- }
197
- rawEntryPath = entry.path;
198
- }
199
- }
200
- if (!rawEntryPath) {
201
- throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
202
- }
203
- return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, limits, signal);
204
- }
205
- catch (error) {
206
- if (error instanceof Error) {
207
- const mapped = classifyArchiveParserError(error.message, { cause: error });
208
- if (mapped)
209
- throw mapped;
210
- }
211
- if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
212
- throw new ArchiveFormatError(error.message, { cause: error });
213
- }
214
- throw error;
215
- }
216
- }
217
- assertPortableArchiveKind(kind);
218
- return kind === "zip"
219
- ? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
220
- : await readTarEntry(staged.path, requestedEntry, options.maxBytes);
221
- }
222
- finally {
223
- await staged.cleanup();
224
- }
217
+ const buffer = await readArchiveInput(archivePath);
218
+ const physicalCount = kind === "zip"
219
+ ? admitZipBuffer(buffer, resolveExtractLimits())
220
+ : undefined;
221
+ const native = getNativeBinding();
222
+ if (native)
223
+ return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, physicalCount);
224
+ assertPortableArchiveKind(kind);
225
+ return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes)
226
+ : await readTarEntry(buffer, requestedEntry, options.maxBytes);
225
227
  }
@@ -20,7 +20,7 @@ type ArchiveOutputPathParams = {
20
20
  deadline?: ExtractionDeadline;
21
21
  };
22
22
  export declare function prepareArchiveOutputPath(params: ArchiveOutputPathParams): Promise<void>;
23
- export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>): Promise<void>;
23
+ export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?: AsyncDirectoryGuard): Promise<void>;
24
24
  export declare function withStagedArchiveDestination<T>(params: {
25
25
  destinationRealDir: string;
26
26
  stagingDirPrefix?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAEA,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,GAClE,OAAO,CAAC,IAAI,CAAC,CAEf;AA8GD,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":"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,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
@@ -44,16 +45,16 @@ export async function assertDirectoryIdentityGuard(guard) {
44
45
  }
45
46
  }
46
47
  export async function prepareArchiveDestinationDir(destDir) {
47
- const stat = await fs.lstat(destDir);
48
+ const stat = fsSync.lstatSync(destDir);
48
49
  if (stat.isSymbolicLink()) {
49
50
  throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
50
51
  }
51
52
  if (!stat.isDirectory()) {
52
53
  throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
53
54
  }
54
- const realPath = await fs.realpath(destDir);
55
- const realStat = await fs.stat(realPath);
56
- const postStat = await fs.lstat(destDir);
55
+ const realPath = fsSync.realpathSync.native(destDir);
56
+ const realStat = fsSync.statSync(realPath);
57
+ const postStat = fsSync.lstatSync(destDir);
57
58
  if (realStat.dev !== stat.dev ||
58
59
  realStat.ino !== stat.ino ||
59
60
  postStat.isSymbolicLink() ||
@@ -71,7 +72,7 @@ async function assertNoSymlinkTraversal(params) {
71
72
  current = path.join(current, part);
72
73
  let stat;
73
74
  try {
74
- stat = await fs.lstat(current);
75
+ stat = fsSync.lstatSync(current);
75
76
  }
76
77
  catch (err) {
77
78
  if (isNotFoundPathError(err)) {
@@ -87,7 +88,7 @@ async function assertNoSymlinkTraversal(params) {
87
88
  export async function assertResolvedInsideDestination(params) {
88
89
  let resolved;
89
90
  try {
90
- resolved = await fs.realpath(params.targetPath);
91
+ resolved = fsSync.realpathSync.native(params.targetPath);
91
92
  }
92
93
  catch (err) {
93
94
  if (isNotFoundPathError(err)) {
@@ -113,10 +114,10 @@ async function mkdirArchiveOutput(params) {
113
114
  export async function prepareArchiveOutputPath(params) {
114
115
  await prepareOutputPath(params);
115
116
  }
116
- export async function preparePrivateArchiveOutputPath(params, assertGuards) {
117
- await prepareOutputPath(params, assertGuards, true);
117
+ export async function preparePrivateArchiveOutputPath(params, assertGuards, destinationGuard) {
118
+ await prepareOutputPath(params, assertGuards, true, destinationGuard);
118
119
  }
119
- async function prepareOutputPath(params, assertGuards, privateWorkingMode = false) {
120
+ async function prepareOutputPath(params, assertGuards, privateWorkingMode = false, existingDestinationGuard) {
120
121
  checkExtractionDeadline(params.deadline);
121
122
  const targetRoot = privateWorkingMode ? {
122
123
  async mkdir(relativePath) {
@@ -136,16 +137,24 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
136
137
  mode: 0o700,
137
138
  rejectSymlinks: true,
138
139
  beforeComponent: async () => {
139
- await assertDirectoryIdentityGuard(destinationGuard);
140
- checkExtractionDeadline(params.deadline);
141
- await assertGuards?.();
140
+ await assertOutputGuards();
142
141
  checkExtractionDeadline(params.deadline);
143
142
  },
144
143
  });
145
144
  },
146
145
  } : await root(params.destinationRealDir);
147
146
  checkExtractionDeadline(params.deadline);
148
- const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
147
+ const destinationGuard = existingDestinationGuard ?? await createDirectoryIdentityGuard(params.destinationRealDir);
148
+ const assertOutputGuards = async () => {
149
+ // The merge's callback verifies this same original destination guard.
150
+ if (existingDestinationGuard && assertGuards)
151
+ await assertGuards();
152
+ else {
153
+ await assertDirectoryIdentityGuard(destinationGuard);
154
+ checkExtractionDeadline(params.deadline);
155
+ await assertGuards?.();
156
+ }
157
+ };
149
158
  checkExtractionDeadline(params.deadline);
150
159
  const relPath = params.relPath.split(path.sep).join(path.posix.sep);
151
160
  await assertNoSymlinkTraversal({
@@ -158,9 +167,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
158
167
  await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
159
168
  checkExtractionDeadline(params.deadline);
160
169
  await ownExtractionDestinationMutation(params.deadline, async () => {
161
- await assertDirectoryIdentityGuard(destinationGuard);
162
- checkExtractionDeadline(params.deadline);
163
- await assertGuards?.();
170
+ await assertOutputGuards();
164
171
  checkExtractionDeadline(params.deadline);
165
172
  await mkdirArchiveOutput({
166
173
  targetRoot,
@@ -184,9 +191,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
184
191
  await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
185
192
  checkExtractionDeadline(params.deadline);
186
193
  await ownExtractionDestinationMutation(params.deadline, async () => {
187
- await assertDirectoryIdentityGuard(destinationGuard);
188
- checkExtractionDeadline(params.deadline);
189
- await assertGuards?.();
194
+ await assertOutputGuards();
190
195
  checkExtractionDeadline(params.deadline);
191
196
  await mkdirArchiveOutput({
192
197
  targetRoot,
@@ -1,18 +1,25 @@
1
1
  import type { TarMeterLimits } from "./archive-limits.js";
2
2
  import { type AdmittedTarMember } from "./archive-tar-wasm.js";
3
- export declare function inspectTar(params: {
3
+ /** Buffers are private immutable snapshots, just like the staged file route. */
4
+ type TarInput = {
4
5
  archivePath: string;
6
+ archiveBuffer?: never;
7
+ } | {
8
+ archiveBuffer: Buffer;
9
+ archivePath?: never;
10
+ };
11
+ export declare function inspectTar(params: TarInput & {
5
12
  limits: TarMeterLimits;
6
13
  signal?: AbortSignal;
7
14
  onMember?: (entry: AdmittedTarMember) => void;
8
15
  }): Promise<void>;
9
16
  /** Replay in physical order, retaining at most one decoded chunk. Every range
10
- * comes from complete admission of the immutable staged input. */
11
- export declare function replayTar<T extends AdmittedTarMember>(params: {
12
- archivePath: string;
17
+ * comes from complete admission of the immutable input. */
18
+ export declare function replayTar<T extends AdmittedTarMember>(params: TarInput & {
13
19
  limits: TarMeterLimits;
14
20
  signal?: AbortSignal;
15
21
  members: readonly T[];
16
22
  consume(member: T, payload: AsyncIterable<Buffer>): Promise<void>;
17
23
  }): Promise<void>;
24
+ export {};
18
25
  //# sourceMappingURL=archive-tar-stream.d.ts.map
@@ -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;AA4ChF,wBAAsB,UAAU,CAAC,MAAM,EAAE;IACvC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;kEACkE;AAClE,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE;IACnE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAClE,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;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,10 +1,14 @@
1
1
  import fs from "node:fs";
2
- import { Writable } from "node:stream";
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, validateGzipContainerTail } from "./archive-gzip-tail.js";
5
+ import { GzipInput, 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
+ function* bufferChunks(buffer) {
9
+ for (let offset = 0; offset < buffer.length; offset += 65536)
10
+ yield buffer.subarray(offset, offset + 65536);
11
+ }
8
12
  async function gzipFile(filePath) {
9
13
  const handle = await fs.promises.open(filePath, "r");
10
14
  try {
@@ -17,10 +21,14 @@ async function gzipFile(filePath) {
17
21
  }
18
22
  }
19
23
  async function withTarStream(params, consume) {
20
- const gzip = await gzipFile(params.archivePath);
24
+ const buffer = params.archiveBuffer;
25
+ const gzip = buffer !== undefined ? buffer[0] === 31 && buffer[1] === 139 : await gzipFile(params.archivePath);
21
26
  const parser = new TarParserStream(params.limits, params.onMember);
22
- const input = fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
23
- const decoder = gzip ? createGunzip() : undefined;
27
+ const input = buffer !== undefined
28
+ ? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
29
+ : 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;
24
32
  const gzipInput = decoder ? new GzipInput(decoder) : undefined;
25
33
  const destroy = (error) => {
26
34
  input.destroy(error);
@@ -42,8 +50,12 @@ async function withTarStream(params, consume) {
42
50
  const error = await settled;
43
51
  if (error)
44
52
  throw error;
45
- if (gzipInput)
46
- await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
53
+ if (gzipInput) {
54
+ if (buffer !== undefined)
55
+ await validateGzipBufferTail(buffer, gzipInput.tailOffset, params.signal);
56
+ else
57
+ await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
58
+ }
47
59
  return result;
48
60
  }
49
61
  finally {
@@ -57,7 +69,7 @@ export async function inspectTar(params) {
57
69
  });
58
70
  }
59
71
  /** Replay in physical order, retaining at most one decoded chunk. Every range
60
- * comes from complete admission of the immutable staged input. */
72
+ * comes from complete admission of the immutable input. */
61
73
  export async function replayTar(params) {
62
74
  await withTarStream(params, async (parser) => {
63
75
  const iterator = parser[Symbol.asyncIterator]();
@@ -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;AAuClE,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,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"}