@poe-platform/safe-bash 0.1.628 → 0.1.630
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/README.md +1 -2
- package/dist/safe-bash/chunks/chunk-3BZ5NQ73.js +3952 -0
- package/dist/safe-bash/chunks/chunk-3BZ5NQ73.js.map +7 -0
- package/dist/safe-bash/chunks/{chunk-KYNRHYHP.js → chunk-6YSBXTTJ.js} +5 -5
- package/dist/safe-bash/chunks/{chunk-HHJSJWUD.js → chunk-K7W6Q6FN.js} +4 -4
- package/dist/safe-bash/chunks/{chunk-WPEMUXV4.js → chunk-KQ6J7JHQ.js} +4 -4
- package/dist/safe-bash/commands/archive/zip.d.ts.map +1 -1
- package/dist/safe-bash/commands/archive/zip.js +18 -17
- package/dist/safe-bash/commands/archive/zip.js.map +1 -1
- package/dist/safe-bash/commands/llm/index.browser.js +2 -2
- package/dist/safe-bash/commands/network/index.browser.js +2 -2
- package/dist/safe-bash/commands/node/browser.d.ts +2 -0
- package/dist/safe-bash/commands/node/browser.d.ts.map +1 -0
- package/dist/safe-bash/commands/node/browser.js +2 -0
- package/dist/safe-bash/commands/node/browser.js.map +1 -0
- package/dist/safe-bash/commands/node/index.browser.js +16 -0
- package/dist/safe-bash/commands/node/index.browser.js.map +7 -0
- package/dist/safe-bash/commands/node/safejs.d.ts +6 -0
- package/dist/safe-bash/commands/node/safejs.d.ts.map +1 -1
- package/dist/safe-bash/commands/node/safejs.js +25 -1
- package/dist/safe-bash/commands/node/safejs.js.map +1 -1
- package/dist/safe-bash/commands/safejs/index.d.ts +5 -17
- package/dist/safe-bash/commands/safejs/index.d.ts.map +1 -1
- package/dist/safe-bash/commands/safejs/index.js +3 -202
- package/dist/safe-bash/commands/safejs/index.js.map +1 -1
- package/dist/safe-bash/commands/safejs/runtime.d.ts +15 -0
- package/dist/safe-bash/commands/safejs/runtime.d.ts.map +1 -0
- package/dist/safe-bash/commands/safejs/runtime.js +190 -0
- package/dist/safe-bash/commands/safejs/runtime.js.map +1 -0
- package/dist/safe-bash/commands/yq/index.browser.js +2 -2
- package/dist/safe-bash/core.browser.js +515 -4364
- package/dist/safe-bash/core.browser.js.map +4 -4
- package/dist/safe-bash/core.d.ts +1 -0
- package/dist/safe-bash/core.d.ts.map +1 -1
- package/dist/safe-bash/core.js +1 -0
- package/dist/safe-bash/core.js.map +1 -1
- package/dist/safe-bash/index.d.ts +1 -0
- package/dist/safe-bash/index.d.ts.map +1 -1
- package/dist/safe-bash/index.js +1 -0
- package/dist/safe-bash/index.js.map +1 -1
- package/package.json +9 -3
- /package/dist/safe-bash/chunks/{chunk-KYNRHYHP.js.map → chunk-6YSBXTTJ.js.map} +0 -0
- /package/dist/safe-bash/chunks/{chunk-HHJSJWUD.js.map → chunk-K7W6Q6FN.js.map} +0 -0
- /package/dist/safe-bash/chunks/{chunk-WPEMUXV4.js.map → chunk-KQ6J7JHQ.js.map} +0 -0
|
@@ -0,0 +1,3952 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UsageError,
|
|
3
|
+
pathOf,
|
|
4
|
+
writeDiagnostic
|
|
5
|
+
} from "./chunk-GFZKQIPL.js";
|
|
6
|
+
import {
|
|
7
|
+
PublicDiagnostic,
|
|
8
|
+
publicDiagnosticMessage,
|
|
9
|
+
yieldTurn
|
|
10
|
+
} from "./chunk-UYX72YHY.js";
|
|
11
|
+
import {
|
|
12
|
+
readBytes,
|
|
13
|
+
toByteSource,
|
|
14
|
+
writeBytes
|
|
15
|
+
} from "./chunk-CXMIIXJA.js";
|
|
16
|
+
import {
|
|
17
|
+
import_buffer,
|
|
18
|
+
init_platform
|
|
19
|
+
} from "./chunk-2KL6W6NB.js";
|
|
20
|
+
|
|
21
|
+
// packages/safe-bash/src/commands/node/browser.ts
|
|
22
|
+
init_platform();
|
|
23
|
+
|
|
24
|
+
// packages/safe-bash/src/commands/node/safejs.ts
|
|
25
|
+
init_platform();
|
|
26
|
+
|
|
27
|
+
// packages/safe-bash/src/integrations/safejs/values.ts
|
|
28
|
+
init_platform();
|
|
29
|
+
function record(value, name) {
|
|
30
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
+
throw new TypeError(`${name} must be a plain object`);
|
|
32
|
+
}
|
|
33
|
+
const prototype = Object.getPrototypeOf(value);
|
|
34
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
35
|
+
throw new TypeError(`${name} must be a plain object`);
|
|
36
|
+
}
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
39
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
40
|
+
if (typeof key !== "string" || descriptor === void 0 || !("value" in descriptor)) {
|
|
41
|
+
throw new TypeError(`${name} must contain only string-keyed data properties`);
|
|
42
|
+
}
|
|
43
|
+
Object.defineProperty(result, key, {
|
|
44
|
+
value: descriptor.value,
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
function onlyKeys(value, allowed) {
|
|
53
|
+
for (const key of Object.keys(value)) {
|
|
54
|
+
if (!allowed.includes(key)) throw new TypeError(`Unsupported option: ${key}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function stringValue(value, name) {
|
|
58
|
+
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function abortError() {
|
|
62
|
+
return Object.assign(new Error("The operation was aborted"), {
|
|
63
|
+
name: "AbortError",
|
|
64
|
+
code: "ABORT_ERR"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function checkSignal(signal) {
|
|
68
|
+
if (signal?.aborted) throw abortError();
|
|
69
|
+
}
|
|
70
|
+
async function withSignal(signal, operation) {
|
|
71
|
+
checkSignal(signal);
|
|
72
|
+
if (signal === void 0) return operation();
|
|
73
|
+
let onAbort = () => void 0;
|
|
74
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
75
|
+
onAbort = () => reject(abortError());
|
|
76
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
return await Promise.race([Promise.resolve().then(() => {
|
|
80
|
+
checkSignal(signal);
|
|
81
|
+
return operation();
|
|
82
|
+
}), cancelled]);
|
|
83
|
+
} finally {
|
|
84
|
+
signal.removeEventListener("abort", onAbort);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// packages/safe-bash/src/commands/safejs/runtime.ts
|
|
89
|
+
init_platform();
|
|
90
|
+
|
|
91
|
+
// packages/safe-bash/src/integrations/safejs/index.ts
|
|
92
|
+
init_platform();
|
|
93
|
+
|
|
94
|
+
// packages/safe-bash/src/integrations/safejs/filesystem.ts
|
|
95
|
+
init_platform();
|
|
96
|
+
|
|
97
|
+
// packages/safe-js/dist/browser/safe-fs.js
|
|
98
|
+
init_platform();
|
|
99
|
+
|
|
100
|
+
// packages/safe-js/dist/browser/chunks/chunk-SB2GDI2R.js
|
|
101
|
+
init_platform();
|
|
102
|
+
var platform = Object.freeze({
|
|
103
|
+
errno(_code) {
|
|
104
|
+
return void 0;
|
|
105
|
+
},
|
|
106
|
+
callbackAuthorities: false,
|
|
107
|
+
randomUUID() {
|
|
108
|
+
const crypto = globalThis.crypto;
|
|
109
|
+
if (typeof crypto?.randomUUID === "function") return crypto.randomUUID();
|
|
110
|
+
if (typeof crypto?.getRandomValues !== "function") {
|
|
111
|
+
throw new FsError("ENOTSUP", { message: "secure crypto is required for overlay staging" });
|
|
112
|
+
}
|
|
113
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
114
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
115
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
116
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
117
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
var operationKey = /* @__PURE__ */ Symbol("filesystem-comparison");
|
|
121
|
+
var contexts = /* @__PURE__ */ new WeakSet();
|
|
122
|
+
var comparisonContext = Object.freeze({
|
|
123
|
+
active(options) {
|
|
124
|
+
const context = options[operationKey];
|
|
125
|
+
return context !== void 0 && contexts.has(context);
|
|
126
|
+
},
|
|
127
|
+
async run(options, action) {
|
|
128
|
+
const context = Object.freeze({});
|
|
129
|
+
contexts.add(context);
|
|
130
|
+
const nested = { ...options, [operationKey]: context };
|
|
131
|
+
try {
|
|
132
|
+
return await action(nested);
|
|
133
|
+
} finally {
|
|
134
|
+
contexts.delete(context);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
var descriptions = {
|
|
139
|
+
EACCES: "permission denied",
|
|
140
|
+
EAGAIN: "resource temporarily unavailable",
|
|
141
|
+
EBADF: "bad file descriptor",
|
|
142
|
+
EBUSY: "resource busy or locked",
|
|
143
|
+
ECANCELED: "operation canceled",
|
|
144
|
+
EEXIST: "file already exists",
|
|
145
|
+
EFBIG: "file too large",
|
|
146
|
+
EINTR: "interrupted system call",
|
|
147
|
+
EINVAL: "invalid argument",
|
|
148
|
+
EIO: "input/output error",
|
|
149
|
+
EISDIR: "illegal operation on a directory",
|
|
150
|
+
ELOOP: "too many symbolic links encountered",
|
|
151
|
+
EMFILE: "too many open files",
|
|
152
|
+
ENAMETOOLONG: "name too long",
|
|
153
|
+
ENFILE: "file table overflow",
|
|
154
|
+
ENOENT: "no such file or directory",
|
|
155
|
+
ENOMEM: "not enough memory",
|
|
156
|
+
ENOSPC: "no space left on device",
|
|
157
|
+
ENOSYS: "function not implemented",
|
|
158
|
+
ENOTDIR: "not a directory",
|
|
159
|
+
ENOTEMPTY: "directory not empty",
|
|
160
|
+
ENOTSUP: "operation not supported",
|
|
161
|
+
EOPNOTSUPP: "operation not supported",
|
|
162
|
+
EPERM: "operation not permitted",
|
|
163
|
+
EPIPE: "broken pipe",
|
|
164
|
+
EROFS: "read-only file system",
|
|
165
|
+
ESPIPE: "invalid seek",
|
|
166
|
+
ETIMEDOUT: "operation timed out",
|
|
167
|
+
EXDEV: "cross-device link not permitted"
|
|
168
|
+
};
|
|
169
|
+
var FsError = class extends Error {
|
|
170
|
+
code;
|
|
171
|
+
errno;
|
|
172
|
+
syscall;
|
|
173
|
+
path;
|
|
174
|
+
dest;
|
|
175
|
+
constructor(code, options = {}) {
|
|
176
|
+
if (!isErrnoCode(code)) throw new TypeError(`Unsupported errno code: ${String(code)}`);
|
|
177
|
+
const errno = platform.errno(code);
|
|
178
|
+
const operation = options.syscall ? `, ${options.syscall}` : "";
|
|
179
|
+
const path = options.path === void 0 ? "" : ` '${options.path}'`;
|
|
180
|
+
const destination = options.dest === void 0 ? "" : ` -> '${options.dest}'`;
|
|
181
|
+
super(`${code}: ${options.message ?? descriptions[code]}${operation}${path}${destination}`, options);
|
|
182
|
+
this.name = "FsError";
|
|
183
|
+
this.code = code;
|
|
184
|
+
this.errno = errno;
|
|
185
|
+
if (options.syscall !== void 0) this.syscall = options.syscall;
|
|
186
|
+
if (options.path !== void 0) this.path = options.path;
|
|
187
|
+
if (options.dest !== void 0) this.dest = options.dest;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function isErrnoCode(code) {
|
|
191
|
+
return typeof code === "string" && Object.hasOwn(descriptions, code);
|
|
192
|
+
}
|
|
193
|
+
function isFsError(error, code) {
|
|
194
|
+
return error instanceof FsError && (code === void 0 || error.code === code);
|
|
195
|
+
}
|
|
196
|
+
var ACCESS_MODES = Object.freeze({ F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1 });
|
|
197
|
+
async function finishCleanup(cleanup, failed) {
|
|
198
|
+
try {
|
|
199
|
+
await cleanup();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!failed) throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function abortable(operation, signal) {
|
|
205
|
+
signal?.throwIfAborted();
|
|
206
|
+
if (!signal) return operation();
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const onAbort = () => {
|
|
209
|
+
signal.removeEventListener("abort", onAbort);
|
|
210
|
+
reject(signal.reason);
|
|
211
|
+
};
|
|
212
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
213
|
+
try {
|
|
214
|
+
Promise.resolve(operation()).then(
|
|
215
|
+
(result) => {
|
|
216
|
+
signal.removeEventListener("abort", onAbort);
|
|
217
|
+
resolve(result);
|
|
218
|
+
},
|
|
219
|
+
(error) => {
|
|
220
|
+
signal.removeEventListener("abort", onAbort);
|
|
221
|
+
reject(error);
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
signal.removeEventListener("abort", onAbort);
|
|
226
|
+
reject(error);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function* readBytes2(source, signal) {
|
|
231
|
+
signal?.throwIfAborted();
|
|
232
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
233
|
+
let finished = false;
|
|
234
|
+
let failed = false;
|
|
235
|
+
try {
|
|
236
|
+
while (true) {
|
|
237
|
+
const result = await abortable(() => iterator.next(), signal);
|
|
238
|
+
if (result.done) {
|
|
239
|
+
finished = true;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!(result.value instanceof Uint8Array)) throw new TypeError("Byte sources must yield Uint8Array chunks");
|
|
243
|
+
yield result.value;
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
failed = true;
|
|
247
|
+
throw error;
|
|
248
|
+
} finally {
|
|
249
|
+
if (!finished && iterator.return) {
|
|
250
|
+
const cleanup = Promise.resolve().then(() => iterator.return());
|
|
251
|
+
if (signal?.aborted) void cleanup.catch(() => {
|
|
252
|
+
});
|
|
253
|
+
else await finishCleanup(() => abortable(() => cleanup, signal), failed);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function dirname(path) {
|
|
258
|
+
if (typeof path !== "string") throw new TypeError("path must be a string");
|
|
259
|
+
let end = path.length;
|
|
260
|
+
while (end > 0 && path[end - 1] === "/") end--;
|
|
261
|
+
if (end === 0) return path.startsWith("/") ? "/" : ".";
|
|
262
|
+
const boundary = path.lastIndexOf("/", end - 1);
|
|
263
|
+
if (boundary < 0) return ".";
|
|
264
|
+
if (boundary === 0) return "/";
|
|
265
|
+
if (boundary === 1 && path.startsWith("/")) return "//";
|
|
266
|
+
return path.slice(0, boundary);
|
|
267
|
+
}
|
|
268
|
+
function assertString(value) {
|
|
269
|
+
if (typeof value !== "string") throw new TypeError("path must be a string");
|
|
270
|
+
}
|
|
271
|
+
function basename(path, suffix = "") {
|
|
272
|
+
assertString(path);
|
|
273
|
+
assertString(suffix);
|
|
274
|
+
if (suffix && path === suffix) return "";
|
|
275
|
+
let end = path.length;
|
|
276
|
+
while (end > 0 && path[end - 1] === "/") end--;
|
|
277
|
+
if (end === 0 && suffix && suffix.length <= path.length) return path;
|
|
278
|
+
const start = path.lastIndexOf("/", end - 1) + 1;
|
|
279
|
+
const base = path.slice(start, end);
|
|
280
|
+
if (suffix.length > base.length && suffix.length <= path.length && suffix.endsWith(base)) return path.slice(start);
|
|
281
|
+
return suffix && suffix.length < base.length && base.endsWith(suffix) ? base.slice(0, -suffix.length) : base;
|
|
282
|
+
}
|
|
283
|
+
var dirname2 = dirname;
|
|
284
|
+
function extname(path) {
|
|
285
|
+
const base = basename(path);
|
|
286
|
+
const position = base.lastIndexOf(".");
|
|
287
|
+
return position <= 0 || base === ".." ? "" : base.slice(position);
|
|
288
|
+
}
|
|
289
|
+
function isAbsolutePath(path) {
|
|
290
|
+
assertString(path);
|
|
291
|
+
return path.startsWith("/");
|
|
292
|
+
}
|
|
293
|
+
function joinPath(...paths) {
|
|
294
|
+
for (const path of paths) assertString(path);
|
|
295
|
+
const joined = paths.filter(Boolean).join("/");
|
|
296
|
+
if (!joined) return ".";
|
|
297
|
+
const absolute = joined.startsWith("/");
|
|
298
|
+
const components2 = [];
|
|
299
|
+
for (const component of joined.split("/")) {
|
|
300
|
+
if (!component || component === ".") continue;
|
|
301
|
+
if (component === "..") {
|
|
302
|
+
if (components2.length && components2.at(-1) !== "..") components2.pop();
|
|
303
|
+
else if (!absolute) components2.push(component);
|
|
304
|
+
} else components2.push(component);
|
|
305
|
+
}
|
|
306
|
+
const result = `${absolute ? "/" : ""}${components2.join("/")}` || ".";
|
|
307
|
+
return joined.endsWith("/") && result !== "/" ? `${result}/` : result;
|
|
308
|
+
}
|
|
309
|
+
var posixPath2 = Object.freeze({ basename, dirname: dirname2, extname, join: joinPath, isAbsolute: isAbsolutePath });
|
|
310
|
+
function complete(stat) {
|
|
311
|
+
const { identityScope, dev, ino } = stat;
|
|
312
|
+
return (typeof identityScope === "object" && identityScope !== null || typeof identityScope === "symbol") && typeof dev === "number" && Number.isSafeInteger(dev) && dev >= 0 && typeof ino === "number" && Number.isSafeInteger(ino) && ino >= 0;
|
|
313
|
+
}
|
|
314
|
+
function compareIdentity(left, right) {
|
|
315
|
+
if (!left || !right || !complete(left) || !complete(right)) return "unknown";
|
|
316
|
+
return left.identityScope === right.identityScope && left.dev === right.dev && left.ino === right.ino ? "same" : "distinct";
|
|
317
|
+
}
|
|
318
|
+
var resolvers = /* @__PURE__ */ new WeakMap();
|
|
319
|
+
var authorities = /* @__PURE__ */ new WeakMap();
|
|
320
|
+
function assertCallbackAuthorityAllowed() {
|
|
321
|
+
if (!platform.callbackAuthorities) throw new FsError("ENOTSUP", { message: "custom comparison authorities require Node context" });
|
|
322
|
+
}
|
|
323
|
+
function registerEntryAuthority(filesystem, authority) {
|
|
324
|
+
if (authorities.has(filesystem)) throw new TypeError("entry authority already registered");
|
|
325
|
+
authorities.set(filesystem, authority);
|
|
326
|
+
}
|
|
327
|
+
async function resolveEntryView(filesystem, path, options = {}) {
|
|
328
|
+
let location = { filesystem, path };
|
|
329
|
+
let readOnly2 = false;
|
|
330
|
+
const visited = /* @__PURE__ */ new Set();
|
|
331
|
+
for (; ; ) {
|
|
332
|
+
options.signal?.throwIfAborted();
|
|
333
|
+
if (visited.has(location.filesystem)) throw new FsError("EIO", { path, message: "cyclic entry view" });
|
|
334
|
+
visited.add(location.filesystem);
|
|
335
|
+
readOnly2 ||= location.readOnly === true || location.filesystem.capabilities.readOnly === true;
|
|
336
|
+
if (location.stat) return { filesystem: location.filesystem, path: location.path, stat: location.stat, readOnly: readOnly2 };
|
|
337
|
+
const resolve = resolvers.get(location.filesystem);
|
|
338
|
+
if (!resolve) {
|
|
339
|
+
let stat;
|
|
340
|
+
let followedPath;
|
|
341
|
+
try {
|
|
342
|
+
followedPath = await location.filesystem.realpath(location.path, options);
|
|
343
|
+
options.signal?.throwIfAborted();
|
|
344
|
+
stat = await location.filesystem.lstat(followedPath, options);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
options.signal?.throwIfAborted();
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
options.signal?.throwIfAborted();
|
|
350
|
+
if (stat.type === "symlink") throw new FsError("EIO", { path, message: "followed entry changed during observation" });
|
|
351
|
+
return { filesystem: location.filesystem, path: followedPath, stat, readOnly: readOnly2 };
|
|
352
|
+
}
|
|
353
|
+
let next;
|
|
354
|
+
try {
|
|
355
|
+
next = await resolve(location.path, options);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
options.signal?.throwIfAborted();
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
options.signal?.throwIfAborted();
|
|
361
|
+
if (next.filesystem === location.filesystem && next.stat) {
|
|
362
|
+
return { filesystem: next.filesystem, path: next.path, stat: next.stat, readOnly: readOnly2 || next.readOnly === true };
|
|
363
|
+
}
|
|
364
|
+
location = next;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function compareResolvedEntries(own, peer, options = {}) {
|
|
368
|
+
options.signal?.throwIfAborted();
|
|
369
|
+
if (comparisonContext.active(options)) return "unknown";
|
|
370
|
+
const identity = compareIdentity(own.stat, peer.stat);
|
|
371
|
+
if (identity !== "unknown") return identity;
|
|
372
|
+
return comparisonContext.run(options, async (options2) => {
|
|
373
|
+
const queried = /* @__PURE__ */ new Set();
|
|
374
|
+
let result = "unknown";
|
|
375
|
+
for (const [left, right] of [[own, peer], [peer, own]]) {
|
|
376
|
+
options2.signal?.throwIfAborted();
|
|
377
|
+
const authority = authorities.get(left.filesystem);
|
|
378
|
+
const key = authority ?? left.filesystem;
|
|
379
|
+
if (queried.has(key)) continue;
|
|
380
|
+
queried.add(key);
|
|
381
|
+
let answer;
|
|
382
|
+
try {
|
|
383
|
+
if (authority) answer = await authority(left, right, options2);
|
|
384
|
+
else if (left.filesystem.compareEntry) {
|
|
385
|
+
assertCallbackAuthorityAllowed();
|
|
386
|
+
answer = await left.filesystem.compareEntry(left.path, right.filesystem, right.path, options2);
|
|
387
|
+
} else continue;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
options2.signal?.throwIfAborted();
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
options2.signal?.throwIfAborted();
|
|
393
|
+
if (answer !== "same" && answer !== "distinct" && answer !== "unknown") {
|
|
394
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "invalid entry comparison answer" });
|
|
395
|
+
}
|
|
396
|
+
if (answer === "unknown") continue;
|
|
397
|
+
if (result !== "unknown" && result !== answer) {
|
|
398
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "conflicting entry comparison answers" });
|
|
399
|
+
}
|
|
400
|
+
result = answer;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function compareEntries(filesystem, path, peer, peerPath, options = {}) {
|
|
406
|
+
options.signal?.throwIfAborted();
|
|
407
|
+
if (comparisonContext.active(options)) return "unknown";
|
|
408
|
+
const own = await resolveEntryView(filesystem, path, options);
|
|
409
|
+
const other = await resolveEntryView(peer, peerPath, options);
|
|
410
|
+
return compareResolvedEntries(own, other, options);
|
|
411
|
+
}
|
|
412
|
+
var entries = /* @__PURE__ */ new WeakMap();
|
|
413
|
+
function getOwnedS3Entry(view) {
|
|
414
|
+
return entries.get(view.filesystem)?.(view);
|
|
415
|
+
}
|
|
416
|
+
var responses = /* @__PURE__ */ new WeakMap();
|
|
417
|
+
var observations = /* @__PURE__ */ new WeakMap();
|
|
418
|
+
function recordOwnedResourceStat(response, filesystem, path, stat) {
|
|
419
|
+
const entry = responses.get(response)?.get(path);
|
|
420
|
+
if (entry) observations.set(stat, { filesystem, path, entry });
|
|
421
|
+
}
|
|
422
|
+
function getOwnedWebDavEntry(view) {
|
|
423
|
+
const observation = observations.get(view.stat);
|
|
424
|
+
return observation?.filesystem === view.filesystem && observation.path === view.path ? observation.entry : void 0;
|
|
425
|
+
}
|
|
426
|
+
function ownedResponseIdentifier(response, path) {
|
|
427
|
+
return responses.get(response)?.get(path)?.identifier;
|
|
428
|
+
}
|
|
429
|
+
var queries = /* @__PURE__ */ new WeakMap();
|
|
430
|
+
var comparisons = /* @__PURE__ */ new WeakMap();
|
|
431
|
+
var callbacks = /* @__PURE__ */ new WeakMap();
|
|
432
|
+
function registerResourceQuery(filesystem, query, baseComparison, callback) {
|
|
433
|
+
comparisons.set(filesystem, baseComparison);
|
|
434
|
+
if (callback) callbacks.set(filesystem, callback);
|
|
435
|
+
queries.set(filesystem, async (path, options) => {
|
|
436
|
+
options.signal?.throwIfAborted();
|
|
437
|
+
const identifier = await query(path, options);
|
|
438
|
+
options.signal?.throwIfAborted();
|
|
439
|
+
return identifier;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function resourceIdentifier(value) {
|
|
443
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"{}|\\^`]+$/.test(value) || /%(?![0-9A-Fa-f]{2})/.test(value) || /[^\x21-\x7e]/.test(value)) throw new Error("invalid DAV:resource-id URI");
|
|
444
|
+
new URL(value);
|
|
445
|
+
if (/^urn:uuid:/i.test(value)) {
|
|
446
|
+
if (!/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
447
|
+
throw new Error("invalid DAV:resource-id UUID");
|
|
448
|
+
}
|
|
449
|
+
return value.toLowerCase();
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
var compareWebDavResources = async (own, peer, options) => {
|
|
454
|
+
options.signal?.throwIfAborted();
|
|
455
|
+
let explicit = false;
|
|
456
|
+
let answer = "unknown";
|
|
457
|
+
const visited = /* @__PURE__ */ new Set();
|
|
458
|
+
for (const [left, right] of [[own, peer], [peer, own]]) {
|
|
459
|
+
const baseComparison = comparisons.get(left.filesystem);
|
|
460
|
+
if (!baseComparison) continue;
|
|
461
|
+
const method = left.filesystem.compareEntry;
|
|
462
|
+
const comparison = method === baseComparison ? callbacks.get(left.filesystem) : method;
|
|
463
|
+
if (method === baseComparison && !comparison) continue;
|
|
464
|
+
explicit = true;
|
|
465
|
+
if (!comparison || visited.has(left.filesystem)) continue;
|
|
466
|
+
visited.add(left.filesystem);
|
|
467
|
+
options.signal?.throwIfAborted();
|
|
468
|
+
assertCallbackAuthorityAllowed();
|
|
469
|
+
const result = await comparison.call(left.filesystem, left.path, right.filesystem, right.path, options);
|
|
470
|
+
options.signal?.throwIfAborted();
|
|
471
|
+
if (result !== "same" && result !== "distinct" && result !== "unknown") {
|
|
472
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "invalid explicit WebDAV comparison" });
|
|
473
|
+
}
|
|
474
|
+
if (result === "unknown") continue;
|
|
475
|
+
if (answer !== "unknown" && answer !== result) {
|
|
476
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "conflicting explicit WebDAV comparisons" });
|
|
477
|
+
}
|
|
478
|
+
answer = result;
|
|
479
|
+
}
|
|
480
|
+
const builtin = await compareProtocolEntries(own, peer, options);
|
|
481
|
+
if (builtin === "same" && answer === "distinct") {
|
|
482
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "explicit WebDAV comparison contradicts built-in identity" });
|
|
483
|
+
}
|
|
484
|
+
if (builtin === "same") return "same";
|
|
485
|
+
return explicit ? answer : builtin;
|
|
486
|
+
};
|
|
487
|
+
async function compareProtocolEntries(own, peer, options) {
|
|
488
|
+
const left = getOwnedWebDavEntry(own);
|
|
489
|
+
const right = getOwnedWebDavEntry(peer);
|
|
490
|
+
const ownQuery = queries.get(own.filesystem);
|
|
491
|
+
const peerQuery = queries.get(peer.filesystem);
|
|
492
|
+
if (!ownQuery || !peerQuery) return "unknown";
|
|
493
|
+
options.signal?.throwIfAborted();
|
|
494
|
+
const ownId = await ownQuery(own.path, options);
|
|
495
|
+
options.signal?.throwIfAborted();
|
|
496
|
+
const peerId = await peerQuery(peer.path, options);
|
|
497
|
+
options.signal?.throwIfAborted();
|
|
498
|
+
if (ownId === void 0 || peerId === void 0) return "unknown";
|
|
499
|
+
if (left && right) {
|
|
500
|
+
const same = left.storage === right.storage && left.resource === right.resource;
|
|
501
|
+
if (same !== (ownId === peerId)) throw new FsError("EIO", { path: own.path, dest: peer.path, message: "conflicting owned and protocol WebDAV identities" });
|
|
502
|
+
}
|
|
503
|
+
return ownId === peerId ? "same" : "distinct";
|
|
504
|
+
}
|
|
505
|
+
function directoryEntryLimit(options, path) {
|
|
506
|
+
options.signal?.throwIfAborted();
|
|
507
|
+
const limit = options.maxEntries;
|
|
508
|
+
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
509
|
+
throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer" });
|
|
510
|
+
}
|
|
511
|
+
return limit;
|
|
512
|
+
}
|
|
513
|
+
function admitDirectoryEntries(count, limit, path) {
|
|
514
|
+
if (limit !== void 0 && count > limit) {
|
|
515
|
+
throw new FsError("EFBIG", { syscall: "readdir", path, message: "directory entry limit exceeded" });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function components(path, signal) {
|
|
519
|
+
const result = [];
|
|
520
|
+
let start = 0;
|
|
521
|
+
for (let offset = 0; offset <= path.length; offset++) {
|
|
522
|
+
signal?.throwIfAborted();
|
|
523
|
+
if (offset !== path.length && path.charCodeAt(offset) !== 47) continue;
|
|
524
|
+
if (offset > start) result.push({ name: path.slice(start, offset), start });
|
|
525
|
+
start = offset + 1;
|
|
526
|
+
}
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
function resolveMissingTarget(root, path, signal) {
|
|
530
|
+
const fail4 = (code, failedPath = path) => {
|
|
531
|
+
throw new FsError(code, { syscall: "realpath", path: failedPath });
|
|
532
|
+
};
|
|
533
|
+
const origin = components(path, signal);
|
|
534
|
+
const initial = { node: root, name: "" };
|
|
535
|
+
let position = initial;
|
|
536
|
+
let links = 0;
|
|
537
|
+
let missing = origin.length;
|
|
538
|
+
let missingLink = false;
|
|
539
|
+
for (let index = 0; index <= origin.length; index++) {
|
|
540
|
+
const token = origin[index];
|
|
541
|
+
if (!token && path.length > 0 && !path.endsWith("/")) break;
|
|
542
|
+
const before = position;
|
|
543
|
+
const frames = [{ names: [token ?? { name: ".", start: path.length }], next: 0 }];
|
|
544
|
+
let first = true;
|
|
545
|
+
let originLink = false;
|
|
546
|
+
let found = true;
|
|
547
|
+
while (frames.length > 0) {
|
|
548
|
+
signal?.throwIfAborted();
|
|
549
|
+
const frame = frames.at(-1);
|
|
550
|
+
if (!frame) break;
|
|
551
|
+
const component = frame.names[frame.next++];
|
|
552
|
+
if (!component) {
|
|
553
|
+
frames.pop();
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const current = position.node;
|
|
557
|
+
if (current.type !== "directory") return fail4("ENOTDIR");
|
|
558
|
+
if ((current.mode >> 6 & 1) !== 1) return fail4("EACCES");
|
|
559
|
+
const name = component.name;
|
|
560
|
+
if (name === ".") {
|
|
561
|
+
first = false;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (name === "..") {
|
|
565
|
+
position = position.parent ?? initial;
|
|
566
|
+
first = false;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (new TextEncoder().encode(name).byteLength > 255) return fail4("ENAMETOOLONG");
|
|
570
|
+
const node = current.entries.get(name);
|
|
571
|
+
if (!node) {
|
|
572
|
+
found = false;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
if (first) originLink = node.type === "symlink";
|
|
576
|
+
first = false;
|
|
577
|
+
if (node.type === "symlink") {
|
|
578
|
+
if (++links > 40) return fail4("ELOOP");
|
|
579
|
+
const names2 = components(node.target, signal);
|
|
580
|
+
if (node.target.endsWith("/")) names2.push({ name: ".", start: node.target.length });
|
|
581
|
+
if (node.target.startsWith("/")) position = initial;
|
|
582
|
+
frames.push({ names: names2, next: 0 });
|
|
583
|
+
} else position = { node, name, parent: position };
|
|
584
|
+
}
|
|
585
|
+
if (!found) {
|
|
586
|
+
position = before;
|
|
587
|
+
missing = index;
|
|
588
|
+
missingLink = originLink;
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (missingLink) {
|
|
593
|
+
let end = path.length;
|
|
594
|
+
for (let count = origin.length; count > missing; count--) {
|
|
595
|
+
signal?.throwIfAborted();
|
|
596
|
+
if (count === missing + 1 && path[end - 1] !== "/") return fail4("ENOENT", path.slice(0, end));
|
|
597
|
+
const last = origin[count - 1];
|
|
598
|
+
if (!last) break;
|
|
599
|
+
const boundary = last.start - 1;
|
|
600
|
+
end = boundary <= 0 ? 1 : boundary === 1 && path.startsWith("/") ? 2 : boundary;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const names = [];
|
|
604
|
+
for (let current = position; current?.parent; current = current.parent) {
|
|
605
|
+
signal?.throwIfAborted();
|
|
606
|
+
names.push(current.name);
|
|
607
|
+
}
|
|
608
|
+
names.reverse();
|
|
609
|
+
for (let index = missing; index < origin.length; index++) {
|
|
610
|
+
signal?.throwIfAborted();
|
|
611
|
+
const name = origin[index]?.name;
|
|
612
|
+
if (name === "..") names.pop();
|
|
613
|
+
else if (name && name !== ".") names.push(name);
|
|
614
|
+
}
|
|
615
|
+
return `/${names.join("/")}`;
|
|
616
|
+
}
|
|
617
|
+
var MemoryLedger = class {
|
|
618
|
+
constructor(limits) {
|
|
619
|
+
this.limits = limits;
|
|
620
|
+
}
|
|
621
|
+
limits;
|
|
622
|
+
retainedBytes = 0;
|
|
623
|
+
metadataUnits = 0;
|
|
624
|
+
get availableBytes() {
|
|
625
|
+
return this.limits.maxRetainedBytes - this.retainedBytes;
|
|
626
|
+
}
|
|
627
|
+
fileSize(length, syscall, path) {
|
|
628
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > this.limits.maxFileBytes) {
|
|
629
|
+
throw new FsError("EFBIG", { syscall, path });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
check(bytes, units, syscall, path) {
|
|
633
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.availableBytes || !Number.isSafeInteger(units) || units < 0 || units > this.limits.maxMetadataUnits - this.metadataUnits) {
|
|
634
|
+
throw new FsError("ENOSPC", { syscall, path });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
reserve(bytes, units, syscall, path) {
|
|
638
|
+
this.check(bytes, units, syscall, path);
|
|
639
|
+
this.retainedBytes += bytes;
|
|
640
|
+
this.metadataUnits += units;
|
|
641
|
+
}
|
|
642
|
+
release(bytes, units) {
|
|
643
|
+
if (bytes < 0 || units < 0 || bytes > this.retainedBytes || units > this.metadataUnits) {
|
|
644
|
+
throw new Error("Invalid Memory filesystem reservation release");
|
|
645
|
+
}
|
|
646
|
+
this.retainedBytes -= bytes;
|
|
647
|
+
this.metadataUnits -= units;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
var MemoryAllocation = class {
|
|
651
|
+
constructor(data, ledger) {
|
|
652
|
+
this.data = data;
|
|
653
|
+
this.ledger = ledger;
|
|
654
|
+
}
|
|
655
|
+
data;
|
|
656
|
+
ledger;
|
|
657
|
+
references = 1;
|
|
658
|
+
retain() {
|
|
659
|
+
this.references++;
|
|
660
|
+
}
|
|
661
|
+
release() {
|
|
662
|
+
if (this.references <= 0) throw new Error("Memory allocation already released");
|
|
663
|
+
if (--this.references === 0) this.ledger.release(this.data.byteLength, 0);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
function readConfigRecord(value, label, keys) {
|
|
667
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
668
|
+
throw new TypeError(`${label} must be a record.`);
|
|
669
|
+
}
|
|
670
|
+
const prototype = Object.getPrototypeOf(value);
|
|
671
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
672
|
+
throw new TypeError(`${label} must be a plain record.`);
|
|
673
|
+
}
|
|
674
|
+
const record2 = /* @__PURE__ */ Object.create(null);
|
|
675
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
676
|
+
if (typeof key !== "string" || keys !== void 0 && !keys.includes(key)) {
|
|
677
|
+
throw new TypeError(`Unknown ${label}: ${String(key)}`);
|
|
678
|
+
}
|
|
679
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
680
|
+
if (!("value" in property)) throw new TypeError(`${label}.${key} must be a data property.`);
|
|
681
|
+
record2[key] = property.value;
|
|
682
|
+
}
|
|
683
|
+
return Object.freeze(record2);
|
|
684
|
+
}
|
|
685
|
+
var defaultMemoryFileSystemLimits = Object.freeze({
|
|
686
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
687
|
+
maxRetainedBytes: 64 * 1024 * 1024,
|
|
688
|
+
maxMetadataUnits: 1e4
|
|
689
|
+
});
|
|
690
|
+
function normalizeMemoryFileSystemLimits(options) {
|
|
691
|
+
const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
|
|
692
|
+
const record2 = readConfigRecord(options, "memory option", keys);
|
|
693
|
+
const limits = { ...defaultMemoryFileSystemLimits };
|
|
694
|
+
for (const key of keys) {
|
|
695
|
+
const value = Object.hasOwn(record2, key) ? record2[key] : limits[key];
|
|
696
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
|
|
697
|
+
throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
|
|
698
|
+
}
|
|
699
|
+
limits[key] = value;
|
|
700
|
+
}
|
|
701
|
+
return Object.freeze(limits);
|
|
702
|
+
}
|
|
703
|
+
var typeModes = { file: 32768, directory: 16384, symlink: 40960 };
|
|
704
|
+
var ext4HtreeEof64 = (1n << 63n) - 1n;
|
|
705
|
+
var ownedStats = /* @__PURE__ */ new WeakMap();
|
|
706
|
+
var ownedStores = /* @__PURE__ */ new WeakMap();
|
|
707
|
+
var registeredAuthorities = /* @__PURE__ */ new WeakSet();
|
|
708
|
+
var compareOwnedMemory = async (own, peer, options) => {
|
|
709
|
+
options.signal?.throwIfAborted();
|
|
710
|
+
let explicit = false;
|
|
711
|
+
let answer = "unknown";
|
|
712
|
+
const visited = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const [left, right] of [[own, peer], [peer, own]]) {
|
|
714
|
+
if (!registeredAuthorities.has(left.filesystem) || visited.has(left.filesystem)) continue;
|
|
715
|
+
visited.add(left.filesystem);
|
|
716
|
+
const comparison = left.filesystem.compareEntry;
|
|
717
|
+
if (comparison === memoryImplementation.compareEntry?.value) continue;
|
|
718
|
+
explicit = true;
|
|
719
|
+
if (!comparison) continue;
|
|
720
|
+
options.signal?.throwIfAborted();
|
|
721
|
+
assertCallbackAuthorityAllowed();
|
|
722
|
+
const result = await comparison.call(left.filesystem, left.path, right.filesystem, right.path, options);
|
|
723
|
+
options.signal?.throwIfAborted();
|
|
724
|
+
if (result !== "same" && result !== "distinct" && result !== "unknown") {
|
|
725
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "invalid explicit Memory comparison" });
|
|
726
|
+
}
|
|
727
|
+
if (result === "unknown") continue;
|
|
728
|
+
if (answer !== "unknown" && answer !== result) {
|
|
729
|
+
throw new FsError("EIO", { path: own.path, dest: peer.path, message: "conflicting explicit Memory comparisons" });
|
|
730
|
+
}
|
|
731
|
+
answer = result;
|
|
732
|
+
}
|
|
733
|
+
if (explicit) return answer;
|
|
734
|
+
const owner = ownedStores.get(own.filesystem);
|
|
735
|
+
const observation = ownedStats.get(own.stat);
|
|
736
|
+
if (!owner?.intact() || observation?.filesystem !== own.filesystem || observation.path !== own.path || observation.root !== owner.root) return "unknown";
|
|
737
|
+
const qualified = getOwnedS3Entry(peer) ?? getOwnedWebDavEntry(peer);
|
|
738
|
+
options.signal?.throwIfAborted();
|
|
739
|
+
return qualified ? "distinct" : "unknown";
|
|
740
|
+
};
|
|
741
|
+
var MemoryFileSystem = class _MemoryFileSystem {
|
|
742
|
+
capabilities = ((filesystem) => {
|
|
743
|
+
return Object.freeze({
|
|
744
|
+
read: true,
|
|
745
|
+
stat: true,
|
|
746
|
+
readdir: true,
|
|
747
|
+
realpath: true,
|
|
748
|
+
access: true,
|
|
749
|
+
write: true,
|
|
750
|
+
append: true,
|
|
751
|
+
exclusiveCreate: true,
|
|
752
|
+
explicitDirectories: true,
|
|
753
|
+
implicitDirectories: false,
|
|
754
|
+
mkdir: true,
|
|
755
|
+
recursiveMkdir: true,
|
|
756
|
+
remove: true,
|
|
757
|
+
removeDirectory: true,
|
|
758
|
+
recursiveRemove: true,
|
|
759
|
+
rename: true,
|
|
760
|
+
atomicRenameNoReplace: true,
|
|
761
|
+
copy: true,
|
|
762
|
+
exclusiveCopy: true,
|
|
763
|
+
readlink: true,
|
|
764
|
+
truncate: true,
|
|
765
|
+
streamingAppend: true,
|
|
766
|
+
randomAccessWrite: true,
|
|
767
|
+
readOnly: false,
|
|
768
|
+
symlinks: true,
|
|
769
|
+
hardlinks: true,
|
|
770
|
+
permissions: true,
|
|
771
|
+
timestamps: true,
|
|
772
|
+
atomicRename: true,
|
|
773
|
+
atomicFileStaging: true,
|
|
774
|
+
atomicFileMutation: true,
|
|
775
|
+
atomicDirectoryMetadata: true,
|
|
776
|
+
streamingRead: true,
|
|
777
|
+
retainedRead: true,
|
|
778
|
+
get retainedResize() {
|
|
779
|
+
return stockRetainedResize(filesystem);
|
|
780
|
+
},
|
|
781
|
+
streamingWrite: true,
|
|
782
|
+
get descriptorWriteStream() {
|
|
783
|
+
return stockDescriptorWrite(filesystem);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
})(this);
|
|
787
|
+
identityScope = /* @__PURE__ */ Symbol();
|
|
788
|
+
nextInode = 1;
|
|
789
|
+
ledger;
|
|
790
|
+
root;
|
|
791
|
+
constructor(options = {}) {
|
|
792
|
+
this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
|
|
793
|
+
this.ledger.reserve(0, 1, "mkdir", "/");
|
|
794
|
+
this.root = this.directory(493);
|
|
795
|
+
const root = this.root;
|
|
796
|
+
ownedStores.set(this, {
|
|
797
|
+
root,
|
|
798
|
+
ledger: this.ledger,
|
|
799
|
+
capabilities: this.capabilities,
|
|
800
|
+
intact: () => this.root === root
|
|
801
|
+
});
|
|
802
|
+
if (this.compareEntry === memoryImplementation.compareEntry?.value) {
|
|
803
|
+
registeredAuthorities.add(this);
|
|
804
|
+
registerEntryAuthority(this, compareOwnedMemory);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
compareEntry(path, peer, peerPath, options = {}) {
|
|
808
|
+
return compareEntries(this, path, peer, peerPath, options);
|
|
809
|
+
}
|
|
810
|
+
canonicalizeMissingTarget(path, options = {}) {
|
|
811
|
+
options.signal?.throwIfAborted();
|
|
812
|
+
const owner = ownedStores.get(this);
|
|
813
|
+
if (!owner || Object.getPrototypeOf(this) !== _MemoryFileSystem.prototype || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root) return void 0;
|
|
814
|
+
for (const name of ["realpath", "lstat", "resolve", "permission", "validatePath", "fail", "snapshot"]) {
|
|
815
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, name) ?? Object.getOwnPropertyDescriptor(_MemoryFileSystem.prototype, name);
|
|
816
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value) return void 0;
|
|
817
|
+
}
|
|
818
|
+
if (path !== "") this.validatePath(path, "realpath");
|
|
819
|
+
return resolveMissingTarget(owner.root, path || ".", options.signal);
|
|
820
|
+
}
|
|
821
|
+
metadata(mode2) {
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
return {
|
|
824
|
+
mode: mode2,
|
|
825
|
+
ino: this.nextInode++,
|
|
826
|
+
nlink: 1,
|
|
827
|
+
references: 0,
|
|
828
|
+
revision: 0,
|
|
829
|
+
atimeMs: now,
|
|
830
|
+
mtimeMs: now,
|
|
831
|
+
ctimeMs: now,
|
|
832
|
+
birthtimeMs: now
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
directory(mode2) {
|
|
836
|
+
return { ...this.metadata(typeModes.directory | mode2), type: "directory", entries: /* @__PURE__ */ new Map() };
|
|
837
|
+
}
|
|
838
|
+
addNode(parent, name, create, syscall, path, retainedBytes = 0) {
|
|
839
|
+
const bytes = name.length * 2 + retainedBytes;
|
|
840
|
+
this.ledger.reserve(bytes, 2, syscall, path);
|
|
841
|
+
try {
|
|
842
|
+
const node = create();
|
|
843
|
+
parent.entries.set(name, node);
|
|
844
|
+
this.changed(parent);
|
|
845
|
+
return node;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
this.ledger.release(bytes, 2);
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
releaseNode(node) {
|
|
852
|
+
if (node.nlink !== 0 || node.references !== 0) return;
|
|
853
|
+
this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
|
|
854
|
+
if (node.type === "file") node.allocation.release();
|
|
855
|
+
}
|
|
856
|
+
releaseReference(node, path) {
|
|
857
|
+
node.references--;
|
|
858
|
+
this.ledger.release(path.length * 2, 1);
|
|
859
|
+
this.releaseNode(node);
|
|
860
|
+
}
|
|
861
|
+
replaceData(node, allocation, length = allocation.data.byteLength) {
|
|
862
|
+
const previous = node.allocation;
|
|
863
|
+
node.data = allocation.data.subarray(0, length);
|
|
864
|
+
node.allocation = allocation;
|
|
865
|
+
previous.release();
|
|
866
|
+
}
|
|
867
|
+
fail(code, syscall, path, dest) {
|
|
868
|
+
throw new FsError(code, { syscall, path, ...dest === void 0 ? {} : { dest } });
|
|
869
|
+
}
|
|
870
|
+
validatePath(path, syscall) {
|
|
871
|
+
if (typeof path !== "string" || path.includes("\0")) this.fail("EINVAL", syscall, path);
|
|
872
|
+
if (path.length === 0) this.fail("ENOENT", syscall, path);
|
|
873
|
+
}
|
|
874
|
+
permission(node, mask, syscall, path) {
|
|
875
|
+
if ((node.mode >> 6 & mask) !== mask) this.fail("EACCES", syscall, path);
|
|
876
|
+
}
|
|
877
|
+
mode(mode2, fallback, syscall, path) {
|
|
878
|
+
const value = mode2 ?? fallback;
|
|
879
|
+
if (!Number.isInteger(value) || value < 0 || value > 4095) this.fail("EINVAL", syscall, path);
|
|
880
|
+
return value;
|
|
881
|
+
}
|
|
882
|
+
integer(value, syscall, path) {
|
|
883
|
+
if (!Number.isSafeInteger(value) || value < 0) this.fail("EINVAL", syscall, path);
|
|
884
|
+
}
|
|
885
|
+
changed(node) {
|
|
886
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
887
|
+
node.mtimeMs = node.ctimeMs = Date.now();
|
|
888
|
+
}
|
|
889
|
+
resolve(path, syscall, options = {}) {
|
|
890
|
+
this.validatePath(path, syscall);
|
|
891
|
+
const pending = path.split("/").filter(Boolean);
|
|
892
|
+
if (path.endsWith("/")) pending.push("");
|
|
893
|
+
const stack = [{ node: this.root, name: "" }];
|
|
894
|
+
let links = 0;
|
|
895
|
+
while (pending.length > 0) {
|
|
896
|
+
const component = pending.shift();
|
|
897
|
+
const current = stack[stack.length - 1].node;
|
|
898
|
+
if (current.type !== "directory") this.fail("ENOTDIR", syscall, path);
|
|
899
|
+
if (!(component === "" && pending.length === 0 && options.resizeCreate !== void 0)) this.permission(current, 1, syscall, path);
|
|
900
|
+
if (component === "." || component === "") continue;
|
|
901
|
+
if (component === "..") {
|
|
902
|
+
if (stack.length > 1) stack.pop();
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (new TextEncoder().encode(component).byteLength > 255) this.fail("ENAMETOOLONG", syscall, path);
|
|
906
|
+
if (options.resizeCreate === true && pending.length === 1 && pending[0] === "") this.fail("EISDIR", syscall, path);
|
|
907
|
+
let node = current.entries.get(component);
|
|
908
|
+
if (!node && options.createDirectories !== void 0) {
|
|
909
|
+
this.permission(current, 3, syscall, path);
|
|
910
|
+
const mode2 = options.createDirectories;
|
|
911
|
+
node = this.addNode(current, component, () => this.directory(mode2), syscall, path);
|
|
912
|
+
}
|
|
913
|
+
if (!node) {
|
|
914
|
+
if (options.allowMissing && pending.length === 0) {
|
|
915
|
+
return { node, parent: current, name: component, path: [...stack.map((entry2) => entry2.name), component].join("/") };
|
|
916
|
+
}
|
|
917
|
+
this.fail("ENOENT", syscall, path);
|
|
918
|
+
}
|
|
919
|
+
if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
|
|
920
|
+
if (++links > 40) this.fail("ELOOP", syscall, path);
|
|
921
|
+
const target = node.target.split("/").filter(Boolean);
|
|
922
|
+
if (node.target.endsWith("/")) target.push("");
|
|
923
|
+
if (options.resizeCreate !== void 0 && target.at(-1) === "" && pending[0] === "") target.pop();
|
|
924
|
+
pending.unshift(...target);
|
|
925
|
+
if (node.target.startsWith("/")) stack.splice(1);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
stack.push({ node, name: component });
|
|
929
|
+
}
|
|
930
|
+
const entry = stack[stack.length - 1];
|
|
931
|
+
return {
|
|
932
|
+
node: entry.node,
|
|
933
|
+
parent: stack[stack.length - 2]?.node ?? this.root,
|
|
934
|
+
name: entry.name,
|
|
935
|
+
path: stack.map((part) => part.name).join("/") || "/"
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
file(path, syscall) {
|
|
939
|
+
const node = this.resolve(path, syscall).node;
|
|
940
|
+
if (node.type !== "file") this.fail("EISDIR", syscall, path);
|
|
941
|
+
return node;
|
|
942
|
+
}
|
|
943
|
+
entry(path, syscall, allowMissing = false) {
|
|
944
|
+
this.validatePath(path, syscall);
|
|
945
|
+
const location = this.resolve(path.replace(/\/+$/, "") || "/", syscall, { followFinal: false, allowMissing });
|
|
946
|
+
if (path.endsWith("/") && location.node && location.node.type !== "directory") this.fail("ENOTDIR", syscall, path);
|
|
947
|
+
return location;
|
|
948
|
+
}
|
|
949
|
+
terminalDot(path) {
|
|
950
|
+
return /(?:^|\/)\.{1,2}\/*$/.test(path);
|
|
951
|
+
}
|
|
952
|
+
snapshot(node) {
|
|
953
|
+
return {
|
|
954
|
+
type: node.type,
|
|
955
|
+
...Number.isSafeInteger(node.revision) ? { revision: node.revision } : {},
|
|
956
|
+
preferredIoBlockSize: 4096,
|
|
957
|
+
size: node.type === "file" ? node.data.byteLength : node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0,
|
|
958
|
+
mode: node.mode,
|
|
959
|
+
...ownedStores.get(this)?.intact() ? { identityScope: this.identityScope } : {},
|
|
960
|
+
ino: node.ino,
|
|
961
|
+
dev: 0,
|
|
962
|
+
uid: 0,
|
|
963
|
+
gid: 0,
|
|
964
|
+
nlink: node.type === "directory" && node.nlink !== 0 ? 2 + [...node.entries.values()].filter((entry) => entry.type === "directory").length : node.nlink,
|
|
965
|
+
atimeMs: node.atimeMs,
|
|
966
|
+
mtimeMs: node.mtimeMs,
|
|
967
|
+
ctimeMs: node.ctimeMs,
|
|
968
|
+
birthtimeMs: node.birthtimeMs
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
bytes(data, syscall, path) {
|
|
972
|
+
const allocation = this.allocate(data.byteLength, syscall, path);
|
|
973
|
+
try {
|
|
974
|
+
allocation.data.set(data);
|
|
975
|
+
return allocation;
|
|
976
|
+
} catch (error) {
|
|
977
|
+
allocation.release();
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
allocate(length, syscall, path) {
|
|
982
|
+
this.ledger.fileSize(length, syscall, path);
|
|
983
|
+
this.ledger.reserve(length, 0, syscall, path);
|
|
984
|
+
try {
|
|
985
|
+
return new MemoryAllocation(new Uint8Array(length), this.ledger);
|
|
986
|
+
} catch (cause) {
|
|
987
|
+
this.ledger.release(length, 0);
|
|
988
|
+
throw new FsError("EFBIG", { syscall, path, cause });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
prepareWrite(path, options, syscall) {
|
|
992
|
+
const flag = options.flag ?? "w";
|
|
993
|
+
if (!["w", "wx", "a", "ax"].includes(flag)) this.fail("EINVAL", syscall, path);
|
|
994
|
+
const mode2 = this.mode(options.mode, 438, syscall, path);
|
|
995
|
+
const exclusive = flag === "wx" || flag === "ax";
|
|
996
|
+
const location = this.resolve(path, syscall, { followFinal: !exclusive, allowMissing: true });
|
|
997
|
+
if (location.node) {
|
|
998
|
+
if (exclusive) this.fail("EEXIST", syscall, path);
|
|
999
|
+
if (location.node.type !== "file") this.fail("EISDIR", syscall, path);
|
|
1000
|
+
this.permission(location.node, 2, syscall, path);
|
|
1001
|
+
} else {
|
|
1002
|
+
this.permission(location.parent, 3, syscall, path);
|
|
1003
|
+
}
|
|
1004
|
+
return { location, mode: mode2, append: flag === "a" || flag === "ax" };
|
|
1005
|
+
}
|
|
1006
|
+
openWrite(path, options, syscall, target = this.prepareWrite(path, options, syscall)) {
|
|
1007
|
+
const { location, mode: mode2 } = target;
|
|
1008
|
+
if (location.node) return location.node;
|
|
1009
|
+
return this.addNode(location.parent, location.name, () => {
|
|
1010
|
+
const allocation = new MemoryAllocation(new Uint8Array(), this.ledger);
|
|
1011
|
+
return { ...this.metadata(typeModes.file | mode2), type: "file", data: allocation.data, allocation };
|
|
1012
|
+
}, syscall, path);
|
|
1013
|
+
}
|
|
1014
|
+
writeData(path, data, options, syscall) {
|
|
1015
|
+
if (!(data instanceof Uint8Array)) throw new TypeError("Memory files require Uint8Array data");
|
|
1016
|
+
const target = this.prepareWrite(path, options, syscall);
|
|
1017
|
+
const current = target.location.node;
|
|
1018
|
+
const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
|
|
1019
|
+
this.ledger.fileSize(length, syscall, path);
|
|
1020
|
+
const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
|
|
1021
|
+
this.ledger.check(data.byteLength + growth + (current ? 0 : target.location.name.length * 2), current ? 0 : 2, syscall, path);
|
|
1022
|
+
const copied = this.bytes(data, syscall, path);
|
|
1023
|
+
let allocation = copied;
|
|
1024
|
+
let transferred = false;
|
|
1025
|
+
try {
|
|
1026
|
+
if (!current) {
|
|
1027
|
+
if (target.append && length > 0) {
|
|
1028
|
+
const capacity = Math.min(
|
|
1029
|
+
Math.max(length, 64),
|
|
1030
|
+
this.ledger.limits.maxFileBytes,
|
|
1031
|
+
this.ledger.availableBytes - target.location.name.length * 2
|
|
1032
|
+
);
|
|
1033
|
+
allocation = this.allocate(capacity, syscall, path);
|
|
1034
|
+
allocation.data.set(copied.data);
|
|
1035
|
+
}
|
|
1036
|
+
const node = this.addNode(target.location.parent, target.location.name, () => ({
|
|
1037
|
+
...this.metadata(typeModes.file | target.mode),
|
|
1038
|
+
type: "file",
|
|
1039
|
+
data: allocation.data.subarray(0, length),
|
|
1040
|
+
allocation
|
|
1041
|
+
}), syscall, path);
|
|
1042
|
+
transferred = true;
|
|
1043
|
+
return node;
|
|
1044
|
+
}
|
|
1045
|
+
if (target.append) this.writeAt(current, copied.data, current.data.byteLength, syscall, path);
|
|
1046
|
+
else {
|
|
1047
|
+
this.replaceData(current, copied);
|
|
1048
|
+
transferred = true;
|
|
1049
|
+
this.changed(current);
|
|
1050
|
+
}
|
|
1051
|
+
return current;
|
|
1052
|
+
} finally {
|
|
1053
|
+
if (allocation !== copied && !transferred) allocation.release();
|
|
1054
|
+
if (!transferred || allocation !== copied) copied.release();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
writeAt(node, data, position, syscall, path) {
|
|
1058
|
+
if (data.byteLength === 0) {
|
|
1059
|
+
this.changed(node);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const end = position + data.byteLength;
|
|
1063
|
+
this.ledger.fileSize(end, syscall, path);
|
|
1064
|
+
const length = Math.max(node.data.byteLength, end);
|
|
1065
|
+
let allocation = node.allocation;
|
|
1066
|
+
if (length > allocation.data.byteLength) {
|
|
1067
|
+
this.ledger.check(length, 0, syscall, path);
|
|
1068
|
+
const capacity = Math.min(
|
|
1069
|
+
Math.max(length, node.data.byteLength * 2, 64),
|
|
1070
|
+
this.ledger.limits.maxFileBytes,
|
|
1071
|
+
this.ledger.availableBytes
|
|
1072
|
+
);
|
|
1073
|
+
allocation = this.allocate(capacity, syscall, path);
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
const storage = allocation.data;
|
|
1077
|
+
if (allocation !== node.allocation) storage.set(node.data);
|
|
1078
|
+
if (position > node.data.byteLength) storage.fill(0, node.data.byteLength, position);
|
|
1079
|
+
storage.set(data, position);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
if (allocation !== node.allocation) allocation.release();
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
if (allocation !== node.allocation) this.replaceData(node, allocation, length);
|
|
1085
|
+
else node.data = allocation.data.subarray(0, length);
|
|
1086
|
+
this.changed(node);
|
|
1087
|
+
}
|
|
1088
|
+
async readFile(path, options = {}) {
|
|
1089
|
+
options.signal?.throwIfAborted();
|
|
1090
|
+
if (options.maxBytes !== void 0) this.integer(options.maxBytes, "readFile", path);
|
|
1091
|
+
const node = this.file(path, "readFile");
|
|
1092
|
+
this.permission(node, 4, "readFile", path);
|
|
1093
|
+
if (options.maxBytes !== void 0 && node.data.byteLength > options.maxBytes) this.fail("EFBIG", "readFile", path);
|
|
1094
|
+
node.atimeMs = Date.now();
|
|
1095
|
+
return new Uint8Array(node.data);
|
|
1096
|
+
}
|
|
1097
|
+
expectEntry(node, expected, path, unchanged = true) {
|
|
1098
|
+
if (expected === null) {
|
|
1099
|
+
if (node) this.fail("EAGAIN", "fileStaging", path);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!node) this.fail("EAGAIN", "fileStaging", path);
|
|
1103
|
+
const current = this.snapshot(node);
|
|
1104
|
+
if (!expected.identityScope || expected.identityScope !== current.identityScope || !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev)) this.fail("ENOTSUP", "fileStaging", path);
|
|
1105
|
+
if (current.ino !== expected.ino || current.dev !== expected.dev || current.type !== expected.type) this.fail("EAGAIN", "fileStaging", path);
|
|
1106
|
+
if (unchanged) {
|
|
1107
|
+
if (!Number.isSafeInteger(expected.revision) || !Number.isSafeInteger(current.revision)) this.fail("ENOTSUP", "fileStaging", path);
|
|
1108
|
+
if (current.revision !== expected.revision || current.size !== expected.size || current.mode !== expected.mode || current.nlink !== expected.nlink || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs) this.fail("EAGAIN", "fileStaging", path);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
stagingLocations(staging) {
|
|
1112
|
+
const parent = this.entry(staging.parent.path, "fileStaging");
|
|
1113
|
+
this.expectEntry(parent.node, staging.parent.stat, staging.parent.path, false);
|
|
1114
|
+
if (parent.node.type !== "directory") this.fail("ENOTDIR", "fileStaging", staging.parent.path);
|
|
1115
|
+
const directory = this.entry(staging.directory.path, "fileStaging");
|
|
1116
|
+
this.expectEntry(directory.node, staging.directory.stat, staging.directory.path, false);
|
|
1117
|
+
if (directory.parent !== parent.node || directory.node.type !== "directory" || (directory.node.mode & 511) !== 448) this.fail("EAGAIN", "fileStaging", staging.directory.path);
|
|
1118
|
+
const file = this.entry(staging.file.path, "fileStaging", true);
|
|
1119
|
+
if (file.parent !== directory.node || !["file", "symlink"].includes(staging.file.stat.type)) this.fail("EAGAIN", "fileStaging", staging.file.path);
|
|
1120
|
+
return { directory, file };
|
|
1121
|
+
}
|
|
1122
|
+
async createStagedFile(directoryPath, name, content, options) {
|
|
1123
|
+
options.signal?.throwIfAborted();
|
|
1124
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0")) this.fail("EINVAL", "createStagedFile", directoryPath);
|
|
1125
|
+
const location = this.entry(directoryPath, "createStagedFile", true);
|
|
1126
|
+
if (location.node) this.fail("EEXIST", "createStagedFile", directoryPath);
|
|
1127
|
+
this.expectEntry(location.parent, options.parent, directoryPath, false);
|
|
1128
|
+
this.permission(location.parent, 3, "createStagedFile", directoryPath);
|
|
1129
|
+
const mode2 = this.mode(options.mode, content.type === "file" ? 438 : 511, "createStagedFile", directoryPath);
|
|
1130
|
+
for (const time of [options.atimeMs, options.mtimeMs]) if (time !== void 0 && !Number.isFinite(time)) this.fail("EINVAL", "createStagedFile", directoryPath);
|
|
1131
|
+
if (content.type === "file" && !(content.data instanceof Uint8Array)) throw new TypeError("Staged files require Uint8Array data");
|
|
1132
|
+
if (content.type === "symlink") this.validatePath(content.target, "createStagedFile");
|
|
1133
|
+
const retained = (location.name.length + name.length) * 2 + (content.type === "symlink" ? content.target.length * 2 : 0);
|
|
1134
|
+
this.ledger.reserve(retained, 4, "createStagedFile", directoryPath);
|
|
1135
|
+
let allocation;
|
|
1136
|
+
let directory;
|
|
1137
|
+
let file;
|
|
1138
|
+
try {
|
|
1139
|
+
if (content.type === "file") allocation = this.bytes(content.data, "createStagedFile", directoryPath);
|
|
1140
|
+
directory = this.directory(448);
|
|
1141
|
+
file = content.type === "file" ? { ...this.metadata(typeModes.file | mode2), type: "file", data: allocation.data, allocation } : { ...this.metadata(typeModes.symlink | mode2), type: "symlink", target: content.target };
|
|
1142
|
+
if (options.atimeMs !== void 0) file.atimeMs = options.atimeMs;
|
|
1143
|
+
if (options.mtimeMs !== void 0) file.mtimeMs = options.mtimeMs;
|
|
1144
|
+
directory.entries.set(name, file);
|
|
1145
|
+
location.parent.entries.set(location.name, directory);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
allocation?.release();
|
|
1148
|
+
this.ledger.release(retained, 4);
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
this.changed(location.parent);
|
|
1152
|
+
const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
|
|
1153
|
+
const receipt = (path, node) => Object.freeze({ path, stat: Object.freeze(this.snapshot(node)) });
|
|
1154
|
+
return Object.freeze({ parent: receipt(parentPath, location.parent), directory: receipt(location.path, directory), file: receipt(`${location.path}/${name}`, file) });
|
|
1155
|
+
}
|
|
1156
|
+
async publishStagedFile(staging, destination, options) {
|
|
1157
|
+
options.signal?.throwIfAborted();
|
|
1158
|
+
const { directory, file } = this.stagingLocations(staging);
|
|
1159
|
+
this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
1160
|
+
const target = this.entry(destination, "publishStagedFile", true);
|
|
1161
|
+
this.expectEntry(target.parent, options.parent, destination, false);
|
|
1162
|
+
this.expectEntry(target.node, options.destination, destination);
|
|
1163
|
+
if (target.parent === directory.node || target.node === directory.node || target.node === file.node) this.fail("EINVAL", "publishStagedFile", destination);
|
|
1164
|
+
if (target.node && (target.node.type !== "file" || target.node.nlink !== 1)) this.fail("EAGAIN", "publishStagedFile", destination);
|
|
1165
|
+
return _MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...options, noReplace: options.destination === null });
|
|
1166
|
+
}
|
|
1167
|
+
async removeStagedFile(staging, options = {}) {
|
|
1168
|
+
options.signal?.throwIfAborted();
|
|
1169
|
+
const { directory, file } = this.stagingLocations(staging);
|
|
1170
|
+
if (file.node) this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
1171
|
+
const node = directory.node;
|
|
1172
|
+
if (node.entries.size !== (file.node ? 1 : 0)) this.fail("ENOTEMPTY", "removeStagedFile", staging.directory.path);
|
|
1173
|
+
this.permission(directory.parent, 3, "removeStagedFile", staging.directory.path);
|
|
1174
|
+
this.permission(node, 3, "removeStagedFile", staging.file.path);
|
|
1175
|
+
directory.parent.entries.delete(directory.name);
|
|
1176
|
+
if (file.node) {
|
|
1177
|
+
node.entries.delete(file.name);
|
|
1178
|
+
this.ledger.release(file.name.length * 2, 1);
|
|
1179
|
+
file.node.nlink--;
|
|
1180
|
+
file.node.ctimeMs = Date.now();
|
|
1181
|
+
file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
|
|
1182
|
+
this.releaseNode(file.node);
|
|
1183
|
+
}
|
|
1184
|
+
this.ledger.release(directory.name.length * 2, 1);
|
|
1185
|
+
node.nlink = 0;
|
|
1186
|
+
node.ctimeMs = Date.now();
|
|
1187
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1188
|
+
this.releaseNode(node);
|
|
1189
|
+
this.changed(directory.parent);
|
|
1190
|
+
}
|
|
1191
|
+
async prepareDirectory(path, options) {
|
|
1192
|
+
options.signal?.throwIfAborted();
|
|
1193
|
+
const location = this.entry(path, "prepareDirectory", true);
|
|
1194
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
1195
|
+
this.expectEntry(location.node, options.expected, path, false);
|
|
1196
|
+
const mode2 = options.mode === void 0 ? void 0 : this.mode(options.mode, 0, "prepareDirectory", path);
|
|
1197
|
+
for (const time of [options.atimeMs, options.mtimeMs]) if (time !== void 0 && !Number.isFinite(time)) this.fail("EINVAL", "prepareDirectory", path);
|
|
1198
|
+
let node = location.node;
|
|
1199
|
+
if (!node) {
|
|
1200
|
+
this.permission(location.parent, 3, "prepareDirectory", path);
|
|
1201
|
+
node = this.addNode(location.parent, location.name, () => this.directory(mode2 ?? 511), "prepareDirectory", path);
|
|
1202
|
+
}
|
|
1203
|
+
if (node.type !== "directory") this.fail("ENOTDIR", "prepareDirectory", path);
|
|
1204
|
+
if (mode2 !== void 0) node.mode = typeModes.directory | mode2;
|
|
1205
|
+
if (options.atimeMs !== void 0) node.atimeMs = options.atimeMs;
|
|
1206
|
+
if (options.mtimeMs !== void 0) node.mtimeMs = options.mtimeMs;
|
|
1207
|
+
node.ctimeMs = Date.now();
|
|
1208
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1209
|
+
return Object.freeze(this.snapshot(node));
|
|
1210
|
+
}
|
|
1211
|
+
async writeFileConditional(path, data, options) {
|
|
1212
|
+
options.signal?.throwIfAborted();
|
|
1213
|
+
const location = this.entry(path, "writeFileConditional", true);
|
|
1214
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
1215
|
+
this.expectEntry(location.node, options.expected, path);
|
|
1216
|
+
if (location.node && location.node.type !== "file") this.fail("EINVAL", "writeFileConditional", path);
|
|
1217
|
+
this.writeData(path, data, { ...options.mode === void 0 ? {} : { mode: options.mode }, flag: options.append ? "a" : "w" }, "writeFileConditional");
|
|
1218
|
+
return Object.freeze(this.snapshot(location.parent.entries.get(location.name)));
|
|
1219
|
+
}
|
|
1220
|
+
async removeFileConditional(path, options) {
|
|
1221
|
+
options.signal?.throwIfAborted();
|
|
1222
|
+
const location = this.entry(path, "removeFileConditional", true);
|
|
1223
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
1224
|
+
this.expectEntry(location.node, options.expected, path);
|
|
1225
|
+
const node = location.node;
|
|
1226
|
+
if (node.type !== "file") this.fail("EINVAL", "removeFileConditional", path);
|
|
1227
|
+
this.permission(location.parent, 3, "removeFileConditional", path);
|
|
1228
|
+
location.parent.entries.delete(location.name);
|
|
1229
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
1230
|
+
node.nlink--;
|
|
1231
|
+
node.ctimeMs = Date.now();
|
|
1232
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1233
|
+
this.releaseNode(node);
|
|
1234
|
+
this.changed(location.parent);
|
|
1235
|
+
}
|
|
1236
|
+
async writeFile(path, data, options = {}) {
|
|
1237
|
+
options.signal?.throwIfAborted();
|
|
1238
|
+
this.writeData(path, data, options, "writeFile");
|
|
1239
|
+
}
|
|
1240
|
+
async appendFile(path, data, options = {}) {
|
|
1241
|
+
options.signal?.throwIfAborted();
|
|
1242
|
+
this.writeData(path, data, { ...options, flag: "a" }, "appendFile");
|
|
1243
|
+
}
|
|
1244
|
+
async stat(path, options = {}) {
|
|
1245
|
+
options.signal?.throwIfAborted();
|
|
1246
|
+
return this.snapshot(this.resolve(path, "stat").node);
|
|
1247
|
+
}
|
|
1248
|
+
async lstat(path, options = {}) {
|
|
1249
|
+
options.signal?.throwIfAborted();
|
|
1250
|
+
const stat = this.snapshot(this.resolve(path, "lstat", { followFinal: false }).node);
|
|
1251
|
+
ownedStats.set(stat, { filesystem: this, path, root: this.root });
|
|
1252
|
+
return stat;
|
|
1253
|
+
}
|
|
1254
|
+
async readdir(path, options = {}) {
|
|
1255
|
+
const limit = directoryEntryLimit(options, path);
|
|
1256
|
+
const node = this.resolve(path, "readdir").node;
|
|
1257
|
+
if (node.type !== "directory") this.fail("ENOTDIR", "readdir", path);
|
|
1258
|
+
this.permission(node, 4, "readdir", path);
|
|
1259
|
+
admitDirectoryEntries(node.entries.size, limit, path);
|
|
1260
|
+
node.atimeMs = Date.now();
|
|
1261
|
+
return [...node.entries].map(([name, entry]) => ({ name, type: entry.type })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1262
|
+
}
|
|
1263
|
+
async mkdir(path, options = {}) {
|
|
1264
|
+
options.signal?.throwIfAborted();
|
|
1265
|
+
this.validatePath(path, "mkdir");
|
|
1266
|
+
const mode2 = this.mode(options.mode, 511, "mkdir", path);
|
|
1267
|
+
if (options.recursive) {
|
|
1268
|
+
const node = this.resolve(path, "mkdir", { createDirectories: mode2 }).node;
|
|
1269
|
+
if (node.type !== "directory") this.fail("EEXIST", "mkdir", path);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const location = this.resolve(path.replace(/\/+$/, "") || "/", "mkdir", { allowMissing: true, followFinal: false });
|
|
1273
|
+
if (location.node) this.fail("EEXIST", "mkdir", path);
|
|
1274
|
+
this.permission(location.parent, 3, "mkdir", path);
|
|
1275
|
+
this.addNode(location.parent, location.name, () => this.directory(mode2), "mkdir", path);
|
|
1276
|
+
}
|
|
1277
|
+
async rmdir(path, options = {}) {
|
|
1278
|
+
options.signal?.throwIfAborted();
|
|
1279
|
+
const location = this.entry(path, "rmdir");
|
|
1280
|
+
const node = location.node;
|
|
1281
|
+
if (this.terminalDot(path)) this.fail("EINVAL", "rmdir", path);
|
|
1282
|
+
if (node === this.root) this.fail("EBUSY", "rmdir", path);
|
|
1283
|
+
if (node.type !== "directory") this.fail("ENOTDIR", "rmdir", path);
|
|
1284
|
+
this.permission(location.parent, 3, "rmdir", path);
|
|
1285
|
+
if (node.entries.size > 0) this.fail("ENOTEMPTY", "rmdir", path);
|
|
1286
|
+
location.parent.entries.delete(location.name);
|
|
1287
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
1288
|
+
node.nlink = 0;
|
|
1289
|
+
node.ctimeMs = Date.now();
|
|
1290
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1291
|
+
this.releaseNode(node);
|
|
1292
|
+
this.changed(location.parent);
|
|
1293
|
+
}
|
|
1294
|
+
async rm(path, options = {}) {
|
|
1295
|
+
options.signal?.throwIfAborted();
|
|
1296
|
+
let location;
|
|
1297
|
+
try {
|
|
1298
|
+
location = this.entry(path, "rm");
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
if (options.force && error instanceof FsError && error.code === "ENOENT") return;
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
const node = location.node;
|
|
1304
|
+
if (this.terminalDot(path)) this.fail("EINVAL", "rm", path);
|
|
1305
|
+
if (node === this.root) this.fail("EBUSY", "rm", path);
|
|
1306
|
+
this.permission(location.parent, 3, "rm", path);
|
|
1307
|
+
if (node.type === "directory" && !options.recursive) this.fail("EISDIR", "rm", path);
|
|
1308
|
+
const removed = [node];
|
|
1309
|
+
for (let index = 0; index < removed.length; index++) {
|
|
1310
|
+
const entry = removed[index];
|
|
1311
|
+
if (entry.type === "directory" && entry.entries.size > 0) {
|
|
1312
|
+
this.permission(entry, 7, "rm", path);
|
|
1313
|
+
for (const child of entry.entries.values()) removed.push(child);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
location.parent.entries.delete(location.name);
|
|
1317
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
1318
|
+
for (const entry of removed) {
|
|
1319
|
+
if (entry.type === "directory") {
|
|
1320
|
+
for (const name of entry.entries.keys()) this.ledger.release(name.length * 2, 1);
|
|
1321
|
+
entry.entries.clear();
|
|
1322
|
+
}
|
|
1323
|
+
entry.nlink--;
|
|
1324
|
+
entry.ctimeMs = Date.now();
|
|
1325
|
+
entry.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.revision + 1);
|
|
1326
|
+
this.releaseNode(entry);
|
|
1327
|
+
}
|
|
1328
|
+
this.changed(location.parent);
|
|
1329
|
+
}
|
|
1330
|
+
async rename(source, destination, options = {}) {
|
|
1331
|
+
options.signal?.throwIfAborted();
|
|
1332
|
+
try {
|
|
1333
|
+
const origin = this.entry(source, "rename");
|
|
1334
|
+
const target = this.entry(destination, "rename", true);
|
|
1335
|
+
const node = origin.node;
|
|
1336
|
+
if (this.terminalDot(source) || this.terminalDot(destination)) this.fail("EINVAL", "rename", source, destination);
|
|
1337
|
+
if (node === this.root || target.node === this.root) this.fail("EBUSY", "rename", source, destination);
|
|
1338
|
+
if (destination.endsWith("/") && node.type !== "directory") this.fail("ENOTDIR", "rename", source, destination);
|
|
1339
|
+
if (options.noReplace && target.node) this.fail("EEXIST", "rename", source, destination);
|
|
1340
|
+
if (target.node === node) return;
|
|
1341
|
+
if (node.type === "directory" && target.path.startsWith(`${origin.path}/`)) this.fail("EINVAL", "rename", source, destination);
|
|
1342
|
+
this.permission(origin.parent, 3, "rename", source);
|
|
1343
|
+
this.permission(target.parent, 3, "rename", destination);
|
|
1344
|
+
if (target.node) {
|
|
1345
|
+
if (node.type === "directory" && target.node.type !== "directory") this.fail("ENOTDIR", "rename", source, destination);
|
|
1346
|
+
if (node.type !== "directory" && target.node.type === "directory") this.fail("EISDIR", "rename", source, destination);
|
|
1347
|
+
if (target.node.type === "directory" && target.node.entries.size > 0) this.fail("ENOTEMPTY", "rename", source, destination);
|
|
1348
|
+
}
|
|
1349
|
+
const nameGrowth = target.node ? 0 : (target.name.length - origin.name.length) * 2;
|
|
1350
|
+
this.ledger.reserve(Math.max(0, nameGrowth), 0, "rename", source);
|
|
1351
|
+
try {
|
|
1352
|
+
target.parent.entries.set(target.name, node);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
this.ledger.release(Math.max(0, nameGrowth), 0);
|
|
1355
|
+
throw error;
|
|
1356
|
+
}
|
|
1357
|
+
origin.parent.entries.delete(origin.name);
|
|
1358
|
+
if (target.node) {
|
|
1359
|
+
this.ledger.release(origin.name.length * 2, 1);
|
|
1360
|
+
target.node.nlink--;
|
|
1361
|
+
target.node.ctimeMs = Date.now();
|
|
1362
|
+
target.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, target.node.revision + 1);
|
|
1363
|
+
this.releaseNode(target.node);
|
|
1364
|
+
} else if (nameGrowth < 0) this.ledger.release(-nameGrowth, 0);
|
|
1365
|
+
this.changed(origin.parent);
|
|
1366
|
+
this.changed(target.parent);
|
|
1367
|
+
node.ctimeMs = Date.now();
|
|
1368
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
if (error instanceof FsError) throw new FsError(error.code, { syscall: "rename", path: source, dest: destination, cause: error });
|
|
1371
|
+
throw error;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async copyFile(source, destination, options = {}) {
|
|
1375
|
+
options.signal?.throwIfAborted();
|
|
1376
|
+
try {
|
|
1377
|
+
const origin = this.file(source, "copyFile");
|
|
1378
|
+
this.permission(origin, 4, "copyFile", source);
|
|
1379
|
+
const target = this.resolve(destination, "copyFile", { followFinal: !options.exclusive, allowMissing: true });
|
|
1380
|
+
if (target.node && options.exclusive) this.fail("EEXIST", "copyFile", source, destination);
|
|
1381
|
+
if (target.node === origin) this.fail("EINVAL", "copyFile", source, destination);
|
|
1382
|
+
const node = this.writeData(destination, origin.data, { mode: origin.mode & 4095, flag: options.exclusive ? "wx" : "w" }, "copyFile");
|
|
1383
|
+
node.mode = origin.mode;
|
|
1384
|
+
this.changed(node);
|
|
1385
|
+
origin.atimeMs = Date.now();
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
if (error instanceof FsError) throw new FsError(error.code, { syscall: "copyFile", path: source, dest: destination, cause: error });
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async realpath(path, options = {}) {
|
|
1392
|
+
options.signal?.throwIfAborted();
|
|
1393
|
+
return this.resolve(path, "realpath").path;
|
|
1394
|
+
}
|
|
1395
|
+
async access(path, mode2 = 0, options = {}) {
|
|
1396
|
+
options.signal?.throwIfAborted();
|
|
1397
|
+
if (!Number.isInteger(mode2) || mode2 < 0 || mode2 > 7) this.fail("EINVAL", "access", path);
|
|
1398
|
+
this.permission(this.resolve(path, "access").node, mode2, "access", path);
|
|
1399
|
+
}
|
|
1400
|
+
async readlink(path, options = {}) {
|
|
1401
|
+
options.signal?.throwIfAborted();
|
|
1402
|
+
const node = this.resolve(path, "readlink", { followFinal: false }).node;
|
|
1403
|
+
if (node.type !== "symlink") this.fail("EINVAL", "readlink", path);
|
|
1404
|
+
return node.target;
|
|
1405
|
+
}
|
|
1406
|
+
async symlink(target, path, options = {}) {
|
|
1407
|
+
options.signal?.throwIfAborted();
|
|
1408
|
+
this.validatePath(target, "symlink");
|
|
1409
|
+
const location = this.resolve(path, "symlink", { followFinal: false, allowMissing: true });
|
|
1410
|
+
if (location.node) this.fail("EEXIST", "symlink", path);
|
|
1411
|
+
this.permission(location.parent, 3, "symlink", path);
|
|
1412
|
+
this.addNode(location.parent, location.name, () => ({
|
|
1413
|
+
...this.metadata(typeModes.symlink | 511),
|
|
1414
|
+
type: "symlink",
|
|
1415
|
+
target
|
|
1416
|
+
}), "symlink", path, target.length * 2);
|
|
1417
|
+
}
|
|
1418
|
+
async link(existingPath, newPath, options = {}) {
|
|
1419
|
+
options.signal?.throwIfAborted();
|
|
1420
|
+
const node = this.resolve(existingPath, "link", { followFinal: false }).node;
|
|
1421
|
+
if (node.type === "directory") this.fail("EPERM", "link", existingPath, newPath);
|
|
1422
|
+
const target = this.resolve(newPath, "link", { followFinal: false, allowMissing: true });
|
|
1423
|
+
if (target.node) this.fail("EEXIST", "link", existingPath, newPath);
|
|
1424
|
+
this.permission(target.parent, 3, "link", newPath);
|
|
1425
|
+
this.ledger.reserve(target.name.length * 2, 1, "link", newPath);
|
|
1426
|
+
try {
|
|
1427
|
+
target.parent.entries.set(target.name, node);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
this.ledger.release(target.name.length * 2, 1);
|
|
1430
|
+
throw error;
|
|
1431
|
+
}
|
|
1432
|
+
node.nlink++;
|
|
1433
|
+
node.ctimeMs = Date.now();
|
|
1434
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1435
|
+
this.changed(target.parent);
|
|
1436
|
+
}
|
|
1437
|
+
async chmod(path, mode2, options = {}) {
|
|
1438
|
+
options.signal?.throwIfAborted();
|
|
1439
|
+
const permissions = this.mode(mode2, 0, "chmod", path);
|
|
1440
|
+
const node = this.resolve(path, "chmod").node;
|
|
1441
|
+
node.mode = typeModes[node.type] | permissions;
|
|
1442
|
+
node.ctimeMs = Date.now();
|
|
1443
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1444
|
+
}
|
|
1445
|
+
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
1446
|
+
options.signal?.throwIfAborted();
|
|
1447
|
+
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)) this.fail("EINVAL", "utimes", path);
|
|
1448
|
+
const node = this.resolve(path, "utimes").node;
|
|
1449
|
+
node.atimeMs = atimeMs;
|
|
1450
|
+
node.mtimeMs = mtimeMs;
|
|
1451
|
+
node.ctimeMs = Date.now();
|
|
1452
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
1453
|
+
}
|
|
1454
|
+
async truncate(path, length = 0, options = {}) {
|
|
1455
|
+
options.signal?.throwIfAborted();
|
|
1456
|
+
this.integer(length, "truncate", path);
|
|
1457
|
+
const node = this.file(path, "truncate");
|
|
1458
|
+
this.permission(node, 2, "truncate", path);
|
|
1459
|
+
this.resizeNode(node, length, "truncate", path);
|
|
1460
|
+
}
|
|
1461
|
+
resizeNode(node, length, syscall, path) {
|
|
1462
|
+
const data = this.allocate(length, syscall, path);
|
|
1463
|
+
try {
|
|
1464
|
+
data.data.set(node.data.subarray(0, length));
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
data.release();
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
1469
|
+
this.replaceData(node, data);
|
|
1470
|
+
this.changed(node);
|
|
1471
|
+
}
|
|
1472
|
+
async openResizeFile(path, options = {}) {
|
|
1473
|
+
options.signal?.throwIfAborted();
|
|
1474
|
+
if (!stockRetainedResize(this)) throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
|
|
1475
|
+
const location = this.resolve(path, "openResizeFile", { allowMissing: options.create === true, resizeCreate: options.create ?? false });
|
|
1476
|
+
let mode2 = 438;
|
|
1477
|
+
if (location.node) {
|
|
1478
|
+
if (location.node.type !== "file") this.fail("EISDIR", "openResizeFile", path);
|
|
1479
|
+
this.permission(location.node, 2, "openResizeFile", path);
|
|
1480
|
+
} else {
|
|
1481
|
+
this.permission(location.parent, 3, "openResizeFile", path);
|
|
1482
|
+
mode2 = this.mode(options.mode, mode2, "openResizeFile", path);
|
|
1483
|
+
}
|
|
1484
|
+
this.ledger.reserve(path.length * 2, 1, "openResizeFile", path);
|
|
1485
|
+
let node;
|
|
1486
|
+
try {
|
|
1487
|
+
node = this.openWrite(path, {}, "openResizeFile", { location, mode: mode2, append: false });
|
|
1488
|
+
node.references++;
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
this.ledger.release(path.length * 2, 1);
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
const snapshot = this.snapshot.bind(this);
|
|
1494
|
+
const integer2 = this.integer.bind(this);
|
|
1495
|
+
const resizeNode = this.resizeNode.bind(this);
|
|
1496
|
+
const releaseReference = this.releaseReference.bind(this);
|
|
1497
|
+
let closing;
|
|
1498
|
+
const current = (signal, syscall) => {
|
|
1499
|
+
signal?.throwIfAborted();
|
|
1500
|
+
if (!node) throw new FsError("EBADF", { syscall, path });
|
|
1501
|
+
return node;
|
|
1502
|
+
};
|
|
1503
|
+
return {
|
|
1504
|
+
async stat(options2 = {}) {
|
|
1505
|
+
return snapshot(current(options2.signal, "fstat"));
|
|
1506
|
+
},
|
|
1507
|
+
async seekEnd(options2 = {}) {
|
|
1508
|
+
return BigInt(current(options2.signal, "lseek").data.byteLength);
|
|
1509
|
+
},
|
|
1510
|
+
async truncate(length, options2 = {}) {
|
|
1511
|
+
const file = current(options2.signal, "ftruncate");
|
|
1512
|
+
integer2(length, "ftruncate", path);
|
|
1513
|
+
resizeNode(file, length, "ftruncate", path);
|
|
1514
|
+
},
|
|
1515
|
+
close() {
|
|
1516
|
+
if (!closing) {
|
|
1517
|
+
const retained = node;
|
|
1518
|
+
node = void 0;
|
|
1519
|
+
closing = Promise.resolve().then(() => {
|
|
1520
|
+
if (retained) releaseReference(retained, path);
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return closing;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
async openReadFile(path, options = {}) {
|
|
1528
|
+
const signal = options.signal;
|
|
1529
|
+
signal?.throwIfAborted();
|
|
1530
|
+
const allowDirectory = options.allowDirectory === true;
|
|
1531
|
+
signal?.throwIfAborted();
|
|
1532
|
+
const unsupported2 = () => {
|
|
1533
|
+
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
1534
|
+
};
|
|
1535
|
+
const owner = ownedStores.get(this);
|
|
1536
|
+
if (!owner || Object.getPrototypeOf(this) !== _MemoryFileSystem.prototype || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root || Object.getOwnPropertyDescriptor(this, "ledger")?.value !== owner.ledger) unsupported2();
|
|
1537
|
+
for (const name of [
|
|
1538
|
+
"openReadFile",
|
|
1539
|
+
"readFile",
|
|
1540
|
+
"readStream",
|
|
1541
|
+
"stat",
|
|
1542
|
+
"lstat",
|
|
1543
|
+
"realpath",
|
|
1544
|
+
"access",
|
|
1545
|
+
"file",
|
|
1546
|
+
"resolve",
|
|
1547
|
+
"permission",
|
|
1548
|
+
"validatePath",
|
|
1549
|
+
"fail",
|
|
1550
|
+
"snapshot",
|
|
1551
|
+
"integer",
|
|
1552
|
+
"releaseReference",
|
|
1553
|
+
"releaseNode"
|
|
1554
|
+
]) {
|
|
1555
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, name) ?? Object.getOwnPropertyDescriptor(_MemoryFileSystem.prototype, name);
|
|
1556
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value) unsupported2();
|
|
1557
|
+
}
|
|
1558
|
+
const retainedRead = Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead;
|
|
1559
|
+
signal?.throwIfAborted();
|
|
1560
|
+
if (retainedRead !== true) unsupported2();
|
|
1561
|
+
const selected = allowDirectory ? this.resolve(path, "openReadFile").node : this.file(path, "openReadFile");
|
|
1562
|
+
if (selected.type !== "file" && selected.type !== "directory") this.fail("EISDIR", "openReadFile", path);
|
|
1563
|
+
let node = selected;
|
|
1564
|
+
this.permission(node, 4, "openReadFile", path);
|
|
1565
|
+
signal?.throwIfAborted();
|
|
1566
|
+
this.ledger.reserve(path.length * 2, 1, "openReadFile", path);
|
|
1567
|
+
node.references++;
|
|
1568
|
+
const snapshot = this.snapshot.bind(this);
|
|
1569
|
+
const integer2 = this.integer.bind(this);
|
|
1570
|
+
const releaseReference = this.releaseReference.bind(this);
|
|
1571
|
+
let closing;
|
|
1572
|
+
const current = (signal2, syscall) => {
|
|
1573
|
+
signal2?.throwIfAborted();
|
|
1574
|
+
if (!node) throw new FsError("EBADF", { syscall, path });
|
|
1575
|
+
return node;
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
async stat(options2 = {}) {
|
|
1579
|
+
return snapshot(current(options2.signal, "fstat"));
|
|
1580
|
+
},
|
|
1581
|
+
async seekEnd(options2 = {}) {
|
|
1582
|
+
const file = current(options2.signal, "lseek");
|
|
1583
|
+
return file.type === "directory" ? ext4HtreeEof64 : BigInt(file.data.byteLength);
|
|
1584
|
+
},
|
|
1585
|
+
async read(position, maxBytes, options2 = {}) {
|
|
1586
|
+
const file = current(options2.signal, "read");
|
|
1587
|
+
integer2(position, "read", path);
|
|
1588
|
+
integer2(maxBytes, "read", path);
|
|
1589
|
+
if (maxBytes === 0 || maxBytes > Number.MAX_SAFE_INTEGER - position) {
|
|
1590
|
+
throw new FsError("EINVAL", { syscall: "read", path });
|
|
1591
|
+
}
|
|
1592
|
+
if (file.type === "directory") throw new FsError("EISDIR", { syscall: "read", path });
|
|
1593
|
+
const bytes = file.data.slice(position, position + maxBytes);
|
|
1594
|
+
file.atimeMs = Date.now();
|
|
1595
|
+
return bytes;
|
|
1596
|
+
},
|
|
1597
|
+
close() {
|
|
1598
|
+
if (node) {
|
|
1599
|
+
releaseReference(node, path);
|
|
1600
|
+
node = void 0;
|
|
1601
|
+
}
|
|
1602
|
+
closing ??= Promise.resolve();
|
|
1603
|
+
return closing;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
async *readStream(path, options = {}) {
|
|
1608
|
+
options.signal?.throwIfAborted();
|
|
1609
|
+
const start = options.start ?? 0;
|
|
1610
|
+
const chunkSize2 = options.chunkSize ?? 64 * 1024;
|
|
1611
|
+
this.integer(start, "readStream", path);
|
|
1612
|
+
this.integer(chunkSize2, "readStream", path);
|
|
1613
|
+
if (chunkSize2 === 0) this.fail("EINVAL", "readStream", path);
|
|
1614
|
+
if (options.endExclusive !== void 0) {
|
|
1615
|
+
this.integer(options.endExclusive, "readStream", path);
|
|
1616
|
+
if (options.endExclusive < start) this.fail("EINVAL", "readStream", path);
|
|
1617
|
+
}
|
|
1618
|
+
const node = this.file(path, "readStream");
|
|
1619
|
+
this.permission(node, 4, "readStream", path);
|
|
1620
|
+
this.ledger.reserve(path.length * 2, 1, "readStream", path);
|
|
1621
|
+
node.references++;
|
|
1622
|
+
const allocation = node.allocation;
|
|
1623
|
+
allocation.retain();
|
|
1624
|
+
const data = node.data;
|
|
1625
|
+
try {
|
|
1626
|
+
const end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
|
|
1627
|
+
node.atimeMs = Date.now();
|
|
1628
|
+
for (let offset = start; offset < end; offset += chunkSize2) {
|
|
1629
|
+
options.signal?.throwIfAborted();
|
|
1630
|
+
yield data.slice(offset, Math.min(offset + chunkSize2, end));
|
|
1631
|
+
}
|
|
1632
|
+
options.signal?.throwIfAborted();
|
|
1633
|
+
} finally {
|
|
1634
|
+
allocation.release();
|
|
1635
|
+
this.releaseReference(node, path);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async writeStream(path, source, options = {}) {
|
|
1639
|
+
options.signal?.throwIfAborted();
|
|
1640
|
+
this.validatePath(path, "writeStream");
|
|
1641
|
+
this.ledger.reserve(path.length * 2, 1, "writeStream", path);
|
|
1642
|
+
let node;
|
|
1643
|
+
try {
|
|
1644
|
+
node = this.openWrite(path, options, "writeStream");
|
|
1645
|
+
node.references++;
|
|
1646
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
1647
|
+
let position = 0;
|
|
1648
|
+
if (!append) {
|
|
1649
|
+
this.replaceData(node, new MemoryAllocation(new Uint8Array(), this.ledger));
|
|
1650
|
+
this.changed(node);
|
|
1651
|
+
}
|
|
1652
|
+
for await (const chunk of source) {
|
|
1653
|
+
options.signal?.throwIfAborted();
|
|
1654
|
+
if (!(chunk instanceof Uint8Array)) throw new TypeError("Memory files require Uint8Array data");
|
|
1655
|
+
this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
|
|
1656
|
+
if (!append) position += chunk.byteLength;
|
|
1657
|
+
}
|
|
1658
|
+
options.signal?.throwIfAborted();
|
|
1659
|
+
} finally {
|
|
1660
|
+
if (node) this.releaseReference(node, path);
|
|
1661
|
+
else this.ledger.release(path.length * 2, 1);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
var memoryImplementation = Object.getOwnPropertyDescriptors(MemoryFileSystem.prototype);
|
|
1666
|
+
function stockDescriptorWrite(filesystem) {
|
|
1667
|
+
if (Object.getPrototypeOf(filesystem) !== MemoryFileSystem.prototype) return false;
|
|
1668
|
+
const owner = ownedStores.get(filesystem);
|
|
1669
|
+
if (!owner || Object.getOwnPropertyDescriptor(filesystem, "root")?.value !== owner.root || Object.getOwnPropertyDescriptor(filesystem, "capabilities")?.value !== owner.capabilities) return false;
|
|
1670
|
+
for (const name of [
|
|
1671
|
+
"writeStream",
|
|
1672
|
+
"writeFile",
|
|
1673
|
+
"appendFile",
|
|
1674
|
+
"access",
|
|
1675
|
+
"stat",
|
|
1676
|
+
"lstat",
|
|
1677
|
+
"realpath",
|
|
1678
|
+
"openWrite",
|
|
1679
|
+
"prepareWrite",
|
|
1680
|
+
"writeData",
|
|
1681
|
+
"addNode",
|
|
1682
|
+
"replaceData",
|
|
1683
|
+
"releaseReference",
|
|
1684
|
+
"releaseNode",
|
|
1685
|
+
"resolve",
|
|
1686
|
+
"permission",
|
|
1687
|
+
"validatePath",
|
|
1688
|
+
"mode",
|
|
1689
|
+
"bytes",
|
|
1690
|
+
"allocate",
|
|
1691
|
+
"changed",
|
|
1692
|
+
"metadata",
|
|
1693
|
+
"integer",
|
|
1694
|
+
"writeAt",
|
|
1695
|
+
"fail"
|
|
1696
|
+
]) {
|
|
1697
|
+
const descriptor = Object.getOwnPropertyDescriptor(filesystem, name) ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
1698
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value) return false;
|
|
1699
|
+
}
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1702
|
+
function stockRetainedResize(filesystem) {
|
|
1703
|
+
if (!stockDescriptorWrite(filesystem)) return false;
|
|
1704
|
+
const owner = ownedStores.get(filesystem);
|
|
1705
|
+
if (!owner || Object.getOwnPropertyDescriptor(filesystem, "ledger")?.value !== owner.ledger) return false;
|
|
1706
|
+
for (const name of ["openResizeFile", "truncate", "resizeNode", "snapshot"]) {
|
|
1707
|
+
const descriptor = Object.getOwnPropertyDescriptor(filesystem, name) ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
1708
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value) return false;
|
|
1709
|
+
}
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
var deviceCapabilities = Object.freeze({
|
|
1713
|
+
readOnly: false,
|
|
1714
|
+
read: true,
|
|
1715
|
+
stat: true,
|
|
1716
|
+
realpath: true,
|
|
1717
|
+
access: true,
|
|
1718
|
+
readdir: false,
|
|
1719
|
+
write: true,
|
|
1720
|
+
append: true,
|
|
1721
|
+
exclusiveCreate: true,
|
|
1722
|
+
streamingRead: true,
|
|
1723
|
+
retainedRead: true,
|
|
1724
|
+
streamingWrite: true,
|
|
1725
|
+
streamingAppend: true,
|
|
1726
|
+
copy: true,
|
|
1727
|
+
exclusiveCopy: true,
|
|
1728
|
+
remove: false,
|
|
1729
|
+
removeDirectory: false,
|
|
1730
|
+
recursiveRemove: false,
|
|
1731
|
+
rename: false,
|
|
1732
|
+
mkdir: false,
|
|
1733
|
+
recursiveMkdir: false,
|
|
1734
|
+
symlinks: false,
|
|
1735
|
+
hardlinks: false,
|
|
1736
|
+
readlink: false,
|
|
1737
|
+
permissions: false,
|
|
1738
|
+
timestamps: false,
|
|
1739
|
+
truncate: false,
|
|
1740
|
+
randomAccessWrite: false,
|
|
1741
|
+
atomicFileMutation: false,
|
|
1742
|
+
atomicFileStaging: false,
|
|
1743
|
+
atomicDirectoryMetadata: false,
|
|
1744
|
+
atomicRename: false,
|
|
1745
|
+
atomicRenameNoReplace: false,
|
|
1746
|
+
descriptorWriteStream: true,
|
|
1747
|
+
retainedResize: true,
|
|
1748
|
+
atomicResize: false
|
|
1749
|
+
});
|
|
1750
|
+
var syntheticStat = Object.freeze({
|
|
1751
|
+
type: "directory",
|
|
1752
|
+
size: 0,
|
|
1753
|
+
mode: 16749,
|
|
1754
|
+
mtimeMs: 0,
|
|
1755
|
+
atimeMs: 0,
|
|
1756
|
+
ctimeMs: 0
|
|
1757
|
+
});
|
|
1758
|
+
function composeAbortSignals(signals) {
|
|
1759
|
+
const controller = new AbortController();
|
|
1760
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1761
|
+
const dispose = () => {
|
|
1762
|
+
for (const [signal, listener] of listeners) signal.removeEventListener("abort", listener);
|
|
1763
|
+
listeners.clear();
|
|
1764
|
+
};
|
|
1765
|
+
for (const signal of signals) {
|
|
1766
|
+
if (signal.aborted) {
|
|
1767
|
+
controller.abort(signal.reason);
|
|
1768
|
+
return { signal: controller.signal, dispose };
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
try {
|
|
1772
|
+
for (const signal of new Set(signals)) {
|
|
1773
|
+
const listener = () => {
|
|
1774
|
+
dispose();
|
|
1775
|
+
controller.abort(signal.reason);
|
|
1776
|
+
};
|
|
1777
|
+
listeners.set(signal, listener);
|
|
1778
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
1779
|
+
if (signal.aborted) listener();
|
|
1780
|
+
if (controller.signal.aborted) break;
|
|
1781
|
+
}
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
dispose();
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
return { signal: controller.signal, dispose };
|
|
1787
|
+
}
|
|
1788
|
+
var XmlLimitError = class extends SyntaxError {
|
|
1789
|
+
constructor(limit, message) {
|
|
1790
|
+
super(message);
|
|
1791
|
+
this.limit = limit;
|
|
1792
|
+
}
|
|
1793
|
+
limit;
|
|
1794
|
+
};
|
|
1795
|
+
function* find(source, needle, start) {
|
|
1796
|
+
let work = 0;
|
|
1797
|
+
for (let offset = start; offset < source.length; offset++) {
|
|
1798
|
+
if (source.startsWith(needle, offset)) {
|
|
1799
|
+
if (work) yield work;
|
|
1800
|
+
return offset;
|
|
1801
|
+
}
|
|
1802
|
+
if (++work === 512) {
|
|
1803
|
+
yield work;
|
|
1804
|
+
work = 0;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (work) yield work;
|
|
1808
|
+
return -1;
|
|
1809
|
+
}
|
|
1810
|
+
var xmlNamespace = "http://www.w3.org/XML/1998/namespace";
|
|
1811
|
+
var xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
1812
|
+
function invalid(message) {
|
|
1813
|
+
throw new SyntaxError(`Invalid XML: ${message}`);
|
|
1814
|
+
}
|
|
1815
|
+
function validCharacter(point) {
|
|
1816
|
+
return point === 9 || point === 10 || point === 13 || point >= 32 && point <= 55295 || point >= 57344 && point <= 65533 || point >= 65536 && point <= 1114111;
|
|
1817
|
+
}
|
|
1818
|
+
function nameStart(point) {
|
|
1819
|
+
return point === 95 || point >= 65 && point <= 90 || point >= 97 && point <= 122 || point >= 192 && point <= 214 || point >= 216 && point <= 246 || point >= 248 && point <= 767 || point >= 880 && point <= 893 || point >= 895 && point <= 8191 || point >= 8204 && point <= 8205 || point >= 8304 && point <= 8591 || point >= 11264 && point <= 12271 || point >= 12289 && point <= 55295 || point >= 63744 && point <= 64975 || point >= 65008 && point <= 65533 || point >= 65536 && point <= 983039;
|
|
1820
|
+
}
|
|
1821
|
+
function namePart(point) {
|
|
1822
|
+
return nameStart(point) || point === 45 || point === 46 || point === 183 || point >= 48 && point <= 57 || point >= 768 && point <= 879 || point >= 8255 && point <= 8256;
|
|
1823
|
+
}
|
|
1824
|
+
function* qualifiedName(name) {
|
|
1825
|
+
let prefix = "";
|
|
1826
|
+
let start = 0;
|
|
1827
|
+
let first = true;
|
|
1828
|
+
let work = 0;
|
|
1829
|
+
for (let offset = 0; offset < name.length; ) {
|
|
1830
|
+
const point = name.codePointAt(offset);
|
|
1831
|
+
if (point === 58) {
|
|
1832
|
+
if (start !== 0 || first) invalid("invalid qualified name");
|
|
1833
|
+
prefix = name.slice(0, offset);
|
|
1834
|
+
start = offset + 1;
|
|
1835
|
+
first = true;
|
|
1836
|
+
} else {
|
|
1837
|
+
if (!(first ? nameStart(point) : namePart(point))) invalid("invalid qualified name");
|
|
1838
|
+
first = false;
|
|
1839
|
+
}
|
|
1840
|
+
const width = point > 65535 ? 2 : 1;
|
|
1841
|
+
offset += width;
|
|
1842
|
+
work += width;
|
|
1843
|
+
if (work >= 512) {
|
|
1844
|
+
yield work;
|
|
1845
|
+
work = 0;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (first) invalid("invalid qualified name");
|
|
1849
|
+
if (work) yield work;
|
|
1850
|
+
return [prefix, name.slice(start)];
|
|
1851
|
+
}
|
|
1852
|
+
function* entities(text) {
|
|
1853
|
+
let result = "";
|
|
1854
|
+
let offset = 0;
|
|
1855
|
+
while (offset < text.length) {
|
|
1856
|
+
const start = yield* find(text, "&", offset);
|
|
1857
|
+
if (start < 0) return result + text.slice(offset);
|
|
1858
|
+
result += text.slice(offset, start);
|
|
1859
|
+
const end = yield* find(text, ";", start + 1);
|
|
1860
|
+
if (end < 0) invalid("unterminated entity");
|
|
1861
|
+
const entity = text.slice(start + 1, end);
|
|
1862
|
+
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
1863
|
+
if (Object.hasOwn(predefined, entity)) result += predefined[entity];
|
|
1864
|
+
else {
|
|
1865
|
+
const hexadecimal = entity.startsWith("#x");
|
|
1866
|
+
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
1867
|
+
if (!entity.startsWith("#") || !digits.length) invalid("undeclared entity");
|
|
1868
|
+
let point = 0;
|
|
1869
|
+
for (let index = 0; index < digits.length; index++) {
|
|
1870
|
+
const code = digits.charCodeAt(index);
|
|
1871
|
+
const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55 : hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
|
|
1872
|
+
if (digit < 0) invalid("undeclared entity");
|
|
1873
|
+
point = point * (hexadecimal ? 16 : 10) + digit;
|
|
1874
|
+
if (point > 1114111) invalid("invalid character reference");
|
|
1875
|
+
if ((index + 1) % 512 === 0) yield 512;
|
|
1876
|
+
}
|
|
1877
|
+
if (digits.length % 512) yield digits.length % 512;
|
|
1878
|
+
if (!validCharacter(point)) invalid("invalid character reference");
|
|
1879
|
+
result += String.fromCodePoint(point);
|
|
1880
|
+
}
|
|
1881
|
+
offset = end + 1;
|
|
1882
|
+
}
|
|
1883
|
+
return result;
|
|
1884
|
+
}
|
|
1885
|
+
function* validDeclaration(content, expectedEncoding) {
|
|
1886
|
+
let offset = 0;
|
|
1887
|
+
const whitespace = function* () {
|
|
1888
|
+
const start = offset;
|
|
1889
|
+
while (offset < content.length && " \n\r".includes(content[offset])) {
|
|
1890
|
+
offset++;
|
|
1891
|
+
if ((offset - start) % 512 === 0) yield 512;
|
|
1892
|
+
}
|
|
1893
|
+
if ((offset - start) % 512) yield (offset - start) % 512;
|
|
1894
|
+
return offset - start;
|
|
1895
|
+
};
|
|
1896
|
+
const field = function* (name) {
|
|
1897
|
+
if (content.slice(offset, offset + name.length).toLowerCase() !== name) return void 0;
|
|
1898
|
+
offset += name.length;
|
|
1899
|
+
yield name.length;
|
|
1900
|
+
yield* whitespace();
|
|
1901
|
+
if (content[offset++] !== "=") return void 0;
|
|
1902
|
+
yield* whitespace();
|
|
1903
|
+
const quote = content[offset++];
|
|
1904
|
+
if (quote !== "'" && quote !== '"') return void 0;
|
|
1905
|
+
const start = offset;
|
|
1906
|
+
while (offset < content.length && content[offset] !== quote) {
|
|
1907
|
+
offset++;
|
|
1908
|
+
if ((offset - start) % 512 === 0) yield 512;
|
|
1909
|
+
}
|
|
1910
|
+
if ((offset - start) % 512) yield (offset - start) % 512;
|
|
1911
|
+
if (offset >= content.length) return void 0;
|
|
1912
|
+
return content.slice(start, offset++);
|
|
1913
|
+
};
|
|
1914
|
+
if (!(yield* whitespace()) || (yield* field("version")) !== "1.0") return false;
|
|
1915
|
+
let spacing = yield* whitespace();
|
|
1916
|
+
if (content.slice(offset, offset + 8).toLowerCase() === "encoding") {
|
|
1917
|
+
if (!spacing) return false;
|
|
1918
|
+
const encoding = yield* field("encoding");
|
|
1919
|
+
if (encoding === void 0 || encoding.length > 8 || !["utf-8", "utf-16", "utf-16le", "utf-16be"].includes(encoding.toLowerCase())) return false;
|
|
1920
|
+
if (expectedEncoding !== void 0 && encoding.toLowerCase() !== expectedEncoding.toLowerCase()) return false;
|
|
1921
|
+
spacing = yield* whitespace();
|
|
1922
|
+
}
|
|
1923
|
+
if (content.slice(offset, offset + 10).toLowerCase() === "standalone") {
|
|
1924
|
+
if (!spacing) return false;
|
|
1925
|
+
const standalone = yield* field("standalone");
|
|
1926
|
+
if (standalone === void 0 || standalone.length > 3 || !["yes", "no"].includes(standalone.toLowerCase())) return false;
|
|
1927
|
+
yield* whitespace();
|
|
1928
|
+
}
|
|
1929
|
+
return offset === content.length;
|
|
1930
|
+
}
|
|
1931
|
+
function* parseXmlSteps(input, limits = {}) {
|
|
1932
|
+
const maxDepth = limits.maxDepth ?? 64;
|
|
1933
|
+
const maxNodes = limits.maxNodes ?? 1e5;
|
|
1934
|
+
const maxAttributes = limits.maxAttributes ?? 1e4;
|
|
1935
|
+
const retainContent = limits.retainContent !== false;
|
|
1936
|
+
const maxContentNodes = limits.maxContentNodes ?? (retainContent ? 1e5 : maxNodes);
|
|
1937
|
+
const emptyContent = Object.freeze([]);
|
|
1938
|
+
const emptyAttributes = Object.freeze([]);
|
|
1939
|
+
const emptyNamespaces = /* @__PURE__ */ new Map();
|
|
1940
|
+
const maxAttributesPerElement = limits.maxAttributesPerElement ?? 128;
|
|
1941
|
+
const maxNamespaces = limits.maxNamespaces ?? 256;
|
|
1942
|
+
for (const limit of [maxDepth, maxNodes, maxAttributes, maxContentNodes, maxAttributesPerElement, maxNamespaces]) {
|
|
1943
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("XML limits must be positive integers");
|
|
1944
|
+
}
|
|
1945
|
+
const chunks = [];
|
|
1946
|
+
let chunk = "";
|
|
1947
|
+
for (let index = input.charCodeAt(0) === 65279 ? 1 : 0; index < input.length; index++) {
|
|
1948
|
+
const point = input.codePointAt(index);
|
|
1949
|
+
if (!validCharacter(point)) invalid("invalid character");
|
|
1950
|
+
if (point === 13) {
|
|
1951
|
+
chunk += "\n";
|
|
1952
|
+
if (input.charCodeAt(index + 1) === 10) index++;
|
|
1953
|
+
} else {
|
|
1954
|
+
chunk += String.fromCodePoint(point);
|
|
1955
|
+
if (point > 65535) index++;
|
|
1956
|
+
}
|
|
1957
|
+
if (chunk.length >= 512) {
|
|
1958
|
+
chunks.push(chunk);
|
|
1959
|
+
chunk = "";
|
|
1960
|
+
yield 512;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
chunks.push(chunk);
|
|
1964
|
+
if (chunk.length) yield chunk.length;
|
|
1965
|
+
const source = chunks.join("");
|
|
1966
|
+
const stack = [];
|
|
1967
|
+
let root;
|
|
1968
|
+
let declaration;
|
|
1969
|
+
let offset = 0;
|
|
1970
|
+
let nodes = 0;
|
|
1971
|
+
let attributeCount = 0;
|
|
1972
|
+
let contentNodes = 0;
|
|
1973
|
+
const admitContent = () => {
|
|
1974
|
+
if (++contentNodes > maxContentNodes) throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
1975
|
+
};
|
|
1976
|
+
const whitespace = function* () {
|
|
1977
|
+
let work = 0;
|
|
1978
|
+
while (offset < source.length && " \n\r".includes(source[offset])) {
|
|
1979
|
+
offset++;
|
|
1980
|
+
if (++work === 512) {
|
|
1981
|
+
yield work;
|
|
1982
|
+
work = 0;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
if (work) yield work;
|
|
1986
|
+
};
|
|
1987
|
+
const readName = function* () {
|
|
1988
|
+
const start = offset;
|
|
1989
|
+
while (offset < source.length && !" \r\n/=>?".includes(source[offset])) {
|
|
1990
|
+
offset++;
|
|
1991
|
+
if ((offset - start) % 512 === 0) yield 512;
|
|
1992
|
+
}
|
|
1993
|
+
if ((offset - start) % 512) yield (offset - start) % 512;
|
|
1994
|
+
const name = source.slice(start, offset);
|
|
1995
|
+
yield* qualifiedName(name);
|
|
1996
|
+
return name;
|
|
1997
|
+
};
|
|
1998
|
+
const appendText = function* (text, kind = "text") {
|
|
1999
|
+
const parent = stack.at(-1);
|
|
2000
|
+
if (parent) {
|
|
2001
|
+
parent.element.text += text;
|
|
2002
|
+
if (retainContent && (text.length || kind === "cdata")) {
|
|
2003
|
+
admitContent();
|
|
2004
|
+
parent.content.push({ kind, text });
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
for (let index = 0; index < text.length; index++) {
|
|
2008
|
+
if (!" \r\n".includes(text[index])) invalid("text outside the root");
|
|
2009
|
+
if ((index + 1) % 512 === 0) yield 512;
|
|
2010
|
+
}
|
|
2011
|
+
if (text.length % 512) yield text.length % 512;
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
while (offset < source.length) {
|
|
2015
|
+
yield 1;
|
|
2016
|
+
if (source[offset] !== "<") {
|
|
2017
|
+
const next = yield* find(source, "<", offset);
|
|
2018
|
+
const text = source.slice(offset, next < 0 ? source.length : next);
|
|
2019
|
+
if ((yield* find(text, "]]>", 0)) >= 0) invalid("CDATA terminator in text");
|
|
2020
|
+
yield* appendText(yield* entities(text));
|
|
2021
|
+
offset += text.length;
|
|
2022
|
+
} else if (source.startsWith("<!--", offset)) {
|
|
2023
|
+
const end = yield* find(source, "-->", offset + 4);
|
|
2024
|
+
if (end < 0 || (yield* find(source.slice(offset + 4, end), "--", 0)) >= 0 || source.slice(offset + 4, end).endsWith("-")) {
|
|
2025
|
+
invalid("malformed comment");
|
|
2026
|
+
}
|
|
2027
|
+
const parent = stack.at(-1);
|
|
2028
|
+
if (retainContent && parent) {
|
|
2029
|
+
admitContent();
|
|
2030
|
+
parent.content.push({ kind: "comment", text: source.slice(offset + 4, end) });
|
|
2031
|
+
}
|
|
2032
|
+
offset = end + 3;
|
|
2033
|
+
} else if (source.startsWith("<![CDATA[", offset)) {
|
|
2034
|
+
if (!stack.length) invalid("CDATA outside root");
|
|
2035
|
+
const end = yield* find(source, "]]>", offset + 9);
|
|
2036
|
+
if (end < 0) invalid("unterminated CDATA");
|
|
2037
|
+
yield* appendText(source.slice(offset + 9, end), "cdata");
|
|
2038
|
+
offset = end + 3;
|
|
2039
|
+
} else if (source.startsWith("<?", offset)) {
|
|
2040
|
+
const start = offset;
|
|
2041
|
+
offset += 2;
|
|
2042
|
+
const target = yield* readName();
|
|
2043
|
+
const end = yield* find(source, "?>", offset);
|
|
2044
|
+
if (end < 0) invalid("unterminated processing instruction");
|
|
2045
|
+
const content = source.slice(offset, end);
|
|
2046
|
+
if (target.length === 3 && target.toLowerCase() === "xml") {
|
|
2047
|
+
if (start !== 0 || target !== "xml" || !(yield* validDeclaration(content, limits.expectedEncoding))) {
|
|
2048
|
+
invalid("unsupported XML declaration");
|
|
2049
|
+
}
|
|
2050
|
+
if (retainContent) declaration = source.slice(start, end + 2);
|
|
2051
|
+
} else if (content && !" \n\r".includes(content[0])) invalid("invalid processing instruction");
|
|
2052
|
+
if (!(target.length === 3 && target.toLowerCase() === "xml")) {
|
|
2053
|
+
const parent = stack.at(-1);
|
|
2054
|
+
if (retainContent && parent) {
|
|
2055
|
+
let start2 = 0;
|
|
2056
|
+
while (start2 < content.length && " \n\r".includes(content[start2])) {
|
|
2057
|
+
start2++;
|
|
2058
|
+
if (start2 % 512 === 0) yield 512;
|
|
2059
|
+
}
|
|
2060
|
+
if (start2 % 512) yield start2 % 512;
|
|
2061
|
+
admitContent();
|
|
2062
|
+
parent.content.push({ kind: "processing-instruction", target, text: content.slice(start2) });
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
offset = end + 2;
|
|
2066
|
+
} else if (source.startsWith("<!", offset)) {
|
|
2067
|
+
invalid("DTD and entity declarations are forbidden");
|
|
2068
|
+
} else if (source.startsWith("</", offset)) {
|
|
2069
|
+
offset += 2;
|
|
2070
|
+
const name = yield* readName();
|
|
2071
|
+
yield* whitespace();
|
|
2072
|
+
if (source[offset++] !== ">" || stack.pop()?.name !== name) invalid("mismatched closing tag");
|
|
2073
|
+
} else {
|
|
2074
|
+
offset++;
|
|
2075
|
+
const name = yield* readName();
|
|
2076
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
2077
|
+
let namespaces = stack.at(-1)?.namespaces ?? /* @__PURE__ */ new Map([["xml", xmlNamespace]]);
|
|
2078
|
+
let ownsNamespaces = stack.length === 0;
|
|
2079
|
+
while (true) {
|
|
2080
|
+
const beforeSpace = offset;
|
|
2081
|
+
yield* whitespace();
|
|
2082
|
+
if (source[offset] === "/" || source[offset] === ">") break;
|
|
2083
|
+
if (offset === beforeSpace) invalid("attributes require whitespace");
|
|
2084
|
+
const attribute = yield* readName();
|
|
2085
|
+
if (attributes.has(attribute)) invalid("duplicate attribute");
|
|
2086
|
+
if (++attributeCount > maxAttributes) throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
2087
|
+
if (attributes.size >= maxAttributesPerElement) throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
2088
|
+
yield* whitespace();
|
|
2089
|
+
if (source[offset++] !== "=") invalid("missing attribute equals");
|
|
2090
|
+
yield* whitespace();
|
|
2091
|
+
const quote = source[offset++];
|
|
2092
|
+
if (quote !== '"' && quote !== "'") invalid("unquoted attribute");
|
|
2093
|
+
const end = yield* find(source, quote, offset);
|
|
2094
|
+
if (end < 0) invalid("unterminated attribute");
|
|
2095
|
+
const raw = source.slice(offset, end);
|
|
2096
|
+
if ((yield* find(raw, "<", 0)) >= 0) invalid("less-than in attribute");
|
|
2097
|
+
let normalized = "";
|
|
2098
|
+
for (let index = 0; index < raw.length; index++) {
|
|
2099
|
+
const character = raw[index];
|
|
2100
|
+
normalized += character === " " || character === "\n" || character === "\r" ? " " : character;
|
|
2101
|
+
if ((index + 1) % 512 === 0) yield 512;
|
|
2102
|
+
}
|
|
2103
|
+
if (raw.length % 512) yield raw.length % 512;
|
|
2104
|
+
const value = yield* entities(normalized);
|
|
2105
|
+
attributes.set(attribute, value);
|
|
2106
|
+
offset = end + 1;
|
|
2107
|
+
if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
|
|
2108
|
+
const prefix2 = attribute === "xmlns" ? "" : attribute.slice(6);
|
|
2109
|
+
if (prefix2 === "xmlns" || value === xmlnsNamespace || prefix2 === "xml" !== (value === xmlNamespace) || prefix2 !== "" && value === "") invalid("invalid namespace binding");
|
|
2110
|
+
if (!ownsNamespaces) {
|
|
2111
|
+
const copy = /* @__PURE__ */ new Map();
|
|
2112
|
+
for (const [key, uri] of namespaces) {
|
|
2113
|
+
copy.set(key, uri);
|
|
2114
|
+
yield 1;
|
|
2115
|
+
}
|
|
2116
|
+
namespaces = copy;
|
|
2117
|
+
ownsNamespaces = true;
|
|
2118
|
+
}
|
|
2119
|
+
namespaces.set(prefix2, value);
|
|
2120
|
+
if (namespaces.size > maxNamespaces) throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
2124
|
+
for (const attribute of attributes.keys()) {
|
|
2125
|
+
if (attribute === "xmlns" || attribute.startsWith("xmlns:")) continue;
|
|
2126
|
+
const [prefix2, localName2] = yield* qualifiedName(attribute);
|
|
2127
|
+
if (prefix2 && !namespaces.has(prefix2)) invalid("unbound attribute prefix");
|
|
2128
|
+
const key = JSON.stringify([prefix2 ? namespaces.get(prefix2) : "", localName2]);
|
|
2129
|
+
if (expanded.has(key)) invalid("duplicate expanded attribute");
|
|
2130
|
+
expanded.add(key);
|
|
2131
|
+
}
|
|
2132
|
+
const [prefix, localName] = yield* qualifiedName(name);
|
|
2133
|
+
if (prefix === "xmlns" || prefix && !namespaces.has(prefix)) invalid("unbound element prefix");
|
|
2134
|
+
if (++nodes > maxNodes) throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
|
|
2135
|
+
if (stack.length + 1 > maxDepth) throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
|
|
2136
|
+
const namespace = namespaces.get(prefix) ?? "";
|
|
2137
|
+
limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
|
|
2138
|
+
admitContent();
|
|
2139
|
+
const retainedAttributes = [];
|
|
2140
|
+
for (const [attribute, value] of retainContent ? attributes : []) {
|
|
2141
|
+
const [prefix2, localName2] = yield* qualifiedName(attribute);
|
|
2142
|
+
admitContent();
|
|
2143
|
+
retainedAttributes.push({
|
|
2144
|
+
name: attribute,
|
|
2145
|
+
localName: localName2,
|
|
2146
|
+
namespace: attribute === "xmlns" || prefix2 === "xmlns" ? xmlnsNamespace : prefix2 ? namespaces.get(prefix2) : "",
|
|
2147
|
+
value
|
|
2148
|
+
});
|
|
2149
|
+
yield 1;
|
|
2150
|
+
}
|
|
2151
|
+
const content = retainContent ? [] : void 0;
|
|
2152
|
+
const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...root === void 0 && declaration !== void 0 ? { declaration } : {} };
|
|
2153
|
+
const parent = stack.at(-1);
|
|
2154
|
+
if (parent) {
|
|
2155
|
+
parent.element.children.push(element);
|
|
2156
|
+
parent.content?.push(element);
|
|
2157
|
+
} else if (root) invalid("multiple root elements");
|
|
2158
|
+
else root = element;
|
|
2159
|
+
const empty = source[offset] === "/";
|
|
2160
|
+
if (empty) offset++;
|
|
2161
|
+
if (source[offset++] !== ">") invalid("unterminated start tag");
|
|
2162
|
+
if (!empty) stack.push({ element, content, name, namespaces });
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (stack.length || !root) invalid("incomplete document");
|
|
2166
|
+
return root;
|
|
2167
|
+
}
|
|
2168
|
+
function parseXml(input, limits = {}) {
|
|
2169
|
+
const parser = parseXmlSteps(input, limits);
|
|
2170
|
+
let result = parser.next();
|
|
2171
|
+
while (!result.done) result = parser.next();
|
|
2172
|
+
return result.value;
|
|
2173
|
+
}
|
|
2174
|
+
var XmlResponseLimitError = class extends SyntaxError {
|
|
2175
|
+
};
|
|
2176
|
+
function invalid2(message) {
|
|
2177
|
+
throw new SyntaxError(`Invalid WebDAV XML: ${message}`);
|
|
2178
|
+
}
|
|
2179
|
+
function parseXml2(input, limits = {}) {
|
|
2180
|
+
const { maxResponses, ...documentLimits } = limits;
|
|
2181
|
+
if (maxResponses !== void 0 && (!Number.isSafeInteger(maxResponses) || maxResponses < 1)) {
|
|
2182
|
+
throw new RangeError("XML limits must be positive integers");
|
|
2183
|
+
}
|
|
2184
|
+
let responses2 = 0;
|
|
2185
|
+
try {
|
|
2186
|
+
return parseXml(input, {
|
|
2187
|
+
...documentLimits,
|
|
2188
|
+
retainContent: false,
|
|
2189
|
+
onElement(element, parent, depth) {
|
|
2190
|
+
if (maxResponses !== void 0 && depth === 2 && parent?.namespace === "DAV:" && parent.localName === "multistatus" && element.namespace === "DAV:" && element.localName === "response" && ++responses2 > maxResponses) {
|
|
2191
|
+
throw new XmlResponseLimitError("WebDAV XML response limit exceeded");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
if (error instanceof XmlLimitError) invalid2(error.message);
|
|
2197
|
+
if (error instanceof SyntaxError && error.message.startsWith("Invalid XML:")) {
|
|
2198
|
+
error.message = error.message.replace("Invalid XML:", "Invalid WebDAV XML:");
|
|
2199
|
+
}
|
|
2200
|
+
throw error;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function davChildren(element, localName) {
|
|
2204
|
+
return element.children.filter((child) => child.namespace === "DAV:" && child.localName === localName);
|
|
2205
|
+
}
|
|
2206
|
+
function davChild(element, localName) {
|
|
2207
|
+
const children = davChildren(element, localName);
|
|
2208
|
+
if (children.length > 1) invalid2(`duplicate DAV:${localName}`);
|
|
2209
|
+
return children[0];
|
|
2210
|
+
}
|
|
2211
|
+
function scalar(element) {
|
|
2212
|
+
if (element.children.length) invalid2("expected text-only element");
|
|
2213
|
+
return element.text.trim();
|
|
2214
|
+
}
|
|
2215
|
+
var timestampNamespace = "urn:virtual-bash:metadata";
|
|
2216
|
+
var propfindBody = `<?xml version="1.0" encoding="utf-8"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/><d:getlastmodified/><d:creationdate/><d:getetag/><v:timestamps xmlns:v="${timestampNamespace}"/></d:prop></d:propfind>`;
|
|
2217
|
+
var lockBody = '<d:lockinfo xmlns:d="DAV:"><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype></d:lockinfo>';
|
|
2218
|
+
var resourceIdBody = '<?xml version="1.0" encoding="utf-8"?><d:propfind xmlns:d="DAV:"><d:prop><d:resource-id/></d:prop></d:propfind>';
|
|
2219
|
+
function fail3(code, syscall, path, message) {
|
|
2220
|
+
throw new FsError(code, { syscall, path, ...message === void 0 ? {} : { message } });
|
|
2221
|
+
}
|
|
2222
|
+
function positive(value, name, zero = false) {
|
|
2223
|
+
if (!Number.isSafeInteger(value) || value < (zero ? 0 : 1)) {
|
|
2224
|
+
throw new FsError("EINVAL", { message: `${name} must be a ${zero ? "nonnegative" : "positive"} safe integer` });
|
|
2225
|
+
}
|
|
2226
|
+
return value;
|
|
2227
|
+
}
|
|
2228
|
+
function createRequestTimeout(timeoutMs) {
|
|
2229
|
+
const controller = new AbortController();
|
|
2230
|
+
const timer = setTimeout(() => {
|
|
2231
|
+
controller.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
|
2232
|
+
}, timeoutMs);
|
|
2233
|
+
if (typeof timer === "object" && timer !== null) {
|
|
2234
|
+
const handle = timer;
|
|
2235
|
+
handle.unref?.();
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
signal: controller.signal,
|
|
2239
|
+
dispose() {
|
|
2240
|
+
clearTimeout(timer);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function nativeRequestStreamsSupported(url) {
|
|
2245
|
+
let body;
|
|
2246
|
+
try {
|
|
2247
|
+
body = new ReadableStream({ start(destination) {
|
|
2248
|
+
destination.close();
|
|
2249
|
+
} });
|
|
2250
|
+
let duplexAccessed = false;
|
|
2251
|
+
const init = {
|
|
2252
|
+
method: "PUT",
|
|
2253
|
+
body,
|
|
2254
|
+
get duplex() {
|
|
2255
|
+
duplexAccessed = true;
|
|
2256
|
+
return "half";
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
const request = new Request(url, init);
|
|
2260
|
+
return duplexAccessed && !request.headers.has("Content-Type");
|
|
2261
|
+
} catch {
|
|
2262
|
+
return false;
|
|
2263
|
+
} finally {
|
|
2264
|
+
if (body && !body.locked) void body.cancel().catch(() => {
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function strongEtag(value, path) {
|
|
2269
|
+
if (!value || !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(value)) {
|
|
2270
|
+
fail3("ENOTSUP", "webdav", path, "operation requires a strong entity tag");
|
|
2271
|
+
}
|
|
2272
|
+
return value;
|
|
2273
|
+
}
|
|
2274
|
+
function normalize(path) {
|
|
2275
|
+
if (typeof path !== "string" || path.includes("\0") || path.includes("\\")) {
|
|
2276
|
+
fail3("EINVAL", "resolve", String(path), "invalid WebDAV path");
|
|
2277
|
+
}
|
|
2278
|
+
try {
|
|
2279
|
+
encodeURIComponent(path);
|
|
2280
|
+
} catch {
|
|
2281
|
+
fail3("EINVAL", "resolve", path, "path contains an unpaired surrogate");
|
|
2282
|
+
}
|
|
2283
|
+
const segments = [];
|
|
2284
|
+
for (const segment of path.split("/")) {
|
|
2285
|
+
if (!segment || segment === ".") continue;
|
|
2286
|
+
if (segment === "..") {
|
|
2287
|
+
if (!segments.length) fail3("EACCES", "resolve", path, "path escapes WebDAV root");
|
|
2288
|
+
segments.pop();
|
|
2289
|
+
} else segments.push(segment);
|
|
2290
|
+
}
|
|
2291
|
+
return `/${segments.join("/")}`;
|
|
2292
|
+
}
|
|
2293
|
+
function validateDirectoryWalkPath(path, syscall = "access") {
|
|
2294
|
+
if (typeof path !== "string") fail3("EINVAL", "resolve", String(path), "invalid WebDAV path");
|
|
2295
|
+
let bytes = 0;
|
|
2296
|
+
let components2 = 0;
|
|
2297
|
+
let inComponent = false;
|
|
2298
|
+
for (let offset = 0; offset < path.length; offset++) {
|
|
2299
|
+
const point = path.codePointAt(offset);
|
|
2300
|
+
if (point === 47) inComponent = false;
|
|
2301
|
+
else if (!inComponent) {
|
|
2302
|
+
components2++;
|
|
2303
|
+
inComponent = true;
|
|
2304
|
+
}
|
|
2305
|
+
bytes += point <= 127 ? 1 : point <= 2047 ? 2 : point <= 65535 ? 3 : 4;
|
|
2306
|
+
if (point > 65535) offset++;
|
|
2307
|
+
if (bytes > 65536 || components2 > 256) {
|
|
2308
|
+
fail3("ENAMETOOLONG", syscall, path, "directory access exceeds the 64KiB path or 256 component limit");
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function requiresCollection(path) {
|
|
2313
|
+
const last = path.split("/").at(-1);
|
|
2314
|
+
return last === "" || last === "." || last === "..";
|
|
2315
|
+
}
|
|
2316
|
+
function validateUrlText(text) {
|
|
2317
|
+
if (/[\u0000-\u0020\u007f\\?#]/.test(text)) throw new Error("invalid URL characters");
|
|
2318
|
+
for (const segment of text.split("/")) {
|
|
2319
|
+
const decoded = decodeURIComponent(segment);
|
|
2320
|
+
if (decoded === "." || decoded === ".." || /[/\\\u0000]/.test(decoded)) {
|
|
2321
|
+
throw new Error("ambiguous URL segment");
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function urlSegments(url) {
|
|
2326
|
+
const pathname = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
2327
|
+
const segments = pathname.slice(1).split("/");
|
|
2328
|
+
if (segments.length === 1 && segments[0] === "") return [];
|
|
2329
|
+
if (segments.some((segment) => segment === "")) throw new Error("empty URL segment");
|
|
2330
|
+
return segments.map((segment) => decodeURIComponent(segment));
|
|
2331
|
+
}
|
|
2332
|
+
function statusCode(element) {
|
|
2333
|
+
const text = scalar(element);
|
|
2334
|
+
if (!/^HTTP\/\d+\.\d+ [2-5]\d\d(?:[ \t].*)?$/.test(text)) throw new Error("invalid DAV status line");
|
|
2335
|
+
return Number(text.split(" ")[1]);
|
|
2336
|
+
}
|
|
2337
|
+
var WebDavFileSystem = class {
|
|
2338
|
+
capabilities = Object.freeze({
|
|
2339
|
+
read: true,
|
|
2340
|
+
stat: true,
|
|
2341
|
+
readdir: true,
|
|
2342
|
+
realpath: true,
|
|
2343
|
+
access: true,
|
|
2344
|
+
write: true,
|
|
2345
|
+
append: true,
|
|
2346
|
+
exclusiveCreate: true,
|
|
2347
|
+
explicitDirectories: true,
|
|
2348
|
+
implicitDirectories: false,
|
|
2349
|
+
mkdir: true,
|
|
2350
|
+
recursiveMkdir: true,
|
|
2351
|
+
remove: true,
|
|
2352
|
+
recursiveRemove: true,
|
|
2353
|
+
rename: true,
|
|
2354
|
+
atomicRenameNoReplace: false,
|
|
2355
|
+
copy: true,
|
|
2356
|
+
exclusiveCopy: true,
|
|
2357
|
+
readlink: false,
|
|
2358
|
+
truncate: false,
|
|
2359
|
+
randomAccessWrite: false,
|
|
2360
|
+
removeDirectory: false,
|
|
2361
|
+
streamingAppend: false,
|
|
2362
|
+
symlinks: false,
|
|
2363
|
+
hardlinks: false,
|
|
2364
|
+
permissions: false,
|
|
2365
|
+
timestamps: true,
|
|
2366
|
+
atomicRename: false,
|
|
2367
|
+
streamingRead: true,
|
|
2368
|
+
streamingWrite: true
|
|
2369
|
+
});
|
|
2370
|
+
base;
|
|
2371
|
+
baseSegments;
|
|
2372
|
+
transport;
|
|
2373
|
+
requestStreamSupport;
|
|
2374
|
+
headers;
|
|
2375
|
+
maxResponseBytes;
|
|
2376
|
+
maxXmlBytes;
|
|
2377
|
+
maxEntries;
|
|
2378
|
+
timeoutMs;
|
|
2379
|
+
walkDeadlines = /* @__PURE__ */ new WeakMap();
|
|
2380
|
+
overwritePolicy;
|
|
2381
|
+
configuredComparison;
|
|
2382
|
+
atomicEmptyDirectory;
|
|
2383
|
+
etags = /* @__PURE__ */ new WeakMap();
|
|
2384
|
+
constructor(options) {
|
|
2385
|
+
if (typeof options.fetch !== "function") throw new FsError("EINVAL", { message: "an explicit fetch transport is required" });
|
|
2386
|
+
if (options.requestStreamSupport !== void 0 && options.requestStreamSupport !== "native" && typeof options.requestStreamSupport !== "boolean") {
|
|
2387
|
+
throw new FsError("EINVAL", { message: "requestStreamSupport must be native or a boolean" });
|
|
2388
|
+
}
|
|
2389
|
+
if (options.compareEntry !== void 0 && typeof options.compareEntry !== "function") {
|
|
2390
|
+
throw new FsError("EINVAL", { message: "compareEntry must be a function" });
|
|
2391
|
+
}
|
|
2392
|
+
if (options.compareEntry !== void 0) assertCallbackAuthorityAllowed();
|
|
2393
|
+
try {
|
|
2394
|
+
validateUrlText(options.baseUrl);
|
|
2395
|
+
this.base = new URL(options.baseUrl);
|
|
2396
|
+
if (!["http:", "https:"].includes(this.base.protocol) || this.base.username || this.base.password || this.base.search || this.base.hash) throw new Error("invalid base URL");
|
|
2397
|
+
this.baseSegments = urlSegments(this.base);
|
|
2398
|
+
this.base.pathname = `/${this.baseSegments.map(encodeURIComponent).join("/")}${this.baseSegments.length ? "/" : ""}`;
|
|
2399
|
+
this.headers = new Headers(options.headers);
|
|
2400
|
+
for (const name of this.headers.keys()) {
|
|
2401
|
+
if ([
|
|
2402
|
+
"host",
|
|
2403
|
+
"destination",
|
|
2404
|
+
"depth",
|
|
2405
|
+
"overwrite",
|
|
2406
|
+
"if",
|
|
2407
|
+
"if-match",
|
|
2408
|
+
"if-none-match",
|
|
2409
|
+
"range",
|
|
2410
|
+
"content-length",
|
|
2411
|
+
"content-type",
|
|
2412
|
+
"transfer-encoding",
|
|
2413
|
+
"connection",
|
|
2414
|
+
"proxy-authorization",
|
|
2415
|
+
"lock-token",
|
|
2416
|
+
"timeout"
|
|
2417
|
+
].includes(name)) {
|
|
2418
|
+
throw new Error(`reserved header ${name}`);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
if (this.base.protocol !== "https:" && [...this.headers.keys()].some((name) => name === "authorization" || name === "cookie")) {
|
|
2422
|
+
throw new Error("explicit credentials require HTTPS");
|
|
2423
|
+
}
|
|
2424
|
+
} catch (cause) {
|
|
2425
|
+
throw new FsError("EINVAL", { message: "invalid WebDAV base URL or headers", cause });
|
|
2426
|
+
}
|
|
2427
|
+
const binding = options.atomicEmptyDirectory;
|
|
2428
|
+
if (binding !== void 0) {
|
|
2429
|
+
if (!binding || typeof binding !== "object" || binding.namespaceUrl !== this.base.href || typeof binding.removeEmptyDirectory !== "function") {
|
|
2430
|
+
throw new FsError("EINVAL", { message: "atomicEmptyDirectory must bind the canonical WebDAV namespace and a callback" });
|
|
2431
|
+
}
|
|
2432
|
+
this.atomicEmptyDirectory = Object.freeze({ namespaceUrl: this.base.href, removeEmptyDirectory: binding.removeEmptyDirectory });
|
|
2433
|
+
}
|
|
2434
|
+
this.transport = options.fetch === globalThis.fetch ? options.fetch.bind(globalThis) : options.fetch;
|
|
2435
|
+
this.requestStreamSupport = options.requestStreamSupport ?? (options.fetch === globalThis.fetch ? "native" : false);
|
|
2436
|
+
this.capabilities = Object.freeze({
|
|
2437
|
+
...this.capabilities,
|
|
2438
|
+
removeDirectory: this.atomicEmptyDirectory !== void 0,
|
|
2439
|
+
streamingAppend: this.requestStreamSupport !== false
|
|
2440
|
+
});
|
|
2441
|
+
this.maxResponseBytes = positive(options.maxResponseBytes ?? 64 * 1024 * 1024, "maxResponseBytes");
|
|
2442
|
+
this.maxXmlBytes = positive(options.maxXmlBytes ?? 2 * 1024 * 1024, "maxXmlBytes");
|
|
2443
|
+
this.maxEntries = positive(options.maxEntries ?? 1e4, "maxEntries");
|
|
2444
|
+
this.timeoutMs = positive(options.timeoutMs ?? 3e4, "timeoutMs");
|
|
2445
|
+
this.overwritePolicy = options.overwritePolicy ?? "lock";
|
|
2446
|
+
this.configuredComparison = options.compareEntry !== void 0;
|
|
2447
|
+
if (!["lock", "etag"].includes(this.overwritePolicy)) throw new FsError("EINVAL", { message: "invalid overwritePolicy" });
|
|
2448
|
+
if (this.timeoutMs > 2147483647) throw new FsError("EINVAL", { message: "timeoutMs exceeds timer range" });
|
|
2449
|
+
registerResourceQuery(this, (path, settings) => this.resourceId(path, settings), originalWebDavComparison, options.compareEntry);
|
|
2450
|
+
registerEntryAuthority(this, compareWebDavResources);
|
|
2451
|
+
}
|
|
2452
|
+
compareEntry(path, peer, peerPath, options = {}) {
|
|
2453
|
+
return compareEntries(this, path, peer, peerPath, options);
|
|
2454
|
+
}
|
|
2455
|
+
async resourceId(input, options) {
|
|
2456
|
+
const path = normalize(input);
|
|
2457
|
+
return this.request("PROPFIND", path, options, {
|
|
2458
|
+
headers: { Depth: "0", "Content-Type": "application/xml; charset=utf-8" },
|
|
2459
|
+
body: resourceIdBody
|
|
2460
|
+
}, async (response, signal) => {
|
|
2461
|
+
if (response.status !== 207) this.httpError(response.status, "PROPFIND", path);
|
|
2462
|
+
const responses2 = await this.multistatus(response, signal, "PROPFIND", path);
|
|
2463
|
+
if (responses2.length !== 1) throw new Error("resource identity requires exactly one DAV:response");
|
|
2464
|
+
const entry = responses2[0];
|
|
2465
|
+
const href = davChild(entry, "href");
|
|
2466
|
+
if (!href) throw new Error("missing resource identity response href");
|
|
2467
|
+
if (this.hrefPath(scalar(href)) !== path) fail3("EACCES", "PROPFIND", path, "resource identity response addresses another resource");
|
|
2468
|
+
const wholeStatus = davChild(entry, "status");
|
|
2469
|
+
const propstats = davChildren(entry, "propstat");
|
|
2470
|
+
if (wholeStatus) {
|
|
2471
|
+
if (propstats.length) throw new Error("conflicting response status and propstat");
|
|
2472
|
+
this.httpError(statusCode(wholeStatus), "PROPFIND", path);
|
|
2473
|
+
}
|
|
2474
|
+
if (propstats.length === 0) throw new Error("missing resource identity propstat");
|
|
2475
|
+
let seen = false;
|
|
2476
|
+
let identifier;
|
|
2477
|
+
for (const propstat of propstats) {
|
|
2478
|
+
const status = davChild(propstat, "status");
|
|
2479
|
+
const prop = davChild(propstat, "prop");
|
|
2480
|
+
if (!status || !prop) throw new Error("incomplete resource identity propstat");
|
|
2481
|
+
const code = statusCode(status);
|
|
2482
|
+
if (code !== 200 && code !== 404) this.httpError(code, "PROPFIND", path);
|
|
2483
|
+
for (const property of davChildren(prop, "resource-id")) {
|
|
2484
|
+
if (seen) throw new Error("duplicate or conflicting resource identity property");
|
|
2485
|
+
seen = true;
|
|
2486
|
+
if (code === 404) continue;
|
|
2487
|
+
const value = davChild(property, "href");
|
|
2488
|
+
if (!value || property.children.length !== 1 || property.text.trim()) throw new Error("invalid resource identity property");
|
|
2489
|
+
identifier = resourceIdentifier(scalar(value));
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
const owned = ownedResponseIdentifier(response, path);
|
|
2493
|
+
if (owned !== void 0 && identifier !== void 0 && resourceIdentifier(owned) !== identifier) {
|
|
2494
|
+
throw new Error("resource identity contradicts provider provenance");
|
|
2495
|
+
}
|
|
2496
|
+
return identifier;
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
url(path, collection = false) {
|
|
2500
|
+
const relative = path.slice(1).split("/").map(encodeURIComponent).join("/");
|
|
2501
|
+
return `${this.base.href}${relative}${collection && relative ? "/" : ""}`;
|
|
2502
|
+
}
|
|
2503
|
+
hrefPath(href) {
|
|
2504
|
+
try {
|
|
2505
|
+
validateUrlText(href);
|
|
2506
|
+
if (!(href.startsWith("/") && !href.startsWith("//")) && !/^https?:\/\//i.test(href)) throw new Error("href must be absolute");
|
|
2507
|
+
const url = new URL(href, this.base);
|
|
2508
|
+
if (url.origin !== this.base.origin || url.username || url.password || url.search || url.hash) throw new Error("href is not confined");
|
|
2509
|
+
const segments = urlSegments(url);
|
|
2510
|
+
if (segments.length < this.baseSegments.length || this.baseSegments.some((segment, index) => segments[index] !== segment)) {
|
|
2511
|
+
throw new Error("href escapes root");
|
|
2512
|
+
}
|
|
2513
|
+
return normalize(`/${segments.slice(this.baseSegments.length).join("/")}`);
|
|
2514
|
+
} catch (cause) {
|
|
2515
|
+
throw new FsError("EACCES", { syscall: "webdav", message: "response URL escapes or ambiguously addresses WebDAV root", cause });
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
httpError(status, method, path, exclusive = false) {
|
|
2519
|
+
const codes = {
|
|
2520
|
+
400: "EINVAL",
|
|
2521
|
+
401: "EACCES",
|
|
2522
|
+
403: "EACCES",
|
|
2523
|
+
404: "ENOENT",
|
|
2524
|
+
405: method === "MKCOL" ? "EEXIST" : "ENOTSUP",
|
|
2525
|
+
408: "ETIMEDOUT",
|
|
2526
|
+
409: method === "PUT" || method === "MKCOL" ? "ENOENT" : "EINVAL",
|
|
2527
|
+
410: "ENOENT",
|
|
2528
|
+
412: exclusive ? "EEXIST" : "EAGAIN",
|
|
2529
|
+
413: "EFBIG",
|
|
2530
|
+
414: "ENAMETOOLONG",
|
|
2531
|
+
415: "ENOTSUP",
|
|
2532
|
+
423: "EBUSY",
|
|
2533
|
+
429: "EAGAIN",
|
|
2534
|
+
501: "ENOTSUP",
|
|
2535
|
+
503: "EAGAIN",
|
|
2536
|
+
504: "ETIMEDOUT",
|
|
2537
|
+
507: "ENOSPC"
|
|
2538
|
+
};
|
|
2539
|
+
fail3(
|
|
2540
|
+
status >= 300 && status < 400 ? "ENOTSUP" : codes[status] ?? "EIO",
|
|
2541
|
+
method,
|
|
2542
|
+
path,
|
|
2543
|
+
status >= 300 && status < 400 ? "WebDAV redirects are not followed" : `WebDAV HTTP status ${status}`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
async request(method, path, options, init, consume, collection = false, received) {
|
|
2547
|
+
for await (const result of this.requestStream(method, path, options, init, async function* (response, signal) {
|
|
2548
|
+
yield await consume(response, signal);
|
|
2549
|
+
}, collection, received)) return result;
|
|
2550
|
+
throw new Error("missing WebDAV response");
|
|
2551
|
+
}
|
|
2552
|
+
async *requestStream(method, path, options, init, consume, collection = false, received) {
|
|
2553
|
+
if (options.signal?.aborted) fail3("ECANCELED", method, path);
|
|
2554
|
+
const walk = this.walkDeadlines.get(options);
|
|
2555
|
+
const deadline = walk?.deadline ?? createRequestTimeout(this.timeoutMs);
|
|
2556
|
+
if (walk) walk.deadline = deadline;
|
|
2557
|
+
const timeout = deadline.signal;
|
|
2558
|
+
try {
|
|
2559
|
+
const headers = new Headers(this.headers);
|
|
2560
|
+
for (const [name, value] of new Headers(init.headers)) headers.set(name, value);
|
|
2561
|
+
headers.set("Cache-Control", "no-cache");
|
|
2562
|
+
const signalScope = options.signal ? composeAbortSignals([timeout, options.signal]) : void 0;
|
|
2563
|
+
const signal = signalScope?.signal ?? timeout;
|
|
2564
|
+
let response;
|
|
2565
|
+
const receive = (value, late) => {
|
|
2566
|
+
if (value.redirected || value.type === "opaqueredirect") fail3("ENOTSUP", method, path, "transport followed or hid a redirect");
|
|
2567
|
+
if (value.url && this.hrefPath(value.url) !== path) fail3("EACCES", method, path, "transport changed the requested resource");
|
|
2568
|
+
received?.(value, late);
|
|
2569
|
+
};
|
|
2570
|
+
try {
|
|
2571
|
+
let url = this.url(path, collection);
|
|
2572
|
+
for (let attempt = 0; ; attempt++) {
|
|
2573
|
+
signal.throwIfAborted();
|
|
2574
|
+
response = await new Promise((resolve, reject) => {
|
|
2575
|
+
let abandoned = false;
|
|
2576
|
+
const abort = () => {
|
|
2577
|
+
abandoned = true;
|
|
2578
|
+
signal.removeEventListener("abort", abort);
|
|
2579
|
+
reject(signal.reason);
|
|
2580
|
+
};
|
|
2581
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2582
|
+
try {
|
|
2583
|
+
Promise.resolve(this.transport(url, {
|
|
2584
|
+
...init,
|
|
2585
|
+
method,
|
|
2586
|
+
headers,
|
|
2587
|
+
redirect: "manual",
|
|
2588
|
+
credentials: "omit",
|
|
2589
|
+
signal
|
|
2590
|
+
})).then((value) => {
|
|
2591
|
+
signal.removeEventListener("abort", abort);
|
|
2592
|
+
if (!abandoned) resolve(value);
|
|
2593
|
+
else {
|
|
2594
|
+
try {
|
|
2595
|
+
receive(value, true);
|
|
2596
|
+
} catch (ignoredError) {
|
|
2597
|
+
void ignoredError;
|
|
2598
|
+
} finally {
|
|
2599
|
+
if (value.body && !value.body.locked) void value.body.cancel(signal.reason).catch(() => {
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}, (error) => {
|
|
2604
|
+
signal.removeEventListener("abort", abort);
|
|
2605
|
+
reject(error);
|
|
2606
|
+
}).catch(reject);
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
signal.removeEventListener("abort", abort);
|
|
2609
|
+
reject(error);
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
receive(response, false);
|
|
2613
|
+
signal.throwIfAborted();
|
|
2614
|
+
const location = response.headers.get("Location");
|
|
2615
|
+
if (attempt === 0 && method === "PROPFIND" && !url.endsWith("/") && [301, 302, 307, 308].includes(response.status) && location !== null && this.hrefPath(location) === path && new URL(location, this.base).href === `${url}/`) {
|
|
2616
|
+
await response.body?.cancel();
|
|
2617
|
+
signal.throwIfAborted();
|
|
2618
|
+
url += "/";
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
yield* consume(response, signal);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
} catch (cause) {
|
|
2625
|
+
if (options.signal?.aborted) throw new FsError("ECANCELED", { syscall: method, path, cause });
|
|
2626
|
+
if (timeout.aborted) throw new FsError("ETIMEDOUT", { syscall: method, path, cause });
|
|
2627
|
+
if (isFsError(cause)) throw cause;
|
|
2628
|
+
throw new FsError("EIO", { syscall: method, path, message: "WebDAV transport or response failure", cause });
|
|
2629
|
+
} finally {
|
|
2630
|
+
signalScope?.dispose();
|
|
2631
|
+
if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
} finally {
|
|
2635
|
+
if (!walk) deadline.dispose();
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
async bytes(response, limit, signal) {
|
|
2639
|
+
const chunks = [];
|
|
2640
|
+
let size = 0;
|
|
2641
|
+
for await (const chunk of this.bodyChunks(response, limit, signal)) {
|
|
2642
|
+
chunks.push(chunk);
|
|
2643
|
+
size += chunk.byteLength;
|
|
2644
|
+
}
|
|
2645
|
+
const data = new Uint8Array(size);
|
|
2646
|
+
let offset = 0;
|
|
2647
|
+
for (const chunk of chunks) {
|
|
2648
|
+
data.set(chunk, offset);
|
|
2649
|
+
offset += chunk.byteLength;
|
|
2650
|
+
}
|
|
2651
|
+
return data;
|
|
2652
|
+
}
|
|
2653
|
+
async *bodyChunks(response, limit, signal) {
|
|
2654
|
+
const length = response.headers.get("content-length");
|
|
2655
|
+
if (length !== null && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length)))) {
|
|
2656
|
+
fail3("EIO", "webdav", "", "invalid response Content-Length");
|
|
2657
|
+
}
|
|
2658
|
+
const encoding = response.headers.get("content-encoding");
|
|
2659
|
+
const expected = length !== null && (!encoding || encoding.toLowerCase() === "identity") ? Number(length) : void 0;
|
|
2660
|
+
if (expected !== void 0 && expected > limit) fail3("EFBIG", "webdav", "", "response exceeds byte limit");
|
|
2661
|
+
if (!response.body) {
|
|
2662
|
+
if (expected !== void 0 && expected !== 0) fail3("EIO", "webdav", "", "response body length differs from Content-Length");
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
const reader = response.body.getReader();
|
|
2666
|
+
let size = 0;
|
|
2667
|
+
const abort = () => {
|
|
2668
|
+
void reader.cancel(signal.reason).catch(() => {
|
|
2669
|
+
});
|
|
2670
|
+
};
|
|
2671
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2672
|
+
try {
|
|
2673
|
+
while (true) {
|
|
2674
|
+
signal.throwIfAborted();
|
|
2675
|
+
const result = await reader.read();
|
|
2676
|
+
signal.throwIfAborted();
|
|
2677
|
+
if (result.done) break;
|
|
2678
|
+
size += result.value.byteLength;
|
|
2679
|
+
if (size > limit) fail3("EFBIG", "webdav", "", "response exceeds byte limit");
|
|
2680
|
+
if (expected !== void 0 && size > expected) fail3("EIO", "webdav", "", "response body length differs from Content-Length");
|
|
2681
|
+
yield new Uint8Array(result.value);
|
|
2682
|
+
}
|
|
2683
|
+
if (expected !== void 0 && size !== expected) fail3("EIO", "webdav", "", "response body length differs from Content-Length");
|
|
2684
|
+
} finally {
|
|
2685
|
+
signal.removeEventListener("abort", abort);
|
|
2686
|
+
void reader.cancel().catch(() => {
|
|
2687
|
+
});
|
|
2688
|
+
reader.releaseLock();
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
async xml(response, signal, maxResponses) {
|
|
2692
|
+
const data = await this.bytes(response, this.maxXmlBytes, signal);
|
|
2693
|
+
const encoding = data[0] === 255 && data[1] === 254 || data[0] === 60 && data[1] === 0 ? "utf-16le" : data[0] === 254 && data[1] === 255 || data[0] === 0 && data[1] === 60 ? "utf-16be" : "utf-8";
|
|
2694
|
+
return parseXml2(new TextDecoder(encoding, { fatal: true }).decode(data), maxResponses === void 0 ? void 0 : { maxResponses });
|
|
2695
|
+
}
|
|
2696
|
+
async multistatus(response, signal, method = "PROPFIND", path = "") {
|
|
2697
|
+
const link = response.headers.get("link");
|
|
2698
|
+
if (link && /\brel\s*=\s*(?:"[^"]*\bnext\b[^"]*"|'[^']*\bnext\b[^']*'|next\b)/i.test(link)) {
|
|
2699
|
+
fail3("ENOTSUP", method, path, "paginated WebDAV responses are unsupported");
|
|
2700
|
+
}
|
|
2701
|
+
let root;
|
|
2702
|
+
try {
|
|
2703
|
+
root = await this.xml(response, signal, this.maxEntries);
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
if (error instanceof XmlResponseLimitError) fail3("EFBIG", method, path, "response exceeds entry limit");
|
|
2706
|
+
throw error;
|
|
2707
|
+
}
|
|
2708
|
+
if (root.namespace !== "DAV:" || root.localName !== "multistatus") throw new Error("expected DAV:multistatus");
|
|
2709
|
+
const responses2 = davChildren(root, "response");
|
|
2710
|
+
if (responses2.length > this.maxEntries) fail3("EFBIG", method, path, "response exceeds entry limit");
|
|
2711
|
+
return responses2;
|
|
2712
|
+
}
|
|
2713
|
+
async lockFailure(response, signal, path) {
|
|
2714
|
+
try {
|
|
2715
|
+
const members = /* @__PURE__ */ new Set();
|
|
2716
|
+
let failure;
|
|
2717
|
+
for (const element of await this.multistatus(response, signal, "LOCK", path)) {
|
|
2718
|
+
const href = davChild(element, "href");
|
|
2719
|
+
const status = davChild(element, "status");
|
|
2720
|
+
if (!href || !status) throw new Error("invalid LOCK multistatus");
|
|
2721
|
+
const member = this.hrefPath(scalar(href));
|
|
2722
|
+
if (member !== path && !member.startsWith(`${path}/`)) {
|
|
2723
|
+
fail3("EACCES", "LOCK", path, "LOCK response member is outside requested subtree");
|
|
2724
|
+
}
|
|
2725
|
+
if (members.has(member)) throw new Error("duplicate LOCK response href");
|
|
2726
|
+
members.add(member);
|
|
2727
|
+
const code = statusCode(status);
|
|
2728
|
+
if (code >= 300 && (!failure || failure.status === 424 && code !== 424)) {
|
|
2729
|
+
failure = { member, status: code };
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (!failure) throw new Error("LOCK multistatus without a reported failure");
|
|
2733
|
+
this.httpError(failure.status, "LOCK", failure.member);
|
|
2734
|
+
} catch (cause) {
|
|
2735
|
+
throw new FsError(isFsError(cause) ? cause.code : "EIO", {
|
|
2736
|
+
syscall: "LOCK",
|
|
2737
|
+
path,
|
|
2738
|
+
message: "WebDAV LOCK multistatus failure",
|
|
2739
|
+
cause
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
entryStat(element, path) {
|
|
2744
|
+
const wholeStatus = davChild(element, "status");
|
|
2745
|
+
if (wholeStatus) {
|
|
2746
|
+
const code = statusCode(wholeStatus);
|
|
2747
|
+
if (code < 200 || code >= 300) this.httpError(code, "PROPFIND", path);
|
|
2748
|
+
}
|
|
2749
|
+
const properties = /* @__PURE__ */ new Map();
|
|
2750
|
+
const failures = /* @__PURE__ */ new Map();
|
|
2751
|
+
let timestampProperty;
|
|
2752
|
+
let timestampSeen = false;
|
|
2753
|
+
for (const propstat of davChildren(element, "propstat")) {
|
|
2754
|
+
const status = davChild(propstat, "status");
|
|
2755
|
+
const prop = davChild(propstat, "prop");
|
|
2756
|
+
if (!status || !prop) throw new Error("incomplete DAV:propstat");
|
|
2757
|
+
const code = statusCode(status);
|
|
2758
|
+
for (const property of prop.children) {
|
|
2759
|
+
if (property.namespace === timestampNamespace && property.localName === "timestamps") {
|
|
2760
|
+
if (timestampSeen) throw new Error("duplicate timestamp property");
|
|
2761
|
+
timestampSeen = true;
|
|
2762
|
+
if (code === 200) timestampProperty = property;
|
|
2763
|
+
}
|
|
2764
|
+
if (property.namespace !== "DAV:") continue;
|
|
2765
|
+
if (properties.has(property.localName) || failures.has(property.localName)) throw new Error("duplicate DAV property");
|
|
2766
|
+
if (code >= 200 && code < 300) properties.set(property.localName, property);
|
|
2767
|
+
else failures.set(property.localName, code);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const required = (name) => {
|
|
2771
|
+
const property = properties.get(name);
|
|
2772
|
+
if (property) return property;
|
|
2773
|
+
const failure = failures.get(name);
|
|
2774
|
+
if (failure !== void 0 && failure !== 404) this.httpError(failure, "PROPFIND", path);
|
|
2775
|
+
return fail3("ENOTSUP", "stat", path, `server does not expose DAV:${name}`);
|
|
2776
|
+
};
|
|
2777
|
+
const resourceType = required("resourcetype");
|
|
2778
|
+
const directory = !!davChild(resourceType, "collection");
|
|
2779
|
+
if (resourceType.text.trim() || resourceType.children.some((child) => child.namespace !== "DAV:" || child.localName !== "collection")) {
|
|
2780
|
+
fail3("ENOTSUP", "stat", path, "unsupported DAV resource type");
|
|
2781
|
+
}
|
|
2782
|
+
const sizeText = directory ? "0" : scalar(required("getcontentlength"));
|
|
2783
|
+
if (!/^\d+$/.test(sizeText) || !Number.isSafeInteger(Number(sizeText))) throw new Error("invalid content length property");
|
|
2784
|
+
const date = (name) => {
|
|
2785
|
+
const property = properties.get(name);
|
|
2786
|
+
if (!property) return void 0;
|
|
2787
|
+
const value = Date.parse(scalar(property));
|
|
2788
|
+
if (!Number.isFinite(value)) throw new Error("invalid date property");
|
|
2789
|
+
return value;
|
|
2790
|
+
};
|
|
2791
|
+
const birthtimeMs = date("creationdate");
|
|
2792
|
+
const etag = properties.get("getetag");
|
|
2793
|
+
const validator = etag && scalar(etag);
|
|
2794
|
+
let mtimeMs = date("getlastmodified") ?? 0;
|
|
2795
|
+
let atimeMs = 0;
|
|
2796
|
+
if (timestampProperty) {
|
|
2797
|
+
const timestamps = JSON.parse(scalar(timestampProperty));
|
|
2798
|
+
if (typeof timestamps !== "object" || timestamps === null || !("version" in timestamps) || timestamps.version !== 1 || !("etag" in timestamps) || typeof timestamps.etag !== "string" || !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(timestamps.etag) || !("type" in timestamps) || timestamps.type !== "file" && timestamps.type !== "directory" || !("atimeMs" in timestamps) || typeof timestamps.atimeMs !== "number" || !Number.isFinite(timestamps.atimeMs) || Math.abs(timestamps.atimeMs) > 864e13 || !("mtimeMs" in timestamps) || typeof timestamps.mtimeMs !== "number" || !Number.isFinite(timestamps.mtimeMs) || Math.abs(timestamps.mtimeMs) > 864e13) throw new Error("invalid timestamp property");
|
|
2799
|
+
if (timestamps.etag === validator && timestamps.type === (directory ? "directory" : "file")) {
|
|
2800
|
+
mtimeMs = timestamps.mtimeMs;
|
|
2801
|
+
atimeMs = timestamps.atimeMs;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const stat = {
|
|
2805
|
+
type: directory ? "directory" : "file",
|
|
2806
|
+
size: Number(sizeText),
|
|
2807
|
+
mode: directory ? 16895 : 33206,
|
|
2808
|
+
mtimeMs,
|
|
2809
|
+
atimeMs,
|
|
2810
|
+
ctimeMs: 0,
|
|
2811
|
+
...birthtimeMs === void 0 ? {} : { birthtimeMs }
|
|
2812
|
+
};
|
|
2813
|
+
if (validator) this.etags.set(stat, validator);
|
|
2814
|
+
return stat;
|
|
2815
|
+
}
|
|
2816
|
+
async entries(path, depth, options, collection = depth === "1", limit) {
|
|
2817
|
+
return this.request("PROPFIND", path, options, {
|
|
2818
|
+
headers: { Depth: depth, "Content-Type": "application/xml; charset=utf-8" },
|
|
2819
|
+
body: propfindBody
|
|
2820
|
+
}, async (response, signal) => {
|
|
2821
|
+
if (response.status !== 207) this.httpError(response.status, "PROPFIND", path);
|
|
2822
|
+
const result = /* @__PURE__ */ new Map();
|
|
2823
|
+
let children = 0;
|
|
2824
|
+
for (const element of await this.multistatus(response, signal)) {
|
|
2825
|
+
signal.throwIfAborted();
|
|
2826
|
+
const href = davChild(element, "href");
|
|
2827
|
+
if (!href) throw new Error("missing DAV:href");
|
|
2828
|
+
const member = this.hrefPath(scalar(href));
|
|
2829
|
+
const prefix = path === "/" ? "/" : `${path}/`;
|
|
2830
|
+
if (member !== path && (depth === "0" || !member.startsWith(prefix) || member.slice(prefix.length).includes("/"))) {
|
|
2831
|
+
fail3("EACCES", "PROPFIND", path, "response member is outside requested depth");
|
|
2832
|
+
}
|
|
2833
|
+
if (result.has(member)) throw new Error("duplicate response href");
|
|
2834
|
+
if (member !== path) admitDirectoryEntries(++children, limit, path);
|
|
2835
|
+
const stat = this.entryStat(element, member);
|
|
2836
|
+
recordOwnedResourceStat(response, this, member, stat);
|
|
2837
|
+
result.set(member, stat);
|
|
2838
|
+
}
|
|
2839
|
+
if (!result.has(path)) throw new Error("response omitted requested resource");
|
|
2840
|
+
return result;
|
|
2841
|
+
}, collection);
|
|
2842
|
+
}
|
|
2843
|
+
async mutation(method, path, options, init = {}, collection = false) {
|
|
2844
|
+
await this.request(method, path, options, init, async (response, signal) => {
|
|
2845
|
+
if (response.status === 207) {
|
|
2846
|
+
for (const element of await this.multistatus(response, signal)) {
|
|
2847
|
+
const href = davChild(element, "href");
|
|
2848
|
+
const status = davChild(element, "status");
|
|
2849
|
+
if (!href || !status) throw new Error("invalid mutation multistatus");
|
|
2850
|
+
const member = this.hrefPath(scalar(href));
|
|
2851
|
+
const code = statusCode(status);
|
|
2852
|
+
if (code < 200 || code >= 300) {
|
|
2853
|
+
try {
|
|
2854
|
+
this.httpError(code, method, member);
|
|
2855
|
+
} catch (cause) {
|
|
2856
|
+
throw new FsError("EIO", { syscall: method, path, message: "WebDAV mutation partially failed", cause });
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
fail3("EIO", method, path, "unexpected mutation multistatus without a reported failure");
|
|
2861
|
+
}
|
|
2862
|
+
const accepted = method === "PUT" ? [200, 201, 204] : method === "MKCOL" ? [201] : method === "DELETE" ? [204] : [201, 204];
|
|
2863
|
+
if (!accepted.includes(response.status)) {
|
|
2864
|
+
const headers = new Headers(init.headers);
|
|
2865
|
+
this.httpError(response.status, method, path, headers.get("If-None-Match") === "*" || headers.get("Overwrite") === "F");
|
|
2866
|
+
}
|
|
2867
|
+
}, collection);
|
|
2868
|
+
}
|
|
2869
|
+
unsupported(operation, path) {
|
|
2870
|
+
return fail3("ENOTSUP", operation, path, `${operation} has no safe portable WebDAV equivalent`);
|
|
2871
|
+
}
|
|
2872
|
+
async maybeStat(path, options) {
|
|
2873
|
+
try {
|
|
2874
|
+
return await this.stat(path, options);
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
if (isFsError(error, "ENOENT")) return void 0;
|
|
2877
|
+
throw error;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
async withWalkDeadline(options, walk) {
|
|
2881
|
+
if (this.walkDeadlines.has(options)) return walk(options);
|
|
2882
|
+
const scopedOptions = { ...options };
|
|
2883
|
+
const owned = {};
|
|
2884
|
+
this.walkDeadlines.set(scopedOptions, owned);
|
|
2885
|
+
try {
|
|
2886
|
+
return await walk(scopedOptions);
|
|
2887
|
+
} finally {
|
|
2888
|
+
owned.deadline?.dispose();
|
|
2889
|
+
this.walkDeadlines.delete(scopedOptions);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async stat(path, options = {}) {
|
|
2893
|
+
validateDirectoryWalkPath(path, "stat");
|
|
2894
|
+
return this.statWithAncestors(path, options);
|
|
2895
|
+
}
|
|
2896
|
+
async statWithAncestors(path, options) {
|
|
2897
|
+
const normalized = normalize(path);
|
|
2898
|
+
const collection = requiresCollection(path);
|
|
2899
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
2900
|
+
try {
|
|
2901
|
+
const stat = (await this.entries(normalized, "0", scopedOptions, collection)).get(normalized);
|
|
2902
|
+
if (collection && stat.type !== "directory") fail3("ENOTDIR", "stat", path);
|
|
2903
|
+
return stat;
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
if (!isFsError(error, "ENOENT")) throw error;
|
|
2906
|
+
let parent = "";
|
|
2907
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
2908
|
+
parent += `/${segment}`;
|
|
2909
|
+
const ancestor = (await this.entries(parent, "0", scopedOptions)).get(parent);
|
|
2910
|
+
if (ancestor.type !== "directory") fail3("ENOTDIR", "stat", path);
|
|
2911
|
+
}
|
|
2912
|
+
throw error;
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
async lstat(path, options = {}) {
|
|
2917
|
+
return this.stat(path, options);
|
|
2918
|
+
}
|
|
2919
|
+
async readdir(path, options = {}) {
|
|
2920
|
+
let limit;
|
|
2921
|
+
if (options.maxEntries !== void 0) {
|
|
2922
|
+
if (options.signal?.aborted) fail3("ECANCELED", "readdir", path);
|
|
2923
|
+
limit = directoryEntryLimit(options, path);
|
|
2924
|
+
}
|
|
2925
|
+
const normalized = normalize(path);
|
|
2926
|
+
const entries2 = await this.entries(normalized, "1", options, true, limit);
|
|
2927
|
+
if (entries2.get(normalized).type !== "directory") fail3("ENOTDIR", "readdir", path);
|
|
2928
|
+
return [...entries2].filter(([member]) => member !== normalized).map(([member, stat]) => ({ name: member.slice(member.lastIndexOf("/") + 1), type: stat.type })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
2929
|
+
}
|
|
2930
|
+
async readFile(path, options = {}) {
|
|
2931
|
+
const normalized = normalize(path);
|
|
2932
|
+
const limit = Math.min(this.maxResponseBytes, positive(options.maxBytes ?? this.maxResponseBytes, "maxBytes", true));
|
|
2933
|
+
if ((await this.stat(path, options)).type === "directory") fail3("EISDIR", "readFile", path);
|
|
2934
|
+
return this.request("GET", normalized, options, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
|
|
2935
|
+
if (response.status !== 200) this.httpError(response.status, "GET", path);
|
|
2936
|
+
return this.bytes(response, limit, signal);
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
async *readStream(path, options = {}) {
|
|
2940
|
+
const normalized = normalize(path);
|
|
2941
|
+
const start = positive(options.start ?? 0, "start", true);
|
|
2942
|
+
const end = options.endExclusive === void 0 ? Infinity : positive(options.endExclusive, "endExclusive", true);
|
|
2943
|
+
const chunkSize2 = positive(options.chunkSize ?? 64 * 1024, "chunkSize");
|
|
2944
|
+
if (end < start) fail3("EINVAL", "readStream", path, "endExclusive precedes start");
|
|
2945
|
+
if ((await this.stat(path, options)).type === "directory") fail3("EISDIR", "readStream", path);
|
|
2946
|
+
yield* this.requestStream("GET", normalized, options, { headers: { "Accept-Encoding": "identity" } }, async function* (response, signal) {
|
|
2947
|
+
if (response.status !== 200) this.httpError(response.status, "GET", path);
|
|
2948
|
+
let position = 0;
|
|
2949
|
+
for await (const chunk of this.bodyChunks(response, this.maxResponseBytes, signal)) {
|
|
2950
|
+
const first = Math.max(0, start - position);
|
|
2951
|
+
const last = Math.min(chunk.byteLength, end - position);
|
|
2952
|
+
for (let offset = first; offset < last; offset += chunkSize2) {
|
|
2953
|
+
signal.throwIfAborted();
|
|
2954
|
+
yield chunk.slice(offset, Math.min(last, offset + chunkSize2));
|
|
2955
|
+
}
|
|
2956
|
+
position += chunk.byteLength;
|
|
2957
|
+
if (position >= end) return;
|
|
2958
|
+
}
|
|
2959
|
+
}.bind(this));
|
|
2960
|
+
}
|
|
2961
|
+
async prepareWrite(path, options) {
|
|
2962
|
+
validateDirectoryWalkPath(path, "writeFile");
|
|
2963
|
+
const normalized = normalize(path);
|
|
2964
|
+
if (options.mode !== void 0) this.unsupported("writeFile mode", path);
|
|
2965
|
+
if (options.flag !== void 0 && !["w", "wx", "a", "ax"].includes(options.flag)) fail3("EINVAL", "writeFile", path);
|
|
2966
|
+
if (options.signal?.aborted) fail3("ECANCELED", "writeFile", path);
|
|
2967
|
+
if (normalized === "/") fail3("EISDIR", "writeFile", path);
|
|
2968
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
2969
|
+
if (requiresCollection(path)) {
|
|
2970
|
+
await this.stat(path, scopedOptions);
|
|
2971
|
+
fail3("EISDIR", "writeFile", path);
|
|
2972
|
+
}
|
|
2973
|
+
let parent = "";
|
|
2974
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
2975
|
+
parent += `/${segment}`;
|
|
2976
|
+
await this.stat(`${parent}/`, scopedOptions);
|
|
2977
|
+
}
|
|
2978
|
+
const exclusive = options.flag === "wx" || options.flag === "ax";
|
|
2979
|
+
const existing = exclusive ? void 0 : await this.maybeStat(normalized, scopedOptions);
|
|
2980
|
+
if (existing?.type === "directory") fail3("EISDIR", "writeFile", path);
|
|
2981
|
+
let prefix = new Uint8Array();
|
|
2982
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
2983
|
+
if (exclusive || options.flag === "a" && !existing) headers["If-None-Match"] = "*";
|
|
2984
|
+
if (options.flag === "a" && existing) {
|
|
2985
|
+
const snapshot = await this.request("GET", normalized, scopedOptions, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
|
|
2986
|
+
if (response.status !== 200) this.httpError(response.status, "GET", path);
|
|
2987
|
+
const etag = strongEtag(response.headers.get("ETag"), path);
|
|
2988
|
+
if (response.headers.get("Content-Encoding") && response.headers.get("Content-Encoding").toLowerCase() !== "identity") {
|
|
2989
|
+
fail3("ENOTSUP", "appendFile", path, "conditional append requires an identity representation");
|
|
2990
|
+
}
|
|
2991
|
+
return { etag, data: await this.bytes(response, this.maxResponseBytes, signal) };
|
|
2992
|
+
});
|
|
2993
|
+
prefix = snapshot.data;
|
|
2994
|
+
headers["If-Match"] = snapshot.etag;
|
|
2995
|
+
}
|
|
2996
|
+
return { normalized, headers, prefix };
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
async writeFile(path, data, options = {}) {
|
|
3000
|
+
if (!(data instanceof Uint8Array)) fail3("EINVAL", "writeFile", path, "data must be Uint8Array");
|
|
3001
|
+
const { normalized, headers, prefix } = await this.prepareWrite(path, options);
|
|
3002
|
+
if (options.flag === "a" && prefix.byteLength + data.byteLength > this.maxResponseBytes) fail3("EFBIG", "appendFile", path);
|
|
3003
|
+
const body = new Uint8Array(prefix.byteLength + data.byteLength);
|
|
3004
|
+
body.set(prefix);
|
|
3005
|
+
body.set(data, prefix.byteLength);
|
|
3006
|
+
await this.mutation("PUT", normalized, options, {
|
|
3007
|
+
headers,
|
|
3008
|
+
body
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
async writeStream(path, source, options = {}) {
|
|
3012
|
+
if (options.signal?.aborted) fail3("ECANCELED", "writeStream", path);
|
|
3013
|
+
if (this.requestStreamSupport === false || this.requestStreamSupport === "native" && !nativeRequestStreamsSupported(this.base.href)) {
|
|
3014
|
+
fail3("ENOTSUP", "writeStream", path, "transport does not declare supported request streams");
|
|
3015
|
+
}
|
|
3016
|
+
const { normalized, headers, prefix } = await this.prepareWrite(path, options);
|
|
3017
|
+
const deadline = createRequestTimeout(this.timeoutMs);
|
|
3018
|
+
const timeout = deadline.signal;
|
|
3019
|
+
try {
|
|
3020
|
+
const controller = new AbortController();
|
|
3021
|
+
const signalScope = composeAbortSignals([timeout, controller.signal, ...options.signal ? [options.signal] : []]);
|
|
3022
|
+
const signal = signalScope.signal;
|
|
3023
|
+
const limit = this.maxResponseBytes;
|
|
3024
|
+
const chunks = (async function* () {
|
|
3025
|
+
let size = prefix.byteLength;
|
|
3026
|
+
if (prefix.byteLength) yield prefix;
|
|
3027
|
+
for await (const chunk of readBytes2(source, signal)) {
|
|
3028
|
+
size += chunk.byteLength;
|
|
3029
|
+
if (size > limit) fail3("EFBIG", "writeStream", path, "upload exceeds byte limit");
|
|
3030
|
+
yield new Uint8Array(chunk);
|
|
3031
|
+
}
|
|
3032
|
+
})();
|
|
3033
|
+
let finished = false;
|
|
3034
|
+
let uploadError;
|
|
3035
|
+
let body;
|
|
3036
|
+
try {
|
|
3037
|
+
body = new ReadableStream({
|
|
3038
|
+
async pull(destination) {
|
|
3039
|
+
try {
|
|
3040
|
+
const result = await chunks.next();
|
|
3041
|
+
if (result.done) {
|
|
3042
|
+
finished = true;
|
|
3043
|
+
destination.close();
|
|
3044
|
+
} else destination.enqueue(result.value);
|
|
3045
|
+
} catch (cause) {
|
|
3046
|
+
uploadError = cause;
|
|
3047
|
+
destination.error(cause);
|
|
3048
|
+
}
|
|
3049
|
+
},
|
|
3050
|
+
cancel(reason) {
|
|
3051
|
+
controller.abort(reason);
|
|
3052
|
+
void chunks.return(void 0).catch(() => {
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
}, { highWaterMark: 0 });
|
|
3056
|
+
try {
|
|
3057
|
+
const request = { headers, body, duplex: "half" };
|
|
3058
|
+
await this.mutation("PUT", normalized, options, request);
|
|
3059
|
+
if (uploadError !== void 0) throw uploadError;
|
|
3060
|
+
if (!finished) fail3("EIO", "writeStream", path, "server responded before consuming the upload");
|
|
3061
|
+
} catch (cause) {
|
|
3062
|
+
if (options.signal?.aborted) throw new FsError("ECANCELED", { syscall: "writeStream", path, cause });
|
|
3063
|
+
if (timeout.aborted) throw new FsError("ETIMEDOUT", { syscall: "writeStream", path, cause });
|
|
3064
|
+
if (isFsError(uploadError)) throw uploadError;
|
|
3065
|
+
if (isFsError(cause)) throw cause;
|
|
3066
|
+
throw new FsError("EIO", { syscall: "writeStream", path, cause });
|
|
3067
|
+
}
|
|
3068
|
+
} finally {
|
|
3069
|
+
controller.abort();
|
|
3070
|
+
signalScope.dispose();
|
|
3071
|
+
if (body && !body.locked) void body.cancel().catch(() => {
|
|
3072
|
+
});
|
|
3073
|
+
void chunks.return(void 0).catch(() => {
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
} finally {
|
|
3077
|
+
deadline.dispose();
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
async appendFile(path, data, options = {}) {
|
|
3081
|
+
await this.writeFile(path, data, { ...options, flag: "a" });
|
|
3082
|
+
}
|
|
3083
|
+
async mkdir(path, options = {}) {
|
|
3084
|
+
const normalized = normalize(path);
|
|
3085
|
+
if (options.mode !== void 0) this.unsupported("mkdir mode", path);
|
|
3086
|
+
if (!options.recursive) {
|
|
3087
|
+
if (normalized === "/") fail3("EEXIST", "mkdir", path);
|
|
3088
|
+
return this.mutation("MKCOL", normalized, options, {}, true);
|
|
3089
|
+
}
|
|
3090
|
+
let current = "";
|
|
3091
|
+
for (const segment of normalized.slice(1).split("/").filter(Boolean)) {
|
|
3092
|
+
current += `/${segment}`;
|
|
3093
|
+
const existing = await this.maybeStat(`${current}/`, options);
|
|
3094
|
+
if (existing) {
|
|
3095
|
+
if (existing.type !== "directory") fail3("ENOTDIR", "mkdir", current);
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
try {
|
|
3099
|
+
await this.mutation("MKCOL", current, options, {}, true);
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
if (!isFsError(error, "EEXIST")) throw error;
|
|
3102
|
+
if ((await this.stat(`${current}/`, options)).type !== "directory") fail3("ENOTDIR", "mkdir", current);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
if (normalized === "/") await this.stat(normalized, options);
|
|
3106
|
+
}
|
|
3107
|
+
async rmdir(path, options = {}) {
|
|
3108
|
+
try {
|
|
3109
|
+
const normalized = normalize(path);
|
|
3110
|
+
if (options.signal?.aborted) fail3("ECANCELED", "rmdir", path);
|
|
3111
|
+
if (normalized === "/") fail3("EBUSY", "rmdir", path, "cannot remove WebDAV root");
|
|
3112
|
+
const stat = await this.stat(path, options);
|
|
3113
|
+
if (options.signal?.aborted) fail3("ECANCELED", "rmdir", path);
|
|
3114
|
+
if (stat.type !== "directory") fail3("ENOTDIR", "rmdir", path);
|
|
3115
|
+
if (this.atomicEmptyDirectory) {
|
|
3116
|
+
await this.atomicRmdir(normalized, path, options);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
const entries2 = await this.readdir(path, options);
|
|
3120
|
+
if (options.signal?.aborted) fail3("ECANCELED", "rmdir", path);
|
|
3121
|
+
if (entries2.length) fail3("ENOTEMPTY", "rmdir", path);
|
|
3122
|
+
this.unsupported("rmdir", path);
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
if (isFsError(error) && (error.syscall !== "rmdir" || error.path !== path)) {
|
|
3125
|
+
throw new FsError(error.code, { syscall: "rmdir", path, cause: error });
|
|
3126
|
+
}
|
|
3127
|
+
throw error;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
async atomicRmdir(normalized, path, options) {
|
|
3131
|
+
const deadline = createRequestTimeout(this.timeoutMs);
|
|
3132
|
+
const timeout = deadline.signal;
|
|
3133
|
+
try {
|
|
3134
|
+
const signalScope = options.signal ? composeAbortSignals([timeout, options.signal]) : void 0;
|
|
3135
|
+
const signal = signalScope?.signal ?? timeout;
|
|
3136
|
+
try {
|
|
3137
|
+
signal.throwIfAborted();
|
|
3138
|
+
const receipt = await new Promise((resolve, reject) => {
|
|
3139
|
+
const abort = () => {
|
|
3140
|
+
signal.removeEventListener("abort", abort);
|
|
3141
|
+
reject(signal.reason);
|
|
3142
|
+
};
|
|
3143
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
3144
|
+
try {
|
|
3145
|
+
Promise.resolve(this.atomicEmptyDirectory.removeEmptyDirectory(Object.freeze({
|
|
3146
|
+
operation: "atomic-empty-rmdir/v1",
|
|
3147
|
+
namespaceUrl: this.base.href,
|
|
3148
|
+
path: normalized,
|
|
3149
|
+
signal
|
|
3150
|
+
}))).then((result) => {
|
|
3151
|
+
signal.removeEventListener("abort", abort);
|
|
3152
|
+
resolve(result);
|
|
3153
|
+
}, (error) => {
|
|
3154
|
+
signal.removeEventListener("abort", abort);
|
|
3155
|
+
reject(error);
|
|
3156
|
+
});
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
signal.removeEventListener("abort", abort);
|
|
3159
|
+
reject(error);
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
signal.throwIfAborted();
|
|
3163
|
+
if (!receipt || receipt.operation !== "atomic-empty-rmdir/v1" || receipt.namespaceUrl !== this.base.href || receipt.path !== normalized || receipt.outcome !== "removed") {
|
|
3164
|
+
fail3("EIO", "rmdir", path, "atomic empty-directory receipt mismatch; removal outcome is uncertain");
|
|
3165
|
+
}
|
|
3166
|
+
} catch (cause) {
|
|
3167
|
+
if (options.signal?.aborted) throw new FsError("ECANCELED", { syscall: "rmdir", path, cause });
|
|
3168
|
+
if (timeout.aborted) throw new FsError("ETIMEDOUT", { syscall: "rmdir", path, cause });
|
|
3169
|
+
if (isFsError(cause)) throw cause;
|
|
3170
|
+
const code = cause && typeof cause === "object" && "code" in cause && isErrnoCode(cause.code) ? cause.code : "EIO";
|
|
3171
|
+
throw new FsError(code, { syscall: "rmdir", path, cause });
|
|
3172
|
+
} finally {
|
|
3173
|
+
signalScope?.dispose();
|
|
3174
|
+
}
|
|
3175
|
+
} finally {
|
|
3176
|
+
deadline.dispose();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
async rm(path, options = {}) {
|
|
3180
|
+
const normalized = normalize(path);
|
|
3181
|
+
if (normalized === "/") fail3("EBUSY", "rm", path, "cannot remove WebDAV root");
|
|
3182
|
+
try {
|
|
3183
|
+
const stat = await this.stat(path, options);
|
|
3184
|
+
if (stat.type === "directory" && !options.recursive) this.unsupported("nonrecursive directory removal", path);
|
|
3185
|
+
await this.mutation("DELETE", normalized, options, {}, stat.type === "directory");
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
if (!(options.force && isFsError(error, "ENOENT"))) throw error;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
async guardTransferIdentity(source, destination, options) {
|
|
3191
|
+
if (!this.configuredComparison && this.compareEntry === originalWebDavComparison) return;
|
|
3192
|
+
const comparison = await compareEntries(this, source, this, destination, options);
|
|
3193
|
+
if (source === destination) {
|
|
3194
|
+
if (comparison === "distinct") fail3("EIO", "compareEntry", source, "comparison contradicts identical WebDAV paths");
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (comparison === "same") fail3("EINVAL", "compareEntry", destination, "source and destination refer to the same entry");
|
|
3198
|
+
if (comparison === "unknown") fail3("ENOTSUP", "compareEntry", destination, "configured authority cannot establish distinct entries");
|
|
3199
|
+
}
|
|
3200
|
+
async transfer(method, source, destination, options) {
|
|
3201
|
+
const from = normalize(source);
|
|
3202
|
+
const to = normalize(destination);
|
|
3203
|
+
if (from === "/" || to === "/") fail3("EBUSY", method, source, "cannot replace or transfer WebDAV root");
|
|
3204
|
+
const stat = await this.stat(source, options);
|
|
3205
|
+
if (method === "COPY" && stat.type === "directory") fail3("EISDIR", "copyFile", source);
|
|
3206
|
+
if (requiresCollection(destination) && stat.type !== "directory") fail3("ENOTDIR", method, destination);
|
|
3207
|
+
if (from === to) {
|
|
3208
|
+
await this.guardTransferIdentity(from, to, options);
|
|
3209
|
+
if (method === "MOVE") return;
|
|
3210
|
+
fail3(options.exclusive ? "EEXIST" : "EINVAL", "copyFile", source, "source and destination are identical");
|
|
3211
|
+
}
|
|
3212
|
+
if (stat.type === "directory" && to.startsWith(`${from}/`)) fail3("EINVAL", method, destination, "cannot move directory into itself");
|
|
3213
|
+
const existing = await this.maybeStat(destination, options);
|
|
3214
|
+
if (existing) await this.guardTransferIdentity(from, to, options);
|
|
3215
|
+
if (existing && options.exclusive) fail3("EEXIST", method, destination);
|
|
3216
|
+
const checkType = (target) => {
|
|
3217
|
+
if (target.type === "directory" && stat.type === "file") fail3("EISDIR", method, destination);
|
|
3218
|
+
if (target.type !== "directory" && stat.type === "directory") fail3("ENOTDIR", method, destination);
|
|
3219
|
+
};
|
|
3220
|
+
if (existing) checkType(existing);
|
|
3221
|
+
if (stat.type === "directory" && from.startsWith(`${to}/`)) fail3("EINVAL", method, destination, "cannot replace an ancestor of the source");
|
|
3222
|
+
const destinationUrl = this.url(to, stat.type === "directory");
|
|
3223
|
+
const headers = {
|
|
3224
|
+
Destination: destinationUrl,
|
|
3225
|
+
Overwrite: existing ? "T" : "F",
|
|
3226
|
+
Depth: method === "COPY" ? "0" : "infinity"
|
|
3227
|
+
};
|
|
3228
|
+
const sourceEtag = this.etags.get(stat);
|
|
3229
|
+
if (existing && sourceEtag && /^"[\x21\x23-\x7e\x80-\xff]*"$/.test(sourceEtag)) headers["If-Match"] = sourceEtag;
|
|
3230
|
+
const mutate = () => this.mutation(method, from, options, { headers }, stat.type === "directory");
|
|
3231
|
+
if (!existing) return mutate();
|
|
3232
|
+
if (this.overwritePolicy === "etag" && existing.type === "file") {
|
|
3233
|
+
headers.If = `<${destinationUrl}> ([${strongEtag(this.etags.get(existing), destination)}])`;
|
|
3234
|
+
return mutate();
|
|
3235
|
+
}
|
|
3236
|
+
await this.withDestinationLock(to, existing, options, async (token) => {
|
|
3237
|
+
const locked = await this.stat(destination, options);
|
|
3238
|
+
checkType(locked);
|
|
3239
|
+
if (locked.type === "directory" && (await this.readdir(destination, options)).length) fail3("ENOTEMPTY", method, destination);
|
|
3240
|
+
headers.If = `<${destinationUrl}> (${token})`;
|
|
3241
|
+
await mutate();
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
async withDestinationLock(path, stat, options, operation) {
|
|
3245
|
+
let token;
|
|
3246
|
+
const collection = stat.type === "directory";
|
|
3247
|
+
const validator = this.etags.get(stat);
|
|
3248
|
+
const unlock = async (value) => {
|
|
3249
|
+
await this.request("UNLOCK", path, {}, { headers: { "Lock-Token": value } }, async (response) => {
|
|
3250
|
+
if (response.status !== 204) this.httpError(response.status, "UNLOCK", path);
|
|
3251
|
+
}, collection).catch(() => {
|
|
3252
|
+
});
|
|
3253
|
+
};
|
|
3254
|
+
try {
|
|
3255
|
+
await this.request("LOCK", path, options, {
|
|
3256
|
+
headers: {
|
|
3257
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
3258
|
+
Depth: "infinity",
|
|
3259
|
+
Timeout: "Second-60",
|
|
3260
|
+
"If-Match": validator && /^"[\x21\x23-\x7e\x80-\xff]*"$/.test(validator) ? validator : "*"
|
|
3261
|
+
},
|
|
3262
|
+
body: lockBody
|
|
3263
|
+
}, async (response, signal) => {
|
|
3264
|
+
if (response.status === 207) return this.lockFailure(response, signal, path);
|
|
3265
|
+
if (response.status !== 200) this.httpError(response.status, "LOCK", path);
|
|
3266
|
+
const root = await this.xml(response, signal);
|
|
3267
|
+
const discovery = davChild(root, "lockdiscovery");
|
|
3268
|
+
const active = discovery && davChild(discovery, "activelock");
|
|
3269
|
+
const scope = active && davChild(active, "lockscope");
|
|
3270
|
+
const type = active && davChild(active, "locktype");
|
|
3271
|
+
const depth = active && davChild(active, "depth");
|
|
3272
|
+
const lockToken = active && davChild(active, "locktoken");
|
|
3273
|
+
const href = lockToken && davChild(lockToken, "href");
|
|
3274
|
+
const lockRoot = active && davChild(active, "lockroot");
|
|
3275
|
+
const rootHref = lockRoot && davChild(lockRoot, "href");
|
|
3276
|
+
const timeout = active && davChild(active, "timeout");
|
|
3277
|
+
if (root.namespace !== "DAV:" || root.localName !== "prop" || !scope || !davChild(scope, "exclusive") || davChild(scope, "shared") || !type || !davChild(type, "write") || !depth || scalar(depth) !== "infinity" || !href || `<${scalar(href)}>` !== token || lockRoot !== void 0 && (!rootHref || this.hrefPath(scalar(rootHref)) !== path) || !timeout || !/^Second-[1-9]\d*$/i.test(scalar(timeout)) || Number(scalar(timeout).slice(7)) > 4294967295) {
|
|
3278
|
+
fail3("ENOTSUP", "LOCK", path, "server did not grant a finite exclusive depth-infinity write lock");
|
|
3279
|
+
}
|
|
3280
|
+
}, collection, (response, late) => {
|
|
3281
|
+
if (response.status !== 200) return;
|
|
3282
|
+
const header = response.headers.get("Lock-Token");
|
|
3283
|
+
if (!header || !/^<[a-z][a-z0-9+.-]*:[^<>\s\x00-\x20\x7f]+>$/i.test(header)) throw new Error("invalid LOCK token");
|
|
3284
|
+
if (late) void unlock(header);
|
|
3285
|
+
else token = header;
|
|
3286
|
+
});
|
|
3287
|
+
await operation(token);
|
|
3288
|
+
} finally {
|
|
3289
|
+
if (token) await unlock(token);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
async rename(source, destination, options = {}) {
|
|
3293
|
+
if (options.noReplace) {
|
|
3294
|
+
options.signal?.throwIfAborted();
|
|
3295
|
+
fail3("ENOTSUP", "rename", source, "atomic no-replace rename is unsupported");
|
|
3296
|
+
}
|
|
3297
|
+
await this.transfer("MOVE", source, destination, options);
|
|
3298
|
+
}
|
|
3299
|
+
async copyFile(source, destination, options = {}) {
|
|
3300
|
+
await this.transfer("COPY", source, destination, options);
|
|
3301
|
+
}
|
|
3302
|
+
async realpath(path, options = {}) {
|
|
3303
|
+
const normalized = normalize(path);
|
|
3304
|
+
await this.stat(path, options);
|
|
3305
|
+
return normalized;
|
|
3306
|
+
}
|
|
3307
|
+
async access(path, mode2 = 0, options = {}) {
|
|
3308
|
+
if (!Number.isInteger(mode2) || mode2 < 0 || mode2 > 7) fail3("EINVAL", "access", path);
|
|
3309
|
+
if (options.signal?.aborted) fail3("ECANCELED", "access", path);
|
|
3310
|
+
if (mode2 & 2) this.unsupported("access write/execute permission checks", path);
|
|
3311
|
+
if (mode2 & 1) validateDirectoryWalkPath(path);
|
|
3312
|
+
const stat = await (mode2 & 1 ? this.stat(path, options) : this.statWithAncestors(path, options));
|
|
3313
|
+
if (options.signal?.aborted) fail3("ECANCELED", "access", path);
|
|
3314
|
+
if (mode2 & 1 && stat.type !== "directory") this.unsupported("access execute permission checks", path);
|
|
3315
|
+
if (mode2 & 4) {
|
|
3316
|
+
if (stat.type === "directory") await this.readdir(path, options);
|
|
3317
|
+
else await this.request("GET", normalize(path), options, { headers: { "Accept-Encoding": "identity" } }, async (response) => {
|
|
3318
|
+
if (response.status !== 200) this.httpError(response.status, "GET", path);
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
if (options.signal?.aborted) fail3("ECANCELED", "access", path);
|
|
3322
|
+
}
|
|
3323
|
+
async readlink(path, _options = {}) {
|
|
3324
|
+
return this.unsupported("readlink", path);
|
|
3325
|
+
}
|
|
3326
|
+
async symlink(_target, path, _options = {}) {
|
|
3327
|
+
this.unsupported("symlink", path);
|
|
3328
|
+
}
|
|
3329
|
+
async link(_source, path, _options = {}) {
|
|
3330
|
+
this.unsupported("link", path);
|
|
3331
|
+
}
|
|
3332
|
+
async chmod(path, _mode, _options = {}) {
|
|
3333
|
+
this.unsupported("chmod", path);
|
|
3334
|
+
}
|
|
3335
|
+
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
3336
|
+
const normalized = normalize(path);
|
|
3337
|
+
if (![atimeMs, mtimeMs].every((value) => Number.isFinite(value) && Math.abs(value) <= 864e13)) fail3("EINVAL", "utimes", path);
|
|
3338
|
+
const stat = await this.stat(path, options);
|
|
3339
|
+
const etag = strongEtag(this.etags.get(stat), path);
|
|
3340
|
+
const metadata = JSON.stringify({ version: 1, etag, type: stat.type, atimeMs, mtimeMs }).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
3341
|
+
await this.request("PROPPATCH", normalized, options, {
|
|
3342
|
+
headers: { "Content-Type": "application/xml; charset=utf-8", "If-Match": etag },
|
|
3343
|
+
body: `<d:propertyupdate xmlns:d="DAV:" xmlns:v="${timestampNamespace}"><d:set><d:prop><v:timestamps>${metadata}</v:timestamps></d:prop></d:set></d:propertyupdate>`
|
|
3344
|
+
}, async (response, signal) => {
|
|
3345
|
+
if (response.status !== 207) this.httpError(response.status, "PROPPATCH", path);
|
|
3346
|
+
const entries2 = await this.multistatus(response, signal);
|
|
3347
|
+
if (entries2.length !== 1) throw new Error("PROPPATCH must report the requested resource");
|
|
3348
|
+
const entry = entries2[0];
|
|
3349
|
+
const href = davChild(entry, "href");
|
|
3350
|
+
if (!href || this.hrefPath(scalar(href)) !== normalized) fail3("EACCES", "PROPPATCH", path, "unexpected response resource");
|
|
3351
|
+
const status = davChild(entry, "status");
|
|
3352
|
+
if (status) this.httpError(statusCode(status), "PROPPATCH", path);
|
|
3353
|
+
const propstats = davChildren(entry, "propstat");
|
|
3354
|
+
if (propstats.length !== 1) throw new Error("PROPPATCH must report the requested property");
|
|
3355
|
+
const prop = davChild(propstats[0], "prop");
|
|
3356
|
+
const result = davChild(propstats[0], "status");
|
|
3357
|
+
const property = prop?.children[0];
|
|
3358
|
+
if (!result || prop?.children.length !== 1 || property?.namespace !== timestampNamespace || property.localName !== "timestamps") throw new Error("invalid PROPPATCH property result");
|
|
3359
|
+
const code = statusCode(result);
|
|
3360
|
+
if (code !== 200) this.httpError(code, "PROPPATCH", path);
|
|
3361
|
+
}, stat.type === "directory");
|
|
3362
|
+
const updated = await this.stat(path, options);
|
|
3363
|
+
if (updated.atimeMs !== atimeMs || updated.mtimeMs !== mtimeMs) {
|
|
3364
|
+
fail3("EAGAIN", "utimes", path, "server did not retain requested timestamps for the current representation");
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
async truncate(path, _length, _options = {}) {
|
|
3368
|
+
this.unsupported("truncate", path);
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
var originalWebDavComparison = WebDavFileSystem.prototype.compareEntry;
|
|
3372
|
+
|
|
3373
|
+
// packages/safe-bash/src/integrations/safejs/filesystem.ts
|
|
3374
|
+
function makeSafeJsFsModule(makeFsModule, fs, options = {}) {
|
|
3375
|
+
return makeFsModule({ adapter: fs, ...options });
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// packages/safe-bash/src/integrations/safejs/shell.ts
|
|
3379
|
+
init_platform();
|
|
3380
|
+
function guestOptions(value) {
|
|
3381
|
+
if (value === void 0) return {};
|
|
3382
|
+
const options = record(value, "shell options");
|
|
3383
|
+
onlyKeys(options, ["cwd", "env", "stdin"]);
|
|
3384
|
+
const env = options.env === void 0 ? void 0 : record(options.env, "env");
|
|
3385
|
+
const entries2 = env === void 0 ? void 0 : Object.entries(env).map(([key, entry]) => {
|
|
3386
|
+
if (key.includes("\0") || key.includes("=")) throw new TypeError("Invalid environment key");
|
|
3387
|
+
const text = stringValue(entry, `env.${key}`);
|
|
3388
|
+
if (text.includes("\0")) throw new TypeError("Invalid environment value");
|
|
3389
|
+
return [key, text];
|
|
3390
|
+
});
|
|
3391
|
+
return {
|
|
3392
|
+
...options.cwd === void 0 ? {} : { cwd: stringValue(options.cwd, "cwd") },
|
|
3393
|
+
...options.stdin === void 0 ? {} : { stdin: stringValue(options.stdin, "stdin") },
|
|
3394
|
+
...entries2 === void 0 ? {} : { env: Object.fromEntries(entries2) }
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
function makeSafeJsShellModule(executor, options) {
|
|
3398
|
+
if (options.fs === void 0) throw new TypeError("An explicit filesystem is required");
|
|
3399
|
+
if (!(options.signal instanceof AbortSignal)) throw new TypeError("An explicit signal is required");
|
|
3400
|
+
if (options.replayPolicy !== "read-side-effect") {
|
|
3401
|
+
throw new TypeError("Shell operations require the read-side-effect replay policy");
|
|
3402
|
+
}
|
|
3403
|
+
if (typeof options.declareHostOperation !== "function") {
|
|
3404
|
+
throw new TypeError("SafeJS declareHostOperation must be injected");
|
|
3405
|
+
}
|
|
3406
|
+
const execute = typeof executor === "function" ? executor : executor.exec.bind(executor);
|
|
3407
|
+
if (typeof execute !== "function") throw new TypeError("A shell executor is required");
|
|
3408
|
+
async function exec(source, input) {
|
|
3409
|
+
const text = stringValue(source, "source");
|
|
3410
|
+
const request = guestOptions(input);
|
|
3411
|
+
return withSignal(options.signal, async () => {
|
|
3412
|
+
const result = await execute(text, { ...request, fs: options.fs, signal: options.signal });
|
|
3413
|
+
if (!Number.isInteger(result.exitCode) || result.exitCode < 0 || result.exitCode > 255) {
|
|
3414
|
+
throw new TypeError("Shell exitCode must be an integer between 0 and 255");
|
|
3415
|
+
}
|
|
3416
|
+
return {
|
|
3417
|
+
stdout: stringValue(result.stdout, "stdout"),
|
|
3418
|
+
stderr: stringValue(result.stderr, "stderr"),
|
|
3419
|
+
exitCode: result.exitCode
|
|
3420
|
+
};
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
return { exec: options.declareHostOperation(exec, options.replayPolicy) };
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// packages/safe-bash/src/commands/safejs/io.ts
|
|
3427
|
+
init_platform();
|
|
3428
|
+
|
|
3429
|
+
// packages/safe-bash/src/commands/safejs/types.ts
|
|
3430
|
+
init_platform();
|
|
3431
|
+
var SafeJsCommandLimitError = class extends Error {
|
|
3432
|
+
constructor(resource) {
|
|
3433
|
+
super(`SafeJS command limit exceeded: ${resource}`);
|
|
3434
|
+
this.resource = resource;
|
|
3435
|
+
this.name = "SafeJsCommandLimitError";
|
|
3436
|
+
}
|
|
3437
|
+
resource;
|
|
3438
|
+
code = "SAFEJS_LIMIT";
|
|
3439
|
+
};
|
|
3440
|
+
|
|
3441
|
+
// packages/safe-bash/src/commands/safejs/render.ts
|
|
3442
|
+
init_platform();
|
|
3443
|
+
var Text = class {
|
|
3444
|
+
constructor(limit, fail) {
|
|
3445
|
+
this.limit = limit;
|
|
3446
|
+
this.fail = fail;
|
|
3447
|
+
}
|
|
3448
|
+
limit;
|
|
3449
|
+
fail;
|
|
3450
|
+
parts = [];
|
|
3451
|
+
size = 0;
|
|
3452
|
+
active = /* @__PURE__ */ new Set();
|
|
3453
|
+
append(value) {
|
|
3454
|
+
if (import_buffer.Buffer.byteLength(value) > this.limit - this.size) {
|
|
3455
|
+
const error = new SafeJsCommandLimitError("maxOutputBytes");
|
|
3456
|
+
this.fail(error);
|
|
3457
|
+
throw error;
|
|
3458
|
+
}
|
|
3459
|
+
this.size += import_buffer.Buffer.byteLength(value);
|
|
3460
|
+
this.parts.push(value);
|
|
3461
|
+
}
|
|
3462
|
+
quoted(value) {
|
|
3463
|
+
if (import_buffer.Buffer.byteLength(value) + 2 > this.limit - this.size) {
|
|
3464
|
+
const error = new SafeJsCommandLimitError("maxOutputBytes");
|
|
3465
|
+
this.fail(error);
|
|
3466
|
+
throw error;
|
|
3467
|
+
}
|
|
3468
|
+
this.append(JSON.stringify(value));
|
|
3469
|
+
}
|
|
3470
|
+
json(value, depth = 0) {
|
|
3471
|
+
if (depth > 64) throw new TypeError("output JSON exceeds depth 64");
|
|
3472
|
+
if (value === null || value === void 0) {
|
|
3473
|
+
this.append("null");
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
if (typeof value === "string") {
|
|
3477
|
+
this.quoted(value);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
3481
|
+
this.append(JSON.stringify(value));
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
if (typeof value !== "object" || this.active.has(value)) throw new TypeError("output requires acyclic JSON data");
|
|
3485
|
+
this.active.add(value);
|
|
3486
|
+
try {
|
|
3487
|
+
if (Array.isArray(value)) {
|
|
3488
|
+
this.append("[");
|
|
3489
|
+
for (let index = 0; index < value.length; index++) {
|
|
3490
|
+
if (index) this.append(",");
|
|
3491
|
+
const property = Object.getOwnPropertyDescriptor(value, String(index));
|
|
3492
|
+
if (property && !("value" in property)) throw new TypeError("output arrays must contain data properties");
|
|
3493
|
+
this.json(property?.value, depth + 1);
|
|
3494
|
+
}
|
|
3495
|
+
this.append("]");
|
|
3496
|
+
} else {
|
|
3497
|
+
const entries2 = record(value, "output value");
|
|
3498
|
+
this.append("{");
|
|
3499
|
+
let first = true;
|
|
3500
|
+
for (const [key, item] of Object.entries(entries2)) {
|
|
3501
|
+
if (item === void 0) continue;
|
|
3502
|
+
if (!first) this.append(",");
|
|
3503
|
+
first = false;
|
|
3504
|
+
this.quoted(key);
|
|
3505
|
+
this.append(":");
|
|
3506
|
+
this.json(item, depth + 1);
|
|
3507
|
+
}
|
|
3508
|
+
this.append("}");
|
|
3509
|
+
}
|
|
3510
|
+
} finally {
|
|
3511
|
+
this.active.delete(value);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
finish() {
|
|
3515
|
+
return this.parts.join("");
|
|
3516
|
+
}
|
|
3517
|
+
};
|
|
3518
|
+
function renderOutput(values, limit, fail) {
|
|
3519
|
+
const text = new Text(limit, fail);
|
|
3520
|
+
for (let index = 0; index < values.length; index++) {
|
|
3521
|
+
if (index) text.append(" ");
|
|
3522
|
+
const value = values[index];
|
|
3523
|
+
if (typeof value === "string") text.append(value);
|
|
3524
|
+
else if (value === void 0) text.append("undefined");
|
|
3525
|
+
else text.json(value);
|
|
3526
|
+
}
|
|
3527
|
+
text.append("\n");
|
|
3528
|
+
return text.finish();
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
// packages/safe-bash/src/commands/safejs/io.ts
|
|
3532
|
+
var chunkSize = 64 * 1024;
|
|
3533
|
+
var GuestInput = class {
|
|
3534
|
+
constructor(source, limit, signal, fail, resource = "maxInputBytes") {
|
|
3535
|
+
this.limit = limit;
|
|
3536
|
+
this.signal = signal;
|
|
3537
|
+
this.fail = fail;
|
|
3538
|
+
this.resource = resource;
|
|
3539
|
+
this.iterator = readBytes(source, signal);
|
|
3540
|
+
}
|
|
3541
|
+
limit;
|
|
3542
|
+
signal;
|
|
3543
|
+
fail;
|
|
3544
|
+
resource;
|
|
3545
|
+
iterator;
|
|
3546
|
+
pending = new Uint8Array();
|
|
3547
|
+
consumed = 0;
|
|
3548
|
+
pulls = 0;
|
|
3549
|
+
queue = Promise.resolve();
|
|
3550
|
+
async take(size) {
|
|
3551
|
+
this.signal.throwIfAborted();
|
|
3552
|
+
while (!this.pending.length) {
|
|
3553
|
+
if (++this.pulls % 64 === 0) await yieldTurn(this.signal);
|
|
3554
|
+
const next = await this.iterator.next();
|
|
3555
|
+
if (next.done) return void 0;
|
|
3556
|
+
this.pending = next.value;
|
|
3557
|
+
}
|
|
3558
|
+
const length = Math.min(size, this.pending.length);
|
|
3559
|
+
if (length > this.limit - this.consumed) {
|
|
3560
|
+
const error = new SafeJsCommandLimitError(this.resource);
|
|
3561
|
+
this.fail(error);
|
|
3562
|
+
throw error;
|
|
3563
|
+
}
|
|
3564
|
+
const bytes = this.pending.slice(0, length);
|
|
3565
|
+
this.pending = this.pending.subarray(length);
|
|
3566
|
+
this.consumed += length;
|
|
3567
|
+
return bytes;
|
|
3568
|
+
}
|
|
3569
|
+
serialize(operation) {
|
|
3570
|
+
const result = this.queue.then(operation);
|
|
3571
|
+
this.queue = result.then(() => {
|
|
3572
|
+
}, () => {
|
|
3573
|
+
});
|
|
3574
|
+
return result;
|
|
3575
|
+
}
|
|
3576
|
+
readBytes(size = chunkSize) {
|
|
3577
|
+
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 1 || size > chunkSize) throw new TypeError("readBytes size must be an integer from 1 through 65536");
|
|
3578
|
+
return this.serialize(async () => {
|
|
3579
|
+
const bytes = await this.take(size);
|
|
3580
|
+
return bytes === void 0 ? null : Array.from(bytes);
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
readText() {
|
|
3584
|
+
return this.serialize(async () => {
|
|
3585
|
+
const pieces = [];
|
|
3586
|
+
for (; ; ) {
|
|
3587
|
+
const chunk = await this.take(chunkSize);
|
|
3588
|
+
if (!chunk) break;
|
|
3589
|
+
pieces.push(chunk);
|
|
3590
|
+
}
|
|
3591
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(import_buffer.Buffer.concat(pieces));
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
async close() {
|
|
3595
|
+
await this.iterator.return(void 0);
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
var GuestOutput = class {
|
|
3599
|
+
constructor(stdout, stderr, limit, signal, fail) {
|
|
3600
|
+
this.stdout = stdout;
|
|
3601
|
+
this.stderr = stderr;
|
|
3602
|
+
this.limit = limit;
|
|
3603
|
+
this.signal = signal;
|
|
3604
|
+
this.fail = fail;
|
|
3605
|
+
}
|
|
3606
|
+
stdout;
|
|
3607
|
+
stderr;
|
|
3608
|
+
limit;
|
|
3609
|
+
signal;
|
|
3610
|
+
fail;
|
|
3611
|
+
queue = Promise.resolve();
|
|
3612
|
+
produced = 0;
|
|
3613
|
+
stderrFailed = false;
|
|
3614
|
+
enqueue(bytes, target) {
|
|
3615
|
+
this.signal.throwIfAborted();
|
|
3616
|
+
if (bytes.length > this.limit - this.produced) {
|
|
3617
|
+
const error = new SafeJsCommandLimitError("maxOutputBytes");
|
|
3618
|
+
this.fail(error);
|
|
3619
|
+
throw error;
|
|
3620
|
+
}
|
|
3621
|
+
this.produced += bytes.length;
|
|
3622
|
+
const queued = this.queue.then(async () => {
|
|
3623
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) await writeBytes(target, bytes.subarray(offset, offset + chunkSize), this.signal);
|
|
3624
|
+
}).catch((error) => {
|
|
3625
|
+
if (target === this.stderr) this.stderrFailed = true;
|
|
3626
|
+
this.fail(error);
|
|
3627
|
+
throw error;
|
|
3628
|
+
});
|
|
3629
|
+
this.queue = queued;
|
|
3630
|
+
void queued.catch(() => {
|
|
3631
|
+
});
|
|
3632
|
+
return queued;
|
|
3633
|
+
}
|
|
3634
|
+
text(value, stderr = false) {
|
|
3635
|
+
if (typeof value !== "string") throw new TypeError("stdio text output requires a string");
|
|
3636
|
+
if (import_buffer.Buffer.byteLength(value) > this.limit - this.produced) {
|
|
3637
|
+
const error = new SafeJsCommandLimitError("maxOutputBytes");
|
|
3638
|
+
this.fail(error);
|
|
3639
|
+
throw error;
|
|
3640
|
+
}
|
|
3641
|
+
return this.enqueue(import_buffer.Buffer.from(value), stderr ? this.stderr : this.stdout);
|
|
3642
|
+
}
|
|
3643
|
+
bytes(value, stderr = false) {
|
|
3644
|
+
if (!Array.isArray(value) || value.length > chunkSize) throw new TypeError("stdio byte output requires an array of at most 65536 bytes");
|
|
3645
|
+
for (const byte of value) if (!Number.isInteger(byte) || byte < 0 || byte > 255) throw new TypeError("stdio byte values must be integers from 0 through 255");
|
|
3646
|
+
return this.enqueue(Uint8Array.from(value), stderr ? this.stderr : this.stdout);
|
|
3647
|
+
}
|
|
3648
|
+
console(args, stderr) {
|
|
3649
|
+
void this.text(renderOutput(args, this.limit - this.produced, this.fail), stderr);
|
|
3650
|
+
}
|
|
3651
|
+
result(value) {
|
|
3652
|
+
return this.text(renderOutput([value], this.limit - this.produced, this.fail));
|
|
3653
|
+
}
|
|
3654
|
+
async drain() {
|
|
3655
|
+
await this.queue;
|
|
3656
|
+
}
|
|
3657
|
+
};
|
|
3658
|
+
|
|
3659
|
+
// packages/safe-bash/src/commands/safejs/options.ts
|
|
3660
|
+
init_platform();
|
|
3661
|
+
var defaultSafeJsLimits = Object.freeze({
|
|
3662
|
+
maxSourceBytes: 1024 * 1024,
|
|
3663
|
+
maxInputBytes: 8 * 1024 * 1024,
|
|
3664
|
+
maxOutputBytes: 8 * 1024 * 1024,
|
|
3665
|
+
timeoutMs: 5e3,
|
|
3666
|
+
maxSteps: 1e5,
|
|
3667
|
+
maxCallDepth: 128,
|
|
3668
|
+
stringLength: 1024 * 1024,
|
|
3669
|
+
arrayLength: 1e5,
|
|
3670
|
+
dataSize: 16 * 1024 * 1024
|
|
3671
|
+
});
|
|
3672
|
+
function commandLimits(options = {}) {
|
|
3673
|
+
const result = { ...defaultSafeJsLimits, ...options };
|
|
3674
|
+
for (const [name, value] of Object.entries(result)) {
|
|
3675
|
+
if (!Object.hasOwn(defaultSafeJsLimits, name)) throw new TypeError(`Unknown SafeJS limit: ${name}`);
|
|
3676
|
+
if (!Number.isSafeInteger(value) || value < (name === "timeoutMs" ? 1 : 0)) throw new RangeError(`Invalid SafeJS limit: ${name}`);
|
|
3677
|
+
}
|
|
3678
|
+
if (result.timeoutMs > 2147483647) throw new RangeError("SafeJS timeoutMs exceeds timer range");
|
|
3679
|
+
return Object.freeze(result);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// packages/safe-bash/src/commands/safejs/runtime.ts
|
|
3683
|
+
var GuestDiagnostic = class extends PublicDiagnostic {
|
|
3684
|
+
constructor(info) {
|
|
3685
|
+
super(info.message);
|
|
3686
|
+
this.info = info;
|
|
3687
|
+
}
|
|
3688
|
+
info;
|
|
3689
|
+
};
|
|
3690
|
+
function errorInfo(error) {
|
|
3691
|
+
if (typeof error === "string") return { name: "Error", code: "", message: error };
|
|
3692
|
+
if (typeof error !== "object" || error === null) return { name: "Error", code: "", message: "SafeJS execution failed" };
|
|
3693
|
+
const field = (name) => {
|
|
3694
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, name);
|
|
3695
|
+
return descriptor && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : "";
|
|
3696
|
+
};
|
|
3697
|
+
return { name: field("name"), code: field("code"), message: field("message") || "SafeJS execution failed" };
|
|
3698
|
+
}
|
|
3699
|
+
function statusField(error, name) {
|
|
3700
|
+
if (typeof error !== "object" || error === null) return "";
|
|
3701
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, name);
|
|
3702
|
+
return descriptor && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : "";
|
|
3703
|
+
}
|
|
3704
|
+
function validateRuntime(runtime) {
|
|
3705
|
+
if (runtime === void 0) return;
|
|
3706
|
+
for (const key of ["run", "createBudget", "makeFsModule", "declareHostOperation"]) {
|
|
3707
|
+
if (typeof runtime[key] !== "function") throw new TypeError(`SafeJS runtime.${key} must be injected`);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
function createSafeJsCommands(options, dialect) {
|
|
3711
|
+
const limits = commandLimits(options.limits);
|
|
3712
|
+
const runtime = options.runtime;
|
|
3713
|
+
validateRuntime(runtime);
|
|
3714
|
+
return [{ name: dialect.name, description: dialect.description, async execute(context) {
|
|
3715
|
+
const deadline = Date.now() + limits.timeoutMs;
|
|
3716
|
+
const diagnose = async (message) => {
|
|
3717
|
+
const diagnostic = new AbortController();
|
|
3718
|
+
const timer = setTimeout(() => diagnostic.abort(), Math.max(1, Math.min(limits.timeoutMs, deadline - Date.now())));
|
|
3719
|
+
try {
|
|
3720
|
+
await writeDiagnostic(context.stderr, `${dialect.name}: ${message.slice(0, 4096)}
|
|
3721
|
+
`, AbortSignal.any([context.signal, diagnostic.signal]));
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
context.signal.throwIfAborted();
|
|
3724
|
+
if (!diagnostic.signal.aborted) throw error;
|
|
3725
|
+
} finally {
|
|
3726
|
+
clearTimeout(timer);
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
let parsed;
|
|
3730
|
+
try {
|
|
3731
|
+
parsed = dialect.invocation(context.args);
|
|
3732
|
+
} catch (error) {
|
|
3733
|
+
await diagnose(publicDiagnosticMessage(error, context.onInternalError));
|
|
3734
|
+
return { exitCode: 2 };
|
|
3735
|
+
}
|
|
3736
|
+
context.signal.throwIfAborted();
|
|
3737
|
+
if (parsed.help) {
|
|
3738
|
+
await writeBytes(context.stdout, import_buffer.Buffer.from(dialect.help), context.signal);
|
|
3739
|
+
return { exitCode: 0 };
|
|
3740
|
+
}
|
|
3741
|
+
if (!runtime) {
|
|
3742
|
+
await diagnose("runtime not installed; inject run, createBudget, makeFsModule and declareHostOperation");
|
|
3743
|
+
return { exitCode: 127 };
|
|
3744
|
+
}
|
|
3745
|
+
const controller = new AbortController();
|
|
3746
|
+
const signal = AbortSignal.any([context.signal, controller.signal]);
|
|
3747
|
+
let failure;
|
|
3748
|
+
let hasFailure = false;
|
|
3749
|
+
const fail = (error) => {
|
|
3750
|
+
if (!hasFailure && !controller.signal.aborted) {
|
|
3751
|
+
hasFailure = true;
|
|
3752
|
+
failure = error;
|
|
3753
|
+
queueMicrotask(() => controller.abort(error));
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
const timeout = setTimeout(() => fail(new SafeJsCommandLimitError("timeoutMs")), limits.timeoutMs);
|
|
3757
|
+
let input;
|
|
3758
|
+
const output = new GuestOutput(context.stdout, context.stderr, limits.maxOutputBytes, signal, fail);
|
|
3759
|
+
let exitCode = 0;
|
|
3760
|
+
let thrown;
|
|
3761
|
+
let failed = false;
|
|
3762
|
+
try {
|
|
3763
|
+
const fromStdin = parsed.source === void 0 && parsed.file === "-";
|
|
3764
|
+
let source = parsed.source;
|
|
3765
|
+
let filename = parsed.file;
|
|
3766
|
+
if (source === void 0) {
|
|
3767
|
+
if (!fromStdin) filename = pathOf(context, parsed.file);
|
|
3768
|
+
const capabilities = fromStdin ? void 0 : await withSignal(signal, async () => await context.fs.capabilitiesFor?.(filename, { signal }) ?? context.fs.capabilities);
|
|
3769
|
+
const bytes = fromStdin ? context.stdin : context.fs.readStream && capabilities?.streamingRead !== false ? context.fs.readStream(filename, { signal, chunkSize: 65536 }) : toByteSource(await withSignal(signal, () => context.fs.readFile(filename, { signal, maxBytes: limits.maxSourceBytes })));
|
|
3770
|
+
const reader = new GuestInput(bytes, limits.maxSourceBytes, signal, fail, "maxSourceBytes");
|
|
3771
|
+
try {
|
|
3772
|
+
source = await reader.readText();
|
|
3773
|
+
} finally {
|
|
3774
|
+
await reader.close();
|
|
3775
|
+
}
|
|
3776
|
+
} else if (import_buffer.Buffer.byteLength(source) > limits.maxSourceBytes) throw new SafeJsCommandLimitError("maxSourceBytes");
|
|
3777
|
+
if (source.startsWith("\uFEFF")) source = source.slice(1);
|
|
3778
|
+
input = new GuestInput(fromStdin ? toByteSource("") : context.stdin, limits.maxInputBytes, signal, fail);
|
|
3779
|
+
const guestInput = input;
|
|
3780
|
+
const declare = runtime.declareHostOperation;
|
|
3781
|
+
const stdio = {
|
|
3782
|
+
readBytes: declare(async (size) => guestInput.readBytes(size), "read-side-effect"),
|
|
3783
|
+
readText: declare(async () => guestInput.readText(), "read-side-effect"),
|
|
3784
|
+
write: declare(async (text) => output.text(text), "read-side-effect"),
|
|
3785
|
+
writeBytes: declare(async (bytes) => output.bytes(bytes), "read-side-effect"),
|
|
3786
|
+
error: declare(async (text) => output.text(text, true), "read-side-effect"),
|
|
3787
|
+
errorBytes: declare(async (bytes) => output.bytes(bytes, true), "read-side-effect")
|
|
3788
|
+
};
|
|
3789
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
3790
|
+
for (const [key, value] of Object.entries(context.env)) env[key] = value;
|
|
3791
|
+
const command = {
|
|
3792
|
+
args: [...parsed.args],
|
|
3793
|
+
cwd: context.cwd,
|
|
3794
|
+
env,
|
|
3795
|
+
setExitCode: declare((value) => {
|
|
3796
|
+
signal.throwIfAborted();
|
|
3797
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 255) throw new TypeError("exit code must be an integer from 0 through 255");
|
|
3798
|
+
exitCode = value;
|
|
3799
|
+
}, "read-side-effect")
|
|
3800
|
+
};
|
|
3801
|
+
const budget = runtime.createBudget({
|
|
3802
|
+
maxSteps: limits.maxSteps,
|
|
3803
|
+
deadline,
|
|
3804
|
+
maxCallDepth: limits.maxCallDepth,
|
|
3805
|
+
stringLength: limits.stringLength,
|
|
3806
|
+
arrayLength: limits.arrayLength,
|
|
3807
|
+
dataSize: limits.dataSize
|
|
3808
|
+
});
|
|
3809
|
+
const modules = { fs: makeSafeJsFsModule(runtime.makeFsModule, context.fs, { cwd: context.cwd, signal }), stdio, command };
|
|
3810
|
+
const prepared = dialect.prepare?.(source, { ...parsed, file: filename }, modules);
|
|
3811
|
+
const result = record(await withSignal(signal, () => runtime.run(prepared?.source ?? source, {
|
|
3812
|
+
budget,
|
|
3813
|
+
filename,
|
|
3814
|
+
modules,
|
|
3815
|
+
signal,
|
|
3816
|
+
...prepared ? { bindings: prepared.bindings } : {},
|
|
3817
|
+
sink: { log: (...args) => output.console(args, false), error: (...args) => output.console(args, true) }
|
|
3818
|
+
})), "SafeJS run result");
|
|
3819
|
+
signal.throwIfAborted();
|
|
3820
|
+
if (result.ok !== true && result.ok !== false) throw new TypeError("Invalid SafeJS run result.ok");
|
|
3821
|
+
if (!result.ok) {
|
|
3822
|
+
const info = errorInfo(result.error);
|
|
3823
|
+
throw new GuestDiagnostic(info);
|
|
3824
|
+
}
|
|
3825
|
+
if (parsed.print && result.returnValue !== void 0) await output.result(result.returnValue);
|
|
3826
|
+
await output.drain();
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
thrown = hasFailure ? failure : error;
|
|
3829
|
+
failed = true;
|
|
3830
|
+
} finally {
|
|
3831
|
+
try {
|
|
3832
|
+
await output.drain();
|
|
3833
|
+
} catch (error) {
|
|
3834
|
+
thrown = hasFailure ? failure : error;
|
|
3835
|
+
failed = true;
|
|
3836
|
+
}
|
|
3837
|
+
controller.abort();
|
|
3838
|
+
clearTimeout(timeout);
|
|
3839
|
+
await input?.close().catch(() => {
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
context.signal.throwIfAborted();
|
|
3843
|
+
if (failed) {
|
|
3844
|
+
const info = thrown instanceof GuestDiagnostic ? thrown.info : { name: statusField(thrown, "name"), code: statusField(thrown, "code") };
|
|
3845
|
+
const detail = thrown instanceof SafeJsCommandLimitError ? thrown.message : publicDiagnosticMessage(thrown, context.onInternalError);
|
|
3846
|
+
if (!output.stderrFailed) await diagnose(detail);
|
|
3847
|
+
return { exitCode: thrown instanceof SafeJsCommandLimitError || info.code === "budgetExceeded" ? 124 : thrown instanceof UsageError || info.name === "ParseError" ? 2 : 1 };
|
|
3848
|
+
}
|
|
3849
|
+
return { exitCode };
|
|
3850
|
+
} }];
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// packages/safe-bash/src/commands/node/safejs.ts
|
|
3854
|
+
function createSafeJsNodeCommands(options) {
|
|
3855
|
+
const settings = record(options, "node options");
|
|
3856
|
+
onlyKeys(settings, ["runtime", "limits", "replace"]);
|
|
3857
|
+
if (Object.hasOwn(settings, "replace") && typeof settings.replace !== "boolean") throw new TypeError("node replace must be boolean");
|
|
3858
|
+
delete settings.replace;
|
|
3859
|
+
return Object.freeze([createSafeJsNodeCommand(settings)]);
|
|
3860
|
+
}
|
|
3861
|
+
function safeJsNodeCommands(options) {
|
|
3862
|
+
const definitions = createSafeJsNodeCommands(options);
|
|
3863
|
+
const replace = options.replace ?? false;
|
|
3864
|
+
return { name: "node-commands", setup(host) {
|
|
3865
|
+
if (!replace && host.commands.has("node")) throw new Error("Command already registered: node");
|
|
3866
|
+
for (const definition of definitions) host.commands.register(definition, { replace });
|
|
3867
|
+
} };
|
|
3868
|
+
}
|
|
3869
|
+
function invocation(args) {
|
|
3870
|
+
let source;
|
|
3871
|
+
let print = false;
|
|
3872
|
+
let index = 0;
|
|
3873
|
+
for (; index < args.length; index++) {
|
|
3874
|
+
const argument = args[index];
|
|
3875
|
+
if (argument === "--") {
|
|
3876
|
+
index++;
|
|
3877
|
+
break;
|
|
3878
|
+
}
|
|
3879
|
+
if (argument === "-h" || argument === "--help") return { file: "-", args: [], print: false, help: true };
|
|
3880
|
+
if (argument === "--input-type" || argument.startsWith("--input-type=")) {
|
|
3881
|
+
const value = argument === "--input-type" ? args[++index] : argument.slice(13);
|
|
3882
|
+
if (value !== "module") throw new UsageError("SafeJS node supports only --input-type=module");
|
|
3883
|
+
continue;
|
|
3884
|
+
}
|
|
3885
|
+
const mode = argument === "--eval" || argument.startsWith("--eval=") || argument.startsWith("-e") ? "eval" : argument === "--print" || argument.startsWith("--print=") || argument.startsWith("-p") ? "print" : void 0;
|
|
3886
|
+
if (mode) {
|
|
3887
|
+
if (source !== void 0) throw new UsageError("conflicting source selectors");
|
|
3888
|
+
print = mode === "print";
|
|
3889
|
+
const equal = argument.indexOf("=");
|
|
3890
|
+
source = argument.startsWith("--") ? equal >= 0 ? argument.slice(equal + 1) : args[++index] : argument.length > 2 ? argument.slice(2) : args[++index];
|
|
3891
|
+
if (source === void 0) throw new UsageError(`${argument} requires JavaScript source`);
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
if (argument !== "-" && argument.startsWith("-")) throw new UsageError(`unsupported node option '${argument}'`);
|
|
3895
|
+
break;
|
|
3896
|
+
}
|
|
3897
|
+
if (source !== void 0) return { source, file: print ? "<node -p>" : "<node -e>", args: args.slice(index), print, help: false };
|
|
3898
|
+
return { file: args[index] ?? "-", args: args.slice(index + 1), print: false, help: false };
|
|
3899
|
+
}
|
|
3900
|
+
function createSafeJsNodeCommand(options) {
|
|
3901
|
+
const settings = record(options, "node options");
|
|
3902
|
+
onlyKeys(settings, ["runtime", "limits"]);
|
|
3903
|
+
if (settings.runtime === void 0 || settings.runtime === null) throw new TypeError("node requires an injected SafeJS runtime");
|
|
3904
|
+
options = settings;
|
|
3905
|
+
const definitions = createSafeJsCommands(options, {
|
|
3906
|
+
name: "node",
|
|
3907
|
+
description: "Execute JavaScript with an injected SafeJS runtime and virtual I/O",
|
|
3908
|
+
help: 'Usage: node [-e SOURCE | -p EXPRESSION | FILE | -] [ARG...]\nExecutes with the injected SafeJS interpreter; no native Node.js process.\nSupports --eval, --print, --input-type=module and -- before operands.\nNo source operand reads stdin. Files and inline source leave stdin for guest data.\nUse async imports from fs or require("node:fs/promises").\nNative modules, synchronous fs and local module loading are not supported.\n',
|
|
3909
|
+
invocation,
|
|
3910
|
+
prepare(source, selected, modules) {
|
|
3911
|
+
const command = modules.command;
|
|
3912
|
+
const stdio = modules.stdio;
|
|
3913
|
+
const fs = modules.fs;
|
|
3914
|
+
const processModule = {
|
|
3915
|
+
argv: ["/virtual/bin/node", ...selected.source === void 0 ? [selected.file] : [], ...selected.args],
|
|
3916
|
+
env: command.env,
|
|
3917
|
+
cwd: options.runtime.declareHostOperation(() => command.cwd, "read-side-effect"),
|
|
3918
|
+
exitCode: 0,
|
|
3919
|
+
stdin: { readText: stdio.readText, readBytes: stdio.readBytes },
|
|
3920
|
+
stdout: { write: stdio.write },
|
|
3921
|
+
stderr: { write: stdio.error }
|
|
3922
|
+
};
|
|
3923
|
+
modules.fs = { ...fs, default: fs };
|
|
3924
|
+
const requiredModules = /* @__PURE__ */ new Map([["fs/promises", fs], ["node:fs/promises", fs]]);
|
|
3925
|
+
return {
|
|
3926
|
+
source: (selected.print ? `console.log((
|
|
3927
|
+
${source}
|
|
3928
|
+
));` : source) + "\n;__safeBashSetExitCode(process.exitCode);",
|
|
3929
|
+
bindings: {
|
|
3930
|
+
process: processModule,
|
|
3931
|
+
__safeBashSetExitCode: command.setExitCode,
|
|
3932
|
+
require: options.runtime.declareHostOperation((name) => {
|
|
3933
|
+
const module = typeof name === "string" ? requiredModules.get(name) : void 0;
|
|
3934
|
+
if (!module) throw new TypeError("Unsupported node module; use fs/promises or node:fs/promises");
|
|
3935
|
+
return module;
|
|
3936
|
+
}, "read-side-effect")
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
return Object.freeze(definitions[0]);
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
export {
|
|
3945
|
+
makeSafeJsShellModule,
|
|
3946
|
+
SafeJsCommandLimitError,
|
|
3947
|
+
defaultSafeJsLimits,
|
|
3948
|
+
createSafeJsNodeCommands,
|
|
3949
|
+
safeJsNodeCommands,
|
|
3950
|
+
createSafeJsNodeCommand
|
|
3951
|
+
};
|
|
3952
|
+
//# sourceMappingURL=chunk-3BZ5NQ73.js.map
|