@componentor/fs 3.0.44 → 3.0.46
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/dist/index.js +337 -50
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +337 -50
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +337 -50
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +337 -50
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +26 -0
|
@@ -154,6 +154,16 @@ var VFSEngine = class {
|
|
|
154
154
|
superblockDirty = false;
|
|
155
155
|
// Free inode hint — skip O(n) scan
|
|
156
156
|
freeInodeHint = 0;
|
|
157
|
+
// Implicit directory support — tracks all directory prefixes implied by file paths.
|
|
158
|
+
// Rebuilt lazily when pathIndex changes (tracked via generation counter).
|
|
159
|
+
// Map value is the stable timestamp (ms since epoch) assigned when the implicit
|
|
160
|
+
// dir was first discovered, so that stat() returns consistent mtime/ctime/atime
|
|
161
|
+
// across repeated calls.
|
|
162
|
+
implicitDirs = /* @__PURE__ */ new Map();
|
|
163
|
+
implicitDirsGen = -1;
|
|
164
|
+
// generation when implicitDirs was last rebuilt
|
|
165
|
+
pathIndexGen = 0;
|
|
166
|
+
// bumped on every pathIndex mutation
|
|
157
167
|
// Configurable upper bounds
|
|
158
168
|
maxInodes = 4e6;
|
|
159
169
|
maxBlocks = 4e6;
|
|
@@ -438,6 +448,7 @@ var VFSEngine = class {
|
|
|
438
448
|
}
|
|
439
449
|
this.pathIndex.set(path, i);
|
|
440
450
|
}
|
|
451
|
+
this.pathIndexGen++;
|
|
441
452
|
}
|
|
442
453
|
// ========== Low-level inode I/O ==========
|
|
443
454
|
readInode(idx) {
|
|
@@ -510,14 +521,23 @@ var VFSEngine = class {
|
|
|
510
521
|
growPathTable(needed) {
|
|
511
522
|
const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
|
|
512
523
|
const growth = newSize - this.pathTableSize;
|
|
513
|
-
const dataSize = this.totalBlocks * this.blockSize;
|
|
514
|
-
const dataBuf = new Uint8Array(dataSize);
|
|
515
|
-
this.handle.read(dataBuf, { at: this.dataOffset });
|
|
516
524
|
const newTotalSize = this.handle.getSize() + growth;
|
|
517
525
|
this.handle.truncate(newTotalSize);
|
|
526
|
+
const dataSize = this.totalBlocks * this.blockSize;
|
|
527
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
528
|
+
const scratch = new Uint8Array(Math.min(CHUNK, Math.max(dataSize, 1)));
|
|
529
|
+
let remaining = dataSize;
|
|
530
|
+
while (remaining > 0) {
|
|
531
|
+
const chunk = Math.min(remaining, CHUNK);
|
|
532
|
+
const srcAt = this.dataOffset + (remaining - chunk);
|
|
533
|
+
const dstAt = this.dataOffset + growth + (remaining - chunk);
|
|
534
|
+
const slice = chunk < scratch.length ? scratch.subarray(0, chunk) : scratch;
|
|
535
|
+
this.handle.read(slice, { at: srcAt });
|
|
536
|
+
this.handle.write(slice, { at: dstAt });
|
|
537
|
+
remaining -= chunk;
|
|
538
|
+
}
|
|
518
539
|
const newBitmapOffset = this.bitmapOffset + growth;
|
|
519
540
|
const newDataOffset = this.dataOffset + growth;
|
|
520
|
-
this.handle.write(dataBuf, { at: newDataOffset });
|
|
521
541
|
this.handle.write(this.bitmap, { at: newBitmapOffset });
|
|
522
542
|
this.pathTableSize = newSize;
|
|
523
543
|
this.bitmapOffset = newBitmapOffset;
|
|
@@ -525,6 +545,23 @@ var VFSEngine = class {
|
|
|
525
545
|
this.superblockDirty = true;
|
|
526
546
|
}
|
|
527
547
|
// ========== Bitmap I/O ==========
|
|
548
|
+
// Write `length` zero bytes at absolute file offset `at` via a small
|
|
549
|
+
// reusable scratch buffer. Used to materialize POSIX "holes" when a
|
|
550
|
+
// write starts past the current file size — those bytes must read as
|
|
551
|
+
// zeros rather than whatever stale data happened to live in the
|
|
552
|
+
// underlying storage blocks.
|
|
553
|
+
zeroFileRange(at, length) {
|
|
554
|
+
if (length <= 0) return;
|
|
555
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
556
|
+
const zeros = new Uint8Array(Math.min(length, CHUNK));
|
|
557
|
+
let written = 0;
|
|
558
|
+
while (written < length) {
|
|
559
|
+
const n = Math.min(CHUNK, length - written);
|
|
560
|
+
const slice = n < zeros.length ? zeros.subarray(0, n) : zeros;
|
|
561
|
+
this.handle.write(slice, { at: at + written });
|
|
562
|
+
written += n;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
528
565
|
allocateBlocks(count) {
|
|
529
566
|
if (count === 0) return 0;
|
|
530
567
|
const bitmap = this.bitmap;
|
|
@@ -732,6 +769,7 @@ var VFSEngine = class {
|
|
|
732
769
|
};
|
|
733
770
|
this.writeInode(idx, inode);
|
|
734
771
|
this.pathIndex.set(path, idx);
|
|
772
|
+
this.pathIndexGen++;
|
|
735
773
|
return idx;
|
|
736
774
|
}
|
|
737
775
|
// ========== Public API — called by server worker dispatch ==========
|
|
@@ -849,17 +887,28 @@ var VFSEngine = class {
|
|
|
849
887
|
}
|
|
850
888
|
const inode = this.readInode(existingIdx);
|
|
851
889
|
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
852
|
-
const
|
|
853
|
-
const
|
|
854
|
-
combined.set(existing);
|
|
855
|
-
combined.set(data, existing.byteLength);
|
|
856
|
-
const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
|
|
857
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
890
|
+
const combinedSize = inode.size + data.byteLength;
|
|
891
|
+
const neededBlocks = Math.ceil(combinedSize / this.blockSize);
|
|
858
892
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
859
|
-
this.
|
|
893
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
894
|
+
if (inode.size > 0) {
|
|
895
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
896
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
897
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
898
|
+
let copied = 0;
|
|
899
|
+
while (copied < inode.size) {
|
|
900
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
901
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
902
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
903
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
904
|
+
copied += n;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
908
|
+
this.handle.write(data, { at: newBase + inode.size });
|
|
860
909
|
inode.firstBlock = newFirst;
|
|
861
910
|
inode.blockCount = neededBlocks;
|
|
862
|
-
inode.size =
|
|
911
|
+
inode.size = combinedSize;
|
|
863
912
|
inode.mtime = Date.now();
|
|
864
913
|
this.writeInode(existingIdx, inode);
|
|
865
914
|
this.commitPending();
|
|
@@ -877,6 +926,7 @@ var VFSEngine = class {
|
|
|
877
926
|
inode.type = INODE_TYPE.FREE;
|
|
878
927
|
this.writeInode(idx, inode);
|
|
879
928
|
this.pathIndex.delete(path);
|
|
929
|
+
this.pathIndexGen++;
|
|
880
930
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
881
931
|
this.commitPending();
|
|
882
932
|
return { status: 0 };
|
|
@@ -885,7 +935,12 @@ var VFSEngine = class {
|
|
|
885
935
|
stat(path) {
|
|
886
936
|
path = this.normalizePath(path);
|
|
887
937
|
const idx = this.resolvePathComponents(path, true);
|
|
888
|
-
if (idx === void 0)
|
|
938
|
+
if (idx === void 0) {
|
|
939
|
+
if (this.isImplicitDirectory(path)) {
|
|
940
|
+
return this.encodeImplicitDirStatResponse(path);
|
|
941
|
+
}
|
|
942
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
943
|
+
}
|
|
889
944
|
return this.encodeStatResponse(idx);
|
|
890
945
|
}
|
|
891
946
|
// ---- LSTAT (no symlink follow for the FINAL component) ----
|
|
@@ -894,7 +949,12 @@ var VFSEngine = class {
|
|
|
894
949
|
let idx = this.resolvePathComponents(path, false);
|
|
895
950
|
if (idx === void 0) {
|
|
896
951
|
idx = this.resolvePathComponents(path, true);
|
|
897
|
-
if (idx === void 0)
|
|
952
|
+
if (idx === void 0) {
|
|
953
|
+
if (this.isImplicitDirectory(path)) {
|
|
954
|
+
return this.encodeImplicitDirStatResponse(path);
|
|
955
|
+
}
|
|
956
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
957
|
+
}
|
|
898
958
|
}
|
|
899
959
|
return this.encodeStatResponse(idx);
|
|
900
960
|
}
|
|
@@ -903,13 +963,17 @@ var VFSEngine = class {
|
|
|
903
963
|
let nlink = inode.nlink;
|
|
904
964
|
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
905
965
|
const path = this.readPath(inode.pathOffset, inode.pathLength);
|
|
906
|
-
const children = this.
|
|
966
|
+
const children = this.getDirectChildrenWithImplicit(path);
|
|
907
967
|
let subdirCount = 0;
|
|
908
968
|
for (const child of children) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
969
|
+
if (child.type === "implicit") {
|
|
970
|
+
subdirCount++;
|
|
971
|
+
} else {
|
|
972
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
973
|
+
if (childIdx !== void 0) {
|
|
974
|
+
const childInode = this.readInode(childIdx);
|
|
975
|
+
if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
|
|
976
|
+
}
|
|
913
977
|
}
|
|
914
978
|
}
|
|
915
979
|
nlink = 2 + subdirCount;
|
|
@@ -935,7 +999,9 @@ var VFSEngine = class {
|
|
|
935
999
|
if (recursive) {
|
|
936
1000
|
return this.mkdirRecursive(path);
|
|
937
1001
|
}
|
|
938
|
-
if (this.pathIndex.has(path)
|
|
1002
|
+
if (this.pathIndex.has(path) || this.isImplicitDirectory(path)) {
|
|
1003
|
+
return { status: CODE_TO_STATUS.EEXIST, data: null };
|
|
1004
|
+
}
|
|
939
1005
|
const parentStatus = this.ensureParent(path);
|
|
940
1006
|
if (parentStatus !== 0) return { status: parentStatus, data: null };
|
|
941
1007
|
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
@@ -970,7 +1036,26 @@ var VFSEngine = class {
|
|
|
970
1036
|
path = this.normalizePath(path);
|
|
971
1037
|
const recursive = (flags & 1) !== 0;
|
|
972
1038
|
const idx = this.pathIndex.get(path);
|
|
973
|
-
if (idx === void 0)
|
|
1039
|
+
if (idx === void 0) {
|
|
1040
|
+
if (this.isImplicitDirectory(path)) {
|
|
1041
|
+
const children2 = this.getDirectChildrenWithImplicit(path);
|
|
1042
|
+
if (children2.length > 0) {
|
|
1043
|
+
if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
|
|
1044
|
+
for (const desc of this.getAllDescendants(path)) {
|
|
1045
|
+
const descIdx = this.pathIndex.get(desc);
|
|
1046
|
+
const descInode = this.readInode(descIdx);
|
|
1047
|
+
this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
|
|
1048
|
+
descInode.type = INODE_TYPE.FREE;
|
|
1049
|
+
this.writeInode(descIdx, descInode);
|
|
1050
|
+
this.pathIndex.delete(desc);
|
|
1051
|
+
}
|
|
1052
|
+
this.pathIndexGen++;
|
|
1053
|
+
this.commitPending();
|
|
1054
|
+
}
|
|
1055
|
+
return { status: 0 };
|
|
1056
|
+
}
|
|
1057
|
+
return { status: CODE_TO_STATUS.ENOENT };
|
|
1058
|
+
}
|
|
974
1059
|
const inode = this.readInode(idx);
|
|
975
1060
|
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
|
|
976
1061
|
const children = this.getDirectChildren(path);
|
|
@@ -988,6 +1073,7 @@ var VFSEngine = class {
|
|
|
988
1073
|
inode.type = INODE_TYPE.FREE;
|
|
989
1074
|
this.writeInode(idx, inode);
|
|
990
1075
|
this.pathIndex.delete(path);
|
|
1076
|
+
this.pathIndexGen++;
|
|
991
1077
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
992
1078
|
this.commitPending();
|
|
993
1079
|
return { status: 0 };
|
|
@@ -996,20 +1082,33 @@ var VFSEngine = class {
|
|
|
996
1082
|
readdir(path, flags = 0) {
|
|
997
1083
|
path = this.normalizePath(path);
|
|
998
1084
|
const resolved = this.resolvePathFull(path, true);
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1085
|
+
let effectiveDirPath;
|
|
1086
|
+
if (resolved) {
|
|
1087
|
+
const inode = this.readInode(resolved.idx);
|
|
1088
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
1089
|
+
effectiveDirPath = resolved.resolvedPath;
|
|
1090
|
+
} else if (this.isImplicitDirectory(path)) {
|
|
1091
|
+
effectiveDirPath = path;
|
|
1092
|
+
} else {
|
|
1093
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
1094
|
+
}
|
|
1002
1095
|
const withFileTypes = (flags & 1) !== 0;
|
|
1003
|
-
const children = this.
|
|
1096
|
+
const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
|
|
1004
1097
|
if (withFileTypes) {
|
|
1005
1098
|
let totalSize2 = 4;
|
|
1006
1099
|
const entries = [];
|
|
1007
|
-
for (const
|
|
1008
|
-
const name =
|
|
1100
|
+
for (const child of children) {
|
|
1101
|
+
const name = child.path.substring(child.path.lastIndexOf("/") + 1);
|
|
1009
1102
|
const nameBytes = encoder.encode(name);
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1103
|
+
let type;
|
|
1104
|
+
if (child.type === "implicit") {
|
|
1105
|
+
type = INODE_TYPE.DIRECTORY;
|
|
1106
|
+
} else {
|
|
1107
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
1108
|
+
const childInode = this.readInode(childIdx);
|
|
1109
|
+
type = childInode.type;
|
|
1110
|
+
}
|
|
1111
|
+
entries.push({ name: nameBytes, type });
|
|
1013
1112
|
totalSize2 += 2 + nameBytes.byteLength + 1;
|
|
1014
1113
|
}
|
|
1015
1114
|
const buf2 = new Uint8Array(totalSize2);
|
|
@@ -1027,8 +1126,8 @@ var VFSEngine = class {
|
|
|
1027
1126
|
}
|
|
1028
1127
|
let totalSize = 4;
|
|
1029
1128
|
const nameEntries = [];
|
|
1030
|
-
for (const
|
|
1031
|
-
const name =
|
|
1129
|
+
for (const child of children) {
|
|
1130
|
+
const name = child.path.substring(child.path.lastIndexOf("/") + 1);
|
|
1032
1131
|
const nameBytes = encoder.encode(name);
|
|
1033
1132
|
nameEntries.push(nameBytes);
|
|
1034
1133
|
totalSize += 2 + nameBytes.byteLength;
|
|
@@ -1069,6 +1168,7 @@ var VFSEngine = class {
|
|
|
1069
1168
|
this.writeInode(idx, inode);
|
|
1070
1169
|
this.pathIndex.delete(oldPath);
|
|
1071
1170
|
this.pathIndex.set(newPath, idx);
|
|
1171
|
+
this.pathIndexGen++;
|
|
1072
1172
|
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
1073
1173
|
const prefix = oldPath === "/" ? "/" : oldPath + "/";
|
|
1074
1174
|
const toRename = [];
|
|
@@ -1097,7 +1197,7 @@ var VFSEngine = class {
|
|
|
1097
1197
|
path = this.normalizePath(path);
|
|
1098
1198
|
const idx = this.resolvePathComponents(path, true);
|
|
1099
1199
|
const buf = new Uint8Array(1);
|
|
1100
|
-
buf[0] = idx !== void 0 ? 1 : 0;
|
|
1200
|
+
buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
|
|
1101
1201
|
return { status: 0, data: buf };
|
|
1102
1202
|
}
|
|
1103
1203
|
// ---- TRUNCATE ----
|
|
@@ -1122,13 +1222,29 @@ var VFSEngine = class {
|
|
|
1122
1222
|
} else if (len > inode.size) {
|
|
1123
1223
|
const neededBlocks = Math.ceil(len / this.blockSize);
|
|
1124
1224
|
if (neededBlocks > inode.blockCount) {
|
|
1125
|
-
const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
|
|
1126
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
1127
1225
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1226
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
1227
|
+
if (inode.size > 0) {
|
|
1228
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
1229
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
1230
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
1231
|
+
let copied = 0;
|
|
1232
|
+
while (copied < inode.size) {
|
|
1233
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
1234
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
1235
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
1236
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
1237
|
+
copied += n;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
1241
|
+
this.zeroFileRange(newBase + inode.size, len - inode.size);
|
|
1131
1242
|
inode.firstBlock = newFirst;
|
|
1243
|
+
} else {
|
|
1244
|
+
this.zeroFileRange(
|
|
1245
|
+
this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
|
|
1246
|
+
len - inode.size
|
|
1247
|
+
);
|
|
1132
1248
|
}
|
|
1133
1249
|
inode.blockCount = neededBlocks;
|
|
1134
1250
|
inode.size = len;
|
|
@@ -1149,14 +1265,45 @@ var VFSEngine = class {
|
|
|
1149
1265
|
if (flags & 1 && this.pathIndex.has(destPath)) {
|
|
1150
1266
|
return { status: CODE_TO_STATUS.EEXIST };
|
|
1151
1267
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1268
|
+
if (srcPath === destPath) return { status: 0 };
|
|
1269
|
+
const srcSize = srcInode.size;
|
|
1270
|
+
const srcFirstBlock = srcInode.firstBlock;
|
|
1271
|
+
const emptyStatus = this.write(destPath, new Uint8Array(0));
|
|
1272
|
+
if (emptyStatus.status !== 0) return emptyStatus;
|
|
1273
|
+
if (srcSize === 0) return { status: 0 };
|
|
1274
|
+
const destIdx = this.resolvePathComponents(destPath, true);
|
|
1275
|
+
if (destIdx === void 0) return { status: CODE_TO_STATUS.EIO };
|
|
1276
|
+
const destInode = this.readInode(destIdx);
|
|
1277
|
+
const neededBlocks = Math.ceil(srcSize / this.blockSize);
|
|
1278
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
1279
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
1280
|
+
const srcBase = this.dataOffset + srcFirstBlock * this.blockSize;
|
|
1281
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
1282
|
+
const scratch = new Uint8Array(Math.min(CHUNK, srcSize));
|
|
1283
|
+
let copied = 0;
|
|
1284
|
+
while (copied < srcSize) {
|
|
1285
|
+
const n = Math.min(CHUNK, srcSize - copied);
|
|
1286
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
1287
|
+
this.handle.read(slice, { at: srcBase + copied });
|
|
1288
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
1289
|
+
copied += n;
|
|
1290
|
+
}
|
|
1291
|
+
destInode.firstBlock = newFirst;
|
|
1292
|
+
destInode.blockCount = neededBlocks;
|
|
1293
|
+
destInode.size = srcSize;
|
|
1294
|
+
destInode.mtime = Date.now();
|
|
1295
|
+
this.writeInode(destIdx, destInode);
|
|
1296
|
+
this.commitPending();
|
|
1297
|
+
return { status: 0 };
|
|
1154
1298
|
}
|
|
1155
1299
|
// ---- ACCESS ----
|
|
1156
1300
|
access(path, mode = 0) {
|
|
1157
1301
|
path = this.normalizePath(path);
|
|
1158
1302
|
const idx = this.resolvePathComponents(path, true);
|
|
1159
|
-
if (idx === void 0)
|
|
1303
|
+
if (idx === void 0) {
|
|
1304
|
+
if (this.isImplicitDirectory(path)) return { status: 0 };
|
|
1305
|
+
return { status: CODE_TO_STATUS.ENOENT };
|
|
1306
|
+
}
|
|
1160
1307
|
if (mode === 0) return { status: 0 };
|
|
1161
1308
|
if (!this.strictPermissions) return { status: 0 };
|
|
1162
1309
|
const inode = this.readInode(idx);
|
|
@@ -1176,7 +1323,12 @@ var VFSEngine = class {
|
|
|
1176
1323
|
realpath(path) {
|
|
1177
1324
|
path = this.normalizePath(path);
|
|
1178
1325
|
const idx = this.resolvePathComponents(path, true);
|
|
1179
|
-
if (idx === void 0)
|
|
1326
|
+
if (idx === void 0) {
|
|
1327
|
+
if (this.isImplicitDirectory(path)) {
|
|
1328
|
+
return { status: 0, data: encoder.encode(path) };
|
|
1329
|
+
}
|
|
1330
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
1331
|
+
}
|
|
1180
1332
|
const inode = this.readInode(idx);
|
|
1181
1333
|
const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
|
|
1182
1334
|
return { status: 0, data: encoder.encode(resolvedPath) };
|
|
@@ -1314,16 +1466,35 @@ var VFSEngine = class {
|
|
|
1314
1466
|
if (endPos > inode.size) {
|
|
1315
1467
|
const neededBlocks = Math.ceil(endPos / this.blockSize);
|
|
1316
1468
|
if (neededBlocks > inode.blockCount) {
|
|
1317
|
-
const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
1318
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
1319
1469
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1470
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
1471
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
1472
|
+
if (inode.size > 0) {
|
|
1473
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
1474
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
1475
|
+
let copied = 0;
|
|
1476
|
+
while (copied < inode.size) {
|
|
1477
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
1478
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
1479
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
1480
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
1481
|
+
copied += n;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
1485
|
+
if (pos > inode.size) {
|
|
1486
|
+
this.zeroFileRange(newBase + inode.size, pos - inode.size);
|
|
1487
|
+
}
|
|
1488
|
+
this.handle.write(data, { at: newBase + pos });
|
|
1324
1489
|
inode.firstBlock = newFirst;
|
|
1325
1490
|
inode.blockCount = neededBlocks;
|
|
1326
1491
|
} else {
|
|
1492
|
+
if (pos > inode.size) {
|
|
1493
|
+
this.zeroFileRange(
|
|
1494
|
+
this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
|
|
1495
|
+
pos - inode.size
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1327
1498
|
const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
|
|
1328
1499
|
this.handle.write(data, { at: dataOffset });
|
|
1329
1500
|
}
|
|
@@ -1346,6 +1517,7 @@ var VFSEngine = class {
|
|
|
1346
1517
|
fstat(fd) {
|
|
1347
1518
|
const entry = this.fdTable.get(fd);
|
|
1348
1519
|
if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
|
|
1520
|
+
if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
|
|
1349
1521
|
return this.encodeStatResponse(entry.inodeIdx);
|
|
1350
1522
|
}
|
|
1351
1523
|
// ---- FTRUNCATE ----
|
|
@@ -1368,6 +1540,7 @@ var VFSEngine = class {
|
|
|
1368
1540
|
fchmod(fd, mode) {
|
|
1369
1541
|
const entry = this.fdTable.get(fd);
|
|
1370
1542
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
1543
|
+
if (entry.implicitPath) return { status: 0 };
|
|
1371
1544
|
const inode = this.readInode(entry.inodeIdx);
|
|
1372
1545
|
inode.mode = inode.mode & S_IFMT | mode & 4095;
|
|
1373
1546
|
inode.ctime = Date.now();
|
|
@@ -1378,6 +1551,7 @@ var VFSEngine = class {
|
|
|
1378
1551
|
fchown(fd, uid, gid) {
|
|
1379
1552
|
const entry = this.fdTable.get(fd);
|
|
1380
1553
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
1554
|
+
if (entry.implicitPath) return { status: 0 };
|
|
1381
1555
|
const inode = this.readInode(entry.inodeIdx);
|
|
1382
1556
|
inode.uid = uid;
|
|
1383
1557
|
inode.gid = gid;
|
|
@@ -1389,6 +1563,7 @@ var VFSEngine = class {
|
|
|
1389
1563
|
futimes(fd, atime, mtime) {
|
|
1390
1564
|
const entry = this.fdTable.get(fd);
|
|
1391
1565
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
1566
|
+
if (entry.implicitPath) return { status: 0 };
|
|
1392
1567
|
const inode = this.readInode(entry.inodeIdx);
|
|
1393
1568
|
inode.atime = atime;
|
|
1394
1569
|
inode.mtime = mtime;
|
|
@@ -1400,7 +1575,16 @@ var VFSEngine = class {
|
|
|
1400
1575
|
opendir(path, tabId2) {
|
|
1401
1576
|
path = this.normalizePath(path);
|
|
1402
1577
|
const idx = this.resolvePathComponents(path, true);
|
|
1403
|
-
if (idx === void 0)
|
|
1578
|
+
if (idx === void 0) {
|
|
1579
|
+
if (this.isImplicitDirectory(path)) {
|
|
1580
|
+
const fd2 = this.nextFd++;
|
|
1581
|
+
this.fdTable.set(fd2, { tabId: tabId2, inodeIdx: -1, position: 0, flags: 0, implicitPath: path });
|
|
1582
|
+
const buf2 = new Uint8Array(4);
|
|
1583
|
+
new DataView(buf2.buffer).setUint32(0, fd2, true);
|
|
1584
|
+
return { status: 0, data: buf2 };
|
|
1585
|
+
}
|
|
1586
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
1587
|
+
}
|
|
1404
1588
|
const inode = this.readInode(idx);
|
|
1405
1589
|
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
1406
1590
|
const fd = this.nextFd++;
|
|
@@ -1439,6 +1623,106 @@ var VFSEngine = class {
|
|
|
1439
1623
|
}
|
|
1440
1624
|
return children.sort();
|
|
1441
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Rebuild the set of all implicit directory paths.
|
|
1628
|
+
* An implicit directory is any ancestor path of a file/symlink in pathIndex
|
|
1629
|
+
* that doesn't itself have an explicit inode entry.
|
|
1630
|
+
* Only rebuilt when pathIndex has changed (tracked via generation counter).
|
|
1631
|
+
*/
|
|
1632
|
+
rebuildImplicitDirs() {
|
|
1633
|
+
if (this.implicitDirsGen === this.pathIndexGen) return;
|
|
1634
|
+
const now = Date.now();
|
|
1635
|
+
const prev = this.implicitDirs;
|
|
1636
|
+
this.implicitDirs = /* @__PURE__ */ new Map();
|
|
1637
|
+
for (const filePath of this.pathIndex.keys()) {
|
|
1638
|
+
let pos = filePath.length;
|
|
1639
|
+
while (true) {
|
|
1640
|
+
pos = filePath.lastIndexOf("/", pos - 1);
|
|
1641
|
+
if (pos <= 0) break;
|
|
1642
|
+
const ancestor = filePath.substring(0, pos);
|
|
1643
|
+
if (this.implicitDirs.has(ancestor)) break;
|
|
1644
|
+
if (!this.pathIndex.has(ancestor)) {
|
|
1645
|
+
this.implicitDirs.set(ancestor, prev.get(ancestor) ?? now);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
this.implicitDirsGen = this.pathIndexGen;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Check if a path is an implicit directory (exists because files exist under it,
|
|
1653
|
+
* but no explicit directory inode was created for it).
|
|
1654
|
+
*/
|
|
1655
|
+
isImplicitDirectory(path) {
|
|
1656
|
+
if (path === "/") return false;
|
|
1657
|
+
this.rebuildImplicitDirs();
|
|
1658
|
+
return this.implicitDirs.has(path);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Get direct children of a directory path, including implicit subdirectories.
|
|
1662
|
+
* Returns unique child full paths. Each entry is tagged with whether it's a
|
|
1663
|
+
* real inode or an implicit directory.
|
|
1664
|
+
*/
|
|
1665
|
+
getDirectChildrenWithImplicit(dirPath) {
|
|
1666
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
1667
|
+
const childNames = /* @__PURE__ */ new Map();
|
|
1668
|
+
for (const path of this.pathIndex.keys()) {
|
|
1669
|
+
if (path === dirPath) continue;
|
|
1670
|
+
if (!path.startsWith(prefix)) continue;
|
|
1671
|
+
const rest = path.substring(prefix.length);
|
|
1672
|
+
const slashPos = rest.indexOf("/");
|
|
1673
|
+
if (slashPos === -1) {
|
|
1674
|
+
childNames.set(rest, "real");
|
|
1675
|
+
} else {
|
|
1676
|
+
const childName = rest.substring(0, slashPos);
|
|
1677
|
+
if (!childNames.has(childName)) {
|
|
1678
|
+
const childFullPath = prefix + childName;
|
|
1679
|
+
childNames.set(childName, this.pathIndex.has(childFullPath) ? "real" : "implicit");
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const result = [];
|
|
1684
|
+
for (const [name, type] of childNames) {
|
|
1685
|
+
result.push({ path: prefix + name, type });
|
|
1686
|
+
}
|
|
1687
|
+
result.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1688
|
+
return result;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Encode a synthetic stat response for an implicit directory.
|
|
1692
|
+
* Returns directory stats with default mode, zero size, current timestamps.
|
|
1693
|
+
*/
|
|
1694
|
+
encodeImplicitDirStatResponse(path) {
|
|
1695
|
+
this.rebuildImplicitDirs();
|
|
1696
|
+
const ts = this.implicitDirs.get(path) ?? Date.now();
|
|
1697
|
+
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
1698
|
+
const children = this.getDirectChildrenWithImplicit(path);
|
|
1699
|
+
let subdirCount = 0;
|
|
1700
|
+
for (const child of children) {
|
|
1701
|
+
if (child.type === "implicit") {
|
|
1702
|
+
subdirCount++;
|
|
1703
|
+
} else {
|
|
1704
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
1705
|
+
if (childIdx !== void 0) {
|
|
1706
|
+
const childInode = this.readInode(childIdx);
|
|
1707
|
+
if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const nlink = 2 + subdirCount;
|
|
1712
|
+
const buf = new Uint8Array(53);
|
|
1713
|
+
const view = new DataView(buf.buffer);
|
|
1714
|
+
view.setUint8(0, INODE_TYPE.DIRECTORY);
|
|
1715
|
+
view.setUint32(1, mode, true);
|
|
1716
|
+
view.setFloat64(5, 0, true);
|
|
1717
|
+
view.setFloat64(13, ts, true);
|
|
1718
|
+
view.setFloat64(21, ts, true);
|
|
1719
|
+
view.setFloat64(29, ts, true);
|
|
1720
|
+
view.setUint32(37, this.processUid, true);
|
|
1721
|
+
view.setUint32(41, this.processGid, true);
|
|
1722
|
+
view.setUint32(45, 0, true);
|
|
1723
|
+
view.setUint32(49, nlink, true);
|
|
1724
|
+
return { status: 0, data: buf };
|
|
1725
|
+
}
|
|
1442
1726
|
getAllDescendants(dirPath) {
|
|
1443
1727
|
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
1444
1728
|
const descendants = [];
|
|
@@ -1456,7 +1740,10 @@ var VFSEngine = class {
|
|
|
1456
1740
|
if (lastSlash <= 0) return 0;
|
|
1457
1741
|
const parentPath = path.substring(0, lastSlash);
|
|
1458
1742
|
const parentIdx = this.pathIndex.get(parentPath);
|
|
1459
|
-
if (parentIdx === void 0)
|
|
1743
|
+
if (parentIdx === void 0) {
|
|
1744
|
+
if (this.isImplicitDirectory(parentPath)) return 0;
|
|
1745
|
+
return CODE_TO_STATUS.ENOENT;
|
|
1746
|
+
}
|
|
1460
1747
|
const parentInode = this.readInode(parentIdx);
|
|
1461
1748
|
if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
|
|
1462
1749
|
return 0;
|