@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.
Files changed (34) hide show
  1. package/README.md +16 -7
  2. package/dist/safe-fs/contracts/filesystem.d.ts +2 -0
  3. package/dist/safe-fs/contracts/io.js +26 -36
  4. package/dist/safe-fs/contracts/path.d.ts +2 -7
  5. package/dist/safe-fs/contracts/path.js +1 -7
  6. package/dist/safe-fs/contracts/virtual-path.d.ts +8 -3
  7. package/dist/safe-fs/contracts/virtual-path.js +22 -5
  8. package/dist/safe-fs/core.d.ts +1 -0
  9. package/dist/safe-fs/core.js +1 -0
  10. package/dist/safe-fs/fs/devices/path.js +18 -17
  11. package/dist/safe-fs/fs/memory/index.js +57 -43
  12. package/dist/safe-fs/fs/memory/limits.d.ts +5 -1
  13. package/dist/safe-fs/fs/memory/limits.js +7 -11
  14. package/dist/safe-fs/fs/memory/missing-target.d.ts +2 -1
  15. package/dist/safe-fs/fs/memory/missing-target.js +8 -1
  16. package/dist/safe-fs/fs/overlay/index.d.ts +4 -1
  17. package/dist/safe-fs/fs/overlay/index.js +57 -11
  18. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
  19. package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
  20. package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
  21. package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
  22. package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
  23. package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
  24. package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
  25. package/dist/safe-fs/fs/s3/http/request.js +1 -109
  26. package/dist/safe-fs/fs/s3/http/signature.js +7 -5
  27. package/dist/safe-fs/fs/s3/http/transport.js +6 -3
  28. package/dist/safe-fs/fs/s3/namespace.js +1 -1
  29. package/dist/safe-fs/fs/scoped.js +2 -2
  30. package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
  31. package/dist/safe-fs/fs/webdav/webdav.js +5 -4
  32. package/dist/xml-ast/index.d.ts +2 -0
  33. package/dist/xml-ast/index.js +68 -31
  34. package/package.json +9 -2
@@ -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
- lastFastMapMiss.map = this;
100
- lastFastMapMiss.key = k;
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
- lastFastMapMiss.map = this;
112
- lastFastMapMiss.key = k;
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 (this === lastFastMapMiss.map && k === lastFastMapMiss.key && this._next < this._keys.length) {
201
+ if (k === this._missKey && this._next < this._keys.length) {
203
202
  const entryIdx = this._next++;
204
- this._table[lastFastMapMiss.slot] = entryIdx;
203
+ this._table[this._missSlot] = entryIdx;
205
204
  this._keys[entryIdx] = k;
206
205
  this._vals[entryIdx] = v;
207
206
  this.size++;
208
- lastFastMapMiss.map = undefined;
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 = 65_536 - path.length;
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 > 256)
1094
+ if (++initialComponents > this.ledger.limits.maxPathComponents)
1089
1095
  this.fail("ENAMETOOLONG", syscall, path);
1090
1096
  }
1091
1097
  }
1092
- const pending = path.split("/").filter(Boolean);
1093
- let remainingComponents = 256 - pending.length;
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.length > remainingPathUnits)
1149
+ if (pathByteLength(node.target) > remainingPathUnits)
1144
1150
  this.fail("ENAMETOOLONG", syscall, path);
