@openclaw/fs-safe 0.5.2 → 0.5.4

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 (79) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +3 -2
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +12 -1
  5. package/dist/archive-errors.d.ts +1 -0
  6. package/dist/archive-errors.d.ts.map +1 -1
  7. package/dist/archive-errors.js +4 -0
  8. package/dist/archive-native.d.ts.map +1 -1
  9. package/dist/archive-native.js +6 -3
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +43 -36
  12. package/dist/archive-tar-meta.d.ts.map +1 -1
  13. package/dist/archive-tar-meta.js +8 -0
  14. package/dist/archive-tar-runtime.d.ts +1 -0
  15. package/dist/archive-tar-runtime.d.ts.map +1 -1
  16. package/dist/archive-tar-runtime.js +11 -0
  17. package/dist/archive-tar.d.ts.map +1 -1
  18. package/dist/archive-tar.js +1 -0
  19. package/dist/archive-zip-entry.d.ts +6 -1
  20. package/dist/archive-zip-entry.d.ts.map +1 -1
  21. package/dist/archive-zip-entry.js +11 -1
  22. package/dist/archive-zip-integrity.d.ts +2 -1
  23. package/dist/archive-zip-integrity.d.ts.map +1 -1
  24. package/dist/archive-zip-integrity.js +12 -2
  25. package/dist/archive-zip-preflight.d.ts.map +1 -1
  26. package/dist/archive-zip-preflight.js +8 -2
  27. package/dist/archive.d.ts.map +1 -1
  28. package/dist/archive.js +6 -5
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +1 -0
  32. package/dist/file-hash.d.ts.map +1 -1
  33. package/dist/file-hash.js +2 -11
  34. package/dist/file-store.d.ts.map +1 -1
  35. package/dist/file-store.js +31 -16
  36. package/dist/move-path.d.ts.map +1 -1
  37. package/dist/move-path.js +2 -10
  38. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  39. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  40. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  41. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  42. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  43. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  44. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  45. package/dist/pinned-open.d.ts.map +1 -1
  46. package/dist/pinned-open.js +23 -6
  47. package/dist/private-temp-workspace.d.ts.map +1 -1
  48. package/dist/private-temp-workspace.js +45 -4
  49. package/dist/publish-file-failure.d.ts +1 -0
  50. package/dist/publish-file-failure.d.ts.map +1 -1
  51. package/dist/publish-file-failure.js +2 -1
  52. package/dist/publish-file.d.ts.map +1 -1
  53. package/dist/publish-file.js +49 -36
  54. package/dist/read-error.d.ts +2 -0
  55. package/dist/read-error.d.ts.map +1 -0
  56. package/dist/read-error.js +11 -0
  57. package/dist/read-open-flags.d.ts +8 -0
  58. package/dist/read-open-flags.d.ts.map +1 -0
  59. package/dist/read-open-flags.js +13 -0
  60. package/dist/regular-file.d.ts.map +1 -1
  61. package/dist/regular-file.js +3 -8
  62. package/dist/root-impl.d.ts.map +1 -1
  63. package/dist/root-impl.js +6 -6
  64. package/dist/secret-file.d.ts.map +1 -1
  65. package/dist/secret-file.js +10 -3
  66. package/dist/secret-read-async.d.ts.map +1 -1
  67. package/dist/secret-read-async.js +5 -1
  68. package/dist/symlink-parents.d.ts.map +1 -1
  69. package/dist/symlink-parents.js +9 -8
  70. package/dist/trash.d.ts.map +1 -1
  71. package/dist/trash.js +10 -1
  72. package/docs/advanced.md +6 -0
  73. package/docs/archive.md +2 -2
  74. package/docs/errors.md +5 -3
  75. package/docs/file-store.md +4 -0
  76. package/docs/secret-file.md +4 -0
  77. package/docs/temp.md +5 -0
  78. package/docs/types.md +2 -2
  79. package/package.json +10 -3
@@ -8,7 +8,8 @@ import { ensureParentInRoot, openWritableStoreRoot, writeStreamToTempSource, } f
8
8
  import { writeFileSyncAtomic } from "./file-store-sync-write.js";
