@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/replace-file.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import syncFs from "node:fs";
|
|
2
|
+
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
6
|
import { applyDirectoryMode, applyDirectoryModeSync, writeTempFile, writeTempFileSync, } from "./replace-file-descriptor.js";
|
|
7
|
+
import { atomicExpectedContentHash, validateRenameIdentity, withAtomicRenameIdentityLock, withAtomicRenameIdentityLockSync, } from "./replace-file-rename-policy.js";
|
|
8
|
+
import { AsyncAtomicTempOwner, SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
7
9
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
8
|
-
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
9
10
|
import { sleep, sleepSync } from "./timing.js";
|
|
10
11
|
import { serializePathWrite } from "./write-queue.js";
|
|
11
12
|
function isRetryableRenameError(error) {
|
|
@@ -17,6 +18,7 @@ function isPermissionRenameError(error) {
|
|
|
17
18
|
}
|
|
18
19
|
async function renameWithRetry(params) {
|
|
19
20
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
21
|
+
await params.assertSourceCurrent();
|
|
20
22
|
try {
|
|
21
23
|
await params.fsModule.rename(params.src, params.dest);
|
|
22
24
|
return { method: "rename" };
|
|
@@ -34,6 +36,7 @@ async function renameWithRetry(params) {
|
|
|
34
36
|
destinationHardlinks: params.destinationHardlinks,
|
|
35
37
|
restore: params.copyFallbackRestore,
|
|
36
38
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
39
|
+
expectedSourceIdentity: params.sourceIdentity,
|
|
37
40
|
sync: params.syncFallback,
|
|
38
41
|
});
|
|
39
42
|
return { method: "copy-fallback" };
|
|
@@ -45,6 +48,7 @@ async function renameWithRetry(params) {
|
|
|
45
48
|
}
|
|
46
49
|
function renameWithRetrySync(params) {
|
|
47
50
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
51
|
+
params.assertSourceCurrent();
|
|
48
52
|
try {
|
|
49
53
|
params.fsModule.renameSync(params.src, params.dest);
|
|
50
54
|
return { method: "rename" };
|
|
@@ -62,6 +66,7 @@ function renameWithRetrySync(params) {
|
|
|
62
66
|
destinationHardlinks: params.destinationHardlinks,
|
|
63
67
|
restore: params.copyFallbackRestore,
|
|
64
68
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
69
|
+
expectedSourceIdentity: params.sourceIdentity,
|
|
65
70
|
fchmodSync: params.fchmodSync,
|
|
66
71
|
sync: params.syncFallback,
|
|
67
72
|
});
|
|
@@ -158,26 +163,20 @@ function syncDirectoryBestEffortSync(fsModule, dirPath) {
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
|
-
async function cleanupTempFile(params) {
|
|
162
|
-
const cleanupError = await params.fsModule
|
|
163
|
-
.rm(params.tempPath, { force: true })
|
|
164
|
-
.catch((error) => error);
|
|
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;
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
166
|
export async function replaceFileAtomic(options) {
|
|
176
167
|
const filePath = options.filePath;
|
|
177
168
|
validateReplaceFilePath(filePath);
|
|
178
169
|
validateRestoreOptions(options);
|
|
170
|
+
validateRenameIdentity(options.renameIdentity);
|
|
179
171
|
return await serializePathWrite(path.resolve(filePath), async () => {
|
|
180
|
-
|
|
172
|
+
if (options.renameIdentity !== "verify-content-with-lock") {
|
|
173
|
+
return await replaceFileAtomicUnserialized(options);
|
|
174
|
+
}
|
|
175
|
+
await (options.fileSystem?.promises ?? fs).mkdir(path.dirname(filePath), {
|
|
176
|
+
recursive: true,
|
|
177
|
+
mode: options.dirMode ?? 0o700,
|
|
178
|
+
});
|
|
179
|
+
return await withAtomicRenameIdentityLock(filePath, async () => await replaceFileAtomicUnserialized(options));
|
|
181
180
|
});
|
|
182
181
|
}
|
|
183
182
|
async function replaceFileAtomicUnserialized(options) {
|
|
@@ -186,24 +185,27 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
186
185
|
const dir = path.dirname(filePath);
|
|
187
186
|
const dirMode = options.dirMode ?? 0o700;
|
|
188
187
|
const mode = await resolveMode(options);
|
|
188
|
+
const expectedHash = atomicExpectedContentHash(options.renameIdentity, options.content);
|
|
189
189
|
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
190
|
-
const
|
|
191
|
-
let tempExists = false;
|
|
190
|
+
const tempOwner = new AsyncAtomicTempOwner(tempPath);
|
|
192
191
|
let originalError;
|
|
193
192
|
try {
|
|
194
193
|
await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
|
|
195
194
|
await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode });
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
tempOwner.start();
|
|
196
|
+
tempOwner.adopt(await writeTempFile({
|
|
198
197
|
fsModule,
|
|
199
198
|
tempPath,
|
|
200
199
|
content: options.content,
|
|
201
200
|
mode,
|
|
202
201
|
sync: options.syncTempFile === true,
|
|
202
|
+
onIdentity: tempOwner.onIdentity,
|
|
203
203
|
}));
|
|
204
|
+
await tempOwner.assertCurrent(fsModule);
|
|
204
205
|
if (options.beforeRename) {
|
|
205
206
|
await options.beforeRename({ filePath, tempPath });
|
|
206
207
|
}
|
|
208
|
+
await tempOwner.assertCurrent(fsModule);
|
|
207
209
|
await assertDestinationHardlinkPolicy(fsModule, filePath, options.destinationHardlinks);
|
|
208
210
|
const result = await renameWithRetry({
|
|
209
211
|
fsModule,
|
|
@@ -215,15 +217,23 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
215
217
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
216
218
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
217
219
|
destinationHardlinks: options.destinationHardlinks,
|
|
220
|
+
sourceIdentity: tempOwner.identity,
|
|
221
|
+
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
218
222
|
syncFallback: options.syncTempFile === true,
|
|
219
223
|
});
|
|
220
224
|
if (result.method === "rename") {
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
tempOwner.markRenamed();
|
|
226
|
+
await tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
await tempOwner.assertCurrent(fsModule);
|
|
223
230
|
}
|
|
224
231
|
if (options.syncParentDir) {
|
|
225
232
|
await syncDirectoryBestEffort(fsModule, dir);
|
|
226
233
|
}
|
|
234
|
+
if (result.method === "rename") {
|
|
235
|
+
await tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
236
|
+
}
|
|
227
237
|
return result;
|
|
228
238
|
}
|
|
229
239
|
catch (error) {
|
|
@@ -231,27 +241,34 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
231
241
|
throw error;
|
|
232
242
|
}
|
|
233
243
|
finally {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
originalError,
|
|
240
|
-
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
if (tempRemoved)
|
|
244
|
-
unregisterTempPath();
|
|
244
|
+
await tempOwner.finish({
|
|
245
|
+
fsModule,
|
|
246
|
+
originalError,
|
|
247
|
+
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
248
|
+
});
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
export function replaceFileAtomicSync(options) {
|
|
248
252
|
const filePath = options.filePath;
|
|
249
253
|
validateReplaceFilePath(filePath);
|
|
250
254
|
validateRestoreOptions(options);
|
|
255
|
+
validateRenameIdentity(options.renameIdentity);
|
|
256
|
+
if (options.renameIdentity !== "verify-content-with-lock") {
|
|
257
|
+
return replaceFileAtomicSyncUnserialized(options);
|
|
258
|
+
}
|
|
259
|
+
(options.fileSystem ?? syncFs).mkdirSync(path.dirname(filePath), {
|
|
260
|
+
recursive: true,
|
|
261
|
+
mode: options.dirMode ?? 0o700,
|
|
262
|
+
});
|
|
263
|
+
return withAtomicRenameIdentityLockSync(filePath, () => replaceFileAtomicSyncUnserialized(options));
|
|
264
|
+
}
|
|
265
|
+
function replaceFileAtomicSyncUnserialized(options) {
|
|
266
|
+
const filePath = options.filePath;
|
|
251
267
|
const fsModule = options.fileSystem ?? syncFs;
|
|
252
268
|
const dir = path.dirname(filePath);
|
|
253
269
|
const dirMode = options.dirMode ?? 0o700;
|
|
254
270
|
const mode = resolveModeSync(options);
|
|
271
|
+
const expectedHash = atomicExpectedContentHash(options.renameIdentity, options.content);
|
|
255
272
|
const fchmodSync = options.fileSystem?.fchmodSync ?? (options.fileSystem === undefined ? syncFs.fchmodSync : undefined);
|
|
256
273
|
if (!fchmodSync &&
|
|
257
274
|
(options.mode !== undefined ||
|
|
@@ -260,24 +277,26 @@ export function replaceFileAtomicSync(options) {
|
|
|
260
277
|
throw missingFchmodSyncError();
|
|
261
278
|
}
|
|
262
279
|
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
263
|
-
const
|
|
264
|
-
let tempExists = false;
|
|
280
|
+
const tempOwner = new SyncAtomicTempOwner(tempPath);
|
|
265
281
|
let originalError;
|
|
266
282
|
try {
|
|
267
283
|
fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
268
284
|
applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync });
|
|
269
|
-
|
|
270
|
-
|
|
285
|
+
tempOwner.start();
|
|
286
|
+
tempOwner.adopt(writeTempFileSync({
|
|
271
287
|
fsModule,
|
|
272
288
|
tempPath,
|
|
273
289
|
content: options.content,
|
|
274
290
|
mode,
|
|
275
291
|
fchmodSync,
|
|
276
292
|
sync: options.syncTempFile === true,
|
|
293
|
+
onIdentity: tempOwner.onIdentity,
|
|
277
294
|
}));
|
|
295
|
+
tempOwner.assertCurrent(fsModule);
|
|
278
296
|
if (options.beforeRename) {
|
|
279
297
|
options.beforeRename({ filePath, tempPath });
|
|
280
298
|
}
|
|
299
|
+
tempOwner.assertCurrent(fsModule);
|
|
281
300
|
assertDestinationHardlinkPolicySync(fsModule, filePath, options.destinationHardlinks);
|
|
282
301
|
const result = renameWithRetrySync({
|
|
283
302
|
fsModule,
|
|
@@ -289,16 +308,24 @@ export function replaceFileAtomicSync(options) {
|
|
|
289
308
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
290
309
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
291
310
|
destinationHardlinks: options.destinationHardlinks,
|
|
311
|
+
sourceIdentity: tempOwner.identity,
|
|
312
|
+
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
292
313
|
fchmodSync,
|
|
293
314
|
syncFallback: options.syncTempFile === true,
|
|
294
315
|
});
|
|
295
316
|
if (result.method === "rename") {
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
tempOwner.markRenamed();
|
|
318
|
+
tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
tempOwner.assertCurrent(fsModule);
|
|
298
322
|
}
|
|
299
323
|
if (options.syncParentDir) {
|
|
300
324
|
syncDirectoryBestEffortSync(fsModule, dir);
|
|
301
325
|
}
|
|
326
|
+
if (result.method === "rename") {
|
|
327
|
+
tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
328
|
+
}
|
|
302
329
|
return result;
|
|
303
330
|
}
|
|
304
331
|
catch (error) {
|
|
@@ -306,23 +333,10 @@ export function replaceFileAtomicSync(options) {
|
|
|
306
333
|
throw error;
|
|
307
334
|
}
|
|
308
335
|
finally {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
catch (cleanupError) {
|
|
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;
|
|
321
|
-
}
|
|
322
|
-
// The temp file is best-effort cleanup after write failure.
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (tempRemoved)
|
|
326
|
-
unregisterTempPath();
|
|
336
|
+
tempOwner.finish({
|
|
337
|
+
fsModule,
|
|
338
|
+
originalError,
|
|
339
|
+
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
340
|
+
});
|
|
327
341
|
}
|
|
328
342
|
}
|
package/dist/root-impl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
|
package/dist/root-impl.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants as fsConstants } 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 { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
@@ -10,6 +11,7 @@ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
|
10
11
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
11
12
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
12
13
|
import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
14
|
+
import { openedPathResolutionError, recordFileOpenFailure, recordOpenedFileFailure, recordPreOpenFileChange } from "./opened-file-failure.js";
|
|
13
15
|
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
14
16
|
import { getNativeBinding } from "./native.js";
|
|
15
17
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
@@ -101,25 +103,15 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
101
103
|
if (preOpenStat) {
|
|
102
104
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
103
105
|
}
|
|
106
|
+
const openFlags = options?.symlinks === "follow-within-root"
|
|
107
|
+
? OPEN_READ_FOLLOW_FLAGS
|
|
108
|
+
: OPEN_READ_FLAGS;
|
|
109
|
+
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
104
110
|
let handle;
|
|
105
111
|
try {
|
|
106
|
-
|
|
107
|
-
? OPEN_READ_FOLLOW_FLAGS
|
|
108
|
-
: OPEN_READ_FLAGS;
|
|
109
|
-
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
110
|
-
handle = await fs.open(filePath, openFlags);
|
|
111
|
-
try {
|
|
112
|
-
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
await handle.close().catch(() => { });
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
112
|
+
handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
|
|
118
113
|
}
|
|
119
114
|
catch (err) {
|
|
120
|
-
if (isNotFoundPathError(err)) {
|
|
121
|
-
throw fileNotFoundError();
|
|
122
|
-
}
|
|
123
115
|
if (isSymlinkOpenError(err)) {
|
|
124
116
|
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
125
117
|
}
|
|
@@ -130,16 +122,35 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
130
122
|
throw err;
|
|
131
123
|
}
|
|
132
124
|
try {
|
|
125
|
+
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
133
126
|
const stat = await handle.stat();
|
|
134
127
|
if (!stat.isFile()) {
|
|
135
128
|
throw new FsSafeError("not-file", "not a file");
|
|
136
129
|
}
|
|
137
130
|
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
138
|
-
|
|
131
|
+
let openedIdentity;
|
|
132
|
+
const identity = await inspectFileIdentity(async () => (openedIdentity = await handle.stat({ bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
|
|
133
|
+
if ([0, 1].includes(stat.nlink)) {
|
|
134
|
+
await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
});
|
|
139
138
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
140
139
|
throw hardlinkedPathNotAllowedError();
|
|
141
140
|
}
|
|
142
|
-
|
|
141
|
+
const inspectPathIdentity = async (inspect) => {
|
|
142
|
+
try {
|
|
143
|
+
return await inspectFileIdentity(inspect, identity);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const failure = isNotFoundPathError(error) ? openedPathResolutionError(fileNotFoundError()) : error;
|
|
147
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
148
|
+
await recordOpenedFileFailure(failure, handle, filePath, identity);
|
|
149
|
+
}
|
|
150
|
+
throw failure;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
await inspectPathIdentity(async () => {
|
|
143
154
|
const pathStat = options?.symlinks === "follow-within-root"
|
|
144
155
|
? await fs.stat(filePath, { bigint: true })
|
|
145
156
|
: await fs.lstat(filePath, { bigint: true });
|
|
@@ -147,25 +158,26 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
147
158
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
148
159
|
}
|
|
149
160
|
return pathStat;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
await
|
|
161
|
+
});
|
|
162
|
+
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
163
|
+
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
164
|
+
.catch(async (error) => {
|
|
165
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
166
|
+
await recordOpenedFileFailure(error, handle, filePath, identity);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
await inspectPathIdentity(async () => {
|
|
153
171
|
const realStat = await fs.stat(realPath, { bigint: true });
|
|
154
172
|
if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
|
|
155
173
|
throw hardlinkedPathNotAllowedError();
|
|
156
174
|
}
|
|
157
175
|
return realStat;
|
|
158
|
-
}
|
|
159
|
-
return openResult({ handle, realPath, stat });
|
|
176
|
+
});
|
|
177
|
+
return { opened: openResult({ handle, realPath, stat }), identity };
|
|
160
178
|
}
|
|
161
179
|
catch (err) {
|
|
162
180
|
await handle.close().catch(() => { });
|
|
163
|
-
if (err instanceof FsSafeError) {
|
|
164
|
-
throw err;
|
|
165
|
-
}
|
|
166
|
-
if (isNotFoundPathError(err)) {
|
|
167
|
-
throw fileNotFoundError();
|
|
168
|
-
}
|
|
169
181
|
throw err;
|
|
170
182
|
}
|
|
171
183
|
}
|
|
@@ -210,8 +222,7 @@ class RootHandle {
|
|
|
210
222
|
async read(relativePath, options = {}) {
|
|
211
223
|
return await readFileInRoot(this.context, {
|
|
212
224
|
relativePath,
|
|
213
|
-
...
|
|
214
|
-
...options,
|
|
225
|
+
...mergeReadOptions(this.defaults, options),
|
|
215
226
|
});
|
|
216
227
|
}
|
|
217
228
|
async readBytes(relativePath, options = {}) {
|
|
@@ -227,8 +238,7 @@ class RootHandle {
|
|
|
227
238
|
async readAbsolute(filePath, options = {}) {
|
|
228
239
|
return await readPathInRoot(this.context, {
|
|
229
240
|
filePath,
|
|
230
|
-
...
|
|
231
|
-
...options,
|
|
241
|
+
...mergeReadOptions(this.defaults, options),
|
|
232
242
|
});
|
|
233
243
|
}
|
|
234
244
|
reader(options = {}) {
|
|
@@ -313,13 +323,14 @@ class RootHandle {
|
|
|
313
323
|
}
|
|
314
324
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
315
325
|
assertValidRootDestinationPath(relativePath);
|
|
326
|
+
const { maxBytes, ...copyOptions } = this.mutationOptions(options);
|
|
316
327
|
await copyFileInRoot(this.context, {
|
|
317
328
|
sourcePath,
|
|
318
329
|
relativePath,
|
|
319
|
-
maxBytes: this.defaults.maxBytes,
|
|
330
|
+
maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
|
|
320
331
|
mkdir: this.defaults.mkdir,
|
|
321
332
|
mode: this.defaults.mode,
|
|
322
|
-
...
|
|
333
|
+
...copyOptions,
|
|
323
334
|
});
|
|
324
335
|
}
|
|
325
336
|
async exists(relativePath) {
|
|
@@ -369,12 +380,24 @@ class RootHandle {
|
|
|
369
380
|
function readDefaults(defaults) {
|
|
370
381
|
return {
|
|
371
382
|
hardlinks: defaults.hardlinks,
|
|
372
|
-
maxBytes: defaults.maxBytes
|
|
383
|
+
maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
|
|
373
384
|
nonBlockingRead: defaults.nonBlockingRead,
|
|
374
385
|
symlinks: defaults.symlinks,
|
|
375
386
|
};
|
|
376
387
|
}
|
|
388
|
+
function mergeReadOptions(defaults, options) {
|
|
389
|
+
const merged = readDefaults(defaults);
|
|
390
|
+
if (options.hardlinks !== undefined)
|
|
391
|
+
merged.hardlinks = options.hardlinks;
|
|
392
|
+
merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
|
|
393
|
+
if (options.nonBlockingRead !== undefined)
|
|
394
|
+
merged.nonBlockingRead = options.nonBlockingRead;
|
|
395
|
+
if (options.symlinks !== undefined)
|
|
396
|
+
merged.symlinks = options.symlinks;
|
|
397
|
+
return merged;
|
|
398
|
+
}
|
|
377
399
|
export async function root(rootDir, defaults = {}) {
|
|
400
|
+
normalizeMaxBytes(defaults.maxBytes);
|
|
378
401
|
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
379
402
|
}
|
|
380
403
|
async function openFileInRoot(root, params) {
|
|
@@ -383,19 +406,10 @@ async function openFileInRoot(root, params) {
|
|
|
383
406
|
rejectUnsafeDeviceReads: true,
|
|
384
407
|
rejectSymlinks: params.symlinks !== "follow-within-root",
|
|
385
408
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
symlinks: params.symlinks,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
catch (err) {
|
|
394
|
-
if (err instanceof FsSafeError) {
|
|
395
|
-
throw err;
|
|
396
|
-
}
|
|
397
|
-
throw err;
|
|
398
|
-
}
|
|
409
|
+
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
410
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
411
|
+
symlinks: params.symlinks,
|
|
412
|
+
});
|
|
399
413
|
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
400
414
|
await opened.handle.close().catch(() => { });
|
|
401
415
|
throw hardlinkedPathNotAllowedError();
|
|
@@ -430,9 +444,10 @@ async function readPathInRoot(root, params) {
|
|
|
430
444
|
});
|
|
431
445
|
}
|
|
432
446
|
export async function readLocalFileSafely(params) {
|
|
447
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
433
448
|
const opened = await openLocalFileSafely({ filePath: params.filePath });
|
|
434
449
|
try {
|
|
435
|
-
return await readOpenedFileSafely({ opened, maxBytes
|
|
450
|
+
return await readOpenedFileSafely({ opened, maxBytes });
|
|
436
451
|
}
|
|
437
452
|
finally {
|
|
438
453
|
await opened.handle.close().catch(() => { });
|
|
@@ -440,15 +455,13 @@ export async function readLocalFileSafely(params) {
|
|
|
440
455
|
}
|
|
441
456
|
export async function openLocalFileSafely(params) {
|
|
442
457
|
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
443
|
-
return await openVerifiedLocalFile(params.filePath);
|
|
458
|
+
return (await openVerifiedLocalFile(params.filePath)).opened;
|
|
444
459
|
}
|
|
445
460
|
function emitWriteBoundaryWarning(reason) {
|
|
446
461
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
447
462
|
}
|
|
448
463
|
function buildAtomicWriteTempPath(targetPath) {
|
|
449
|
-
|
|
450
|
-
const base = path.basename(targetPath);
|
|
451
|
-
return path.join(dir, `.${base}.${process.pid}.${randomUUID()}.tmp`);
|
|
464
|
+
return path.join(path.dirname(targetPath), `.fs-safe-${randomUUID()}.tmp`);
|
|
452
465
|
}
|
|
453
466
|
function rootWriteQueueKey(root, relativePath) {
|
|
454
467
|
return `${root.rootReal}\0${relativePath}`;
|
|
@@ -687,7 +700,7 @@ async function writeFileInRoot(root, params) {
|
|
|
687
700
|
await writeFileFallback(root, params);
|
|
688
701
|
return;
|
|
689
702
|
}
|
|
690
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
703
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
|
|
691
704
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
692
705
|
await commitPinnedWriteInRoot(root, pinned, params);
|
|
693
706
|
});
|
|
@@ -743,7 +756,7 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
743
756
|
async function copyFileInRoot(root, params) {
|
|
744
757
|
assertValidRootRelativePath(params.relativePath);
|
|
745
758
|
assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
|
|
746
|
-
const source = await openVerifiedLocalFile(params.sourcePath, {
|
|
759
|
+
const { opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.sourcePath, {
|
|
747
760
|
hardlinks: params.sourceHardlinks,
|
|
748
761
|
});
|
|
749
762
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
@@ -754,7 +767,7 @@ async function copyFileInRoot(root, params) {
|
|
|
754
767
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
755
768
|
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
756
769
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
757
|
-
await assertCopySourceCurrent(source);
|
|
770
|
+
await assertCopySourceCurrent(source, sourceIdentity);
|
|
758
771
|
let identity;
|
|
759
772
|
try {
|
|
760
773
|
identity = await runPinnedWriteHelper({
|
|
@@ -773,7 +786,7 @@ async function copyFileInRoot(root, params) {
|
|
|
773
786
|
throw normalizePinnedWriteError(error);
|
|
774
787
|
}
|
|
775
788
|
try {
|
|
776
|
-
await assertCopySourcePathCurrent(source);
|
|
789
|
+
await assertCopySourcePathCurrent(source, sourceIdentity);
|
|
777
790
|
}
|
|
778
791
|
catch (error) {
|
|
779
792
|
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
@@ -786,18 +799,18 @@ async function copyFileInRoot(root, params) {
|
|
|
786
799
|
await source.handle.close().catch(() => { });
|
|
787
800
|
}
|
|
788
801
|
}
|
|
789
|
-
async function assertCopySourceCurrent(source) {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
throw new FsSafeError("path-mismatch", "copy source descriptor changed");
|
|
793
|
-
}
|
|
794
|
-
await assertCopySourcePathCurrent(source);
|
|
802
|
+
async function assertCopySourceCurrent(source, identity) {
|
|
803
|
+
await inspectFileIdentity(() => source.handle.stat({ bigint: true }), identity);
|
|
804
|
+
await assertCopySourcePathCurrent(source, identity);
|
|
795
805
|
}
|
|
796
|
-
async function assertCopySourcePathCurrent(source) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
806
|
+
async function assertCopySourcePathCurrent(source, identity) {
|
|
807
|
+
await inspectFileIdentity(async () => {
|
|
808
|
+
const current = await fs.lstat(source.realPath, { bigint: true });
|
|
809
|
+
if (current.isSymbolicLink() || !current.isFile()) {
|
|
810
|
+
throw new FsSafeError("path-mismatch", "copy source path changed");
|
|
811
|
+
}
|
|
812
|
+
return current;
|
|
813
|
+
}, identity);
|
|
801
814
|
}
|
|
802
815
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
803
816
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
@@ -809,7 +822,7 @@ async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
|
809
822
|
await fs.rm(targetPath);
|
|
810
823
|
});
|
|
811
824
|
}
|
|
812
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
825
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
813
826
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
814
827
|
relativePath,
|
|
815
828
|
denyMutations,
|
|
@@ -827,22 +840,40 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
827
840
|
if (!basename || basename === "." || basename === "/") {
|
|
828
841
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
829
842
|
}
|
|
843
|
+
// Exclusive publication cannot inherit an existing file's mode. Keep type
|
|
844
|
+
// and alias checks, but never open a competing owner's record just to reject it.
|
|
845
|
+
if (!overwrite) {
|
|
846
|
+
try {
|
|
847
|
+
const existing = await fs.stat(resolved);
|
|
848
|
+
if (!existing.isFile())
|
|
849
|
+
throw new FsSafeError("not-file", "not a file");
|
|
850
|
+
if (existing.nlink > 1)
|
|
851
|
+
throw hardlinkedPathNotAllowedError();
|
|
852
|
+
throw new FsSafeError("already-exists", "file already exists");
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (!isNotFoundPathError(error))
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
830
859
|
let mode = requestedMode ?? 0o600;
|
|
831
860
|
try {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
861
|
+
if (overwrite) {
|
|
862
|
+
const opened = await openFileInRoot(root, {
|
|
863
|
+
relativePath,
|
|
864
|
+
hardlinks: "reject",
|
|
865
|
+
nonBlockingRead: true,
|
|
866
|
+
symlinks: "follow-within-root",
|
|
867
|
+
});
|
|
868
|
+
try {
|
|
869
|
+
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
870
|
+
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
871
|
+
throw outsideWorkspaceError();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
await opened.handle.close().catch(() => { });
|
|
842
876
|
}
|
|
843
|
-
}
|
|
844
|
-
finally {
|
|
845
|
-
await opened.handle.close().catch(() => { });
|
|
846
877
|
}
|
|
847
878
|
}
|
|
848
879
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAaA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAwER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AAkID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|