@openclaw/fs-safe 0.1.1 → 0.2.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 (207) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +58 -2
  3. package/dist/absolute-path.d.ts +14 -0
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +203 -2
  6. package/dist/advanced.d.ts +2 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +2 -1
  9. package/dist/archive-staging.d.ts.map +1 -1
  10. package/dist/archive-staging.js +81 -7
  11. package/dist/archive.d.ts.map +1 -1
  12. package/dist/archive.js +25 -42
  13. package/dist/bounded-read-stream.d.ts +8 -0
  14. package/dist/bounded-read-stream.d.ts.map +1 -0
  15. package/dist/bounded-read-stream.js +20 -0
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +1 -0
  19. package/dist/directory-guard.d.ts +18 -0
  20. package/dist/directory-guard.d.ts.map +1 -0
  21. package/dist/directory-guard.js +70 -0
  22. package/dist/file-lock.d.ts +4 -2
  23. package/dist/file-lock.d.ts.map +1 -1
  24. package/dist/file-lock.js +13 -2
  25. package/dist/file-store-boundary.d.ts +25 -0
  26. package/dist/file-store-boundary.d.ts.map +1 -0
  27. package/dist/file-store-boundary.js +168 -0
  28. package/dist/file-store-prune.d.ts +12 -0
  29. package/dist/file-store-prune.d.ts.map +1 -0
  30. package/dist/file-store-prune.js +86 -0
  31. package/dist/file-store-source.d.ts +5 -0
  32. package/dist/file-store-source.d.ts.map +1 -0
  33. package/dist/file-store-source.js +30 -0
  34. package/dist/file-store.d.ts +2 -6
  35. package/dist/file-store.d.ts.map +1 -1
  36. package/dist/file-store.js +122 -125
  37. package/dist/guarded-mkdir.d.ts +6 -0
  38. package/dist/guarded-mkdir.d.ts.map +1 -0
  39. package/dist/guarded-mkdir.js +45 -0
  40. package/dist/guarded-mutation.d.ts +33 -0
  41. package/dist/guarded-mutation.d.ts.map +1 -0
  42. package/dist/guarded-mutation.js +76 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -0
  46. package/dist/json-document-store.d.ts +2 -0
  47. package/dist/json-document-store.d.ts.map +1 -1
  48. package/dist/json-document-store.js +7 -3
  49. package/dist/json-durable-queue.d.ts +45 -0
  50. package/dist/json-durable-queue.d.ts.map +1 -0
  51. package/dist/json-durable-queue.js +200 -0
  52. package/dist/json.d.ts +35 -5
  53. package/dist/json.d.ts.map +1 -1
  54. package/dist/json.js +69 -2
  55. package/dist/local-roots.d.ts.map +1 -1
  56. package/dist/local-roots.js +17 -3
  57. package/dist/lock-config.d.ts +10 -0
  58. package/dist/lock-config.d.ts.map +1 -0
  59. package/dist/lock-config.js +12 -0
  60. package/dist/move-path.d.ts +1 -0
  61. package/dist/move-path.d.ts.map +1 -1
  62. package/dist/move-path.js +195 -7
  63. package/dist/output.d.ts +13 -0
  64. package/dist/output.d.ts.map +1 -0
  65. package/dist/output.js +69 -0
  66. package/dist/path-stat.d.ts +4 -0
  67. package/dist/path-stat.d.ts.map +1 -0
  68. package/dist/path-stat.js +15 -0
  69. package/dist/path.d.ts +1 -0
  70. package/dist/path.d.ts.map +1 -1
  71. package/dist/path.js +6 -3
  72. package/dist/pinned-write.d.ts.map +1 -1
  73. package/dist/pinned-write.js +44 -19
  74. package/dist/private-file-store.d.ts +5 -7
  75. package/dist/private-file-store.d.ts.map +1 -1
  76. package/dist/private-file-store.js +21 -34
  77. package/dist/private-temp-workspace.d.ts.map +1 -1
  78. package/dist/private-temp-workspace.js +28 -15
  79. package/dist/regular-file.d.ts.map +1 -1
  80. package/dist/regular-file.js +52 -8
  81. package/dist/replace-directory.d.ts.map +1 -1
  82. package/dist/replace-directory.js +5 -4
  83. package/dist/replace-file.d.ts +1 -1
  84. package/dist/replace-file.d.ts.map +1 -1
  85. package/dist/replace-file.js +93 -31
  86. package/dist/root-context.d.ts +23 -0
  87. package/dist/root-context.d.ts.map +1 -0
  88. package/dist/root-context.js +62 -0
  89. package/dist/root-errors.d.ts +4 -0
  90. package/dist/root-errors.d.ts.map +1 -0
  91. package/dist/root-errors.js +21 -0
  92. package/dist/root-impl.d.ts +108 -0
  93. package/dist/root-impl.d.ts.map +1 -0
  94. package/dist/{safe-root.js → root-impl.js} +484 -265
  95. package/dist/root.d.ts +1 -107
  96. package/dist/root.d.ts.map +1 -1
  97. package/dist/root.js +1 -1332
  98. package/dist/safe-path-segment.d.ts +9 -0
  99. package/dist/safe-path-segment.d.ts.map +1 -0
  100. package/dist/safe-path-segment.js +51 -0
  101. package/dist/secret-file.d.ts.map +1 -1
  102. package/dist/secret-file.js +6 -1
  103. package/dist/sibling-temp.d.ts +1 -0
  104. package/dist/sibling-temp.d.ts.map +1 -1
  105. package/dist/sibling-temp.js +32 -7
  106. package/dist/sidecar-lock.d.ts +2 -0
  107. package/dist/sidecar-lock.d.ts.map +1 -1
  108. package/dist/sidecar-lock.js +93 -13
  109. package/dist/store.d.ts +1 -0
  110. package/dist/store.d.ts.map +1 -1
  111. package/dist/store.js +1 -0
  112. package/dist/temp-target.d.ts.map +1 -1
  113. package/dist/temp-target.js +7 -4
  114. package/dist/test-hooks.d.ts +6 -0
  115. package/dist/test-hooks.d.ts.map +1 -1
  116. package/dist/text-atomic.d.ts +7 -0
  117. package/dist/text-atomic.d.ts.map +1 -1
  118. package/dist/text-atomic.js +3 -2
  119. package/dist/trash.d.ts.map +1 -1
  120. package/dist/trash.js +48 -13
  121. package/docs/advanced.md +110 -0
  122. package/docs/archive.md +199 -0
  123. package/docs/atomic.md +191 -0
  124. package/docs/config.md +94 -0
  125. package/docs/contributing.md +80 -0
  126. package/docs/errors.md +154 -0
  127. package/docs/file-store.md +213 -0
  128. package/docs/filename.md +0 -0
  129. package/docs/index.md +77 -0
  130. package/docs/install-path.md +167 -0
  131. package/docs/install.md +150 -0
  132. package/docs/json-store.md +204 -0
  133. package/docs/json.md +192 -0
  134. package/docs/local-roots.md +164 -0
  135. package/docs/output.md +92 -0
  136. package/docs/path-scope.md +130 -0
  137. package/docs/path.md +191 -0
  138. package/docs/permissions.md +95 -0
  139. package/docs/private-file-store.md +45 -0
  140. package/docs/python-helper.md +103 -0
  141. package/docs/quickstart.md +147 -0
  142. package/docs/reading.md +174 -0
  143. package/docs/regular-file.md +175 -0
  144. package/docs/root.md +205 -0
  145. package/docs/secret-file.md +154 -0
  146. package/docs/secure-file.md +82 -0
  147. package/docs/security-model.md +99 -0
  148. package/docs/sidecar-lock.md +230 -0
  149. package/docs/store.md +77 -0
  150. package/docs/temp.md +284 -0
  151. package/docs/test-hooks.md +74 -0
  152. package/docs/testing.md +178 -0
  153. package/docs/timing.md +121 -0
  154. package/docs/types.md +155 -0
  155. package/docs/walk.md +63 -0
  156. package/docs/writing.md +222 -0
  157. package/package.json +10 -2
  158. package/dist/archive-utils.d.ts +0 -3
  159. package/dist/archive-utils.d.ts.map +0 -1
  160. package/dist/archive-utils.js +0 -26
  161. package/dist/boundary-file-read.d.ts +0 -44
  162. package/dist/boundary-file-read.d.ts.map +0 -1
  163. package/dist/boundary-file-read.js +0 -129
  164. package/dist/boundary-path.d.ts +0 -39
  165. package/dist/boundary-path.d.ts.map +0 -1
  166. package/dist/boundary-path.js +0 -598
  167. package/dist/fs-pinned-path-helper.d.ts +0 -7
  168. package/dist/fs-pinned-path-helper.d.ts.map +0 -1
  169. package/dist/fs-pinned-path-helper.js +0 -182
  170. package/dist/fs-pinned-write-helper.d.ts +0 -21
  171. package/dist/fs-pinned-write-helper.d.ts.map +0 -1
  172. package/dist/fs-pinned-write-helper.js +0 -263
  173. package/dist/hardlink-guards.d.ts +0 -7
  174. package/dist/hardlink-guards.d.ts.map +0 -1
  175. package/dist/hardlink-guards.js +0 -30
  176. package/dist/install-safe-path.d.ts +0 -20
  177. package/dist/install-safe-path.d.ts.map +0 -1
  178. package/dist/install-safe-path.js +0 -94
  179. package/dist/json-file.d.ts +0 -3
  180. package/dist/json-file.d.ts.map +0 -1
  181. package/dist/json-file.js +0 -123
  182. package/dist/json-files.d.ts +0 -20
  183. package/dist/json-files.d.ts.map +0 -1
  184. package/dist/json-files.js +0 -153
  185. package/dist/path-alias-guards.d.ts +0 -19
  186. package/dist/path-alias-guards.d.ts.map +0 -1
  187. package/dist/path-alias-guards.js +0 -21
  188. package/dist/path-guards.d.ts +0 -7
  189. package/dist/path-guards.d.ts.map +0 -1
  190. package/dist/path-guards.js +0 -49
  191. package/dist/path-safety.d.ts +0 -12
  192. package/dist/path-safety.d.ts.map +0 -1
  193. package/dist/path-safety.js +0 -50
  194. package/dist/safe-open-sync.d.ts +0 -24
  195. package/dist/safe-open-sync.d.ts.map +0 -1
  196. package/dist/safe-open-sync.js +0 -71
  197. package/dist/safe-root.d.ts +0 -123
  198. package/dist/safe-root.d.ts.map +0 -1
  199. package/dist/secure-temp-workspace.d.ts +0 -25
  200. package/dist/secure-temp-workspace.d.ts.map +0 -1
  201. package/dist/secure-temp-workspace.js +0 -136
  202. package/dist/sibling-temp-file.d.ts +0 -16
  203. package/dist/sibling-temp-file.d.ts.map +0 -1
  204. package/dist/sibling-temp-file.js +0 -73
  205. package/dist/sibling-temp-write.d.ts +0 -8
  206. package/dist/sibling-temp-write.d.ts.map +0 -1
  207. package/dist/sibling-temp-write.js +0 -40
