@openclaw/fs-safe 0.19.0 → 0.21.0
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 +50 -0
- package/README.md +24 -6
- package/dist/advanced.d.ts +4 -0
- package/dist/advanced.js +2 -0
- package/dist/archive-plan.d.ts +2 -7
- package/dist/archive-read.js +9 -18
- package/dist/archive-zip-entry.d.ts +11 -11
- package/dist/archive-zip-entry.js +3 -35
- package/dist/archive-zip-integrity.d.ts +2 -2
- package/dist/archive-zip-integrity.js +2 -12
- package/dist/archive-zip-loader.d.ts +7 -3
- package/dist/archive-zip-loader.js +10 -9
- package/dist/archive-zip-preflight.d.ts +2 -1
- package/dist/archive-zip-preflight.js +16 -7
- package/dist/archive.js +17 -16
- package/dist/atomic.d.ts +1 -1
- package/dist/directory-receipt.js +5 -7
- package/dist/effective-uid.js +1 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -2
- package/dist/file-lock-sync-root-held.js +1 -4
- package/dist/file-store.d.ts +4 -7
- package/dist/json-document-store.d.ts +4 -9
- package/dist/local-file-access.js +2 -5
- package/dist/move-path-cleanup.js +4 -4
- package/dist/native-binding.d.ts +28 -1
- package/dist/native-staged-symlink.d.ts +13 -0
- package/dist/native-staged-symlink.js +303 -0
- package/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/dist/owner-dacl-batch-worker.js +54 -0
- package/dist/owner-dacl-batch.d.ts +5 -0
- package/dist/owner-dacl-batch.js +64 -0
- package/dist/owner-dacl.d.ts +2 -0
- package/dist/owner-dacl.js +3 -0
- package/dist/path.js +17 -1
- package/dist/permission-exec.js +3 -6
- package/dist/permissions-public.d.ts +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/pinned-mutation-admission.d.ts +0 -1
- package/dist/pinned-open.d.ts +0 -1
- package/dist/pinned-open.js +1 -2
- package/dist/publish-copy-stage.js +4 -0
- package/dist/read-opened-file.d.ts +2 -5
- package/dist/regular-file.js +3 -3
- package/dist/replace-file-buffer.d.ts +4 -0
- package/dist/replace-file-buffer.js +36 -0
- package/dist/replace-file-copy-fallback.d.ts +3 -2
- package/dist/replace-file-copy-fallback.js +66 -38
- package/dist/replace-file-descriptor.d.ts +4 -0
- package/dist/replace-file-descriptor.js +9 -1
- package/dist/replace-file-destination.d.ts +17 -0
- package/dist/replace-file-destination.js +61 -0
- package/dist/replace-file-mutation.d.ts +26 -0
- package/dist/replace-file-mutation.js +47 -0
- package/dist/replace-file-temp-owner.d.ts +2 -2
- package/dist/replace-file-temp-owner.js +16 -4
- package/dist/replace-file-types.d.ts +55 -0
- package/dist/replace-file-types.js +1 -0
- package/dist/replace-file.d.ts +3 -55
- package/dist/replace-file.js +29 -10
- package/dist/retained-file-types.d.ts +61 -0
- package/dist/retained-file-types.js +1 -0
- package/dist/retained-file.d.ts +3 -0
- package/dist/retained-file.js +121 -0
- package/dist/root-directory-entry.d.ts +9 -0
- package/dist/root-directory-entry.js +28 -0
- package/dist/root-directory-list.d.ts +7 -1
- package/dist/root-directory-list.js +48 -23
- package/dist/root-handle-context.d.ts +4 -0
- package/dist/root-handle-context.js +12 -0
- package/dist/root-impl.d.ts +3 -3
- package/dist/root-impl.js +8 -5
- package/dist/root-observed-path.d.ts +0 -1
- package/dist/root-observed-path.js +0 -3
- package/dist/root-path-observation.d.ts +4 -11
- package/dist/root-path.js +7 -10
- package/dist/root-walk.d.ts +19 -12
- package/dist/root-walk.js +49 -18
- package/dist/root-write-admission.js +0 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.js +8 -2
- package/dist/secure-file.js +3 -2
- package/dist/sidecar-lock.js +5 -3
- package/dist/staged-symlink-types.d.ts +49 -0
- package/dist/staged-symlink-types.js +1 -0
- package/dist/symlink-parents.js +58 -7
- package/dist/temp-target.js +5 -2
- package/dist/temp-workspace-admission.js +22 -21
- package/dist/temp-workspace-child-admission.d.ts +1 -1
- package/dist/temp-workspace-child-admission.js +14 -9
- package/dist/temp-workspace-owner.js +4 -9
- package/dist/temp-workspace-ownership.d.ts +8 -0
- package/dist/temp-workspace-ownership.js +52 -0
- package/dist/test-hooks.d.ts +3 -0
- package/dist/text-atomic.d.ts +2 -1
- package/dist/text-atomic.js +2 -0
- package/dist/trash.js +27 -1
- package/dist/walk.d.ts +2 -5
- package/dist/watch-alias.d.ts +6 -0
- package/dist/watch-alias.js +80 -0
- package/dist/watch-hints.d.ts +8 -0
- package/dist/watch-hints.js +77 -0
- package/dist/watch-native.d.ts +32 -0
- package/dist/watch-native.js +56 -0
- package/dist/watch-scan.d.ts +24 -0
- package/dist/watch-scan.js +269 -0
- package/dist/watch-types.d.ts +58 -0
- package/dist/watch-types.js +1 -0
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +502 -0
- package/dist/windows-owner.d.ts +0 -1
- package/dist/windows-owner.js +0 -1
- package/dist/windows-security-bridge.cs +6 -4
- package/dist/windows-security-bridge.ps1 +78 -3
- package/dist/windows-security-command.d.ts +8 -0
- package/dist/windows-security-command.js +66 -12
- package/dist/windows-security-facts.d.ts +3 -0
- package/dist/windows-security-facts.js +4 -0
- package/docs/advanced.md +4 -2
- package/docs/archive.md +8 -0
- package/docs/atomic.md +72 -3
- package/docs/contributing.md +35 -0
- package/docs/durability.md +7 -0
- package/docs/index.md +1 -0
- package/docs/install.md +28 -0
- package/docs/native-helper.md +14 -4
- package/docs/native.md +45 -1
- package/docs/permissions.md +66 -0
- package/docs/public-api.md +7 -1
- package/docs/retained-file.md +113 -0
- package/docs/root.md +6 -1
- package/docs/security-model.md +4 -1
- package/docs/sidecar-lock.md +2 -0
- package/docs/staged-symlink.md +123 -0
- package/docs/store.md +3 -1
- package/docs/temp.md +24 -4
- package/docs/testing.md +88 -0
- package/docs/types.md +6 -0
- package/docs/walk.md +22 -1
- package/docs/watch.md +184 -0
- package/docs/writing.md +10 -0
- package/package.json +13 -9
|
@@ -2,6 +2,7 @@ import type { BigIntStats, Stats } from "node:fs";
|
|
|
2
2
|
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import { type RootContext } from "./root-context.js";
|
|
4
4
|
import type { RootPathDirectoryObservationGuard, RootPathObservationReceipt, RootPathParentObservationReceipt } from "./root-path.js";
|
|
5
|
+
import { type ExactStatIdentity } from "./stat-observation.js";
|
|
5
6
|
import type { DirEntry, PathStat } from "./types.js";
|
|
6
7
|
export declare function pathStatFromStats(stat: Stats | BigIntStats): PathStat;
|
|
7
8
|
export type RootDirectoryObservationGuard = AsyncDirectoryGuard<BigIntStats>;
|
|
@@ -21,6 +22,7 @@ export type RootDirectoryListing = {
|
|
|
21
22
|
next(): Promise<{
|
|
22
23
|
kind: "entry";
|
|
23
24
|
entry: DirEntry;
|
|
25
|
+
identity?: ExactStatIdentity;
|
|
24
26
|
} | {
|
|
25
27
|
kind: "limit";
|
|
26
28
|
name: string;
|
|
@@ -33,6 +35,10 @@ export type RootDirectoryListingOptions = {
|
|
|
33
35
|
snapshot: boolean;
|
|
34
36
|
maxNames?: number;
|
|
35
37
|
metadataBatchSize?: number;
|
|
38
|
+
/** Internal exact metadata lane, supported by streaming filesystem order. */
|
|
39
|
+
exactIdentity?: boolean;
|
|
40
|
+
/** Internal owner receives cleanup failures, including acquisition rollback. */
|
|
41
|
+
onCleanupFailure?: (error: unknown) => void;
|
|
36
42
|
admitEntry(): boolean;
|
|
37
43
|
};
|
|
38
|
-
export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
|
|
44
|
+
export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
|
|
@@ -11,6 +11,7 @@ import { errorCauseOptions, rootPathChangedError } from "./root-errors.js";
|
|
|
11
11
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
12
12
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
13
13
|
import { realpathSync } from "./realpath.js";
|
|
14
|
+
import { inspectStatObservationSync } from "./stat-observation.js";
|
|
14
15
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
15
16
|
const METADATA_BATCH_SIZE = 32;
|
|
16
17
|
export function pathStatFromStats(stat) {
|
|
@@ -173,14 +174,7 @@ function normalizeInitialDirectoryError(error) {
|
|
|
173
174
|
export async function listDirectoryPath(root, directory, withFileTypes, receipt) {
|
|
174
175
|
let guard;
|
|
175
176
|
if (receipt) {
|
|
176
|
-
|
|
177
|
-
receipt.directoryGuard.dir !== directory ||
|
|
178
|
-
receipt.target !== receipt.directoryGuard ||
|
|
179
|
-
(!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
|
|
180
|
-
!receipt.directoryGuard.stat.isDirectory())) {
|
|
181
|
-
throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
|
|
182
|
-
}
|
|
183
|
-
guard = receipt.directoryGuard;
|
|
177
|
+
guard = directoryGuardFromReceipt(directory, receipt);
|
|
184
178
|
}
|
|
185
179
|
else {
|
|
186
180
|
try {
|
|
@@ -192,6 +186,16 @@ export async function listDirectoryPath(root, directory, withFileTypes, receipt)
|
|
|
192
186
|
}
|
|
193
187
|
return await listGuardedDirectoryPath(root, guard, withFileTypes, receipt);
|
|
194
188
|
}
|
|
189
|
+
function directoryGuardFromReceipt(directory, receipt) {
|
|
190
|
+
if (receipt.kind !== "directory" || receipt.targetPath !== directory ||
|
|
191
|
+
receipt.directoryGuard.dir !== directory ||
|
|
192
|
+
receipt.target !== receipt.directoryGuard ||
|
|
193
|
+
(!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
|
|
194
|
+
!receipt.directoryGuard.stat.isDirectory())) {
|
|
195
|
+
throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
|
|
196
|
+
}
|
|
197
|
+
return receipt.directoryGuard;
|
|
198
|
+
}
|
|
195
199
|
async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
|
|
196
200
|
let entries;
|
|
197
201
|
try {
|
|
@@ -221,19 +225,30 @@ async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
|
|
|
221
225
|
await assertRootDirectoryObservationGuard(root, guard);
|
|
222
226
|
return entries;
|
|
223
227
|
}
|
|
224
|
-
export async function openRootDirectoryListing(root, directory, options) {
|
|
225
|
-
|
|
226
|
-
throw
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
export async function openRootDirectoryListing(root, directory, options, receipt) {
|
|
229
|
+
if (options.exactIdentity && options.order !== "filesystem")
|
|
230
|
+
throw new TypeError("exact entry identities require filesystem order");
|
|
231
|
+
let guard;
|
|
232
|
+
if (receipt) {
|
|
233
|
+
guard = directoryGuardFromReceipt(directory, receipt);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const admitted = await createRootDirectoryObservationGuard(root, directory).catch((error) => {
|
|
237
|
+
throw normalizeDirectoryError(error);
|
|
238
|
+
});
|
|
239
|
+
// Retain exact admission identities; metadata rechecks can use numeric Stats
|
|
240
|
+
// only when every identity component is losslessly representable.
|
|
241
|
+
guard = extendDirectoryObservationGuard({
|
|
242
|
+
stat: admitted.stat,
|
|
243
|
+
identity: { dev: admitted.stat.dev, ino: admitted.stat.ino },
|
|
244
|
+
}, admitted.dir, admitted.realPath);
|
|
245
|
+
}
|
|
234
246
|
const assertCurrent = async () => {
|
|
235
247
|
options.signal?.throwIfAborted();
|
|
236
|
-
|
|
248
|
+
if (receipt)
|
|
249
|
+
assertRootPathObservationReceiptCurrent(root, receipt);
|
|
250
|
+
else
|
|
251
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
237
252
|
options.signal?.throwIfAborted();
|
|
238
253
|
};
|
|
239
254
|
let handle;
|
|
@@ -248,7 +263,13 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
248
263
|
const close = async () => {
|
|
249
264
|
const owned = handle;
|
|
250
265
|
handle = undefined;
|
|
251
|
-
|
|
266
|
+
try {
|
|
267
|
+
await owned?.close();
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
options.onCleanupFailure?.(error);
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
252
273
|
};
|
|
253
274
|
try {
|
|
254
275
|
await assertCurrent();
|
|
@@ -257,7 +278,7 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
257
278
|
handle = await fs.opendir(guard.realPath, { bufferSize: 1 });
|
|
258
279
|
}
|
|
259
280
|
else if (options.snapshot) {
|
|
260
|
-
snapshot = await listGuardedDirectoryPath(root, guard, true);
|
|
281
|
+
snapshot = await listGuardedDirectoryPath(root, guard, true, receipt);
|
|
261
282
|
}
|
|
262
283
|
else if (options.maxNames !== undefined) {
|
|
263
284
|
names = [];
|
|
@@ -369,9 +390,13 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
369
390
|
if (!options.admitEntry())
|
|
370
391
|
return { kind: "limit", name };
|
|
371
392
|
// The stream's post-read fence is also the pre-stat fence in this owned operation.
|
|
372
|
-
const
|
|
393
|
+
const pathname = path.join(guard.realPath, name);
|
|
394
|
+
const observed = options.exactIdentity ? inspectStatObservationSync(bigint => bigint
|
|
395
|
+
? fsSync.lstatSync(pathname, { bigint: true }) : fsSync.lstatSync(pathname)) : undefined;
|
|
396
|
+
const stat = observed?.stat ?? fsSync.lstatSync(pathname);
|
|
373
397
|
await assertCurrent();
|
|
374
|
-
|
|
398
|
+
const entry = { name, ...pathStatFromStats(stat) };
|
|
399
|
+
return observed ? { kind: "entry", entry, identity: observed.identity } : { kind: "entry", entry };
|
|
375
400
|
}
|
|
376
401
|
catch (error) {
|
|
377
402
|
throw normalizeDirectoryError(error);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RootContext } from "./root-context.js";
|
|
2
|
+
/** Internal registration; never reconstruct authority from public pathname fields. */
|
|
3
|
+
export declare function registerRootHandleContext(handle: object, context: RootContext): void;
|
|
4
|
+
export declare function rootHandleContext(handle: object): RootContext;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
const contexts = new WeakMap();
|
|
3
|
+
/** Internal registration; never reconstruct authority from public pathname fields. */
|
|
4
|
+
export function registerRootHandleContext(handle, context) {
|
|
5
|
+
contexts.set(handle, context);
|
|
6
|
+
}
|
|
7
|
+
export function rootHandleContext(handle) {
|
|
8
|
+
const context = contexts.get(handle);
|
|
9
|
+
if (!context)
|
|
10
|
+
throw new FsSafeError("invalid-path", "watch requires a genuine fs-safe Root");
|
|
11
|
+
return context;
|
|
12
|
+
}
|
package/dist/root-impl.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type ReadResult } from "./read-opened-file.js";
|
|
|
5
5
|
import { type RootEntriesOptions } from "./root-entries.js";
|
|
6
6
|
import { type RootContext } from "./root-context.js";
|
|
7
7
|
import type { DirEntry, PathStat } from "./types.js";
|
|
8
|
-
import { type RootWalkEntry, type RootWalkOptions } from "./root-walk.js";
|
|
8
|
+
import { type RootWalkEntry, type RootWalkOptions, type RootWalkSymlinkPolicy } from "./root-walk.js";
|
|
9
9
|
import { type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions } from "./root-options.js";
|
|
10
10
|
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
11
11
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
@@ -54,7 +54,7 @@ export interface Root {
|
|
|
54
54
|
}): Promise<DirEntry[]>;
|
|
55
55
|
entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
|
|
56
56
|
move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
|
|
57
|
-
walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry
|
|
57
|
+
walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
|
|
58
58
|
}
|
|
59
59
|
export declare class RootHandle implements Root {
|
|
60
60
|
private readonly context;
|
|
@@ -97,7 +97,7 @@ export declare class RootHandle implements Root {
|
|
|
97
97
|
}): Promise<DirEntry[]>;
|
|
98
98
|
move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
|
|
99
99
|
entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
|
|
100
|
-
walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry
|
|
100
|
+
walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
|
|
101
101
|
}
|
|
102
102
|
export declare function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
|
|
103
103
|
export declare function rootFromDirectoryGuard(guard: {
|
package/dist/root-impl.js
CHANGED
|
@@ -51,6 +51,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { registerRootHandleContext } from "./root-handle-context.js";
|
|
54
55
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
55
56
|
import fs from "node:fs/promises";
|
|
56
57
|
import path from "node:path";
|
|
@@ -85,7 +86,7 @@ import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-li
|
|
|
85
86
|
import { statResolvedPathInRoot } from "./root-path-stat.js";
|
|
86
87
|
import { entriesInRoot } from "./root-entries.js";
|
|
87
88
|
import { assertMoveMutationAllowed } from "./root-move-preflight.js";
|
|
88
|
-
import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
89
|
+
import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, createRootObservationGuard, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
89
90
|
import { errorCauseOptions, fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
|
|
90
91
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
91
92
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
@@ -199,6 +200,7 @@ export class RootHandle {
|
|
|
199
200
|
this.rootWithSep = context.rootWithSep;
|
|
200
201
|
this.defaults = defaults;
|
|
201
202
|
registerFileLockSyncRootAdapter(this, context, defaults);
|
|
203
|
+
registerRootHandleContext(this, context);
|
|
202
204
|
}
|
|
203
205
|
mutationOptions(options) {
|
|
204
206
|
return {
|
|
@@ -409,9 +411,13 @@ export class RootHandle {
|
|
|
409
411
|
assertValidRootRelativePath(relativePath);
|
|
410
412
|
return walkRoot({
|
|
411
413
|
rootReal: this.context.rootReal,
|
|
414
|
+
observeRoot: () => createRootObservationGuard(this.context),
|
|
412
415
|
stat: relative => this.stat(relative),
|
|
413
|
-
list: async (relative, listingOptions) => {
|
|
416
|
+
list: async (relative, listingOptions, receipt) => {
|
|
414
417
|
validatePinnedRelativePath(relative);
|
|
418
|
+
if (receipt) {
|
|
419
|
+
return await openRootDirectoryListing(this.context, receipt.targetPath, listingOptions, receipt);
|
|
420
|
+
}
|
|
415
421
|
const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
|
|
416
422
|
return await openRootDirectoryListing(this.context, resolved.resolved, listingOptions);
|
|
417
423
|
},
|
|
@@ -798,7 +804,6 @@ async function mkdirPathInRoot(root, params) {
|
|
|
798
804
|
const prepared = policy && resolved.relativePosix !== ""
|
|
799
805
|
? await preparePinnedWriteMutationAdmission({
|
|
800
806
|
rootReal: resolved.rootReal,
|
|
801
|
-
rootWithSep: ensureTrailingSep(resolved.rootReal),
|
|
802
807
|
rootIdentity: root.rootIdentity,
|
|
803
808
|
resolvedTargetPath: resolved.resolved,
|
|
804
809
|
originalPath: params.relativePath,
|
|
@@ -1045,10 +1050,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1045
1050
|
throw err;
|
|
1046
1051
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
1047
1052
|
}
|
|
1048
|
-
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
1049
1053
|
return {
|
|
1050
1054
|
rootReal: resolved.rootCanonicalPath,
|
|
1051
|
-
rootWithSep,
|
|
1052
1055
|
canonicalPath: resolved.canonicalPath,
|
|
1053
1056
|
};
|
|
1054
1057
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type RootPathObservationKind, type RootPathObservationReceipt } from "./root-path.js";
|
|
2
2
|
import { type RootContext } from "./root-context.js";
|
|
3
3
|
export type PinnedObservedPath = {
|
|
4
|
-
rootReal: string;
|
|
5
4
|
resolved: string;
|
|
6
5
|
receipt?: RootPathObservationReceipt;
|
|
7
6
|
};
|
|
@@ -63,7 +63,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
63
63
|
// A receipt is emitted only for the straight traversal whose lexical and
|
|
64
64
|
// canonical cursors stayed identical and inside the checked boundary.
|
|
65
65
|
return {
|
|
66
|
-
rootReal: resolved.rootCanonicalPath,
|
|
67
66
|
resolved: resolved.canonicalPath,
|
|
68
67
|
receipt: observed.receipt,
|
|
69
68
|
};
|
|
@@ -71,7 +70,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
71
70
|
const relativeResolved = path.relative(resolved.rootCanonicalPath, resolved.canonicalPath);
|
|
72
71
|
if (relativeResolved === "" || relativeResolved === ".") {
|
|
73
72
|
return {
|
|
74
|
-
rootReal: resolved.rootCanonicalPath,
|
|
75
73
|
resolved: resolved.canonicalPath,
|
|
76
74
|
};
|
|
77
75
|
}
|
|
@@ -87,7 +85,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
87
85
|
if (!admittedCanonicalPath)
|
|
88
86
|
throw outsideWorkspaceError();
|
|
89
87
|
return {
|
|
90
|
-
rootReal: resolved.rootCanonicalPath,
|
|
91
88
|
resolved: admittedCanonicalPath.path,
|
|
92
89
|
};
|
|
93
90
|
}
|
|
@@ -7,14 +7,11 @@ export type RootPathObservationKind = "stat" | "directory";
|
|
|
7
7
|
export type RootPathDirectoryObservationGuard = DirectoryObservationGuard | NativeDirectoryObservationGuard;
|
|
8
8
|
export type RootPathTargetObservation = StatObservationReceipt | NativeDirectoryObservationGuard;
|
|
9
9
|
/** An exact receipt owned by one stat/list operation. Never cache it. */
|
|
10
|
-
export type RootPathObservationReceipt = {
|
|
11
|
-
kind: RootPathObservationKind;
|
|
12
|
-
rootGuard: DirectoryObservationGuard;
|
|
10
|
+
export type RootPathObservationReceipt = Pick<RootPathObservationRequest & {
|
|
13
11
|
directoryGuard: RootPathDirectoryObservationGuard;
|
|
14
|
-
directoryObserver?: NativeDirectoryObservationBackend;
|
|
15
12
|
targetPath: string;
|
|
16
13
|
target: RootPathTargetObservation;
|
|
17
|
-
}
|
|
14
|
+
}, "kind" | "rootGuard" | "directoryGuard" | "directoryObserver" | "targetPath" | "target">;
|
|
18
15
|
/** A failed initial target lookup must not discard its already admitted parent. */
|
|
19
16
|
export type RootPathParentObservationReceipt = Omit<RootPathObservationReceipt, "kind" | "target"> & {
|
|
20
17
|
kind: "stat-parent";
|
|
@@ -24,16 +21,12 @@ export type RootPathObservationRequest = {
|
|
|
24
21
|
rootGuard: DirectoryObservationGuard;
|
|
25
22
|
directoryObserver?: NativeDirectoryObservationBackend;
|
|
26
23
|
};
|
|
27
|
-
export type RootPathTraversalObservation = {
|
|
24
|
+
export type RootPathTraversalObservation = Omit<Partial<RootPathObservationReceipt> & {
|
|
28
25
|
enabled: boolean;
|
|
29
26
|
request: RootPathObservationRequest;
|
|
30
27
|
targetIndex: number;
|
|
31
28
|
directoryIndex: number;
|
|
32
|
-
|
|
33
|
-
directoryObserver?: NativeDirectoryObservationBackend;
|
|
34
|
-
targetPath?: string;
|
|
35
|
-
target?: RootPathTargetObservation;
|
|
36
|
-
};
|
|
29
|
+
}, "kind" | "rootGuard">;
|
|
37
30
|
export type RootPathObservedTraversalEntry = StatObservationReceipt | {
|
|
38
31
|
stat: fs.Stats | BigIntStats;
|
|
39
32
|
identity?: undefined;
|
package/dist/root-path.js
CHANGED
|
@@ -214,8 +214,8 @@ function finalizeLexicalResolution(context, kind) {
|
|
|
214
214
|
rootPath: context.rootPath,
|
|
215
215
|
rootCanonicalPath: context.rootCanonicalPath,
|
|
216
216
|
relativePath: relativeInsideRoot(context.rootCanonicalPath, context.state.canonicalCursor),
|
|
217
|
-
exists: kind
|
|
218
|
-
kind
|
|
217
|
+
exists: kind !== "missing",
|
|
218
|
+
kind,
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
function applyResolvedSymlinkHop(context, linkCanonical) {
|
|
@@ -374,12 +374,9 @@ function traverseRootPath(params, { mode = "native", observation: observationOut
|
|
|
374
374
|
const completeObservation = observation?.enabled === true && observation.directoryGuard !== undefined &&
|
|
375
375
|
observation.targetPath === state.canonicalCursor && observation.target !== undefined;
|
|
376
376
|
const kind = completeObservation
|
|
377
|
-
?
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
? "directory"
|
|
381
|
-
: toResolvedKind(observation.target.stat),
|
|
382
|
-
}
|
|
377
|
+
? isNativeDirectoryObservationGuard(observation.target)
|
|
378
|
+
? "directory"
|
|
379
|
+
: toResolvedKind(observation.target.stat)
|
|
383
380
|
: getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
|
|
384
381
|
if (completeObservation && observationOutput) {
|
|
385
382
|
observationOutput.receipt = {
|
|
@@ -399,11 +396,11 @@ function getPathKindSync(absolutePath, preserveFinalSymlink) {
|
|
|
399
396
|
const stat = preserveFinalSymlink
|
|
400
397
|
? fs.lstatSync(operationPath)
|
|
401
398
|
: fs.statSync(operationPath);
|
|
402
|
-
return
|
|
399
|
+
return toResolvedKind(stat);
|
|
403
400
|
}
|
|
404
401
|
catch (error) {
|
|
405
402
|
if (isNotFoundPathError(error)) {
|
|
406
|
-
return
|
|
403
|
+
return "missing";
|
|
407
404
|
}
|
|
408
405
|
throw error;
|
|
409
406
|
}
|
package/dist/root-walk.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import type { DirectoryObservationGuard } from "./directory-guard.js";
|
|
2
|
+
import { type RootPathObservationReceipt } from "./root-path.js";
|
|
1
3
|
import type { RootDirectoryListing, RootDirectoryListingOptions } from "./root-directory-list.js";
|
|
2
4
|
import type { PathStat } from "./types.js";
|
|
3
|
-
export type RootWalkSymlinkPolicy = "skip" | "follow-within-root";
|
|
5
|
+
export type RootWalkSymlinkPolicy = "skip" | "follow-within-root" | "include";
|
|
6
|
+
type LegacyRootWalkSymlinkPolicy = Exclude<RootWalkSymlinkPolicy, "include">;
|
|
4
7
|
export type RootWalkLimitBehavior = "truncate" | "throw";
|
|
5
8
|
export type RootWalkDirectoryErrorBehavior = "throw" | "skip-and-report";
|
|
6
9
|
export type RootWalkEntryFilterResult = "include" | "skip" | "skip-subtree";
|
|
7
|
-
export type RootWalkDataEntryKind = "file" | "directory" | "other";
|
|
8
|
-
export type RootWalkEntryKind = RootWalkDataEntryKind | "directory-error" | "truncated";
|
|
9
|
-
|
|
10
|
+
export type RootWalkDataEntryKind<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = "file" | "directory" | "other" | ("include" extends Policy ? "symlink" : never);
|
|
11
|
+
export type RootWalkEntryKind<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = RootWalkDataEntryKind<Policy> | "directory-error" | "truncated";
|
|
12
|
+
type WalkEntryOfKind<Kind extends RootWalkDataEntryKind<RootWalkSymlinkPolicy>> = {
|
|
10
13
|
relativePath: string;
|
|
11
|
-
kind:
|
|
14
|
+
kind: Kind;
|
|
12
15
|
size: number;
|
|
13
16
|
};
|
|
14
|
-
export type
|
|
17
|
+
export type RootWalkDataEntry<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = WalkEntryOfKind<RootWalkDataEntryKind> | ("include" extends Policy ? WalkEntryOfKind<"symlink"> : never);
|
|
18
|
+
export type RootWalkEntry<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = RootWalkDataEntry<Policy> | {
|
|
15
19
|
relativePath: string;
|
|
16
20
|
kind: "truncated";
|
|
17
21
|
size: 0;
|
|
@@ -21,21 +25,24 @@ export type RootWalkEntry = RootWalkDataEntry | {
|
|
|
21
25
|
size: 0;
|
|
22
26
|
error: unknown;
|
|
23
27
|
};
|
|
24
|
-
export type RootWalkEntryFilter = (entry: RootWalkDataEntry) => RootWalkEntryFilterResult | Promise<RootWalkEntryFilterResult>;
|
|
25
|
-
export type RootWalkOptions = {
|
|
28
|
+
export type RootWalkEntryFilter<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = (entry: RootWalkDataEntry<Policy>) => RootWalkEntryFilterResult | Promise<RootWalkEntryFilterResult>;
|
|
29
|
+
export type RootWalkOptions<Policy extends RootWalkSymlinkPolicy = LegacyRootWalkSymlinkPolicy> = {
|
|
26
30
|
maxDepth?: number;
|
|
27
31
|
maxEntries?: number;
|
|
28
32
|
order?: "sorted" | "filesystem";
|
|
29
|
-
symlinkPolicy:
|
|
33
|
+
symlinkPolicy: Policy;
|
|
30
34
|
signal?: AbortSignal;
|
|
31
35
|
limitBehavior?: RootWalkLimitBehavior;
|
|
32
|
-
entryFilter?: RootWalkEntryFilter
|
|
36
|
+
entryFilter?: RootWalkEntryFilter<Policy>;
|
|
33
37
|
onDirectoryError?: RootWalkDirectoryErrorBehavior;
|
|
34
38
|
};
|
|
39
|
+
type AnyRootWalkOptions = RootWalkOptions | RootWalkOptions<"include"> | RootWalkOptions<RootWalkSymlinkPolicy>;
|
|
40
|
+
type AnyRootWalkEntry = RootWalkEntry<RootWalkSymlinkPolicy>;
|
|
35
41
|
type RootWalkCapability = {
|
|
36
42
|
rootReal: string;
|
|
43
|
+
observeRoot(): Promise<DirectoryObservationGuard | undefined>;
|
|
37
44
|
stat(relativePath: string): Promise<PathStat>;
|
|
38
|
-
list(relativePath: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
|
|
45
|
+
list(relativePath: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
|
|
39
46
|
};
|
|
40
|
-
export declare function walkRoot(root: RootWalkCapability, relativePath: string, options:
|
|
47
|
+
export declare function walkRoot(root: RootWalkCapability, relativePath: string, options: AnyRootWalkOptions): AsyncGenerator<AnyRootWalkEntry>;
|
|
41
48
|
export {};
|
package/dist/root-walk.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
3
|
import { expandRelativePathWithHome } from "./root-context.js";
|
|
4
|
-
import { resolveRootPath, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
4
|
+
import { resolveRootPath, resolveRootPathWithObservation, ROOT_PATH_ALIAS_POLICIES, RootPathObservationError, } from "./root-path.js";
|
|
5
5
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
6
|
+
function filterEntry(options, entry) {
|
|
7
|
+
if (entry.kind === "symlink") {
|
|
8
|
+
return options.symlinkPolicy === "include" ? options.entryFilter?.(entry) ?? "include" : "skip";
|
|
9
|
+
}
|
|
10
|
+
return options.entryFilter?.(entry) ?? "include";
|
|
11
|
+
}
|
|
6
12
|
function validateBudget(name, value) {
|
|
7
13
|
if (value === undefined)
|
|
8
14
|
return Number.POSITIVE_INFINITY;
|
|
@@ -24,7 +30,7 @@ function limitEntry(relativePath) {
|
|
|
24
30
|
return { relativePath, kind: "truncated", size: 0 };
|
|
25
31
|
}
|
|
26
32
|
export async function* walkRoot(root, relativePath, options) {
|
|
27
|
-
if (!["skip", "follow-within-root"].includes(options.symlinkPolicy)) {
|
|
33
|
+
if (!["skip", "follow-within-root", "include"].includes(options.symlinkPolicy)) {
|
|
28
34
|
throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
|
|
29
35
|
}
|
|
30
36
|
if (options.order !== undefined && !["sorted", "filesystem"].includes(options.order)) {
|
|
@@ -62,24 +68,50 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
62
68
|
throw error;
|
|
63
69
|
return { relativePath: directory, kind: "directory-error", size: 0, error };
|
|
64
70
|
};
|
|
65
|
-
async function* visit(directory, depth) {
|
|
71
|
+
async function* visit(directory, depth, admittedChildPath) {
|
|
66
72
|
options.signal?.throwIfAborted();
|
|
67
73
|
let listing;
|
|
74
|
+
let canonicalDirectory;
|
|
68
75
|
try {
|
|
69
76
|
const expandedDirectory = depth === 0 ? await expandRelativePathWithHome(directory) : directory;
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
77
|
+
const noFollowChildSymlinks = depth > 0 && options.symlinkPolicy !== "follow-within-root";
|
|
78
|
+
const includeChild = depth > 0 && options.symlinkPolicy === "include";
|
|
79
|
+
const resolution = {
|
|
80
|
+
absolutePath: admittedChildPath ?? path.resolve(root.rootReal, expandedDirectory),
|
|
73
81
|
rootPath: root.rootReal,
|
|
74
82
|
rootCanonicalPath: root.rootReal,
|
|
75
83
|
boundaryLabel: "root walk",
|
|
76
|
-
policy:
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
policy: noFollowChildSymlinks ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
85
|
+
};
|
|
86
|
+
let receipt;
|
|
87
|
+
let resolvedDirectory;
|
|
88
|
+
if (includeChild) {
|
|
89
|
+
const rootGuard = await root.observeRoot();
|
|
90
|
+
if (!rootGuard) {
|
|
91
|
+
throw new FsSafeError("helper-unavailable", "root walk requires an exact directory observation");
|
|
92
|
+
}
|
|
93
|
+
const observed = await resolveRootPathWithObservation({
|
|
94
|
+
...resolution,
|
|
95
|
+
rootIdentity: rootGuard.identity,
|
|
96
|
+
}, { kind: "directory", rootGuard });
|
|
97
|
+
resolvedDirectory = observed.resolved;
|
|
98
|
+
receipt = observed.receipt;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
resolvedDirectory = await resolveRootPath(resolution);
|
|
102
|
+
}
|
|
103
|
+
if (noFollowChildSymlinks && resolvedDirectory.kind === "symlink") {
|
|
104
|
+
if (options.symlinkPolicy === "skip")
|
|
105
|
+
return;
|
|
106
|
+
throw new FsSafeError("path-mismatch", `root walk directory became a symlink: ${directory}`);
|
|
107
|
+
}
|
|
80
108
|
if (!resolvedDirectory.exists || resolvedDirectory.kind !== "directory") {
|
|
81
109
|
throw new FsSafeError("not-file", `root walk path is not a directory: ${directory || "."}`);
|
|
82
110
|
}
|
|
111
|
+
if (includeChild && (!receipt || receipt.directoryGuard.realPath !== resolvedDirectory.canonicalPath)) {
|
|
112
|
+
throw new FsSafeError("path-mismatch", "root walk directory observation was not retained");
|
|
113
|
+
}
|
|
114
|
+
canonicalDirectory = resolvedDirectory.canonicalPath;
|
|
83
115
|
if (visitedDirectories.has(resolvedDirectory.canonicalPath)) {
|
|
84
116
|
return;
|
|
85
117
|
}
|
|
@@ -97,10 +129,10 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
97
129
|
signal: options.signal,
|
|
98
130
|
snapshot: maxEntries === Number.POSITIVE_INFINITY,
|
|
99
131
|
admitEntry,
|
|
100
|
-
});
|
|
132
|
+
}, receipt);
|
|
101
133
|
}
|
|
102
134
|
catch (error) {
|
|
103
|
-
yield onDirectoryError(directory, error);
|
|
135
|
+
yield onDirectoryError(directory, error instanceof RootPathObservationError ? error.error : error);
|
|
104
136
|
return;
|
|
105
137
|
}
|
|
106
138
|
// A thrown undefined still needs to be retained if closing also fails.
|
|
@@ -131,10 +163,9 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
131
163
|
const entry = next.entry;
|
|
132
164
|
let kind = entryKind(entry);
|
|
133
165
|
let size = entry.size;
|
|
134
|
-
if (kind === "symlink")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
166
|
+
if (kind === "symlink" && options.symlinkPolicy === "skip")
|
|
167
|
+
continue;
|
|
168
|
+
if (kind === "symlink" && options.symlinkPolicy === "follow-within-root") {
|
|
138
169
|
const resolved = await resolveRootPath({
|
|
139
170
|
absolutePath: path.resolve(root.rootReal, child),
|
|
140
171
|
rootPath: root.rootReal,
|
|
@@ -149,7 +180,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
149
180
|
size = target.size;
|
|
150
181
|
}
|
|
151
182
|
const walkEntry = { relativePath: child, kind, size };
|
|
152
|
-
let filterResult = options
|
|
183
|
+
let filterResult = filterEntry(options, walkEntry);
|
|
153
184
|
if (typeof filterResult !== "string") {
|
|
154
185
|
filterResult = (await filterResult) ?? "include";
|
|
155
186
|
options.signal?.throwIfAborted();
|
|
@@ -177,7 +208,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
177
208
|
yield onLimit(child);
|
|
178
209
|
return;
|
|
179
210
|
}
|
|
180
|
-
yield* visit(child, depth + 1);
|
|
211
|
+
yield* visit(child, depth + 1, options.symlinkPolicy === "include" ? path.join(canonicalDirectory, name) : undefined);
|
|
181
212
|
if (truncated)
|
|
182
213
|
return;
|
|
183
214
|
}
|
|
@@ -201,7 +201,6 @@ export async function resolveGuardedWriteTargetInRoot(root, params) {
|
|
|
201
201
|
const relativeParent = path.relative(resolvedPath.rootReal, path.dirname(resolvedPath.resolved));
|
|
202
202
|
const prepared = await preparePinnedWriteMutationAdmission({
|
|
203
203
|
rootReal: resolvedPath.rootReal,
|
|
204
|
-
rootWithSep: resolvedPath.rootWithSep,
|
|
205
204
|
rootIdentity: root.rootIdentity,
|
|
206
205
|
resolvedTargetPath: resolvedPath.resolved,
|
|
207
206
|
originalPath: params.relativePath,
|
|
@@ -288,7 +287,6 @@ export async function resolvePinnedWriteTargetInRoot(root, relativePath, request
|
|
|
288
287
|
if (policy) {
|
|
289
288
|
({ relativeParentPath, mutationAdmission } = await preparePinnedWriteMutationAdmission({
|
|
290
289
|
rootReal,
|
|
291
|
-
rootWithSep,
|
|
292
290
|
rootIdentity: root.rootIdentity,
|
|
293
291
|
resolvedTargetPath: resolved,
|
|
294
292
|
originalPath: relativePath,
|
|
@@ -7,5 +7,6 @@ export declare function assertNoDriveRelativePathSegments(value: string, label:
|
|
|
7
7
|
export declare function trimHyphenEdges(value: string): string;
|
|
8
8
|
export declare function isSafePathSegment(segment: string, options?: SafePathSegmentOptions): boolean;
|
|
9
9
|
export declare function assertSafePathSegment(segment: string, options?: SafePathSegmentOptions): string;
|
|
10
|
+
export declare function normalizeSafePathSegment(value: string): string;
|
|
10
11
|
export declare function sanitizeSafePathSegment(value: string): string | undefined;
|
|
11
12
|
export declare function assertSafePathPrefix(prefix: string, options?: SafePathSegmentOptions): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
3
4
|
const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
@@ -34,6 +35,8 @@ export function isSafePathSegment(segment, options = {}) {
|
|
|
34
35
|
!segment.includes("/") &&
|
|
35
36
|
!segment.includes("\\") &&
|
|
36
37
|
!segment.includes("\0") &&
|
|
38
|
+
// Segments are portable identifiers: CON.json and CON.<pid>.tmp name devices on Windows.
|
|
39
|
+
!isWindowsReservedDeviceName(segment) &&
|
|
37
40
|
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
38
41
|
(options.allowDotPrefix === true
|
|
39
42
|
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
@@ -47,13 +50,16 @@ export function assertSafePathSegment(segment, options = {}) {
|
|
|
47
50
|
}
|
|
48
51
|
return segment;
|
|
49
52
|
}
|
|
50
|
-
export function
|
|
53
|
+
export function normalizeSafePathSegment(value) {
|
|
51
54
|
const sanitized = value
|
|
52
55
|
.trim()
|
|
53
56
|
.replace(/[\\/]+/g, "-")
|
|
54
57
|
.replace(/\0/g, "")
|
|
55
58
|
.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
56
|
-
|
|
59
|
+
return trimHyphenEdges(sanitized);
|
|
60
|
+
}
|
|
61
|
+
export function sanitizeSafePathSegment(value) {
|
|
62
|
+
const trimmed = normalizeSafePathSegment(value);
|
|
57
63
|
return isSafePathSegment(trimmed, { allowDotPrefix: true }) ? trimmed : undefined;
|
|
58
64
|
}
|
|
59
65
|
export function assertSafePathPrefix(prefix, options = {}) {
|
package/dist/secure-file.js
CHANGED
|
@@ -202,7 +202,8 @@ function inspectOpenedPermissions(stat, platform) {
|
|
|
202
202
|
groupReadable: isGroupReadable(bits),
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
-
async function assertSecurePermissions(options,
|
|
205
|
+
async function assertSecurePermissions(options, opened, fd) {
|
|
206
|
+
const { pathStat: stat, realPath, identity } = opened;
|
|
206
207
|
if (options.permissions?.allowInsecure) {
|
|
207
208
|
return undefined;
|
|
208
209
|
}
|
|
@@ -282,7 +283,7 @@ export async function readSecureFile(options) {
|
|
|
282
283
|
const opened = await openSecureHandle(options, maxBytes);
|
|
283
284
|
try {
|
|
284
285
|
assertTrustedDirs(options, opened.realPath);
|
|
285
|
-
const permissions = await assertSecurePermissions(options, opened
|
|
286
|
+
const permissions = await assertSecurePermissions(options, opened, opened.handle.fd);
|
|
286
287
|
const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
|
|
287
288
|
const finalIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
|
|
288
289
|
if (!finalIdentity.isFile()) {
|