@openclaw/fs-safe 0.2.4 → 0.2.5
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 +15 -1
- package/README.md +2 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +12 -1
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +6 -5
- package/dist/json-document-store.d.ts +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +5 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +157 -4
- package/dist/json-stringify.d.ts +2 -0
- package/dist/json-stringify.d.ts.map +1 -0
- package/dist/json-stringify.js +7 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +3 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -1
- package/dist/path.js +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -10
- package/dist/pinned-python.d.ts.map +1 -1
- package/dist/pinned-python.js +125 -43
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +6 -2
- package/dist/root-paths.js +25 -3
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +3 -1
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +8 -1
- package/docs/index.md +1 -1
- package/docs/json-store.md +3 -1
- package/package.json +4 -4
- package/dist/archive-utils.d.ts +0 -3
- package/dist/archive-utils.d.ts.map +0 -1
- package/dist/archive-utils.js +0 -26
- package/dist/boundary-file-read.d.ts +0 -44
- package/dist/boundary-file-read.d.ts.map +0 -1
- package/dist/boundary-file-read.js +0 -129
- package/dist/boundary-path.d.ts +0 -39
- package/dist/boundary-path.d.ts.map +0 -1
- package/dist/boundary-path.js +0 -598
- package/dist/fs-pinned-path-helper.d.ts +0 -7
- package/dist/fs-pinned-path-helper.d.ts.map +0 -1
- package/dist/fs-pinned-path-helper.js +0 -182
- package/dist/fs-pinned-write-helper.d.ts +0 -21
- package/dist/fs-pinned-write-helper.d.ts.map +0 -1
- package/dist/fs-pinned-write-helper.js +0 -263
- package/dist/hardlink-guards.d.ts +0 -7
- package/dist/hardlink-guards.d.ts.map +0 -1
- package/dist/hardlink-guards.js +0 -30
- package/dist/install-safe-path.d.ts +0 -20
- package/dist/install-safe-path.d.ts.map +0 -1
- package/dist/install-safe-path.js +0 -94
- package/dist/json-file.d.ts +0 -3
- package/dist/json-file.d.ts.map +0 -1
- package/dist/json-file.js +0 -123
- package/dist/json-files.d.ts +0 -20
- package/dist/json-files.d.ts.map +0 -1
- package/dist/json-files.js +0 -153
- package/dist/path-alias-guards.d.ts +0 -19
- package/dist/path-alias-guards.d.ts.map +0 -1
- package/dist/path-alias-guards.js +0 -21
- package/dist/path-guards.d.ts +0 -7
- package/dist/path-guards.d.ts.map +0 -1
- package/dist/path-guards.js +0 -49
- package/dist/path-safety.d.ts +0 -12
- package/dist/path-safety.d.ts.map +0 -1
- package/dist/path-safety.js +0 -50
- package/dist/path-scope.d.ts +0 -2
- package/dist/path-scope.d.ts.map +0 -1
- package/dist/path-scope.js +0 -1
- package/dist/private-file-store.d.ts +0 -60
- package/dist/private-file-store.d.ts.map +0 -1
- package/dist/private-file-store.js +0 -233
- package/dist/safe-open-sync.d.ts +0 -24
- package/dist/safe-open-sync.d.ts.map +0 -1
- package/dist/safe-open-sync.js +0 -71
- package/dist/safe-root.d.ts +0 -123
- package/dist/safe-root.d.ts.map +0 -1
- package/dist/safe-root.js +0 -1060
- package/dist/secure-temp-workspace.d.ts +0 -25
- package/dist/secure-temp-workspace.d.ts.map +0 -1
- package/dist/secure-temp-workspace.js +0 -136
- package/dist/sibling-temp-file.d.ts +0 -16
- package/dist/sibling-temp-file.d.ts.map +0 -1
- package/dist/sibling-temp-file.js +0 -73
- package/dist/sibling-temp-write.d.ts +0 -8
- package/dist/sibling-temp-write.d.ts.map +0 -1
- package/dist/sibling-temp-write.js +0 -40
package/dist/pinned-python.js
CHANGED
|
@@ -87,6 +87,85 @@ def reject_unsafe_endpoint(st):
|
|
|
87
87
|
if stat.S_ISREG(mode) and st.st_nlink > 1:
|
|
88
88
|
raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
|
|
89
89
|
|
|
90
|
+
def copy_bytes(source_fd, dest_fd):
|
|
91
|
+
while True:
|
|
92
|
+
chunk = os.read(source_fd, 65536)
|
|
93
|
+
if not chunk:
|
|
94
|
+
break
|
|
95
|
+
view = memoryview(chunk)
|
|
96
|
+
while view:
|
|
97
|
+
written = os.write(dest_fd, view)
|
|
98
|
+
if written <= 0:
|
|
99
|
+
raise OSError(errno.EIO, "short write")
|
|
100
|
+
view = view[written:]
|
|
101
|
+
|
|
102
|
+
def write_all(fd, data):
|
|
103
|
+
view = memoryview(data)
|
|
104
|
+
while view:
|
|
105
|
+
written = os.write(fd, view)
|
|
106
|
+
if written <= 0:
|
|
107
|
+
raise OSError(errno.EIO, "short write")
|
|
108
|
+
view = view[written:]
|
|
109
|
+
|
|
110
|
+
def link_unsupported(exc):
|
|
111
|
+
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
|
112
|
+
return getattr(exc, "errno", None) in unsupported
|
|
113
|
+
|
|
114
|
+
def link_no_replace(name, new_name, source_fd, target_fd):
|
|
115
|
+
linked = False
|
|
116
|
+
try:
|
|
117
|
+
os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
|
|
118
|
+
linked = True
|
|
119
|
+
os.unlink(name, dir_fd=source_fd)
|
|
120
|
+
except Exception:
|
|
121
|
+
if linked:
|
|
122
|
+
try: os.unlink(new_name, dir_fd=target_fd)
|
|
123
|
+
except FileNotFoundError: pass
|
|
124
|
+
raise
|
|
125
|
+
os.fsync(source_fd)
|
|
126
|
+
if source_fd != target_fd:
|
|
127
|
+
os.fsync(target_fd)
|
|
128
|
+
|
|
129
|
+
def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
|
|
130
|
+
source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
|
|
131
|
+
dest_fd = None
|
|
132
|
+
success = False
|
|
133
|
+
try:
|
|
134
|
+
if expected is not None:
|
|
135
|
+
source_stat = os.fstat(source_fd)
|
|
136
|
+
if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
|
|
137
|
+
raise RuntimeError("fs-safe-source-mismatch")
|
|
138
|
+
dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
|
|
139
|
+
copy_bytes(source_fd, dest_fd)
|
|
140
|
+
os.fsync(dest_fd)
|
|
141
|
+
success = True
|
|
142
|
+
finally:
|
|
143
|
+
os.close(source_fd)
|
|
144
|
+
if dest_fd is not None:
|
|
145
|
+
os.close(dest_fd)
|
|
146
|
+
if dest_fd is not None and not success:
|
|
147
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
148
|
+
except FileNotFoundError: pass
|
|
149
|
+
if unlink_source:
|
|
150
|
+
try:
|
|
151
|
+
os.unlink(source_name, dir_fd=source_parent_fd)
|
|
152
|
+
except Exception:
|
|
153
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
154
|
+
except FileNotFoundError: pass
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode):
|
|
158
|
+
if overwrite:
|
|
159
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
|
|
163
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
164
|
+
except OSError as exc:
|
|
165
|
+
if not link_unsupported(exc):
|
|
166
|
+
raise
|
|
167
|
+
copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, unlink_source=True)
|
|
168
|
+
|
|
90
169
|
def stat_path(root_fd, payload):
|
|
91
170
|
relative = payload.get("relativePath", "")
|
|
92
171
|
segments = split_relative(relative)
|
|
@@ -161,7 +240,18 @@ def rename_path(root_fd, payload):
|
|
|
161
240
|
try:
|
|
162
241
|
from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
|
|
163
242
|
reject_unsafe_endpoint(from_stat)
|
|
164
|
-
|
|
243
|
+
overwrite = payload.get("overwrite", True)
|
|
244
|
+
if not overwrite and stat.S_ISREG(from_stat.st_mode):
|
|
245
|
+
try:
|
|
246
|
+
link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
|
|
247
|
+
except OSError as exc:
|
|
248
|
+
if not link_unsupported(exc):
|
|
249
|
+
raise
|
|
250
|
+
copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
|
|
251
|
+
return None
|
|
252
|
+
if not overwrite and stat.S_ISDIR(from_stat.st_mode):
|
|
253
|
+
raise RuntimeError("fs-safe-directory-noreplace-unsupported")
|
|
254
|
+
if not overwrite:
|
|
165
255
|
try:
|
|
166
256
|
os.lstat(to_base, dir_fd=to_parent_fd)
|
|
167
257
|
raise FileExistsError(errno.EEXIST, "destination exists", to_base)
|
|
@@ -206,16 +296,11 @@ def write_path(root_fd, payload):
|
|
|
206
296
|
except FileNotFoundError:
|
|
207
297
|
pass
|
|
208
298
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
209
|
-
|
|
210
|
-
while view:
|
|
211
|
-
written = os.write(temp_fd, view)
|
|
212
|
-
if written <= 0:
|
|
213
|
-
raise OSError(errno.EIO, "short write")
|
|
214
|
-
view = view[written:]
|
|
299
|
+
write_all(temp_fd, data)
|
|
215
300
|
os.fsync(temp_fd)
|
|
216
301
|
os.close(temp_fd)
|
|
217
302
|
temp_fd = None
|
|
218
|
-
|
|
303
|
+
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
219
304
|
temp_name = None
|
|
220
305
|
os.fsync(parent_fd)
|
|
221
306
|
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
@@ -248,12 +333,6 @@ def copy_path(root_fd, payload):
|
|
|
248
333
|
if max_bytes >= 0 and source_stat.st_size > max_bytes:
|
|
249
334
|
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
250
335
|
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
251
|
-
if not overwrite:
|
|
252
|
-
try:
|
|
253
|
-
os.lstat(basename, dir_fd=parent_fd)
|
|
254
|
-
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
255
|
-
except FileNotFoundError:
|
|
256
|
-
pass
|
|
257
336
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
258
337
|
written_bytes = 0
|
|
259
338
|
while True:
|
|
@@ -272,7 +351,7 @@ def copy_path(root_fd, payload):
|
|
|
272
351
|
os.fsync(temp_fd)
|
|
273
352
|
os.close(temp_fd)
|
|
274
353
|
temp_fd = None
|
|
275
|
-
|
|
354
|
+
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
276
355
|
temp_name = None
|
|
277
356
|
os.fsync(parent_fd)
|
|
278
357
|
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
@@ -396,6 +475,9 @@ function mapWorkerError(response) {
|
|
|
396
475
|
if (message.includes("fs-safe-source-mismatch")) {
|
|
397
476
|
return new FsSafeError("path-mismatch", "source path changed during copy");
|
|
398
477
|
}
|
|
478
|
+
if (message.includes("fs-safe-directory-noreplace-unsupported")) {
|
|
479
|
+
return new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
480
|
+
}
|
|
399
481
|
if (code === "FileNotFoundError" || errno === 2) {
|
|
400
482
|
return new FsSafeError("not-found", "file not found");
|
|
401
483
|
}
|
|
@@ -413,19 +495,19 @@ function mapWorkerError(response) {
|
|
|
413
495
|
}
|
|
414
496
|
return new FsSafeError("helper-failed", message);
|
|
415
497
|
}
|
|
416
|
-
function rejectPending(error) {
|
|
417
|
-
if (!worker) {
|
|
498
|
+
function rejectPending(error, targetWorker = worker) {
|
|
499
|
+
if (!targetWorker || worker !== targetWorker) {
|
|
418
500
|
return;
|
|
419
501
|
}
|
|
420
|
-
setWorkerRef(
|
|
421
|
-
for (const pending of
|
|
502
|
+
setWorkerRef(targetWorker, false);
|
|
503
|
+
for (const pending of targetWorker.pending.values()) {
|
|
422
504
|
pending.reject(error);
|
|
423
505
|
}
|
|
424
|
-
|
|
506
|
+
targetWorker.pending.clear();
|
|
425
507
|
worker = null;
|
|
426
508
|
}
|
|
427
|
-
function handleWorkerLine(line) {
|
|
428
|
-
if (
|
|
509
|
+
function handleWorkerLine(currentWorker, line) {
|
|
510
|
+
if (worker !== currentWorker || !line.trim()) {
|
|
429
511
|
return;
|
|
430
512
|
}
|
|
431
513
|
let decoded;
|
|
@@ -433,11 +515,11 @@ function handleWorkerLine(line) {
|
|
|
433
515
|
decoded = JSON.parse(line);
|
|
434
516
|
}
|
|
435
517
|
catch {
|
|
436
|
-
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
|
|
518
|
+
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
|
|
437
519
|
return;
|
|
438
520
|
}
|
|
439
521
|
if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
|
|
440
|
-
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"));
|
|
522
|
+
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
|
|
441
523
|
return;
|
|
442
524
|
}
|
|
443
525
|
const response = decoded;
|
|
@@ -445,13 +527,13 @@ function handleWorkerLine(line) {
|
|
|
445
527
|
if (id === undefined) {
|
|
446
528
|
return;
|
|
447
529
|
}
|
|
448
|
-
const pending =
|
|
530
|
+
const pending = currentWorker.pending.get(id);
|
|
449
531
|
if (!pending) {
|
|
450
532
|
return;
|
|
451
533
|
}
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
setWorkerRef(
|
|
534
|
+
currentWorker.pending.delete(id);
|
|
535
|
+
if (currentWorker.pending.size === 0) {
|
|
536
|
+
setWorkerRef(currentWorker, false);
|
|
455
537
|
}
|
|
456
538
|
if (response.ok === true) {
|
|
457
539
|
pending.resolve(response.result);
|
|
@@ -467,28 +549,28 @@ function getWorker() {
|
|
|
467
549
|
const child = spawn(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
|
|
468
550
|
stdio: ["pipe", "pipe", "pipe"],
|
|
469
551
|
});
|
|
470
|
-
|
|
552
|
+
const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
|
|
553
|
+
worker = currentWorker;
|
|
471
554
|
child.stdout.setEncoding("utf8");
|
|
472
555
|
child.stderr.setEncoding("utf8");
|
|
473
556
|
child.stdout.on("data", (chunk) => {
|
|
474
|
-
|
|
475
|
-
if (!current) {
|
|
557
|
+
if (worker !== currentWorker) {
|
|
476
558
|
return;
|
|
477
559
|
}
|
|
478
|
-
|
|
560
|
+
currentWorker.stdoutBuffer += chunk;
|
|
479
561
|
for (;;) {
|
|
480
|
-
const newline =
|
|
562
|
+
const newline = currentWorker.stdoutBuffer.indexOf("\n");
|
|
481
563
|
if (newline < 0) {
|
|
482
564
|
break;
|
|
483
565
|
}
|
|
484
|
-
const line =
|
|
485
|
-
|
|
486
|
-
handleWorkerLine(line);
|
|
566
|
+
const line = currentWorker.stdoutBuffer.slice(0, newline);
|
|
567
|
+
currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
|
|
568
|
+
handleWorkerLine(currentWorker, line);
|
|
487
569
|
}
|
|
488
570
|
});
|
|
489
571
|
child.stderr.on("data", (chunk) => {
|
|
490
|
-
if (worker) {
|
|
491
|
-
|
|
572
|
+
if (worker === currentWorker) {
|
|
573
|
+
currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
|
|
492
574
|
}
|
|
493
575
|
});
|
|
494
576
|
child.once("error", (error) => {
|
|
@@ -497,17 +579,17 @@ function getWorker() {
|
|
|
497
579
|
: error instanceof Error
|
|
498
580
|
? error
|
|
499
581
|
: new Error(String(error));
|
|
500
|
-
rejectPending(mapped);
|
|
582
|
+
rejectPending(mapped, currentWorker);
|
|
501
583
|
});
|
|
502
584
|
child.once("close", (code, signal) => {
|
|
503
|
-
const stderr =
|
|
504
|
-
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
|
|
585
|
+
const stderr = currentWorker.stderr.trim();
|
|
586
|
+
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
|
|
505
587
|
});
|
|
506
588
|
process.once("exit", () => {
|
|
507
589
|
child.kill("SIGTERM");
|
|
508
590
|
});
|
|
509
|
-
setWorkerRef(
|
|
510
|
-
return
|
|
591
|
+
setWorkerRef(currentWorker, false);
|
|
592
|
+
return currentWorker;
|
|
511
593
|
}
|
|
512
594
|
function setRefable(value, ref) {
|
|
513
595
|
if (!value) {
|
package/dist/root-impl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
|
package/dist/root-impl.js
CHANGED
|
@@ -20,6 +20,7 @@ import { resolveRootPath } from "./root-path.js";
|
|
|
20
20
|
import { assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
|
|
21
21
|
import { isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, } from "./root-errors.js";
|
|
22
22
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
23
|
+
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
23
24
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
24
25
|
import { serializePathWrite } from "./write-queue.js";
|
|
25
26
|
function logWarn(message) {
|
|
@@ -260,12 +261,12 @@ class RootHandle {
|
|
|
260
261
|
}
|
|
261
262
|
async writeJson(relativePath, data, options = {}) {
|
|
262
263
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
263
|
-
const json =
|
|
264
|
+
const json = stringifyJsonDocument(data, replacer, space);
|
|
264
265
|
await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
265
266
|
}
|
|
266
267
|
async createJson(relativePath, data, options = {}) {
|
|
267
268
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
268
|
-
const json =
|
|
269
|
+
const json = stringifyJsonDocument(data, replacer, space);
|
|
269
270
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
270
271
|
}
|
|
271
272
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
@@ -1050,6 +1051,9 @@ async function movePathFallback(root, params) {
|
|
|
1050
1051
|
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
1051
1052
|
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
1052
1053
|
}
|
|
1054
|
+
if (!params.overwrite && sourceStat.isDirectory()) {
|
|
1055
|
+
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
1056
|
+
}
|
|
1053
1057
|
if (!params.overwrite) {
|
|
1054
1058
|
try {
|
|
1055
1059
|
await fs.lstat(target.resolved);
|
package/dist/root-paths.js
CHANGED
|
@@ -62,7 +62,7 @@ export function resolvePathWithinRoot(params) {
|
|
|
62
62
|
}
|
|
63
63
|
const resolved = path.resolve(root, raw);
|
|
64
64
|
const rel = path.relative(root, resolved);
|
|
65
|
-
if (!rel || rel.startsWith(
|
|
65
|
+
if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
66
66
|
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
67
67
|
}
|
|
68
68
|
return { ok: true, path: resolved };
|
|
@@ -118,7 +118,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
118
118
|
}
|
|
119
119
|
async function assertNoSymlinkSegments(params) {
|
|
120
120
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
121
|
-
if (relative.startsWith(
|
|
121
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
122
122
|
throw new Error(`Invalid path: must stay within ${params.scopeLabel}`);
|
|
123
123
|
}
|
|
124
124
|
let current = params.rootDir;
|
|
@@ -266,7 +266,10 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
266
266
|
const rootDir = path.resolve(params.rootDir);
|
|
267
267
|
const rootRealPath = await resolveRealPathIfExists(rootDir);
|
|
268
268
|
const root = rootRealPath ? await openRoot(rootDir) : undefined;
|
|
269
|
-
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
269
|
+
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
270
|
+
relativePath !== ".." &&
|
|
271
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
272
|
+
!path.isAbsolute(relativePath);
|
|
270
273
|
const resolveExistingRelativePath = async (requestedPath) => {
|
|
271
274
|
const raw = requestedPath.trim();
|
|
272
275
|
const lexicalPathResult = resolvePathWithinRoot({
|
|
@@ -316,6 +319,25 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
316
319
|
}
|
|
317
320
|
catch (err) {
|
|
318
321
|
if (allowMissingFallback && err instanceof FsSafeError && err.code === "not-found") {
|
|
322
|
+
if (!rootRealPath) {
|
|
323
|
+
resolvedPaths.push(pathResult.fallbackPath);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await assertNoSymlinkSegments({
|
|
328
|
+
rootDir,
|
|
329
|
+
targetPath: pathResult.fallbackPath,
|
|
330
|
+
scopeLabel: params.scopeLabel,
|
|
331
|
+
});
|
|
332
|
+
const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
|
|
333
|
+
const existingRealPath = await fs.realpath(existingPath);
|
|
334
|
+
if (!isPathInside(rootRealPath, existingRealPath)) {
|
|
335
|
+
return invalidPath(params.scopeLabel);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return invalidPath(params.scopeLabel);
|
|
340
|
+
}
|
|
319
341
|
resolvedPaths.push(pathResult.fallbackPath);
|
|
320
342
|
continue;
|
|
321
343
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAsKF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAa/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -9,7 +9,9 @@ import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadab
|
|
|
9
9
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
10
10
|
const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
11
11
|
function isAbsolutePathname(value) {
|
|
12
|
-
return path.isAbsolute(value) ||
|
|
12
|
+
return (path.isAbsolute(value) ||
|
|
13
|
+
(process.platform === "win32" &&
|
|
14
|
+
(isWindowsDriveLetterPath(value, "win32") || isWindowsNetworkPath(value, "win32"))));
|
|
13
15
|
}
|
|
14
16
|
function label(options) {
|
|
15
17
|
return options.label ?? "Secure file";
|
package/dist/timing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
|
package/dist/timing.js
CHANGED
|
@@ -12,7 +12,14 @@ export async function withTimeout(promise, timeoutMs, labelOrOptions = {
|
|
|
12
12
|
return await Promise.race([
|
|
13
13
|
promise,
|
|
14
14
|
new Promise((_, reject) => {
|
|
15
|
-
timeoutId = setTimeout(() =>
|
|
15
|
+
timeoutId = setTimeout(() => {
|
|
16
|
+
try {
|
|
17
|
+
reject(createError());
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
reject(error);
|
|
21
|
+
}
|
|
22
|
+
}, timeoutMs);
|
|
16
23
|
}),
|
|
17
24
|
]);
|
|
18
25
|
}
|
package/docs/index.md
CHANGED
|
@@ -64,7 +64,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
64
64
|
| [`extractArchive`](archive.md) | ZIP/TAR extraction with size, count, link, and traversal limits. |
|
|
65
65
|
| [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
|
|
66
66
|
| [Permissions](permissions.md) | POSIX mode and Windows ACL inspection/remediation helpers. |
|
|
67
|
-
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry
|
|
67
|
+
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry, fail-closed stale-lock handling, and opt-in caller-approved stale removal. |
|
|
68
68
|
| [`FsSafeError`](errors.md) | Closed code union (with `policy` / `operational` category) you can branch on. |
|
|
69
69
|
| [`pathScope()`](path-scope.md) | Lower-level absolute-path boundary helper; lives behind `@openclaw/fs-safe/advanced`. |
|
|
70
70
|
| [`@openclaw/fs-safe/advanced`](advanced.md) | Directory of lower-level composition helpers (path scopes, regular-file I/O, install paths, sibling-temp writes, …). |
|
package/docs/json-store.md
CHANGED
|
@@ -53,7 +53,7 @@ type JsonStoreLockOptions = {
|
|
|
53
53
|
staleMs?: number; // default 30_000
|
|
54
54
|
timeoutMs?: number; // default 30_000
|
|
55
55
|
retry?: FileLockRetryOptions;
|
|
56
|
-
staleRecovery?: "fail-closed";
|
|
56
|
+
staleRecovery?: "fail-closed" | "remove-if-unchanged";
|
|
57
57
|
managerKey?: string; // default `fs-safe.json-store:<filePath>`
|
|
58
58
|
};
|
|
59
59
|
|
|
@@ -141,6 +141,8 @@ When `lock` is falsy, `read` / `write` / `update` are unlocked. The `update` sha
|
|
|
141
141
|
|
|
142
142
|
Process-wide lock defaults from `configureFsSafeLocks()` apply only after locking is explicitly enabled. They do not make JSON stores lock by default.
|
|
143
143
|
|
|
144
|
+
JSON store locks do not expose `shouldRemoveStaleLock`, so `staleRecovery: "remove-if-unchanged"` cannot remove a stale sidecar by itself. Use the lower-level [file lock](sidecar-lock.md) API when your application needs custom owner-liveness checks and caller-approved stale-lock removal.
|
|
145
|
+
|
|
144
146
|
The default `managerKey` namespaces the in-process `FileLockManager` per absolute file path, so two `jsonStore` calls on the same file share lock state automatically.
|
|
145
147
|
|
|
146
148
|
## Common patterns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
},
|
|
100
100
|
"scripts": {
|
|
101
101
|
"benchmark": "node scripts/benchmark.mjs",
|
|
102
|
-
"build": "tsc -p tsconfig.json",
|
|
102
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
103
103
|
"lint:file-size": "node scripts/check-file-size.mjs",
|
|
104
104
|
"lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
|
|
105
105
|
"prepack": "node scripts/prepack-build.mjs",
|
|
@@ -115,9 +115,9 @@
|
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
117
|
"@types/node": "^22.15.19",
|
|
118
|
-
"@vitest/coverage-v8": "
|
|
118
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
119
119
|
"typescript": "^5.8.3",
|
|
120
|
-
"vitest": "^
|
|
120
|
+
"vitest": "^4.1.6"
|
|
121
121
|
},
|
|
122
122
|
"engines": {
|
|
123
123
|
"node": ">=20.11"
|
package/dist/archive-utils.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"archive-utils.d.ts","sourceRoot":"","sources":["../src/archive-utils.ts"],"names":[],"mappings":"AAEA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE"}
|
package/dist/archive-utils.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
export async function withTimeout(promise, timeoutMs, label) {
|
|
3
|
-
let timeoutId;
|
|
4
|
-
try {
|
|
5
|
-
return await Promise.race([
|
|
6
|
-
promise,
|
|
7
|
-
new Promise((_, reject) => {
|
|
8
|
-
timeoutId = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
9
|
-
}),
|
|
10
|
-
]);
|
|
11
|
-
}
|
|
12
|
-
finally {
|
|
13
|
-
if (timeoutId) {
|
|
14
|
-
clearTimeout(timeoutId);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export async function fileExists(filePath) {
|
|
19
|
-
try {
|
|
20
|
-
await fs.stat(filePath);
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import type { PathAliasPolicy } from "./path-alias-guards.js";
|
|
3
|
-
import { type SafeOpenSyncAllowedType, type SafeOpenSyncFailureReason } from "./safe-open-sync.js";
|
|
4
|
-
type BoundaryReadFs = Pick<typeof fs, "closeSync" | "constants" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "realpathSync">;
|
|
5
|
-
export type BoundaryFileOpenFailureReason = SafeOpenSyncFailureReason | "validation";
|
|
6
|
-
export type BoundaryFileOpenResult = {
|
|
7
|
-
ok: true;
|
|
8
|
-
path: string;
|
|
9
|
-
fd: number;
|
|
10
|
-
stat: fs.Stats;
|
|
11
|
-
rootRealPath: string;
|
|
12
|
-
} | {
|
|
13
|
-
ok: false;
|
|
14
|
-
reason: BoundaryFileOpenFailureReason;
|
|
15
|
-
error?: unknown;
|
|
16
|
-
};
|
|
17
|
-
export type BoundaryFileOpenFailure = Extract<BoundaryFileOpenResult, {
|
|
18
|
-
ok: false;
|
|
19
|
-
}>;
|
|
20
|
-
export type OpenBoundaryFileSyncParams = {
|
|
21
|
-
absolutePath: string;
|
|
22
|
-
rootPath: string;
|
|
23
|
-
boundaryLabel: string;
|
|
24
|
-
rootRealPath?: string;
|
|
25
|
-
maxBytes?: number;
|
|
26
|
-
rejectHardlinks?: boolean;
|
|
27
|
-
allowedType?: SafeOpenSyncAllowedType;
|
|
28
|
-
skipLexicalRootCheck?: boolean;
|
|
29
|
-
ioFs?: BoundaryReadFs;
|
|
30
|
-
};
|
|
31
|
-
export type OpenBoundaryFileParams = OpenBoundaryFileSyncParams & {
|
|
32
|
-
aliasPolicy?: PathAliasPolicy;
|
|
33
|
-
};
|
|
34
|
-
export declare function canUseBoundaryFileOpen(ioFs: typeof fs): boolean;
|
|
35
|
-
export declare function openBoundaryFileSync(params: OpenBoundaryFileSyncParams): BoundaryFileOpenResult;
|
|
36
|
-
export declare function matchBoundaryFileOpenFailure<T>(failure: BoundaryFileOpenFailure, handlers: {
|
|
37
|
-
path?: (failure: BoundaryFileOpenFailure) => T;
|
|
38
|
-
validation?: (failure: BoundaryFileOpenFailure) => T;
|
|
39
|
-
io?: (failure: BoundaryFileOpenFailure) => T;
|
|
40
|
-
fallback: (failure: BoundaryFileOpenFailure) => T;
|
|
41
|
-
}): T;
|
|
42
|
-
export declare function openBoundaryFile(params: OpenBoundaryFileParams): Promise<BoundaryFileOpenResult>;
|
|
43
|
-
export {};
|
|
44
|
-
//# sourceMappingURL=boundary-file-read.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"boundary-file-read.d.ts","sourceRoot":"","sources":["../src/boundary-file-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAE7B,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,EAAE,EACP,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,cAAc,GACd,cAAc,CACjB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,yBAAyB,GAAG,YAAY,CAAC;AAErF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,6BAA6B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1E,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAErF,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,0BAA0B,GAAG;IAChE,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAQF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW/D;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,sBAAsB,CAuB/F;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC/C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IACrD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC7C,QAAQ,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;CACnD,GACA,CAAC,CAUH;AAoDD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAsBjC"}
|