@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.
- package/CHANGELOG.md +35 -0
- package/README.md +3 -2
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +12 -1
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +4 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +6 -3
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +43 -36
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +8 -0
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +11 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +1 -0
- package/dist/archive-zip-entry.d.ts +6 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +11 -1
- package/dist/archive-zip-integrity.d.ts +2 -1
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +12 -2
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +6 -5
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +2 -11
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +31 -16
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +2 -10
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +23 -6
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- package/dist/publish-file-failure.d.ts +1 -0
- package/dist/publish-file-failure.d.ts.map +1 -1
- package/dist/publish-file-failure.js +2 -1
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +49 -36
- package/dist/read-error.d.ts +2 -0
- package/dist/read-error.d.ts.map +1 -0
- package/dist/read-error.js +11 -0
- package/dist/read-open-flags.d.ts +8 -0
- package/dist/read-open-flags.d.ts.map +1 -0
- package/dist/read-open-flags.js +13 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +3 -8
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +6 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +10 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/docs/advanced.md +6 -0
- package/docs/archive.md +2 -2
- package/docs/errors.md +5 -3
- package/docs/file-store.md +4 -0
- package/docs/secret-file.md +4 -0
- package/docs/temp.md +5 -0
- package/docs/types.md +2 -2
- package/package.json +10 -3
package/dist/file-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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);
|
package/dist/move-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"
|
|
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,
|
|
124
|
+
sourceHandle = await fs.open(params.from, resolveReadOpenFlags());
|
|
133
125
|
}
|
|
134
126
|
catch (error) {
|
|
135
127
|
const code = error?.code;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
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"}
|
package/dist/pinned-open.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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;
|
|
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 {
|
|
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) =>
|
|
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
|
-
|
|
223
|
+
throwTempWorkspaceOpenFailure(opened);
|
|
188
224
|
}
|
|
189
225
|
try {
|
|
190
|
-
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/publish-file.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
-
|
|
59
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
128
|
-
rememberCreatedTarget(params.failure,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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(
|
|
224
|
-
targetNativeParent = await openNativeParent(
|
|
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,
|
|
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,
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
!
|
|
236
|
-
!sameFileIdentity(
|
|
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,
|
|
268
|
-
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("hardlink", targetPath,
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
!
|
|
272
|
-
!sameFileIdentity(
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
355
|
+
: await removeCreatedTargetIfUnchanged(targetPath, failure.targetCleanupIdentity);
|
|
343
356
|
throw publicationFailure(error, failure, cleanup);
|
|
344
357
|
}
|
|
345
358
|
finally {
|
|
@@ -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;
|
|
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"}
|