@@ -3,6 +3,7 @@ import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileStore, fileStoreSync, } from "./file-store.js";
6
+ import { openRootFileSync } from "./root-file.js";
6
7
  import { registerTempPathForExit } from "./temp-cleanup.js";
7
8
  function sanitizeTempPrefix(prefix) {
8
9
  const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
@@ -12,21 +13,20 @@ function sanitizeTempPrefix(prefix) {
12
13
  return sanitized.endsWith("-") ? sanitized : `${sanitized}-`;
13
14
  }
14
15
  function resolveWorkspaceLeaf(dir, fileName) {
15
- const raw = fileName.trim();
16
- if (!raw ||
17
- raw === "." ||
18
- raw === ".." ||
19
- raw.includes("\0") ||
20
- raw.includes("/") ||
21
- raw.includes("\\") ||
22
- path.basename(raw) !== raw) {
23
- throw new Error(`Invalid temp workspace file name: ${JSON.stringify(fileName)}`);
24
- }
25
- return path.join(dir, raw);
16
+ return path.join(dir, assertWorkspaceFileName(fileName));
26
17
  }
27
18
  function assertWorkspaceFileName(fileName) {
28
- resolveWorkspaceLeaf(".", fileName);
29
- return fileName.trim();
19
+ const value = fileName.trim();
20
+ if (!value ||
21
+ value === "." ||
22
+ value === ".." ||
23
+ value.includes("\0") ||
24
+ value.includes("/") ||
25
+ value.includes("\\") ||
26
+ path.basename(value) !== value) {
27
+ throw new Error(`Invalid temp workspace file name: ${JSON.stringify(fileName)}`);
28
+ }
29
+ return value;
30
30
  }
31
31
  async function ensurePrivateDirectory(dir, mode) {
32
32
  await fs.mkdir(dir, { recursive: true, mode });
@@ -144,8 +144,21 @@ export function tempWorkspaceSync(options) {
144
144
  trailingNewline: writeOptions?.trailingNewline,
145
145
  }),
146
146
  read: (fileName) => {
147
- const filePath = store.path(assertWorkspaceFileName(fileName));
148
- return fsSync.readFileSync(filePath);
147
+ const opened = openRootFileSync({
148
+ absolutePath: store.path(assertWorkspaceFileName(fileName)),
149
+ rootPath: dir,
150
+ boundaryLabel: "temp workspace",
151
+ rejectHardlinks: true,
152
+ });
153
+ if (!opened.ok) {
154
+ throw Object.assign(new Error(`File not found: ${fileName}`), { code: "ENOENT" });
155
+ }
156
+ try {
157
+ return fsSync.readFileSync(opened.fd);
158
+ }
159
+ finally {
160
+ fsSync.closeSync(opened.fd);
161
+ }
149
162
  },
150
163
  cleanup: () => {
151
164
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAO7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AAWD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CA6B3C;AA0CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAoBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
1
+ {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAQ7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AA2DD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAgC3C;AA6CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAoBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
@@ -17,6 +17,44 @@ function resolveRegularFileReadFlags() {
17
17
  ? fsSync.constants.O_NOFOLLOW
18
18
  : 0));
19
19
  }
20
+ async function readFileHandleBounded(params) {
21
+ if (params.maxBytes === undefined) {
22
+ return await params.handle.readFile();
23
+ }
24
+ const chunks = [];
25
+ const scratch = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, params.maxBytes + 1)));
26
+ let total = 0;
27
+ while (true) {
28
+ const { bytesRead } = await params.handle.read(scratch, 0, scratch.length, null);
29
+ if (bytesRead === 0) {
30
+ return Buffer.concat(chunks, total);
31
+ }
32
+ total += bytesRead;
33
+ if (total > params.maxBytes) {
34
+ throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
35
+ }
36
+ chunks.push(Buffer.from(scratch.subarray(0, bytesRead)));
37
+ }
38
+ }
39
+ function readFileDescriptorBounded(params) {
40
+ if (params.maxBytes === undefined) {
41
+ return fsSync.readFileSync(params.fd);
42
+ }
43
+ const chunks = [];
44
+ const scratch = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, params.maxBytes + 1)));
45
+ let total = 0;
46
+ while (true) {
47
+ const bytesRead = fsSync.readSync(params.fd, scratch, 0, scratch.length, null);
48
+ if (bytesRead === 0) {
49
+ return Buffer.concat(chunks, total);
50
+ }
51
+ total += bytesRead;
52
+ if (total > params.maxBytes) {
53
+ throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
54
+ }
55
+ chunks.push(Buffer.from(scratch.subarray(0, bytesRead)));
56
+ }
57
+ }
20
58
  export async function statRegularFile(filePath) {
21
59
  let stat;
22
60
  try {
@@ -69,10 +107,13 @@ export async function readRegularFile(params) {
69
107
  if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
70
108
  throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
71
109
  }
72
- const buffer = await handle.readFile();
73
- if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
74
- throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
75
- }
110
+ // With a byte cap, avoid readFile(): a raced file growth would allocate
111
+ // the oversized content before the post-read check could reject it.
112
+ const buffer = await readFileHandleBounded({
113
+ handle,
114
+ filePath: params.filePath,
115
+ maxBytes: params.maxBytes,
116
+ });
76
117
  return { buffer, stat };
77
118
  }
