@poe-platform/safe-fs 0.1.800 → 0.1.801
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 +54 -40
- 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();
|
|
@@ -968,6 +970,8 @@ export class MemoryFileSystem {
|
|
|
968
970
|
this.fail("EINVAL", syscall, path);
|
|
969
971
|
if (path.length === 0)
|
|
970
972
|
this.fail("ENOENT", syscall, path);
|
|
973
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
974
|
+
validatePath(path, this.ledger.limits);
|
|
971
975
|
}
|
|
972
976
|
permission(node, mask, syscall, path) {
|
|
973
977
|
if (((node.mode >> 6) & mask) !== mask)
|
|
@@ -989,6 +993,8 @@ export class MemoryFileSystem {
|
|
|
989
993
|
node.mtimeMs = node.ctimeMs = now;
|
|
990
994
|
}
|
|
991
995
|
resolveNode(path, syscall, followFinal = true) {
|
|
996
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
997
|
+
validatePath(path, this.ledger.limits);
|
|
992
998
|
const cache = memoryCaches.get(this.ledger);
|
|
993
999
|
if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
|
|
994
1000
|
const fastDir = cache.lastFastDirNode;
|
|
@@ -1075,7 +1081,7 @@ export class MemoryFileSystem {
|
|
|
1075
1081
|
}
|
|
1076
1082
|
}
|
|
1077
1083
|
// Bound all component arrays allocated by this resolution, including cycles.
|
|
1078
|
-
let remainingPathUnits =
|
|
1084
|
+
let remainingPathUnits = this.ledger.limits.maxPathBytes - pathByteLength(path);
|
|
1079
1085
|
if (remainingPathUnits < 0)
|
|
1080
1086
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1081
1087
|
let initialComponents = 0;
|
|
@@ -1085,12 +1091,12 @@ export class MemoryFileSystem {
|
|
|
1085
1091
|
inComp = false;
|
|
1086
1092
|
else if (!inComp) {
|
|
1087
1093
|
inComp = true;
|
|
1088
|
-
if (++initialComponents >
|
|
1094
|
+
if (++initialComponents > this.ledger.limits.maxPathComponents)
|
|
1089
1095
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1090
1096
|
}
|
|
1091
1097
|
}
|
|
1092
|
-
|
|
1093
|
-
let remainingComponents =
|
|
1098
|
+
let pending = path.split("/").filter(Boolean);
|
|
1099
|
+
let remainingComponents = this.ledger.limits.maxPathComponents - pending.length;
|
|
1094
1100
|
if (path.endsWith("/"))
|
|
1095
1101
|
pending.push("");
|
|
1096
1102
|
const stack = [{ node: this.root, name: "" }];
|
|
@@ -1140,9 +1146,9 @@ export class MemoryFileSystem {
|
|
|
1140
1146
|
if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
|
|
1141
1147
|
if (++links > 40)
|
|
1142
1148
|
this.fail("ELOOP", syscall, path);
|
|
1143
|
-
if (node.target
|
|
1149
|
+
if (pathByteLength(node.target) > remainingPathUnits)
|
|
1144
1150
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1145
|
-
remainingPathUnits -= node.target
|
|
1151
|
+
remainingPathUnits -= pathByteLength(node.target);
|
|
1146
1152
|
const target = node.target.split("/").filter(Boolean);
|
|
1147
1153
|
if (target.length > remainingComponents)
|
|
1148
1154
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
@@ -1151,7 +1157,7 @@ export class MemoryFileSystem {
|
|
|
1151
1157
|
target.push("");
|
|
1152
1158
|
if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
|
|
1153
1159
|
target.pop();
|
|
1154
|
-
pending.
|
|
1160
|
+
pending = target.concat(pending);
|
|
1155
1161
|
if (node.target.startsWith("/"))
|
|
1156
1162
|
stack.splice(1);
|
|
1157
1163
|
continue;
|
|
@@ -1167,6 +1173,8 @@ export class MemoryFileSystem {
|
|
|
1167
1173
|
};
|
|
1168
1174
|
}
|
|
1169
1175
|
file(path, syscall) {
|
|
1176
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1177
|
+
validatePath(path, this.ledger.limits);
|
|
1170
1178
|
if (this.symlinkCount === 0 && path.charCodeAt(0) === 47 && path.length > 1 && path.indexOf("/", 1) === -1 && !path.includes("\0")) {
|
|
1171
1179
|
const name = path === _lastFileSlice1Path ? _lastFileSlice1Name : (_lastFileSlice1Path = path, _lastFileSlice1Name = path.slice(1));
|
|
1172
1180
|
if (name !== "." && name !== "..") {
|
|
@@ -1444,6 +1452,8 @@ export class MemoryFileSystem {
|
|
|
1444
1452
|
}, syscall, path);
|
|
1445
1453
|
}
|
|
1446
1454
|
writeData(path, data, options, syscall) {
|
|
1455
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1456
|
+
validatePath(path, this.ledger.limits);
|
|
1447
1457
|
if (!(data instanceof Uint8Array))
|
|
1448
1458
|
throw new TypeError("Memory files require Uint8Array data");
|
|
1449
1459
|
const target = this.prepareWrite(path, options, syscall);
|
|
@@ -1574,6 +1584,8 @@ export class MemoryFileSystem {
|
|
|
1574
1584
|
this.changed(node, now);
|
|
1575
1585
|
}
|
|
1576
1586
|
writeMemoryFileFast(path, data, append, mode) {
|
|
1587
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1588
|
+
validatePath(path, this.ledger.limits);
|
|
1577
1589
|
const cache = memoryCaches.get(this.ledger);
|
|
1578
1590
|
const syscall = append ? "appendFile" : "writeFile";
|
|
1579
1591
|
if (append &&
|
|
@@ -1582,8 +1594,8 @@ export class MemoryFileSystem {
|
|
|
1582
1594
|
cache.lastFastFileNode.nlink !== 0 &&
|
|
1583
1595
|
cache.lastFastDirNode !== undefined &&
|
|
1584
1596
|
cache.lastFastDirNode.nlink !== 0 &&
|
|
1585
|
-
(cache.lastFastDirNode.mode &
|
|
1586
|
-
(cache.lastFastFileNode.mode &
|
|
1597
|
+
(cache.lastFastDirNode.mode & 0o100) !== 0 &&
|
|
1598
|
+
(cache.lastFastFileNode.mode & 0o200) !== 0) {
|
|
1587
1599
|
const current = cache.lastFastFileNode;
|
|
1588
1600
|
const length = current.byteLength + data.byteLength;
|
|
1589
1601
|
this.admitSize(current, length, syscall, path);
|
|
@@ -1599,7 +1611,7 @@ export class MemoryFileSystem {
|
|
|
1599
1611
|
const cachedDir = cache.lastFastDirNode;
|
|
1600
1612
|
if (cachedDir !== undefined &&
|
|
1601
1613
|
cachedDir.nlink !== 0 &&
|
|
1602
|
-
(cachedDir.mode &
|
|
1614
|
+
(cachedDir.mode & 0o100) !== 0 &&
|
|
1603
1615
|
cachedPrefix.length > 1 &&
|
|
1604
1616
|
path.startsWith(cachedPrefix) &&
|
|
1605
1617
|
path.indexOf("/", cachedPrefix.length) === -1) {
|
|
@@ -1616,7 +1628,7 @@ export class MemoryFileSystem {
|
|
|
1616
1628
|
name = path.slice(start);
|
|
1617
1629
|
if (exceedsComponentByteLimit(name))
|
|
1618
1630
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1619
|
-
if (start > 1 && (parent.mode &
|
|
1631
|
+
if (start > 1 && (parent.mode & 0o100) !== 0) {
|
|
1620
1632
|
cache.lastFastDirPrefix = path.slice(0, start);
|
|
1621
1633
|
cache.lastFastDirNode = parent;
|
|
1622
1634
|
}
|
|
@@ -1753,6 +1765,7 @@ export class MemoryFileSystem {
|
|
|
1753
1765
|
let ino = this.nextInode;
|
|
1754
1766
|
this.nextInode = ino + batchCount;
|
|
1755
1767
|
let nextIdx = 1;
|
|
1768
|
+
entries._missKey = "";
|
|
1756
1769
|
entries._next = 1 + batchCount;
|
|
1757
1770
|
entries.size = 1 + batchCount;
|
|
1758
1771
|
const table = entries._table;
|
|
@@ -1802,6 +1815,8 @@ export class MemoryFileSystem {
|
|
|
1802
1815
|
}
|
|
1803
1816
|
}
|
|
1804
1817
|
writeMemoryFileInDirFast(dirPrefix, name, data, append, mode, nameHash) {
|
|
1818
|
+
if (this.ledger.limits.maxPathBytes !== Infinity || this.ledger.limits.maxPathComponents !== Infinity)
|
|
1819
|
+
validatePath(dirPrefix + name, this.ledger.limits);
|
|
1805
1820
|
const cache = this._cache;
|
|
1806
1821
|
if (!append &&
|
|
1807
1822
|
dirPrefix === cache.lastFastDirPrefix &&
|
|
@@ -1838,12 +1853,11 @@ export class MemoryFileSystem {
|
|
|
1838
1853
|
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
1839
1854
|
node.sourceRef = node.data.subarray(0);
|
|
1840
1855
|
const entryIdx = entries._next++;
|
|
1841
|
-
entries._table[
|
|
1856
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
1842
1857
|
entries._keys[entryIdx] = name;
|
|
1843
1858
|
entries._vals[entryIdx] = node;
|
|
1844
1859
|
entries.size++;
|
|
1845
|
-
|
|
1846
|
-
lastFastMapMiss.key = "";
|
|
1860
|
+
entries._missKey = "";
|
|
1847
1861
|
this.ledger.reserve(name.length * 2, 2, "writeFile", name);
|
|
1848
1862
|
this.totalBytes += length;
|
|
1849
1863
|
this.mutationTick = (this.mutationTick + 1) | 0;
|
|
@@ -1864,8 +1878,8 @@ export class MemoryFileSystem {
|
|
|
1864
1878
|
cache.lastFastFileNode.nlink !== 0 &&
|
|
1865
1879
|
cache.lastFastDirNode !== undefined &&
|
|
1866
1880
|
cache.lastFastDirNode.nlink !== 0 &&
|
|
1867
|
-
(cache.lastFastDirNode.mode &
|
|
1868
|
-
(cache.lastFastFileNode.mode &
|
|
1881
|
+
(cache.lastFastDirNode.mode & 0o100) !== 0 &&
|
|
1882
|
+
(cache.lastFastFileNode.mode & 0o200) !== 0) {
|
|
1869
1883
|
const current = cache.lastFastFileNode;
|
|
1870
1884
|
const length = current.byteLength + data.byteLength;
|
|
1871
1885
|
this.admitSize(current, length, syscall, name);
|
|
@@ -1878,7 +1892,7 @@ export class MemoryFileSystem {
|
|
|
1878
1892
|
const cachedDir = cache.lastFastDirNode;
|
|
1879
1893
|
if (cachedDir !== undefined &&
|
|
1880
1894
|
cachedDir.nlink !== 0 &&
|
|
1881
|
-
(cachedDir.mode &
|
|
1895
|
+
(cachedDir.mode & 0o100) !== 0 &&
|
|
1882
1896
|
dirPrefix === cache.lastFastDirPrefix) {
|
|
1883
1897
|
parent = cachedDir;
|
|
1884
1898
|
if (exceedsComponentByteLimit(name))
|
|
@@ -1905,7 +1919,7 @@ export class MemoryFileSystem {
|
|
|
1905
1919
|
this.permission(parent, 1, syscall, name);
|
|
1906
1920
|
if (exceedsComponentByteLimit(name))
|
|
1907
1921
|
this.fail("ENAMETOOLONG", syscall, name);
|
|
1908
|
-
if (dirPrefix.length > 1 && (parent.mode &
|
|
1922
|
+
if (dirPrefix.length > 1 && (parent.mode & 0o100) !== 0) {
|
|
1909
1923
|
cache.lastFastDirPrefix = dirPrefix;
|
|
1910
1924
|
cache.lastFastDirNode = parent;
|
|
1911
1925
|
}
|
|
@@ -1984,12 +1998,8 @@ export class MemoryFileSystem {
|
|
|
1984
1998
|
cache.lastFastFilePath = "";
|
|
1985
1999
|
cache.lastFastFileName = name;
|
|
1986
2000
|
cache.lastFastFileNode = node;
|
|
1987
|
-
lastFastMapMiss.map = undefined;
|
|
1988
|
-
lastFastMapMiss.key = "";
|
|
1989
2001
|
}
|
|
1990
2002
|
catch (error) {
|
|
1991
|
-
lastFastMapMiss.map = undefined;
|
|
1992
|
-
lastFastMapMiss.key = "";
|
|
1993
2003
|
this.ledger.release(nameBytes, 2);
|
|
1994
2004
|
throw error;
|
|
1995
2005
|
}
|
|
@@ -3175,6 +3185,7 @@ export function tryOpenMemoryRedirectHandleSync(filesystem, path, append, mode,
|
|
|
3175
3185
|
return undefined;
|
|
3176
3186
|
}
|
|
3177
3187
|
signal?.throwIfAborted();
|
|
3188
|
+
validatePath(path, owner.ledger.limits);
|
|
3178
3189
|
const validMode = (mode & 0o777) === mode
|
|
3179
3190
|
? mode
|
|
3180
3191
|
: mem.mode(mode, 0o666, "open", path);
|
|
@@ -3276,14 +3287,13 @@ export function tryOpenMemoryRedirectHandleSync(filesystem, path, append, mode,
|
|
|
3276
3287
|
}
|
|
3277
3288
|
const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
|
|
3278
3289
|
const entries = parent.entries;
|
|
3279
|
-
if (
|
|
3290
|
+
if (entries._missKey === name && entries._next < entries._keys.length) {
|
|
3280
3291
|
const entryIdx = entries._next++;
|
|
3281
|
-
entries._table[
|
|
3292
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
3282
3293
|
entries._keys[entryIdx] = name;
|
|
3283
3294
|
entries._vals[entryIdx] = newNode;
|
|
3284
3295
|
entries.size++;
|
|
3285
|
-
|
|
3286
|
-
lastFastMapMiss.key = "";
|
|
3296
|
+
entries._missKey = "";
|
|
3287
3297
|
}
|
|
3288
3298
|
else {
|
|
3289
3299
|
entries.set(name, newNode);
|
|
@@ -3359,6 +3369,7 @@ export function tryResolveMemoryDevicePath(filesystem, path, resizeCreate) {
|
|
|
3359
3369
|
const mem = filesystem;
|
|
3360
3370
|
if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, deviceFastMethodNames, false))
|
|
3361
3371
|
return undefined;
|
|
3372
|
+
validatePath(path, ownedStores.get(mem).ledger.limits);
|
|
3362
3373
|
if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
|
|
3363
3374
|
return undefined;
|
|
3364
3375
|
let current = mem.root;
|
|
@@ -3540,6 +3551,7 @@ export function tryGetMemoryDirectoryEntryNamesSync(filesystem, path) {
|
|
|
3540
3551
|
const owner = ownedStores.get(mem);
|
|
3541
3552
|
if (!owner || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
|
|
3542
3553
|
return undefined;
|
|
3554
|
+
validatePath(path, owner.ledger.limits);
|
|
3543
3555
|
const root = mem.root;
|
|
3544
3556
|
if (path === "/") {
|
|
3545
3557
|
if (((root.mode >> 6) & 4) !== 4)
|
|
@@ -3606,6 +3618,7 @@ export function tryMkdirMemorySync(filesystem, path, recursive, mode, signal, di
|
|
|
3606
3618
|
return false;
|
|
3607
3619
|
}
|
|
3608
3620
|
signal?.throwIfAborted();
|
|
3621
|
+
validatePath(path, owner.ledger.limits);
|
|
3609
3622
|
const validMode = mem.mode(mode, 0o777, "mkdir", path);
|
|
3610
3623
|
if (recursive) {
|
|
3611
3624
|
let current = owner.root;
|
|
@@ -3657,6 +3670,7 @@ export function tryRmRfMemorySync(filesystem, path, signal) {
|
|
|
3657
3670
|
return false;
|
|
3658
3671
|
}
|
|
3659
3672
|
signal?.throwIfAborted();
|
|
3673
|
+
validatePath(path, owner.ledger.limits);
|
|
3660
3674
|
let current = owner.root;
|
|
3661
3675
|
let start = 1;
|
|
3662
3676
|
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");
|