@openclaw/fs-safe 0.6.0 → 0.7.1
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 +79 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -27
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -13
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +10 -10
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +113 -82
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +91 -37
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +202 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
package/dist/output-sibling.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
|
-
import fs, {} from "node:fs/promises";
|
|
4
2
|
import path from "node:path";
|
|
5
|
-
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
|
-
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
7
|
-
import { FsSafeError } from "./errors.js";
|
|
8
|
-
import { sameFileIdentity, sameFileIdentityForCleanup } from "./file-identity.js";
|
|
9
|
-
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
10
3
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
11
|
-
import {
|
|
12
|
-
import { serializePathWrite } from "./write-queue.js";
|
|
13
|
-
const OPEN_READ_WRITE_NOFOLLOW = fsSync.constants.O_RDWR |
|
|
14
|
-
(process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
15
|
-
? fsSync.constants.O_NOFOLLOW
|
|
16
|
-
: 0);
|
|
4
|
+
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
17
5
|
function safeFallbackFileName(fallbackFileName) {
|
|
18
6
|
return sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
|
|
19
7
|
}
|
|
@@ -21,103 +9,16 @@ function buildSiblingTempPath(targetPath, fallbackFileName) {
|
|
|
21
9
|
const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName));
|
|
22
10
|
return path.join(path.dirname(targetPath), `.fs-safe-output-${process.pid}-${randomUUID()}-${safeTail}.part`);
|
|
23
11
|
}
|
|
24
|
-
function assertStagedFile(pathname, opened, maxBytes) {
|
|
25
|
-
if (pathname.isSymbolicLink() || !pathname.isFile() || !opened.isFile()) {
|
|
26
|
-
throw new FsSafeError("not-file", "sibling-staged output must be a regular file");
|
|
27
|
-
}
|
|
28
|
-
if (!sameFileIdentity(pathname, opened)) {
|
|
29
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed while opening");
|
|
30
|
-
}
|
|
31
|
-
if (opened.nlink > 1) {
|
|
32
|
-
throw new FsSafeError("hardlink", "hardlinked sibling-staged output not allowed");
|
|
33
|
-
}
|
|
34
|
-
if (maxBytes !== undefined && opened.size > maxBytes) {
|
|
35
|
-
throw new FsSafeError("too-large", `sibling-staged output exceeds maxBytes (${maxBytes})`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function openPinnedStagedFile(tempPath, maxBytes) {
|
|
39
|
-
const preview = await fs.lstat(tempPath);
|
|
40
|
-
if (preview.isSymbolicLink()) {
|
|
41
|
-
throw new FsSafeError("symlink", "symlink sibling-staged output not allowed");
|
|
42
|
-
}
|
|
43
|
-
const handle = await fs.open(tempPath, OPEN_READ_WRITE_NOFOLLOW);
|
|
44
|
-
try {
|
|
45
|
-
const identity = await handle.stat();
|
|
46
|
-
assertStagedFile(await fs.lstat(tempPath), identity, maxBytes);
|
|
47
|
-
return { handle, identity };
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
await handle.close().catch(() => undefined);
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function syncFile(handle) {
|
|
55
|
-
try {
|
|
56
|
-
await handle.sync();
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (error.code !== "EPERM")
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function cleanupTempPath(tempPath, identity) {
|
|
64
|
-
try {
|
|
65
|
-
const current = await fs.lstat(tempPath);
|
|
66
|
-
if (!identity || (!current.isSymbolicLink() && sameFileIdentityForCleanup(current, identity))) {
|
|
67
|
-
await fs.rm(tempPath, { force: true });
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
return error.code === "ENOENT";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
12
|
export async function writeExternalFileViaSibling(params) {
|
|
76
13
|
const finalPath = path.resolve(params.finalPath);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
unregisterTempPath.setIdentity(await pinned.handle.stat({ bigint: true }));
|
|
88
|
-
try {
|
|
89
|
-
if (params.mode !== undefined) {
|
|
90
|
-
await pinned.handle.chmod(params.mode);
|
|
91
|
-
}
|
|
92
|
-
await syncFile(pinned.handle);
|
|
93
|
-
await serializePathWrite(finalPath, async () => {
|
|
94
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
95
|
-
const currentIdentity = await pinned.handle.stat();
|
|
96
|
-
assertStagedFile(await fs.lstat(tempPath), currentIdentity, params.maxBytes);
|
|
97
|
-
if (!sameFileIdentity(currentIdentity, stagedIdentity)) {
|
|
98
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed before rename");
|
|
99
|
-
}
|
|
100
|
-
await fs.rename(tempPath, finalPath);
|
|
101
|
-
renamed = true;
|
|
102
|
-
const published = await fs.lstat(finalPath);
|
|
103
|
-
if (published.isSymbolicLink() || !sameFileIdentity(published, currentIdentity)) {
|
|
104
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed during rename");
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
await syncDirectoryBestEffort(parentPath);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
await pinned.handle.close().catch(() => undefined);
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
let cleanupComplete = renamed;
|
|
117
|
-
if (!renamed) {
|
|
118
|
-
cleanupComplete = await cleanupTempPath(tempPath, stagedIdentity);
|
|
119
|
-
}
|
|
120
|
-
if (cleanupComplete)
|
|
121
|
-
unregisterTempPath();
|
|
122
|
-
}
|
|
14
|
+
const { result } = await writeCallbackSibling({
|
|
15
|
+
tempPath: buildSiblingTempPath(finalPath, params.fallbackFileName),
|
|
16
|
+
write: params.write,
|
|
17
|
+
resolveFinalPath: () => finalPath,
|
|
18
|
+
mode: params.mode,
|
|
19
|
+
maxBytes: params.maxBytes,
|
|
20
|
+
syncTempFile: true,
|
|
21
|
+
syncParentDir: true,
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
123
24
|
}
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAgDrC"}
|
package/dist/output.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
4
5
|
import { isPathInside } from "./path.js";
|
|
@@ -48,6 +49,7 @@ function assertFileTargetPath(targetPath) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export async function writeExternalFileWithinRoot(options) {
|
|
52
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
51
53
|
const targetRoot = await root(options.rootDir);
|
|
52
54
|
const requestedTargetPath = options.path;
|
|
53
55
|
if (requestedTargetPath.length === 0) {
|
|
@@ -72,7 +74,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
72
74
|
finalPath: siblingFinalPath,
|
|
73
75
|
write: options.write,
|
|
74
76
|
fallbackFileName: options.fallbackFileName,
|
|
75
|
-
maxBytes
|
|
77
|
+
maxBytes,
|
|
76
78
|
mode: options.mode,
|
|
77
79
|
});
|
|
78
80
|
return { path: siblingFinalPath, result };
|
|
@@ -84,7 +86,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
84
86
|
try {
|
|
85
87
|
const result = await options.write(staged.path);
|
|
86
88
|
await targetRoot.copyIn(targetPath, staged.path, {
|
|
87
|
-
maxBytes
|
|
89
|
+
maxBytes,
|
|
88
90
|
mode: options.mode,
|
|
89
91
|
mkdir: true,
|
|
90
92
|
sourceHardlinks: "reject",
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAQF,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,CAoDvB"}
|
package/dist/pinned-open.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { isUnsafeDeviceReadPath } from "./device-path.js";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
6
|
function isExpectedPathError(error) {
|
|
7
7
|
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
8
8
|
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
9
9
|
}
|
|
10
|
-
function sameFileIdentity(left, right) {
|
|
11
|
-
return hasSameFileIdentity(left, right);
|
|
12
|
-
}
|
|
13
10
|
export function openPinnedFileSync(params) {
|
|
14
11
|
const ioFs = params.ioFs ?? fs;
|
|
15
12
|
const allowedType = params.allowedType ?? "file";
|
|
@@ -29,53 +26,30 @@ export function openPinnedFileSync(params) {
|
|
|
29
26
|
if (isUnsafeDeviceReadPath(realPath)) {
|
|
30
27
|
return { ok: false, reason: "validation" };
|
|
31
28
|
}
|
|
32
|
-
const preOpenStat =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
error: new FsSafeError("not-file", "path does not have the required file type"),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
reason: "validation",
|
|
44
|
-
error: new FsSafeError("hardlink", "path must not be hardlinked"),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (params.maxBytes !== undefined &&
|
|
48
|
-
preOpenStat.isFile() &&
|
|
49
|
-
preOpenStat.size > params.maxBytes) {
|
|
50
|
-
return { ok: false, reason: "validation" };
|
|
51
|
-
}
|
|
29
|
+
const preOpenStat = inspectFileIdentitySync(() => {
|
|
30
|
+
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
31
|
+
assertAllowedStat(stat, allowedType, params);
|
|
32
|
+
return stat;
|
|
33
|
+
});
|
|
52
34
|
fd = ioFs.openSync(realPath, openReadFlags);
|
|
53
35
|
const openedStat = ioFs.fstatSync(fd);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
reason: "validation",
|
|
65
|
-
error: new FsSafeError("hardlink", "path must not be hardlinked"),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
|
|
69
|
-
return { ok: false, reason: "validation" };
|
|
70
|
-
}
|
|
71
|
-
if (!sameFileIdentity(preOpenStat, openedStat)) {
|
|
72
|
-
return { ok: false, reason: "validation" };
|
|
73
|
-
}
|
|
36
|
+
const identity = inspectFileIdentitySync(() => {
|
|
37
|
+
const stat = ioFs.fstatSync(fd, { bigint: true });
|
|
38
|
+
assertAllowedStat(stat, allowedType, params);
|
|
39
|
+
return stat;
|
|
40
|
+
}, preOpenStat);
|
|
41
|
+
inspectFileIdentitySync(() => {
|
|
42
|
+
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
43
|
+
assertAllowedStat(stat, allowedType, params);
|
|
44
|
+
return stat;
|
|
45
|
+
}, identity);
|
|
74
46
|
const opened = { ok: true, path: realPath, fd, stat: openedStat };
|
|
75
47
|
fd = null;
|
|
76
48
|
return opened;
|
|
77
49
|
}
|
|
78
50
|
catch (error) {
|
|
51
|
+
if (error instanceof FsSafeError)
|
|
52
|
+
return { ok: false, reason: "validation", error };
|
|
79
53
|
if (isExpectedPathError(error)) {
|
|
80
54
|
return { ok: false, reason: "path", error };
|
|
81
55
|
}
|
|
@@ -87,9 +61,14 @@ export function openPinnedFileSync(params) {
|
|
|
87
61
|
}
|
|
88
62
|
}
|
|
89
63
|
}
|
|
90
|
-
function
|
|
91
|
-
if (allowedType === "directory") {
|
|
92
|
-
|
|
64
|
+
function assertAllowedStat(stat, allowedType, params) {
|
|
65
|
+
if (stat.isSymbolicLink() || !(allowedType === "directory" ? stat.isDirectory() : stat.isFile())) {
|
|
66
|
+
throw new FsSafeError("not-file", "path does not have the required file type");
|
|
67
|
+
}
|
|
68
|
+
if (params.rejectHardlinks && stat.isFile() && stat.nlink > 1n) {
|
|
69
|
+
throw new FsSafeError("hardlink", "path must not be hardlinked");
|
|
70
|
+
}
|
|
71
|
+
if (params.maxBytes !== undefined && stat.isFile() && stat.size > params.maxBytes) {
|
|
72
|
+
throw new FsSafeError("too-large", "file exceeds byte limit");
|
|
93
73
|
}
|
|
94
|
-
return stat.isFile();
|
|
95
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
6
|
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
@@ -11,6 +12,7 @@ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
|
11
12
|
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
12
13
|
import { getNativeBinding } from "./native.js";
|
|
13
14
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
15
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
14
16
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
15
17
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
16
18
|
function byteLength(input, encoding) {
|
|
@@ -59,20 +61,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
export async function runPinnedWriteHelper(params) {
|
|
64
|
+
const normalizedParams = { ...params, maxBytes: normalizeMaxBytes(params.maxBytes) };
|
|
62
65
|
assertSafeBasename(params.basename);
|
|
63
66
|
validatePinnedOperationPayload({
|
|
64
67
|
relativeParentPath: params.relativeParentPath,
|
|
65
68
|
});
|
|
66
69
|
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
67
70
|
// content verification can replace the strict post-rename inode check.
|
|
68
|
-
if (
|
|
69
|
-
return await runPinnedWriteFallback(
|
|
71
|
+
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
72
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
70
73
|
}
|
|
71
74
|
const native = getNativeBinding();
|
|
72
75
|
if (native) {
|
|
73
|
-
return await runPinnedWriteNative(native,
|
|
76
|
+
return await runPinnedWriteNative(native, normalizedParams);
|
|
74
77
|
}
|
|
75
|
-
return await runPinnedWriteFallback(
|
|
78
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
76
79
|
}
|
|
77
80
|
export async function runPinnedWriteWithRenamePolicy(params) {
|
|
78
81
|
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
@@ -153,7 +156,8 @@ async function runPinnedWriteFallback(params) {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
|
-
|
|
159
|
+
// Private staging must not consume the destination basename's filename budget.
|
|
160
|
+
const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
|
|
157
161
|
const tempFlags = fsSync.constants.O_WRONLY |
|
|
158
162
|
fsSync.constants.O_CREAT |
|
|
159
163
|
fsSync.constants.O_EXCL |
|
|
@@ -209,11 +213,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
209
213
|
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
210
214
|
}
|
|
211
215
|
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
212
|
-
|
|
213
|
-
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
214
|
-
? fsSync.constants.O_NOFOLLOW
|
|
215
|
-
: 0);
|
|
216
|
-
readHandle = await fs.open(targetPath, readFlags);
|
|
216
|
+
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
217
217
|
const readHandleStat = await readHandle.stat({ bigint: true });
|
|
218
218
|
const actualHash = sha256Hex(await readHandle.readFile());
|
|
219
219
|
if (actualHash !== expectedHash) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type FileStore, type FileStoreSync } from "./file-store.js";
|
|
2
2
|
import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
|
|
3
|
-
|
|
3
|
+
import { type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
|
|
4
|
+
export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
|
|
4
5
|
export type TempWorkspaceOptions = {
|
|
5
6
|
rootDir: string;
|
|
6
7
|
prefix: string;
|
|
7
8
|
dirMode?: number;
|
|
8
9
|
mode?: number;
|
|
10
|
+
cleanupSafety?: TempWorkspaceCleanupSafety;
|
|
9
11
|
};
|
|
10
12
|
export type TempWorkspace = {
|
|
11
13
|
dir: string;
|
|
@@ -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":"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"}
|
|
@@ -4,11 +4,18 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileStore, fileStoreSync, } from "./file-store.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
8
7
|
import { isNotFoundPathError } from "./path.js";
|
|
9
8
|
import { throwFsSafeReadError } from "./read-error.js";
|
|
10
9
|
import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
|
|
11
10
|
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
11
|
+
import { TempWorkspaceCleanupCapability, TempWorkspaceCleanupOwner, } from "./temp-workspace-owner.js";
|
|
12
|
+
function resolveTempWorkspaceCleanupSafety(value) {
|
|
13
|
+
if (value === undefined || value === "compatible")
|
|
14
|
+
return "compatible";
|
|
15
|
+
if (value === "require-bounded")
|
|
16
|
+
return value;
|
|
17
|
+
throw new TypeError("cleanupSafety must be compatible or require-bounded");
|
|
18
|
+
}
|
|
12
19
|
function sanitizeTempPrefix(prefix) {
|
|
13
20
|
const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
14
21
|
if (!sanitized || sanitized === "." || sanitized === "..") {
|
|
@@ -79,52 +86,47 @@ function ensurePrivateDirectorySync(dir, mode) {
|
|
|
79
86
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
|
-
async function cleanupWorkspace(dir, identity) {
|
|
83
|
-
let current;
|
|
84
|
-
try {
|
|
85
|
-
current = await fs.lstat(dir, { bigint: true });
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
89
|
-
}
|
|
90
|
-
if (!sameFileIdentityForCleanup(current, identity)) {
|
|
91
|
-
return "identity-mismatch";
|
|
92
|
-
}
|
|
93
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
94
|
-
return "removed";
|
|
95
|
-
}
|
|
96
|
-
function cleanupWorkspaceSync(dir, identity) {
|
|
97
|
-
let current;
|
|
98
|
-
try {
|
|
99
|
-
current = fsSync.lstatSync(dir, { bigint: true });
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
103
|
-
}
|
|
104
|
-
if (!sameFileIdentityForCleanup(current, identity)) {
|
|
105
|
-
return "identity-mismatch";
|
|
106
|
-
}
|
|
107
|
-
fsSync.rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
return "removed";
|
|
109
|
-
}
|
|
110
89
|
async function createTempWorkspace(options) {
|
|
111
90
|
const dirMode = options.dirMode ?? 0o700;
|
|
112
91
|
const mode = options.mode ?? 0o600;
|
|
92
|
+
const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
|
|
113
93
|
const requestedRoot = path.resolve(options.rootDir);
|
|
114
94
|
const root = await fs.realpath(requestedRoot).catch(() => requestedRoot);
|
|
115
95
|
await ensurePrivateDirectory(root, dirMode);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
96
|
+
const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
|
|
97
|
+
let dir;
|
|
98
|
+
let stat;
|
|
99
|
+
let cleanupOwner;
|
|
100
|
+
let unregisterTempDir;
|
|
101
|
+
try {
|
|
102
|
+
dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
103
|
+
await fs.chmod(dir, dirMode).catch(() => undefined);
|
|
104
|
+
stat = await fs.lstat(dir, { bigint: true });
|
|
105
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
106
|
+
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
107
|
+
}
|
|
108
|
+
if (capability.parent)
|
|
109
|
+
capability.assertCurrent();
|
|
110
|
+
cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
|
|
111
|
+
unregisterTempDir = registerTempPathForExit(dir, {
|
|
112
|
+
cleanupSync: () => cleanupOwner.cleanupSync(),
|
|
113
|
+
});
|
|
121
114
|
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
try {
|
|
117
|
+
if (cleanupOwner)
|
|
118
|
+
cleanupOwner.cleanupSync();
|
|
119
|
+
else
|
|
120
|
+
capability.close();
|
|
121
|
+
}
|
|
122
|
+
catch (cleanupError) {
|
|
123
|
+
throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const owner = cleanupOwner;
|
|
122
128
|
const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
|
|
123
|
-
|
|
124
|
-
const unregisterTempDir = registerTempPathForExit(dir, {
|
|
125
|
-
recursive: true,
|
|
126
|
-
identity: cleanupIdentity,
|
|
127
|
-
});
|
|
129
|
+
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
128
130
|
const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
|
|
129
131
|
return {
|
|
130
132
|
dir,
|
|
@@ -148,7 +150,7 @@ async function createTempWorkspace(options) {
|
|
|
148
150
|
},
|
|
149
151
|
cleanup: async () => {
|
|
150
152
|
try {
|
|
151
|
-
return await
|
|
153
|
+
return await owner.cleanup();
|
|
152
154
|
}
|
|
153
155
|
finally {
|
|
154
156
|
unregisterTempDir();
|
|
@@ -156,7 +158,7 @@ async function createTempWorkspace(options) {
|
|
|
156
158
|
},
|
|
157
159
|
[Symbol.asyncDispose]: async () => {
|
|
158
160
|
try {
|
|
159
|
-
await
|
|
161
|
+
await owner.cleanup();
|
|
160
162
|
}
|
|
161
163
|
finally {
|
|
162
164
|
unregisterTempDir();
|
|
@@ -182,6 +184,7 @@ export async function withTempWorkspace(options, run) {
|
|
|
182
184
|
export function tempWorkspaceSync(options) {
|
|
183
185
|
const dirMode = options.dirMode ?? 0o700;
|
|
184
186
|
const mode = options.mode ?? 0o600;
|
|
187
|
+
const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
|
|
185
188
|
const requestedRoot = path.resolve(options.rootDir);
|
|
186
189
|
let root = requestedRoot;
|
|
187
190
|
try {
|
|
@@ -191,23 +194,45 @@ export function tempWorkspaceSync(options) {
|
|
|
191
194
|
root = requestedRoot;
|
|
192
195
|
}
|
|
193
196
|
ensurePrivateDirectorySync(root, dirMode);
|
|
194
|
-
const
|
|
197
|
+
const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
|
|
198
|
+
let dir;
|
|
199
|
+
let stat;
|
|
200
|
+
let cleanupOwner;
|
|
201
|
+
let unregisterTempDir;
|
|
195
202
|
try {
|
|
196
|
-
fsSync.
|
|
203
|
+
dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
204
|
+
try {
|
|
205
|
+
fsSync.chmodSync(dir, dirMode);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
209
|
+
}
|
|
210
|
+
stat = fsSync.lstatSync(dir, { bigint: true });
|
|
211
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
212
|
+
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
213
|
+
}
|
|
214
|
+
if (capability.parent)
|
|
215
|
+
capability.assertCurrent();
|
|
216
|
+
cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
|
|
217
|
+
unregisterTempDir = registerTempPathForExit(dir, {
|
|
218
|
+
cleanupSync: () => cleanupOwner.cleanupSync(),
|
|
219
|
+
});
|
|
197
220
|
}
|
|
198
|
-
catch {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
try {
|
|
223
|
+
if (cleanupOwner)
|
|
224
|
+
cleanupOwner.cleanupSync();
|
|
225
|
+
else
|
|
226
|
+
capability.close();
|
|
227
|
+
}
|
|
228
|
+
catch (cleanupError) {
|
|
229
|
+
throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
204
232
|
}
|
|
233
|
+
const owner = cleanupOwner;
|
|
205
234
|
const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
|
|
206
|
-
|
|
207
|
-
const unregisterTempDir = registerTempPathForExit(dir, {
|
|
208
|
-
recursive: true,
|
|
209
|
-
identity: cleanupIdentity,
|
|
210
|
-
});
|
|
235
|
+
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
211
236
|
const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
|
|
212
237
|
return {
|
|
213
238
|
dir,
|
|
@@ -244,7 +269,7 @@ export function tempWorkspaceSync(options) {
|
|
|
244
269
|
},
|
|
245
270
|
cleanup: () => {
|
|
246
271
|
try {
|
|
247
|
-
return
|
|
272
|
+
return owner.cleanupSync();
|
|
248
273
|
}
|
|
249
274
|
finally {
|
|
250
275
|
unregisterTempDir();
|
|
@@ -252,7 +277,7 @@ export function tempWorkspaceSync(options) {
|
|
|
252
277
|
},
|
|
253
278
|
[Symbol.dispose]: () => {
|
|
254
279
|
try {
|
|
255
|
-
|
|
280
|
+
owner.cleanupSync();
|
|
256
281
|
}
|
|
257
282
|
finally {
|
|
258
283
|
unregisterTempDir();
|
|
@@ -1 +1 @@
|
|
|
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,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;
|
|
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,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,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;AAsND,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
|
@@ -8,6 +8,7 @@ import { hashFileHandle } from "./file-hash.js";
|
|
|
8
8
|
import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
|
|
9
9
|
import { syncNativeFileBestEffort } from "./native-operations.js";
|
|
10
10
|
import { getNativeBinding, requireNativeBinding } from "./native.js";
|
|
11
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
11
12
|
import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
|
|
12
13
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
13
14
|
const HARDLINK_FALLBACK_CODES = new Set([
|
|
@@ -29,10 +30,7 @@ export function isHardlinkFallbackError(error) {
|
|
|
29
30
|
return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
|
|
30
31
|
}
|
|
31
32
|
function sourceOpenFlags() {
|
|
32
|
-
return (
|
|
33
|
-
(process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
34
|
-
? fsSync.constants.O_NOFOLLOW
|
|
35
|
-
: 0));
|
|
33
|
+
return resolveReadOpenFlags();
|
|
36
34
|
}
|
|
37
35
|
function directoryOpenFlags() {
|
|
38
36
|
return (fsSync.constants.O_RDONLY |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBtB"}
|