@openclaw/fs-safe 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
5
|
+
const PINNED_PYTHON_WORKER_SOURCE = String.raw `
|
|
6
|
+
import base64
|
|
7
|
+
import errno
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import secrets
|
|
11
|
+
import stat
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
DIR_FLAGS = os.O_RDONLY
|
|
15
|
+
if hasattr(os, "O_DIRECTORY"):
|
|
16
|
+
DIR_FLAGS |= os.O_DIRECTORY
|
|
17
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
18
|
+
DIR_FLAGS |= os.O_NOFOLLOW
|
|
19
|
+
READ_FLAGS = os.O_RDONLY
|
|
20
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
21
|
+
READ_FLAGS |= os.O_NOFOLLOW
|
|
22
|
+
WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
23
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
24
|
+
WRITE_FLAGS |= os.O_NOFOLLOW
|
|
25
|
+
|
|
26
|
+
def split_relative(value):
|
|
27
|
+
if value in ("", "."):
|
|
28
|
+
return []
|
|
29
|
+
if "\x00" in value or value.startswith("/") or value.startswith("//"):
|
|
30
|
+
raise OSError(errno.EPERM, "invalid relative path")
|
|
31
|
+
if value.startswith("..\\"):
|
|
32
|
+
raise OSError(errno.EPERM, "path traversal is not allowed")
|
|
33
|
+
parts = [part for part in value.split("/") if part and part != "."]
|
|
34
|
+
for part in parts:
|
|
35
|
+
if part == "..":
|
|
36
|
+
raise OSError(errno.EPERM, "path traversal is not allowed")
|
|
37
|
+
return parts
|
|
38
|
+
|
|
39
|
+
def open_dir(path_value, dir_fd=None):
|
|
40
|
+
return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
|
|
41
|
+
|
|
42
|
+
def walk_dir(root_fd, segments, mkdir_enabled=False):
|
|
43
|
+
current_fd = os.dup(root_fd)
|
|
44
|
+
try:
|
|
45
|
+
for segment in segments:
|
|
46
|
+
try:
|
|
47
|
+
next_fd = open_dir(segment, dir_fd=current_fd)
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
if not mkdir_enabled:
|
|
50
|
+
raise
|
|
51
|
+
os.mkdir(segment, 0o777, dir_fd=current_fd)
|
|
52
|
+
next_fd = open_dir(segment, dir_fd=current_fd)
|
|
53
|
+
os.close(current_fd)
|
|
54
|
+
current_fd = next_fd
|
|
55
|
+
return current_fd
|
|
56
|
+
except Exception:
|
|
57
|
+
os.close(current_fd)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
def parent_and_basename(root_fd, relative):
|
|
61
|
+
segments = split_relative(relative)
|
|
62
|
+
if not segments:
|
|
63
|
+
raise OSError(errno.EPERM, "operation requires a non-root path")
|
|
64
|
+
parent_fd = walk_dir(root_fd, segments[:-1])
|
|
65
|
+
return parent_fd, segments[-1]
|
|
66
|
+
|
|
67
|
+
def encode_stat(st):
|
|
68
|
+
mode = st.st_mode
|
|
69
|
+
return {
|
|
70
|
+
"dev": st.st_dev,
|
|
71
|
+
"gid": st.st_gid,
|
|
72
|
+
"ino": st.st_ino,
|
|
73
|
+
"isDirectory": stat.S_ISDIR(mode),
|
|
74
|
+
"isFile": stat.S_ISREG(mode),
|
|
75
|
+
"isSymbolicLink": stat.S_ISLNK(mode),
|
|
76
|
+
"mode": mode,
|
|
77
|
+
"mtimeMs": st.st_mtime * 1000,
|
|
78
|
+
"nlink": st.st_nlink,
|
|
79
|
+
"size": st.st_size,
|
|
80
|
+
"uid": st.st_uid,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def reject_unsafe_endpoint(st):
|
|
84
|
+
mode = st.st_mode
|
|
85
|
+
if stat.S_ISLNK(mode):
|
|
86
|
+
raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
|
|
87
|
+
if stat.S_ISREG(mode) and st.st_nlink > 1:
|
|
88
|
+
raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
|
|
89
|
+
|
|
90
|
+
def stat_path(root_fd, payload):
|
|
91
|
+
relative = payload.get("relativePath", "")
|
|
92
|
+
segments = split_relative(relative)
|
|
93
|
+
if not segments:
|
|
94
|
+
return encode_stat(os.fstat(root_fd))
|
|
95
|
+
parent_fd, basename = parent_and_basename(root_fd, relative)
|
|
96
|
+
try:
|
|
97
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
98
|
+
if payload.get("rejectSymlink", True) and stat.S_ISLNK(st.st_mode):
|
|
99
|
+
raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
|
|
100
|
+
return encode_stat(st)
|
|
101
|
+
finally:
|
|
102
|
+
os.close(parent_fd)
|
|
103
|
+
|
|
104
|
+
def readdir_path(root_fd, payload):
|
|
105
|
+
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
|
|
106
|
+
try:
|
|
107
|
+
names = sorted(os.listdir(dir_fd))
|
|
108
|
+
if not payload.get("withFileTypes", False):
|
|
109
|
+
return names
|
|
110
|
+
entries = []
|
|
111
|
+
for name in names:
|
|
112
|
+
st = os.lstat(name, dir_fd=dir_fd)
|
|
113
|
+
entry = encode_stat(st)
|
|
114
|
+
entry["name"] = name
|
|
115
|
+
entries.append(entry)
|
|
116
|
+
return entries
|
|
117
|
+
finally:
|
|
118
|
+
os.close(dir_fd)
|
|
119
|
+
|
|
120
|
+
def mkdirp_path(root_fd, payload):
|
|
121
|
+
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
|
|
122
|
+
os.close(dir_fd)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def remove_tree(parent_fd, basename):
|
|
126
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
127
|
+
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
128
|
+
dir_fd = open_dir(basename, dir_fd=parent_fd)
|
|
129
|
+
try:
|
|
130
|
+
for child in os.listdir(dir_fd):
|
|
131
|
+
remove_tree(dir_fd, child)
|
|
132
|
+
finally:
|
|
133
|
+
os.close(dir_fd)
|
|
134
|
+
os.rmdir(basename, dir_fd=parent_fd)
|
|
135
|
+
else:
|
|
136
|
+
os.unlink(basename, dir_fd=parent_fd)
|
|
137
|
+
|
|
138
|
+
def remove_path(root_fd, payload):
|
|
139
|
+
parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
|
|
140
|
+
try:
|
|
141
|
+
try:
|
|
142
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
if payload.get("force", True):
|
|
145
|
+
return None
|
|
146
|
+
raise
|
|
147
|
+
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
148
|
+
if payload.get("recursive", False):
|
|
149
|
+
remove_tree(parent_fd, basename)
|
|
150
|
+
else:
|
|
151
|
+
os.rmdir(basename, dir_fd=parent_fd)
|
|
152
|
+
else:
|
|
153
|
+
os.unlink(basename, dir_fd=parent_fd)
|
|
154
|
+
return None
|
|
155
|
+
finally:
|
|
156
|
+
os.close(parent_fd)
|
|
157
|
+
|
|
158
|
+
def rename_path(root_fd, payload):
|
|
159
|
+
from_parent_fd, from_base = parent_and_basename(root_fd, payload["from"])
|
|
160
|
+
to_parent_fd, to_base = parent_and_basename(root_fd, payload["to"])
|
|
161
|
+
try:
|
|
162
|
+
from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
|
|
163
|
+
reject_unsafe_endpoint(from_stat)
|
|
164
|
+
if not payload.get("overwrite", True):
|
|
165
|
+
try:
|
|
166
|
+
os.lstat(to_base, dir_fd=to_parent_fd)
|
|
167
|
+
raise FileExistsError(errno.EEXIST, "destination exists", to_base)
|
|
168
|
+
except FileNotFoundError:
|
|
169
|
+
pass
|
|
170
|
+
os.rename(from_base, to_base, src_dir_fd=from_parent_fd, dst_dir_fd=to_parent_fd)
|
|
171
|
+
os.fsync(from_parent_fd)
|
|
172
|
+
if from_parent_fd != to_parent_fd:
|
|
173
|
+
os.fsync(to_parent_fd)
|
|
174
|
+
return None
|
|
175
|
+
finally:
|
|
176
|
+
os.close(from_parent_fd)
|
|
177
|
+
os.close(to_parent_fd)
|
|
178
|
+
|
|
179
|
+
def create_temp_file(parent_fd, basename, mode):
|
|
180
|
+
prefix = "." + basename + "."
|
|
181
|
+
for _ in range(128):
|
|
182
|
+
candidate = prefix + secrets.token_hex(6) + ".tmp"
|
|
183
|
+
try:
|
|
184
|
+
fd = os.open(candidate, WRITE_FLAGS, mode, dir_fd=parent_fd)
|
|
185
|
+
return candidate, fd
|
|
186
|
+
except FileExistsError:
|
|
187
|
+
continue
|
|
188
|
+
raise RuntimeError("failed to allocate pinned temp file")
|
|
189
|
+
|
|
190
|
+
def write_path(root_fd, payload):
|
|
191
|
+
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
192
|
+
temp_fd = None
|
|
193
|
+
temp_name = None
|
|
194
|
+
basename = payload["basename"]
|
|
195
|
+
mode = int(payload.get("mode", 0o600))
|
|
196
|
+
overwrite = bool(payload.get("overwrite", True))
|
|
197
|
+
max_bytes = int(payload.get("maxBytes", -1))
|
|
198
|
+
data = base64.b64decode(payload.get("base64", ""))
|
|
199
|
+
try:
|
|
200
|
+
if max_bytes >= 0 and len(data) > max_bytes:
|
|
201
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, len(data)))
|
|
202
|
+
if not overwrite:
|
|
203
|
+
try:
|
|
204
|
+
os.lstat(basename, dir_fd=parent_fd)
|
|
205
|
+
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
206
|
+
except FileNotFoundError:
|
|
207
|
+
pass
|
|
208
|
+
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
209
|
+
view = memoryview(data)
|
|
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:]
|
|
215
|
+
os.fsync(temp_fd)
|
|
216
|
+
os.close(temp_fd)
|
|
217
|
+
temp_fd = None
|
|
218
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
219
|
+
temp_name = None
|
|
220
|
+
os.fsync(parent_fd)
|
|
221
|
+
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
222
|
+
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
223
|
+
finally:
|
|
224
|
+
if temp_fd is not None:
|
|
225
|
+
os.close(temp_fd)
|
|
226
|
+
if temp_name is not None:
|
|
227
|
+
try:
|
|
228
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
229
|
+
except FileNotFoundError:
|
|
230
|
+
pass
|
|
231
|
+
os.close(parent_fd)
|
|
232
|
+
|
|
233
|
+
def copy_path(root_fd, payload):
|
|
234
|
+
source_fd = os.open(payload["sourcePath"], READ_FLAGS)
|
|
235
|
+
parent_fd = None
|
|
236
|
+
temp_fd = None
|
|
237
|
+
temp_name = None
|
|
238
|
+
try:
|
|
239
|
+
source_stat = os.fstat(source_fd)
|
|
240
|
+
if not stat.S_ISREG(source_stat.st_mode):
|
|
241
|
+
raise RuntimeError("fs-safe-not-file")
|
|
242
|
+
if source_stat.st_dev != int(payload["sourceDev"]) or source_stat.st_ino != int(payload["sourceIno"]):
|
|
243
|
+
raise RuntimeError("fs-safe-source-mismatch")
|
|
244
|
+
basename = payload["basename"]
|
|
245
|
+
mode = int(payload.get("mode", 0o600))
|
|
246
|
+
overwrite = bool(payload.get("overwrite", True))
|
|
247
|
+
max_bytes = int(payload.get("maxBytes", -1))
|
|
248
|
+
if max_bytes >= 0 and source_stat.st_size > max_bytes:
|
|
249
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
250
|
+
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
|
+
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
258
|
+
written_bytes = 0
|
|
259
|
+
while True:
|
|
260
|
+
chunk = os.read(source_fd, 65536)
|
|
261
|
+
if not chunk:
|
|
262
|
+
break
|
|
263
|
+
written_bytes += len(chunk)
|
|
264
|
+
if max_bytes >= 0 and written_bytes > max_bytes:
|
|
265
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, written_bytes))
|
|
266
|
+
view = memoryview(chunk)
|
|
267
|
+
while view:
|
|
268
|
+
written = os.write(temp_fd, view)
|
|
269
|
+
if written <= 0:
|
|
270
|
+
raise OSError(errno.EIO, "short write")
|
|
271
|
+
view = view[written:]
|
|
272
|
+
os.fsync(temp_fd)
|
|
273
|
+
os.close(temp_fd)
|
|
274
|
+
temp_fd = None
|
|
275
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
276
|
+
temp_name = None
|
|
277
|
+
os.fsync(parent_fd)
|
|
278
|
+
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
279
|
+
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
280
|
+
finally:
|
|
281
|
+
os.close(source_fd)
|
|
282
|
+
if temp_fd is not None:
|
|
283
|
+
os.close(temp_fd)
|
|
284
|
+
if temp_name is not None and parent_fd is not None:
|
|
285
|
+
try:
|
|
286
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
287
|
+
except FileNotFoundError:
|
|
288
|
+
pass
|
|
289
|
+
if parent_fd is not None:
|
|
290
|
+
os.close(parent_fd)
|
|
291
|
+
|
|
292
|
+
def run_operation(operation, root_path, payload):
|
|
293
|
+
root_fd = open_dir(root_path)
|
|
294
|
+
try:
|
|
295
|
+
if operation == "stat":
|
|
296
|
+
return stat_path(root_fd, payload)
|
|
297
|
+
if operation == "readdir":
|
|
298
|
+
return readdir_path(root_fd, payload)
|
|
299
|
+
if operation == "mkdirp":
|
|
300
|
+
return mkdirp_path(root_fd, payload)
|
|
301
|
+
if operation == "remove":
|
|
302
|
+
return remove_path(root_fd, payload)
|
|
303
|
+
if operation == "rename":
|
|
304
|
+
return rename_path(root_fd, payload)
|
|
305
|
+
if operation == "write":
|
|
306
|
+
return write_path(root_fd, payload)
|
|
307
|
+
if operation == "copy":
|
|
308
|
+
return copy_path(root_fd, payload)
|
|
309
|
+
raise RuntimeError("unknown operation: " + operation)
|
|
310
|
+
finally:
|
|
311
|
+
os.close(root_fd)
|
|
312
|
+
|
|
313
|
+
for line in sys.stdin:
|
|
314
|
+
try:
|
|
315
|
+
request = json.loads(line)
|
|
316
|
+
result = run_operation(request["operation"], request["rootPath"], request.get("payload") or {})
|
|
317
|
+
response = {"id": request["id"], "ok": True, "result": result}
|
|
318
|
+
except Exception as exc:
|
|
319
|
+
response = {
|
|
320
|
+
"id": request.get("id") if isinstance(locals().get("request"), dict) else None,
|
|
321
|
+
"ok": False,
|
|
322
|
+
"code": exc.__class__.__name__,
|
|
323
|
+
"errno": getattr(exc, "errno", None),
|
|
324
|
+
"message": str(exc),
|
|
325
|
+
}
|
|
326
|
+
print(json.dumps(response, separators=(",", ":")), flush=True)
|
|
327
|
+
`;
|
|
328
|
+
let nextRequestId = 1;
|
|
329
|
+
let worker = null;
|
|
330
|
+
export function __resetPinnedPythonWorkerForTest() {
|
|
331
|
+
const currentWorker = worker;
|
|
332
|
+
worker = null;
|
|
333
|
+
if (!currentWorker) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
currentWorker.pending.clear();
|
|
337
|
+
currentWorker.child.kill("SIGTERM");
|
|
338
|
+
}
|
|
339
|
+
const PYTHON_CANDIDATE_DEFAULTS = [
|
|
340
|
+
"/usr/bin/python3",
|
|
341
|
+
"/opt/homebrew/bin/python3",
|
|
342
|
+
"/usr/local/bin/python3",
|
|
343
|
+
];
|
|
344
|
+
function canExecute(binPath) {
|
|
345
|
+
try {
|
|
346
|
+
fsSync.accessSync(binPath, fsSync.constants.X_OK);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function resolvePython() {
|
|
354
|
+
const configured = getFsSafePythonConfig().pythonPath;
|
|
355
|
+
if (configured) {
|
|
356
|
+
return configured;
|
|
357
|
+
}
|
|
358
|
+
for (const candidate of PYTHON_CANDIDATE_DEFAULTS) {
|
|
359
|
+
if (canExecute(candidate)) {
|
|
360
|
+
return candidate;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return "python3";
|
|
364
|
+
}
|
|
365
|
+
function assertPinnedHelperSupported() {
|
|
366
|
+
if (process.platform === "win32") {
|
|
367
|
+
throw new FsSafeError("unsupported-platform", "fd-relative pinned filesystem operations are not available on Windows");
|
|
368
|
+
}
|
|
369
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
370
|
+
throw new FsSafeError("helper-unavailable", "Python helper is disabled");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function isSpawnUnavailable(error) {
|
|
374
|
+
if (!(error instanceof Error)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const maybeErrno = error;
|
|
378
|
+
return (typeof maybeErrno.syscall === "string" &&
|
|
379
|
+
maybeErrno.syscall.startsWith("spawn") &&
|
|
380
|
+
["EACCES", "ENOENT", "ENOEXEC"].includes(maybeErrno.code ?? ""));
|
|
381
|
+
}
|
|
382
|
+
function mapWorkerError(response) {
|
|
383
|
+
const code = typeof response.code === "string" ? response.code : "";
|
|
384
|
+
const errno = typeof response.errno === "number" ? response.errno : undefined;
|
|
385
|
+
const message = typeof response.message === "string" && response.message
|
|
386
|
+
? response.message
|
|
387
|
+
: "pinned helper failed";
|
|
388
|
+
const tooLarge = message.match(/fs-safe-too-large:(\d+):(\d+)/);
|
|
389
|
+
if (tooLarge) {
|
|
390
|
+
const [, limit, got] = tooLarge;
|
|
391
|
+
return new FsSafeError("too-large", `file exceeds limit of ${limit} bytes (got at least ${got})`);
|
|
392
|
+
}
|
|
393
|
+
if (message.includes("fs-safe-not-file")) {
|
|
394
|
+
return new FsSafeError("not-file", "not a file");
|
|
395
|
+
}
|
|
396
|
+
if (message.includes("fs-safe-source-mismatch")) {
|
|
397
|
+
return new FsSafeError("path-mismatch", "source path changed during copy");
|
|
398
|
+
}
|
|
399
|
+
if (code === "FileNotFoundError" || errno === 2) {
|
|
400
|
+
return new FsSafeError("not-found", "file not found");
|
|
401
|
+
}
|
|
402
|
+
if (code === "FileExistsError" || errno === 17) {
|
|
403
|
+
return new FsSafeError("already-exists", message);
|
|
404
|
+
}
|
|
405
|
+
if (errno === 39) {
|
|
406
|
+
return new FsSafeError("not-empty", "directory is not empty");
|
|
407
|
+
}
|
|
408
|
+
if (errno === 1 || errno === 13 || errno === 21) {
|
|
409
|
+
return new FsSafeError("not-removable", "path is not removable under root");
|
|
410
|
+
}
|
|
411
|
+
if (code === "NotADirectoryError" || code === "OSError" || errno === 20 || errno === 40) {
|
|
412
|
+
return new FsSafeError("path-alias", message);
|
|
413
|
+
}
|
|
414
|
+
return new FsSafeError("helper-failed", message);
|
|
415
|
+
}
|
|
416
|
+
function rejectPending(error) {
|
|
417
|
+
if (!worker) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
setWorkerRef(worker, false);
|
|
421
|
+
for (const pending of worker.pending.values()) {
|
|
422
|
+
pending.reject(error);
|
|
423
|
+
}
|
|
424
|
+
worker.pending.clear();
|
|
425
|
+
worker = null;
|
|
426
|
+
}
|
|
427
|
+
function handleWorkerLine(line) {
|
|
428
|
+
if (!worker || !line.trim()) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
let decoded;
|
|
432
|
+
try {
|
|
433
|
+
decoded = JSON.parse(line);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
|
|
440
|
+
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const response = decoded;
|
|
444
|
+
const id = typeof response.id === "number" ? response.id : undefined;
|
|
445
|
+
if (id === undefined) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const pending = worker.pending.get(id);
|
|
449
|
+
if (!pending) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
worker.pending.delete(id);
|
|
453
|
+
if (worker.pending.size === 0) {
|
|
454
|
+
setWorkerRef(worker, false);
|
|
455
|
+
}
|
|
456
|
+
if (response.ok === true) {
|
|
457
|
+
pending.resolve(response.result);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
pending.reject(mapWorkerError(decoded));
|
|
461
|
+
}
|
|
462
|
+
function getWorker() {
|
|
463
|
+
assertPinnedHelperSupported();
|
|
464
|
+
if (worker) {
|
|
465
|
+
return worker;
|
|
466
|
+
}
|
|
467
|
+
const child = spawn(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
|
|
468
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
469
|
+
});
|
|
470
|
+
worker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
|
|
471
|
+
child.stdout.setEncoding("utf8");
|
|
472
|
+
child.stderr.setEncoding("utf8");
|
|
473
|
+
child.stdout.on("data", (chunk) => {
|
|
474
|
+
const current = worker;
|
|
475
|
+
if (!current) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
current.stdoutBuffer += chunk;
|
|
479
|
+
for (;;) {
|
|
480
|
+
const newline = current.stdoutBuffer.indexOf("\n");
|
|
481
|
+
if (newline < 0) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const line = current.stdoutBuffer.slice(0, newline);
|
|
485
|
+
current.stdoutBuffer = current.stdoutBuffer.slice(newline + 1);
|
|
486
|
+
handleWorkerLine(line);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
child.stderr.on("data", (chunk) => {
|
|
490
|
+
if (worker) {
|
|
491
|
+
worker.stderr = `${worker.stderr}${chunk}`.slice(-4096);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
child.once("error", (error) => {
|
|
495
|
+
const mapped = isSpawnUnavailable(error)
|
|
496
|
+
? new FsSafeError("helper-unavailable", "Python helper is unavailable", { cause: error })
|
|
497
|
+
: error instanceof Error
|
|
498
|
+
? error
|
|
499
|
+
: new Error(String(error));
|
|
500
|
+
rejectPending(mapped);
|
|
501
|
+
});
|
|
502
|
+
child.once("close", (code, signal) => {
|
|
503
|
+
const stderr = worker?.stderr.trim();
|
|
504
|
+
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
|
|
505
|
+
});
|
|
506
|
+
process.once("exit", () => {
|
|
507
|
+
child.kill("SIGTERM");
|
|
508
|
+
});
|
|
509
|
+
setWorkerRef(worker, false);
|
|
510
|
+
return worker;
|
|
511
|
+
}
|
|
512
|
+
function setRefable(value, ref) {
|
|
513
|
+
if (!value) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const method = ref ? "ref" : "unref";
|
|
517
|
+
const refable = value;
|
|
518
|
+
refable[method]?.();
|
|
519
|
+
}
|
|
520
|
+
function setWorkerRef(currentWorker, ref) {
|
|
521
|
+
setRefable(currentWorker.child, ref);
|
|
522
|
+
setRefable(currentWorker.child.stdin, ref);
|
|
523
|
+
setRefable(currentWorker.child.stdout, ref);
|
|
524
|
+
setRefable(currentWorker.child.stderr, ref);
|
|
525
|
+
}
|
|
526
|
+
export async function runPinnedPythonOperation(params) {
|
|
527
|
+
const requestId = nextRequestId++;
|
|
528
|
+
const currentWorker = getWorker();
|
|
529
|
+
if (typeof currentWorker.child.stdin?.write !== "function") {
|
|
530
|
+
throw new FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
|
|
531
|
+
}
|
|
532
|
+
setWorkerRef(currentWorker, true);
|
|
533
|
+
return await new Promise((resolve, reject) => {
|
|
534
|
+
currentWorker.pending.set(requestId, {
|
|
535
|
+
reject,
|
|
536
|
+
resolve: (value) => resolve(value),
|
|
537
|
+
});
|
|
538
|
+
const request = JSON.stringify({
|
|
539
|
+
id: requestId,
|
|
540
|
+
operation: params.operation,
|
|
541
|
+
rootPath: params.rootPath,
|
|
542
|
+
payload: params.payload,
|
|
543
|
+
});
|
|
544
|
+
currentWorker.child.stdin.write(`${request}\n`, (error) => {
|
|
545
|
+
if (error) {
|
|
546
|
+
currentWorker.pending.delete(requestId);
|
|
547
|
+
if (currentWorker.pending.size === 0) {
|
|
548
|
+
setWorkerRef(currentWorker, false);
|
|
549
|
+
}
|
|
550
|
+
reject(error);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
export function assertPinnedPythonOperationAvailable() {
|
|
556
|
+
const currentWorker = getWorker();
|
|
557
|
+
if (typeof currentWorker.child.stdin?.write !== "function") {
|
|
558
|
+
throw new FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
export function validatePinnedOperationPayload(payload) {
|
|
562
|
+
if (typeof payload.relativePath === "string") {
|
|
563
|
+
validatePinnedRelativePath(payload.relativePath);
|
|
564
|
+
}
|
|
565
|
+
if (typeof payload.relativeParentPath === "string") {
|
|
566
|
+
validatePinnedRelativePath(payload.relativeParentPath);
|
|
567
|
+
}
|
|
568
|
+
if (typeof payload.from === "string") {
|
|
569
|
+
validatePinnedRelativePath(payload.from);
|
|
570
|
+
}
|
|
571
|
+
if (typeof payload.to === "string") {
|
|
572
|
+
validatePinnedRelativePath(payload.to);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
export function isPinnedHelperUnavailable(error) {
|
|
576
|
+
return error instanceof Error &&
|
|
577
|
+
"code" in error &&
|
|
578
|
+
error.code === "helper-unavailable";
|
|
579
|
+
}
|
|
580
|
+
function validatePinnedRelativePath(relativePath) {
|
|
581
|
+
if (relativePath.length === 0 || relativePath === ".") {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (relativePath.includes("\0")) {
|
|
585
|
+
throw new FsSafeError("invalid-path", "relative path contains a NUL byte");
|
|
586
|
+
}
|
|
587
|
+
if (relativePath.startsWith("/") ||
|
|
588
|
+
relativePath.startsWith("//") ||
|
|
589
|
+
relativePath === ".." ||
|
|
590
|
+
relativePath.startsWith("../") ||
|
|
591
|
+
relativePath.startsWith("..\\")) {
|
|
592
|
+
throw new FsSafeError("invalid-path", "relative path must not escape root");
|
|
593
|
+
}
|
|
594
|
+
for (const segment of relativePath.split("/")) {
|
|
595
|
+
if (segment === "..") {
|
|
596
|
+
throw new FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Readable } from "node:stream";
|
|
2
|
+
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
+
type PinnedWriteInput = {
|
|
4
|
+
kind: "buffer";
|
|
5
|
+
data: string | Buffer;
|
|
6
|
+
encoding?: BufferEncoding;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "stream";
|
|
9
|
+
stream: Readable;
|
|
10
|
+
};
|
|
11
|
+
export declare function runPinnedWriteHelper(params: {
|
|
12
|
+
rootPath: string;
|
|
13
|
+
relativeParentPath: string;
|
|
14
|
+
basename: string;
|
|
15
|
+
mkdir: boolean;
|
|
16
|
+
mode: number;
|
|
17
|
+
overwrite?: boolean;
|
|
18
|
+
maxBytes?: number;
|
|
19
|
+
input: PinnedWriteInput;
|
|
20
|
+
}): Promise<FileIdentityStat>;
|
|
21
|
+
export declare function runPinnedCopyHelper(params: {
|
|
22
|
+
rootPath: string;
|
|
23
|
+
relativeParentPath: string;
|
|
24
|
+
basename: string;
|
|
25
|
+
mkdir: boolean;
|
|
26
|
+
mode: number;
|
|
27
|
+
overwrite?: boolean;
|
|
28
|
+
maxBytes?: number;
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
sourceIdentity: FileIdentityStat;
|
|
31
|
+
}): Promise<FileIdentityStat>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=pinned-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAQ3D,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAuFzC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB5B"}
|