@poe-platform/safe-fs 0.1.724 → 0.1.725

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 CHANGED
@@ -339,6 +339,8 @@ Each batch lists from the beginning because previous keys have been deleted; it
339
339
 
340
340
  Known identity Content-Length responses use one result buffer; other responses grow storage up to the configured ceiling and return a view without a final copy. Growth can temporarily retain the old and new buffers (up to three times the response size), plus transport chunks. These per-response defaults leave headroom in Workers; hosts must still budget for metadata parsing, text decoding, transport buffers, and concurrent reads, especially when raising the limits. Use streaming reads for large files.
341
341
 
342
+ WebDAV metadata parsing limits the document to 100,000 elements, 100,000 content nodes, 100,000 attributes, and 256 levels of nesting, independently of `maxEntries`. Text is bounded by `maxXmlBytes`. Exceeding a structural budget reports `EFBIG`; parsing yields cooperatively so caller cancellation and `timeoutMs` remain active.
343
+
342
344
  See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic directory removal. A recursive WebDAV DELETE does not satisfy that contract.
343
345
 
344
346
  </details>
@@ -1,7 +1,7 @@
1
1
  import { FsError, isErrnoCode, isFsError } from "../../contracts/errors.js";
2
2
  import { composeAbortSignals } from "../../contracts/abort.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
- import { davChild, davChildren, parseXml, scalar, XmlResponseLimitError } from "./xml.js";
4
+ import { davChild, davChildren, parseXmlSteps, scalar, XmlResponseLimitError, XmlResourceLimitError } from "./xml.js";
5
5
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
6
  import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
7
7
  import { compareWebDavResources, ownedResponseIdentifier, recordOwnedResourceStat, registerResourceQuery, resourceIdentifier } from "./resource-id.js";
@@ -561,7 +561,34 @@ export class WebDavFileSystem {
561
561
  const data = await this.bytes(response, this.maxXmlBytes, signal);
562
562
  const encoding = (data[0] === 0xff && data[1] === 0xfe) || (data[0] === 0x3c && data[1] === 0)
563
563
  ? "utf-16le" : (data[0] === 0xfe && data[1] === 0xff) || (data[0] === 0 && data[1] === 0x3c) ? "utf-16be" : "utf-8";
564
- return parseXml(new TextDecoder(encoding, { fatal: true }).decode(data), maxResponses === undefined ? undefined : { maxResponses });
564
+ const parser = parseXmlSteps(new TextDecoder(encoding, { fatal: true }).decode(data), {
565
+ ...(maxResponses === undefined ? {} : { maxResponses }),
566
+ maxNodes: 100_000, maxContentNodes: 100_000, maxDepth: 256,
567
+ maxAttributes: 100_000, maxTextLength: this.maxXmlBytes,
568
+ });
569
+ let work = 0;
570
+ try {
571
+ while (true) {
572
+ signal.throwIfAborted();
573
+ const step = parser.next();
574
+ if (step.done)
575
+ return step.value;
576
+ work += step.value;
577
+ if (work >= 4096) {
578
+ work = 0;
579
+ // Yield to the host task queue so request deadlines and caller aborts can fire.
580
+ await new Promise(resolve => setTimeout(resolve, 0));
581
+ }
582
+ }
583
+ }
584
+ catch (error) {
585
+ if (error instanceof XmlResourceLimitError)
586
+ fail("EFBIG", "webdav", "", error.message);
587
+ throw error;
588
+ }
589
+ finally {
590
+ parser.return(undefined);
591
+ }
565
592
  }