78
119
  finally {
@@ -99,10 +140,13 @@ function readOpenedRegularFileSync(params) {
99
140
  if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
100
141
  throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
101
142
  }
102
- const buffer = fsSync.readFileSync(params.fd);
103
- if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
104
- throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
105
- }
143
+ // Keep capped sync reads incremental for the same reason as async reads:
144
+ // readFileSync(fd) would buffer a raced oversized file before throwing.
145
+ const buffer = readFileDescriptorBounded({
146
+ fd: params.fd,
147
+ filePath: params.filePath,
148
+ maxBytes: params.maxBytes,
149
+ });
106
150
  return { buffer, stat };
107
151
  }
108
152
  export function readRegularFileSync(params) {
@@ -1 +1 @@
1
- {"version":3,"file":"replace-directory.d.ts","sourceRoot":"","sources":["../src/replace-directory.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiCf"}
1
+ {"version":3,"file":"replace-directory.d.ts","sourceRoot":"","sources":["../src/replace-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { guardedRename, guardedRm } from "./guarded-mutation.js";
3
4
  export async function replaceDirectoryAtomic(options) {
4
5
  const targetDir = path.resolve(options.targetDir);
5
6
  const stagedDir = path.resolve(options.stagedDir);
@@ -8,7 +9,7 @@ export async function replaceDirectoryAtomic(options) {
8
9
  let backupCreated = false;
9
10
  await fs.mkdir(parentDir, { recursive: true });
10
11
  try {
11
- await fs.rename(targetDir, backupDir);
12
+ await guardedRename({ from: targetDir, to: backupDir });
12
13
  backupCreated = true;
13
14
  }
14
15
  catch (err) {
@@ -17,16 +18,16 @@ export async function replaceDirectoryAtomic(options) {
17
18
  }
18
19
  }
19
20
  try {
20
- await fs.rename(stagedDir, targetDir);
21
+ await guardedRename({ from: stagedDir, to: targetDir });
21
22
  }
22
23
  catch (err) {
23
24
  if (backupCreated) {
24
- await fs.rename(backupDir, targetDir).catch(() => undefined);
25
+ await guardedRename({ from: backupDir, to: targetDir }).catch(() => undefined);
25
26
  backupCreated = false;
26
27
  }
27
28
  throw err;
28
29
  }
29
30
  if (backupCreated) {
30
- await fs.rm(backupDir, { recursive: true, force: true });
31
+ await guardedRm({ target: backupDir, recursive: true, force: true, verifyAfter: false });
31
32
  }
32
33
  }
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  export type ReplaceFileAtomicFileSystem = {
4
4
  promises: Pick<typeof fs, "mkdir" | "chmod" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat">;
5
5
  };
6
- export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "chmodSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "statSync" | "lstatSync">;
6
+ export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "chmodSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "statSync" | "lstatSync">;
7
7
  type ReplaceFileAtomicBaseOptions = {
8
8
  filePath: string;
9
9
  content: string | Uint8Array;
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAKlC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,CACd,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AA+NF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAMlC;AAyDD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAiEzB"}
1
+ {"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAMlC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,cAAc,GACd,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,CACd,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AAySF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAMlC;AAyDD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAiEzB"}
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import syncFs from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { assertSafePathPrefix } from "./safe-path-segment.js";
5
6
  import { registerTempPathForExit } from "./temp-cleanup.js";
6
7
  import { serializePathWrite } from "./write-queue.js";
7
8
  function isRetryableRenameError(error) {
@@ -11,6 +12,12 @@ function isPermissionRenameError(error) {
11
12
  const code = error.code;
12
13
  return code === "EPERM" || code === "EEXIST";
13
14
  }
15
+ const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants;
16
+ const OPEN_READ_FLAGS = syncFs.constants.O_RDONLY | (SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0);
17
+ const OPEN_WRITE_EXCLUSIVE_FLAGS = syncFs.constants.O_WRONLY |
18
+ syncFs.constants.O_CREAT |
19
+ syncFs.constants.O_EXCL |
20
+ (SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0);
14
21
  async function sleep(ms) {
15
22
  await new Promise((resolve) => setTimeout(resolve, ms));
16
23
  }
@@ -26,17 +33,7 @@ async function renameWithRetry(params) {
26
33
  continue;
27
34
  }
28
35
  if (params.copyFallbackOnPermissionError && isPermissionRenameError(error)) {
29
- const stat = await params.fsModule.lstat(params.dest).catch((lstatError) => {
30
- if (lstatError.code === "ENOENT") {
31
- return null;
32
- }
33
- throw lstatError;
34
- });
35
- if (stat?.isSymbolicLink()) {
36
- throw new Error(`Refusing copy fallback through symlink destination: ${params.dest}`);
37
- }
38
- await params.fsModule.copyFile(params.src, params.dest);
39
- await params.fsModule.unlink(params.src).catch(() => undefined);
36
+ await copyFallbackReplace(params.fsModule, params.src, params.dest);
40
37
  return { method: "copy-fallback" };
41
38
  }
42
39
  throw error;
@@ -62,25 +59,7 @@ function renameWithRetrySync(params) {
62
59
  continue;
63
60
  }
64
61
  if (params.copyFallbackOnPermissionError && isPermissionRenameError(error)) {
65
- let stat = null;
66
- try {
67
- stat = params.fsModule.lstatSync(params.dest);
68
- }
69
- catch (lstatError) {
70
- if (lstatError.code !== "ENOENT") {
71
- throw lstatError;
72
- }
73
- }
74
- if (stat?.isSymbolicLink()) {
75
- throw new Error(`Refusing copy fallback through symlink destination: ${params.dest}`);
76
- }
77
- params.fsModule.copyFileSync(params.src, params.dest);
78
- try {
79
- params.fsModule.unlinkSync(params.src);
80
- }
81
- catch {
82
- // Best-effort cleanup after fallback replacement.
83
- }
62
+ copyFallbackReplaceSync(params.fsModule, params.src, params.dest);
84
63
  return { method: "copy-fallback" };
85
64
  }
86
65
  throw error;
@@ -88,6 +67,88 @@ function renameWithRetrySync(params) {
88
67
  }
89
68
  throw new Error("Atomic rename retry loop exhausted.");
90
69
  }
70
+ async function copyFallbackReplace(fsModule, src, dest) {
71
+ const sourceStat = await fsModule.lstat(src);
72
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
73
+ throw new Error(`Refusing copy fallback from non-file source: ${src}`);
74
+ }
75
+ const destStat = await fsModule.lstat(dest).catch((lstatError) => {
76
+ if (lstatError.code === "ENOENT") {
77
+ return null;
78
+ }
79
+ throw lstatError;
80
+ });
81
+ if (destStat?.isSymbolicLink()) {
82
+ throw new Error(`Refusing copy fallback through symlink destination: ${dest}`);
83
+ }
84
+ if (destStat) {
85
+ await fsModule.rm(dest, { force: true });
86
+ }
87
+ let sourceHandle = null;
88
+ let destHandle = null;
89
+ try {
90
+ sourceHandle = await fsModule.open(src, OPEN_READ_FLAGS);
91
+ destHandle = await fsModule.open(dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
92
+ await destHandle.writeFile(await sourceHandle.readFile());
93
+ }
94
+ finally {
95
+ await destHandle?.close().catch(() => undefined);
96
+ await sourceHandle?.close().catch(() => undefined);
97
+ }
98
+ await fsModule.unlink(src).catch(() => undefined);
99
+ }
100
+ function copyFallbackReplaceSync(fsModule, src, dest) {
101
+ const sourceStat = fsModule.lstatSync(src);
102
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
103
+ throw new Error(`Refusing copy fallback from non-file source: ${src}`);
104
+ }
105
+ let destStat = null;
106
+ try {
107
+ destStat = fsModule.lstatSync(dest);
108
+ }
109
+ catch (lstatError) {
110
+ if (lstatError.code !== "ENOENT") {
111
+ throw lstatError;
112
+ }
113
+ }
114
+ if (destStat?.isSymbolicLink()) {
115
+ throw new Error(`Refusing copy fallback through symlink destination: ${dest}`);
116
+ }
117
+ if (destStat) {
118
+ fsModule.rmSync(dest, { force: true });
119
+ }
120
+ let sourceFd;
121
+ let destFd;
122
+ try {
123
+ sourceFd = fsModule.openSync(src, OPEN_READ_FLAGS);
124
+ destFd = fsModule.openSync(dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
125
+ fsModule.writeFileSync(destFd, fsModule.readFileSync(sourceFd));
126
+ }
127
+ finally {
128
+ if (destFd !== undefined) {
129
+ try {
130
+ fsModule.closeSync(destFd);
131
+ }
132
+ catch {
133
+ // Best-effort close after fallback replacement.
134
+ }
135
+ }
136
+ if (sourceFd !== undefined) {
137
+ try {
138
+ fsModule.closeSync(sourceFd);
139
+ }
140
+ catch {
141
+ // Best-effort close after fallback replacement.
142
+ }
143
+ }
144
+ }
145
+ try {
146
+ fsModule.unlinkSync(src);
147
+ }
148
+ catch {
149
+ // Best-effort cleanup after fallback replacement.
150
+ }
151
+ }
91
152
  function validateReplaceFilePath(filePath) {
92
153
  if (!filePath || filePath.includes("\0")) {
93
154
  throw new Error("Atomic replace file path must be non-empty.");
@@ -95,7 +156,8 @@ function validateReplaceFilePath(filePath) {
95
156
  }
96
157
  function buildReplaceTempPath(filePath, tempPrefix) {
97
158
  const dir = path.dirname(filePath);
98
- return path.join(dir, `${tempPrefix ?? ".fs-safe-replace"}.${process.pid}.${randomUUID()}.tmp`);
159
+ const safePrefix = assertSafePathPrefix(tempPrefix ?? ".fs-safe-replace", { label: "atomic replace temp prefix" });
160
+ return path.join(dir, `${safePrefix}.${process.pid}.${randomUUID()}.tmp`);
99
161
  }
100
162
  async function resolveMode(options) {
101
163
  const defaultMode = options.mode ?? 0o600;
@@ -0,0 +1,23 @@
1
+ export type RootContext = {
2
+ rootDir: string;
3
+ rootReal: string;
4
+ rootWithSep: string;
5
+ };
6
+ export declare const ensureTrailingSep: (value: string) => string;
7
+ export declare function assertValidRootRelativePath(relativePath: string): void;
8
+ export declare function expandRelativePathWithHome(relativePath: string): Promise<string>;
9
+ export declare function resolveRootContext(rootDir: string): Promise<RootContext>;
10
+ export declare function resolvePathInRoot(root: RootContext, relativePath: string): Promise<{
11
+ rootReal: string;
12
+ rootWithSep: string;
13
+ resolved: string;
14
+ }>;
15
+ export declare function resolvePathWithinRoot(params: {
16
+ rootDir: string;
17
+ relativePath: string;
18
+ }): Promise<{
19
+ rootReal: string;
20
+ rootWithSep: string;
21
+ resolved: string;
22
+ }>;
23
+ //# sourceMappingURL=root-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAID,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuB9E;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAQtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { expandHomePrefix } from "./home-dir.js";
6
+ import { assertNoNulPathInput, isNotFoundPathError, isPathInside } from "./path.js";
7
+ export const ensureTrailingSep = (value) => value.endsWith(path.sep) ? value : value + path.sep;
8
+ export function assertValidRootRelativePath(relativePath) {
9
+ assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
10
+ }
11
+ let cachedHomePath;
12
+ export async function expandRelativePathWithHome(relativePath) {
13
+ const rawHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
14
+ if (cachedHomePath?.raw !== rawHome) {
15
+ let realHome = rawHome;
16
+ try {
17
+ realHome = await fs.realpath(rawHome);
18
+ }
19
+ catch {
20
+ // If the home dir cannot be canonicalized, keep lexical expansion behavior.
21
+ }
22
+ cachedHomePath = { raw: rawHome, real: realHome };
23
+ }
24
+ return expandHomePrefix(relativePath, { home: cachedHomePath.real });
25
+ }
26
+ export async function resolveRootContext(rootDir) {
27
+ assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
28
+ let rootReal;
29
+ try {
30
+ rootReal = await fs.realpath(rootDir);
31
+ const rootStat = await fs.stat(rootReal);
32
+ if (!rootStat.isDirectory()) {
33
+ throw new FsSafeError("invalid-path", "root dir is not a directory");
34
+ }
35
+ }
36
+ catch (err) {
37
+ if (err instanceof FsSafeError) {
38
+ throw err;
39
+ }
40
+ if (isNotFoundPathError(err)) {
41
+ throw new FsSafeError("not-found", "root dir not found");
42
+ }
43
+ throw err;
44
+ }
45
+ return {
46
+ rootDir: path.resolve(rootDir),
47
+ rootReal,
48
+ rootWithSep: ensureTrailingSep(rootReal),
49
+ };
50
+ }
51
+ export async function resolvePathInRoot(root, relativePath) {
52
+ assertValidRootRelativePath(relativePath);
53
+ const expanded = await expandRelativePathWithHome(relativePath);
54
+ const resolved = path.resolve(root.rootWithSep, expanded);
55
+ if (!isPathInside(root.rootWithSep, resolved)) {
56
+ throw new FsSafeError("outside-workspace", "file is outside workspace root");
57
+ }
58
+ return { rootReal: root.rootReal, rootWithSep: root.rootWithSep, resolved };
59
+ }
60
+ export async function resolvePathWithinRoot(params) {
61
+ return await resolvePathInRoot(await resolveRootContext(params.rootDir), params.relativePath);
62
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isAlreadyExistsError(error: unknown): boolean;
2
+ export declare function normalizePinnedWriteError(error: unknown): Error;
3
+ export declare function normalizePinnedPathError(error: unknown): Error;
4
+ //# sourceMappingURL=root-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO9D"}
@@ -0,0 +1,21 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { hasNodeErrorCode } from "./path.js";
3
+ export function isAlreadyExistsError(error) {
4
+ return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
5
+ }
6
+ export function normalizePinnedWriteError(error) {
7
+ if (error instanceof FsSafeError) {
8
+ return error;
9
+ }
10
+ return new FsSafeError("invalid-path", "path is not a regular file under root", {
11
+ cause: error instanceof Error ? error : undefined,
12
+ });
13
+ }
14
+ export function normalizePinnedPathError(error) {
15
+ if (error instanceof FsSafeError) {
16
+ return error;
17
+ }
18
+ return new FsSafeError("path-alias", "path is not under root", {
19
+ cause: error instanceof Error ? error : undefined,
20
+ });
21
+ }
@@ -0,0 +1,108 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import type { DirEntry, PathStat } from "./types.js";
4
+ export type OpenResult = {
5
+ handle: FileHandle;
6
+ realPath: string;
7
+ stat: Stats;
8
+ [Symbol.asyncDispose](): Promise<void>;
9
+ };
10
+ export type ReadResult = {
11
+ buffer: Buffer;
12
+ realPath: string;
13
+ stat: Stats;
14
+ };
15
+ export type RootOptions = {
16
+ rootDir: string;
17
+ defaults?: RootDefaults;
18
+ };
19
+ export type SymlinkPolicy = "reject" | "follow-within-root";
20
+ export type HardlinkPolicy = "reject" | "allow";
21
+ export type WritableOpenMode = "replace" | "append" | "update";
22
+ export type RootDefaults = {
23
+ hardlinks?: HardlinkPolicy;
24
+ maxBytes?: number;
25
+ mkdir?: boolean;
26
+ mode?: number;
27
+ nonBlockingRead?: boolean;
28
+ symlinks?: SymlinkPolicy;
29
+ };
30
+ export type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
31
+ export type RootOpenOptions = Omit<RootReadOptions, "maxBytes">;
32
+ export type RootWriteOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
33
+ encoding?: BufferEncoding;
34
+ overwrite?: boolean;
35
+ };
36
+ export type RootOpenWritableOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
37
+ writeMode?: WritableOpenMode;
38
+ };
39
+ export type RootCopyOptions = Pick<RootDefaults, "maxBytes" | "mkdir" | "mode"> & {
40
+ sourceHardlinks?: HardlinkPolicy;
41
+ };
42
+ export type RootWriteJsonOptions = RootWriteOptions & {
43
+ replacer?: Parameters<typeof JSON.stringify>[1];
44
+ space?: Parameters<typeof JSON.stringify>[2];
45
+ trailingNewline?: boolean;
46
+ };
47
+ export type RootCreateOptions = Omit<RootWriteOptions, "overwrite">;
48
+ export type RootCreateJsonOptions = Omit<RootWriteJsonOptions, "overwrite">;
49
+ export type RootAppendOptions = RootWriteOptions & {
50
+ prependNewlineIfNeeded?: boolean;
51
+ };
52
+ export declare const DEFAULT_ROOT_MAX_BYTES: number;
53
+ export interface Root {
54
+ readonly rootDir: string;
55
+ readonly rootReal: string;
56
+ readonly rootWithSep: string;
57
+ readonly defaults: RootDefaults;
58
+ resolve(relativePath: string): Promise<string>;
59
+ open(relativePath: string, options?: RootOpenOptions): Promise<OpenResult>;
60
+ read(relativePath: string, options?: RootReadOptions): Promise<ReadResult>;
61
+ readBytes(relativePath: string, options?: RootReadOptions): Promise<Buffer>;
62
+ readText(relativePath: string, options?: RootReadOptions & {
63
+ encoding?: BufferEncoding;
64
+ }): Promise<string>;
65
+ readJson<T = unknown>(relativePath: string, options?: RootReadOptions & {
66
+ encoding?: BufferEncoding;
67
+ }): Promise<T>;
68
+ readAbsolute(filePath: string, options?: RootReadOptions): Promise<ReadResult>;
69
+ reader(options?: RootReadOptions): (filePath: string) => Promise<Buffer>;
70
+ openWritable(relativePath: string, options?: RootOpenWritableOptions): Promise<WritableOpenResult>;
71
+ append(relativePath: string, data: string | Buffer, options?: RootAppendOptions): Promise<void>;
72
+ remove(relativePath: string): Promise<void>;
73
+ mkdir(relativePath: string): Promise<void>;
74
+ ensureRoot(): Promise<void>;
75
+ write(relativePath: string, data: string | Buffer, options?: RootWriteOptions): Promise<void>;
76
+ create(relativePath: string, data: string | Buffer, options?: RootCreateOptions): Promise<void>;
77
+ writeJson(relativePath: string, data: unknown, options?: RootWriteJsonOptions): Promise<void>;
78
+ createJson(relativePath: string, data: unknown, options?: RootCreateJsonOptions): Promise<void>;
79
+ copyIn(relativePath: string, sourcePath: string, options?: RootCopyOptions): Promise<void>;
80
+ exists(relativePath: string): Promise<boolean>;
81
+ stat(relativePath: string): Promise<PathStat>;
82
+ list(relativePath: string, options?: {
83
+ withFileTypes?: false;
84
+ }): Promise<string[]>;
85
+ list(relativePath: string, options: {
86
+ withFileTypes: true;
87
+ }): Promise<DirEntry[]>;
88
+ move(fromRelative: string, toRelative: string, options?: {
89
+ overwrite?: boolean;
90
+ }): Promise<void>;
91
+ }
92
+ export declare function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
93
+ export declare function readLocalFileSafely(params: {
94
+ filePath: string;
95
+ maxBytes?: number;
96
+ }): Promise<ReadResult>;
97
+ export declare function openLocalFileSafely(params: {
98
+ filePath: string;
99
+ }): Promise<OpenResult>;
100
+ export type WritableOpenResult = {
101
+ handle: FileHandle;
102
+ createdForWrite: boolean;
103
+ realPath: string;
104
+ stat: Stats;
105
+ [Symbol.asyncDispose](): Promise<void>;
106
+ };
107
+ export declare function resolveOpenedFileRealPathForHandle(handle: FileHandle, ioPath: string): Promise<string>;
108
+ //# sourceMappingURL=root-impl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}