9
9
  import { createJsonStore } from "./json-document-store.js";
10
10
  import { stringifyJsonDocument } from "./json-stringify.js";
11
- import { resolveSafeRelativePath } from "./path.js";
11
+ import { isNotFoundPathError, resolveSafeRelativePath } from "./path.js";
12
+ import { throwFsSafeReadError } from "./read-error.js";
12
13
  import { root } from "./root.js";
13
14
  import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
14
15
  import { readRegularFile } from "./regular-file.js";
@@ -20,7 +21,19 @@ function assertRelativePath(relativePath) {
20
21
  if (!raw || raw !== relativePath) {
21
22
  throw new FsSafeError("invalid-path", "store key must be non-empty and unpadded");
22
23
  }
23
- return assertNoDriveRelativePathSegments(raw.replaceAll("\\", "/"), "store key");
24
+ assertNoDriveRelativePathSegments(raw.replaceAll("\\", "/"), "store key");
25
+ const segments = raw.split("/");
26
+ const delegated = segments.includes("..") || raw.includes("\\") ||
27
+ path.posix.isAbsolute(raw) || path.win32.isAbsolute(raw) || raw.startsWith("//");
28
+ if (delegated)
29
+ return raw;
30
+ if (segments.every((segment) => segment.length === 0 || segment === ".") ||
31
+ segments.some((segment) => segment.length === 0 || segment === ".") ||
32
+ raw.normalize("NFC") !== raw ||
33
+ segments.some((segment) => /[ .]$/u.test(segment))) {
34
+ throw new FsSafeError("invalid-path", "store key must use one canonical relative spelling");
35
+ }
36
+ return raw;
24
37
  }
25
38
  function resolveStorePath(rootDir, relativePath) {
26
39
  return resolveSafeRelativePath(rootDir, assertRelativePath(relativePath));
@@ -31,13 +44,9 @@ function assertMaxBytes(size, maxBytes) {
31
44
  }
32
45
  }
33
46
  function isNotFound(error) {
34
- if (!error) {
35
- return false;
36
- }
37
47
  return error instanceof FsSafeError
38
48
  ? error.code === "not-found"
39
- : error.code === "ENOENT" ||
40
- error.code === "ENOTDIR";
49
+ : isNotFoundPathError(error);
41
50
  }