566
593
  async multistatus(response, signal, method = "PROPFIND", path = "") {
567
594
  const link = response.headers.get("link");
@@ -6,7 +6,10 @@ export interface XmlLimits extends Omit<DocumentLimits, "onElement"> {
6
6
  }
7
7
  export declare class XmlResponseLimitError extends SyntaxError {
8
8
  }
9
+ export declare class XmlResourceLimitError extends SyntaxError {
10
+ }
9
11
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
12
+ export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
10
13
  export declare function davChildren(element: XmlElement, localName: string): XmlElement[];
11
14
  export declare function davChild(element: XmlElement, localName: string): XmlElement | undefined;
12
15
  export declare function scalar(element: XmlElement): string;
@@ -1,15 +1,31 @@
1
- import { parseXml as parseDocument, XmlLimitError } from "../../xml.js";
1
+ import { parseXmlSteps as parseDocumentSteps, XmlLimitError } from "../../xml.js";
2
2
  export class XmlResponseLimitError extends SyntaxError {
3
3
  }
4
+ export class XmlResourceLimitError extends SyntaxError {
5
+ }
4
6
  function invalid(message) { throw new SyntaxError(`Invalid WebDAV XML: ${message}`); }
5
7
  export function parseXml(input, limits = {}) {
8
+ const parser = parseXmlSteps(input, limits);
9
+ try {
10
+ let step = parser.next();
11
+ while (!step.done)
12
+ step = parser.next();
13
+ return step.value;
14
+ }
15
+ catch (error) {
16
+ if (error instanceof XmlResourceLimitError)
17
+ throw new SyntaxError(error.message);
18
+ throw error;
19
+ }
20
+ }
21
+ export function* parseXmlSteps(input, limits = {}) {
6
22
  const { maxResponses, ...documentLimits } = limits;
7
23
  if (maxResponses !== undefined && (!Number.isSafeInteger(maxResponses) || maxResponses < 1)) {
8
24
  throw new RangeError("XML limits must be positive integers");
9
25
  }
10
26
  let responses = 0;
11
27
  try {
12
- return parseDocument(input, { ...documentLimits, retainContent: false,
28
+ return yield* parseDocumentSteps(input, { ...documentLimits, retainContent: false,
13
29
  onElement(element, parent, depth) {
14
30
  if (maxResponses !== undefined && depth === 2 && parent?.namespace === "DAV:" && parent.localName === "multistatus"
15
31
  && element.namespace === "DAV:" && element.localName === "response" && ++responses > maxResponses) {
@@ -20,7 +36,7 @@ export function parseXml(input, limits = {}) {
20
36
  }
21
37
  catch (error) {
22
38
  if (error instanceof XmlLimitError)
23
- invalid(error.message);
39
+ throw new XmlResourceLimitError(`Invalid WebDAV XML: ${error.message}`);
24
40
  if (error instanceof SyntaxError && error.message.startsWith("Invalid XML:")) {
25
41
  error.message = error.message.replace("Invalid XML:", "Invalid WebDAV XML:");
26
42
  }
@@ -288,9 +288,10 @@ export function* parseXmlSteps(input, limits = {}) {
288
288
  const parent = stack.at(-1);
289
289
  if (parent) {
290
290
  parent.element.text += text;
291
- if (retainContent && (text.length || kind === "cdata")) {
291
+ if (text.length || kind === "cdata") {
292
292
  admitContent();
293
- parent.content.push({ kind, text });
293
+ if (retainContent)
294
+ parent.content.push({ kind, text });
294
295
  }
295
296
  }
296
297
  else {
@@ -302,9 +303,10 @@ export function* parseXmlSteps(input, limits = {}) {
302
303
  }
303
304
  if (text.length % 512)
304
305
  yield text.length % 512;
305
- if (retainContent && text.length) {
306
+ if (text.length) {
306
307
  admitContent();
307
- (root ? epilog : prolog).push({ kind, text });
308
+ if (retainContent)
309
+ (root ? epilog : prolog).push({ kind, text });
308
310
  }
309
311
  }
310
312
  };
@@ -326,8 +328,8 @@ export function* parseXmlSteps(input, limits = {}) {
326
328
  const parent = stack.at(-1);
327
329
  const text = source.slice(offset + 4, end);
328
330
  admitText(text);
331
+ admitContent();
329
332
  if (retainContent) {
330
- admitContent();
331
333
  (parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
332
334
  }
333
335
  offset = end + 3;
@@ -371,8 +373,8 @@ export function* parseXmlSteps(input, limits = {}) {
371
373
  yield start % 512;
372
374
  const text = content.slice(start);
373
375
  admitText(text);
376
+ admitContent();
374
377
  if (retainContent) {
375
- admitContent();
376
378
  (parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
377
379
  }
378
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.724",
3
+ "version": "0.1.725",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",