@poe-platform/safe-fs 0.1.800 → 0.1.802
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 +4 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +2 -0
- package/dist/safe-fs/contracts/path.d.ts +1 -0
- package/dist/safe-fs/contracts/virtual-path.d.ts +8 -3
- package/dist/safe-fs/contracts/virtual-path.js +22 -5
- package/dist/safe-fs/fs/devices/path.js +18 -17
- package/dist/safe-fs/fs/memory/index.js +62 -41
- package/dist/safe-fs/fs/memory/limits.d.ts +5 -1
- package/dist/safe-fs/fs/memory/limits.js +7 -11
- package/dist/safe-fs/fs/memory/missing-target.d.ts +2 -1
- package/dist/safe-fs/fs/memory/missing-target.js +8 -1
- package/dist/safe-fs/fs/s3/namespace.js +1 -1
- package/dist/safe-fs/fs/scoped.js +2 -2
- package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
- package/dist/safe-fs/fs/webdav/webdav.js +5 -4
- package/dist/xml-ast/index.js +35 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ Capture `retainFileSystemCleanup(scoped, callback, { maxOperations })` before
|
|
|
123
123
|
cancellation to clean up through a bounded view after ordinary scoped operations
|
|
124
124
|
close. Cleanup preserves its owning scope and limits across separately bundled
|
|
125
125
|
command runtimes, including restrictions added by filesystem wrappers.
|
|
126
|
+
Omit `maxOperations` or pass `Infinity` for unlimited cleanup operations.
|
|
126
127
|
|
|
127
128
|
| Backend or wrapper | Use it for |
|
|
128
129
|
| --- | --- |
|
|
@@ -252,7 +253,7 @@ There are no package environment variables, implicit credentials, or automatic `
|
|
|
252
253
|
|
|
253
254
|
| API | Options and defaults |
|
|
254
255
|
| --- | --- |
|
|
255
|
-
| `createFileSystem(config, { registry })` | Required `config.type`; `config.options` defaults to an empty record. `registry` is required. Built-in `memory` accepts optional file, retained-byte, metadata
|
|
256
|
+
| `createFileSystem(config, { registry })` | Required `config.type`; `config.options` defaults to an empty record. `registry` is required. Built-in `memory` accepts optional file, retained-byte, metadata, total-byte, `maxPathBytes` and `maxPathComponents` quotas (omitted, `undefined`, or `Infinity` disables each quota); built-in `real` requires `root`. |
|
|
256
257
|
| `createNodeFileSystemAdapterRegistry(extensions?)` | Optional map of additional adapter descriptors; defaults to only `memory` and `real`. |
|
|
257
258
|
| Memory / read-only | Memory accepts independent optional `maxFileBytes`, `maxRetainedBytes`, `maxMetadataUnits` and `maxBytes` quotas, all unlimited by default. Read-only takes the backing filesystem, without an options object. |
|
|
258
259
|
| Real | Required `root`: existing absolute host directory; the constructor/factory also accepts the root string directly. |
|
|
@@ -277,6 +278,8 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
277
278
|
| `copyFile` | `exclusive` (default false) |
|
|
278
279
|
| `readStream` | `start` (default 0), `endExclusive` (default end of file), `chunkSize` (built-in default 64 KiB) |
|
|
279
280
|
|
|
281
|
+
`validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
|
|
282
|
+
|
|
280
283
|
`access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
|
|
281
284
|
|
|
282
285
|
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
|
|
@@ -105,6 +105,8 @@ export interface FileSystemCapabilities {
|
|
|
105
105
|
readonly [capability: string]: boolean | undefined;
|
|
106
106
|
}
|
|
107
107
|
export interface FsOptions {
|
|
108
|
+
/** Optional virtual device path traversal quotas; disabled by default. */
|
|
109
|
+
readonly pathLimits?: import("./virtual-path.js").PathLimits;
|
|
108
110
|
readonly signal?: AbortSignal;
|
|
109
111
|
}
|
|
110
112
|
export interface ChmodOptions extends FsOptions {
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./virtual-path.js";
|
|
2
2
|
export { posixPath, basename, dirname, extname, joinPath, isAbsolutePath } from "./portable-path.js";
|
|
3
|
+
export type { PathLimits } from "./virtual-path.js";
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
export declare const MAX_PATH_BYTES
|
|
2
|
-
export declare const MAX_PATH_COMPONENTS
|
|
3
|
-
export
|
|
1
|
+
export declare const MAX_PATH_BYTES: number;
|
|
2
|
+
export declare const MAX_PATH_COMPONENTS: number;
|
|
3
|
+
export interface PathLimits {
|
|
4
|
+
readonly maxPathBytes?: number | undefined;
|
|
5
|
+
readonly maxPathComponents?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function pathByteLength(path: string): number;
|
|
8
|
+
export declare function validatePath(path: string, limits?: PathLimits | number): void;
|
|
4
9
|
export declare function resolvePath2(cwd: string, path0: string): string;
|
|
5
10
|
export declare function resolvePath(cwd: string, ...paths: string[]): string;
|
|
6
11
|
export declare function normalizePath(path: string, cwd?: string): string;
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { FsError } from "./errors.js";
|
|
2
|
-
export const MAX_PATH_BYTES =
|
|
3
|
-
export const MAX_PATH_COMPONENTS =
|
|
4
|
-
export function
|
|
2
|
+
export const MAX_PATH_BYTES = Infinity;
|
|
3
|
+
export const MAX_PATH_COMPONENTS = Infinity;
|
|
4
|
+
export function pathByteLength(path) {
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
for (const point of path) {
|
|
7
|
+
const code = point.codePointAt(0);
|
|
8
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
9
|
+
}
|
|
10
|
+
return bytes;
|
|
11
|
+
}
|
|
12
|
+
export function validatePath(path, limits = {}) {
|
|
13
|
+
const maxBytes = typeof limits === "number" ? Infinity : limits.maxPathBytes ?? Infinity;
|
|
14
|
+
const maxComponents = typeof limits === "number" ? limits : limits.maxPathComponents ?? Infinity;
|
|
15
|
+
for (const value of [maxBytes, maxComponents]) {
|
|
16
|
+
if (value !== Infinity && (!Number.isSafeInteger(value) || value < 0)) {
|
|
17
|
+
throw new RangeError("Path limits must be nonnegative safe integers or Infinity");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
if (typeof path !== "string" || path.includes("\0")) {
|
|
6
21
|
throw new FsError("EINVAL", { syscall: "resolve", message: "paths must be strings without NUL bytes" });
|
|
7
22
|
}
|
|
8
|
-
if (path
|
|
23
|
+
if (maxBytes !== Infinity && pathByteLength(path) > maxBytes) {
|
|
9
24
|
throw new FsError("ENAMETOOLONG", { syscall: "resolve", path });
|
|
10
25
|
}
|
|
11
|
-
|
|
26
|
+
if (maxComponents === Infinity)
|
|
27
|
+
return;
|
|
28
|
+
const limit = maxComponents;
|
|
12
29
|
let components = 0;
|
|
13
30
|
let inComponent = false;
|
|
14
31
|
for (let i = 0; i < path.length; i++) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
|
|
3
3
|
import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
|
|
4
4
|
import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../memory/index.js";
|
|
5
5
|
export const nullPath = "/dev/null";
|
|
@@ -23,20 +23,20 @@ export function lexicalDevicePath(path) {
|
|
|
23
23
|
return `/${parts.join("/")}`;
|
|
24
24
|
}
|
|
25
25
|
async function resolveResizeDevicePath(filesystem, path, options, create, namespace, traversal) {
|
|
26
|
-
validatePath(path);
|
|
26
|
+
validatePath(path, options.pathLimits);
|
|
27
27
|
const components = (value) => {
|
|
28
28
|
const result = value.split("/").filter(Boolean);
|
|
29
29
|
if (value.endsWith("/"))
|
|
30
30
|
result.push("");
|
|
31
31
|
return result;
|
|
32
32
|
};
|
|
33
|
-
|
|
34
|
-
let remainingComponents =
|
|
33
|
+
let pending = components(path);
|
|
34
|
+
let remainingComponents = (options.pathLimits?.maxPathComponents ?? Infinity) - pending.length;
|
|
35
35
|
if (remainingComponents < 0)
|
|
36
36
|
throw new FsError("ENAMETOOLONG", { path });
|
|
37
37
|
const parts = [];
|
|
38
38
|
let links = 0;
|
|
39
|
-
let expanded = path
|
|
39
|
+
let expanded = pathByteLength(path);
|
|
40
40
|
let boundary;
|
|
41
41
|
while (pending.length) {
|
|
42
42
|
options.signal?.throwIfAborted();
|
|
@@ -99,9 +99,9 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
99
99
|
throw new FsError("ELOOP", { path });
|
|
100
100
|
const target = await Reflect.apply(readlink, filesystem, [candidate, options]);
|
|
101
101
|
options.signal?.throwIfAborted();
|
|
102
|
-
validatePath(target);
|
|
103
|
-
expanded += target
|
|
104
|
-
if (expanded >
|
|
102
|
+
validatePath(target, options.pathLimits);
|
|
103
|
+
expanded += pathByteLength(target);
|
|
104
|
+
if (expanded > (options.pathLimits?.maxPathBytes ?? Infinity))
|
|
105
105
|
throw new FsError("ENAMETOOLONG", { path });
|
|
106
106
|
if (target.startsWith("/"))
|
|
107
107
|
parts.splice(0, parts.length, ...(boundary ?? "/").split("/").filter(Boolean));
|
|
@@ -111,7 +111,7 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
111
111
|
if (targetParts.length > remainingComponents)
|
|
112
112
|
throw new FsError("ENAMETOOLONG", { path });
|
|
113
113
|
remainingComponents -= targetParts.length;
|
|
114
|
-
pending.
|
|
114
|
+
pending = targetParts.concat(pending);
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -141,7 +141,8 @@ function exceedsComponentByteLimit(value) {
|
|
|
141
141
|
}
|
|
142
142
|
export async function resolveDevicePath(filesystem, path, options, followFinal = true, resizeCreate, traversal) {
|
|
143
143
|
options.signal?.throwIfAborted();
|
|
144
|
-
|
|
144
|
+
validatePath(path, options.pathLimits);
|
|
145
|
+
if (options.pathLimits === undefined && Reflect.get(filesystem, pathNamespace) === undefined) {
|
|
145
146
|
const fast = tryResolveMemoryDevicePath(filesystem, path, resizeCreate);
|
|
146
147
|
if (fast !== undefined)
|
|
147
148
|
return fast;
|
|
@@ -155,13 +156,13 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
155
156
|
const aliases = typeof lstat === "function";
|
|
156
157
|
if (!aliases && lexical !== nullPath && lexical !== deviceDirectory && lexical !== "/")
|
|
157
158
|
return lexical;
|
|
158
|
-
|
|
159
|
-
let remainingComponents =
|
|
159
|
+
let pending = path.split("/");
|
|
160
|
+
let remainingComponents = (options.pathLimits?.maxPathComponents ?? Infinity) - pending.filter(Boolean).length;
|
|
160
161
|
if (remainingComponents < 0)
|
|
161
162
|
throw new FsError("ENAMETOOLONG", { path });
|
|
162
163
|
const parts = [];
|
|
163
164
|
let links = 0;
|
|
164
|
-
let expanded = path
|
|
165
|
+
let expanded = pathByteLength(path);
|
|
165
166
|
let absolute = path.startsWith("/");
|
|
166
167
|
let traversalFailure;
|
|
167
168
|
let failedDepth = Infinity;
|
|
@@ -218,9 +219,9 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
218
219
|
throw new FsError("ELOOP", { path });
|
|
219
220
|
const target = await Reflect.apply(readlink, filesystem, [lookup, options]);
|
|
220
221
|
options.signal?.throwIfAborted();
|
|
221
|
-
validatePath(target);
|
|
222
|
-
expanded += target
|
|
223
|
-
if (expanded >
|
|
222
|
+
validatePath(target, options.pathLimits);
|
|
223
|
+
expanded += pathByteLength(target);
|
|
224
|
+
if (expanded > (options.pathLimits?.maxPathBytes ?? Infinity))
|
|
224
225
|
throw new FsError("ENAMETOOLONG", { path });
|
|
225
226
|
const targetSplit = target.split("/");
|
|
226
227
|
const nonEmptyCount = targetSplit.filter(Boolean).length;
|
|
@@ -232,7 +233,7 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
232
233
|
absolute = true;
|
|
233
234
|
failedDepth = Infinity;
|
|
234
235
|
}
|
|
235
|
-
pending.
|
|
236
|
+
pending = targetSplit.concat(pending);
|
|
236
237
|
continue;
|
|
237
238
|
}
|
|
238
239
|
if (stat && pending.length && stat.type !== "directory") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
|
-
import { normalizePath } from "../../contracts/virtual-path.js";
|
|
2
|
+
import { normalizePath, validatePath, pathByteLength } from "../../contracts/virtual-path.js";
|
|
3
3
|
import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
4
4
|
import { getOwnedS3Entry } from "../s3/registry.js";
|
|
5
5
|
import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
|
|
@@ -56,10 +56,10 @@ DUMMY_POOL_FILE_NODE.atimeMs = DUMMY_POOL_FILE_NODE.mtimeMs = DUMMY_POOL_FILE_NO
|
|
|
56
56
|
const defaultDateNow = Date.now;
|
|
57
57
|
let fastWriteCachedNow = Date.now();
|
|
58
58
|
let fastWriteNowTick = 0;
|
|
59
|
-
const lastFastMapMiss = {
|
|
60
|
-
map: undefined, key: "", slot: -1,
|
|
61
|
-
};
|
|
62
59
|
class FastDirectoryEntriesMap {
|
|
60
|
+
// Keep speculative slots local so a failed write cannot pin a tenant globally.
|
|
61
|
+
_missKey = "";
|
|
62
|
+
_missSlot = -1;
|
|
63
63
|
_table = new Int16Array(128).fill(-1);
|
|
64
64
|
_mask = 127;
|
|
65
65
|
_keys = new Array(64).fill("");
|
|
@@ -96,9 +96,8 @@ class FastDirectoryEntriesMap {
|
|
|
96
96
|
getForWriteWithHash(k, h) {
|
|
97
97
|
let slot = h & this._mask;
|
|
98
98
|
if (this.size === 0) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
lastFastMapMiss.slot = slot;
|
|
99
|
+
this._missKey = k;
|
|
100
|
+
this._missSlot = slot;
|
|
102
101
|
return undefined;
|
|
103
102
|
}
|
|
104
103
|
const mask = this._mask;
|
|
@@ -108,9 +107,8 @@ class FastDirectoryEntriesMap {
|
|
|
108
107
|
while (true) {
|
|
109
108
|
const idx = table[slot];
|
|
110
109
|
if (idx === -1) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
lastFastMapMiss.slot = firstDeleted !== -1 ? firstDeleted : slot;
|
|
110
|
+
this._missKey = k;
|
|
111
|
+
this._missSlot = firstDeleted !== -1 ? firstDeleted : slot;
|
|
114
112
|
return undefined;
|
|
115
113
|
}
|
|
116
114
|
if (idx === -2) {
|
|
@@ -160,6 +158,7 @@ class FastDirectoryEntriesMap {
|
|
|
160
158
|
return this.get(k) !== undefined;
|
|
161
159
|
}
|
|
162
160
|
_rebuild(newCap) {
|
|
161
|
+
this._missKey = "";
|
|
163
162
|
const newTableLen = newCap * 2;
|
|
164
163
|
const newMask = newTableLen - 1;
|
|
165
164
|
const newTable = newCap <= 16384 ? new Int16Array(newTableLen).fill(-1) : new Int32Array(newTableLen).fill(-1);
|
|
@@ -199,15 +198,16 @@ class FastDirectoryEntriesMap {
|
|
|
199
198
|
this._next = writeIdx;
|
|
200
199
|
}
|
|
201
200
|
set(k, v) {
|
|
202
|
-
if (
|
|
201
|
+
if (k === this._missKey && this._next < this._keys.length) {
|
|
203
202
|
const entryIdx = this._next++;
|
|
204
|
-
this._table[
|
|
203
|
+
this._table[this._missSlot] = entryIdx;
|
|
205
204
|
this._keys[entryIdx] = k;
|
|
206
205
|
this._vals[entryIdx] = v;
|
|
207
206
|
this.size++;
|
|
208
|
-
|
|
207
|
+
this._missKey = "";
|
|
209
208
|
return this;
|
|
210
209
|
}
|
|
210
|
+
this._missKey = "";
|
|
211
211
|
if (this._next >= this._keys.length) {
|
|
212
212
|
this._rebuild(this.size * 2 <= this._keys.length ? this._keys.length : this._keys.length * 2);
|
|
213
213
|
}
|
|
@@ -239,6 +239,7 @@ class FastDirectoryEntriesMap {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
delete(k) {
|
|
242
|
+
this._missKey = "";
|
|
242
243
|
if (this.size === 0)
|
|
243
244
|
return false;
|
|
244
245
|
let slot = this._hash(k);
|
|
@@ -266,6 +267,7 @@ class FastDirectoryEntriesMap {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
clear() {
|
|
270
|
+
this._missKey = "";
|
|
269
271
|
if (this._next > 0) {
|
|
270
272
|
this._table.fill(-1);
|
|
271
273
|
for (let i = 0; i < this._next; i++) {
|
|
@@ -710,7 +712,7 @@ export class MemoryFileSystem {
|
|
|
710
712
|
const owner = ownedStores.get(this);
|
|
711
713
|
if (path !== "")
|
|
712
714
|
this.validatePath(path, "realpath");
|
|
713
|
-
return resolveMissingTarget(owner.root, path || ".", options.signal);
|
|
715
|
+
return resolveMissingTarget(owner.root, path || ".", options.signal, this.ledger.limits);
|
|
714
716
|
}
|
|
715
717
|
async confineExtraction(roots, options = {}) {
|
|
716
718
|
options.signal?.throwIfAborted();
|
|
@@ -935,6 +937,7 @@ export class MemoryFileSystem {
|
|
|
935
937
|
}
|
|
936
938
|
replaceData(node, allocation, length = allocation.data.byteLength) {
|
|
937
939
|
const previous = node.allocation;
|
|
940
|
+
node.sourceRef = undefined;
|
|
938
941
|
this.totalBytes += length - node.byteLength;
|
|
939
942
|
node.byteLength = length;
|
|
940
943
|
node.view = length === allocation.data.byteLength ? allocation.data : undefined;
|
|
@@ -968,6 +971,8 @@ export class MemoryFileSystem {
|
|
|
968
971
|
this.fail("EINVAL", syscall, path);
|
|
969
972
|
if (path.length === 0)
|
|
970
973
|
this.fail("ENOENT", syscall, path);
|
|
974
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
975
|
+
validatePath(path, this.ledger.limits);
|
|
971
976
|
}
|
|
972
977
|
permission(node, mask, syscall, path) {
|
|
973
978
|
if (((node.mode >> 6) & mask) !== mask)
|
|
@@ -989,6 +994,8 @@ export class MemoryFileSystem {
|
|
|
989
994
|
node.mtimeMs = node.ctimeMs = now;
|
|
990
995
|
}
|
|
991
996
|
resolveNode(path, syscall, followFinal = true) {
|
|
997
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
998
|
+
validatePath(path, this.ledger.limits);
|
|
992
999
|
const cache = memoryCaches.get(this.ledger);
|
|
993
1000
|
if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
|
|
994
1001
|
const fastDir = cache.lastFastDirNode;
|
|
@@ -1075,7 +1082,7 @@ export class MemoryFileSystem {
|
|
|
1075
1082
|
}
|
|
1076
1083
|
}
|
|
1077
1084
|
// Bound all component arrays allocated by this resolution, including cycles.
|
|
1078
|
-
let remainingPathUnits =
|
|
1085
|
+
let remainingPathUnits = this.ledger.limits.maxPathBytes - pathByteLength(path);
|
|
1079
1086
|
if (remainingPathUnits < 0)
|
|
1080
1087
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1081
1088
|
let initialComponents = 0;
|
|
@@ -1085,12 +1092,12 @@ export class MemoryFileSystem {
|
|
|
1085
1092
|
inComp = false;
|
|
1086
1093
|
else if (!inComp) {
|
|
1087
1094
|
inComp = true;
|
|
1088
|
-
if (++initialComponents >
|
|
1095
|
+
if (++initialComponents > this.ledger.limits.maxPathComponents)
|
|
1089
1096
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1090
1097
|
}
|
|
1091
1098
|
}
|
|
1092
|
-
|
|
1093
|
-
let remainingComponents =
|
|
1099
|
+
let pending = path.split("/").filter(Boolean);
|
|
1100
|
+
let remainingComponents = this.ledger.limits.maxPathComponents - pending.length;
|
|
1094
1101
|
if (path.endsWith("/"))
|
|
1095
1102
|
pending.push("");
|
|
1096
1103
|
const stack = [{ node: this.root, name: "" }];
|
|
@@ -1140,9 +1147,9 @@ export class MemoryFileSystem {
|
|
|
1140
1147
|
if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
|
|
1141
1148
|
if (++links > 40)
|
|
1142
1149
|
this.fail("ELOOP", syscall, path);
|
|
1143
|
-
if (node.target
|
|
1150
|
+
if (pathByteLength(node.target) > remainingPathUnits)
|
|
1144
1151
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1145
|
-
remainingPathUnits -= node.target
|
|
1152
|
+
remainingPathUnits -= pathByteLength(node.target);
|
|
1146
1153
|
const target = node.target.split("/").filter(Boolean);
|
|
1147
1154
|
if (target.length > remainingComponents)
|
|
1148
1155
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
@@ -1151,7 +1158,7 @@ export class MemoryFileSystem {
|
|
|
1151
1158
|
target.push("");
|
|
1152
1159
|
if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
|
|
1153
1160
|
target.pop();
|
|
1154
|
-
pending.
|
|
1161
|
+
pending = target.concat(pending);
|
|
1155
1162
|
if (node.target.startsWith("/"))
|
|
1156
1163
|
stack.splice(1);
|
|
1157
1164
|
continue;
|
|
@@ -1167,6 +1174,8 @@ export class MemoryFileSystem {
|
|
|
1167
1174
|
};
|
|
1168
1175
|
}
|
|
1169
1176
|
file(path, syscall) {
|
|
1177
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1178
|
+
validatePath(path, this.ledger.limits);
|
|
1170
1179
|
if (this.symlinkCount === 0 && path.charCodeAt(0) === 47 && path.length > 1 && path.indexOf("/", 1) === -1 && !path.includes("\0")) {
|
|
1171
1180
|
const name = path === _lastFileSlice1Path ? _lastFileSlice1Name : (_lastFileSlice1Path = path, _lastFileSlice1Name = path.slice(1));
|
|
1172
1181
|
if (name !== "." && name !== "..") {
|
|
@@ -1444,6 +1453,8 @@ export class MemoryFileSystem {
|
|
|
1444
1453
|
}, syscall, path);
|
|
1445
1454
|
}
|
|
1446
1455
|
writeData(path, data, options, syscall) {
|
|
1456
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1457
|
+
validatePath(path, this.ledger.limits);
|
|
1447
1458
|
if (!(data instanceof Uint8Array))
|
|
1448
1459
|
throw new TypeError("Memory files require Uint8Array data");
|
|
1449
1460
|
const target = this.prepareWrite(path, options, syscall);
|
|
@@ -1571,9 +1582,12 @@ export class MemoryFileSystem {
|
|
|
1571
1582
|
node.byteLength = length;
|
|
1572
1583
|
node.view = length === allocation.data.byteLength ? allocation.data : undefined;
|
|
1573
1584
|
}
|
|
1585
|
+
node.sourceRef = undefined;
|
|
1574
1586
|
this.changed(node, now);
|
|
1575
1587
|
}
|
|
1576
1588
|
writeMemoryFileFast(path, data, append, mode) {
|
|
1589
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1590
|
+
validatePath(path, this.ledger.limits);
|
|
1577
1591
|
const cache = memoryCaches.get(this.ledger);
|
|
1578
1592
|
const syscall = append ? "appendFile" : "writeFile";
|
|
1579
1593
|
if (append &&
|
|
@@ -1582,8 +1596,8 @@ export class MemoryFileSystem {
|
|
|
1582
1596
|
cache.lastFastFileNode.nlink !== 0 &&
|
|
1583
1597
|
cache.lastFastDirNode !== undefined &&
|
|
1584
1598
|
cache.lastFastDirNode.nlink !== 0 &&
|
|
1585
|
-
(cache.lastFastDirNode.mode &
|
|
1586
|
-
(cache.lastFastFileNode.mode &
|
|
1599
|
+
(cache.lastFastDirNode.mode & 0o100) !== 0 &&
|
|
1600
|
+
(cache.lastFastFileNode.mode & 0o200) !== 0) {
|
|
1587
1601
|
const current = cache.lastFastFileNode;
|
|
1588
1602
|
const length = current.byteLength + data.byteLength;
|
|
1589
1603
|
this.admitSize(current, length, syscall, path);
|
|
@@ -1599,7 +1613,7 @@ export class MemoryFileSystem {
|
|
|
1599
1613
|
const cachedDir = cache.lastFastDirNode;
|
|
1600
1614
|
if (cachedDir !== undefined &&
|
|
1601
1615
|
cachedDir.nlink !== 0 &&
|
|
1602
|
-
(cachedDir.mode &
|
|
1616
|
+
(cachedDir.mode & 0o100) !== 0 &&
|
|
1603
1617
|
cachedPrefix.length > 1 &&
|
|
1604
1618
|
path.startsWith(cachedPrefix) &&
|
|
1605
1619
|
path.indexOf("/", cachedPrefix.length) === -1) {
|
|
@@ -1616,7 +1630,7 @@ export class MemoryFileSystem {
|
|
|
1616
1630
|
name = path.slice(start);
|
|
1617
1631
|
if (exceedsComponentByteLimit(name))
|
|
1618
1632
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1619
|
-
if (start > 1 && (parent.mode &
|
|
1633
|
+
if (start > 1 && (parent.mode & 0o100) !== 0) {
|
|
1620
1634
|
cache.lastFastDirPrefix = path.slice(0, start);
|
|
1621
1635
|
cache.lastFastDirNode = parent;
|
|
1622
1636
|
}
|
|
@@ -1753,6 +1767,7 @@ export class MemoryFileSystem {
|
|
|
1753
1767
|
let ino = this.nextInode;
|
|
1754
1768
|
this.nextInode = ino + batchCount;
|
|
1755
1769
|
let nextIdx = 1;
|
|
1770
|
+
entries._missKey = "";
|
|
1756
1771
|
entries._next = 1 + batchCount;
|
|
1757
1772
|
entries.size = 1 + batchCount;
|
|
1758
1773
|
const table = entries._table;
|
|
@@ -1802,6 +1817,8 @@ export class MemoryFileSystem {
|
|
|
1802
1817
|
}
|
|
1803
1818
|
}
|
|
1804
1819
|
writeMemoryFileInDirFast(dirPrefix, name, data, append, mode, nameHash) {
|
|
1820
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1821
|
+
validatePath(dirPrefix + name, this.ledger.limits);
|
|
1805
1822
|
const cache = this._cache;
|
|
1806
1823
|
if (!append &&
|
|
1807
1824
|
dirPrefix === cache.lastFastDirPrefix &&
|
|
@@ -1817,6 +1834,7 @@ export class MemoryFileSystem {
|
|
|
1817
1834
|
const entries = parent.entries;
|
|
1818
1835
|
const existing = nameHash !== undefined ? entries.getForWriteWithHash(name, nameHash) : entries.getForWrite(name);
|
|
1819
1836
|
if (existing === undefined && entries._next < entries._keys.length) {
|
|
1837
|
+
this.ledger.reserve(name.length * 2, 2, "writeFile", name);
|
|
1820
1838
|
const now = fastWriteCachedNow;
|
|
1821
1839
|
const length = data.byteLength;
|
|
1822
1840
|
const node = sharedFileNodePool[--sharedFileNodePoolLen];
|
|
@@ -1838,13 +1856,11 @@ export class MemoryFileSystem {
|
|
|
1838
1856
|
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
1839
1857
|
node.sourceRef = node.data.subarray(0);
|
|
1840
1858
|
const entryIdx = entries._next++;
|
|
1841
|
-
entries._table[
|
|
1859
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
1842
1860
|
entries._keys[entryIdx] = name;
|
|
1843
1861
|
entries._vals[entryIdx] = node;
|
|
1844
1862
|
entries.size++;
|
|
1845
|
-
|
|
1846
|
-
lastFastMapMiss.key = "";
|
|
1847
|
-
this.ledger.reserve(name.length * 2, 2, "writeFile", name);
|
|
1863
|
+
entries._missKey = "";
|
|
1848
1864
|
this.totalBytes += length;
|
|
1849
1865
|
this.mutationTick = (this.mutationTick + 1) | 0;
|
|
1850
1866
|
parent.revision = parent.revision < 1073741823 ? (parent.revision + 1) | 0 : Math.min(Number.MAX_SAFE_INTEGER + 1, parent.revision + 1);
|
|
@@ -1864,8 +1880,8 @@ export class MemoryFileSystem {
|
|
|
1864
1880
|
cache.lastFastFileNode.nlink !== 0 &&
|
|
1865
1881
|
cache.lastFastDirNode !== undefined &&
|
|
1866
1882
|
cache.lastFastDirNode.nlink !== 0 &&
|
|
1867
|
-
(cache.lastFastDirNode.mode &
|
|
1868
|
-
(cache.lastFastFileNode.mode &
|
|
1883
|
+
(cache.lastFastDirNode.mode & 0o100) !== 0 &&
|
|
1884
|
+
(cache.lastFastFileNode.mode & 0o200) !== 0) {
|
|
1869
1885
|
const current = cache.lastFastFileNode;
|
|
1870
1886
|
const length = current.byteLength + data.byteLength;
|
|
1871
1887
|
this.admitSize(current, length, syscall, name);
|
|
@@ -1878,7 +1894,7 @@ export class MemoryFileSystem {
|
|
|
1878
1894
|
const cachedDir = cache.lastFastDirNode;
|
|
1879
1895
|
if (cachedDir !== undefined &&
|
|
1880
1896
|
cachedDir.nlink !== 0 &&
|
|
1881
|
-
(cachedDir.mode &
|
|
1897
|
+
(cachedDir.mode & 0o100) !== 0 &&
|
|
1882
1898
|
dirPrefix === cache.lastFastDirPrefix) {
|
|
1883
1899
|
parent = cachedDir;
|
|
1884
1900
|
if (exceedsComponentByteLimit(name))
|
|
@@ -1905,7 +1921,7 @@ export class MemoryFileSystem {
|
|
|
1905
1921
|
this.permission(parent, 1, syscall, name);
|
|
1906
1922
|
if (exceedsComponentByteLimit(name))
|
|
1907
1923
|
this.fail("ENAMETOOLONG", syscall, name);
|
|
1908
|
-
if (dirPrefix.length > 1 && (parent.mode &
|
|
1924
|
+
if (dirPrefix.length > 1 && (parent.mode & 0o100) !== 0) {
|
|
1909
1925
|
cache.lastFastDirPrefix = dirPrefix;
|
|
1910
1926
|
cache.lastFastDirNode = parent;
|
|
1911
1927
|
}
|
|
@@ -1984,12 +2000,8 @@ export class MemoryFileSystem {
|
|
|
1984
2000
|
cache.lastFastFilePath = "";
|
|
1985
2001
|
cache.lastFastFileName = name;
|
|
1986
2002
|
cache.lastFastFileNode = node;
|
|
1987
|
-
lastFastMapMiss.map = undefined;
|
|
1988
|
-
lastFastMapMiss.key = "";
|
|
1989
2003
|
}
|
|
1990
2004
|
catch (error) {
|
|
1991
|
-
lastFastMapMiss.map = undefined;
|
|
1992
|
-
lastFastMapMiss.key = "";
|
|
1993
2005
|
this.ledger.release(nameBytes, 2);
|
|
1994
2006
|
throw error;
|
|
1995
2007
|
}
|
|
@@ -3175,6 +3187,7 @@ export function tryOpenMemoryRedirectHandleSync(filesystem, path, append, mode,
|
|
|
3175
3187
|
return undefined;
|
|
3176
3188
|
}
|
|
3177
3189
|
signal?.throwIfAborted();
|
|
3190
|
+
validatePath(path, owner.ledger.limits);
|
|
3178
3191
|
const validMode = (mode & 0o777) === mode
|
|
3179
3192
|
? mode
|
|
3180
3193
|
: mem.mode(mode, 0o666, "open", path);
|
|
@@ -3276,14 +3289,13 @@ export function tryOpenMemoryRedirectHandleSync(filesystem, path, append, mode,
|
|
|
3276
3289
|
}
|
|
3277
3290
|
const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
|
|
3278
3291
|
const entries = parent.entries;
|
|
3279
|
-
if (
|
|
3292
|
+
if (entries._missKey === name && entries._next < entries._keys.length) {
|
|
3280
3293
|
const entryIdx = entries._next++;
|
|
3281
|
-
entries._table[
|
|
3294
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
3282
3295
|
entries._keys[entryIdx] = name;
|
|
3283
3296
|
entries._vals[entryIdx] = newNode;
|
|
3284
3297
|
entries.size++;
|
|
3285
|
-
|
|
3286
|
-
lastFastMapMiss.key = "";
|
|
3298
|
+
entries._missKey = "";
|
|
3287
3299
|
}
|
|
3288
3300
|
else {
|
|
3289
3301
|
entries.set(name, newNode);
|
|
@@ -3359,6 +3371,7 @@ export function tryResolveMemoryDevicePath(filesystem, path, resizeCreate) {
|
|
|
3359
3371
|
const mem = filesystem;
|
|
3360
3372
|
if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, deviceFastMethodNames, false))
|
|
3361
3373
|
return undefined;
|
|
3374
|
+
validatePath(path, ownedStores.get(mem).ledger.limits);
|
|
3362
3375
|
if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
|
|
3363
3376
|
return undefined;
|
|
3364
3377
|
let current = mem.root;
|
|
@@ -3446,6 +3459,9 @@ export function getLastReadMemoryFileSourceRef(view) {
|
|
|
3446
3459
|
return ref;
|
|
3447
3460
|
}
|
|
3448
3461
|
export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal, captureSourceRef = false) {
|
|
3462
|
+
// A capture belongs only to the most recent read, including failed/fallback reads.
|
|
3463
|
+
lastReadViewData = undefined;
|
|
3464
|
+
lastReadViewSourceRef = undefined;
|
|
3449
3465
|
const mem = filesystem;
|
|
3450
3466
|
if (mem._owner === undefined || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
|
|
3451
3467
|
return undefined;
|
|
@@ -3461,6 +3477,8 @@ export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal, ca
|
|
|
3461
3477
|
node.atimeMs = Date.now();
|
|
3462
3478
|
else if ((++fastWriteNowTick & 63) === 0)
|
|
3463
3479
|
node.atimeMs = fastWriteCachedNow = Date.now();
|
|
3480
|
+
else
|
|
3481
|
+
node.atimeMs = fastWriteCachedNow;
|
|
3464
3482
|
if (captureSourceRef && node.revision === 0) {
|
|
3465
3483
|
lastReadViewData = new WeakRef(data);
|
|
3466
3484
|
lastReadViewSourceRef = node.sourceRef === undefined ? undefined : new WeakRef(node.sourceRef);
|
|
@@ -3540,6 +3558,7 @@ export function tryGetMemoryDirectoryEntryNamesSync(filesystem, path) {
|
|
|
3540
3558
|
const owner = ownedStores.get(mem);
|
|
3541
3559
|
if (!owner || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
|
|
3542
3560
|
return undefined;
|
|
3561
|
+
validatePath(path, owner.ledger.limits);
|
|
3543
3562
|
const root = mem.root;
|
|
3544
3563
|
if (path === "/") {
|
|
3545
3564
|
if (((root.mode >> 6) & 4) !== 4)
|
|
@@ -3606,6 +3625,7 @@ export function tryMkdirMemorySync(filesystem, path, recursive, mode, signal, di
|
|
|
3606
3625
|
return false;
|
|
3607
3626
|
}
|
|
3608
3627
|
signal?.throwIfAborted();
|
|
3628
|
+
validatePath(path, owner.ledger.limits);
|
|
3609
3629
|
const validMode = mem.mode(mode, 0o777, "mkdir", path);
|
|
3610
3630
|
if (recursive) {
|
|
3611
3631
|
let current = owner.root;
|
|
@@ -3657,6 +3677,7 @@ export function tryRmRfMemorySync(filesystem, path, signal) {
|
|
|
3657
3677
|
return false;
|
|
3658
3678
|
}
|
|
3659
3679
|
signal?.throwIfAborted();
|
|
3680
|
+
validatePath(path, owner.ledger.limits);
|
|
3660
3681
|
let current = owner.root;
|
|
3661
3682
|
let start = 1;
|
|
3662
3683
|
let targetNode;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export interface MemoryFileSystemLimits {
|
|
2
2
|
readonly maxBytes?: number;
|
|
3
|
+
readonly maxPathBytes: number;
|
|
4
|
+
readonly maxPathComponents: number;
|
|
3
5
|
readonly maxFileBytes: number;
|
|
4
6
|
readonly maxRetainedBytes: number;
|
|
5
7
|
readonly maxMetadataUnits: number;
|
|
6
8
|
}
|
|
7
|
-
export type MemoryFileSystemOptions =
|
|
9
|
+
export type MemoryFileSystemOptions = {
|
|
10
|
+
readonly [Key in keyof MemoryFileSystemLimits]?: MemoryFileSystemLimits[Key] | undefined;
|
|
11
|
+
};
|
|
8
12
|
export declare const defaultMemoryFileSystemLimits: Readonly<MemoryFileSystemLimits>;
|
|
9
13
|
export declare function normalizeMemoryFileSystemLimits(options: unknown): Readonly<MemoryFileSystemLimits>;
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { readConfigRecord } from "../../config.js";
|
|
2
2
|
export const defaultMemoryFileSystemLimits = Object.freeze({
|
|
3
|
+
maxPathBytes: Infinity,
|
|
4
|
+
maxPathComponents: Infinity,
|
|
3
5
|
maxFileBytes: Infinity,
|
|
4
6
|
maxRetainedBytes: Infinity,
|
|
5
7
|
maxMetadataUnits: Infinity,
|
|
6
8
|
});
|
|
7
9
|
export function normalizeMemoryFileSystemLimits(options) {
|
|
8
|
-
const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
|
|
10
|
+
const keys = ["maxPathBytes", "maxPathComponents", "maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
|
|
9
11
|
const record = readConfigRecord(options, "memory option", [...keys, "maxBytes"]);
|
|
10
12
|
const limits = { ...defaultMemoryFileSystemLimits };
|
|
11
|
-
|
|
12
|
-
if (typeof record.maxBytes !== "number" || !Number.isSafeInteger(record.maxBytes) || record.maxBytes < 0) {
|
|
13
|
-
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
14
|
-
}
|
|
15
|
-
limits.maxBytes = record.maxBytes;
|
|
16
|
-
}
|
|
17
|
-
for (const key of keys) {
|
|
18
|
-
if (!Object.hasOwn(record, key))
|
|
19
|
-
continue;
|
|
13
|
+
for (const key of [...keys, "maxBytes"]) {
|
|
20
14
|
const value = record[key];
|
|
15
|
+
if (value === undefined || value === Infinity)
|
|
16
|
+
continue;
|
|
21
17
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
|
|
22
|
-
throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
|
|
18
|
+
throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer or Infinity`);
|
|
23
19
|
}
|
|
24
20
|
limits[key] = value;
|
|
25
21
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PathLimits } from "../../contracts/virtual-path.js";
|
|
1
2
|
type Node = {
|
|
2
3
|
type: "file";
|
|
3
4
|
mode: number;
|
|
@@ -10,5 +11,5 @@ type Node = {
|
|
|
10
11
|
mode: number;
|
|
11
12
|
entries: ReadonlyMap<string, Node>;
|
|
12
13
|
};
|
|
13
|
-
export declare function resolveMissingTarget(root: Node, path: string, signal?: AbortSignal): string;
|
|
14
|
+
export declare function resolveMissingTarget(root: Node, path: string, signal?: AbortSignal, limits?: PathLimits): string;
|
|
14
15
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
|
|
1
2
|
import { FsError } from "../../contracts/errors.js";
|
|
2
3
|
function utf8ByteLength(value) {
|
|
3
4
|
let bytes = 0;
|
|
@@ -32,11 +33,14 @@ function components(path, signal) {
|
|
|
32
33
|
}
|
|
33
34
|
return result;
|
|
34
35
|
}
|
|
35
|
-
export function resolveMissingTarget(root, path, signal) {
|
|
36
|
+
export function resolveMissingTarget(root, path, signal, limits = {}) {
|
|
37
|
+
validatePath(path, limits);
|
|
36
38
|
const fail = (code, failedPath = path) => {
|
|
37
39
|
throw new FsError(code, { syscall: "realpath", path: failedPath });
|
|
38
40
|
};
|
|
39
41
|
const origin = components(path, signal);
|
|
42
|
+
let remainingBytes = (limits.maxPathBytes ?? Infinity) - pathByteLength(path);
|
|
43
|
+
let remainingComponents = (limits.maxPathComponents ?? Infinity) - origin.length;
|
|
40
44
|
const initial = { node: root, name: "" };
|
|
41
45
|
let position = initial;
|
|
42
46
|
let links = 0;
|
|
@@ -89,7 +93,10 @@ export function resolveMissingTarget(root, path, signal) {
|
|
|
89
93
|
if (node.type === "symlink") {
|
|
90
94
|
if (++links > 40)
|
|
91
95
|
return fail("ELOOP");
|
|
96
|
+
validatePath(node.target, { maxPathBytes: remainingBytes, maxPathComponents: remainingComponents });
|
|
92
97
|
const names = components(node.target, signal);
|
|
98
|
+
remainingBytes -= pathByteLength(node.target);
|
|
99
|
+
remainingComponents -= names.length;
|
|
93
100
|
if (node.target.endsWith("/"))
|
|
94
101
|
names.push({ name: ".", start: node.target.length });
|
|
95
102
|
if (node.target.startsWith("/"))
|
|
@@ -18,7 +18,7 @@ export async function createS3NamespaceFileSystem(options) {
|
|
|
18
18
|
throw new TypeError('An explicit S3 bucket and manifest key are required');
|
|
19
19
|
const maxBytes = limit(options.maxBytes);
|
|
20
20
|
const maxEntries = limit(options.maxEntries);
|
|
21
|
-
const maxManifestBytes =
|
|
21
|
+
const maxManifestBytes = options.maxManifestBytes === Infinity ? Infinity : limit(options.maxManifestBytes);
|
|
22
22
|
const maxAttempts = limit(options.maxAttempts);
|
|
23
23
|
const descriptorOptions = options;
|
|
24
24
|
for (const key of ['chunkBytes', 'maxStagedBytes', 'maxStagedPages', 'maxFileBytes', 'maxOpenFiles']) {
|
|
@@ -537,8 +537,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
537
537
|
}
|
|
538
538
|
export function retainFileSystemCleanup(filesystem, cleanupCallback, options) {
|
|
539
539
|
const maximum = options?.maxOperations ?? Infinity;
|
|
540
|
-
if (
|
|
541
|
-
throw new RangeError("cleanup maxOperations must be a nonnegative safe integer");
|
|
540
|
+
if (maximum !== Infinity && (!Number.isSafeInteger(maximum) || maximum < 0))
|
|
541
|
+
throw new RangeError("cleanup maxOperations must be a nonnegative safe integer or Infinity");
|
|
542
542
|
if (typeof cleanupCallback !== "function")
|
|
543
543
|
throw new TypeError("cleanup callback must be a function");
|
|
544
544
|
const scope = originals.get(filesystem);
|
|
@@ -29,6 +29,8 @@ export interface WebDavFileSystemOptions {
|
|
|
29
29
|
readonly maxResponseBytes?: number;
|
|
30
30
|
/** Metadata response ceiling before decoding/parsing; unlimited unless configured. */
|
|
31
31
|
readonly maxXmlBytes?: number;
|
|
32
|
+
/** Timestamp property UTF-8 byte ceiling; unlimited unless configured. */
|
|
33
|
+
readonly maxTimestampPropertyBytes?: number;
|
|
32
34
|
readonly maxEntries?: number;
|
|
33
35
|
/** XML structure ceilings; unlimited unless configured. */
|
|
34
36
|
readonly xmlLimits?: Pick<XmlLimits, "maxNodes" | "maxContentNodes" | "maxDepth" | "maxAttributes">;
|
|
@@ -80,6 +82,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
80
82
|
private readonly headers;
|
|
81
83
|
private readonly maxResponseBytes;
|
|
82
84
|
private readonly maxXmlBytes;
|
|
85
|
+
private readonly maxTimestampPropertyBytes;
|
|
83
86
|
private readonly maxEntries;
|
|
84
87
|
private readonly xmlLimits;
|
|
85
88
|
private readonly timeoutMs;
|
|
@@ -6,16 +6,15 @@ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admissi
|
|
|
6
6
|
import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
7
7
|
import { compareWebDavResources, ownedResponseIdentifier, recordOwnedResourceStat, registerResourceQuery, resourceIdentifier } from "./resource-id.js";
|
|
8
8
|
const timestampNamespace = "urn:virtual-bash:metadata";
|
|
9
|
-
const maxTimestampPropertyBytes = 4096;
|
|
10
9
|
/** Admit only five scalar members before native JSON parsing can allocate a graph. */
|
|
11
|
-
function preflightTimestamps(property) {
|
|
10
|
+
function preflightTimestamps(property, maxTimestampPropertyBytes) {
|
|
12
11
|
// Check the untrimmed text first; counting UTF-8 bytes does not allocate an encoded copy.
|
|
13
12
|
let bytes = 0;
|
|
14
13
|
for (const point of property.text) {
|
|
15
14
|
const code = point.codePointAt(0);
|
|
16
15
|
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
17
16
|
if (bytes > maxTimestampPropertyBytes)
|
|
18
|
-
throw new Error("timestamp property exceeds
|
|
17
|
+
throw new Error("timestamp property exceeds byte limit");
|
|
19
18
|
}
|
|
20
19
|
const text = scalar(property);
|
|
21
20
|
let offset = 0;
|
|
@@ -233,6 +232,7 @@ export class WebDavFileSystem {
|
|
|
233
232
|
headers;
|
|
234
233
|
maxResponseBytes;
|
|
235
234
|
maxXmlBytes;
|
|
235
|
+
maxTimestampPropertyBytes;
|
|
236
236
|
maxEntries;
|
|
237
237
|
xmlLimits;
|
|
238
238
|
timeoutMs;
|
|
@@ -290,6 +290,7 @@ export class WebDavFileSystem {
|
|
|
290
290
|
streamingAppend: this.requestStreamSupport !== false,
|
|
291
291
|
});
|
|
292
292
|
this.maxResponseBytes = options.maxResponseBytes === undefined ? Infinity : positive(options.maxResponseBytes, "maxResponseBytes", false, true);
|
|
293
|
+
this.maxTimestampPropertyBytes = positive(options.maxTimestampPropertyBytes ?? Infinity, "maxTimestampPropertyBytes", true, true);
|
|
293
294
|
this.maxXmlBytes = options.maxXmlBytes === undefined ? Infinity : positive(options.maxXmlBytes, "maxXmlBytes", false, true);
|
|
294
295
|
this.xmlLimits = Object.freeze({ ...options.xmlLimits });
|
|
295
296
|
for (const [name, value] of Object.entries(this.xmlLimits))
|
|
@@ -714,7 +715,7 @@ export class WebDavFileSystem {
|
|
|
714
715
|
let mtimeMs = date("getlastmodified") ?? 0;
|
|
715
716
|
let atimeMs = 0;
|
|
716
717
|
if (timestampProperty) {
|
|
717
|
-
const timestamps = JSON.parse(preflightTimestamps(timestampProperty));
|
|
718
|
+
const timestamps = JSON.parse(preflightTimestamps(timestampProperty, this.maxTimestampPropertyBytes));
|
|
718
719
|
if (typeof timestamps !== "object" || timestamps === null
|
|
719
720
|
|| !("version" in timestamps) || timestamps.version !== 1
|
|
720
721
|
|| !("etag" in timestamps) || typeof timestamps.etag !== "string" || !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(timestamps.etag)
|
package/dist/xml-ast/index.js
CHANGED
|
@@ -264,27 +264,42 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
264
264
|
throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
265
265
|
};
|
|
266
266
|
let pendingWork = 0;
|
|
267
|
-
const skipWhitespace = ()
|
|
267
|
+
const skipWhitespace = function* () {
|
|
268
268
|
const start = offset;
|
|
269
269
|
while (offset < source.length) {
|
|
270
270
|
const c = source.charCodeAt(offset);
|
|
271
271
|
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
272
272
|
break;
|
|
273
273
|
offset++;
|
|
274
|
+
pendingWork++;
|
|
275
|
+
if (pendingWork >= 512) {
|
|
276
|
+
yield 512;
|
|
277
|
+
pendingWork -= 512;
|
|
278
|
+
}
|
|
274
279
|
}
|
|
275
280
|
return offset - start;
|
|
276
281
|
};
|
|
277
|
-
const scanName = ()
|
|
282
|
+
const scanName = function* () {
|
|
278
283
|
const start = offset;
|
|
279
284
|
while (offset < source.length) {
|
|
280
285
|
const c = source.charCodeAt(offset);
|
|
281
286
|
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
282
287
|
break;
|
|
283
288
|
offset++;
|
|
289
|
+
pendingWork++;
|
|
290
|
+
if (pendingWork >= 512) {
|
|
291
|
+
yield 512;
|
|
292
|
+
pendingWork -= 512;
|
|
293
|
+
}
|
|
284
294
|
}
|
|
285
295
|
const name = source.slice(start, offset);
|
|
296
|
+
pendingWork += offset - start;
|
|
297
|
+
while (pendingWork >= 512) {
|
|
298
|
+
yield 512;
|
|
299
|
+
pendingWork -= 512;
|
|
300
|
+
}
|
|
286
301
|
const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
|
|
287
|
-
return [name, prefix, localName
|
|
302
|
+
return [name, prefix, localName];
|
|
288
303
|
};
|
|
289
304
|
while (offset < source.length) {
|
|
290
305
|
pendingWork += 1;
|
|
@@ -322,11 +337,15 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
322
337
|
}
|
|
323
338
|
else {
|
|
324
339
|
for (let index = 0; index < resolved.length; index++) {
|
|
340
|
+
pendingWork++;
|
|
341
|
+
if (pendingWork >= 512) {
|
|
342
|
+
yield 512;
|
|
343
|
+
pendingWork -= 512;
|
|
344
|
+
}
|
|
325
345
|
const c = resolved.charCodeAt(index);
|
|
326
346
|
if (c !== 32 && c !== 9 && c !== 13 && c !== 10)
|
|
327
347
|
invalid("text outside the root");
|
|
328
348
|
}
|
|
329
|
-
pendingWork += resolved.length;
|
|
330
349
|
while (pendingWork >= 512) {
|
|
331
350
|
yield 512;
|
|
332
351
|
pendingWork -= 512;
|
|
@@ -371,8 +390,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
371
390
|
else if (source.startsWith("<?", offset)) {
|
|
372
391
|
const start = offset;
|
|
373
392
|
offset += 2;
|
|
374
|
-
const [target
|
|
375
|
-
pendingWork += wName;
|
|
393
|
+
const [target] = yield* scanName();
|
|
376
394
|
while (pendingWork >= 512) {
|
|
377
395
|
yield 512;
|
|
378
396
|
pendingWork -= 512;
|
|
@@ -415,8 +433,8 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
415
433
|
}
|
|
416
434
|
else if (source.charCodeAt(offset + 1) === 47) {
|
|
417
435
|
offset += 2;
|
|
418
|
-
const [name
|
|
419
|
-
|
|
436
|
+
const [name] = yield* scanName();
|
|
437
|
+
yield* skipWhitespace();
|
|
420
438
|
while (pendingWork >= 512) {
|
|
421
439
|
yield 512;
|
|
422
440
|
pendingWork -= 512;
|
|
@@ -430,13 +448,14 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
430
448
|
else {
|
|
431
449
|
offset++;
|
|
432
450
|
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
433
|
-
const [name, prefix, localName
|
|
434
|
-
? [repeated.name, repeated.prefix, repeated.localName
|
|
435
|
-
: scanName();
|
|
451
|
+
const [name, prefix, localName] = repeated
|
|
452
|
+
? [repeated.name, repeated.prefix, repeated.localName]
|
|
453
|
+
: (yield* scanName());
|
|
436
454
|
if (repeated)
|
|
437
455
|
offset += name.length;
|
|
438
456
|
// Preserve scan/validation work while reusing the admitted spelling.
|
|
439
|
-
|
|
457
|
+
if (repeated)
|
|
458
|
+
pendingWork += name.length * 2;
|
|
440
459
|
while (pendingWork >= 512) {
|
|
441
460
|
yield 512;
|
|
442
461
|
pendingWork -= 512;
|
|
@@ -446,8 +465,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
446
465
|
let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
447
466
|
let ownsNamespaces = stack.length === 0;
|
|
448
467
|
while (!repeated) {
|
|
449
|
-
const ws = skipWhitespace();
|
|
450
|
-
pendingWork += ws;
|
|
468
|
+
const ws = (yield* skipWhitespace());
|
|
451
469
|
while (pendingWork >= 512) {
|
|
452
470
|
yield 512;
|
|
453
471
|
pendingWork -= 512;
|
|
@@ -457,8 +475,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
457
475
|
break;
|
|
458
476
|
if (ws === 0)
|
|
459
477
|
invalid("attributes require whitespace");
|
|
460
|
-
const [attribute, attrPrefix, attrLocal
|
|
461
|
-
pendingWork += wAttr;
|
|
478
|
+
const [attribute, attrPrefix, attrLocal] = yield* scanName();
|
|
462
479
|
while (pendingWork >= 512) {
|
|
463
480
|
yield 512;
|
|
464
481
|
pendingWork -= 512;
|
|
@@ -469,10 +486,10 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
469
486
|
throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
470
487
|
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
471
488
|
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
472
|
-
|
|
489
|
+
yield* skipWhitespace();
|
|
473
490
|
if (source[offset++] !== "=")
|
|
474
491
|
invalid("missing attribute equals");
|
|
475
|
-
|
|
492
|
+
yield* skipWhitespace();
|
|
476
493
|
const quote = source[offset++];
|
|
477
494
|
if (quote !== '"' && quote !== "'")
|
|
478
495
|
invalid("unquoted attribute");
|