@openclaw/fs-safe 0.5.0 → 0.5.2
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 +18 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +25 -5
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +6 -3
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +30 -12
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +7 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +7 -4
- package/dist/archive-zip-entry.d.ts +1 -0
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-integrity.d.ts +4 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +44 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +6 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +5 -2
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +30 -8
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +36 -128
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +7 -2
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-write.js +2 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +213 -168
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +28 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +19 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +66 -7
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +31 -7
- package/docs/file-store.md +10 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +17 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +37 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +10 -6
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/file-lock-sync.js
CHANGED
|
@@ -2,8 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
|
-
import { computeSidecarLockDelayMs,
|
|
5
|
+
import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, } from "./sidecar-lock-policy.js";
|
|
6
|
+
import { sleepSync } from "./timing.js";
|
|
6
7
|
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
8
|
+
const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
9
|
+
const SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
7
10
|
function getSyncHeldLocks() {
|
|
8
11
|
const globalWithState = globalThis;
|
|
9
12
|
if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
|
|
@@ -11,6 +14,36 @@ function getSyncHeldLocks() {
|
|
|
11
14
|
}
|
|
12
15
|
return globalWithState[SYNC_HELD_LOCKS_KEY];
|
|
13
16
|
}
|
|
17
|
+
function releaseAllSyncHeldLocks() {
|
|
18
|
+
const heldLocks = getSyncHeldLocks();
|
|
19
|
+
for (const [normalizedTargetPath, held] of heldLocks) {
|
|
20
|
+
if (held.timer) {
|
|
21
|
+
clearInterval(held.timer);
|
|
22
|
+
held.timer = undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
fs.closeSync(held.fd);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Best-effort process-exit cleanup.
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A surviving sidecar fails closed and can be reclaimed by policy.
|
|
35
|
+
}
|
|
36
|
+
heldLocks.delete(normalizedTargetPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ensureSyncExitCleanupRegistered() {
|
|
40
|
+
const globalWithCleanup = globalThis;
|
|
41
|
+
if (globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY])
|
|
42
|
+
return;
|
|
43
|
+
globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY] = true;
|
|
44
|
+
globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] = releaseAllSyncHeldLocks;
|
|
45
|
+
process.on("exit", releaseAllSyncHeldLocks);
|
|
46
|
+
}
|
|
14
47
|
function verifySyncHeldLock(held) {
|
|
15
48
|
const current = readSidecarLockSnapshotSync(held.lockPath, held.parsePayload);
|
|
16
49
|
return !!current && sidecarLockSnapshotMatches(current, held.snapshot);
|
|
@@ -71,8 +104,9 @@ function boundedLockPath(lockPath, lockRoot) {
|
|
|
71
104
|
return path.join(parentReal, path.basename(resolved));
|
|
72
105
|
}
|
|
73
106
|
function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
108
|
+
if (createdAtMs !== null)
|
|
109
|
+
return nowMs - createdAtMs > staleMs;
|
|
76
110
|
try {
|
|
77
111
|
return nowMs - fs.statSync(lockPath).mtimeMs > staleMs;
|
|
78
112
|
}
|
|
@@ -80,8 +114,16 @@ function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
|
|
|
80
114
|
return true;
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
117
|
+
function reclaimGuardExists(reclaimGuardPath) {
|
|
118
|
+
try {
|
|
119
|
+
fs.lstatSync(reclaimGuardPath);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code === "ENOENT")
|
|
124
|
+
return false;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
85
127
|
}
|
|
86
128
|
export function acquireFileLockSync(targetPath, options) {
|
|
87
129
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
@@ -99,7 +141,25 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
99
141
|
const retry = options.retry ?? {};
|
|
100
142
|
const startedAt = Date.now();
|
|
101
143
|
let attempt = 0;
|
|
144
|
+
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
145
|
+
const waitForRetry = () => {
|
|
146
|
+
const elapsed = Date.now() - startedAt;
|
|
147
|
+
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
148
|
+
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
149
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
150
|
+
code: "file_lock_timeout",
|
|
151
|
+
lockPath,
|
|
152
|
+
normalizedTargetPath,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
sleepSync(computeSidecarLockDelayMs(retry, attempt));
|
|
156
|
+
attempt += 1;
|
|
157
|
+
};
|
|
102
158
|
while (true) {
|
|
159
|
+
if (reclaimGuardExists(reclaimGuardPath)) {
|
|
160
|
+
waitForRetry();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
103
163
|
let fd;
|
|
104
164
|
try {
|
|
105
165
|
const payload = options.payload();
|
|
@@ -126,6 +186,7 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
126
186
|
snapshot,
|
|
127
187
|
};
|
|
128
188
|
heldLocks.set(normalizedTargetPath, createdHeld);
|
|
189
|
+
ensureSyncExitCleanupRegistered();
|
|
129
190
|
const returnedHandle = createSyncHeldLockHandle(createdHeld);
|
|
130
191
|
if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
|
|
131
192
|
createdHeld.timer = setInterval(() => {
|
|
@@ -151,20 +212,12 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
151
212
|
if (error.code !== "EEXIST")
|
|
152
213
|
throw error;
|
|
153
214
|
if (heldLocks.has(normalizedTargetPath)) {
|
|
154
|
-
|
|
155
|
-
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
156
|
-
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
157
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
158
|
-
code: "file_lock_timeout",
|
|
159
|
-
lockPath,
|
|
160
|
-
normalizedTargetPath,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
164
|
-
attempt += 1;
|
|
215
|
+
waitForRetry();
|
|
165
216
|
continue;
|
|
166
217
|
}
|
|
167
|
-
const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload
|
|
218
|
+
const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload, {
|
|
219
|
+
rejectNonFile: true,
|
|
220
|
+
});
|
|
168
221
|
if (!snapshot)
|
|
169
222
|
continue;
|
|
170
223
|
const nowMs = Date.now();
|
|
@@ -187,18 +240,28 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
187
240
|
raw: snapshot.raw,
|
|
188
241
|
payload: snapshot.payload,
|
|
189
242
|
})) {
|
|
190
|
-
|
|
243
|
+
let ownsReclaimGuard = false;
|
|
191
244
|
try {
|
|
192
|
-
fs.mkdirSync(
|
|
245
|
+
fs.mkdirSync(reclaimGuardPath);
|
|
246
|
+
ownsReclaimGuard = true;
|
|
193
247
|
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
194
248
|
continue;
|
|
195
249
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
catch (reclaimError) {
|
|
251
|
+
if (reclaimError.code !== "EEXIST") {
|
|
252
|
+
throw reclaimError;
|
|
199
253
|
}
|
|
200
|
-
|
|
201
|
-
|
|
254
|
+
waitForRetry();
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (ownsReclaimGuard) {
|
|
259
|
+
try {
|
|
260
|
+
fs.rmdirSync(reclaimGuardPath);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// A surviving reclaim guard fails closed.
|
|
264
|
+
}
|
|
202
265
|
}
|
|
203
266
|
}
|
|
204
267
|
}
|
|
@@ -208,17 +271,7 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
208
271
|
normalizedTargetPath,
|
|
209
272
|
});
|
|
210
273
|
}
|
|
211
|
-
|
|
212
|
-
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
213
|
-
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
214
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
215
|
-
code: "file_lock_timeout",
|
|
216
|
-
lockPath,
|
|
217
|
-
normalizedTargetPath,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
221
|
-
attempt += 1;
|
|
274
|
+
waitForRetry();
|
|
222
275
|
}
|
|
223
276
|
}
|
|
224
277
|
}
|
|
@@ -22,4 +22,10 @@ export declare function ensureParentSync(params: {
|
|
|
22
22
|
filePath: string;
|
|
23
23
|
mode: number;
|
|
24
24
|
}): SyncParentGuard;
|
|
25
|
+
export declare function ensureStoreDirectorySync(params: {
|
|
26
|
+
rootDir: string;
|
|
27
|
+
targetDir: string;
|
|
28
|
+
mode: number;
|
|
29
|
+
messagePrefix: "private store" | "store";
|
|
30
|
+
}): SyncParentGuard;
|
|
25
31
|
//# sourceMappingURL=file-store-boundary.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
|
|
@@ -5,6 +5,7 @@ import { Transform } from "node:stream";
|
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { assertSyncDirectoryGuard as assertDirectoryGuardSync, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
8
9
|
import { isPathInside, isPathRelativeEscape } from "./path.js";
|
|
9
10
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
10
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -128,8 +129,16 @@ function chmodDirectorySyncBestEffort(dir, mode) {
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
export function ensureParentSync(params) {
|
|
132
|
+
return ensureStoreDirectorySync({
|
|
133
|
+
rootDir: params.rootDir,
|
|
134
|
+
targetDir: path.dirname(path.resolve(params.filePath)),
|
|
135
|
+
mode: params.mode,
|
|
136
|
+
messagePrefix: "store",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function ensureStoreDirectorySync(params) {
|
|
131
140
|
const rootDir = path.resolve(params.rootDir);
|
|
132
|
-
const dir = path.
|
|
141
|
+
const dir = path.resolve(params.targetDir);
|
|
133
142
|
const relative = path.relative(rootDir, dir);
|
|
134
143
|
if (isPathRelativeEscape(relative)) {
|
|
135
144
|
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
@@ -137,7 +146,7 @@ export function ensureParentSync(params) {
|
|
|
137
146
|
syncFs.mkdirSync(rootDir, { recursive: true, mode: params.mode });
|
|
138
147
|
const rootStat = syncFs.lstatSync(rootDir);
|
|
139
148
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
140
|
-
throw new FsSafeError("not-file",
|
|
149
|
+
throw new FsSafeError("not-file", `${params.messagePrefix} root must be a directory: ${rootDir}`);
|
|
141
150
|
}
|
|
142
151
|
const rootReal = syncFs.realpathSync(rootDir);
|
|
143
152
|
chmodDirectorySyncBestEffort(rootDir, params.mode);
|
|
@@ -147,7 +156,7 @@ export function ensureParentSync(params) {
|
|
|
147
156
|
try {
|
|
148
157
|
const stat = syncFs.lstatSync(current);
|
|
149
158
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
150
|
-
throw new FsSafeError("not-file",
|
|
159
|
+
throw new FsSafeError("not-file", `${params.messagePrefix} directory component must be a directory: ${current}`);
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
catch (error) {
|
|
@@ -156,9 +165,15 @@ export function ensureParentSync(params) {
|
|
|
156
165
|
}
|
|
157
166
|
syncFs.mkdirSync(current, { mode: params.mode });
|
|
158
167
|
}
|
|
168
|
+
const currentRootStat = syncFs.lstatSync(rootDir);
|
|
169
|
+
const currentRootReal = syncFs.realpathSync(rootDir);
|
|
159
170
|
const currentReal = syncFs.realpathSync(current);
|
|
160
|
-
if (
|
|
161
|
-
|
|
171
|
+
if (currentRootStat.isSymbolicLink() ||
|
|
172
|
+
!currentRootStat.isDirectory() ||
|
|
173
|
+
!sameFileIdentity(rootStat, currentRootStat) ||
|
|
174
|
+
currentRootReal !== rootReal ||
|
|
175
|
+
!isPathInside(rootReal, currentReal)) {
|
|
176
|
+
throw new FsSafeError("outside-workspace", `${params.messagePrefix} directory escapes root`);
|
|
162
177
|
}
|
|
163
178
|
chmodDirectorySyncBestEffort(current, params.mode);
|
|
164
179
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAuBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA+ET"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { assertSyncDirectoryGuard, ensureParentSync, ensureStoreDirectorySync, } from "./file-store-boundary.js";
|
|
7
|
+
import { isPathInside } from "./path.js";
|
|
8
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
9
|
+
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
10
|
+
return ensureStoreDirectorySync({
|
|
11
|
+
rootDir,
|
|
12
|
+
targetDir,
|
|
13
|
+
mode,
|
|
14
|
+
messagePrefix: "private store",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function writeFileSyncAtomic(params) {
|
|
18
|
+
const filePath = path.resolve(params.filePath);
|
|
19
|
+
if (!isPathInside(params.rootDir, filePath)) {
|
|
20
|
+
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
21
|
+
}
|
|
22
|
+
let parentGuard;
|
|
23
|
+
if (params.privateMode) {
|
|
24
|
+
parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.lstatSync(filePath);
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
28
|
+
throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
parentGuard = ensureParentSync({
|
|
39
|
+
rootDir: params.rootDir,
|
|
40
|
+
filePath,
|
|
41
|
+
mode: params.dirMode,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
45
|
+
let tempExists = false;
|
|
46
|
+
try {
|
|
47
|
+
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
48
|
+
if (parentGuard) {
|
|
49
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
|
|
52
|
+
tempExists = true;
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(tempPath, params.mode);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
58
|
+
}
|
|
59
|
+
const tempStat = fs.lstatSync(tempPath);
|
|
60
|
+
if (parentGuard) {
|
|
61
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
62
|
+
}
|
|
63
|
+
fs.renameSync(tempPath, filePath);
|
|
64
|
+
tempExists = false;
|
|
65
|
+
if (parentGuard) {
|
|
66
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const publishedStat = fs.lstatSync(filePath);
|
|
70
|
+
if (publishedStat.isSymbolicLink() ||
|
|
71
|
+
!publishedStat.isFile() ||
|
|
72
|
+
publishedStat.nlink > 1 ||
|
|
73
|
+
!sameFileIdentity(tempStat, publishedStat)) {
|
|
74
|
+
throw new FsSafeError("path-mismatch", "store target changed after write");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof FsSafeError) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
throw new FsSafeError("path-mismatch", "store target changed after write", {
|
|
82
|
+
cause: error instanceof Error ? error : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return filePath;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (tempExists) {
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(tempPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Best-effort cleanup after write failure.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/file-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
|
package/dist/file-store.js
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import syncFs from "node:fs";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
6
|
-
import { createSyncDirectoryGuard } from "./directory-guard.js";
|
|
7
5
|
import { FsSafeError } from "./errors.js";
|
|
8
|
-
import { pruneExpiredStoreEntries
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { pruneExpiredStoreEntries } from "./file-store-prune.js";
|
|
7
|
+
import { ensureParentInRoot, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
|
|
8
|
+
import { writeFileSyncAtomic } from "./file-store-sync-write.js";
|
|
11
9
|
import { createJsonStore } from "./json-document-store.js";
|
|
12
10
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
13
|
-
import {
|
|
11
|
+
import { resolveSafeRelativePath } from "./path.js";
|
|
14
12
|
import { root } from "./root.js";
|
|
15
13
|
import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
|
|
14
|
+
import { readRegularFile } from "./regular-file.js";
|
|
16
15
|
import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
|
|
16
|
+
import { assertNoDriveRelativePathSegments } from "./safe-path-segment.js";
|
|
17
17
|
import { writeSecretFileAtomic } from "./secret-file.js";
|
|
18
|
-
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
19
18
|
function assertRelativePath(relativePath) {
|
|
20
19
|
const raw = relativePath.trim();
|
|
21
|
-
if (!raw) {
|
|
22
|
-
throw new FsSafeError("invalid-path", "
|
|
20
|
+
if (!raw || raw !== relativePath) {
|
|
21
|
+
throw new FsSafeError("invalid-path", "store key must be non-empty and unpadded");
|
|
23
22
|
}
|
|
24
|
-
return raw.replaceAll("\\", "/");
|
|
23
|
+
return assertNoDriveRelativePathSegments(raw.replaceAll("\\", "/"), "store key");
|
|
25
24
|
}
|
|
26
25
|
function resolveStorePath(rootDir, relativePath) {
|
|
27
26
|
return resolveSafeRelativePath(rootDir, assertRelativePath(relativePath));
|
|
28
27
|
}
|
|
29
|
-
function assertStoreFilePath(rootDir, filePath) {
|
|
30
|
-
if (!isPathInside(rootDir, filePath)) {
|
|
31
|
-
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
28
|
function assertMaxBytes(size, maxBytes) {
|
|
35
29
|
if (maxBytes !== undefined && size > maxBytes) {
|
|
36
30
|
throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
|
|
@@ -69,6 +63,33 @@ function handleSyncStoreReadOpenFailure(opened) {
|
|
|
69
63
|
},
|
|
70
64
|
});
|
|
71
65
|
}
|
|
66
|
+
async function readFileStoreCopySource(params) {
|
|
67
|
+
const sourceStat = await fs.lstat(params.sourcePath);
|
|
68
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
69
|
+
throw new FsSafeError("not-file", "source path is not a file");
|
|
70
|
+
}
|
|
71
|
+
if (params.maxBytes !== undefined && sourceStat.size > params.maxBytes) {
|
|
72
|
+
throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return (await readRegularFile({ filePath: params.sourcePath, maxBytes: params.maxBytes }))
|
|
76
|
+
.buffer;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
if (message.includes("regular file") || message.includes("not a regular file")) {
|
|
81
|
+
throw new FsSafeError("not-file", "source path is not a file", {
|
|
82
|
+
cause: error instanceof Error ? error : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (params.maxBytes !== undefined && message.includes(`exceeds ${params.maxBytes} bytes`)) {
|
|
86
|
+
throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`, {
|
|
87
|
+
cause: error instanceof Error ? error : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
async function copyIntoRoot(params) {
|
|
73
94
|
const relativePath = assertRelativePath(params.relativePath);
|
|
74
95
|
const destination = resolveStorePath(params.rootDir, relativePath);
|
|
@@ -264,119 +285,6 @@ export function fileStore(options) {
|
|
|
264
285
|
},
|
|
265
286
|
};
|
|
266
287
|
}
|
|
267
|
-
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
268
|
-
const root = path.resolve(rootDir);
|
|
269
|
-
const target = path.resolve(targetDir);
|
|
270
|
-
assertStoreFilePath(root, target);
|
|
271
|
-
let current = root;
|
|
272
|
-
syncFs.mkdirSync(current, { recursive: true, mode });
|
|
273
|
-
const rootStat = syncFs.lstatSync(current);
|
|
274
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
275
|
-
throw new FsSafeError("not-file", `private store root must be a directory: ${current}`);
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
syncFs.chmodSync(current, mode);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
282
|
-
}
|
|
283
|
-
for (const segment of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
|
284
|
-
current = path.join(current, segment);
|
|
285
|
-
try {
|
|
286
|
-
const stat = syncFs.lstatSync(current);
|
|
287
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
288
|
-
throw new FsSafeError("not-file", `private store directory component must be a directory: ${current}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
if (error.code !== "ENOENT") {
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
syncFs.mkdirSync(current, { mode });
|
|
296
|
-
}
|
|
297
|
-
const rootReal = syncFs.realpathSync(root);
|
|
298
|
-
const currentReal = syncFs.realpathSync(current);
|
|
299
|
-
if (!isPathInside(rootReal, currentReal)) {
|
|
300
|
-
throw new FsSafeError("outside-workspace", "private store directory escapes root");
|
|
301
|
-
}
|
|
302
|
-
try {
|
|
303
|
-
syncFs.chmodSync(current, mode);
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const guard = createSyncDirectoryGuard(target);
|
|
310
|
-
assertSyncDirectoryGuard(guard);
|
|
311
|
-
return guard;
|
|
312
|
-
}
|
|
313
|
-
function writeFileSyncAtomic(params) {
|
|
314
|
-
const filePath = path.resolve(params.filePath);
|
|
315
|
-
assertStoreFilePath(params.rootDir, filePath);
|
|
316
|
-
let parentGuard;
|
|
317
|
-
if (params.privateMode) {
|
|
318
|
-
parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
319
|
-
try {
|
|
320
|
-
const stat = syncFs.lstatSync(filePath);
|
|
321
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
322
|
-
throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
if (error.code !== "ENOENT") {
|
|
327
|
-
throw error;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
parentGuard = ensureParentSync({
|
|
333
|
-
rootDir: params.rootDir,
|
|
334
|
-
filePath,
|
|
335
|
-
mode: params.dirMode,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
339
|
-
let tempExists = false;
|
|
340
|
-
try {
|
|
341
|
-
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
342
|
-
if (parentGuard) {
|
|
343
|
-
assertSyncDirectoryGuard(parentGuard);
|
|
344
|
-
}
|
|
345
|
-
syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
|
|
346
|
-
tempExists = true;
|
|
347
|
-
try {
|
|
348
|
-
syncFs.chmodSync(tempPath, params.mode);
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
352
|
-
}
|
|
353
|
-
if (parentGuard) {
|
|
354
|
-
assertSyncDirectoryGuard(parentGuard);
|
|
355
|
-
}
|
|
356
|
-
syncFs.renameSync(tempPath, filePath);
|
|
357
|
-
tempExists = false;
|
|
358
|
-
if (parentGuard) {
|
|
359
|
-
assertSyncDirectoryGuard(parentGuard);
|
|
360
|
-
}
|
|
361
|
-
try {
|
|
362
|
-
syncFs.chmodSync(filePath, params.mode);
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
366
|
-
}
|
|
367
|
-
return filePath;
|
|
368
|
-
}
|
|
369
|
-
finally {
|
|
370
|
-
if (tempExists) {
|
|
371
|
-
try {
|
|
372
|
-
syncFs.unlinkSync(tempPath);
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
// Best-effort cleanup after write failure.
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
288
|
export function fileStoreSync(options) {
|
|
381
289
|
const rootDir = path.resolve(options.rootDir);
|
|
382
290
|
const privateMode = options.private ?? false;
|
package/dist/filename.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AA4BA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAgCxF"}
|
package/dist/filename.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
|
|
2
3
|
const WINDOWS_INVALID_FILE_NAME_CHARACTERS = new Set('<>:"/\\|?*');
|
|
4
|
+
function trimWindowsIgnoredSuffix(value) {
|
|
5
|
+
let end = value.length;
|
|
6
|
+
while (end > 0) {
|
|
7
|
+
const character = value.charCodeAt(end - 1);
|
|
8
|
+
if (character !== 0x20 && character !== 0x2e) {
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
end -= 1;
|
|
12
|
+
}
|
|
13
|
+
return end === value.length ? value : value.slice(0, end);
|
|
14
|
+
}
|
|
15
|
+
function suffixWindowsReservedDeviceName(fileName) {
|
|
16
|
+
const extensionIndex = fileName.indexOf(".");
|
|
17
|
+
const baseNameEnd = extensionIndex < 0 ? fileName.length : extensionIndex;
|
|
18
|
+
const baseName = fileName.slice(0, baseNameEnd);
|
|
19
|
+
const deviceBaseName = trimWindowsIgnoredSuffix(baseName);
|
|
20
|
+
if (!WINDOWS_RESERVED_DEVICE_NAMES.has(deviceBaseName.toUpperCase())) {
|
|
21
|
+
return fileName;
|
|
22
|
+
}
|
|
23
|
+
return `${baseName}_${fileName.slice(baseNameEnd)}`;
|
|
24
|
+
}
|
|
3
25
|
export function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
4
26
|
const trimmed = typeof fileName === "string" ? fileName.trim() : "";
|
|
5
27
|
if (!trimmed) {
|
|
@@ -21,8 +43,13 @@ export function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
|
21
43
|
if (!base || base === "." || base === "..") {
|
|
22
44
|
return fallbackName;
|
|
23
45
|
}
|
|
46
|
+
base = suffixWindowsReservedDeviceName(base);
|
|
24
47
|
if (base.length > 200) {
|
|
25
48
|
base = base.slice(0, 200);
|
|
49
|
+
const trailingCodeUnit = base.charCodeAt(base.length - 1);
|
|
50
|
+
if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
|
|
51
|
+
base = base.slice(0, -1);
|
|
52
|
+
}
|
|
26
53
|
}
|
|
27
54
|
return base;
|
|
28
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDlB"}
|