@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,325 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import syncFs from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
6
|
+
import { serializePathWrite } from "./write-queue.js";
|
|
7
|
+
function isRetryableRenameError(error) {
|
|
8
|
+
return error.code === "EBUSY";
|
|
9
|
+
}
|
|
10
|
+
function isPermissionRenameError(error) {
|
|
11
|
+
const code = error.code;
|
|
12
|
+
return code === "EPERM" || code === "EEXIST";
|
|
13
|
+
}
|
|
14
|
+
async function sleep(ms) {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
async function renameWithRetry(params) {
|
|
18
|
+
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
await params.fsModule.rename(params.src, params.dest);
|
|
21
|
+
return { method: "rename" };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (isRetryableRenameError(error) && attempt < params.maxRetries) {
|
|
25
|
+
await sleep(params.baseDelayMs * 2 ** attempt);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (params.copyFallbackOnPermissionError && isPermissionRenameError(error)) {
|
|
29
|
+
const stat = await params.fsModule.lstat(params.dest).catch((lstatError) => {
|
|
30
|
+
if (lstatError.code === "ENOENT") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw lstatError;
|
|
34
|
+
});
|
|
35
|
+
if (stat?.isSymbolicLink()) {
|
|
36
|
+
throw new Error(`Refusing copy fallback through symlink destination: ${params.dest}`);
|
|
37
|
+
}
|
|
38
|
+
await params.fsModule.copyFile(params.src, params.dest);
|
|
39
|
+
await params.fsModule.unlink(params.src).catch(() => undefined);
|
|
40
|
+
return { method: "copy-fallback" };
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error("Atomic rename retry loop exhausted.");
|
|
46
|
+
}
|
|
47
|
+
function sleepSync(ms) {
|
|
48
|
+
if (ms <= 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
52
|
+
}
|
|
53
|
+
function renameWithRetrySync(params) {
|
|
54
|
+
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
params.fsModule.renameSync(params.src, params.dest);
|
|
57
|
+
return { method: "rename" };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isRetryableRenameError(error) && attempt < params.maxRetries) {
|
|
61
|
+
sleepSync(params.baseDelayMs * 2 ** attempt);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (params.copyFallbackOnPermissionError && isPermissionRenameError(error)) {
|
|
65
|
+
let stat = null;
|
|
66
|
+
try {
|
|
67
|
+
stat = params.fsModule.lstatSync(params.dest);
|
|
68
|
+
}
|
|
69
|
+
catch (lstatError) {
|
|
70
|
+
if (lstatError.code !== "ENOENT") {
|
|
71
|
+
throw lstatError;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (stat?.isSymbolicLink()) {
|
|
75
|
+
throw new Error(`Refusing copy fallback through symlink destination: ${params.dest}`);
|
|
76
|
+
}
|
|
77
|
+
params.fsModule.copyFileSync(params.src, params.dest);
|
|
78
|
+
try {
|
|
79
|
+
params.fsModule.unlinkSync(params.src);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Best-effort cleanup after fallback replacement.
|
|
83
|
+
}
|
|
84
|
+
return { method: "copy-fallback" };
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error("Atomic rename retry loop exhausted.");
|
|
90
|
+
}
|
|
91
|
+
function validateReplaceFilePath(filePath) {
|
|
92
|
+
if (!filePath || filePath.includes("\0")) {
|
|
93
|
+
throw new Error("Atomic replace file path must be non-empty.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function buildReplaceTempPath(filePath, tempPrefix) {
|
|
97
|
+
const dir = path.dirname(filePath);
|
|
98
|
+
return path.join(dir, `${tempPrefix ?? ".fs-safe-replace"}.${process.pid}.${randomUUID()}.tmp`);
|
|
99
|
+
}
|
|
100
|
+
async function resolveMode(options) {
|
|
101
|
+
const defaultMode = options.mode ?? 0o600;
|
|
102
|
+
if (!options.preserveExistingMode) {
|
|
103
|
+
return defaultMode;
|
|
104
|
+
}
|
|
105
|
+
const stat = await (options.fileSystem?.promises ?? fs).stat(options.filePath).catch((error) => {
|
|
106
|
+
if (error.code === "ENOENT") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
return stat ? stat.mode : defaultMode;
|
|
112
|
+
}
|
|
113
|
+
function resolveModeSync(options) {
|
|
114
|
+
const defaultMode = options.mode ?? 0o600;
|
|
115
|
+
if (!options.preserveExistingMode) {
|
|
116
|
+
return defaultMode;
|
|
117
|
+
}
|
|
118
|
+
const fsModule = options.fileSystem ?? syncFs;
|
|
119
|
+
let stat;
|
|
120
|
+
try {
|
|
121
|
+
stat = fsModule.statSync(options.filePath);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code !== "ENOENT") {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return stat ? stat.mode : defaultMode;
|
|
129
|
+
}
|
|
130
|
+
async function syncTempFile(fsModule, tempPath) {
|
|
131
|
+
const handle = await fsModule.open(tempPath, "r+");
|
|
132
|
+
try {
|
|
133
|
+
await handle.sync();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error.code !== "EPERM") {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await handle.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function syncTempFileSync(fsModule, tempPath) {
|
|
145
|
+
const fd = fsModule.openSync(tempPath, "r+");
|
|
146
|
+
try {
|
|
147
|
+
fsModule.fsyncSync(fd);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error.code !== "EPERM") {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
fsModule.closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function syncDirectoryBestEffort(fsModule, dirPath) {
|
|
159
|
+
let handle;
|
|
160
|
+
try {
|
|
161
|
+
handle = await fsModule.open(dirPath, "r");
|
|
162
|
+
await handle.sync();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Best-effort on platforms/filesystems that do not support directory fsync.
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await handle?.close().catch(() => undefined);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function syncDirectoryBestEffortSync(fsModule, dirPath) {
|
|
172
|
+
let fd;
|
|
173
|
+
try {
|
|
174
|
+
fd = fsModule.openSync(dirPath, "r");
|
|
175
|
+
fsModule.fsyncSync(fd);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Best-effort on platforms/filesystems that do not support directory fsync.
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
if (fd !== undefined) {
|
|
182
|
+
try {
|
|
183
|
+
fsModule.closeSync(fd);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Best-effort close after directory fsync.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function cleanupTempFile(params) {
|
|
192
|
+
const cleanupError = await params.fsModule
|
|
193
|
+
.rm(params.tempPath, { force: true })
|
|
194
|
+
.catch((error) => error);
|
|
195
|
+
if (cleanupError && params.throwOnCleanupError && params.originalError !== undefined) {
|
|
196
|
+
throw new Error(`Atomic file replace failed (${String(params.originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: params.originalError });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export async function replaceFileAtomic(options) {
|
|
200
|
+
const filePath = options.filePath;
|
|
201
|
+
validateReplaceFilePath(filePath);
|
|
202
|
+
return await serializePathWrite(path.resolve(filePath), async () => {
|
|
203
|
+
return await replaceFileAtomicUnserialized(options);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function replaceFileAtomicUnserialized(options) {
|
|
207
|
+
const filePath = options.filePath;
|
|
208
|
+
const fsModule = options.fileSystem?.promises ?? fs;
|
|
209
|
+
const dir = path.dirname(filePath);
|
|
210
|
+
const dirMode = options.dirMode ?? 0o700;
|
|
211
|
+
const mode = await resolveMode(options);
|
|
212
|
+
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
213
|
+
const unregisterTempPath = registerTempPathForExit(tempPath);
|
|
214
|
+
let tempExists = false;
|
|
215
|
+
let originalError;
|
|
216
|
+
await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
|
|
217
|
+
await fsModule.chmod(dir, dirMode).catch(() => undefined);
|
|
218
|
+
try {
|
|
219
|
+
tempExists = true;
|
|
220
|
+
await fsModule.writeFile(tempPath, options.content, { mode, flag: "wx" });
|
|
221
|
+
if (options.syncTempFile) {
|
|
222
|
+
await syncTempFile(fsModule, tempPath);
|
|
223
|
+
}
|
|
224
|
+
if (options.beforeRename) {
|
|
225
|
+
await options.beforeRename({ filePath, tempPath });
|
|
226
|
+
}
|
|
227
|
+
const result = await renameWithRetry({
|
|
228
|
+
fsModule,
|
|
229
|
+
src: tempPath,
|
|
230
|
+
dest: filePath,
|
|
231
|
+
maxRetries: options.renameMaxRetries ?? 0,
|
|
232
|
+
baseDelayMs: options.renameRetryBaseDelayMs ?? 50,
|
|
233
|
+
copyFallbackOnPermissionError: options.copyFallbackOnPermissionError === true,
|
|
234
|
+
});
|
|
235
|
+
tempExists = false;
|
|
236
|
+
unregisterTempPath();
|
|
237
|
+
await fsModule.chmod(filePath, mode).catch(() => undefined);
|
|
238
|
+
if (options.syncParentDir) {
|
|
239
|
+
await syncDirectoryBestEffort(fsModule, dir);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
originalError = error;
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (tempExists) {
|
|
249
|
+
await cleanupTempFile({
|
|
250
|
+
fsModule,
|
|
251
|
+
tempPath,
|
|
252
|
+
originalError,
|
|
253
|
+
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
unregisterTempPath();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export function replaceFileAtomicSync(options) {
|
|
260
|
+
const filePath = options.filePath;
|
|
261
|
+
validateReplaceFilePath(filePath);
|
|
262
|
+
const fsModule = options.fileSystem ?? syncFs;
|
|
263
|
+
const dir = path.dirname(filePath);
|
|
264
|
+
const dirMode = options.dirMode ?? 0o700;
|
|
265
|
+
const mode = resolveModeSync(options);
|
|
266
|
+
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
267
|
+
const unregisterTempPath = registerTempPathForExit(tempPath);
|
|
268
|
+
let tempExists = false;
|
|
269
|
+
let originalError;
|
|
270
|
+
fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
271
|
+
try {
|
|
272
|
+
fsModule.chmodSync(dir, dirMode);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
tempExists = true;
|
|
279
|
+
fsModule.writeFileSync(tempPath, options.content, { mode, flag: "wx" });
|
|
280
|
+
if (options.syncTempFile) {
|
|
281
|
+
syncTempFileSync(fsModule, tempPath);
|
|
282
|
+
}
|
|
283
|
+
if (options.beforeRename) {
|
|
284
|
+
options.beforeRename({ filePath, tempPath });
|
|
285
|
+
}
|
|
286
|
+
const result = renameWithRetrySync({
|
|
287
|
+
fsModule,
|
|
288
|
+
src: tempPath,
|
|
289
|
+
dest: filePath,
|
|
290
|
+
maxRetries: options.renameMaxRetries ?? 0,
|
|
291
|
+
baseDelayMs: options.renameRetryBaseDelayMs ?? 50,
|
|
292
|
+
copyFallbackOnPermissionError: options.copyFallbackOnPermissionError === true,
|
|
293
|
+
});
|
|
294
|
+
tempExists = false;
|
|
295
|
+
unregisterTempPath();
|
|
296
|
+
try {
|
|
297
|
+
fsModule.chmodSync(filePath, mode);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
301
|
+
}
|
|
302
|
+
if (options.syncParentDir) {
|
|
303
|
+
syncDirectoryBestEffortSync(fsModule, dir);
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
originalError = error;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
if (tempExists) {
|
|
313
|
+
try {
|
|
314
|
+
fsModule.rmSync(tempPath, { force: true });
|
|
315
|
+
}
|
|
316
|
+
catch (cleanupError) {
|
|
317
|
+
if (options.throwOnCleanupError && originalError !== undefined) {
|
|
318
|
+
throw new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
|
|
319
|
+
}
|
|
320
|
+
// The temp file is best-effort cleanup after write failure.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
unregisterTempPath();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type { PathAliasPolicy } from "./path-policy.js";
|
|
3
|
+
import { type PinnedOpenSyncAllowedType, type PinnedOpenSyncFailureReason } from "./pinned-open.js";
|
|
4
|
+
type BoundaryReadFs = Pick<typeof fs, "closeSync" | "constants" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "realpathSync">;
|
|
5
|
+
export type RootFileOpenFailureReason = PinnedOpenSyncFailureReason | "validation";
|
|
6
|
+
export type RootFileOpenResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
path: string;
|
|
9
|
+
fd: number;
|
|
10
|
+
stat: fs.Stats;
|
|
11
|
+
rootRealPath: string;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: RootFileOpenFailureReason;
|
|
15
|
+
error?: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type RootFileOpenFailure = Extract<RootFileOpenResult, {
|
|
18
|
+
ok: false;
|
|
19
|
+
}>;
|
|
20
|
+
export type OpenRootFileSyncParams = {
|
|
21
|
+
absolutePath: string;
|
|
22
|
+
rootPath: string;
|
|
23
|
+
boundaryLabel: string;
|
|
24
|
+
rootRealPath?: string;
|
|
25
|
+
maxBytes?: number;
|
|
26
|
+
rejectHardlinks?: boolean;
|
|
27
|
+
allowedType?: PinnedOpenSyncAllowedType;
|
|
28
|
+
skipLexicalRootCheck?: boolean;
|
|
29
|
+
ioFs?: BoundaryReadFs;
|
|
30
|
+
};
|
|
31
|
+
export type OpenRootFileParams = OpenRootFileSyncParams & {
|
|
32
|
+
aliasPolicy?: PathAliasPolicy;
|
|
33
|
+
};
|
|
34
|
+
export declare function canUseRootFileOpen(ioFs: typeof fs): boolean;
|
|
35
|
+
export declare function openRootFileSync(params: OpenRootFileSyncParams): RootFileOpenResult;
|
|
36
|
+
export declare function matchRootFileOpenFailure<T>(failure: RootFileOpenFailure, handlers: {
|
|
37
|
+
path?: (failure: RootFileOpenFailure) => T;
|
|
38
|
+
validation?: (failure: RootFileOpenFailure) => T;
|
|
39
|
+
io?: (failure: RootFileOpenFailure) => T;
|
|
40
|
+
fallback: (failure: RootFileOpenFailure) => T;
|
|
41
|
+
}): T;
|
|
42
|
+
export declare function openRootFile(params: OpenRootFileParams): Promise<RootFileOpenResult>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=root-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-file.d.ts","sourceRoot":"","sources":["../src/root-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AAE1B,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,yBAAyB,GAAG,2BAA2B,GAAG,YAAY,CAAC;AAEnF,MAAM,MAAM,kBAAkB,GAC1B;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,yBAAyB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,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,yBAAyB,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG;IACxD,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW3D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,kBAAkB,CAuBnF;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACjD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACzC,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;CAC/C,GACA,CAAC,CAUH;AAoDD,wBAAsB,YAAY,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAsB7B"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRootPath, resolveRootPathSync, } from "./root-path.js";
|
|
4
|
+
import { openPinnedFileSync, } from "./pinned-open.js";
|
|
5
|
+
export function canUseRootFileOpen(ioFs) {
|
|
6
|
+
return (typeof ioFs.openSync === "function" &&
|
|
7
|
+
typeof ioFs.closeSync === "function" &&
|
|
8
|
+
typeof ioFs.fstatSync === "function" &&
|
|
9
|
+
typeof ioFs.lstatSync === "function" &&
|
|
10
|
+
typeof ioFs.realpathSync === "function" &&
|
|
11
|
+
typeof ioFs.readFileSync === "function" &&
|
|
12
|
+
typeof ioFs.constants === "object" &&
|
|
13
|
+
ioFs.constants !== null);
|
|
14
|
+
}
|
|
15
|
+
export function openRootFileSync(params) {
|
|
16
|
+
const ioFs = params.ioFs ?? fs;
|
|
17
|
+
const resolved = resolveRootFilePathGeneric({
|
|
18
|
+
absolutePath: params.absolutePath,
|
|
19
|
+
resolve: (absolutePath) => resolveRootPathSync({
|
|
20
|
+
absolutePath,
|
|
21
|
+
rootPath: params.rootPath,
|
|
22
|
+
rootCanonicalPath: params.rootRealPath,
|
|
23
|
+
boundaryLabel: params.boundaryLabel,
|
|
24
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (resolved instanceof Promise) {
|
|
28
|
+
return toBoundaryValidationError(new Error("Unexpected async boundary resolution"));
|
|
29
|
+
}
|
|
30
|
+
return finalizeRootFileOpen({
|
|
31
|
+
resolved,
|
|
32
|
+
maxBytes: params.maxBytes,
|
|
33
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
34
|
+
allowedType: params.allowedType,
|
|
35
|
+
ioFs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function matchRootFileOpenFailure(failure, handlers) {
|
|
39
|
+
switch (failure.reason) {
|
|
40
|
+
case "path":
|
|
41
|
+
return handlers.path ? handlers.path(failure) : handlers.fallback(failure);
|
|
42
|
+
case "validation":
|
|
43
|
+
return handlers.validation ? handlers.validation(failure) : handlers.fallback(failure);
|
|
44
|
+
case "io":
|
|
45
|
+
return handlers.io ? handlers.io(failure) : handlers.fallback(failure);
|
|
46
|
+
}
|
|
47
|
+
return handlers.fallback(failure);
|
|
48
|
+
}
|
|
49
|
+
function openRootFileResolved(params) {
|
|
50
|
+
const opened = openPinnedFileSync({
|
|
51
|
+
filePath: params.absolutePath,
|
|
52
|
+
resolvedPath: params.resolvedPath,
|
|
53
|
+
rejectHardlinks: params.rejectHardlinks ?? true,
|
|
54
|
+
maxBytes: params.maxBytes,
|
|
55
|
+
allowedType: params.allowedType,
|
|
56
|
+
ioFs: params.ioFs,
|
|
57
|
+
});
|
|
58
|
+
if (!opened.ok) {
|
|
59
|
+
return opened;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
path: opened.path,
|
|
64
|
+
fd: opened.fd,
|
|
65
|
+
stat: opened.stat,
|
|
66
|
+
rootRealPath: params.rootRealPath,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function finalizeRootFileOpen(params) {
|
|
70
|
+
if ("ok" in params.resolved) {
|
|
71
|
+
return params.resolved;
|
|
72
|
+
}
|
|
73
|
+
return openRootFileResolved({
|
|
74
|
+
absolutePath: params.resolved.absolutePath,
|
|
75
|
+
resolvedPath: params.resolved.resolvedPath,
|
|
76
|
+
rootRealPath: params.resolved.rootRealPath,
|
|
77
|
+
maxBytes: params.maxBytes,
|
|
78
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
79
|
+
allowedType: params.allowedType,
|
|
80
|
+
ioFs: params.ioFs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function openRootFile(params) {
|
|
84
|
+
const ioFs = params.ioFs ?? fs;
|
|
85
|
+
const maybeResolved = resolveRootFilePathGeneric({
|
|
86
|
+
absolutePath: params.absolutePath,
|
|
87
|
+
resolve: (absolutePath) => resolveRootPath({
|
|
88
|
+
absolutePath,
|
|
89
|
+
rootPath: params.rootPath,
|
|
90
|
+
rootCanonicalPath: params.rootRealPath,
|
|
91
|
+
boundaryLabel: params.boundaryLabel,
|
|
92
|
+
policy: params.aliasPolicy,
|
|
93
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
const resolved = maybeResolved instanceof Promise ? await maybeResolved : maybeResolved;
|
|
97
|
+
return finalizeRootFileOpen({
|
|
98
|
+
resolved,
|
|
99
|
+
maxBytes: params.maxBytes,
|
|
100
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
101
|
+
allowedType: params.allowedType,
|
|
102
|
+
ioFs,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function toBoundaryValidationError(error) {
|
|
106
|
+
return { ok: false, reason: "validation", error };
|
|
107
|
+
}
|
|
108
|
+
function mapResolvedRootPath(absolutePath, resolved) {
|
|
109
|
+
return {
|
|
110
|
+
absolutePath,
|
|
111
|
+
resolvedPath: resolved.canonicalPath,
|
|
112
|
+
rootRealPath: resolved.rootCanonicalPath,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function resolveRootFilePathGeneric(params) {
|
|
116
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
117
|
+
try {
|
|
118
|
+
const resolved = params.resolve(absolutePath);
|
|
119
|
+
if (resolved instanceof Promise) {
|
|
120
|
+
return resolved
|
|
121
|
+
.then((value) => mapResolvedRootPath(absolutePath, value))
|
|
122
|
+
.catch((error) => toBoundaryValidationError(error));
|
|
123
|
+
}
|
|
124
|
+
return mapResolvedRootPath(absolutePath, resolved);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return toBoundaryValidationError(error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type RootPathIntent = "read" | "write" | "create" | "delete" | "stat";
|
|
2
|
+
export type RootPathAliasPolicy = {
|
|
3
|
+
allowFinalSymlinkForUnlink?: boolean;
|
|
4
|
+
allowFinalHardlinkForUnlink?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare const ROOT_PATH_ALIAS_POLICIES: {
|
|
7
|
+
readonly strict: Readonly<{
|
|
8
|
+
allowFinalSymlinkForUnlink: false;
|
|
9
|
+
allowFinalHardlinkForUnlink: false;
|
|
10
|
+
}>;
|
|
11
|
+
readonly unlinkTarget: Readonly<{
|
|
12
|
+
allowFinalSymlinkForUnlink: true;
|
|
13
|
+
allowFinalHardlinkForUnlink: true;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
type ResolveRootPathParams = {
|
|
17
|
+
absolutePath: string;
|
|
18
|
+
rootPath: string;
|
|
19
|
+
boundaryLabel: string;
|
|
20
|
+
intent?: RootPathIntent;
|
|
21
|
+
policy?: RootPathAliasPolicy;
|
|
22
|
+
skipLexicalRootCheck?: boolean;
|
|
23
|
+
rootCanonicalPath?: string;
|
|
24
|
+
};
|
|
25
|
+
type ResolvedRootPathKind = "missing" | "file" | "directory" | "symlink" | "other";
|
|
26
|
+
export type ResolvedRootPath = {
|
|
27
|
+
absolutePath: string;
|
|
28
|
+
canonicalPath: string;
|
|
29
|
+
rootPath: string;
|
|
30
|
+
rootCanonicalPath: string;
|
|
31
|
+
relativePath: string;
|
|
32
|
+
exists: boolean;
|
|
33
|
+
kind: ResolvedRootPathKind;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveRootPath(params: ResolveRootPathParams): Promise<ResolvedRootPath>;
|
|
36
|
+
export declare function resolveRootPathSync(params: ResolveRootPathParams): ResolvedRootPath;
|
|
37
|
+
export declare function resolvePathViaExistingAncestorSync(targetPath: string): string;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=root-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAMA,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;;;;;;;;;CAS3B,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CA+B3B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CA+BnF;AAmkBD,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA6B7E"}
|