42
51
  function handleSyncStoreReadOpenFailure(opened) {
43
52
  return matchRootFileOpenFailure(opened, {
@@ -50,12 +59,16 @@ function handleSyncStoreReadOpenFailure(opened) {
50
59
  });
51
60
  },
52
61
  validation: (failure) => {
62
+ if (failure.error instanceof FsSafeError) {
63
+ throw failure.error;
64
+ }
53
65
  // Validation failures mean the path existed but violated store policy
54
66
  // (directory, hardlink, symlink race). Do not report them as missing.
55
67
  throw new FsSafeError("path-mismatch", "store target failed read validation", {
56
68
  cause: failure.error instanceof Error ? failure.error : undefined,
57
69
  });
58
70
  },
71
+ io: (failure) => throwFsSafeReadError(failure.error, "store"),
59
72
  fallback: (failure) => {
60
73
  throw new FsSafeError("path-mismatch", "store target changed during read", {
61
74
  cause: failure.error instanceof Error ? failure.error : undefined,
@@ -68,9 +81,7 @@ async function readFileStoreCopySource(params) {
68
81
  if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
69
82
  throw new FsSafeError("not-file", "source path is not a file");
70
83
  }
71
- if (params.maxBytes !== undefined && sourceStat.size > params.maxBytes) {
72
- throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`);
73
- }
84
+ assertMaxBytes(sourceStat.size, params.maxBytes);
74
85
  try {
75
86
  return (await readRegularFile({ filePath: params.sourcePath, maxBytes: params.maxBytes }))
76
87
  .buffer;
@@ -230,7 +241,7 @@ export function fileStore(options) {
230
241
  if (isNotFound(error)) {
231
242
  return null;
232
243
  }
233
- throw error;
244
+ throwFsSafeReadError(error, "store");
234
245
  }
235
246
  },
236
247
  readJson: async (relativePath, readOptions) => {
@@ -246,7 +257,7 @@ export function fileStore(options) {
246
257
  if (isNotFound(error)) {
247
258
  return null;
248
259
  }
249
- throw error;
260
+ throwFsSafeReadError(error, "store");
250
261
  }
251
262
  },
252
263
  remove: async (relativePath) => {
@@ -321,10 +332,14 @@ export function fileStoreSync(options) {
321
332
  try {
322
333
  assertMaxBytes(opened.stat.size, readOptions?.maxBytes ?? maxBytes);
323
334
  const limit = readOptions?.maxBytes ?? maxBytes;
324
- const raw = limit === undefined
325
- ? syncFs.readFileSync(opened.fd, "utf8")
326
- : readFileDescriptorBoundedSync(opened.fd, limit).toString("utf8");
327
- return raw;
335
+ try {
336
+ return limit === undefined
337
+ ? syncFs.readFileSync(opened.fd, "utf8")
338
+ : readFileDescriptorBoundedSync(opened.fd, limit).toString("utf8");
339
+ }
340
+ catch (error) {
341
+ throwFsSafeReadError(error, "store");
342
+ }
328
343
  }
329
344
  finally {
330
345
  syncFs.closeSync(opened.fd);
@@ -1 +1 @@
1
- {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AA6VD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA2Df"}
1
+ {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiVD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA2Df"}
package/dist/move-path.js CHANGED
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { guardedRename } from "./guarded-mutation.js";
7
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
7
8
  import { registerTempPathForExit } from "./temp-cleanup.js";
8
9
  export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
9
10
  const code = error?.code;
@@ -94,15 +95,6 @@ async function assertSourceStillMatches(sourcePath, identity) {
94
95
  throw sourceChangedError(sourcePath);
95
96
  }
96
97
  }
97
- function regularReadFlags() {
98
- return (fsConstants.O_RDONLY |
99
- (typeof fsConstants.O_NOFOLLOW === "number" && process.platform !== "win32"
100
- ? fsConstants.O_NOFOLLOW
101
- : 0) |
102
- (typeof fsConstants.O_NONBLOCK === "number" && process.platform !== "win32"
103
- ? fsConstants.O_NONBLOCK
104
- : 0));
105
- }
106
98
  async function chmodDirectoryPinned(directoryPath, mode) {
107
99
  if (process.platform === "win32") {
108
100
  // Node cannot portably open a Windows directory for descriptor-bound
@@ -129,7 +121,7 @@ async function copyRegularFilePinned(params) {
129
121
  let destinationCreated = false;
130
122
  let sourceHandle;
131
123
  try {
132
- sourceHandle = await fs.open(params.from, regularReadFlags());
124
+ sourceHandle = await fs.open(params.from, resolveReadOpenFlags());
133
125
  }
134
126
  catch (error) {
135
127
  const code = error?.code;
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAIzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAiEvB"}
1
+ {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CA+EvB"}
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import { isUnsafeDeviceReadPath } from "./device-path.js";
3
+ import { FsSafeError } from "./errors.js";
3
4
  import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
5
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
4
6
  function isExpectedPathError(error) {
5
7
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
6
8
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
@@ -11,8 +13,7 @@ function sameFileIdentity(left, right) {
11
13
  export function openPinnedFileSync(params) {
12
14
  const ioFs = params.ioFs ?? fs;
13
15
  const allowedType = params.allowedType ?? "file";
14
- const openReadFlags = ioFs.constants.O_RDONLY |
15
- (typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
16
+ const openReadFlags = resolveReadOpenFlags({ constants: ioFs.constants });
16
17
  let fd = null;
17
18
  try {
18
19
  if (isUnsafeDeviceReadPath(params.filePath)) {
@@ -30,10 +31,18 @@ export function openPinnedFileSync(params) {
30
31
  }
31
32
  const preOpenStat = ioFs.lstatSync(realPath);
32
33
  if (!isAllowedType(preOpenStat, allowedType)) {
33
- return { ok: false, reason: "validation" };
34
+ return {
35
+ ok: false,
36
+ reason: "validation",
37
+ error: new FsSafeError("not-file", "path does not have the required file type"),
38
+ };
34
39
  }
35
40
  if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
36
- return { ok: false, reason: "validation" };
41
+ return {
42
+ ok: false,
43
+ reason: "validation",
44
+ error: new FsSafeError("hardlink", "path must not be hardlinked"),
45
+ };
37
46
  }
38
47
  if (params.maxBytes !== undefined &&
39
48
  preOpenStat.isFile() &&
@@ -43,10 +52,18 @@ export function openPinnedFileSync(params) {
43
52
  fd = ioFs.openSync(realPath, openReadFlags);
44
53
  const openedStat = ioFs.fstatSync(fd);
45
54
  if (!isAllowedType(openedStat, allowedType)) {
46
- return { ok: false, reason: "validation" };
55
+ return {
56
+ ok: false,
57
+ reason: "validation",
58
+ error: new FsSafeError("not-file", "path does not have the required file type"),
59
+ };
47
60
  }
48
61
  if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
49
- return { ok: false, reason: "validation" };
62
+ return {
63
+ ok: false,
64
+ reason: "validation",
65
+ error: new FsSafeError("hardlink", "path must not be hardlinked"),
66
+ };
50
67
  }
51
68
  if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
52
69
  return { ok: false, reason: "validation" };
@@ -1 +1 @@
1
- {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAsEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
1
+ {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAUzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA2KF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CA0EnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
@@ -3,8 +3,11 @@ 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
+ import { FsSafeError } from "./errors.js";
7
7
  import { sameFileIdentity } from "./file-identity.js";
8
+ import { isNotFoundPathError } from "./path.js";
9
+ import { throwFsSafeReadError } from "./read-error.js";
10
+ import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
8
11
  import { registerTempPathForExit, } from "./temp-cleanup.js";
9
12
  function sanitizeTempPrefix(prefix) {
10
13
  const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
@@ -29,6 +32,32 @@ function assertWorkspaceFileName(fileName) {
29
32
  }
30
33
  return value;
31
34
  }
35
+ function throwTempWorkspaceOpenFailure(failure) {
36
+ return matchRootFileOpenFailure(failure, {
37
+ path: ({ error }) => {
38
+ if (isNotFoundPathError(error)) {
39
+ throw new FsSafeError("not-found", "temp workspace file not found", { cause: error });
40
+ }
41
+ throw new FsSafeError("path-mismatch", "temp workspace target changed during read", {
42
+ cause: error,
43
+ });
44
+ },
45
+ validation: ({ error }) => {
46
+ if (error instanceof FsSafeError) {
47
+ throw error;
48
+ }
49
+ throw new FsSafeError("path-mismatch", "temp workspace target failed read validation", {
50
+ cause: error,
51
+ });
52
+ },
53
+ io: ({ error }) => throwFsSafeReadError(error, "temp workspace"),
54
+ fallback: ({ error }) => {
55
+ throw new FsSafeError("path-mismatch", "temp workspace target changed during read", {
56
+ cause: error,
57
+ });
58
+ },
59
+ });
60
+ }
32
61
  async function ensurePrivateDirectory(dir, mode) {
33
62
  await fs.mkdir(dir, { recursive: true, mode });
34
63
  const stat = await fs.stat(dir);
@@ -105,7 +134,14 @@ async function createTempWorkspace(options) {
105
134
  trailingNewline: writeOptions?.trailingNewline,
106
135
  }),
107
136
  copyIn: async (fileName, sourcePath) => await store.copyIn(assertWorkspaceFileName(fileName), sourcePath, { mode }),
108
- read: async (fileName) => await store.readBytes(assertWorkspaceFileName(fileName)),
137
+ read: async (fileName) => {
138
+ try {
139
+ return await store.readBytes(assertWorkspaceFileName(fileName));
140
+ }
141
+ catch (error) {
142
+ throwFsSafeReadError(error, "temp workspace");
143
+ }
144
+ },
109
145
  cleanup: async () => {
110
146
  try {
111
147
  return await cleanupWorkspace(dir, identity);
@@ -184,10 +220,15 @@ export function tempWorkspaceSync(options) {
184
220
  rejectHardlinks: true,
185
221
  });
186
222
  if (!opened.ok) {
187
- throw Object.assign(new Error(`File not found: ${fileName}`), { code: "ENOENT" });
223
+ throwTempWorkspaceOpenFailure(opened);
188
224
  }
189
225
  try {
190
- return fsSync.readFileSync(opened.fd);
226
+ try {
227
+ return fsSync.readFileSync(opened.fd);
228
+ }
229
+ catch (error) {
230
+ throwFsSafeReadError(error, "temp workspace");
231
+ }
191
232
  }
192
233
  finally {
193
234
  fsSync.closeSync(opened.fd);
@@ -18,6 +18,7 @@ export type PublishFailureState = {
18
18
  phase: PublishFileExclusiveFailurePhase;
19
19
  targetCreated: boolean;
20
20
  targetIdentity?: FileIdentityStat;
21
+ targetCleanupIdentity?: FileIdentityStat;
21
22
  preserveTarget: boolean;
22
23
  directorySync?: PublishFileExclusiveDirectorySyncFailure;
23
24
  };
@@ -1 +1 @@
1
- {"version":3,"file":"publish-file-failure.d.ts","sourceRoot":"","sources":["../src/publish-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,qCAAqC,GAAG,UAAU,GAAG,UAAU,CAAC;AAC5E,MAAM,MAAM,wCAAwC,GAAG;IACrD,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,gCAAgC,GACxC,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,eAAe,CAAC;AACpB,MAAM,MAAM,2BAA2B,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAC9E,MAAM,MAAM,kCAAkC,GAAG;IAC/C,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,OAAO,EAAE,2BAA2B,CAAC;IACrC,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gCAAgC,GACtC,IAAI,CAIN;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,2BAA2B,GACnC,WAAW,CAcb;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,wCAAwC,CAG1C"}
1
+ {"version":3,"file":"publish-file-failure.d.ts","sourceRoot":"","sources":["../src/publish-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,qCAAqC,GAAG,UAAU,GAAG,UAAU,CAAC;AAC5E,MAAM,MAAM,wCAAwC,GAAG;IACrD,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,gCAAgC,GACxC,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,eAAe,CAAC;AACpB,MAAM,MAAM,2BAA2B,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAC9E,MAAM,MAAM,kCAAkC,GAAG;IAC/C,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,OAAO,EAAE,2BAA2B,CAAC;IACrC,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,qBAAqB,CAAC,EAAE,gBAAgB,CAAC;IACzC,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gCAAgC,GACtC,IAAI,CAKN;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,2BAA2B,GACnC,WAAW,CAcb;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,wCAAwC,CAG1C"}
@@ -1,7 +1,8 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  export function rememberCreatedTarget(state, identity, phase) {
3
3
  state.targetCreated = true;
4
- state.targetIdentity = { dev: identity.dev, ino: identity.ino };
4
+ state.targetIdentity = { dev: Number(identity.dev), ino: Number(identity.ino) };
5
+ state.targetCleanupIdentity = { dev: identity.dev, ino: identity.ino };
5
6
  state.phase = phase;
6
7
  }
7
8
  export function publicationFailure(error, state, cleanup) {
@@ -1 +1 @@
1
- {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI7C,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAoND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA8LtC"}
1
+ {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AA2ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
@@ -38,12 +38,12 @@ function directoryOpenFlags() {
38
38
  return (fsSync.constants.O_RDONLY |
39
39
  (typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
40
40
  }
41
- async function openNativeParent(binding, filePath) {
41
+ async function openNativeParent(filePath) {
42
42
  const parentPath = path.dirname(filePath);
43
43
  const handle = await fs.open(parentPath, directoryOpenFlags());
44
44
  try {
45
- const pathname = await fs.lstat(parentPath);
46
- const opened = binding.fstatIdentity(handle.fd);
45
+ const pathname = await fs.lstat(parentPath, { bigint: true });
46
+ const opened = await handle.stat({ bigint: true });
47
47
  if (pathname.isSymbolicLink() || !sameFileIdentity(pathname, opened)) {
48
48
  throw new FsSafeError("path-mismatch", "publication parent changed while opening");
49
49
  }
@@ -55,8 +55,10 @@ async function openNativeParent(binding, filePath) {
55
55
  }
56
56
  }
57
57
  async function assertPinnedSourceCurrent(params) {
58
- const opened = await params.handle.stat();
59
- const current = await fs.lstat(params.sourcePath);
58
+ // Windows file indexes can exceed Number.MAX_SAFE_INTEGER, so publication
59
+ // fences must compare bigint stats instead of rounded numeric identities.
60
+ const opened = await params.handle.stat({ bigint: true });
61
+ const current = await fs.lstat(params.sourcePath, { bigint: true });
60
62
  if (!opened.isFile() ||
61
63
  current.isSymbolicLink() ||
62
64
  !current.isFile() ||
@@ -90,13 +92,13 @@ async function copyPinnedSource(params) {
90
92
  throw error;
91
93
  }
92
94
  let target;
93
- const createdIdentity = params.native.fstatIdentity(nativeFd);
95
+ const createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
94
96
  rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
95
97
  try {
96
98
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
97
- const identity = await fs.lstat(params.targetPath);
99
+ const identity = await fs.lstat(params.targetPath, { bigint: true });
98
100
  target = await fs.open(params.targetPath, sourceOpenFlags());
99
- const opened = await target.stat();
101
+ const opened = await target.stat({ bigint: true });
100
102
  if (identity.isSymbolicLink() ||
101
103
  !identity.isFile() ||
102
104
  !sameFileIdentity(createdIdentity, identity) ||
@@ -108,7 +110,7 @@ async function copyPinnedSource(params) {
108
110
  nativeFd = undefined;
109
111
  return {
110
112
  handle: target,
111
- stat: await target.stat(),
113
+ exactIdentity: opened,
112
114
  digest: hashed.digest,
113
115
  bytes: hashed.bytes,
114
116
  };
@@ -124,10 +126,10 @@ async function copyPinnedSource(params) {
124
126
  }
125
127
  }
126
128
  const target = await fs.open(params.targetPath, "wx+", 0o600);
127
- const identity = await target.stat();
128
- rememberCreatedTarget(params.failure, identity, "copy-verify");
129
+ const exactIdentity = await target.stat({ bigint: true });
130
+ rememberCreatedTarget(params.failure, exactIdentity, "copy-verify");
129
131
  try {
130
- await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, identity);
132
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, exactIdentity);
131
133
  const hash = createHash("sha256");
132
134
  const buffer = Buffer.allocUnsafe(64 * 1024);
133
135
  let position = 0;
@@ -148,7 +150,7 @@ async function copyPinnedSource(params) {
148
150
  position += bytesRead;
149
151
  }
150
152
  await target.sync();
151
- return { handle: target, stat: identity, digest: hash.digest("hex"), bytes: position };
153
+ return { handle: target, exactIdentity, digest: hash.digest("hex"), bytes: position };
152
154
  }
153
155
  catch (error) {
154
156
  await target.close().catch(() => undefined);
@@ -160,7 +162,7 @@ async function removeCreatedTargetIfUnchanged(targetPath, identity) {
160
162
  return "unknown";
161
163
  }
162
164
  try {
163
- const current = await fs.lstat(targetPath);
165
+ const current = await fs.lstat(targetPath, { bigint: true });
164
166
  if (!current.isSymbolicLink() && sameFileIdentity(current, identity)) {
165
167
  await fs.rm(targetPath);
166
168
  return "removed";
@@ -209,31 +211,38 @@ export async function publishFileExclusive(params) {
209
211
  label: "publication parent",
210
212
  });
211
213
  const sourceIdentity = await source.stat();
212
- if (!sameFileIdentity(sourcePathStat, sourceIdentity) ||
214
+ const sourceExactIdentity = await source.stat({ bigint: true });
215
+ const sourcePathExactIdentity = await fs.lstat(sourcePath, { bigint: true });
216
+ if (sourcePathExactIdentity.isSymbolicLink() ||
217
+ !sourcePathExactIdentity.isFile() ||
218
+ !sameFileIdentity(sourcePathExactIdentity, sourceExactIdentity) ||
213
219
  (params.expectedSourceIdentity &&
214
- !sameFileIdentity(params.expectedSourceIdentity, sourceIdentity))) {
220
+ !sameFileIdentity(params.expectedSourceIdentity, typeof params.expectedSourceIdentity.dev === "bigint" ||
221
+ typeof params.expectedSourceIdentity.ino === "bigint"
222
+ ? sourceExactIdentity
223
+ : sourceIdentity))) {
215
224
  throw new FsSafeError("path-mismatch", "publication source identity did not match");
216
225
  }
217
226
  await parent.assertCurrent();
218
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
227
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
219
228
  const native = params.strategy === "rename-noreplace"
220
229
  ? requireNativeBinding()
221
230
  : getNativeBinding();
222
231
  if (native) {
223
- sourceNativeParent = await openNativeParent(native, sourcePath);
224
- targetNativeParent = await openNativeParent(native, targetPath);
232
+ sourceNativeParent = await openNativeParent(sourcePath);
233
+ targetNativeParent = await openNativeParent(targetPath);
225
234
  }
226
235
  if (params.strategy === "rename-noreplace") {
227
236
  const binding = requireNativeBinding();
228
237
  binding.renameNoReplace(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
229
- rememberCreatedTarget(failure, sourceIdentity, "rename-verify");
238
+ rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
230
239
  // A failed post-rename fence must not delete the only remaining name.
231
240
  failure.preserveTarget = true;
232
- await getFsSafeTestHooks()?.afterPublishTargetCreated?.("rename-noreplace", targetPath, sourceIdentity);
233
- const targetIdentity = await fs.lstat(targetPath);
234
- if (targetIdentity.isSymbolicLink() ||
235
- !targetIdentity.isFile() ||
236
- !sameFileIdentity(targetIdentity, sourceIdentity)) {
241
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("rename-noreplace", targetPath, sourceExactIdentity);
242
+ const targetExactIdentity = await fs.lstat(targetPath, { bigint: true });
243
+ if (targetExactIdentity.isSymbolicLink() ||
244
+ !targetExactIdentity.isFile() ||
245
+ !sameFileIdentity(targetExactIdentity, sourceExactIdentity)) {
237
246
  throw new FsSafeError("path-mismatch", "no-replace publication target changed");
238
247
  }
239
248
  try {
@@ -246,6 +255,7 @@ export async function publishFileExclusive(params) {
246
255
  }
247
256
  }
248
257
  syncNativeFileBestEffort(sourceNativeParent.handle.fd);
258
+ const targetIdentity = await fs.lstat(targetPath);
249
259
  return {
250
260
  method: "rename-noreplace",
251
261
  identity: targetIdentity,
@@ -264,15 +274,16 @@ export async function publishFileExclusive(params) {
264
274
  else {
265
275
  await fs.link(sourcePath, targetPath);
266
276
  }
267
- rememberCreatedTarget(failure, sourceIdentity, "hardlink-verify");
268
- await getFsSafeTestHooks()?.afterPublishTargetCreated?.("hardlink", targetPath, sourceIdentity);
269
- const targetIdentity = await fs.lstat(targetPath);
270
- if (targetIdentity.isSymbolicLink() ||
271
- !targetIdentity.isFile() ||
272
- !sameFileIdentity(targetIdentity, sourceIdentity)) {
277
+ rememberCreatedTarget(failure, sourceExactIdentity, "hardlink-verify");
278
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("hardlink", targetPath, sourceExactIdentity);
279
+ const targetExactIdentity = await fs.lstat(targetPath, { bigint: true });
280
+ if (targetExactIdentity.isSymbolicLink() ||
281
+ !targetExactIdentity.isFile() ||
282
+ !sameFileIdentity(targetExactIdentity, sourceExactIdentity)) {
273
283
  throw new FsSafeError("path-mismatch", "hardlink publication target changed");
274
284
  }
275
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
285
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
286
+ const targetIdentity = await fs.lstat(targetPath);
276
287
  return {
277
288
  method: "hardlink",
278
289
  identity: targetIdentity,
@@ -303,19 +314,21 @@ export async function publishFileExclusive(params) {
303
314
  failure,
304
315
  });
305
316
  target = copied.handle;
306
- targetIdentity = copied.stat;
317
+ targetIdentity = await target.stat();
307
318
  const targetPathStat = await fs.lstat(targetPath);
319
+ const targetPathExactStat = await fs.lstat(targetPath, { bigint: true });
308
320
  const copiedBack = await hashFileHandle(target, native);
309
321
  const sourceAfter = await hashFileHandle(source, native);
310
322
  if (targetPathStat.isSymbolicLink() ||
311
- !sameFileIdentity(targetPathStat, targetIdentity) ||
323
+ targetPathExactStat.isSymbolicLink() ||
324
+ !sameFileIdentity(targetPathExactStat, copied.exactIdentity) ||
312
325
  copiedBack.bytes !== copied.bytes ||
313
326
  copiedBack.digest !== copied.digest ||
314
327
  sourceAfter.bytes !== copied.bytes ||
315
328
  sourceAfter.digest !== copied.digest) {
316
329
  throw new FsSafeError("path-mismatch", "exclusive publication copy failed content fencing");
317
330
  }
318
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
331
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
319
332
  const directorySync = await syncPublishedParent({
320
333
  parent,
321
334
  failure,
@@ -339,7 +352,7 @@ export async function publishFileExclusive(params) {
339
352
  const preserveSyncFailure = failure.phase === "directory-sync" && params.onSyncFailure === "preserve";
340
353
  const cleanup = failure.preserveTarget || preserveSyncFailure
341
354
  ? "preserved"
342
- : await removeCreatedTargetIfUnchanged(targetPath, failure.targetIdentity);
355
+ : await removeCreatedTargetIfUnchanged(targetPath, failure.targetCleanupIdentity);
343
356
  throw publicationFailure(error, failure, cleanup);
344
357
  }
345
358
  finally {
@@ -0,0 +1,2 @@
1
+ export declare function throwFsSafeReadError(error: unknown, label: string): never;
2
+ //# sourceMappingURL=read-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-error.d.ts","sourceRoot":"","sources":["../src/read-error.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
@@ -0,0 +1,11 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { isNodeError } from "./path.js";
3
+ export function throwFsSafeReadError(error, label) {
4
+ if (error instanceof FsSafeError) {
5
+ throw error;
6
+ }
7
+ if (isNodeError(error)) {
8
+ throw new FsSafeError("read-failed", `${label} target could not be read`, { cause: error });
9
+ }
10
+ throw error;
11
+ }
@@ -0,0 +1,8 @@
1
+ import fs from "node:fs";
2
+ type ReadOpenFlagConstants = Pick<typeof fs.constants, "O_RDONLY"> & Partial<Pick<typeof fs.constants, "O_NOFOLLOW" | "O_NONBLOCK">>;
3
+ export declare function resolveReadOpenFlags(options?: {
4
+ constants?: ReadOpenFlagConstants;
5
+ followSymlinks?: boolean;
6
+ }): number;
7
+ export {};
8
+ //# sourceMappingURL=read-open-flags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-open-flags.d.ts","sourceRoot":"","sources":["../src/read-open-flags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,KAAK,qBAAqB,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,GAChE,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,YAAY,GAAG,YAAY,CAAC,CAAC,CAAC;AAElE,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAC7C,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,MAAM,CAaT"}
@@ -0,0 +1,13 @@
1
+ import fs from "node:fs";
2
+ export function resolveReadOpenFlags(options) {
3
+ const constants = options?.constants ?? fs.constants;
4
+ const noFollow = process.platform !== "win32" &&
5
+ options?.followSymlinks !== true &&
6
+ typeof constants.O_NOFOLLOW === "number"
7
+ ? constants.O_NOFOLLOW
8
+ : 0;
9
+ const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number"
10
+ ? constants.O_NONBLOCK
11
+ : 0;
12
+ return constants.O_RDONLY | noFollow | nonBlocking;
13
+ }
@@ -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;AAW7B,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;AAsBD,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,CAyD3C;AA6DD,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,CA6BA;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;AAY7B,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;AAaD,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,CAyD3C;AA6DD,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,CA6BA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}