@openclaw/fs-safe 0.6.0 → 0.7.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 +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 -26
- 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/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- 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 +27 -8
- 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/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 +8 -9
- 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 +40 -26
- 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 +48 -27
- 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.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- 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 +194 -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 +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- 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 +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/package.json +10 -10
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import syncFs, {} from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
3
|
import { sameFileIdentity } from "./file-identity.js";
|
|
4
|
+
import { readOwnedCopySource, readOwnedCopySourceSync } from "./replace-file-copy-source.js";
|
|
5
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
4
6
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants;
|
|
5
7
|
const NOFOLLOW = SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0;
|
|
6
|
-
const OPEN_READ_FLAGS =
|
|
8
|
+
const OPEN_READ_FLAGS = resolveReadOpenFlags();
|
|
7
9
|
const OPEN_READ_WRITE_FLAGS = syncFs.constants.O_RDWR | NOFOLLOW;
|
|
8
10
|
const OPEN_WRITE_EXCLUSIVE_FLAGS = syncFs.constants.O_WRONLY | syncFs.constants.O_CREAT | syncFs.constants.O_EXCL | NOFOLLOW;
|
|
9
11
|
const READ_CHUNK_BYTES = 64 * 1024;
|
|
@@ -236,24 +238,19 @@ function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
export async function copyFallbackReplace(params) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
const source = await readOwnedCopySource({
|
|
242
|
+
fsModule: params.fsModule,
|
|
243
|
+
src: params.src,
|
|
244
|
+
expectedIdentity: params.expectedSourceIdentity,
|
|
245
|
+
});
|
|
246
|
+
const { replacement } = source;
|
|
244
247
|
let destHandle = null;
|
|
245
248
|
try {
|
|
246
|
-
const sourceStat = await sourceHandle.stat();
|
|
247
|
-
const sourceCurrent = await params.fsModule.lstat(params.src);
|
|
248
|
-
if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
|
|
249
|
-
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
250
|
-
}
|
|
251
|
-
const replacement = await sourceHandle.readFile();
|
|
252
249
|
if (params.restore === "restore-original") {
|
|
253
250
|
const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
|
|
254
251
|
if (pinned) {
|
|
255
252
|
destHandle = pinned.handle;
|
|
256
|
-
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes,
|
|
253
|
+
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, source.mode);
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
256
|
if (!destHandle) {
|
|
@@ -269,9 +266,9 @@ export async function copyFallbackReplace(params) {
|
|
|
269
266
|
await assertDestinationHardlinkPolicy(params.fsModule, params.dest, params.destinationHardlinks);
|
|
270
267
|
await params.fsModule.rm(params.dest, { force: true });
|
|
271
268
|
}
|
|
272
|
-
destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS,
|
|
269
|
+
destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
|
|
273
270
|
await destHandle.writeFile(replacement);
|
|
274
|
-
await destHandle.chmod(
|
|
271
|
+
await destHandle.chmod(source.mode);
|
|
275
272
|
if (params.sync) {
|
|
276
273
|
await destHandle.sync();
|
|
277
274
|
}
|
|
@@ -279,28 +276,22 @@ export async function copyFallbackReplace(params) {
|
|
|
279
276
|
}
|
|
280
277
|
finally {
|
|
281
278
|
await destHandle?.close().catch(() => undefined);
|
|
282
|
-
await sourceHandle.close().catch(() => undefined);
|
|
283
279
|
}
|
|
284
280
|
}
|
|
285
281
|
export function copyFallbackReplaceSync(params) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
const source = readOwnedCopySourceSync({
|
|
283
|
+
fsModule: params.fsModule,
|
|
284
|
+
src: params.src,
|
|
285
|
+
expectedIdentity: params.expectedSourceIdentity,
|
|
286
|
+
});
|
|
287
|
+
const { replacement } = source;
|
|
291
288
|
let destFd;
|
|
292
289
|
try {
|
|
293
|
-
const sourceStat = params.fsModule.fstatSync(sourceFd);
|
|
294
|
-
const sourceCurrent = params.fsModule.lstatSync(params.src);
|
|
295
|
-
if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
|
|
296
|
-
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
297
|
-
}
|
|
298
|
-
const replacement = readBoundedSync(params.fsModule, sourceFd, Number.MAX_SAFE_INTEGER);
|
|
299
290
|
if (params.restore === "restore-original") {
|
|
300
291
|
const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
|
|
301
292
|
if (pinned) {
|
|
302
293
|
destFd = pinned.fd;
|
|
303
|
-
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes,
|
|
294
|
+
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, source.mode, params.fchmodSync);
|
|
304
295
|
}
|
|
305
296
|
}
|
|
306
297
|
if (destFd === undefined) {
|
|
@@ -319,9 +310,9 @@ export function copyFallbackReplaceSync(params) {
|
|
|
319
310
|
assertDestinationHardlinkPolicySync(params.fsModule, params.dest, params.destinationHardlinks);
|
|
320
311
|
params.fsModule.rmSync(params.dest, { force: true });
|
|
321
312
|
}
|
|
322
|
-
destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS,
|
|
313
|
+
destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
|
|
323
314
|
writeAllSync(params.fsModule, destFd, replacement);
|
|
324
|
-
params.fchmodSync?.(destFd,
|
|
315
|
+
params.fchmodSync?.(destFd, source.mode);
|
|
325
316
|
if (params.sync) {
|
|
326
317
|
params.fsModule.fsyncSync(destFd);
|
|
327
318
|
}
|
|
@@ -336,11 +327,5 @@ export function copyFallbackReplaceSync(params) {
|
|
|
336
327
|
// Best-effort close after fallback replacement.
|
|
337
328
|
}
|
|
338
329
|
}
|
|
339
|
-
try {
|
|
340
|
-
params.fsModule.closeSync(sourceFd);
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Best-effort close after fallback replacement.
|
|
344
|
-
}
|
|
345
330
|
}
|
|
346
331
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
+
type AsyncSourceFileSystem = Pick<typeof import("node:fs/promises"), "lstat" | "open">;
|
|
3
|
+
type SyncSourceFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readSync">;
|
|
4
|
+
export declare function readOwnedCopySource(params: {
|
|
5
|
+
fsModule: AsyncSourceFileSystem;
|
|
6
|
+
src: string;
|
|
7
|
+
expectedIdentity?: BigIntStats;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
replacement: Buffer;
|
|
10
|
+
mode: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function readOwnedCopySourceSync(params: {
|
|
13
|
+
fsModule: SyncSourceFileSystem;
|
|
14
|
+
src: string;
|
|
15
|
+
expectedIdentity?: BigIntStats;
|
|
16
|
+
}): {
|
|
17
|
+
replacement: Buffer;
|
|
18
|
+
mode: number;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=replace-file-copy-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnD,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AAgEF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA8BxC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import syncFs, {} from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
4
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
|
+
const NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants
|
|
6
|
+
? syncFs.constants.O_NOFOLLOW
|
|
7
|
+
: 0;
|
|
8
|
+
const NONBLOCK = process.platform !== "win32" && "O_NONBLOCK" in syncFs.constants
|
|
9
|
+
? syncFs.constants.O_NONBLOCK
|
|
10
|
+
: 0;
|
|
11
|
+
const OPEN_READ_FLAGS = syncFs.constants.O_RDONLY | NOFOLLOW | NONBLOCK;
|
|
12
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
13
|
+
function assertSourcePreview(source, src) {
|
|
14
|
+
if (source.isSymbolicLink()) {
|
|
15
|
+
throw new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`);
|
|
16
|
+
}
|
|
17
|
+
if (!source.isFile()) {
|
|
18
|
+
throw new FsSafeError("not-file", `Refusing copy fallback from non-file source: ${src}`);
|
|
19
|
+
}
|
|
20
|
+
if (source.nlink !== 1) {
|
|
21
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sourceSymlinkError(src, cause) {
|
|
25
|
+
return new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`, {
|
|
26
|
+
cause,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function openSource(fsModule, src) {
|
|
30
|
+
try {
|
|
31
|
+
return await fsModule.open(src, OPEN_READ_FLAGS);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ELOOP") {
|
|
35
|
+
throw sourceSymlinkError(src, error);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function openSourceSync(fsModule, src) {
|
|
41
|
+
try {
|
|
42
|
+
return fsModule.openSync(src, OPEN_READ_FLAGS);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === "ELOOP") {
|
|
46
|
+
throw sourceSymlinkError(src, error);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readAllSync(fsModule, fd) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let position = 0;
|
|
54
|
+
while (true) {
|
|
55
|
+
const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
56
|
+
const bytesRead = fsModule.readSync(fd, buffer, 0, buffer.length, position);
|
|
57
|
+
if (bytesRead === 0)
|
|
58
|
+
break;
|
|
59
|
+
position += bytesRead;
|
|
60
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
61
|
+
}
|
|
62
|
+
return Buffer.concat(chunks, position);
|
|
63
|
+
}
|
|
64
|
+
export async function readOwnedCopySource(params) {
|
|
65
|
+
assertSourcePreview(await params.fsModule.lstat(params.src), params.src);
|
|
66
|
+
const handle = await openSource(params.fsModule, params.src);
|
|
67
|
+
try {
|
|
68
|
+
if (params.expectedIdentity) {
|
|
69
|
+
const exact = await inspectFileIdentity(() => handle.stat({ bigint: true }), params.expectedIdentity);
|
|
70
|
+
await inspectFileIdentity(() => params.fsModule.lstat(params.src, { bigint: true }), exact);
|
|
71
|
+
}
|
|
72
|
+
const opened = await handle.stat();
|
|
73
|
+
const current = await params.fsModule.lstat(params.src);
|
|
74
|
+
if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
|
|
75
|
+
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
76
|
+
}
|
|
77
|
+
if (opened.nlink !== 1) {
|
|
78
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
|
|
79
|
+
}
|
|
80
|
+
return { replacement: await handle.readFile(), mode: opened.mode };
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await handle.close().catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function readOwnedCopySourceSync(params) {
|
|
87
|
+
assertSourcePreview(params.fsModule.lstatSync(params.src), params.src);
|
|
88
|
+
const fd = openSourceSync(params.fsModule, params.src);
|
|
89
|
+
try {
|
|
90
|
+
if (params.expectedIdentity) {
|
|
91
|
+
const exact = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), params.expectedIdentity);
|
|
92
|
+
inspectFileIdentitySync(() => params.fsModule.lstatSync(params.src, { bigint: true }), exact);
|
|
93
|
+
}
|
|
94
|
+
const opened = params.fsModule.fstatSync(fd);
|
|
95
|
+
const current = params.fsModule.lstatSync(params.src);
|
|
96
|
+
if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
|
|
97
|
+
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
98
|
+
}
|
|
99
|
+
if (opened.nlink !== 1) {
|
|
100
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
|
|
101
|
+
}
|
|
102
|
+
return { replacement: readAllSync(params.fsModule, fd), mode: opened.mode };
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
try {
|
|
106
|
+
params.fsModule.closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort close after reading a copy-fallback source.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
2
|
+
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
3
|
type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
|
|
4
4
|
type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
|
|
5
5
|
export type SyncFchmod = (fd: number, mode: number) => void;
|
|
@@ -7,6 +7,8 @@ export declare function applyDirectoryMode(params: {
|
|
|
7
7
|
fsModule: AsyncTempFileSystem;
|
|
8
8
|
dirPath: string;
|
|
9
9
|
mode: number;
|
|
10
|
+
/** Compatibility for best-effort directory modes; admission and close still fail closed. */
|
|
11
|
+
ignoreChmodError?: boolean;
|
|
10
12
|
}): Promise<void>;
|
|
11
13
|
export declare function applyDirectoryModeSync(params: {
|
|
12
14
|
fsModule: SyncTempFileSystem;
|
|
@@ -20,7 +22,11 @@ export declare function writeTempFile(params: {
|
|
|
20
22
|
content: string | Uint8Array;
|
|
21
23
|
mode: number;
|
|
22
24
|
sync: boolean;
|
|
23
|
-
|
|
25
|
+
onIdentity?: (identity: BigIntStats) => void;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
handle: FileHandle;
|
|
28
|
+
identity: BigIntStats;
|
|
29
|
+
}>;
|
|
24
30
|
export declare function writeTempFileSync(params: {
|
|
25
31
|
fsModule: SyncTempFileSystem;
|
|
26
32
|
tempPath: string;
|
|
@@ -28,6 +34,10 @@ export declare function writeTempFileSync(params: {
|
|
|
28
34
|
mode: number;
|
|
29
35
|
fchmodSync?: SyncFchmod;
|
|
30
36
|
sync: boolean;
|
|
31
|
-
|
|
37
|
+
onIdentity?: (identity: BigIntStats) => void;
|
|
38
|
+
}): {
|
|
39
|
+
fd: number;
|
|
40
|
+
identity: BigIntStats;
|
|
41
|
+
};
|
|
32
42
|
export {};
|
|
33
43
|
//# sourceMappingURL=replace-file-descriptor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,
|
|
1
|
+
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AA2B5D,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
|
|
@@ -2,6 +2,7 @@ import syncFs, {} from "node:fs";
|
|
|
2
2
|
import fs, {} from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
6
|
function directoryOpenFlags() {
|
|
6
7
|
return (syncFs.constants.O_RDONLY |
|
|
7
8
|
syncFs.constants.O_DIRECTORY |
|
|
@@ -31,7 +32,13 @@ export async function applyDirectoryMode(params) {
|
|
|
31
32
|
const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
|
|
32
33
|
try {
|
|
33
34
|
assertSameDirectory(expected, await handle.stat(), params.dirPath);
|
|
34
|
-
|
|
35
|
+
try {
|
|
36
|
+
await handle.chmod(params.mode);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (params.ignoreChmodError !== true)
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
finally {
|
|
37
44
|
await handle.close();
|
|
@@ -55,6 +62,8 @@ export function applyDirectoryModeSync(params) {
|
|
|
55
62
|
export async function writeTempFile(params) {
|
|
56
63
|
const handle = await params.fsModule.open(params.tempPath, "wx", params.mode);
|
|
57
64
|
try {
|
|
65
|
+
const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }));
|
|
66
|
+
params.onIdentity?.(identity);
|
|
58
67
|
await params.fsModule.writeFile(handle, params.content);
|
|
59
68
|
await handle.chmod(params.mode);
|
|
60
69
|
if (params.sync) {
|
|
@@ -67,15 +76,24 @@ export async function writeTempFile(params) {
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
|
-
|
|
79
|
+
await inspectFileIdentity(() => handle.stat({ bigint: true }), identity);
|
|
80
|
+
return { handle, identity };
|
|
71
81
|
}
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
catch (error) {
|
|
83
|
+
try {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
catch (closeError) {
|
|
87
|
+
throw new AggregateError([error, closeError], "Atomic temp write and close failed");
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
export function writeTempFileSync(params) {
|
|
77
93
|
const fd = params.fsModule.openSync(params.tempPath, "wx", params.mode);
|
|
78
94
|
try {
|
|
95
|
+
const identity = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }));
|
|
96
|
+
params.onIdentity?.(identity);
|
|
79
97
|
params.fsModule.writeFileSync(fd, params.content);
|
|
80
98
|
params.fchmodSync?.(fd, params.mode);
|
|
81
99
|
if (params.sync) {
|
|
@@ -88,9 +106,16 @@ export function writeTempFileSync(params) {
|
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
|
-
|
|
109
|
+
inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), identity);
|
|
110
|
+
return { fd, identity };
|
|
92
111
|
}
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
params.fsModule.closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
catch (closeError) {
|
|
117
|
+
throw new AggregateError([error, closeError], "Atomic temp write and close failed");
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
95
120
|
}
|
|
96
121
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RenameIdentityPolicy } from "./pinned-write.js";
|
|
2
|
+
export type { RenameIdentityPolicy };
|
|
3
|
+
export declare function validateRenameIdentity(policy: RenameIdentityPolicy | undefined): void;
|
|
4
|
+
export declare function atomicExpectedContentHash(policy: RenameIdentityPolicy | undefined, content: string | Uint8Array): string | undefined;
|
|
5
|
+
export declare function withAtomicRenameIdentityLock<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
|
|
6
|
+
export declare function withAtomicRenameIdentityLockSync<T>(filePath: string, fn: () => T): T;
|
|
7
|
+
//# sourceMappingURL=replace-file-rename-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-rename-policy.d.ts","sourceRoot":"","sources":["../src/replace-file-rename-policy.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAIrF;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,oBAAoB,GAAG,SAAS,EACxC,OAAO,EAAE,MAAM,GAAG,UAAU,GAC3B,MAAM,GAAG,SAAS,CAGpB;AAcD,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,wBAAgB,gCAAgC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { withFileLock, withFileLockSync } from "./file-lock.js";
|
|
3
|
+
import { sha256Hex } from "./file-identity.js";
|
|
4
|
+
export function validateRenameIdentity(policy) {
|
|
5
|
+
if (policy !== undefined && policy !== "strict" && policy !== "verify-content-with-lock") {
|
|
6
|
+
throw new RangeError("renameIdentity must be strict or verify-content-with-lock");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function atomicExpectedContentHash(policy, content) {
|
|
10
|
+
if (policy !== "verify-content-with-lock")
|
|
11
|
+
return undefined;
|
|
12
|
+
return sha256Hex(typeof content === "string" ? content : Buffer.from(content));
|
|
13
|
+
}
|
|
14
|
+
function lockOptions(filePath) {
|
|
15
|
+
const resolved = path.resolve(filePath);
|
|
16
|
+
return {
|
|
17
|
+
managerKey: `fs-safe.atomic:${resolved}`,
|
|
18
|
+
lockPath: path.join(path.dirname(resolved), `.fs-safe-atomic-${sha256Hex(resolved)}.lock`),
|
|
19
|
+
staleMs: 30_000,
|
|
20
|
+
timeoutMs: 5_000,
|
|
21
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
22
|
+
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function withAtomicRenameIdentityLock(filePath, fn) {
|
|
26
|
+
return await withFileLock(filePath, lockOptions(filePath), fn);
|
|
27
|
+
}
|
|
28
|
+
export function withAtomicRenameIdentityLockSync(filePath, fn) {
|
|
29
|
+
return withFileLockSync(filePath, lockOptions(filePath), fn);
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
+
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
+
type AsyncOwnerFileSystem = Pick<typeof fs, "lstat" | "open" | "unlink">;
|
|
4
|
+
type SyncOwnerFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "unlinkSync">;
|
|
5
|
+
export declare class AsyncAtomicTempOwner {
|
|
6
|
+
#private;
|
|
7
|
+
readonly pathname: string;
|
|
8
|
+
constructor(pathname: string);
|
|
9
|
+
start(): void;
|
|
10
|
+
readonly onIdentity: (identity: BigIntStats) => void;
|
|
11
|
+
adopt(temp: {
|
|
12
|
+
handle: FileHandle;
|
|
13
|
+
identity: BigIntStats;
|
|
14
|
+
}): void;
|
|
15
|
+
get identity(): BigIntStats;
|
|
16
|
+
assertCurrent(fsModule: AsyncOwnerFileSystem, pathname?: string): Promise<void>;
|
|
17
|
+
assertPublished(fsModule: AsyncOwnerFileSystem, pathname: string, expectedHash?: string): Promise<void>;
|
|
18
|
+
markRenamed(): void;
|
|
19
|
+
finish(params: {
|
|
20
|
+
fsModule: AsyncOwnerFileSystem;
|
|
21
|
+
originalError?: unknown;
|
|
22
|
+
throwOnCleanupError: boolean;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SyncAtomicTempOwner {
|
|
26
|
+
#private;
|
|
27
|
+
readonly pathname: string;
|
|
28
|
+
constructor(pathname: string);
|
|
29
|
+
start(): void;
|
|
30
|
+
readonly onIdentity: (identity: BigIntStats) => void;
|
|
31
|
+
adopt(temp: {
|
|
32
|
+
fd: number;
|
|
33
|
+
identity: BigIntStats;
|
|
34
|
+
}): void;
|
|
35
|
+
get identity(): BigIntStats;
|
|
36
|
+
assertCurrent(fsModule: SyncOwnerFileSystem, pathname?: string): void;
|
|
37
|
+
assertPublished(fsModule: SyncOwnerFileSystem, pathname: string, expectedHash?: string): void;
|
|
38
|
+
markRenamed(): void;
|
|
39
|
+
finish(params: {
|
|
40
|
+
fsModule: SyncOwnerFileSystem;
|
|
41
|
+
originalError?: unknown;
|
|
42
|
+
throwOnCleanupError: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=replace-file-temp-owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-temp-owner.d.ts","sourceRoot":"","sources":["../src/replace-file-temp-owner.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMvD,KAAK,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;AACzE,KAAK,mBAAmB,GAAG,IAAI,CAC7B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CACrF,CAAC;AAiGF,qBAAa,oBAAoB;;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAG/D;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAEK,aAAa,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB3F;IAEK,eAAe,CACnB,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA0Cf;IAED,WAAW,IAAI,IAAI,CAGlB;IAEK,MAAM,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,oBAAoB,CAAC;QAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB;CACF;AAED,qBAAa,mBAAmB;;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAGvD;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAED,aAAa,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,SAAgB,GAAG,IAAI,CAkB3E;IAED,eAAe,CACb,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI,CAgDN;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,MAAM,CAAC,MAAM,EAAE;QACb,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,IAAI,CAgCP;CACF"}
|