@poe-platform/safe-fs 0.1.740 → 0.1.741

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
@@ -113,6 +113,11 @@ call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
113
113
  retain only the owned staging entries; replacement entries remain protected.
114
114
  See the [staging contract](src/contracts/filesystem.md#atomic-owned-staging).
115
115
 
116
+ Capture `retainFileSystemCleanup(scoped, callback, { maxOperations })` before
117
+ cancellation to clean up through a bounded view after ordinary scoped operations
118
+ close. Cleanup preserves its owning scope and limits across separately bundled
119
+ command runtimes, including restrictions added by filesystem wrappers.
120
+
116
121
  | Backend or wrapper | Use it for |
117
122
  | --- | --- |
118
123
  | `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams; each path resolution admits at most 65,536 cumulative UTF-16 code units across the input and followed symlink targets, rejecting excess with `ENAMETOOLONG` before component allocation |
@@ -8,6 +8,7 @@ import { openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabi
8
8
  import { createStagingCleanup, snapshotStagingCreation } from "./staging-cleanup.js";
9
9
  import { inspectStagingBindings, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "./staging-ancestry.js";
10
10
  const originals = new WeakMap();
11
+ const retainedCleanupSymbol = Symbol.for("safe-fs.retainedCleanup");
11
12
  const retargeters = new WeakMap();
12
13
  export function retargetScopedFileSystem(scoped, charge, signal, cleanupCharge = charge, maxPathComponents) {
13
14
  const fn = retargeters.get(scoped);
@@ -233,6 +234,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
233
234
  return Reflect.set(original, property, value, original);
234
235
  },
235
236
  get(_target, property) {
237
+ if (property === retainedCleanupSymbol)
238
+ return cleanupBinding;
236
239
  if (property === "capabilities") {
237
240
  const rawCaps = original.capabilities;
238
241
  if (cachedComputedCaps !== undefined &&
@@ -510,6 +513,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
510
513
  return scoped;
511
514
  },
512
515
  });
516
+ const cleanupBinding = Object.freeze({ filesystem: view, retain: retainFileSystemCleanup });
513
517
  const originalRecord = { filesystem: original, signal, cleanupCharge, creationMask, maxPathComponents };
514
518
  originals.set(view, originalRecord);
515
519
  retargeters.set(view, (nextCharge, nextSignal, nextCleanupCharge, nextMaxPathComponents) => {
@@ -538,6 +542,13 @@ export function retainFileSystemCleanup(filesystem, cleanupCallback, options) {
538
542
  if (typeof cleanupCallback !== "function")
539
543
  throw new TypeError("cleanup callback must be a function");
540
544
  const scope = originals.get(filesystem);
545
+ if (!scope) {
546
+ // Bundled commands can hold another module copy. Delegate only for this
547
+ // exact view, never through a wrapper that happens to forward its symbols.
548
+ const binding = Reflect.get(filesystem, retainedCleanupSymbol);
549
+ if (binding?.filesystem === filesystem && typeof binding.retain === "function" && binding.retain !== retainFileSystemCleanup)
550
+ return binding.retain(filesystem, cleanupCallback, options);
551
+ }
541
552
  scope?.signal.throwIfAborted();
542
553
  const backing = scope?.filesystem ?? filesystem;
543
554
  const pending = new Set();
@@ -273,6 +273,7 @@ export function* parseXmlSteps(input, limits = {}) {
273
273
  let nodes = 0;
274
274
  let attributeCount = 0;
275
275
  let contentNodes = 0;
276
+ let previousEmpty;
276
277
  const admitContent = () => {
277
278
  if (++contentNodes > maxContentNodes)
278
279
  throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
@@ -440,17 +441,23 @@ export function* parseXmlSteps(input, limits = {}) {
440
441
  }
441
442
  else {
442
443
  offset++;
443
- const [name, prefix, localName, wName] = scanName();
444
+ const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
445
+ const [name, prefix, localName, wName] = repeated
446
+ ? [repeated.name, repeated.prefix, repeated.localName, repeated.name.length * 2]
447
+ : scanName();
448
+ if (repeated)
449
+ offset += name.length;
450
+ // Preserve scan/validation work while reusing the admitted spelling.
444
451
  pendingWork += wName;
445
452
  while (pendingWork >= 512) {
446
453
  yield 512;
447
454
  pendingWork -= 512;
448
455
  }
449
- const attributes = new Map();
456
+ let attributes;
450
457
  const attrMeta = [];
451
458
  let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
452
459
  let ownsNamespaces = stack.length === 0;
453
- while (true) {
460
+ while (!repeated) {
454
461
  const ws = skipWhitespace();
455
462
  pendingWork += ws;
456
463
  while (pendingWork >= 512) {
@@ -468,6 +475,7 @@ export function* parseXmlSteps(input, limits = {}) {
468
475
  yield 512;
469
476
  pendingWork -= 512;
470
477
  }
478
+ attributes ??= new Map();
471
479
  if (attributes.has(attribute))
472
480
  invalid("duplicate attribute");
473
481
  if (++attributeCount > maxAttributes)
@@ -560,7 +568,7 @@ export function* parseXmlSteps(input, limits = {}) {
560
568
  limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
561
569
  admitContent();
562
570
  const retainedAttributes = [];
563
- if (retainContent) {
571
+ if (retainContent && attributes) {
564
572
  for (let i = 0; i < attrMeta.length; i++) {
565
573
  const [attribute, attrPrefix, attrLocal] = attrMeta[i];
566
574
  const value = attributes.get(attribute);
@@ -590,6 +598,10 @@ export function* parseXmlSteps(input, limits = {}) {
590
598
  offset++;
591
599
  if (source[offset++] !== ">")
592
600
  invalid("unterminated start tag");
601
+ // Only cache compact, attribute-free spellings. Namespace resolution and
602
+ // every admission counter still run for each distinct physical element.
603
+ if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
604
+ previousEmpty = { suffix: name + "/>", name, prefix, localName };
593
605
  if (!empty)
594
606
  stack.push({ element, content, name, namespaces });
595
607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.740",
3
+ "version": "0.1.741",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",