@openclaw/fs-safe 0.4.6 → 0.5.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 +69 -0
- package/README.md +110 -41
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/directory-durability.d.ts +44 -0
- package/dist/directory-durability.d.ts.map +1 -0
- package/dist/directory-durability.js +299 -0
- package/dist/durability.d.ts +4 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +3 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/fsync.d.ts +1 -1
- package/dist/fsync.d.ts.map +1 -1
- package/dist/fsync.js +1 -21
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -13
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +299 -0
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -7
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +42 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +12 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureAbsoluteDirectory } from "./absolute-path.js";
|
|
5
|
+
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
7
|
+
function directoryOpenFlags() {
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
return "r";
|
|
10
|
+
}
|
|
11
|
+
return (fsSync.constants.O_RDONLY |
|
|
12
|
+
fsSync.constants.O_DIRECTORY |
|
|
13
|
+
fsSync.constants.O_NOFOLLOW |
|
|
14
|
+
fsSync.constants.O_NONBLOCK);
|
|
15
|
+
}
|
|
16
|
+
function isWindowsDirectorySyncUnsupported(error) {
|
|
17
|
+
if (process.platform !== "win32") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const code = error.code;
|
|
21
|
+
return (code === "EACCES" ||
|
|
22
|
+
code === "EINVAL" ||
|
|
23
|
+
code === "EISDIR" ||
|
|
24
|
+
code === "ENOSYS" ||
|
|
25
|
+
code === "ENOTSUP" ||
|
|
26
|
+
code === "EPERM");
|
|
27
|
+
}
|
|
28
|
+
function isWindowsDirectoryOpenUnsupported(error) {
|
|
29
|
+
if (process.platform !== "win32") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const code = error.code;
|
|
33
|
+
return code === "EINVAL" || code === "EISDIR" || code === "ENOSYS" || code === "ENOTSUP";
|
|
34
|
+
}
|
|
35
|
+
function unsupportedOutcome(error) {
|
|
36
|
+
const code = error.code;
|
|
37
|
+
return code ? { status: "unsupported", code } : { status: "unsupported" };
|
|
38
|
+
}
|
|
39
|
+
function assertDirectory(identity, pathname, label) {
|
|
40
|
+
if (identity.isSymbolicLink() || !identity.isDirectory()) {
|
|
41
|
+
throw new FsSafeError("not-file", `${label} must be a real directory: ${pathname}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function createDirectoryReceipt(directoryPath, label) {
|
|
45
|
+
const resolvedPath = path.resolve(directoryPath);
|
|
46
|
+
const identity = await fs.lstat(resolvedPath);
|
|
47
|
+
assertDirectory(identity, resolvedPath, label);
|
|
48
|
+
return {
|
|
49
|
+
path: resolvedPath,
|
|
50
|
+
realPath: await fs.realpath(resolvedPath),
|
|
51
|
+
identity,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function createDirectoryReceiptSync(directoryPath, label) {
|
|
55
|
+
const resolvedPath = path.resolve(directoryPath);
|
|
56
|
+
const identity = fsSync.lstatSync(resolvedPath);
|
|
57
|
+
assertDirectory(identity, resolvedPath, label);
|
|
58
|
+
return {
|
|
59
|
+
path: resolvedPath,
|
|
60
|
+
realPath: fsSync.realpathSync(resolvedPath),
|
|
61
|
+
identity,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function assertDirectoryReceiptCurrent(receipt, label) {
|
|
65
|
+
const currentIdentity = await fs.lstat(receipt.path);
|
|
66
|
+
assertDirectory(currentIdentity, receipt.path, label);
|
|
67
|
+
if (!sameFileIdentity(receipt.identity, currentIdentity) ||
|
|
68
|
+
(await fs.realpath(receipt.path)) !== receipt.realPath) {
|
|
69
|
+
throw new FsSafeError("path-mismatch", `${label} changed during durable directory operation: ${receipt.path}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function assertDirectoryReceiptCurrentSync(receipt, label) {
|
|
73
|
+
const currentIdentity = fsSync.lstatSync(receipt.path);
|
|
74
|
+
assertDirectory(currentIdentity, receipt.path, label);
|
|
75
|
+
if (!sameFileIdentity(receipt.identity, currentIdentity) ||
|
|
76
|
+
fsSync.realpathSync(receipt.path) !== receipt.realPath) {
|
|
77
|
+
throw new FsSafeError("path-mismatch", `${label} changed during durable directory operation: ${receipt.path}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function assertOpenDirectoryCurrent(handle, receipt, label) {
|
|
81
|
+
const openedIdentity = await handle.stat();
|
|
82
|
+
assertDirectory(openedIdentity, receipt.path, label);
|
|
83
|
+
if (!sameFileIdentity(receipt.identity, openedIdentity)) {
|
|
84
|
+
throw new FsSafeError("path-mismatch", `${label} handle changed during directory sync: ${receipt.path}`);
|
|
85
|
+
}
|
|
86
|
+
await assertDirectoryReceiptCurrent(receipt, label);
|
|
87
|
+
}
|
|
88
|
+
class PinnedDirectoryImpl {
|
|
89
|
+
receipt;
|
|
90
|
+
#handle;
|
|
91
|
+
#label;
|
|
92
|
+
#closed = false;
|
|
93
|
+
constructor(handle, receipt, label) {
|
|
94
|
+
this.#handle = handle;
|
|
95
|
+
this.receipt = receipt;
|
|
96
|
+
this.#label = label;
|
|
97
|
+
}
|
|
98
|
+
async assertCurrent() {
|
|
99
|
+
if (this.#closed) {
|
|
100
|
+
throw new FsSafeError("helper-failed", `${this.#label} pin is already closed`);
|
|
101
|
+
}
|
|
102
|
+
await assertOpenDirectoryCurrent(this.#handle, this.receipt, this.#label);
|
|
103
|
+
}
|
|
104
|
+
async sync() {
|
|
105
|
+
await this.assertCurrent();
|
|
106
|
+
try {
|
|
107
|
+
await this.#handle.sync();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!isWindowsDirectorySyncUnsupported(error)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
await this.assertCurrent();
|
|
114
|
+
return unsupportedOutcome(error);
|
|
115
|
+
}
|
|
116
|
+
await this.assertCurrent();
|
|
117
|
+
return { status: "synced" };
|
|
118
|
+
}
|
|
119
|
+
async close() {
|
|
120
|
+
if (this.#closed) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.#closed = true;
|
|
124
|
+
await this.#handle.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export async function pinDirectory(directory, options = {}) {
|
|
128
|
+
const label = options.label ?? "directory";
|
|
129
|
+
const receipt = typeof directory === "string" ? await createDirectoryReceipt(directory, label) : directory;
|
|
130
|
+
await assertDirectoryReceiptCurrent(receipt, label);
|
|
131
|
+
const handle = await fs.open(receipt.path, directoryOpenFlags());
|
|
132
|
+
try {
|
|
133
|
+
await assertOpenDirectoryCurrent(handle, receipt, label);
|
|
134
|
+
return new PinnedDirectoryImpl(handle, receipt, label);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
await handle.close().catch(() => undefined);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function syncDirectory(directory, options = {}) {
|
|
142
|
+
const label = options.label ?? "directory";
|
|
143
|
+
const receipt = typeof directory === "string" ? await createDirectoryReceipt(directory, label) : directory;
|
|
144
|
+
let pinned;
|
|
145
|
+
try {
|
|
146
|
+
pinned = await pinDirectory(receipt, { label });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!isWindowsDirectoryOpenUnsupported(error)) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
await assertDirectoryReceiptCurrent(receipt, label);
|
|
153
|
+
return unsupportedOutcome(error);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return await pinned.sync();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await pinned.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function syncDirectorySync(directory, options = {}) {
|
|
163
|
+
const label = options.label ?? "directory";
|
|
164
|
+
const receipt = typeof directory === "string" ? createDirectoryReceiptSync(directory, label) : directory;
|
|
165
|
+
assertDirectoryReceiptCurrentSync(receipt, label);
|
|
166
|
+
let descriptor;
|
|
167
|
+
try {
|
|
168
|
+
descriptor = fsSync.openSync(receipt.path, directoryOpenFlags());
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!isWindowsDirectoryOpenUnsupported(error)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
assertDirectoryReceiptCurrentSync(receipt, label);
|
|
175
|
+
return unsupportedOutcome(error);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const openedIdentity = fsSync.fstatSync(descriptor);
|
|
179
|
+
assertDirectory(openedIdentity, receipt.path, label);
|
|
180
|
+
if (!sameFileIdentity(receipt.identity, openedIdentity)) {
|
|
181
|
+
throw new FsSafeError("path-mismatch", `${label} handle changed during directory sync: ${receipt.path}`);
|
|
182
|
+
}
|
|
183
|
+
assertDirectoryReceiptCurrentSync(receipt, label);
|
|
184
|
+
try {
|
|
185
|
+
fsSync.fsyncSync(descriptor);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!isWindowsDirectorySyncUnsupported(error)) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
assertDirectoryReceiptCurrentSync(receipt, label);
|
|
192
|
+
return unsupportedOutcome(error);
|
|
193
|
+
}
|
|
194
|
+
assertDirectoryReceiptCurrentSync(receipt, label);
|
|
195
|
+
return { status: "synced" };
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
fsSync.closeSync(descriptor);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function syncDirectoryBestEffort(directoryPath) {
|
|
202
|
+
await syncDirectory(directoryPath).catch(() => undefined);
|
|
203
|
+
}
|
|
204
|
+
export function syncDirectoryBestEffortSync(directoryPath) {
|
|
205
|
+
try {
|
|
206
|
+
syncDirectorySync(directoryPath);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Compatibility helper for operations whose primary write may remain usable.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function findExistingAncestorReceipt(targetPath, label) {
|
|
213
|
+
let currentPath = path.resolve(targetPath);
|
|
214
|
+
while (true) {
|
|
215
|
+
try {
|
|
216
|
+
return await createDirectoryReceipt(currentPath, label);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code !== "ENOENT") {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const parentPath = path.dirname(currentPath);
|
|
224
|
+
if (parentPath === currentPath) {
|
|
225
|
+
throw new FsSafeError("not-found", `${label} has no existing directory ancestor`);
|
|
226
|
+
}
|
|
227
|
+
currentPath = parentPath;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export async function ensureDurableDirectory(options) {
|
|
231
|
+
const directoryPath = path.resolve(options.directoryPath);
|
|
232
|
+
const label = options.label ?? "directory";
|
|
233
|
+
const ancestorReceipt = await findExistingAncestorReceipt(directoryPath, label);
|
|
234
|
+
const targetExists = ancestorReceipt.path === directoryPath;
|
|
235
|
+
if (options.expectedExistingIdentity &&
|
|
236
|
+
(!targetExists || !sameFileIdentity(options.expectedExistingIdentity, ancestorReceipt.identity))) {
|
|
237
|
+
throw new FsSafeError("path-mismatch", `${label} changed before durable directory pinning: ${directoryPath}`);
|
|
238
|
+
}
|
|
239
|
+
const ancestor = await pinDirectory(ancestorReceipt, { label });
|
|
240
|
+
const pinnedDirectories = [ancestor];
|
|
241
|
+
try {
|
|
242
|
+
await ancestor.assertCurrent();
|
|
243
|
+
if (!targetExists) {
|
|
244
|
+
if (options.create) {
|
|
245
|
+
await options.create(directoryPath);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const created = await ensureAbsoluteDirectory(directoryPath, {
|
|
249
|
+
mode: options.mode,
|
|
250
|
+
scopeLabel: label,
|
|
251
|
+
});
|
|
252
|
+
if (!created.ok) {
|
|
253
|
+
throw created.error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
await ancestor.assertCurrent();
|
|
258
|
+
let currentPath = ancestor.receipt.path;
|
|
259
|
+
for (const segment of path
|
|
260
|
+
.relative(ancestor.receipt.path, directoryPath)
|
|
261
|
+
.split(path.sep)
|
|
262
|
+
.filter(Boolean)) {
|
|
263
|
+
currentPath = path.join(currentPath, segment);
|
|
264
|
+
pinnedDirectories.push(await pinDirectory(currentPath, { label }));
|
|
265
|
+
}
|
|
266
|
+
let parentSync = { status: "not-needed" };
|
|
267
|
+
for (let index = pinnedDirectories.length - 1; index > 0; index -= 1) {
|
|
268
|
+
const parent = pinnedDirectories[index - 1];
|
|
269
|
+
const child = pinnedDirectories[index];
|
|
270
|
+
if (!parent || !child) {
|
|
271
|
+
throw new FsSafeError("helper-failed", `${label} directory pin chain is incomplete`);
|
|
272
|
+
}
|
|
273
|
+
await child.assertCurrent();
|
|
274
|
+
try {
|
|
275
|
+
const outcome = await parent.sync();
|
|
276
|
+
if (outcome.status === "unsupported") {
|
|
277
|
+
parentSync = outcome;
|
|
278
|
+
}
|
|
279
|
+
else if (parentSync.status === "not-needed") {
|
|
280
|
+
parentSync = outcome;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw new FsSafeError("helper-failed", `${label} could not sync created directory edge ${child.receipt.path} through ${parent.receipt.path}`, { cause: error });
|
|
285
|
+
}
|
|
286
|
+
await child.assertCurrent();
|
|
287
|
+
}
|
|
288
|
+
const finalReceipt = pinnedDirectories.at(-1)?.receipt;
|
|
289
|
+
if (!finalReceipt) {
|
|
290
|
+
throw new FsSafeError("helper-failed", `${label} directory receipt is missing`);
|
|
291
|
+
}
|
|
292
|
+
await ancestor.assertCurrent();
|
|
293
|
+
await assertDirectoryReceiptCurrent(finalReceipt, label);
|
|
294
|
+
return { ...finalReceipt, parentSync };
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
await Promise.all(pinnedDirectories.toReversed().map(async (directory) => directory.close()));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ensureDurableDirectory, pinDirectory, syncDirectory, syncDirectoryBestEffort, syncDirectoryBestEffortSync, syncDirectorySync, type DirectoryReceipt, type DirectorySyncOutcome, type DurableDirectoryReceipt, type EnsureDurableDirectoryOptions, type PinnedDirectory, } from "./directory-durability.js";
|
|
2
|
+
export { isHardlinkFallbackError, publishFileExclusive, type PublishFileExclusiveResult, type PublishFileExclusiveStrategy, type PublishFileExclusiveCleanup, type PublishFileExclusiveDirectorySyncFailure, type PublishFileExclusiveFailureDetails, type PublishFileExclusiveFailurePhase, type PublishFileExclusiveSyncFailurePolicy, } from "./publish-file.js";
|
|
3
|
+
export { sha256File, type Sha256FileInput, type Sha256FileResult, } from "./file-hash.js";
|
|
4
|
+
//# sourceMappingURL=durability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durability.d.ts","sourceRoot":"","sources":["../src/durability.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,aAAa,EACb,uBAAuB,EACvB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,KAAK,0BAA0B,EAC/B,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,wCAAwC,EAC7C,KAAK,kCAAkC,EACvC,KAAK,gCAAgC,EACrC,KAAK,qCAAqC,GAC3C,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ensureDurableDirectory, pinDirectory, syncDirectory, syncDirectoryBestEffort, syncDirectoryBestEffortSync, syncDirectorySync, } from "./directory-durability.js";
|
|
2
|
+
export { isHardlinkFallbackError, publishFileExclusive, } from "./publish-file.js";
|
|
3
|
+
export { sha256File, } from "./file-hash.js";
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
|
|
1
|
+
export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "secret-exists" | "store-reentrant-update" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
|
|
2
2
|
export type FsSafeErrorCategory = "policy" | "operational";
|
|
3
|
+
export type FsSafeErrorDetails = Readonly<Record<string, unknown>>;
|
|
3
4
|
export declare function categorizeFsSafeError(code: FsSafeErrorCode): FsSafeErrorCategory;
|
|
4
5
|
export declare class FsSafeError extends Error {
|
|
5
6
|
readonly code: FsSafeErrorCode;
|
|
6
7
|
readonly category: FsSafeErrorCategory;
|
|
8
|
+
readonly details?: FsSafeErrorDetails;
|
|
7
9
|
constructor(code: FsSafeErrorCode, message: string, options?: {
|
|
8
10
|
cause?: unknown;
|
|
11
|
+
details?: FsSafeErrorDetails;
|
|
9
12
|
});
|
|
10
13
|
}
|
|
11
14
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,eAAe,GACf,wBAAwB,GACxB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAUnE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,mBAAmB,CAEhF;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAEtC,YACE,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;KAAO,EAOhE;CACF"}
|
package/dist/errors.js
CHANGED
|
@@ -11,10 +11,12 @@ export function categorizeFsSafeError(code) {
|
|
|
11
11
|
export class FsSafeError extends Error {
|
|
12
12
|
code;
|
|
13
13
|
category;
|
|
14
|
+
details;
|
|
14
15
|
constructor(code, message, options = {}) {
|
|
15
16
|
super(message, options);
|
|
16
17
|
this.name = "FsSafeError";
|
|
17
18
|
this.code = code;
|
|
18
19
|
this.category = categorizeFsSafeError(code);
|
|
20
|
+
this.details = options.details;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
import { type NativeBinding } from "./native.js";
|
|
3
|
+
export type Sha256FileInput = string | FileHandle;
|
|
4
|
+
export type Sha256FileResult = {
|
|
5
|
+
bytes: number;
|
|
6
|
+
digest: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function hashFileHandle(handle: FileHandle, native?: NativeBinding | undefined): Promise<Sha256FileResult>;
|
|
9
|
+
export declare function sha256File(input: Sha256FileInput): Promise<Sha256FileResult>;
|
|
10
|
+
//# sourceMappingURL=file-hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAcF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AAsCD,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { getNativeBinding } from "./native.js";
|
|
7
|
+
function readFlags() {
|
|
8
|
+
return (fsSync.constants.O_RDONLY |
|
|
9
|
+
(process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
10
|
+
? fsSync.constants.O_NOFOLLOW
|
|
11
|
+
: 0) |
|
|
12
|
+
(process.platform !== "win32" && typeof fsSync.constants.O_NONBLOCK === "number"
|
|
13
|
+
? fsSync.constants.O_NONBLOCK
|
|
14
|
+
: 0));
|
|
15
|
+
}
|
|
16
|
+
export async function hashFileHandle(handle, native = getNativeBinding()) {
|
|
17
|
+
const stat = await handle.stat();
|
|
18
|
+
if (!stat.isFile()) {
|
|
19
|
+
throw new FsSafeError("not-file", "SHA-256 input is not a regular file");
|
|
20
|
+
}
|
|
21
|
+
if (native) {
|
|
22
|
+
return await native.sha256File(handle.fd);
|
|
23
|
+
}
|
|
24
|
+
const hash = createHash("sha256");
|
|
25
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
26
|
+
let position = 0;
|
|
27
|
+
while (true) {
|
|
28
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
29
|
+
if (bytesRead === 0) {
|
|
30
|
+
return { bytes: position, digest: hash.digest("hex") };
|
|
31
|
+
}
|
|
32
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
33
|
+
position += bytesRead;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function hashPath(filePath) {
|
|
37
|
+
const before = await fs.lstat(filePath);
|
|
38
|
+
if (before.isSymbolicLink()) {
|
|
39
|
+
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
|
|
40
|
+
}
|
|
41
|
+
if (!before.isFile()) {
|
|
42
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
43
|
+
}
|
|
44
|
+
let handle;
|
|
45
|
+
try {
|
|
46
|
+
handle = await fs.open(filePath, readFlags());
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error?.code === "ELOOP") {
|
|
50
|
+
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link", {
|
|
51
|
+
cause: error,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const opened = await handle.stat();
|
|
58
|
+
const current = await fs.lstat(filePath);
|
|
59
|
+
if (!opened.isFile()) {
|
|
60
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
61
|
+
}
|
|
62
|
+
if (current.isSymbolicLink() || !current.isFile() || !sameFileIdentity(opened, current)) {
|
|
63
|
+
throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
|
|
64
|
+
}
|
|
65
|
+
return await hashFileHandle(handle);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await handle.close().catch(() => undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function sha256File(input) {
|
|
72
|
+
return typeof input === "string" ? await hashPath(input) : await hashFileHandle(input);
|
|
73
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Root } from "./root-impl.js";
|
|
2
|
+
import { type SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
|
+
import type { SidecarLockCompromisedInfo, SidecarLockRetryOptions, SidecarLockStaleRecovery } from "./sidecar-lock-types.js";
|
|
4
|
+
export type FileLockSyncAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
5
|
+
lockPath?: string;
|
|
6
|
+
staleMs?: number;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
retry?: SidecarLockRetryOptions;
|
|
9
|
+
staleRecovery?: SidecarLockStaleRecovery;
|
|
10
|
+
reentrantOwner?: string;
|
|
11
|
+
payload: () => TPayload;
|
|
12
|
+
shouldReclaim?: (params: {
|
|
13
|
+
lockPath: string;
|
|
14
|
+
normalizedTargetPath: string;
|
|
15
|
+
payload: unknown;
|
|
16
|
+
staleMs: number;
|
|
17
|
+
nowMs: number;
|
|
18
|
+
heldByThisProcess: false;
|
|
19
|
+
}) => boolean;
|
|
20
|
+
shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean;
|
|
21
|
+
parsePayload?: (raw: string) => unknown;
|
|
22
|
+
lockRoot?: Root;
|
|
23
|
+
onCompromised?: (info: SidecarLockCompromisedInfo) => void;
|
|
24
|
+
compromiseCheckIntervalMs?: number;
|
|
25
|
+
};
|
|
26
|
+
export type FileLockSyncHandle = {
|
|
27
|
+
lockPath: string;
|
|
28
|
+
normalizedTargetPath: string;
|
|
29
|
+
verifyStillHeld(): boolean;
|
|
30
|
+
release(): void;
|
|
31
|
+
[Symbol.dispose](): void;
|
|
32
|
+
};
|
|
33
|
+
export declare function acquireFileLockSync<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>): FileLockSyncHandle;
|
|
34
|
+
export declare function withFileLockSync<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>, fn: () => T): T;
|
|
35
|
+
//# sourceMappingURL=file-lock-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAKnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,QAAQ,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AAiGF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CA6IpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
|