@poe-platform/safe-fs 0.1.799 → 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 +16 -7
- package/dist/safe-fs/contracts/filesystem.d.ts +2 -0
- package/dist/safe-fs/contracts/io.js +26 -36
- package/dist/safe-fs/contracts/path.d.ts +2 -7
- package/dist/safe-fs/contracts/path.js +1 -7
- 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/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/devices/path.js +18 -17
- package/dist/safe-fs/fs/memory/index.js +57 -43
- 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/overlay/index.d.ts +4 -1
- package/dist/safe-fs/fs/overlay/index.js +57 -11
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
- package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
- package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
- package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
- package/dist/safe-fs/fs/s3/http/request.js +1 -109
- package/dist/safe-fs/fs/s3/http/signature.js +7 -5
- package/dist/safe-fs/fs/s3/http/transport.js +6 -3
- 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.d.ts +2 -0
- package/dist/xml-ast/index.js +68 -31
- package/package.json +9 -2
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { concatBytes } from "./bytes.js";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { request as httpRequest } from "node:http";
|
|
4
|
-
import { request as httpsRequest } from "node:https";
|
|
5
2
|
import { S3ServiceError } from "../transport.js";
|
|
3
|
+
export { sendRequest } from "#safe-fs-s3-request";
|
|
6
4
|
export function scopeFor(signal, timeout) {
|
|
7
5
|
const controller = new AbortController();
|
|
8
6
|
const abort = () => controller.abort(signal?.reason);
|
|
@@ -31,112 +29,6 @@ export async function abortable(pending, signal) {
|
|
|
31
29
|
signal.removeEventListener("abort", abort);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
|
-
export async function sendRequest(options, body, scope, factory) {
|
|
35
|
-
const signal = scope.signal;
|
|
36
|
-
signal.throwIfAborted();
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
let request;
|
|
39
|
-
let response;
|
|
40
|
-
let headersReceived = false;
|
|
41
|
-
let finished = false;
|
|
42
|
-
const cleanup = () => {
|
|
43
|
-
signal.removeEventListener("abort", abort);
|
|
44
|
-
scope.finish();
|
|
45
|
-
};
|
|
46
|
-
const close = () => {
|
|
47
|
-
if (finished)
|
|
48
|
-
return;
|
|
49
|
-
finished = true;
|
|
50
|
-
cleanup();
|
|
51
|
-
response?.destroy();
|
|
52
|
-
request?.destroy();
|
|
53
|
-
};
|
|
54
|
-
const fail = (error) => {
|
|
55
|
-
reject(signal.aborted ? signal.reason : error);
|
|
56
|
-
close();
|
|
57
|
-
};
|
|
58
|
-
const abort = () => fail(signal.reason);
|
|
59
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
60
|
-
try {
|
|
61
|
-
const create = factory ?? (options.protocol === "https:" ? httpsRequest : httpRequest);
|
|
62
|
-
request = create(options, message => {
|
|
63
|
-
response = message;
|
|
64
|
-
message.on("error", () => { });
|
|
65
|
-
if (finished || signal.aborted) {
|
|
66
|
-
close();
|
|
67
|
-
message.destroy();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
headersReceived = true;
|
|
71
|
-
message.once("close", cleanup);
|
|
72
|
-
let claimed = false;
|
|
73
|
-
const source = {
|
|
74
|
-
[Symbol.asyncIterator]() {
|
|
75
|
-
if (claimed)
|
|
76
|
-
throw new S3ServiceError("InvalidResponse", 502, "response body is single-use");
|
|
77
|
-
claimed = true;
|
|
78
|
-
const iterator = message[Symbol.asyncIterator]();
|
|
79
|
-
let ended = false;
|
|
80
|
-
return {
|
|
81
|
-
async next() {
|
|
82
|
-
try {
|
|
83
|
-
signal.throwIfAborted();
|
|
84
|
-
if (ended)
|
|
85
|
-
return { done: true, value: undefined };
|
|
86
|
-
const result = await abortable(iterator.next(), signal);
|
|
87
|
-
signal.throwIfAborted();
|
|
88
|
-
if (result.done) {
|
|
89
|
-
ended = true;
|
|
90
|
-
if (!message.complete)
|
|
91
|
-
throw new S3ServiceError("InvalidResponse", 502, "incomplete HTTP response");
|
|
92
|
-
close();
|
|
93
|
-
return { done: true, value: undefined };
|
|
94
|
-
}
|
|
95
|
-
if (!(result.value instanceof Uint8Array))
|
|
96
|
-
throw new S3ServiceError("InvalidResponse", 502, "nonbinary HTTP body");
|
|
97
|
-
return { done: false, value: new Uint8Array(result.value) };
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
ended = true;
|
|
101
|
-
close();
|
|
102
|
-
throw signal.aborted ? signal.reason : error;
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
async return() {
|
|
106
|
-
ended = true;
|
|
107
|
-
close();
|
|
108
|
-
return { done: true, value: undefined };
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
resolve({ message, body: source, close });
|
|
114
|
-
});
|
|
115
|
-
request.on("error", fail);
|
|
116
|
-
request.once("close", () => {
|
|
117
|
-
if (!headersReceived)
|
|
118
|
-
fail(new S3ServiceError("InvalidResponse", 502, "connection closed before response headers"));
|
|
119
|
-
});
|
|
120
|
-
if (signal.aborted || finished) {
|
|
121
|
-
abort();
|
|
122
|
-
request.destroy();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
void (async () => {
|
|
126
|
-
for (let offset = 0; offset < body.length; offset += 64 * 1024) {
|
|
127
|
-
signal.throwIfAborted();
|
|
128
|
-
if (!request.write(body.subarray(offset, offset + 64 * 1024)))
|
|
129
|
-
await once(request, "drain", { signal });
|
|
130
|
-
}
|
|
131
|
-
signal.throwIfAborted();
|
|
132
|
-
request.end();
|
|
133
|
-
})().catch(fail);
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
fail(error);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
32
|
export function limitedBody(response, maximum, expected) {
|
|
141
33
|
if (expected !== undefined && expected > maximum) {
|
|
142
34
|
response.close();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256 as hash } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
3
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
4
|
import { S3ServiceError } from "../transport.js";
|
|
3
5
|
export function invalid(message) {
|
|
4
6
|
throw new S3ServiceError("InvalidArgument", 400, message);
|
|
@@ -20,7 +22,7 @@ export function canonicalQuery(entries) {
|
|
|
20
22
|
.map(([name, value]) => `${name}=${value}`).join("&");
|
|
21
23
|
}
|
|
22
24
|
export function sha256(bytes) {
|
|
23
|
-
return
|
|
25
|
+
return bytesToHex(hash(typeof bytes === "string" ? utf8ToBytes(bytes) : bytes));
|
|
24
26
|
}
|
|
25
27
|
export function headerValue(value) {
|
|
26
28
|
if (typeof value !== "string" || /[^\x20-\x7e\t]/.test(value))
|
|
@@ -63,10 +65,10 @@ export function signRequest(input) {
|
|
|
63
65
|
names.map(name => `${name}:${headers[name]}\n`).join(""), signedHeaders, payloadHash].join("\n");
|
|
64
66
|
const scope = `${date}/${input.region}/s3/aws4_request`;
|
|
65
67
|
const stringToSign = ["AWS4-HMAC-SHA256", timestamp, scope, sha256(canonicalRequest)].join("\n");
|
|
66
|
-
let key =
|
|
68
|
+
let key = hmac(hash, utf8ToBytes(`AWS4${credentials.secretAccessKey}`), utf8ToBytes(date));
|
|
67
69
|
for (const part of [input.region, "s3", "aws4_request"])
|
|
68
|
-
key =
|
|
69
|
-
const signature =
|
|
70
|
+
key = hmac(hash, key, utf8ToBytes(part));
|
|
71
|
+
const signature = bytesToHex(hmac(hash, key, utf8ToBytes(stringToSign)));
|
|
70
72
|
headers.authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
71
73
|
if (Object.entries(headers).reduce((size, [name, value]) => size + name.length + value.length + 4, 0) > 16 * 1024)
|
|
72
74
|
invalid("request headers exceed 16 KiB");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { concatBytes } from "./bytes.js";
|
|
2
|
-
import { isIP } from "node:net";
|
|
3
2
|
import { S3ServiceError } from "../transport.js";
|
|
4
3
|
import { abortable, collect, limitedBody, scopeFor, sendRequest } from "./request.js";
|
|
5
4
|
import { canonicalQuery, headerValue, invalid, signRequest, uriEncode } from "./signature.js";
|
|
@@ -35,9 +34,13 @@ function boolean(value, fallback, name) {
|
|
|
35
34
|
invalid(`invalid ${name}`);
|
|
36
35
|
return value ?? fallback;
|
|
37
36
|
}
|
|
37
|
+
function isIPv4(value) {
|
|
38
|
+
const parts = value.split(".");
|
|
39
|
+
return parts.length === 4 && parts.every(part => String(Number(part)) === part && Number(part) >= 0 && Number(part) <= 255);
|
|
40
|
+
}
|
|
38
41
|
function bucketName(bucket) {
|
|
39
42
|
if (typeof bucket !== "string" || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket)
|
|
40
|
-
|| bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") ||
|
|
43
|
+
|| bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") || isIPv4(bucket))
|
|
41
44
|
invalid("expected a standard S3 bucket name");
|
|
42
45
|
}
|
|
43
46
|
function objectKey(key) {
|
|
@@ -179,7 +182,7 @@ export function createS3HttpTransport(options) {
|
|
|
179
182
|
const listEncoding = options.listUrlEncoding ?? "percent";
|
|
180
183
|
if (listEncoding !== "percent" && listEncoding !== "form")
|
|
181
184
|
invalid("unsupported LIST URL encoding");
|
|
182
|
-
if (addressing === "virtual-hosted" && (
|
|
185
|
+
if (addressing === "virtual-hosted" && (endpoint.hostname.includes(":") || isIPv4(endpoint.hostname) || endpoint.hostname === "localhost"))
|
|
183
186
|
invalid("virtual-hosted addressing requires a DNS endpoint");
|
|
184
187
|
const maxPut = limit(options.maxPutBytes, Infinity, "maxPutBytes");
|
|
185
188
|
const maxGet = limit(options.maxGetBytes, Infinity, "maxGetBytes");
|
|
@@ -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.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface XmlElement extends XmlName {
|
|
|
26
26
|
readonly epilog?: readonly XmlContent[];
|
|
27
27
|
}
|
|
28
28
|
export interface XmlLimits {
|
|
29
|
+
/** Opt-in best-effort parsing; each repaired syntax error is reported. DTDs remain forbidden. */
|
|
30
|
+
readonly recover?: (message: string) => void;
|
|
29
31
|
readonly expectedEncoding?: "UTF-8" | "UTF-16" | "UTF-16LE" | "UTF-16BE";
|
|
30
32
|
readonly retainContent?: boolean;
|
|
31
33
|
readonly maxDepth?: number;
|
package/dist/xml-ast/index.js
CHANGED
|
@@ -74,7 +74,7 @@ function qualifiedNameSync(name, cache) {
|
|
|
74
74
|
}
|
|
75
75
|
return parts;
|
|
76
76
|
}
|
|
77
|
-
function* entities(text) {
|
|
77
|
+
function* entities(text, recover) {
|
|
78
78
|
let result = "";
|
|
79
79
|
let offset = 0;
|
|
80
80
|
while (offset < text.length) {
|
|
@@ -83,8 +83,12 @@ function* entities(text) {
|
|
|
83
83
|
return result + text.slice(offset);
|
|
84
84
|
result += text.slice(offset, start);
|
|
85
85
|
const end = yield* find(text, ";", start + 1);
|
|
86
|
-
if (end < 0)
|
|
87
|
-
|
|
86
|
+
if (end < 0) {
|
|
87
|
+
if (!recover)
|
|
88
|
+
invalid("unterminated entity");
|
|
89
|
+
recover("unterminated entity");
|
|
90
|
+
return result + text.slice(start + 1);
|
|
91
|
+
}
|
|
88
92
|
const entity = text.slice(start + 1, end);
|
|
89
93
|
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
90
94
|
if (Object.hasOwn(predefined, entity))
|
|
@@ -92,8 +96,13 @@ function* entities(text) {
|
|
|
92
96
|
else {
|
|
93
97
|
const hexadecimal = entity.startsWith("#x");
|
|
94
98
|
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
95
|
-
if (!entity.startsWith("#") || !digits.length)
|
|
96
|
-
|
|
99
|
+
if (!entity.startsWith("#") || !digits.length) {
|
|
100
|
+
if (!recover)
|
|
101
|
+
invalid("undeclared entity");
|
|
102
|
+
recover("undeclared entity");
|
|
103
|
+
offset = end + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
97
106
|
let point = 0;
|
|
98
107
|
for (let index = 0; index < digits.length; index++) {
|
|
99
108
|
const code = digits.charCodeAt(index);
|
|
@@ -255,27 +264,42 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
255
264
|
throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
256
265
|
};
|
|
257
266
|
let pendingWork = 0;
|
|
258
|
-
const skipWhitespace = ()
|
|
267
|
+
const skipWhitespace = function* () {
|
|
259
268
|
const start = offset;
|
|
260
269
|
while (offset < source.length) {
|
|
261
270
|
const c = source.charCodeAt(offset);
|
|
262
271
|
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
263
272
|
break;
|
|
264
273
|
offset++;
|
|
274
|
+
pendingWork++;
|
|
275
|
+
if (pendingWork >= 512) {
|
|
276
|
+
yield 512;
|
|
277
|
+
pendingWork -= 512;
|
|
278
|
+
}
|
|
265
279
|
}
|
|
266
280
|
return offset - start;
|
|
267
281
|
};
|
|
268
|
-
const scanName = ()
|
|
282
|
+
const scanName = function* () {
|
|
269
283
|
const start = offset;
|
|
270
284
|
while (offset < source.length) {
|
|
271
285
|
const c = source.charCodeAt(offset);
|
|
272
286
|
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
273
287
|
break;
|
|
274
288
|
offset++;
|
|
289
|
+
pendingWork++;
|
|
290
|
+
if (pendingWork >= 512) {
|
|
291
|
+
yield 512;
|
|
292
|
+
pendingWork -= 512;
|
|
293
|
+
}
|
|
275
294
|
}
|
|
276
295
|
const name = source.slice(start, offset);
|
|
296
|
+
pendingWork += offset - start;
|
|
297
|
+
while (pendingWork >= 512) {
|
|
298
|
+
yield 512;
|
|
299
|
+
pendingWork -= 512;
|
|
300
|
+
}
|
|
277
301
|
const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
|
|
278
|
-
return [name, prefix, localName
|
|
302
|
+
return [name, prefix, localName];
|
|
279
303
|
};
|
|
280
304
|
while (offset < source.length) {
|
|
281
305
|
pendingWork += 1;
|
|
@@ -296,7 +320,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
296
320
|
}
|
|
297
321
|
if (text.indexOf("]]>") >= 0)
|
|
298
322
|
invalid("CDATA terminator in text");
|
|
299
|
-
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text)) : text;
|
|
323
|
+
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text, limits.recover)) : text;
|
|
300
324
|
while (pendingWork >= 512) {
|
|
301
325
|
yield 512;
|
|
302
326
|
pendingWork -= 512;
|
|
@@ -313,11 +337,15 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
313
337
|
}
|
|
314
338
|
else {
|
|
315
339
|
for (let index = 0; index < resolved.length; index++) {
|
|
340
|
+
pendingWork++;
|
|
341
|
+
if (pendingWork >= 512) {
|
|
342
|
+
yield 512;
|
|
343
|
+
pendingWork -= 512;
|
|
344
|
+
}
|
|
316
345
|
const c = resolved.charCodeAt(index);
|
|
317
346
|
if (c !== 32 && c !== 9 && c !== 13 && c !== 10)
|
|
318
347
|
invalid("text outside the root");
|
|
319
348
|
}
|
|
320
|
-
pendingWork += resolved.length;
|
|
321
349
|
while (pendingWork >= 512) {
|
|
322
350
|
yield 512;
|
|
323
351
|
pendingWork -= 512;
|
|
@@ -362,8 +390,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
362
390
|
else if (source.startsWith("<?", offset)) {
|
|
363
391
|
const start = offset;
|
|
364
392
|
offset += 2;
|
|
365
|
-
const [target
|
|
366
|
-
pendingWork += wName;
|
|
393
|
+
const [target] = yield* scanName();
|
|
367
394
|
while (pendingWork >= 512) {
|
|
368
395
|
yield 512;
|
|
369
396
|
pendingWork -= 512;
|
|
@@ -406,25 +433,29 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
406
433
|
}
|
|
407
434
|
else if (source.charCodeAt(offset + 1) === 47) {
|
|
408
435
|
offset += 2;
|
|
409
|
-
const [name
|
|
410
|
-
|
|
436
|
+
const [name] = yield* scanName();
|
|
437
|
+
yield* skipWhitespace();
|
|
411
438
|
while (pendingWork >= 512) {
|
|
412
439
|
yield 512;
|
|
413
440
|
pendingWork -= 512;
|
|
414
441
|
}
|
|
415
|
-
if (source[offset++] !== ">" || stack.pop()?.name !== name)
|
|
416
|
-
|
|
442
|
+
if (source[offset++] !== ">" || stack.pop()?.name !== name) {
|
|
443
|
+
if (!limits.recover)
|
|
444
|
+
invalid("mismatched closing tag");
|
|
445
|
+
limits.recover("mismatched closing tag");
|
|
446
|
+
}
|
|
417
447
|
}
|
|
418
448
|
else {
|
|
419
449
|
offset++;
|
|
420
450
|
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
421
|
-
const [name, prefix, localName
|
|
422
|
-
? [repeated.name, repeated.prefix, repeated.localName
|
|
423
|
-
: scanName();
|
|
451
|
+
const [name, prefix, localName] = repeated
|
|
452
|
+
? [repeated.name, repeated.prefix, repeated.localName]
|
|
453
|
+
: (yield* scanName());
|
|
424
454
|
if (repeated)
|
|
425
455
|
offset += name.length;
|
|
426
456
|
// Preserve scan/validation work while reusing the admitted spelling.
|
|
427
|
-
|
|
457
|
+
if (repeated)
|
|
458
|
+
pendingWork += name.length * 2;
|
|
428
459
|
while (pendingWork >= 512) {
|
|
429
460
|
yield 512;
|
|
430
461
|
pendingWork -= 512;
|
|
@@ -434,19 +465,17 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
434
465
|
let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
435
466
|
let ownsNamespaces = stack.length === 0;
|
|
436
467
|
while (!repeated) {
|
|
437
|
-
const ws = skipWhitespace();
|
|
438
|
-
pendingWork += ws;
|
|
468
|
+
const ws = (yield* skipWhitespace());
|
|
439
469
|
while (pendingWork >= 512) {
|
|
440
470
|
yield 512;
|
|
441
471
|
pendingWork -= 512;
|
|
442
472
|
}
|
|
443
473
|
const ch = source.charCodeAt(offset);
|
|
444
|
-
if (ch === 47 || ch === 62)
|
|
474
|
+
if (ch === 47 || ch === 62 || limits.recover && offset === source.length)
|
|
445
475
|
break;
|
|
446
476
|
if (ws === 0)
|
|
447
477
|
invalid("attributes require whitespace");
|
|
448
|
-
const [attribute, attrPrefix, attrLocal
|
|
449
|
-
pendingWork += wAttr;
|
|
478
|
+
const [attribute, attrPrefix, attrLocal] = yield* scanName();
|
|
450
479
|
while (pendingWork >= 512) {
|
|
451
480
|
yield 512;
|
|
452
481
|
pendingWork -= 512;
|
|
@@ -457,10 +486,10 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
457
486
|
throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
458
487
|
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
459
488
|
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
460
|
-
|
|
489
|
+
yield* skipWhitespace();
|
|
461
490
|
if (source[offset++] !== "=")
|
|
462
491
|
invalid("missing attribute equals");
|
|
463
|
-
|
|
492
|
+
yield* skipWhitespace();
|
|
464
493
|
const quote = source[offset++];
|
|
465
494
|
if (quote !== '"' && quote !== "'")
|
|
466
495
|
invalid("unquoted attribute");
|
|
@@ -482,7 +511,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
482
511
|
if (raw.indexOf("<") >= 0)
|
|
483
512
|
invalid("less-than in attribute");
|
|
484
513
|
const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
|
|
485
|
-
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized);
|
|
514
|
+
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
|
|
486
515
|
while (pendingWork >= 512) {
|
|
487
516
|
yield 512;
|
|
488
517
|
pendingWork -= 512;
|
|
@@ -571,8 +600,11 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
571
600
|
const empty = source[offset] === "/";
|
|
572
601
|
if (empty)
|
|
573
602
|
offset++;
|
|
574
|
-
if (source[offset++] !== ">")
|
|
575
|
-
|
|
603
|
+
if (source[offset++] !== ">") {
|
|
604
|
+
if (!limits.recover)
|
|
605
|
+
invalid("unterminated start tag");
|
|
606
|
+
limits.recover("unterminated start tag");
|
|
607
|
+
}
|
|
576
608
|
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
577
609
|
// every admission counter still run for each distinct physical element.
|
|
578
610
|
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
@@ -585,8 +617,13 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
585
617
|
yield pendingWork;
|
|
586
618
|
pendingWork = 0;
|
|
587
619
|
}
|
|
588
|
-
if (
|
|
620
|
+
if (!root)
|
|
589
621
|
invalid("incomplete document");
|
|
622
|
+
if (stack.length) {
|
|
623
|
+
if (!limits.recover)
|
|
624
|
+
invalid("incomplete document");
|
|
625
|
+
limits.recover("incomplete document");
|
|
626
|
+
}
|
|
590
627
|
return root;
|
|
591
628
|
}
|
|
592
629
|
export function parseXml(input, limits = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-platform/safe-fs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.801",
|
|
4
4
|
"description": "Composable filesystem with a portable core and explicit Node adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -133,9 +133,16 @@
|
|
|
133
133
|
"access": "public"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@types/node": "^25.2.2"
|
|
136
|
+
"@types/node": "^25.2.2",
|
|
137
|
+
"@noble/hashes": "2.4.0"
|
|
137
138
|
},
|
|
138
139
|
"imports": {
|
|
140
|
+
"#safe-fs-s3-request": {
|
|
141
|
+
"types": "./dist/safe-fs/fs/s3/http/request-node.d.ts",
|
|
142
|
+
"workerd": "./dist/safe-fs/fs/s3/http/request-fetch.js",
|
|
143
|
+
"browser": "./dist/safe-fs/fs/s3/http/request-fetch.js",
|
|
144
|
+
"default": "./dist/safe-fs/fs/s3/http/request-node.js"
|
|
145
|
+
},
|
|
139
146
|
"#safe-fs-platform": {
|
|
140
147
|
"types": {
|
|
141
148
|
"workerd": "./dist/safe-fs/platform/browser.d.ts",
|