@poe-platform/safe-fs 0.1.506 → 0.1.508

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.
@@ -13,3 +13,5 @@ export { scopeFileSystem } from "./fs/scoped.js";
13
13
  export * from "./fs/webdav/index.js";
14
14
  export * from "./bridge/index.js";
15
15
  export { compareEntries } from "./fs/mount/comparison.js";
16
+ export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
17
+ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
@@ -13,3 +13,4 @@ export { scopeFileSystem } from "./fs/scoped.js";
13
13
  export * from "./fs/webdav/index.js";
14
14
  export * from "./bridge/index.js";
15
15
  export { compareEntries } from "./fs/mount/comparison.js";
16
+ export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
@@ -1,13 +1,7 @@
1
- export interface XmlElement {
2
- readonly namespace: string;
3
- readonly localName: string;
4
- readonly children: XmlElement[];
5
- text: string;
6
- }
7
- export interface XmlLimits {
8
- readonly maxDepth?: number;
9
- readonly maxNodes?: number;
10
- readonly maxAttributes?: number;
1
+ import { type XmlLimits as DocumentLimits } from "../../xml.js";
2
+ import type { XmlElement } from "../../xml.js";
3
+ export type { XmlElement } from "../../xml.js";
4
+ export interface XmlLimits extends Omit<DocumentLimits, "onElement"> {
11
5
  readonly maxResponses?: number;
12
6
  }
13
7
  export declare class XmlResponseLimitError extends SyntaxError {
@@ -1,248 +1,31 @@
1
+ import { parseXml as parseDocument, XmlLimitError } from "../../xml.js";
1
2
  export class XmlResponseLimitError extends SyntaxError {
2
3
  }
3
- const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
4
- const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
5
- function invalid(message) {
6
- throw new SyntaxError(`Invalid WebDAV XML: ${message}`);
7
- }
8
- function validCharacter(point) {
9
- return point === 9 || point === 10 || point === 13
10
- || (point >= 0x20 && point <= 0xd7ff)
11
- || (point >= 0xe000 && point <= 0xfffd)
12
- || (point >= 0x10000 && point <= 0x10ffff);
13
- }
14
- function nameStart(point) {
15
- return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
16
- || (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
17
- || (point >= 0xf8 && point <= 0x2ff) || (point >= 0x370 && point <= 0x37d)
18
- || (point >= 0x37f && point <= 0x1fff) || (point >= 0x200c && point <= 0x200d)
19
- || (point >= 0x2070 && point <= 0x218f) || (point >= 0x2c00 && point <= 0x2fef)
20
- || (point >= 0x3001 && point <= 0xd7ff) || (point >= 0xf900 && point <= 0xfdcf)
21
- || (point >= 0xfdf0 && point <= 0xfffd) || (point >= 0x10000 && point <= 0xeffff);
22
- }
23
- function namePart(point) {
24
- return nameStart(point) || point === 45 || point === 46 || point === 0xb7
25
- || (point >= 48 && point <= 57) || (point >= 0x300 && point <= 0x36f)
26
- || (point >= 0x203f && point <= 0x2040);
27
- }
28
- function qualifiedName(name) {
29
- const parts = name.split(":");
30
- if (parts.length > 2 || parts.some((part) => {
31
- const points = [...part].map((character) => character.codePointAt(0));
32
- return points.length === 0 || !nameStart(points[0]) || points.slice(1).some((point) => !namePart(point));
33
- }))
34
- invalid("invalid qualified name");
35
- return parts.length === 1 ? ["", parts[0]] : [parts[0], parts[1]];
36
- }
37
- function entities(text) {
38
- let result = "";
39
- let offset = 0;
40
- while (offset < text.length) {
41
- const start = text.indexOf("&", offset);
42
- if (start < 0)
43
- return result + text.slice(offset);
44
- result += text.slice(offset, start);
45
- const end = text.indexOf(";", start + 1);
46
- if (end < 0)
47
- invalid("unterminated entity");
48
- const entity = text.slice(start + 1, end);
49
- const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
50
- if (Object.hasOwn(predefined, entity))
51
- result += predefined[entity];
52
- else {
53
- const hexadecimal = entity.startsWith("#x");
54
- const digits = entity.slice(hexadecimal ? 2 : 1);
55
- if (!entity.startsWith("#") || !(hexadecimal ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits)) {
56
- invalid("undeclared entity");
57
- }
58
- const point = Number.parseInt(digits, hexadecimal ? 16 : 10);
59
- if (!validCharacter(point))
60
- invalid("invalid character reference");
61
- result += String.fromCodePoint(point);
62
- }
63
- offset = end + 1;
64
- }
65
- return result;
66
- }
4
+ function invalid(message) { throw new SyntaxError(`Invalid WebDAV XML: ${message}`); }
67
5
  export function parseXml(input, limits = {}) {
68
- const maxDepth = limits.maxDepth ?? 64;
69
- const maxNodes = limits.maxNodes ?? 100_000;
70
- const maxAttributes = limits.maxAttributes ?? 10_000;
71
- for (const limit of [maxDepth, maxNodes, maxAttributes, ...(limits.maxResponses === undefined ? [] : [limits.maxResponses])]) {
72
- if (!Number.isSafeInteger(limit) || limit < 1)
73
- throw new RangeError("XML limits must be positive integers");
6
+ const { maxResponses, ...documentLimits } = limits;
7
+ if (maxResponses !== undefined && (!Number.isSafeInteger(maxResponses) || maxResponses < 1)) {
8
+ throw new RangeError("XML limits must be positive integers");
74
9
  }
75
- for (const character of input) {
76
- if (!validCharacter(character.codePointAt(0)))
77
- invalid("invalid character");
78
- }
79
- const source = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
80
- const stack = [];
81
- let root;
82
- let offset = 0;
83
- let nodes = 0;
84
- let attributeCount = 0;
85
10
  let responses = 0;
86
- const whitespace = () => {
87
- while (offset < source.length && " \t\n\r".includes(source[offset]))
88
- offset++;
89
- };
90
- const readName = () => {
91
- const start = offset;
92
- while (offset < source.length && !" \t\r\n/=>?".includes(source[offset]))
93
- offset++;
94
- const name = source.slice(start, offset);
95
- qualifiedName(name);
96
- return name;
97
- };
98
- const appendText = (text) => {
99
- const parent = stack.at(-1);
100
- if (parent)
101
- parent.element.text += text;
102
- else if (!/^[ \t\r\n]*$/.test(text))
103
- invalid("text outside the root");
104
- };
105
- while (offset < source.length) {
106
- if (source[offset] !== "<") {
107
- const next = source.indexOf("<", offset);
108
- const text = source.slice(offset, next < 0 ? source.length : next);
109
- if (text.includes("]]>"))
110
- invalid("CDATA terminator in text");
111
- appendText(entities(text));
112
- offset += text.length;
113
- }
114
- else if (source.startsWith("<!--", offset)) {
115
- const end = source.indexOf("-->", offset + 4);
116
- if (end < 0 || source.slice(offset + 4, end).includes("--") || source.slice(offset + 4, end).endsWith("-")) {
117
- invalid("malformed comment");
118
- }
119
- offset = end + 3;
120
- }
121
- else if (source.startsWith("<![CDATA[", offset)) {
122
- if (!stack.length)
123
- invalid("CDATA outside root");
124
- const end = source.indexOf("]]>", offset + 9);
125
- if (end < 0)
126
- invalid("unterminated CDATA");
127
- appendText(source.slice(offset + 9, end));
128
- offset = end + 3;
129
- }
130
- else if (source.startsWith("<?", offset)) {
131
- const start = offset;
132
- offset += 2;
133
- const target = readName();
134
- const end = source.indexOf("?>", offset);
135
- if (end < 0)
136
- invalid("unterminated processing instruction");
137
- const content = source.slice(offset, end);
138
- if (target.toLowerCase() === "xml") {
139
- if (start !== 0 || target !== "xml"
140
- || !/^\s+version\s*=\s*(['"])1\.0\1(?:\s+encoding\s*=\s*(['"])(?:UTF-8|UTF-16|UTF-16LE|UTF-16BE)\2)?(?:\s+standalone\s*=\s*(['"])(?:yes|no)\3)?\s*$/i.test(content)) {
141
- invalid("unsupported XML declaration");
11
+ try {
12
+ return parseDocument(input, { ...documentLimits, retainContent: false,
13
+ onElement(element, parent, depth) {
14
+ if (maxResponses !== undefined && depth === 2 && parent?.namespace === "DAV:" && parent.localName === "multistatus"
15
+ && element.namespace === "DAV:" && element.localName === "response" && ++responses > maxResponses) {
16
+ throw new XmlResponseLimitError("WebDAV XML response limit exceeded");
142
17
  }
143
18
  }
144
- else if (content && !" \t\n\r".includes(content[0]))
145
- invalid("invalid processing instruction");
146
- offset = end + 2;
147
- }
148
- else if (source.startsWith("<!", offset)) {
149
- invalid("DTD and entity declarations are forbidden");
150
- }
151
- else if (source.startsWith("</", offset)) {
152
- offset += 2;
153
- const name = readName();
154
- whitespace();
155
- if (source[offset++] !== ">" || stack.pop()?.name !== name)
156
- invalid("mismatched closing tag");
157
- }
158
- else {
159
- offset++;
160
- const name = readName();
161
- const attributes = new Map();
162
- const namespaces = new Map(stack.at(-1)?.namespaces ?? [["xml", xmlNamespace]]);
163
- while (true) {
164
- const beforeSpace = offset;
165
- whitespace();
166
- if (source[offset] === "/" || source[offset] === ">")
167
- break;
168
- if (offset === beforeSpace)
169
- invalid("attributes require whitespace");
170
- const attribute = readName();
171
- if (attributes.has(attribute))
172
- invalid("duplicate attribute");
173
- if (++attributeCount > maxAttributes || attributes.size >= 128)
174
- invalid("XML attribute limit exceeded");
175
- whitespace();
176
- if (source[offset++] !== "=")
177
- invalid("missing attribute equals");
178
- whitespace();
179
- const quote = source[offset++];
180
- if (quote !== '"' && quote !== "'")
181
- invalid("unquoted attribute");
182
- const end = source.indexOf(quote, offset);
183
- if (end < 0)
184
- invalid("unterminated attribute");
185
- const raw = source.slice(offset, end);
186
- if (raw.includes("<"))
187
- invalid("less-than in attribute");
188
- const value = entities(raw.replace(/[\t\n\r]/g, " "));
189
- attributes.set(attribute, value);
190
- offset = end + 1;
191
- if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
192
- const prefix = attribute === "xmlns" ? "" : attribute.slice(6);
193
- if (prefix === "xmlns" || value === xmlnsNamespace
194
- || (prefix === "xml") !== (value === xmlNamespace)
195
- || (prefix !== "" && value === ""))
196
- invalid("invalid namespace binding");
197
- namespaces.set(prefix, value);
198
- if (namespaces.size > 256)
199
- invalid("XML namespace scope limit exceeded");
200
- }
201
- }
202
- const expanded = new Set();
203
- for (const attribute of attributes.keys()) {
204
- if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
205
- continue;
206
- const [prefix, localName] = qualifiedName(attribute);
207
- if (prefix && !namespaces.has(prefix))
208
- invalid("unbound attribute prefix");
209
- const key = JSON.stringify([prefix ? namespaces.get(prefix) : "", localName]);
210
- if (expanded.has(key))
211
- invalid("duplicate expanded attribute");
212
- expanded.add(key);
213
- }
214
- const [prefix, localName] = qualifiedName(name);
215
- if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
216
- invalid("unbound element prefix");
217
- if (++nodes > maxNodes || stack.length + 1 > maxDepth)
218
- invalid("XML resource limit exceeded");
219
- const namespace = namespaces.get(prefix) ?? "";
220
- if (limits.maxResponses !== undefined && stack.length === 1
221
- && root?.namespace === "DAV:" && root.localName === "multistatus"
222
- && namespace === "DAV:" && localName === "response"
223
- && ++responses > limits.maxResponses) {
224
- throw new XmlResponseLimitError("WebDAV XML response limit exceeded");
225
- }
226
- const element = { namespace, localName, children: [], text: "" };
227
- const parent = stack.at(-1);
228
- if (parent)
229
- parent.element.children.push(element);
230
- else if (root)
231
- invalid("multiple root elements");
232
- else
233
- root = element;
234
- const empty = source[offset] === "/";
235
- if (empty)
236
- offset++;
237
- if (source[offset++] !== ">")
238
- invalid("unterminated start tag");
239
- if (!empty)
240
- stack.push({ element, name, namespaces });
19
+ });
20
+ }
21
+ catch (error) {
22
+ if (error instanceof XmlLimitError)
23
+ invalid(error.message);
24
+ if (error instanceof SyntaxError && error.message.startsWith("Invalid XML:")) {
25
+ error.message = error.message.replace("Invalid XML:", "Invalid WebDAV XML:");
241
26
  }
27
+ throw error;
242
28
  }
243
- if (stack.length || !root)
244
- invalid("incomplete document");
245
- return root;
246
29
  }
247
30
  export function davChildren(element, localName) {
248
31
  return element.children.filter((child) => child.namespace === "DAV:" && child.localName === localName);
@@ -0,0 +1,42 @@
1
+ export interface XmlName {
2
+ readonly name: string;
3
+ readonly namespace: string;
4
+ readonly localName: string;
5
+ }
6
+ export interface XmlAttribute extends XmlName {
7
+ readonly value: string;
8
+ }
9
+ export type XmlContent = XmlElement | {
10
+ readonly kind: "text" | "cdata" | "comment";
11
+ readonly text: string;
12
+ } | {
13
+ readonly kind: "processing-instruction";
14
+ readonly target: string;
15
+ readonly text: string;
16
+ };
17
+ export interface XmlElement extends XmlName {
18
+ readonly kind: "element";
19
+ readonly children: XmlElement[];
20
+ readonly content: readonly XmlContent[];
21
+ readonly attributes: readonly XmlAttribute[];
22
+ readonly namespaces: ReadonlyMap<string, string>;
23
+ text: string;
24
+ readonly declaration?: string;
25
+ }
26
+ export interface XmlLimits {
27
+ readonly expectedEncoding?: "UTF-8" | "UTF-16" | "UTF-16LE" | "UTF-16BE";
28
+ readonly retainContent?: boolean;
29
+ readonly maxDepth?: number;
30
+ readonly maxNodes?: number;
31
+ readonly maxAttributes?: number;
32
+ readonly maxAttributesPerElement?: number;
33
+ readonly maxNamespaces?: number;
34
+ readonly maxContentNodes?: number;
35
+ readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
36
+ }
37
+ export declare class XmlLimitError extends SyntaxError {
38
+ readonly limit: string;
39
+ constructor(limit: string, message: string);
40
+ }
41
+ export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
42
+ export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
@@ -0,0 +1,484 @@
1
+ export class XmlLimitError extends SyntaxError {
2
+ limit;
3
+ constructor(limit, message) {
4
+ super(message);
5
+ this.limit = limit;
6
+ }
7
+ }
8
+ function* find(source, needle, start) {
9
+ let work = 0;
10
+ for (let offset = start; offset < source.length; offset++) {
11
+ if (source.startsWith(needle, offset)) {
12
+ if (work)
13
+ yield work;
14
+ return offset;
15
+ }
16
+ if (++work === 512) {
17
+ yield work;
18
+ work = 0;
19
+ }
20
+ }
21
+ if (work)
22
+ yield work;
23
+ return -1;
24
+ }
25
+ const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
26
+ const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
27
+ function invalid(message) {
28
+ throw new SyntaxError(`Invalid XML: ${message}`);
29
+ }
30
+ function validCharacter(point) {
31
+ return point === 9 || point === 10 || point === 13
32
+ || (point >= 0x20 && point <= 0xd7ff)
33
+ || (point >= 0xe000 && point <= 0xfffd)
34
+ || (point >= 0x10000 && point <= 0x10ffff);
35
+ }
36
+ function nameStart(point) {
37
+ return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
38
+ || (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
39
+ || (point >= 0xf8 && point <= 0x2ff) || (point >= 0x370 && point <= 0x37d)
40
+ || (point >= 0x37f && point <= 0x1fff) || (point >= 0x200c && point <= 0x200d)
41
+ || (point >= 0x2070 && point <= 0x218f) || (point >= 0x2c00 && point <= 0x2fef)
42
+ || (point >= 0x3001 && point <= 0xd7ff) || (point >= 0xf900 && point <= 0xfdcf)
43
+ || (point >= 0xfdf0 && point <= 0xfffd) || (point >= 0x10000 && point <= 0xeffff);
44
+ }
45
+ function namePart(point) {
46
+ return nameStart(point) || point === 45 || point === 46 || point === 0xb7
47
+ || (point >= 48 && point <= 57) || (point >= 0x300 && point <= 0x36f)
48
+ || (point >= 0x203f && point <= 0x2040);
49
+ }
50
+ function* qualifiedName(name) {
51
+ let prefix = "";
52
+ let start = 0;
53
+ let first = true;
54
+ let work = 0;
55
+ for (let offset = 0; offset < name.length;) {
56
+ const point = name.codePointAt(offset);
57
+ if (point === 58) {
58
+ if (start !== 0 || first)
59
+ invalid("invalid qualified name");
60
+ prefix = name.slice(0, offset);
61
+ start = offset + 1;
62
+ first = true;
63
+ }
64
+ else {
65
+ if (!(first ? nameStart(point) : namePart(point)))
66
+ invalid("invalid qualified name");
67
+ first = false;
68
+ }
69
+ const width = point > 0xffff ? 2 : 1;
70
+ offset += width;
71
+ work += width;
72
+ if (work >= 512) {
73
+ yield work;
74
+ work = 0;
75
+ }
76
+ }
77
+ if (first)
78
+ invalid("invalid qualified name");
79
+ if (work)
80
+ yield work;
81
+ return [prefix, name.slice(start)];
82
+ }
83
+ function* entities(text) {
84
+ let result = "";
85
+ let offset = 0;
86
+ while (offset < text.length) {
87
+ const start = yield* find(text, "&", offset);
88
+ if (start < 0)
89
+ return result + text.slice(offset);
90
+ result += text.slice(offset, start);
91
+ const end = yield* find(text, ";", start + 1);
92
+ if (end < 0)
93
+ invalid("unterminated entity");
94
+ const entity = text.slice(start + 1, end);
95
+ const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
96
+ if (Object.hasOwn(predefined, entity))
97
+ result += predefined[entity];
98
+ else {
99
+ const hexadecimal = entity.startsWith("#x");
100
+ const digits = entity.slice(hexadecimal ? 2 : 1);
101
+ if (!entity.startsWith("#") || !digits.length)
102
+ invalid("undeclared entity");
103
+ let point = 0;
104
+ for (let index = 0; index < digits.length; index++) {
105
+ const code = digits.charCodeAt(index);
106
+ const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
107
+ : hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
108
+ if (digit < 0)
109
+ invalid("undeclared entity");
110
+ point = point * (hexadecimal ? 16 : 10) + digit;
111
+ if (point > 0x10ffff)
112
+ invalid("invalid character reference");
113
+ if ((index + 1) % 512 === 0)
114
+ yield 512;
115
+ }
116
+ if (digits.length % 512)
117
+ yield digits.length % 512;
118
+ if (!validCharacter(point))
119
+ invalid("invalid character reference");
120
+ result += String.fromCodePoint(point);
121
+ }
122
+ offset = end + 1;
123
+ }
124
+ return result;
125
+ }
126
+ function* validDeclaration(content, expectedEncoding) {
127
+ let offset = 0;
128
+ const whitespace = function* () {
129
+ const start = offset;
130
+ while (offset < content.length && " \t\n\r".includes(content[offset])) {
131
+ offset++;
132
+ if ((offset - start) % 512 === 0)
133
+ yield 512;
134
+ }
135
+ if ((offset - start) % 512)
136
+ yield (offset - start) % 512;
137
+ return offset - start;
138
+ };
139
+ const field = function* (name) {
140
+ if (content.slice(offset, offset + name.length).toLowerCase() !== name)
141
+ return undefined;
142
+ offset += name.length;
143
+ yield name.length;
144
+ yield* whitespace();
145
+ if (content[offset++] !== "=")
146
+ return undefined;
147
+ yield* whitespace();
148
+ const quote = content[offset++];
149
+ if (quote !== "'" && quote !== '"')
150
+ return undefined;
151
+ const start = offset;
152
+ while (offset < content.length && content[offset] !== quote) {
153
+ offset++;
154
+ if ((offset - start) % 512 === 0)
155
+ yield 512;
156
+ }
157
+ if ((offset - start) % 512)
158
+ yield (offset - start) % 512;
159
+ if (offset >= content.length)
160
+ return undefined;
161
+ return content.slice(start, offset++);
162
+ };
163
+ if (!(yield* whitespace()) || (yield* field("version")) !== "1.0")
164
+ return false;
165
+ let spacing = yield* whitespace();
166
+ if (content.slice(offset, offset + 8).toLowerCase() === "encoding") {
167
+ if (!spacing)
168
+ return false;
169
+ const encoding = yield* field("encoding");
170
+ if (encoding === undefined || encoding.length > 8 || !["utf-8", "utf-16", "utf-16le", "utf-16be"].includes(encoding.toLowerCase()))
171
+ return false;
172
+ if (expectedEncoding !== undefined && encoding.toLowerCase() !== expectedEncoding.toLowerCase())
173
+ return false;
174
+ spacing = yield* whitespace();
175
+ }
176
+ if (content.slice(offset, offset + 10).toLowerCase() === "standalone") {
177
+ if (!spacing)
178
+ return false;
179
+ const standalone = yield* field("standalone");
180
+ if (standalone === undefined || standalone.length > 3 || !["yes", "no"].includes(standalone.toLowerCase()))
181
+ return false;
182
+ yield* whitespace();
183
+ }
184
+ return offset === content.length;
185
+ }
186
+ export function* parseXmlSteps(input, limits = {}) {
187
+ const maxDepth = limits.maxDepth ?? 64;
188
+ const maxNodes = limits.maxNodes ?? 100_000;
189
+ const maxAttributes = limits.maxAttributes ?? 10_000;
190
+ const retainContent = limits.retainContent !== false;
191
+ const maxContentNodes = limits.maxContentNodes ?? (retainContent ? 100_000 : maxNodes);
192
+ const emptyContent = Object.freeze([]);
193
+ const emptyAttributes = Object.freeze([]);
194
+ const emptyNamespaces = new Map();
195
+ const maxAttributesPerElement = limits.maxAttributesPerElement ?? 128;
196
+ const maxNamespaces = limits.maxNamespaces ?? 256;
197
+ for (const limit of [maxDepth, maxNodes, maxAttributes, maxContentNodes, maxAttributesPerElement, maxNamespaces]) {
198
+ if (!Number.isSafeInteger(limit) || limit < 1)
199
+ throw new RangeError("XML limits must be positive integers");
200
+ }
201
+ const chunks = [];
202
+ let chunk = "";
203
+ for (let index = input.charCodeAt(0) === 0xfeff ? 1 : 0; index < input.length; index++) {
204
+ const point = input.codePointAt(index);
205
+ if (!validCharacter(point))
206
+ invalid("invalid character");
207
+ if (point === 13) {
208
+ chunk += "\n";
209
+ if (input.charCodeAt(index + 1) === 10)
210
+ index++;
211
+ }
212
+ else {
213
+ chunk += String.fromCodePoint(point);
214
+ if (point > 0xffff)
215
+ index++;
216
+ }
217
+ if (chunk.length >= 512) {
218
+ chunks.push(chunk);
219
+ chunk = "";
220
+ yield 512;
221
+ }
222
+ }
223
+ chunks.push(chunk);
224
+ if (chunk.length)
225
+ yield chunk.length;
226
+ const source = chunks.join("");
227
+ const stack = [];
228
+ let root;
229
+ let declaration;
230
+ let offset = 0;
231
+ let nodes = 0;
232
+ let attributeCount = 0;
233
+ let contentNodes = 0;
234
+ const admitContent = () => {
235
+ if (++contentNodes > maxContentNodes)
236
+ throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
237
+ };
238
+ const whitespace = function* () {
239
+ let work = 0;
240
+ while (offset < source.length && " \t\n\r".includes(source[offset])) {
241
+ offset++;
242
+ if (++work === 512) {
243
+ yield work;
244
+ work = 0;
245
+ }
246
+ }
247
+ if (work)
248
+ yield work;
249
+ };
250
+ const readName = function* () {
251
+ const start = offset;
252
+ while (offset < source.length && !" \t\r\n/=>?".includes(source[offset])) {
253
+ offset++;
254
+ if ((offset - start) % 512 === 0)
255
+ yield 512;
256
+ }
257
+ if ((offset - start) % 512)
258
+ yield (offset - start) % 512;
259
+ const name = source.slice(start, offset);
260
+ yield* qualifiedName(name);
261
+ return name;
262
+ };
263
+ const appendText = function* (text, kind = "text") {
264
+ const parent = stack.at(-1);
265
+ if (parent) {
266
+ parent.element.text += text;
267
+ if (retainContent && (text.length || kind === "cdata")) {
268
+ admitContent();
269
+ parent.content.push({ kind, text });
270
+ }
271
+ }
272
+ else {
273
+ for (let index = 0; index < text.length; index++) {
274
+ if (!" \t\r\n".includes(text[index]))
275
+ invalid("text outside the root");
276
+ if ((index + 1) % 512 === 0)
277
+ yield 512;
278
+ }
279
+ if (text.length % 512)
280
+ yield text.length % 512;
281
+ }
282
+ };
283
+ while (offset < source.length) {
284
+ yield 1;
285
+ if (source[offset] !== "<") {
286
+ const next = yield* find(source, "<", offset);
287
+ const text = source.slice(offset, next < 0 ? source.length : next);
288
+ if ((yield* find(text, "]]>", 0)) >= 0)
289
+ invalid("CDATA terminator in text");
290
+ yield* appendText(yield* entities(text));
291
+ offset += text.length;
292
+ }
293
+ else if (source.startsWith("<!--", offset)) {
294
+ const end = yield* find(source, "-->", offset + 4);
295
+ if (end < 0 || (yield* find(source.slice(offset + 4, end), "--", 0)) >= 0 || source.slice(offset + 4, end).endsWith("-")) {
296
+ invalid("malformed comment");
297
+ }
298
+ const parent = stack.at(-1);
299
+ if (retainContent && parent) {
300
+ admitContent();
301
+ parent.content.push({ kind: "comment", text: source.slice(offset + 4, end) });
302
+ }
303
+ offset = end + 3;
304
+ }
305
+ else if (source.startsWith("<![CDATA[", offset)) {
306
+ if (!stack.length)
307
+ invalid("CDATA outside root");
308
+ const end = yield* find(source, "]]>", offset + 9);
309
+ if (end < 0)
310
+ invalid("unterminated CDATA");
311
+ yield* appendText(source.slice(offset + 9, end), "cdata");
312
+ offset = end + 3;
313
+ }
314
+ else if (source.startsWith("<?", offset)) {
315
+ const start = offset;
316
+ offset += 2;
317
+ const target = yield* readName();
318
+ const end = yield* find(source, "?>", offset);
319
+ if (end < 0)
320
+ invalid("unterminated processing instruction");
321
+ const content = source.slice(offset, end);
322
+ if (target.length === 3 && target.toLowerCase() === "xml") {
323
+ if (start !== 0 || target !== "xml"
324
+ || !(yield* validDeclaration(content, limits.expectedEncoding))) {
325
+ invalid("unsupported XML declaration");
326
+ }
327
+ if (retainContent)
328
+ declaration = source.slice(start, end + 2);
329
+ }
330
+ else if (content && !" \t\n\r".includes(content[0]))
331
+ invalid("invalid processing instruction");
332
+ if (!(target.length === 3 && target.toLowerCase() === "xml")) {
333
+ const parent = stack.at(-1);
334
+ if (retainContent && parent) {
335
+ let start = 0;
336
+ while (start < content.length && " \t\n\r".includes(content[start])) {
337
+ start++;
338
+ if (start % 512 === 0)
339
+ yield 512;
340
+ }
341
+ if (start % 512)
342
+ yield start % 512;
343
+ admitContent();
344
+ parent.content.push({ kind: "processing-instruction", target, text: content.slice(start) });
345
+ }
346
+ }
347
+ offset = end + 2;
348
+ }
349
+ else if (source.startsWith("<!", offset)) {
350
+ invalid("DTD and entity declarations are forbidden");
351
+ }
352
+ else if (source.startsWith("</", offset)) {
353
+ offset += 2;
354
+ const name = yield* readName();
355
+ yield* whitespace();
356
+ if (source[offset++] !== ">" || stack.pop()?.name !== name)
357
+ invalid("mismatched closing tag");
358
+ }
359
+ else {
360
+ offset++;
361
+ const name = yield* readName();
362
+ const attributes = new Map();
363
+ let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
364
+ let ownsNamespaces = stack.length === 0;
365
+ while (true) {
366
+ const beforeSpace = offset;
367
+ yield* whitespace();
368
+ if (source[offset] === "/" || source[offset] === ">")
369
+ break;
370
+ if (offset === beforeSpace)
371
+ invalid("attributes require whitespace");
372
+ const attribute = yield* readName();
373
+ if (attributes.has(attribute))
374
+ invalid("duplicate attribute");
375
+ if (++attributeCount > maxAttributes)
376
+ throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
377
+ if (attributes.size >= maxAttributesPerElement)
378
+ throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
379
+ yield* whitespace();
380
+ if (source[offset++] !== "=")
381
+ invalid("missing attribute equals");
382
+ yield* whitespace();
383
+ const quote = source[offset++];
384
+ if (quote !== '"' && quote !== "'")
385
+ invalid("unquoted attribute");
386
+ const end = yield* find(source, quote, offset);
387
+ if (end < 0)
388
+ invalid("unterminated attribute");
389
+ const raw = source.slice(offset, end);
390
+ if ((yield* find(raw, "<", 0)) >= 0)
391
+ invalid("less-than in attribute");
392
+ let normalized = "";
393
+ for (let index = 0; index < raw.length; index++) {
394
+ const character = raw[index];
395
+ normalized += character === "\t" || character === "\n" || character === "\r" ? " " : character;
396
+ if ((index + 1) % 512 === 0)
397
+ yield 512;
398
+ }
399
+ if (raw.length % 512)
400
+ yield raw.length % 512;
401
+ const value = yield* entities(normalized);
402
+ attributes.set(attribute, value);
403
+ offset = end + 1;
404
+ if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
405
+ const prefix = attribute === "xmlns" ? "" : attribute.slice(6);
406
+ if (prefix === "xmlns" || value === xmlnsNamespace
407
+ || (prefix === "xml") !== (value === xmlNamespace)
408
+ || (prefix !== "" && value === ""))
409
+ invalid("invalid namespace binding");
410
+ if (!ownsNamespaces) {
411
+ const copy = new Map();
412
+ for (const [key, uri] of namespaces) {
413
+ copy.set(key, uri);
414
+ yield 1;
415
+ }
416
+ namespaces = copy;
417
+ ownsNamespaces = true;
418
+ }
419
+ namespaces.set(prefix, value);
420
+ if (namespaces.size > maxNamespaces)
421
+ throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
422
+ }
423
+ }
424
+ const expanded = new Set();
425
+ for (const attribute of attributes.keys()) {
426
+ if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
427
+ continue;
428
+ const [prefix, localName] = yield* qualifiedName(attribute);
429
+ if (prefix && !namespaces.has(prefix))
430
+ invalid("unbound attribute prefix");
431
+ const key = JSON.stringify([prefix ? namespaces.get(prefix) : "", localName]);
432
+ if (expanded.has(key))
433
+ invalid("duplicate expanded attribute");
434
+ expanded.add(key);
435
+ }
436
+ const [prefix, localName] = yield* qualifiedName(name);
437
+ if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
438
+ invalid("unbound element prefix");
439
+ if (++nodes > maxNodes)
440
+ throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
441
+ if (stack.length + 1 > maxDepth)
442
+ throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
443
+ const namespace = namespaces.get(prefix) ?? "";
444
+ limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
445
+ admitContent();
446
+ const retainedAttributes = [];
447
+ for (const [attribute, value] of retainContent ? attributes : []) {
448
+ const [prefix, localName] = yield* qualifiedName(attribute);
449
+ admitContent();
450
+ retainedAttributes.push({ name: attribute, localName,
451
+ namespace: attribute === "xmlns" || prefix === "xmlns" ? xmlnsNamespace : prefix ? namespaces.get(prefix) : "", value });
452
+ yield 1;
453
+ }
454
+ const content = retainContent ? [] : undefined;
455
+ const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...(root === undefined && declaration !== undefined ? { declaration } : {}) };
456
+ const parent = stack.at(-1);
457
+ if (parent) {
458
+ parent.element.children.push(element);
459
+ parent.content?.push(element);
460
+ }
461
+ else if (root)
462
+ invalid("multiple root elements");
463
+ else
464
+ root = element;
465
+ const empty = source[offset] === "/";
466
+ if (empty)
467
+ offset++;
468
+ if (source[offset++] !== ">")
469
+ invalid("unterminated start tag");
470
+ if (!empty)
471
+ stack.push({ element, content, name, namespaces });
472
+ }
473
+ }
474
+ if (stack.length || !root)
475
+ invalid("incomplete document");
476
+ return root;
477
+ }
478
+ export function parseXml(input, limits = {}) {
479
+ const parser = parseXmlSteps(input, limits);
480
+ let result = parser.next();
481
+ while (!result.done)
482
+ result = parser.next();
483
+ return result.value;
484
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.506",
3
+ "version": "0.1.508",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",