@openclaw/fs-safe 0.5.1 → 0.5.3
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 +86 -0
- package/README.md +19 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +37 -6
- 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-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +12 -6
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +62 -37
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +15 -1
- 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 +8 -4
- package/dist/archive-zip-entry.d.ts +7 -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 +5 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +54 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +12 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +62 -139
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- 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/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +21 -4
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- 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/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +216 -169
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +29 -6
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +7 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +34 -8
- package/docs/file-store.md +14 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +21 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +42 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +15 -4
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
|
@@ -189,15 +189,18 @@ function restoreFailure(writeError, cleanup, restoreError) {
|
|
|
189
189
|
? `Atomic copy fallback failed; original destination restored: ${primary.message}`
|
|
190
190
|
: `Atomic copy fallback failed and original restoration failed: ${primary.message}`, { cause, details });
|
|
191
191
|
}
|
|
192
|
-
async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes) {
|
|
192
|
+
async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes, replacementMode) {
|
|
193
|
+
const originalMode = (await handle.stat()).mode;
|
|
193
194
|
const original = await readBounded(handle, maxRestoreBytes);
|
|
194
195
|
try {
|
|
195
196
|
await writeAll(handle, replacement);
|
|
197
|
+
await handle.chmod(replacementMode);
|
|
196
198
|
await handle.sync();
|
|
197
199
|
}
|
|
198
200
|
catch (writeError) {
|
|
199
201
|
try {
|
|
200
202
|
await writeAll(handle, original);
|
|
203
|
+
await handle.chmod(originalMode);
|
|
201
204
|
await handle.sync();
|
|
202
205
|
throw restoreFailure(writeError, "restored");
|
|
203
206
|
}
|
|
@@ -209,15 +212,18 @@ async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes) {
|
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
|
-
function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes) {
|
|
215
|
+
function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes, replacementMode, fchmodSync) {
|
|
216
|
+
const originalMode = fsModule.fstatSync(fd).mode;
|
|
213
217
|
const original = readBoundedSync(fsModule, fd, maxRestoreBytes);
|
|
214
218
|
try {
|
|
215
219
|
writeAllSync(fsModule, fd, replacement);
|
|
220
|
+
fchmodSync?.(fd, replacementMode);
|
|
216
221
|
fsModule.fsyncSync(fd);
|
|
217
222
|
}
|
|
218
223
|
catch (writeError) {
|
|
219
224
|
try {
|
|
220
225
|
writeAllSync(fsModule, fd, original);
|
|
226
|
+
fchmodSync?.(fd, originalMode);
|
|
221
227
|
fsModule.fsyncSync(fd);
|
|
222
228
|
throw restoreFailure(writeError, "restored");
|
|
223
229
|
}
|
|
@@ -247,7 +253,7 @@ export async function copyFallbackReplace(params) {
|
|
|
247
253
|
const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
|
|
248
254
|
if (pinned) {
|
|
249
255
|
destHandle = pinned.handle;
|
|
250
|
-
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes);
|
|
256
|
+
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, sourceStat.mode);
|
|
251
257
|
}
|
|
252
258
|
}
|
|
253
259
|
if (!destHandle) {
|
|
@@ -265,13 +271,16 @@ export async function copyFallbackReplace(params) {
|
|
|
265
271
|
}
|
|
266
272
|
destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
|
|
267
273
|
await destHandle.writeFile(replacement);
|
|
274
|
+
await destHandle.chmod(sourceStat.mode);
|
|
275
|
+
if (params.sync) {
|
|
276
|
+
await destHandle.sync();
|
|
277
|
+
}
|
|
268
278
|
}
|
|
269
279
|
}
|
|
270
280
|
finally {
|
|
271
281
|
await destHandle?.close().catch(() => undefined);
|
|
272
282
|
await sourceHandle.close().catch(() => undefined);
|
|
273
283
|
}
|
|
274
|
-
await params.fsModule.unlink(params.src).catch(() => undefined);
|
|
275
284
|
}
|
|
276
285
|
export function copyFallbackReplaceSync(params) {
|
|
277
286
|
const sourcePreview = params.fsModule.lstatSync(params.src);
|
|
@@ -291,7 +300,7 @@ export function copyFallbackReplaceSync(params) {
|
|
|
291
300
|
const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
|
|
292
301
|
if (pinned) {
|
|
293
302
|
destFd = pinned.fd;
|
|
294
|
-
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes);
|
|
303
|
+
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, sourceStat.mode, params.fchmodSync);
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
306
|
if (destFd === undefined) {
|
|
@@ -312,6 +321,10 @@ export function copyFallbackReplaceSync(params) {
|
|
|
312
321
|
}
|
|
313
322
|
destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
|
|
314
323
|
writeAllSync(params.fsModule, destFd, replacement);
|
|
324
|
+
params.fchmodSync?.(destFd, sourceStat.mode);
|
|
325
|
+
if (params.sync) {
|
|
326
|
+
params.fsModule.fsyncSync(destFd);
|
|
327
|
+
}
|
|
315
328
|
}
|
|
316
329
|
}
|
|
317
330
|
finally {
|
|
@@ -330,10 +343,4 @@ export function copyFallbackReplaceSync(params) {
|
|
|
330
343
|
// Best-effort close after fallback replacement.
|
|
331
344
|
}
|
|
332
345
|
}
|
|
333
|
-
try {
|
|
334
|
-
params.fsModule.unlinkSync(params.src);
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
// Best-effort cleanup after fallback replacement.
|
|
338
|
-
}
|
|
339
346
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import syncFs, { type Stats } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
|
|
4
|
+
type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
|
|
5
|
+
export type SyncFchmod = (fd: number, mode: number) => void;
|
|
6
|
+
export declare function applyDirectoryMode(params: {
|
|
7
|
+
fsModule: AsyncTempFileSystem;
|
|
8
|
+
dirPath: string;
|
|
9
|
+
mode: number;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function applyDirectoryModeSync(params: {
|
|
12
|
+
fsModule: SyncTempFileSystem;
|
|
13
|
+
dirPath: string;
|
|
14
|
+
mode: number;
|
|
15
|
+
fchmodSync?: SyncFchmod;
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function writeTempFile(params: {
|
|
18
|
+
fsModule: AsyncTempFileSystem;
|
|
19
|
+
tempPath: string;
|
|
20
|
+
content: string | Uint8Array;
|
|
21
|
+
mode: number;
|
|
22
|
+
sync: boolean;
|
|
23
|
+
}): Promise<Stats>;
|
|
24
|
+
export declare function writeTempFileSync(params: {
|
|
25
|
+
fsModule: SyncTempFileSystem;
|
|
26
|
+
tempPath: string;
|
|
27
|
+
content: string | Uint8Array;
|
|
28
|
+
mode: number;
|
|
29
|
+
fchmodSync?: SyncFchmod;
|
|
30
|
+
sync: boolean;
|
|
31
|
+
}): Stats;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=replace-file-descriptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAuB,MAAM,kBAAkB,CAAC;AAIvD,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AA2B5D,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,KAAK,CAAC,CAkBjB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,KAAK,CAkBR"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import syncFs, {} from "node:fs";
|
|
2
|
+
import fs, {} from "node:fs/promises";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
5
|
+
function directoryOpenFlags() {
|
|
6
|
+
return (syncFs.constants.O_RDONLY |
|
|
7
|
+
syncFs.constants.O_DIRECTORY |
|
|
8
|
+
syncFs.constants.O_NOFOLLOW |
|
|
9
|
+
syncFs.constants.O_NONBLOCK);
|
|
10
|
+
}
|
|
11
|
+
function assertDirectory(identity, dirPath) {
|
|
12
|
+
if (identity.isSymbolicLink() || !identity.isDirectory()) {
|
|
13
|
+
throw new FsSafeError("not-file", `Atomic replace parent must be a real directory: ${dirPath}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function assertSameDirectory(expected, opened, dirPath) {
|
|
17
|
+
assertDirectory(opened, dirPath);
|
|
18
|
+
if (!sameFileIdentity(expected, opened)) {
|
|
19
|
+
throw new FsSafeError("path-mismatch", `Atomic replace parent changed before its mode could be applied: ${dirPath}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function applyDirectoryMode(params) {
|
|
23
|
+
// Node does not enforce POSIX directory modes on Windows, and its directory
|
|
24
|
+
// descriptors are not consistently openable. mkdir(mode) remains the only
|
|
25
|
+
// bounded behavior there; never fall back to a pathname chmod.
|
|
26
|
+
if (process.platform === "win32") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const expected = await params.fsModule.lstat(params.dirPath);
|
|
30
|
+
assertDirectory(expected, params.dirPath);
|
|
31
|
+
const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
|
|
32
|
+
try {
|
|
33
|
+
assertSameDirectory(expected, await handle.stat(), params.dirPath);
|
|
34
|
+
await handle.chmod(params.mode);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await handle.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function applyDirectoryModeSync(params) {
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const expected = params.fsModule.lstatSync(params.dirPath);
|
|
45
|
+
assertDirectory(expected, params.dirPath);
|
|
46
|
+
const fd = params.fsModule.openSync(params.dirPath, directoryOpenFlags());
|
|
47
|
+
try {
|
|
48
|
+
assertSameDirectory(expected, params.fsModule.fstatSync(fd), params.dirPath);
|
|
49
|
+
params.fchmodSync?.(fd, params.mode);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
params.fsModule.closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function writeTempFile(params) {
|
|
56
|
+
const handle = await params.fsModule.open(params.tempPath, "wx", params.mode);
|
|
57
|
+
try {
|
|
58
|
+
await params.fsModule.writeFile(handle, params.content);
|
|
59
|
+
await handle.chmod(params.mode);
|
|
60
|
+
if (params.sync) {
|
|
61
|
+
try {
|
|
62
|
+
await handle.sync();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code !== "EPERM") {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return await handle.stat();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function writeTempFileSync(params) {
|
|
77
|
+
const fd = params.fsModule.openSync(params.tempPath, "wx", params.mode);
|
|
78
|
+
try {
|
|
79
|
+
params.fsModule.writeFileSync(fd, params.content);
|
|
80
|
+
params.fchmodSync?.(fd, params.mode);
|
|
81
|
+
if (params.sync) {
|
|
82
|
+
try {
|
|
83
|
+
params.fsModule.fsyncSync(fd);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== "EPERM") {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return params.fsModule.fstatSync(fd);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
params.fsModule.closeSync(fd);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/replace-file.d.ts
CHANGED
|
@@ -2,9 +2,16 @@ import syncFs from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy } from "./replace-file-copy-fallback.js";
|
|
4
4
|
export type ReplaceFileAtomicFileSystem = {
|
|
5
|
-
promises: Pick<typeof fs, "mkdir" | "
|
|
5
|
+
promises: Pick<typeof fs, "mkdir" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat"> & {
|
|
6
|
+
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
7
|
+
chmod?: typeof fs.chmod;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync"> & {
|
|
11
|
+
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
12
|
+
chmodSync?: typeof syncFs.chmodSync;
|
|
13
|
+
fchmodSync?: typeof syncFs.fchmodSync;
|
|
6
14
|
};
|
|
7
|
-
export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "chmodSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync">;
|
|
8
15
|
export type { ReplaceFileAtomicRestoreCleanup, ReplaceFileAtomicRestoreFailureDetails, ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy, };
|
|
9
16
|
type ReplaceFileAtomicBaseOptions = {
|
|
10
17
|
filePath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAKL,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EAC1C,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAKL,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EAC1C,MAAM,iCAAiC,CAAC;AAazC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,GAAG;QACF,uEAAuE;QACvE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,cAAc,GACd,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,GACX,eAAe,GACf,UAAU,GACV,WAAW,CACd,GAAG;IACF,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IACpC,UAAU,CAAC,EAAE,OAAO,MAAM,CAAC,UAAU,CAAC;CACvC,CAAC;AAEF,YAAY,EACV,+BAA+B,EAC/B,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,GACrC,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AAgNF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAOlC;AAkED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAoFzB"}
|
package/dist/replace-file.js
CHANGED
|
@@ -3,8 +3,10 @@ import syncFs from "node:fs";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { assertDestinationHardlinkPolicy, assertDestinationHardlinkPolicySync, copyFallbackReplace, copyFallbackReplaceSync, } from "./replace-file-copy-fallback.js";
|
|
6
|
+
import { applyDirectoryMode, applyDirectoryModeSync, writeTempFile, writeTempFileSync, } from "./replace-file-descriptor.js";
|
|
6
7
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
7
8
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
9
|
+
import { sleep, sleepSync } from "./timing.js";
|
|
8
10
|
import { serializePathWrite } from "./write-queue.js";
|
|
9
11
|
function isRetryableRenameError(error) {
|
|
10
12
|
return error.code === "EBUSY";
|
|
@@ -13,9 +15,6 @@ function isPermissionRenameError(error) {
|
|
|
13
15
|
const code = error.code;
|
|
14
16
|
return code === "EPERM" || code === "EEXIST";
|
|
15
17
|
}
|
|
16
|
-
async function sleep(ms) {
|
|
17
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
-
}
|
|
19
18
|
async function renameWithRetry(params) {
|
|
20
19
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
21
20
|
try {
|
|
@@ -35,6 +34,7 @@ async function renameWithRetry(params) {
|
|
|
35
34
|
destinationHardlinks: params.destinationHardlinks,
|
|
36
35
|
restore: params.copyFallbackRestore,
|
|
37
36
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
37
|
+
sync: params.syncFallback,
|
|
38
38
|
});
|
|
39
39
|
return { method: "copy-fallback" };
|
|
40
40
|
}
|
|
@@ -43,12 +43,6 @@ async function renameWithRetry(params) {
|
|
|
43
43
|
}
|
|
44
44
|
throw new Error("Atomic rename retry loop exhausted.");
|
|
45
45
|
}
|
|
46
|
-
function sleepSync(ms) {
|
|
47
|
-
if (ms <= 0) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
51
|
-
}
|
|
52
46
|
function renameWithRetrySync(params) {
|
|
53
47
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
54
48
|
try {
|
|
@@ -68,6 +62,8 @@ function renameWithRetrySync(params) {
|
|
|
68
62
|
destinationHardlinks: params.destinationHardlinks,
|
|
69
63
|
restore: params.copyFallbackRestore,
|
|
70
64
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
65
|
+
fchmodSync: params.fchmodSync,
|
|
66
|
+
sync: params.syncFallback,
|
|
71
67
|
});
|
|
72
68
|
return { method: "copy-fallback" };
|
|
73
69
|
}
|
|
@@ -126,33 +122,8 @@ function resolveModeSync(options) {
|
|
|
126
122
|
}
|
|
127
123
|
return stat ? stat.mode : defaultMode;
|
|
128
124
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
await handle.sync();
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
if (error.code !== "EPERM") {
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
await handle.close();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function syncTempFileSync(fsModule, tempPath) {
|
|
144
|
-
const fd = fsModule.openSync(tempPath, "r+");
|
|
145
|
-
try {
|
|
146
|
-
fsModule.fsyncSync(fd);
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
if (error.code !== "EPERM") {
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
fsModule.closeSync(fd);
|
|
155
|
-
}
|
|
125
|
+
function missingFchmodSyncError() {
|
|
126
|
+
return new TypeError("fileSystem.fchmodSync is required when mode, dirMode, or preserveExistingMode is specified");
|
|
156
127
|
}
|
|
157
128
|
async function syncDirectoryBestEffort(fsModule, dirPath) {
|
|
158
129
|
let handle;
|
|
@@ -191,9 +162,15 @@ async function cleanupTempFile(params) {
|
|
|
191
162
|
const cleanupError = await params.fsModule
|
|
192
163
|
.rm(params.tempPath, { force: true })
|
|
193
164
|
.catch((error) => error);
|
|
194
|
-
if (cleanupError
|
|
195
|
-
|
|
165
|
+
if (!cleanupError)
|
|
166
|
+
return true;
|
|
167
|
+
if (params.throwOnCleanupError) {
|
|
168
|
+
if (params.originalError !== undefined) {
|
|
169
|
+
throw new Error(`Atomic file replace failed (${String(params.originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: params.originalError });
|
|
170
|
+
}
|
|
171
|
+
throw cleanupError;
|
|
196
172
|
}
|
|
173
|
+
return false;
|
|
197
174
|
}
|
|
198
175
|
export async function replaceFileAtomic(options) {
|
|
199
176
|
const filePath = options.filePath;
|
|
@@ -213,15 +190,17 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
213
190
|
const unregisterTempPath = registerTempPathForExit(tempPath);
|
|
214
191
|
let tempExists = false;
|
|
215
192
|
let originalError;
|
|
216
|
-
await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
|
|
217
|
-
await fsModule.chmod(dir, dirMode).catch(() => undefined);
|
|
218
193
|
try {
|
|
194
|
+
await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
|
|
195
|
+
await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode });
|
|
219
196
|
tempExists = true;
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
197
|
+
unregisterTempPath.setIdentity(await writeTempFile({
|
|
198
|
+
fsModule,
|
|
199
|
+
tempPath,
|
|
200
|
+
content: options.content,
|
|
201
|
+
mode,
|
|
202
|
+
sync: options.syncTempFile === true,
|
|
203
|
+
}));
|
|
225
204
|
if (options.beforeRename) {
|
|
226
205
|
await options.beforeRename({ filePath, tempPath });
|
|
227
206
|
}
|
|
@@ -236,10 +215,12 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
236
215
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
237
216
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
238
217
|
destinationHardlinks: options.destinationHardlinks,
|
|
218
|
+
syncFallback: options.syncTempFile === true,
|
|
239
219
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
220
|
+
if (result.method === "rename") {
|
|
221
|
+
tempExists = false;
|
|
222
|
+
unregisterTempPath();
|
|
223
|
+
}
|
|
243
224
|
if (options.syncParentDir) {
|
|
244
225
|
await syncDirectoryBestEffort(fsModule, dir);
|
|
245
226
|
}
|
|
@@ -250,15 +231,17 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
250
231
|
throw error;
|
|
251
232
|
}
|
|
252
233
|
finally {
|
|
234
|
+
let tempRemoved = !tempExists;
|
|
253
235
|
if (tempExists) {
|
|
254
|
-
await cleanupTempFile({
|
|
236
|
+
tempRemoved = await cleanupTempFile({
|
|
255
237
|
fsModule,
|
|
256
238
|
tempPath,
|
|
257
239
|
originalError,
|
|
258
240
|
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
259
241
|
});
|
|
260
242
|
}
|
|
261
|
-
|
|
243
|
+
if (tempRemoved)
|
|
244
|
+
unregisterTempPath();
|
|
262
245
|
}
|
|
263
246
|
}
|
|
264
247
|
export function replaceFileAtomicSync(options) {
|
|
@@ -269,24 +252,29 @@ export function replaceFileAtomicSync(options) {
|
|
|
269
252
|
const dir = path.dirname(filePath);
|
|
270
253
|
const dirMode = options.dirMode ?? 0o700;
|
|
271
254
|
const mode = resolveModeSync(options);
|
|
255
|
+
const fchmodSync = options.fileSystem?.fchmodSync ?? (options.fileSystem === undefined ? syncFs.fchmodSync : undefined);
|
|
256
|
+
if (!fchmodSync &&
|
|
257
|
+
(options.mode !== undefined ||
|
|
258
|
+
options.preserveExistingMode === true ||
|
|
259
|
+
(process.platform !== "win32" && options.dirMode !== undefined))) {
|
|
260
|
+
throw missingFchmodSyncError();
|
|
261
|
+
}
|
|
272
262
|
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
273
263
|
const unregisterTempPath = registerTempPathForExit(tempPath);
|
|
274
264
|
let tempExists = false;
|
|
275
265
|
let originalError;
|
|
276
|
-
fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
277
|
-
try {
|
|
278
|
-
fsModule.chmodSync(dir, dirMode);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
282
|
-
}
|
|
283
266
|
try {
|
|
267
|
+
fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
268
|
+
applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync });
|
|
284
269
|
tempExists = true;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
270
|
+
unregisterTempPath.setIdentity(writeTempFileSync({
|
|
271
|
+
fsModule,
|
|
272
|
+
tempPath,
|
|
273
|
+
content: options.content,
|
|
274
|
+
mode,
|
|
275
|
+
fchmodSync,
|
|
276
|
+
sync: options.syncTempFile === true,
|
|
277
|
+
}));
|
|
290
278
|
if (options.beforeRename) {
|
|
291
279
|
options.beforeRename({ filePath, tempPath });
|
|
292
280
|
}
|
|
@@ -301,14 +289,12 @@ export function replaceFileAtomicSync(options) {
|
|
|
301
289
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
302
290
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
303
291
|
destinationHardlinks: options.destinationHardlinks,
|
|
292
|
+
fchmodSync,
|
|
293
|
+
syncFallback: options.syncTempFile === true,
|
|
304
294
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
fsModule.chmodSync(filePath, mode);
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
295
|
+
if (result.method === "rename") {
|
|
296
|
+
tempExists = false;
|
|
297
|
+
unregisterTempPath();
|
|
312
298
|
}
|
|
313
299
|
if (options.syncParentDir) {
|
|
314
300
|
syncDirectoryBestEffortSync(fsModule, dir);
|
|
@@ -320,17 +306,23 @@ export function replaceFileAtomicSync(options) {
|
|
|
320
306
|
throw error;
|
|
321
307
|
}
|
|
322
308
|
finally {
|
|
309
|
+
let tempRemoved = !tempExists;
|
|
323
310
|
if (tempExists) {
|
|
324
311
|
try {
|
|
325
312
|
fsModule.rmSync(tempPath, { force: true });
|
|
313
|
+
tempRemoved = true;
|
|
326
314
|
}
|
|
327
315
|
catch (cleanupError) {
|
|
328
|
-
if (options.throwOnCleanupError
|
|
329
|
-
|
|
316
|
+
if (options.throwOnCleanupError) {
|
|
317
|
+
if (originalError !== undefined) {
|
|
318
|
+
throw new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
|
|
319
|
+
}
|
|
320
|
+
throw cleanupError;
|
|
330
321
|
}
|
|
331
322
|
// The temp file is best-effort cleanup after write failure.
|
|
332
323
|
}
|
|
333
324
|
}
|
|
334
|
-
|
|
325
|
+
if (tempRemoved)
|
|
326
|
+
unregisterTempPath();
|
|
335
327
|
}
|
|
336
328
|
}
|
package/dist/root-context.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export type RootContext = {
|
|
2
2
|
rootDir: string;
|
|
3
|
+
rootIdentity: {
|
|
4
|
+
dev: number;
|
|
5
|
+
ino: number;
|
|
6
|
+
};
|
|
3
7
|
rootReal: string;
|
|
4
8
|
rootWithSep: string;
|
|
5
9
|
};
|
|
6
10
|
export declare const ensureTrailingSep: (value: string) => string;
|
|
7
11
|
export declare function assertValidRootRelativePath(relativePath: string): void;
|
|
12
|
+
export declare function assertValidRootDestinationPath(relativePath: string): void;
|
|
8
13
|
export declare function expandRelativePathWithHome(relativePath: string): Promise<string>;
|
|
9
14
|
export declare function resolveRootContext(rootDir: string): Promise<RootContext>;
|
|
15
|
+
export declare function assertRootIdentityCurrent(root: RootContext): Promise<void>;
|
|
10
16
|
export declare function resolvePathInRoot(root: RootContext, relativePath: string, options?: {
|
|
11
17
|
aliasErrorCode?: "outside-workspace" | "path-alias";
|
|
12
18
|
allowFinalSymlink?: boolean;
|
|
19
|
+
rejectUnsafeDeviceReads?: boolean;
|
|
20
|
+
rejectSymlinks?: boolean;
|
|
13
21
|
}): Promise<{
|
|
14
22
|
rootReal: string;
|
|
15
23
|
rootWithSep: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAKzE;AAID,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0B9E;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhF;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
|
package/dist/root-context.js
CHANGED
|
@@ -2,13 +2,22 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
5
6
|
import { expandHomePrefix } from "./home-dir.js";
|
|
6
|
-
import { assertNoNulPathInput, isNotFoundPathError, isPathInside } from "./path.js";
|
|
7
|
+
import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, } from "./path.js";
|
|
7
8
|
import { ROOT_PATH_ALIAS_POLICIES, resolveRootPath } from "./root-path.js";
|
|
9
|
+
import { outsideWorkspaceError } from "./root-errors.js";
|
|
10
|
+
import { isDriveRelativePath } from "./safe-path-segment.js";
|
|
8
11
|
export const ensureTrailingSep = (value) => value.endsWith(path.sep) ? value : value + path.sep;
|
|
9
12
|
export function assertValidRootRelativePath(relativePath) {
|
|
10
13
|
assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
|
|
11
14
|
}
|
|
15
|
+
export function assertValidRootDestinationPath(relativePath) {
|
|
16
|
+
assertValidRootRelativePath(relativePath);
|
|
17
|
+
if (isDriveRelativePath(relativePath)) {
|
|
18
|
+
throw new FsSafeError("invalid-path", "relative path must not start with a drive letter");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
let cachedHomePath;
|
|
13
22
|
export async function expandRelativePathWithHome(relativePath) {
|
|
14
23
|
const rawHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
@@ -27,12 +36,14 @@ export async function expandRelativePathWithHome(relativePath) {
|
|
|
27
36
|
export async function resolveRootContext(rootDir) {
|
|
28
37
|
assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
|
|
29
38
|
let rootReal;
|
|
39
|
+
let rootIdentity;
|
|
30
40
|
try {
|
|
31
41
|
rootReal = await fs.realpath(rootDir);
|
|
32
42
|
const rootStat = await fs.stat(rootReal);
|
|
33
43
|
if (!rootStat.isDirectory()) {
|
|
34
44
|
throw new FsSafeError("invalid-path", "root dir is not a directory");
|
|
35
45
|
}
|
|
46
|
+
rootIdentity = { dev: rootStat.dev, ino: rootStat.ino };
|
|
36
47
|
}
|
|
37
48
|
catch (err) {
|
|
38
49
|
if (err instanceof FsSafeError) {
|
|
@@ -45,16 +56,37 @@ export async function resolveRootContext(rootDir) {
|
|
|
45
56
|
}
|
|
46
57
|
return {
|
|
47
58
|
rootDir: path.resolve(rootDir),
|
|
59
|
+
rootIdentity,
|
|
48
60
|
rootReal,
|
|
49
61
|
rootWithSep: ensureTrailingSep(rootReal),
|
|
50
62
|
};
|
|
51
63
|
}
|
|
64
|
+
export async function assertRootIdentityCurrent(root) {
|
|
65
|
+
let current;
|
|
66
|
+
try {
|
|
67
|
+
current = await fs.lstat(root.rootReal);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new FsSafeError("path-mismatch", "root path changed during operation", {
|
|
71
|
+
cause: error instanceof Error ? error : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (current.isSymbolicLink() ||
|
|
75
|
+
!current.isDirectory() ||
|
|
76
|
+
!sameFileIdentity(current, root.rootIdentity)) {
|
|
77
|
+
throw new FsSafeError("path-mismatch", "root path changed during operation");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
52
80
|
export async function resolvePathInRoot(root, relativePath, options) {
|
|
53
81
|
assertValidRootRelativePath(relativePath);
|
|
82
|
+
await assertRootIdentityCurrent(root);
|
|
54
83
|
const expanded = await expandRelativePathWithHome(relativePath);
|
|
55
84
|
const resolved = path.resolve(root.rootWithSep, expanded);
|
|
56
85
|
if (!isPathInside(root.rootWithSep, resolved)) {
|
|
57
|
-
throw
|
|
86
|
+
throw outsideWorkspaceError();
|
|
87
|
+
}
|
|
88
|
+
if (options?.rejectUnsafeDeviceReads === true) {
|
|
89
|
+
assertNoUnsafeDeviceReadPath(resolved);
|
|
58
90
|
}
|
|
59
91
|
const rawAbsolutePath = path.isAbsolute(expanded)
|
|
60
92
|
? expanded
|
|
@@ -66,9 +98,18 @@ export async function resolvePathInRoot(root, relativePath, options) {
|
|
|
66
98
|
rootCanonicalPath: root.rootReal,
|
|
67
99
|
boundaryLabel: "root",
|
|
68
100
|
policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
101
|
+
rejectSymlinks: options?.rejectSymlinks,
|
|
69
102
|
});
|
|
70
103
|
}
|
|
71
104
|
catch (error) {
|
|
105
|
+
if (error instanceof FsSafeError && error.code === "symlink") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
if (hasNodeErrorCode(error, "ENAMETOOLONG")) {
|
|
109
|
+
throw new FsSafeError("invalid-path", "relative path is too long", {
|
|
110
|
+
cause: error instanceof Error ? error : undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
72
113
|
const code = options?.aliasErrorCode ?? "outside-workspace";
|
|
73
114
|
throw new FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
|
|
74
115
|
cause: error instanceof Error ? error : undefined,
|
package/dist/root-errors.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export declare function fileNotFoundError(cause?: unknown): FsSafeError;
|
|
3
|
+
export declare function outsideWorkspaceError(): FsSafeError;
|
|
4
|
+
export declare function directoryComponentNotDirectoryError(cause?: unknown): FsSafeError;
|
|
5
|
+
export declare function hardlinkedPathNotAllowedError(): FsSafeError;
|
|
1
6
|
export declare function isAlreadyExistsError(error: unknown): boolean;
|
|
2
7
|
export declare function normalizePinnedWriteError(error: unknown): Error;
|
|
3
8
|
export declare function normalizePinnedPathError(error: unknown): Error;
|
|
9
|
+
export declare function normalizeRemoveGuardError(error: unknown): Error;
|
|
10
|
+
export declare function normalizeRemovePathError(error: unknown): Error;
|
|
4
11
|
//# sourceMappingURL=root-errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAI9D;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAU/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO9D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAQ/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAe9D"}
|