@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
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
|
+
import { syncQueueDirectoryCreation } from "./json-durable-queue-directory.js";
|
|
5
|
+
import { acknowledgeDurableQueueEntry, claimDurableQueueEntry, completeDeliveredQueueEntry, moveDurableQueueEntryToFailed, } from "./json-durable-queue-ownership.js";
|
|
4
6
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
7
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
5
8
|
import { replaceFileAtomic } from "./replace-file.js";
|
|
6
9
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
10
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
7
11
|
export const DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES = 16 * 1024 * 1024;
|
|
8
12
|
function getErrnoCode(error) {
|
|
9
13
|
return error && typeof error === "object" && "code" in error
|
|
@@ -46,6 +50,7 @@ export function resolveJsonDurableQueueEntryPaths(queueDir, id) {
|
|
|
46
50
|
return {
|
|
47
51
|
jsonPath: path.join(queueDir, `${id}.json`),
|
|
48
52
|
deliveredPath: path.join(queueDir, `${id}.delivered`),
|
|
53
|
+
processingPath: path.join(queueDir, `${id}.processing`),
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
export async function ensureJsonDurableQueueDirs(params) {
|
|
@@ -61,6 +66,7 @@ async function ensureJsonDurableQueueDir(dir, validationRoot) {
|
|
|
61
66
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
62
67
|
await assertNoSymlinkDirectorySegments(root, dir, false);
|
|
63
68
|
await chmodQueueDirectory(dir);
|
|
69
|
+
await syncQueueDirectoryCreation(dir, root.path);
|
|
64
70
|
}
|
|
65
71
|
async function assertJsonDurableQueueDir(dir, validationRoot) {
|
|
66
72
|
const root = validationRoot
|
|
@@ -208,35 +214,55 @@ export async function writeJsonDurableQueueEntry(params) {
|
|
|
208
214
|
content: stringifyJsonDocument(params.entry, null, 2),
|
|
209
215
|
mode: 0o600,
|
|
210
216
|
tempPrefix: params.tempPrefix,
|
|
217
|
+
syncTempFile: true,
|
|
218
|
+
syncParentDir: true,
|
|
211
219
|
});
|
|
212
220
|
}
|
|
213
|
-
async function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
async function replaceJsonDurableQueueEntry(params) {
|
|
222
|
+
await replaceFileAtomic({
|
|
223
|
+
filePath: params.filePath,
|
|
224
|
+
content: stringifyJsonDocument(params.entry, null, 2),
|
|
225
|
+
mode: 0o600,
|
|
226
|
+
tempPrefix: params.tempPrefix,
|
|
227
|
+
syncTempFile: true,
|
|
228
|
+
syncParentDir: true,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async function inspectQueueEntry(inspect, maxBytes, expected) {
|
|
232
|
+
let inspectionFailed = false;
|
|
233
|
+
try {
|
|
234
|
+
return await inspectFileIdentity(async () => {
|
|
235
|
+
try {
|
|
236
|
+
const stat = await inspect();
|
|
237
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
238
|
+
throw new Error("queue entry is not a regular file");
|
|
239
|
+
}
|
|
240
|
+
if (stat.nlink > 1n)
|
|
241
|
+
throw new Error("queue entry hardlinks are not allowed");
|
|
242
|
+
if (stat.size > maxBytes)
|
|
243
|
+
throw new Error(`queue entry exceeds ${maxBytes} bytes`);
|
|
244
|
+
return stat;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
inspectionFailed = true;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}, expected);
|
|
220
251
|
}
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
catch (error) {
|
|
253
|
+
// Translate only the identity helper's rejection, never inspection errors.
|
|
254
|
+
if (inspectionFailed)
|
|
255
|
+
throw error;
|
|
256
|
+
throw new Error("queue entry changed during read", { cause: error });
|
|
223
257
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
258
|
+
}
|
|
259
|
+
async function readBoundedUtf8File(params) {
|
|
260
|
+
const inspectPath = () => fs.promises.lstat(params.filePath, { bigint: true });
|
|
261
|
+
const initialStat = await inspectQueueEntry(inspectPath, params.maxBytes);
|
|
262
|
+
const handle = await fs.promises.open(params.filePath, resolveReadOpenFlags());
|
|
228
263
|
try {
|
|
229
|
-
const openedStat = await handle.stat();
|
|
230
|
-
|
|
231
|
-
if (!openedStat.isFile() ||
|
|
232
|
-
pathStat.isSymbolicLink() ||
|
|
233
|
-
!pathStat.isFile() ||
|
|
234
|
-
openedStat.nlink > 1 ||
|
|
235
|
-
pathStat.nlink > 1 ||
|
|
236
|
-
!sameFileIdentity(initialStat, openedStat) ||
|
|
237
|
-
!sameFileIdentity(pathStat, openedStat)) {
|
|
238
|
-
throw new Error("queue entry changed during read");
|
|
239
|
-
}
|
|
264
|
+
const openedStat = await inspectQueueEntry(() => handle.stat({ bigint: true }), params.maxBytes, initialStat);
|
|
265
|
+
await inspectQueueEntry(inspectPath, params.maxBytes, openedStat);
|
|
240
266
|
const chunks = [];
|
|
241
267
|
const scratch = Buffer.allocUnsafe(Math.min(64 * 1024, params.maxBytes + 1));
|
|
242
268
|
let total = 0;
|
|
@@ -259,35 +285,26 @@ async function readBoundedUtf8File(params) {
|
|
|
259
285
|
export async function readJsonDurableQueueEntry(filePath, options = {}) {
|
|
260
286
|
return JSON.parse(await readBoundedUtf8File({
|
|
261
287
|
filePath,
|
|
262
|
-
maxBytes: options.maxBytes
|
|
288
|
+
maxBytes: normalizeMaxBytes(options.maxBytes, {
|
|
289
|
+
defaultValue: DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES,
|
|
290
|
+
}),
|
|
263
291
|
}));
|
|
264
292
|
}
|
|
265
293
|
export async function ackJsonDurableQueueEntry(paths) {
|
|
266
|
-
|
|
267
|
-
await fs.promises.rename(paths.jsonPath, paths.deliveredPath);
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
if (getErrnoCode(error) === "ENOENT") {
|
|
271
|
-
await unlinkBestEffort(paths.deliveredPath);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
276
|
-
await unlinkBestEffort(paths.deliveredPath);
|
|
294
|
+
await acknowledgeDurableQueueEntry(paths);
|
|
277
295
|
}
|
|
278
296
|
export async function loadJsonDurableQueueEntry(params) {
|
|
279
297
|
try {
|
|
280
|
-
const
|
|
281
|
-
if (!
|
|
298
|
+
const claimedPath = await claimDurableQueueEntry(params.paths);
|
|
299
|
+
if (!claimedPath)
|
|
282
300
|
return null;
|
|
283
|
-
|
|
284
|
-
const raw = await readJsonDurableQueueEntry(params.paths.jsonPath, {
|
|
301
|
+
const raw = await readJsonDurableQueueEntry(claimedPath, {
|
|
285
302
|
maxBytes: params.maxBytes,
|
|
286
303
|
});
|
|
287
304
|
const result = params.read ? await params.read(raw, params.paths.jsonPath) : { entry: raw };
|
|
288
305
|
if (result.migrated) {
|
|
289
|
-
await
|
|
290
|
-
filePath:
|
|
306
|
+
await replaceJsonDurableQueueEntry({
|
|
307
|
+
filePath: claimedPath,
|
|
291
308
|
entry: result.entry,
|
|
292
309
|
tempPrefix: params.tempPrefix,
|
|
293
310
|
});
|
|
@@ -315,28 +332,41 @@ export async function loadPendingJsonDurableQueueEntries(options) {
|
|
|
315
332
|
const now = Date.now();
|
|
316
333
|
for (const file of files) {
|
|
317
334
|
if (file.endsWith(".delivered")) {
|
|
318
|
-
|
|
335
|
+
const id = file.slice(0, -".delivered".length);
|
|
336
|
+
await completeDeliveredQueueEntry(resolveJsonDurableQueueEntryPaths(options.queueDir, id));
|
|
319
337
|
}
|
|
320
338
|
else if (options.cleanupTmpMaxAgeMs !== undefined && file.endsWith(".tmp")) {
|
|
321
339
|
await unlinkStaleTmpBestEffort(path.join(options.queueDir, file), now, options.cleanupTmpMaxAgeMs);
|
|
322
340
|
}
|
|
323
341
|
}
|
|
324
|
-
const
|
|
342
|
+
const ids = [];
|
|
343
|
+
const seenIds = new Set();
|
|
325
344
|
for (const file of files) {
|
|
326
|
-
|
|
345
|
+
const suffix = file.endsWith(".processing")
|
|
346
|
+
? ".processing"
|
|
347
|
+
: file.endsWith(".json")
|
|
348
|
+
? ".json"
|
|
349
|
+
: null;
|
|
350
|
+
if (!suffix)
|
|
327
351
|
continue;
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
const id = file.slice(0, -suffix.length);
|
|
353
|
+
if (seenIds.has(id))
|
|
354
|
+
continue;
|
|
355
|
+
seenIds.add(id);
|
|
356
|
+
ids.push(id);
|
|
357
|
+
}
|
|
358
|
+
const entries = [];
|
|
359
|
+
for (const id of ids) {
|
|
330
360
|
try {
|
|
331
|
-
const
|
|
332
|
-
|
|
361
|
+
const paths = resolveJsonDurableQueueEntryPaths(options.queueDir, id);
|
|
362
|
+
const claimedPath = await claimDurableQueueEntry(paths);
|
|
363
|
+
if (!claimedPath)
|
|
333
364
|
continue;
|
|
334
|
-
}
|
|
335
|
-
const
|
|
336
|
-
const result = options.read ? await options.read(raw, filePath) : { entry: raw };
|
|
365
|
+
const raw = await readJsonDurableQueueEntry(claimedPath, { maxBytes: options.maxBytes });
|
|
366
|
+
const result = options.read ? await options.read(raw, paths.jsonPath) : { entry: raw };
|
|
337
367
|
if (result.migrated) {
|
|
338
|
-
await
|
|
339
|
-
filePath,
|
|
368
|
+
await replaceJsonDurableQueueEntry({
|
|
369
|
+
filePath: claimedPath,
|
|
340
370
|
entry: result.entry,
|
|
341
371
|
tempPrefix: options.tempPrefix,
|
|
342
372
|
});
|
|
@@ -354,5 +384,8 @@ export async function moveJsonDurableQueueEntryToFailed(params) {
|
|
|
354
384
|
const roots = await queueValidationRoots(params.queueDir, params.failedDir);
|
|
355
385
|
await assertJsonDurableQueueDir(params.queueDir, roots.queueRoot);
|
|
356
386
|
await ensureJsonDurableQueueDir(params.failedDir, roots.failedRoot);
|
|
357
|
-
await
|
|
387
|
+
await moveDurableQueueEntryToFailed({
|
|
388
|
+
paths: resolveJsonDurableQueueEntryPaths(params.queueDir, params.id),
|
|
389
|
+
failedPath: path.join(params.failedDir, `${params.id}.json`),
|
|
390
|
+
});
|
|
358
391
|
}
|
package/dist/json.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"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"}
|
package/dist/json.js
CHANGED
|
@@ -3,9 +3,11 @@ import fsSync from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
7
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
7
8
|
import { readRegularFile, readRegularFileSync, statRegularFile } from "./regular-file.js";
|
|
8
9
|
import { openRootFileSync } from "./root-file.js";
|
|
10
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
11
|
import { writeTextAtomic } from "./text-atomic.js";
|
|
10
12
|
import { sleep } from "./timing.js";
|
|
11
13
|
const READ_RETRY_MAX_ATTEMPTS = 5;
|
|
@@ -48,19 +50,35 @@ async function readRegularFileIfExistsWithRetry(filePath, options = {}) {
|
|
|
48
50
|
}
|
|
49
51
|
const JSON_FILE_MODE = 0o600;
|
|
50
52
|
const JSON_DIR_MODE = 0o700;
|
|
51
|
-
const SUPPORTS_SYNC_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants;
|
|
52
53
|
function getErrorCode(err) {
|
|
53
54
|
return err instanceof Error ? err.code : undefined;
|
|
54
55
|
}
|
|
55
|
-
function trySetSecureMode(pathname) {
|
|
56
|
+
function trySetSecureMode(pathname, expectedIdentity) {
|
|
56
57
|
let fd;
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const before = fsSync.lstatSync(pathname, { bigint: true });
|
|
60
|
+
if (before.isSymbolicLink() ||
|
|
61
|
+
!before.isFile() ||
|
|
62
|
+
before.nlink !== 1n ||
|
|
63
|
+
!sameFileIdentityForCleanup(before, expectedIdentity)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
fd = fsSync.openSync(pathname, resolveReadOpenFlags());
|
|
67
|
+
const opened = fsSync.fstatSync(fd, { bigint: true });
|
|
68
|
+
const current = fsSync.lstatSync(pathname, { bigint: true });
|
|
69
|
+
if (!opened.isFile() ||
|
|
70
|
+
opened.nlink !== 1n ||
|
|
71
|
+
current.isSymbolicLink() ||
|
|
72
|
+
!current.isFile() ||
|
|
73
|
+
current.nlink !== 1n ||
|
|
74
|
+
!sameFileIdentityForCleanup(opened, expectedIdentity) ||
|
|
75
|
+
!sameFileIdentityForCleanup(current, opened)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
60
78
|
fsSync.fchmodSync(fd, JSON_FILE_MODE);
|
|
61
79
|
}
|
|
62
80
|
catch {
|
|
63
|
-
//
|
|
81
|
+
// Best-effort mode application never falls back to a mutable pathname.
|
|
64
82
|
}
|
|
65
83
|
finally {
|
|
66
84
|
if (fd !== undefined) {
|
|
@@ -129,6 +147,7 @@ function writeTempJsonFile(pathname, payload) {
|
|
|
129
147
|
try {
|
|
130
148
|
fsSync.writeFileSync(fd, payload, "utf8");
|
|
131
149
|
fsSync.fsyncSync(fd);
|
|
150
|
+
return fsSync.fstatSync(fd, { bigint: true });
|
|
132
151
|
}
|
|
133
152
|
finally {
|
|
134
153
|
fsSync.closeSync(fd);
|
|
@@ -147,16 +166,16 @@ export function tryReadJsonSync(pathname, options = {}) {
|
|
|
147
166
|
}
|
|
148
167
|
}
|
|
149
168
|
export function writeJsonSync(pathname, data) {
|
|
150
|
-
|
|
151
|
-
const tmpPath =
|
|
169
|
+
// Keep literal parent segments so staging follows the same symlinks as the target.
|
|
170
|
+
const tmpPath = path.format({ ...path.parse(pathname), base: `.fs-safe-${randomUUID()}.tmp` });
|
|
152
171
|
const payload = `${stringifyJsonDocument(data, null, 2)}\n`;
|
|
153
|
-
fsSync.mkdirSync(path.dirname(
|
|
172
|
+
fsSync.mkdirSync(path.dirname(pathname), { recursive: true, mode: JSON_DIR_MODE });
|
|
154
173
|
try {
|
|
155
|
-
writeTempJsonFile(tmpPath, payload);
|
|
156
|
-
trySetSecureMode(tmpPath);
|
|
157
|
-
renameJsonFileWithFallback(tmpPath,
|
|
158
|
-
trySetSecureMode(
|
|
159
|
-
trySyncDirectory(
|
|
174
|
+
const tempIdentity = writeTempJsonFile(tmpPath, payload);
|
|
175
|
+
trySetSecureMode(tmpPath, tempIdentity);
|
|
176
|
+
renameJsonFileWithFallback(tmpPath, pathname);
|
|
177
|
+
trySetSecureMode(pathname, tempIdentity);
|
|
178
|
+
trySyncDirectory(pathname);
|
|
160
179
|
}
|
|
161
180
|
finally {
|
|
162
181
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"AAOA,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG,sBAAsB,GAAG;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,sBAAsB,GAAG;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAqGF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,oBAAoB,GAAG,IAAI,CA6B7B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC"}
|
package/dist/local-roots.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
4
5
|
import { expandHomePrefix, resolveUserPath } from "./home-dir.js";
|
|
5
6
|
import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
|
|
@@ -129,6 +130,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
131
132
|
export async function readLocalFileFromRoots(options) {
|
|
133
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
132
134
|
const label = options.label ?? "local roots";
|
|
133
135
|
const requestedPath = path.resolve(resolveLocalPathInput(options.filePath, "file path"));
|
|
134
136
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
@@ -147,8 +149,8 @@ export async function readLocalFileFromRoots(options) {
|
|
|
147
149
|
};
|
|
148
150
|
// Leave maxBytes absent when the caller omits it so Root's own default
|
|
149
151
|
// cap remains in force instead of being overwritten by undefined.
|
|
150
|
-
if (
|
|
151
|
-
readOptions.maxBytes =
|
|
152
|
+
if (maxBytes !== undefined) {
|
|
153
|
+
readOptions.maxBytes = maxBytes;
|
|
152
154
|
}
|
|
153
155
|
// A trusted root symlink has two valid spellings. Preserve the caller's
|
|
154
156
|
// lexical spelling when possible so Root.read() still enforces its
|
package/dist/native-binding.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TarMeterLimits } from "./archive-limits.js";
|
|
1
2
|
export interface NativeFileHash {
|
|
2
3
|
bytes: number;
|
|
3
4
|
digest: string;
|
|
@@ -31,6 +32,11 @@ export interface NativeOpenBeneathResult {
|
|
|
31
32
|
fd: number;
|
|
32
33
|
containment: "kernel-atomic" | "best-effort";
|
|
33
34
|
}
|
|
35
|
+
export interface NativeOwnedTreeRemovalResult {
|
|
36
|
+
outcome?: "removed" | "preserved";
|
|
37
|
+
errorCode?: string;
|
|
38
|
+
errorMessage?: string;
|
|
39
|
+
}
|
|
34
40
|
export interface NativeWindowsAccessControlEntry {
|
|
35
41
|
sid: string;
|
|
36
42
|
mask: number;
|
|
@@ -68,14 +74,17 @@ export interface NativeBinding {
|
|
|
68
74
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
69
75
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
70
76
|
createPrivateDirectory(path: string): void;
|
|
71
|
-
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[],
|
|
77
|
+
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[], limits: TarMeterLimits, signal: AbortSignal): Promise<void>;
|
|
72
78
|
fstatIdentity(fd: number): NativeFileIdentity;
|
|
73
|
-
inspectArchiveNative(path: string, kind: string,
|
|
79
|
+
inspectArchiveNative(path: string, kind: string, limits: TarMeterLimits, signal: AbortSignal): Promise<NativeArchiveEntry[]>;
|
|
74
80
|
linkBeneath(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
75
81
|
mkdirBeneath(rootFd: number, relPath: string, mode: number): void;
|
|
76
82
|
openBeneath(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
77
|
-
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number,
|
|
83
|
+
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number, limits: TarMeterLimits, signal: AbortSignal): Promise<Buffer>;
|
|
78
84
|
readOwnerAndDacl(path: string): NativeWindowsSecurityFacts;
|
|
85
|
+
ownedTreeRemovalAvailable?(parentFd: number): boolean;
|
|
86
|
+
removeOwnedTree?(parentFd: number, basename: string, directoryFd: number): Promise<NativeOwnedTreeRemovalResult>;
|
|
87
|
+
removeOwnedTreeSync?(parentFd: number, basename: string, directoryFd: number): NativeOwnedTreeRemovalResult;
|
|
79
88
|
renameNoReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
80
89
|
renameReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
81
90
|
sha256File(fd: number): Promise<NativeFileHash>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
24
24
|
let renamed = false;
|
|
25
25
|
let completed = false;
|
|
26
26
|
try {
|
|
27
|
-
tempName =
|
|
27
|
+
tempName = `.fs-safe-${randomUUID()}.tmp`;
|
|
28
28
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
29
29
|
tempIdentity = fsSync.fstatSync(tempFd);
|
|
30
30
|
const verificationIdentity = fsSync.fstatSync(tempFd, { bigint: true });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
3
|
+
export declare function recordFileOpenFailure(error: unknown, filePath: string): never;
|
|
4
|
+
export declare function openedPathResolutionError(error?: Error): Error;
|
|
5
|
+
export declare function recordPreOpenFileChange(error: unknown, handle: FileHandle, filePath: string, before: BigIntStats | undefined, opened: BigIntStats | undefined): Promise<void>;
|
|
6
|
+
export declare function recordOpenedFileFailure(error: unknown, handle: FileHandle, filePath: string, identity: BigIntStats): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=opened-file-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
3
|
+
import { isFileObservationFailure, recordFileObservationFailure } from "./file-observation.js";
|
|
4
|
+
export function recordFileOpenFailure(error, filePath) {
|
|
5
|
+
recordFileObservationFailure(error, `open:${filePath}`);
|
|
6
|
+
throw error;
|
|
7
|
+
}
|
|
8
|
+
export function openedPathResolutionError(error = new FsSafeError("path-mismatch", "unable to resolve opened file path")) {
|
|
9
|
+
recordFileObservationFailure(error, "resolution");
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
export async function recordPreOpenFileChange(error, handle, filePath, before, opened) {
|
|
13
|
+
if (!isFileObservationFailure(error, "identity") || !before?.isFile() || !opened?.isFile() ||
|
|
14
|
+
![0n, 1n].includes(before.nlink) || ![0n, 1n].includes(opened.nlink))
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
await inspectFileIdentity(async () => before);
|
|
18
|
+
const current = await inspectFileIdentity(() => handle.stat({ bigint: true }), opened);
|
|
19
|
+
if (current.isFile() && [0n, 1n].includes(current.nlink) &&
|
|
20
|
+
(before.dev !== current.dev || before.ino !== current.ino)) {
|
|
21
|
+
// A stale pathname sample is not proof that its former inode was unlinked.
|
|
22
|
+
recordFileObservationFailure(error, `changed:${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Unknown, changed, or closed descriptor evidence never permits a discard.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function recordOpenedFileFailure(error, handle, filePath, identity) {
|
|
30
|
+
if ((!isFileObservationFailure(error, "resolution") && !isFileObservationFailure(error, "identity")) ||
|
|
31
|
+
!identity.isFile() || identity.nlink > 1n)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
const current = await inspectFileIdentity(() => handle.stat({ bigint: true }), identity);
|
|
35
|
+
if (current.isFile() && current.nlink === 0n)
|
|
36
|
+
recordFileObservationFailure(error, `unlinked:${filePath}`);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// A closed, changed, or unknown descriptor provides no retry evidence.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opened-realpath.d.ts","sourceRoot":"","sources":["../src/opened-realpath.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG7E,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"opened-realpath.d.ts","sourceRoot":"","sources":["../src/opened-realpath.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG7E,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CA2CjB"}
|
package/dist/opened-realpath.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { openedPathResolutionError } from "./opened-file-failure.js";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
5
|
import { isNotFoundPathError } from "./path.js";
|
|
6
6
|
export async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
|
|
@@ -36,6 +36,12 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
38
|
if (!isNotFoundPathError(err)) {
|
|
39
|
+
// Windows can fail here on a deleted-but-open file. Brand only this
|
|
40
|
+
// resolver operation; the caller must still prove unlink on the same fd.
|
|
41
|
+
if (process.platform === "win32" && err instanceof Error &&
|
|
42
|
+
["EPERM", "EBADF"].includes(err.code ?? "")) {
|
|
43
|
+
throw openedPathResolutionError(err);
|
|
44
|
+
}
|
|
39
45
|
throw err;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
@@ -43,7 +49,7 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
43
49
|
if (parentResolved) {
|
|
44
50
|
return parentResolved;
|
|
45
51
|
}
|
|
46
|
-
throw
|
|
52
|
+
throw openedPathResolutionError();
|
|
47
53
|
}
|
|
48
54
|
async function resolveOpenedFileRealPathFromParent(handleStat, ioPath, statOptions) {
|
|
49
55
|
let parentReal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"AAoBA,wBAAsB,2BAA2B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,CAAC,CAAC,CAYb"}
|