@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,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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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 (
|
|
201
|
+
if (k === this._missKey && this._next < this._keys.length) {
|
|
203
202
|
const entryIdx = this._next++;
|
|
204
|
-
this._table[
|
|
203
|
+
this._table[this._missSlot] = entryIdx;
|
|
205
204
|
this._keys[entryIdx] = k;
|
|
206
205
|
this._vals[entryIdx] = v;
|
|
207
206
|
this.size++;
|
|
208
|
-
|
|
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 =
|
|
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 >
|
|
1094
|
+
if (++initialComponents > this.ledger.limits.maxPathComponents)
|
|
1089
1095
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1090
1096
|
}
|
|
1091
1097
|
}
|
|
1092
|
-
|
|
1093
|
-
let remainingComponents =
|
|
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
|
|
1149
|
+
if (pathByteLength(node.target) > remainingPathUnits)
|
|
1144
1150
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
1145
|
-
remainingPathUnits -= node.target
|
|
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.
|
|
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 &
|
|
1586
|
-
(cache.lastFastFileNode.mode &
|
|
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 &
|
|
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 &
|
|
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[
|
|
1856
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
1842
1857
|
entries._keys[entryIdx] = name;
|
|
1843
1858
|
entries._vals[entryIdx] = node;
|
|
1844
1859
|
entries.size++;
|
|
1845
|
-
|
|
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 &
|
|
1868
|
-
(cache.lastFastFileNode.mode &
|
|
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 &
|
|
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 &
|
|
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 (
|
|
3290
|
+
if (entries._missKey === name && entries._next < entries._keys.length) {
|
|
3280
3291
|
const entryIdx = entries._next++;
|
|
3281
|
-
entries._table[
|
|
3292
|
+
entries._table[entries._missSlot] = entryIdx;
|
|
3282
3293
|
entries._keys[entryIdx] = name;
|
|
3283
3294
|
entries._vals[entryIdx] = newNode;
|
|
3284
3295
|
entries.size++;
|
|
3285
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
719
|
-
await this
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
await this
|
|
1128
|
-
|
|
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
|
}
|