@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
package/dist/index.js
CHANGED
|
@@ -4204,6 +4204,16 @@ var VFSEngine = class {
|
|
|
4204
4204
|
superblockDirty = false;
|
|
4205
4205
|
// Free inode hint — skip O(n) scan
|
|
4206
4206
|
freeInodeHint = 0;
|
|
4207
|
+
// Implicit directory support — tracks all directory prefixes implied by file paths.
|
|
4208
|
+
// Rebuilt lazily when pathIndex changes (tracked via generation counter).
|
|
4209
|
+
// Map value is the stable timestamp (ms since epoch) assigned when the implicit
|
|
4210
|
+
// dir was first discovered, so that stat() returns consistent mtime/ctime/atime
|
|
4211
|
+
// across repeated calls.
|
|
4212
|
+
implicitDirs = /* @__PURE__ */ new Map();
|
|
4213
|
+
implicitDirsGen = -1;
|
|
4214
|
+
// generation when implicitDirs was last rebuilt
|
|
4215
|
+
pathIndexGen = 0;
|
|
4216
|
+
// bumped on every pathIndex mutation
|
|
4207
4217
|
// Configurable upper bounds
|
|
4208
4218
|
maxInodes = 4e6;
|
|
4209
4219
|
maxBlocks = 4e6;
|
|
@@ -4488,6 +4498,7 @@ var VFSEngine = class {
|
|
|
4488
4498
|
}
|
|
4489
4499
|
this.pathIndex.set(path, i);
|
|
4490
4500
|
}
|
|
4501
|
+
this.pathIndexGen++;
|
|
4491
4502
|
}
|
|
4492
4503
|
// ========== Low-level inode I/O ==========
|
|
4493
4504
|
readInode(idx) {
|
|
@@ -4560,14 +4571,23 @@ var VFSEngine = class {
|
|
|
4560
4571
|
growPathTable(needed) {
|
|
4561
4572
|
const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
|
|
4562
4573
|
const growth = newSize - this.pathTableSize;
|
|
4563
|
-
const dataSize = this.totalBlocks * this.blockSize;
|
|
4564
|
-
const dataBuf = new Uint8Array(dataSize);
|
|
4565
|
-
this.handle.read(dataBuf, { at: this.dataOffset });
|
|
4566
4574
|
const newTotalSize = this.handle.getSize() + growth;
|
|
4567
4575
|
this.handle.truncate(newTotalSize);
|
|
4576
|
+
const dataSize = this.totalBlocks * this.blockSize;
|
|
4577
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
4578
|
+
const scratch = new Uint8Array(Math.min(CHUNK, Math.max(dataSize, 1)));
|
|
4579
|
+
let remaining = dataSize;
|
|
4580
|
+
while (remaining > 0) {
|
|
4581
|
+
const chunk = Math.min(remaining, CHUNK);
|
|
4582
|
+
const srcAt = this.dataOffset + (remaining - chunk);
|
|
4583
|
+
const dstAt = this.dataOffset + growth + (remaining - chunk);
|
|
4584
|
+
const slice = chunk < scratch.length ? scratch.subarray(0, chunk) : scratch;
|
|
4585
|
+
this.handle.read(slice, { at: srcAt });
|
|
4586
|
+
this.handle.write(slice, { at: dstAt });
|
|
4587
|
+
remaining -= chunk;
|
|
4588
|
+
}
|
|
4568
4589
|
const newBitmapOffset = this.bitmapOffset + growth;
|
|
4569
4590
|
const newDataOffset = this.dataOffset + growth;
|
|
4570
|
-
this.handle.write(dataBuf, { at: newDataOffset });
|
|
4571
4591
|
this.handle.write(this.bitmap, { at: newBitmapOffset });
|
|
4572
4592
|
this.pathTableSize = newSize;
|
|
4573
4593
|
this.bitmapOffset = newBitmapOffset;
|
|
@@ -4575,6 +4595,23 @@ var VFSEngine = class {
|
|
|
4575
4595
|
this.superblockDirty = true;
|
|
4576
4596
|
}
|
|
4577
4597
|
// ========== Bitmap I/O ==========
|
|
4598
|
+
// Write `length` zero bytes at absolute file offset `at` via a small
|
|
4599
|
+
// reusable scratch buffer. Used to materialize POSIX "holes" when a
|
|
4600
|
+
// write starts past the current file size — those bytes must read as
|
|
4601
|
+
// zeros rather than whatever stale data happened to live in the
|
|
4602
|
+
// underlying storage blocks.
|
|
4603
|
+
zeroFileRange(at, length) {
|
|
4604
|
+
if (length <= 0) return;
|
|
4605
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
4606
|
+
const zeros = new Uint8Array(Math.min(length, CHUNK));
|
|
4607
|
+
let written = 0;
|
|
4608
|
+
while (written < length) {
|
|
4609
|
+
const n = Math.min(CHUNK, length - written);
|
|
4610
|
+
const slice = n < zeros.length ? zeros.subarray(0, n) : zeros;
|
|
4611
|
+
this.handle.write(slice, { at: at + written });
|
|
4612
|
+
written += n;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4578
4615
|
allocateBlocks(count) {
|
|
4579
4616
|
if (count === 0) return 0;
|
|
4580
4617
|
const bitmap = this.bitmap;
|
|
@@ -4782,6 +4819,7 @@ var VFSEngine = class {
|
|
|
4782
4819
|
};
|
|
4783
4820
|
this.writeInode(idx, inode);
|
|
4784
4821
|
this.pathIndex.set(path, idx);
|
|
4822
|
+
this.pathIndexGen++;
|
|
4785
4823
|
return idx;
|
|
4786
4824
|
}
|
|
4787
4825
|
// ========== Public API — called by server worker dispatch ==========
|
|
@@ -4899,17 +4937,28 @@ var VFSEngine = class {
|
|
|
4899
4937
|
}
|
|
4900
4938
|
const inode = this.readInode(existingIdx);
|
|
4901
4939
|
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
4902
|
-
const
|
|
4903
|
-
const
|
|
4904
|
-
combined.set(existing);
|
|
4905
|
-
combined.set(data, existing.byteLength);
|
|
4906
|
-
const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
|
|
4907
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
4940
|
+
const combinedSize = inode.size + data.byteLength;
|
|
4941
|
+
const neededBlocks = Math.ceil(combinedSize / this.blockSize);
|
|
4908
4942
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
4909
|
-
this.
|
|
4943
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
4944
|
+
if (inode.size > 0) {
|
|
4945
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
4946
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
4947
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
4948
|
+
let copied = 0;
|
|
4949
|
+
while (copied < inode.size) {
|
|
4950
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
4951
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
4952
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
4953
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
4954
|
+
copied += n;
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
4958
|
+
this.handle.write(data, { at: newBase + inode.size });
|
|
4910
4959
|
inode.firstBlock = newFirst;
|
|
4911
4960
|
inode.blockCount = neededBlocks;
|
|
4912
|
-
inode.size =
|
|
4961
|
+
inode.size = combinedSize;
|
|
4913
4962
|
inode.mtime = Date.now();
|
|
4914
4963
|
this.writeInode(existingIdx, inode);
|
|
4915
4964
|
this.commitPending();
|
|
@@ -4927,6 +4976,7 @@ var VFSEngine = class {
|
|
|
4927
4976
|
inode.type = INODE_TYPE.FREE;
|
|
4928
4977
|
this.writeInode(idx, inode);
|
|
4929
4978
|
this.pathIndex.delete(path);
|
|
4979
|
+
this.pathIndexGen++;
|
|
4930
4980
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
4931
4981
|
this.commitPending();
|
|
4932
4982
|
return { status: 0 };
|
|
@@ -4935,7 +4985,12 @@ var VFSEngine = class {
|
|
|
4935
4985
|
stat(path) {
|
|
4936
4986
|
path = this.normalizePath(path);
|
|
4937
4987
|
const idx = this.resolvePathComponents(path, true);
|
|
4938
|
-
if (idx === void 0)
|
|
4988
|
+
if (idx === void 0) {
|
|
4989
|
+
if (this.isImplicitDirectory(path)) {
|
|
4990
|
+
return this.encodeImplicitDirStatResponse(path);
|
|
4991
|
+
}
|
|
4992
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
4993
|
+
}
|
|
4939
4994
|
return this.encodeStatResponse(idx);
|
|
4940
4995
|
}
|
|
4941
4996
|
// ---- LSTAT (no symlink follow for the FINAL component) ----
|
|
@@ -4944,7 +4999,12 @@ var VFSEngine = class {
|
|
|
4944
4999
|
let idx = this.resolvePathComponents(path, false);
|
|
4945
5000
|
if (idx === void 0) {
|
|
4946
5001
|
idx = this.resolvePathComponents(path, true);
|
|
4947
|
-
if (idx === void 0)
|
|
5002
|
+
if (idx === void 0) {
|
|
5003
|
+
if (this.isImplicitDirectory(path)) {
|
|
5004
|
+
return this.encodeImplicitDirStatResponse(path);
|
|
5005
|
+
}
|
|
5006
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
5007
|
+
}
|
|
4948
5008
|
}
|
|
4949
5009
|
return this.encodeStatResponse(idx);
|
|
4950
5010
|
}
|
|
@@ -4953,13 +5013,17 @@ var VFSEngine = class {
|
|
|
4953
5013
|
let nlink = inode.nlink;
|
|
4954
5014
|
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
4955
5015
|
const path = this.readPath(inode.pathOffset, inode.pathLength);
|
|
4956
|
-
const children = this.
|
|
5016
|
+
const children = this.getDirectChildrenWithImplicit(path);
|
|
4957
5017
|
let subdirCount = 0;
|
|
4958
5018
|
for (const child of children) {
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
5019
|
+
if (child.type === "implicit") {
|
|
5020
|
+
subdirCount++;
|
|
5021
|
+
} else {
|
|
5022
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
5023
|
+
if (childIdx !== void 0) {
|
|
5024
|
+
const childInode = this.readInode(childIdx);
|
|
5025
|
+
if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
|
|
5026
|
+
}
|
|
4963
5027
|
}
|
|
4964
5028
|
}
|
|
4965
5029
|
nlink = 2 + subdirCount;
|
|
@@ -4985,7 +5049,9 @@ var VFSEngine = class {
|
|
|
4985
5049
|
if (recursive) {
|
|
4986
5050
|
return this.mkdirRecursive(path);
|
|
4987
5051
|
}
|
|
4988
|
-
if (this.pathIndex.has(path)
|
|
5052
|
+
if (this.pathIndex.has(path) || this.isImplicitDirectory(path)) {
|
|
5053
|
+
return { status: CODE_TO_STATUS.EEXIST, data: null };
|
|
5054
|
+
}
|
|
4989
5055
|
const parentStatus = this.ensureParent(path);
|
|
4990
5056
|
if (parentStatus !== 0) return { status: parentStatus, data: null };
|
|
4991
5057
|
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
@@ -5020,7 +5086,26 @@ var VFSEngine = class {
|
|
|
5020
5086
|
path = this.normalizePath(path);
|
|
5021
5087
|
const recursive = (flags & 1) !== 0;
|
|
5022
5088
|
const idx = this.pathIndex.get(path);
|
|
5023
|
-
if (idx === void 0)
|
|
5089
|
+
if (idx === void 0) {
|
|
5090
|
+
if (this.isImplicitDirectory(path)) {
|
|
5091
|
+
const children2 = this.getDirectChildrenWithImplicit(path);
|
|
5092
|
+
if (children2.length > 0) {
|
|
5093
|
+
if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
|
|
5094
|
+
for (const desc of this.getAllDescendants(path)) {
|
|
5095
|
+
const descIdx = this.pathIndex.get(desc);
|
|
5096
|
+
const descInode = this.readInode(descIdx);
|
|
5097
|
+
this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
|
|
5098
|
+
descInode.type = INODE_TYPE.FREE;
|
|
5099
|
+
this.writeInode(descIdx, descInode);
|
|
5100
|
+
this.pathIndex.delete(desc);
|
|
5101
|
+
}
|
|
5102
|
+
this.pathIndexGen++;
|
|
5103
|
+
this.commitPending();
|
|
5104
|
+
}
|
|
5105
|
+
return { status: 0 };
|
|
5106
|
+
}
|
|
5107
|
+
return { status: CODE_TO_STATUS.ENOENT };
|
|
5108
|
+
}
|
|
5024
5109
|
const inode = this.readInode(idx);
|
|
5025
5110
|
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
|
|
5026
5111
|
const children = this.getDirectChildren(path);
|
|
@@ -5038,6 +5123,7 @@ var VFSEngine = class {
|
|
|
5038
5123
|
inode.type = INODE_TYPE.FREE;
|
|
5039
5124
|
this.writeInode(idx, inode);
|
|
5040
5125
|
this.pathIndex.delete(path);
|
|
5126
|
+
this.pathIndexGen++;
|
|
5041
5127
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
5042
5128
|
this.commitPending();
|
|
5043
5129
|
return { status: 0 };
|
|
@@ -5046,20 +5132,33 @@ var VFSEngine = class {
|
|
|
5046
5132
|
readdir(path, flags = 0) {
|
|
5047
5133
|
path = this.normalizePath(path);
|
|
5048
5134
|
const resolved = this.resolvePathFull(path, true);
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5135
|
+
let effectiveDirPath;
|
|
5136
|
+
if (resolved) {
|
|
5137
|
+
const inode = this.readInode(resolved.idx);
|
|
5138
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
5139
|
+
effectiveDirPath = resolved.resolvedPath;
|
|
5140
|
+
} else if (this.isImplicitDirectory(path)) {
|
|
5141
|
+
effectiveDirPath = path;
|
|
5142
|
+
} else {
|
|
5143
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
5144
|
+
}
|
|
5052
5145
|
const withFileTypes = (flags & 1) !== 0;
|
|
5053
|
-
const children = this.
|
|
5146
|
+
const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
|
|
5054
5147
|
if (withFileTypes) {
|
|
5055
5148
|
let totalSize2 = 4;
|
|
5056
5149
|
const entries = [];
|
|
5057
|
-
for (const
|
|
5058
|
-
const name =
|
|
5150
|
+
for (const child of children) {
|
|
5151
|
+
const name = child.path.substring(child.path.lastIndexOf("/") + 1);
|
|
5059
5152
|
const nameBytes = encoder10.encode(name);
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5153
|
+
let type;
|
|
5154
|
+
if (child.type === "implicit") {
|
|
5155
|
+
type = INODE_TYPE.DIRECTORY;
|
|
5156
|
+
} else {
|
|
5157
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
5158
|
+
const childInode = this.readInode(childIdx);
|
|
5159
|
+
type = childInode.type;
|
|
5160
|
+
}
|
|
5161
|
+
entries.push({ name: nameBytes, type });
|
|
5063
5162
|
totalSize2 += 2 + nameBytes.byteLength + 1;
|
|
5064
5163
|
}
|
|
5065
5164
|
const buf2 = new Uint8Array(totalSize2);
|
|
@@ -5077,8 +5176,8 @@ var VFSEngine = class {
|
|
|
5077
5176
|
}
|
|
5078
5177
|
let totalSize = 4;
|
|
5079
5178
|
const nameEntries = [];
|
|
5080
|
-
for (const
|
|
5081
|
-
const name =
|
|
5179
|
+
for (const child of children) {
|
|
5180
|
+
const name = child.path.substring(child.path.lastIndexOf("/") + 1);
|
|
5082
5181
|
const nameBytes = encoder10.encode(name);
|
|
5083
5182
|
nameEntries.push(nameBytes);
|
|
5084
5183
|
totalSize += 2 + nameBytes.byteLength;
|
|
@@ -5119,6 +5218,7 @@ var VFSEngine = class {
|
|
|
5119
5218
|
this.writeInode(idx, inode);
|
|
5120
5219
|
this.pathIndex.delete(oldPath);
|
|
5121
5220
|
this.pathIndex.set(newPath, idx);
|
|
5221
|
+
this.pathIndexGen++;
|
|
5122
5222
|
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
5123
5223
|
const prefix = oldPath === "/" ? "/" : oldPath + "/";
|
|
5124
5224
|
const toRename = [];
|
|
@@ -5147,7 +5247,7 @@ var VFSEngine = class {
|
|
|
5147
5247
|
path = this.normalizePath(path);
|
|
5148
5248
|
const idx = this.resolvePathComponents(path, true);
|
|
5149
5249
|
const buf = new Uint8Array(1);
|
|
5150
|
-
buf[0] = idx !== void 0 ? 1 : 0;
|
|
5250
|
+
buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
|
|
5151
5251
|
return { status: 0, data: buf };
|
|
5152
5252
|
}
|
|
5153
5253
|
// ---- TRUNCATE ----
|
|
@@ -5172,13 +5272,29 @@ var VFSEngine = class {
|
|
|
5172
5272
|
} else if (len > inode.size) {
|
|
5173
5273
|
const neededBlocks = Math.ceil(len / this.blockSize);
|
|
5174
5274
|
if (neededBlocks > inode.blockCount) {
|
|
5175
|
-
const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
|
|
5176
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
5177
5275
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
5178
|
-
const
|
|
5179
|
-
|
|
5180
|
-
|
|
5276
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
5277
|
+
if (inode.size > 0) {
|
|
5278
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
5279
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
5280
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
5281
|
+
let copied = 0;
|
|
5282
|
+
while (copied < inode.size) {
|
|
5283
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
5284
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
5285
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
5286
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
5287
|
+
copied += n;
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
5291
|
+
this.zeroFileRange(newBase + inode.size, len - inode.size);
|
|
5181
5292
|
inode.firstBlock = newFirst;
|
|
5293
|
+
} else {
|
|
5294
|
+
this.zeroFileRange(
|
|
5295
|
+
this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
|
|
5296
|
+
len - inode.size
|
|
5297
|
+
);
|
|
5182
5298
|
}
|
|
5183
5299
|
inode.blockCount = neededBlocks;
|
|
5184
5300
|
inode.size = len;
|
|
@@ -5199,14 +5315,45 @@ var VFSEngine = class {
|
|
|
5199
5315
|
if (flags & 1 && this.pathIndex.has(destPath)) {
|
|
5200
5316
|
return { status: CODE_TO_STATUS.EEXIST };
|
|
5201
5317
|
}
|
|
5202
|
-
|
|
5203
|
-
|
|
5318
|
+
if (srcPath === destPath) return { status: 0 };
|
|
5319
|
+
const srcSize = srcInode.size;
|
|
5320
|
+
const srcFirstBlock = srcInode.firstBlock;
|
|
5321
|
+
const emptyStatus = this.write(destPath, new Uint8Array(0));
|
|
5322
|
+
if (emptyStatus.status !== 0) return emptyStatus;
|
|
5323
|
+
if (srcSize === 0) return { status: 0 };
|
|
5324
|
+
const destIdx = this.resolvePathComponents(destPath, true);
|
|
5325
|
+
if (destIdx === void 0) return { status: CODE_TO_STATUS.EIO };
|
|
5326
|
+
const destInode = this.readInode(destIdx);
|
|
5327
|
+
const neededBlocks = Math.ceil(srcSize / this.blockSize);
|
|
5328
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
5329
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
5330
|
+
const srcBase = this.dataOffset + srcFirstBlock * this.blockSize;
|
|
5331
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
5332
|
+
const scratch = new Uint8Array(Math.min(CHUNK, srcSize));
|
|
5333
|
+
let copied = 0;
|
|
5334
|
+
while (copied < srcSize) {
|
|
5335
|
+
const n = Math.min(CHUNK, srcSize - copied);
|
|
5336
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
5337
|
+
this.handle.read(slice, { at: srcBase + copied });
|
|
5338
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
5339
|
+
copied += n;
|
|
5340
|
+
}
|
|
5341
|
+
destInode.firstBlock = newFirst;
|
|
5342
|
+
destInode.blockCount = neededBlocks;
|
|
5343
|
+
destInode.size = srcSize;
|
|
5344
|
+
destInode.mtime = Date.now();
|
|
5345
|
+
this.writeInode(destIdx, destInode);
|
|
5346
|
+
this.commitPending();
|
|
5347
|
+
return { status: 0 };
|
|
5204
5348
|
}
|
|
5205
5349
|
// ---- ACCESS ----
|
|
5206
5350
|
access(path, mode = 0) {
|
|
5207
5351
|
path = this.normalizePath(path);
|
|
5208
5352
|
const idx = this.resolvePathComponents(path, true);
|
|
5209
|
-
if (idx === void 0)
|
|
5353
|
+
if (idx === void 0) {
|
|
5354
|
+
if (this.isImplicitDirectory(path)) return { status: 0 };
|
|
5355
|
+
return { status: CODE_TO_STATUS.ENOENT };
|
|
5356
|
+
}
|
|
5210
5357
|
if (mode === 0) return { status: 0 };
|
|
5211
5358
|
if (!this.strictPermissions) return { status: 0 };
|
|
5212
5359
|
const inode = this.readInode(idx);
|
|
@@ -5226,7 +5373,12 @@ var VFSEngine = class {
|
|
|
5226
5373
|
realpath(path) {
|
|
5227
5374
|
path = this.normalizePath(path);
|
|
5228
5375
|
const idx = this.resolvePathComponents(path, true);
|
|
5229
|
-
if (idx === void 0)
|
|
5376
|
+
if (idx === void 0) {
|
|
5377
|
+
if (this.isImplicitDirectory(path)) {
|
|
5378
|
+
return { status: 0, data: encoder10.encode(path) };
|
|
5379
|
+
}
|
|
5380
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
5381
|
+
}
|
|
5230
5382
|
const inode = this.readInode(idx);
|
|
5231
5383
|
const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
|
|
5232
5384
|
return { status: 0, data: encoder10.encode(resolvedPath) };
|
|
@@ -5364,16 +5516,35 @@ var VFSEngine = class {
|
|
|
5364
5516
|
if (endPos > inode.size) {
|
|
5365
5517
|
const neededBlocks = Math.ceil(endPos / this.blockSize);
|
|
5366
5518
|
if (neededBlocks > inode.blockCount) {
|
|
5367
|
-
const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
5368
|
-
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
5369
5519
|
const newFirst = this.allocateBlocks(neededBlocks);
|
|
5370
|
-
const
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5520
|
+
const newBase = this.dataOffset + newFirst * this.blockSize;
|
|
5521
|
+
const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
|
|
5522
|
+
if (inode.size > 0) {
|
|
5523
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
5524
|
+
const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
|
|
5525
|
+
let copied = 0;
|
|
5526
|
+
while (copied < inode.size) {
|
|
5527
|
+
const n = Math.min(CHUNK, inode.size - copied);
|
|
5528
|
+
const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
|
|
5529
|
+
this.handle.read(slice, { at: oldBase + copied });
|
|
5530
|
+
this.handle.write(slice, { at: newBase + copied });
|
|
5531
|
+
copied += n;
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
5535
|
+
if (pos > inode.size) {
|
|
5536
|
+
this.zeroFileRange(newBase + inode.size, pos - inode.size);
|
|
5537
|
+
}
|
|
5538
|
+
this.handle.write(data, { at: newBase + pos });
|
|
5374
5539
|
inode.firstBlock = newFirst;
|
|
5375
5540
|
inode.blockCount = neededBlocks;
|
|
5376
5541
|
} else {
|
|
5542
|
+
if (pos > inode.size) {
|
|
5543
|
+
this.zeroFileRange(
|
|
5544
|
+
this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
|
|
5545
|
+
pos - inode.size
|
|
5546
|
+
);
|
|
5547
|
+
}
|
|
5377
5548
|
const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
|
|
5378
5549
|
this.handle.write(data, { at: dataOffset });
|
|
5379
5550
|
}
|
|
@@ -5396,6 +5567,7 @@ var VFSEngine = class {
|
|
|
5396
5567
|
fstat(fd) {
|
|
5397
5568
|
const entry = this.fdTable.get(fd);
|
|
5398
5569
|
if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
|
|
5570
|
+
if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
|
|
5399
5571
|
return this.encodeStatResponse(entry.inodeIdx);
|
|
5400
5572
|
}
|
|
5401
5573
|
// ---- FTRUNCATE ----
|
|
@@ -5418,6 +5590,7 @@ var VFSEngine = class {
|
|
|
5418
5590
|
fchmod(fd, mode) {
|
|
5419
5591
|
const entry = this.fdTable.get(fd);
|
|
5420
5592
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
5593
|
+
if (entry.implicitPath) return { status: 0 };
|
|
5421
5594
|
const inode = this.readInode(entry.inodeIdx);
|
|
5422
5595
|
inode.mode = inode.mode & S_IFMT | mode & 4095;
|
|
5423
5596
|
inode.ctime = Date.now();
|
|
@@ -5428,6 +5601,7 @@ var VFSEngine = class {
|
|
|
5428
5601
|
fchown(fd, uid, gid) {
|
|
5429
5602
|
const entry = this.fdTable.get(fd);
|
|
5430
5603
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
5604
|
+
if (entry.implicitPath) return { status: 0 };
|
|
5431
5605
|
const inode = this.readInode(entry.inodeIdx);
|
|
5432
5606
|
inode.uid = uid;
|
|
5433
5607
|
inode.gid = gid;
|
|
@@ -5439,6 +5613,7 @@ var VFSEngine = class {
|
|
|
5439
5613
|
futimes(fd, atime, mtime) {
|
|
5440
5614
|
const entry = this.fdTable.get(fd);
|
|
5441
5615
|
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
5616
|
+
if (entry.implicitPath) return { status: 0 };
|
|
5442
5617
|
const inode = this.readInode(entry.inodeIdx);
|
|
5443
5618
|
inode.atime = atime;
|
|
5444
5619
|
inode.mtime = mtime;
|
|
@@ -5450,7 +5625,16 @@ var VFSEngine = class {
|
|
|
5450
5625
|
opendir(path, tabId) {
|
|
5451
5626
|
path = this.normalizePath(path);
|
|
5452
5627
|
const idx = this.resolvePathComponents(path, true);
|
|
5453
|
-
if (idx === void 0)
|
|
5628
|
+
if (idx === void 0) {
|
|
5629
|
+
if (this.isImplicitDirectory(path)) {
|
|
5630
|
+
const fd2 = this.nextFd++;
|
|
5631
|
+
this.fdTable.set(fd2, { tabId, inodeIdx: -1, position: 0, flags: 0, implicitPath: path });
|
|
5632
|
+
const buf2 = new Uint8Array(4);
|
|
5633
|
+
new DataView(buf2.buffer).setUint32(0, fd2, true);
|
|
5634
|
+
return { status: 0, data: buf2 };
|
|
5635
|
+
}
|
|
5636
|
+
return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
5637
|
+
}
|
|
5454
5638
|
const inode = this.readInode(idx);
|
|
5455
5639
|
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
5456
5640
|
const fd = this.nextFd++;
|
|
@@ -5489,6 +5673,106 @@ var VFSEngine = class {
|
|
|
5489
5673
|
}
|
|
5490
5674
|
return children.sort();
|
|
5491
5675
|
}
|
|
5676
|
+
/**
|
|
5677
|
+
* Rebuild the set of all implicit directory paths.
|
|
5678
|
+
* An implicit directory is any ancestor path of a file/symlink in pathIndex
|
|
5679
|
+
* that doesn't itself have an explicit inode entry.
|
|
5680
|
+
* Only rebuilt when pathIndex has changed (tracked via generation counter).
|
|
5681
|
+
*/
|
|
5682
|
+
rebuildImplicitDirs() {
|
|
5683
|
+
if (this.implicitDirsGen === this.pathIndexGen) return;
|
|
5684
|
+
const now = Date.now();
|
|
5685
|
+
const prev = this.implicitDirs;
|
|
5686
|
+
this.implicitDirs = /* @__PURE__ */ new Map();
|
|
5687
|
+
for (const filePath of this.pathIndex.keys()) {
|
|
5688
|
+
let pos = filePath.length;
|
|
5689
|
+
while (true) {
|
|
5690
|
+
pos = filePath.lastIndexOf("/", pos - 1);
|
|
5691
|
+
if (pos <= 0) break;
|
|
5692
|
+
const ancestor = filePath.substring(0, pos);
|
|
5693
|
+
if (this.implicitDirs.has(ancestor)) break;
|
|
5694
|
+
if (!this.pathIndex.has(ancestor)) {
|
|
5695
|
+
this.implicitDirs.set(ancestor, prev.get(ancestor) ?? now);
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
this.implicitDirsGen = this.pathIndexGen;
|
|
5700
|
+
}
|
|
5701
|
+
/**
|
|
5702
|
+
* Check if a path is an implicit directory (exists because files exist under it,
|
|
5703
|
+
* but no explicit directory inode was created for it).
|
|
5704
|
+
*/
|
|
5705
|
+
isImplicitDirectory(path) {
|
|
5706
|
+
if (path === "/") return false;
|
|
5707
|
+
this.rebuildImplicitDirs();
|
|
5708
|
+
return this.implicitDirs.has(path);
|
|
5709
|
+
}
|
|
5710
|
+
/**
|
|
5711
|
+
* Get direct children of a directory path, including implicit subdirectories.
|
|
5712
|
+
* Returns unique child full paths. Each entry is tagged with whether it's a
|
|
5713
|
+
* real inode or an implicit directory.
|
|
5714
|
+
*/
|
|
5715
|
+
getDirectChildrenWithImplicit(dirPath) {
|
|
5716
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
5717
|
+
const childNames = /* @__PURE__ */ new Map();
|
|
5718
|
+
for (const path of this.pathIndex.keys()) {
|
|
5719
|
+
if (path === dirPath) continue;
|
|
5720
|
+
if (!path.startsWith(prefix)) continue;
|
|
5721
|
+
const rest = path.substring(prefix.length);
|
|
5722
|
+
const slashPos = rest.indexOf("/");
|
|
5723
|
+
if (slashPos === -1) {
|
|
5724
|
+
childNames.set(rest, "real");
|
|
5725
|
+
} else {
|
|
5726
|
+
const childName = rest.substring(0, slashPos);
|
|
5727
|
+
if (!childNames.has(childName)) {
|
|
5728
|
+
const childFullPath = prefix + childName;
|
|
5729
|
+
childNames.set(childName, this.pathIndex.has(childFullPath) ? "real" : "implicit");
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
const result = [];
|
|
5734
|
+
for (const [name, type] of childNames) {
|
|
5735
|
+
result.push({ path: prefix + name, type });
|
|
5736
|
+
}
|
|
5737
|
+
result.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5738
|
+
return result;
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* Encode a synthetic stat response for an implicit directory.
|
|
5742
|
+
* Returns directory stats with default mode, zero size, current timestamps.
|
|
5743
|
+
*/
|
|
5744
|
+
encodeImplicitDirStatResponse(path) {
|
|
5745
|
+
this.rebuildImplicitDirs();
|
|
5746
|
+
const ts = this.implicitDirs.get(path) ?? Date.now();
|
|
5747
|
+
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
5748
|
+
const children = this.getDirectChildrenWithImplicit(path);
|
|
5749
|
+
let subdirCount = 0;
|
|
5750
|
+
for (const child of children) {
|
|
5751
|
+
if (child.type === "implicit") {
|
|
5752
|
+
subdirCount++;
|
|
5753
|
+
} else {
|
|
5754
|
+
const childIdx = this.pathIndex.get(child.path);
|
|
5755
|
+
if (childIdx !== void 0) {
|
|
5756
|
+
const childInode = this.readInode(childIdx);
|
|
5757
|
+
if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
const nlink = 2 + subdirCount;
|
|
5762
|
+
const buf = new Uint8Array(53);
|
|
5763
|
+
const view = new DataView(buf.buffer);
|
|
5764
|
+
view.setUint8(0, INODE_TYPE.DIRECTORY);
|
|
5765
|
+
view.setUint32(1, mode, true);
|
|
5766
|
+
view.setFloat64(5, 0, true);
|
|
5767
|
+
view.setFloat64(13, ts, true);
|
|
5768
|
+
view.setFloat64(21, ts, true);
|
|
5769
|
+
view.setFloat64(29, ts, true);
|
|
5770
|
+
view.setUint32(37, this.processUid, true);
|
|
5771
|
+
view.setUint32(41, this.processGid, true);
|
|
5772
|
+
view.setUint32(45, 0, true);
|
|
5773
|
+
view.setUint32(49, nlink, true);
|
|
5774
|
+
return { status: 0, data: buf };
|
|
5775
|
+
}
|
|
5492
5776
|
getAllDescendants(dirPath) {
|
|
5493
5777
|
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
5494
5778
|
const descendants = [];
|
|
@@ -5506,7 +5790,10 @@ var VFSEngine = class {
|
|
|
5506
5790
|
if (lastSlash <= 0) return 0;
|
|
5507
5791
|
const parentPath = path.substring(0, lastSlash);
|
|
5508
5792
|
const parentIdx = this.pathIndex.get(parentPath);
|
|
5509
|
-
if (parentIdx === void 0)
|
|
5793
|
+
if (parentIdx === void 0) {
|
|
5794
|
+
if (this.isImplicitDirectory(parentPath)) return 0;
|
|
5795
|
+
return CODE_TO_STATUS.ENOENT;
|
|
5796
|
+
}
|
|
5510
5797
|
const parentInode = this.readInode(parentIdx);
|
|
5511
5798
|
if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
|
|
5512
5799
|
return 0;
|