1145
- remainingPathUnits -= node.target.length;
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.unshift(...target);
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 & 1) !== 0 &&
1586
- (cache.lastFastFileNode.mode & 2) !== 0) {
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 & 1) !== 0 &&
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 & 1) !== 0) {
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[lastFastMapMiss.slot] = entryIdx;
1856
+ entries._table[entries._missSlot] = entryIdx;
1842
1857
  entries._keys[entryIdx] = name;
1843
1858
  entries._vals[entryIdx] = node;
1844
1859
  entries.size++;
1845
- lastFastMapMiss.map = undefined;
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 & 1) !== 0 &&
1868
- (cache.lastFastFileNode.mode & 2) !== 0) {
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 & 1) !== 0 &&
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 & 1) !== 0) {
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 (lastFastMapMiss.map === entries && lastFastMapMiss.key === name && entries._next < entries._keys.length) {
3290
+ if (entries._missKey === name && entries._next < entries._keys.length) {
3280
3291
  const entryIdx = entries._next++;
3281
- entries._table[lastFastMapMiss.slot] = entryIdx;
3292
+ entries._table[entries._missSlot] = entryIdx;
3282
3293
  entries._keys[entryIdx] = name;
3283
3294
  entries._vals[entryIdx] = newNode;
3284
3295
  entries.size++;
3285
- lastFastMapMiss.map = undefined;
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;
@@ -3440,7 +3451,7 @@ function isStockMemoryMethods(mem, names, checkRootAccessor = true) {
3440
3451
  let lastReadViewData;
3441
3452
  let lastReadViewSourceRef;
3442
3453
  export function getLastReadMemoryFileSourceRef(view) {
3443
- const ref = view === lastReadViewData ? lastReadViewSourceRef : undefined;
3454
+ const ref = view === lastReadViewData?.deref() ? lastReadViewSourceRef?.deref() : undefined;
3444
3455
  lastReadViewData = undefined;
3445
3456
  lastReadViewSourceRef = undefined;
3446
3457
  return ref;
@@ -3462,8 +3473,8 @@ export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal, ca
3462
3473
  else if ((++fastWriteNowTick & 63) === 0)
3463
3474
  node.atimeMs = fastWriteCachedNow = Date.now();
3464
3475
  if (captureSourceRef && node.revision === 0) {
3465
- lastReadViewData = data;
3466
- lastReadViewSourceRef = node.sourceRef;
3476
+ lastReadViewData = new WeakRef(data);
3477
+ lastReadViewSourceRef = node.sourceRef === undefined ? undefined : new WeakRef(node.sourceRef);
3467
3478
  }
3468
3479
  return data;
3469
3480
  }
@@ -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 = Partial<MemoryFileSystemLimits>;
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
- if (Object.hasOwn(record, "maxBytes") && record.maxBytes !== undefined) {
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("/"))
@@ -1,6 +1,6 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
2
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
3
- import type { CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, StagedFileContent, AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
+ import type { ConditionalWriteFileOptions, ConditionalRemoveFileOptions, PrepareDirectoryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, StagedFileContent, AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
4
4
  export interface OverlayFileSystemOptions {
5
5
  readonly upper: FileSystem;
6
6
  readonly lower: FileSystem;
@@ -56,6 +56,9 @@ export declare class OverlayFileSystem implements FileSystem {
56
56
  private replace;
57
57
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
58
58
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
59
+ prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
60
+ writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
61
+ removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
59
62
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
60
63
  stat(path: string, options?: FsOptions): Promise<FileStat>;
61
64
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
@@ -126,13 +126,13 @@ export class OverlayFileSystem {
126
126
  ...semantics,
127
127
  open: false,
128
128
  conditionalChmod: this.publication.supported(),
129
- atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: this.publication.supported(), atomicStagingAncestry: this.publication.supported(), atomicDirectoryMetadata: false, trustedOwnedStaging: false,
129
+ atomicFilePublication: false, atomicFileMutation: this.publication.supported(), atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: this.publication.supported(), atomicStagingAncestry: this.publication.supported(), retainedStagingCleanup: this.publication.supported(), atomicDirectoryMetadata: this.publication.supported(), trustedOwnedStaging: false,
130
130
  implicitDirectories: false,
131
131
  readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
132
132
  : upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
133
133
  ...(upper.readOnly === undefined ? {} : { readOnly: upper.readOnly }),
134
134
  ...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
135
- atomicRename: false, atomicRenameNoReplace: false,
135
+ atomicRename: this.publication.supported(), atomicRenameNoReplace: this.publication.supported(),
136
136
  descriptorWriteStream: false,
137
137
  retainedResize: false, atomicResize: false,
138
138
  hardlinks: false,
@@ -148,7 +148,7 @@ export class OverlayFileSystem {
148
148
  Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
149
149
  }
150
150
  confineExtraction(roots, options = {}) {
151
- return this.run(options, async () => this.publication.confine(roots, options, (options, operation) => this.run(options, operation, false), this), false);
151
+ return this.run(options, async () => this.publication.confine(roots, options, (options, operation) => this.run(options, operation, false), this, this.maxBufferBytes), false);
152
152
  }
153
153
  createStagedFile(directory, name, content, options) {
154
154
  return this.run(options, async () => {
@@ -715,11 +715,38 @@ export class OverlayFileSystem {
715
715
  const append = options.flag === "a" || options.flag === "ax";
716
716
  if (append && (location.entry?.stat.size ?? 0) + bytes.byteLength > this.maxBufferBytes)
717
717
  fail("EFBIG", path);
718
- await this.replace(location, options, async (temporary) => {
719
- await this.#upper.writeFile(temporary, bytes, { ...options, flag: append ? "a" : "w" });
720
- });
718
+ if (this.publication.supported() && location.entry?.backend === this.#upper) {
719
+ const parent = await this.required(dirname(location.path), options);
720
+ await this.publication.writeFile(location.path, bytes, {
721
+ ...options, parent: this.publication.stat(parent.path, parent.stat),
722
+ expected: this.publication.stat(location.path, location.entry.stat), append,
723
+ }, this.maxBufferBytes);
724
+ }
725
+ else {
726
+ await this.replace(location, options, async (temporary) => {
727
+ await this.#upper.writeFile(temporary, bytes, { ...options, flag: append ? "a" : "w" });
728
+ });
729
+ }
721
730
  });
722
731
  }
732
+ prepareDirectory(path, options) {
733
+ const captured = { ...options };
734
+ return this.run(captured, () => this.publication.prepareDirectory(path, captured), false);
735
+ }
736
+ async writeFileConditional(path, data, options) {
737
+ options.signal?.throwIfAborted();
738
+ if (!(data instanceof Uint8Array))
739
+ throw new TypeError("Overlay files require Uint8Array data");
740
+ const bytes = new Uint8Array(data);
741
+ if (bytes.byteLength > this.maxBufferBytes)
742
+ fail("EFBIG", path);
743
+ const captured = { ...options };
744
+ return this.run(captured, () => this.publication.writeFile(path, bytes, captured, this.maxBufferBytes), false);
745
+ }
746
+ removeFileConditional(path, options) {
747
+ const captured = { ...options };
748
+ return this.run(captured, () => this.publication.removeFile(path, captured), false);
749
+ }
723
750
  async appendFile(path, data, options = {}) {
724
751
  return this.writeFile(path, data, { ...options, flag: "a" });
725
752
  }
@@ -866,11 +893,13 @@ export class OverlayFileSystem {
866
893
  }
867
894
  async rename(source, destination, options = {}) {
868
895
  return this.run(options, async () => {
869
- if (options.noReplace)
896
+ if (options.noReplace && !this.publication.supported())
870
897
  fail("ENOTSUP", source, "atomic no-replace rename is unsupported");
871
898
  this.writable(source);
872
899
  const original = await this.required(source, options, false);
873
900
  const target = await this.resolve(destination, options, false, true);
901
+ if (options.noReplace && target.entry)
902
+ fail("EEXIST", destination);
874
903
  if (original.path === target.path)
875
904
  return;
876
905
  if (original.path === "/" || target.path === "/")
@@ -885,6 +914,7 @@ export class OverlayFileSystem {
885
914
  if (target.entry.stat.type === "directory" && (await this.listing(target.entry, options)).length)
886
915
  fail("ENOTEMPTY", destination);
887
916
  }
917
+ const guard = this.capabilities.atomicRename ? this.publication.prepareRename(original.path, target.path) : undefined;
888
918
  await this.parent(original.path, options);
889
919
  await this.parent(target.path, options);
890
920
  await this.materialize(original, options);
@@ -902,7 +932,10 @@ export class OverlayFileSystem {
902
932
  movedOrigins.push([destination, { ...origin, moves }]);
903
933
  }
904
934
  options.signal?.throwIfAborted();
905
- await this.#upper.rename(original.path, target.path, options);
935
+ if (this.capabilities.atomicRename)
936
+ await this.publication.rename(original.path, target.path, options, guard);
937
+ else
938
+ await this.#upper.rename(original.path, target.path, options);
906
939
  this.whiteouts.add(original.path);
907
940
  if (original.stat.type === "directory")
908
941
  this.opaque.add(target.path);
@@ -998,6 +1031,9 @@ export class OverlayFileSystem {
998
1031
  await this.copyUp(entry, options);
999
1032
  await operation(entry.path);
1000
1033
  }
1034
+ else if (this.publication.supported() && entry.backend === this.#upper) {
1035
+ await operation(entry.path);
1036
+ }
1001
1037
  else
1002
1038
  await this.replace({ path: entry.path, entry }, options, operation);
1003
1039
  });
@@ -1123,9 +1159,19 @@ export class OverlayFileSystem {
1123
1159
  const append = options.flag === "a" || options.flag === "ax";
1124
1160
  if (append && (location.entry?.stat.size ?? 0) + size > this.maxBufferBytes)
1125
1161
  fail("EFBIG", path);
1126
- await this.replace(location, options, async (temporary) => {
1127
- await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
1128
- }, true);
1162
+ if (this.publication.supported() && location.entry?.backend === this.#upper) {
1163
+ const bytes = await this.#upper.readFile(incoming, options);
1164
+ const parent = await this.required(dirname(location.path), options);
1165
+ await this.publication.writeFile(location.path, bytes, {
1166
+ ...options, parent: this.publication.stat(parent.path, parent.stat),
1167
+ expected: this.publication.stat(location.path, location.entry.stat), append,
1168
+ }, this.maxBufferBytes);
1169
+ }
1170
+ else {
1171
+ await this.replace(location, options, async (temporary) => {
1172
+ await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
1173
+ }, true);
1174
+ }
1129
1175
  });
1130
1176
  });
1131
1177
  return;
@@ -1,4 +1,4 @@
1
- import type { CreateStagedFileOptions, FileStaging, FileStat, FileSystem, FsOptions, ChmodOptions, PublishStagedFileOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { ConditionalWriteFileOptions, ConditionalRemoveFileOptions, RenameOptions, PrepareDirectoryOptions, CreateStagedFileOptions, FileStaging, FileStat, FileSystem, FsOptions, ChmodOptions, PublishStagedFileOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  /** Stock Memory mutations commit before returning their promise. No user code or
3
3
  * await is allowed between inspecting the two stores and invoking a mutation. */
4
4
  export declare class OverlayMemoryPublication {
@@ -11,6 +11,7 @@ export declare class OverlayMemoryPublication {
11
11
  private readonly directories;
12
12
  private readonly origins;
13
13
  private readonly stages;
14
+ private readonly stagingAncestors;
14
15
  constructor(upper: FileSystem, lower: FileSystem, whiteouts: Set<string>, opaque: Set<string>, hidden: Set<string>);
15
16
  supported(): boolean;
16
17
  private stores;
@@ -27,9 +28,15 @@ export declare class OverlayMemoryPublication {
27
28
  private capture;
28
29
  private check;
29
30
  prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
31
+ prepareWrite(path: string, options: ConditionalWriteFileOptions): () => true;
32
+ writeFile(path: string, data: Uint8Array, options: ConditionalWriteFileOptions, maxBytes: number, guard?: () => void): Promise<FileStat>;
33
+ removeFile(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
34
+ prepareDirectory(path: string, options: PrepareDirectoryOptions, guard?: () => void): Promise<FileStat>;
35
+ prepareRename(source: string, destination: string): () => void;
36
+ rename(source: string, destination: string, options: RenameOptions, guard: () => void): Promise<void>;
30
37
  private copyParents;
31
- create(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
38
+ create(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions, guard?: () => void): Promise<FileStaging>;
32
39
  publish(staging: FileStaging, path: string, options: PublishStagedFileOptions): Promise<void>;
33
- cleanup(staging: FileStaging, options: FsOptions): Promise<void>;
34
- confine(roots: readonly string[], options: FsOptions, run: <T>(options: FsOptions, operation: () => Promise<T>) => Promise<T>, fs: FileSystem): FileSystem;
40
+ cleanup(staging: FileStaging, options: FsOptions, retained?: boolean): Promise<void>;
41
+ confine(roots: readonly string[], options: FsOptions, run: <T>(options: FsOptions, operation: () => Promise<T>) => Promise<T>, fs: FileSystem, maxBytes: number): FileSystem;
35
42
  }