@componentor/fs 3.0.2 → 3.0.4
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 +74 -16
- package/dist/index.js +1773 -29
- package/dist/index.js.map +1 -1
- package/dist/workers/server.worker.js +88 -13
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +167 -33
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -129,6 +129,15 @@ var STATUS_TO_CODE = {
|
|
|
129
129
|
9: "ELOOP",
|
|
130
130
|
10: "ENOSPC"
|
|
131
131
|
};
|
|
132
|
+
var CODE_TO_STATUS = {
|
|
133
|
+
ENOENT: 1,
|
|
134
|
+
EEXIST: 2,
|
|
135
|
+
EISDIR: 3,
|
|
136
|
+
ENOTDIR: 4,
|
|
137
|
+
ENOTEMPTY: 5,
|
|
138
|
+
EACCES: 6,
|
|
139
|
+
EINVAL: 7,
|
|
140
|
+
EBADF: 8};
|
|
132
141
|
function createError(code, syscall, path) {
|
|
133
142
|
const errno = ErrorCodes[code] ?? -1;
|
|
134
143
|
const messages = {
|
|
@@ -293,11 +302,99 @@ async function unlink(asyncRequest, filePath) {
|
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
// src/vfs/layout.ts
|
|
305
|
+
var VFS_MAGIC = 1447449377;
|
|
306
|
+
var VFS_VERSION = 1;
|
|
307
|
+
var DEFAULT_BLOCK_SIZE = 4096;
|
|
308
|
+
var DEFAULT_INODE_COUNT = 1e4;
|
|
309
|
+
var INODE_SIZE = 64;
|
|
310
|
+
var SUPERBLOCK = {
|
|
311
|
+
SIZE: 64,
|
|
312
|
+
MAGIC: 0,
|
|
313
|
+
// uint32 - 0x56465321
|
|
314
|
+
VERSION: 4,
|
|
315
|
+
// uint32
|
|
316
|
+
INODE_COUNT: 8,
|
|
317
|
+
// uint32 - total inodes allocated
|
|
318
|
+
BLOCK_SIZE: 12,
|
|
319
|
+
// uint32 - data block size (default 4096)
|
|
320
|
+
TOTAL_BLOCKS: 16,
|
|
321
|
+
// uint32 - total data blocks
|
|
322
|
+
FREE_BLOCKS: 20,
|
|
323
|
+
// uint32 - available data blocks
|
|
324
|
+
INODE_OFFSET: 24,
|
|
325
|
+
// float64 - byte offset to inode table
|
|
326
|
+
PATH_OFFSET: 32,
|
|
327
|
+
// float64 - byte offset to path table
|
|
328
|
+
DATA_OFFSET: 40,
|
|
329
|
+
// float64 - byte offset to data region
|
|
330
|
+
BITMAP_OFFSET: 48,
|
|
331
|
+
// float64 - byte offset to free block bitmap
|
|
332
|
+
PATH_USED: 56};
|
|
333
|
+
var INODE = {
|
|
334
|
+
TYPE: 0,
|
|
335
|
+
// uint8 - 0=free, 1=file, 2=directory, 3=symlink
|
|
336
|
+
FLAGS: 1,
|
|
337
|
+
// uint8[3] - reserved
|
|
338
|
+
PATH_OFFSET: 4,
|
|
339
|
+
// uint32 - byte offset into path table
|
|
340
|
+
PATH_LENGTH: 8,
|
|
341
|
+
// uint16 - length of path string
|
|
342
|
+
RESERVED_10: 10,
|
|
343
|
+
// uint16
|
|
344
|
+
MODE: 12,
|
|
345
|
+
// uint32 - permissions (e.g. 0o100644)
|
|
346
|
+
SIZE: 16,
|
|
347
|
+
// float64 - file content size in bytes (using f64 for >4GB)
|
|
348
|
+
FIRST_BLOCK: 24,
|
|
349
|
+
// uint32 - index of first data block
|
|
350
|
+
BLOCK_COUNT: 28,
|
|
351
|
+
// uint32 - number of contiguous data blocks
|
|
352
|
+
MTIME: 32,
|
|
353
|
+
// float64 - last modification time (ms since epoch)
|
|
354
|
+
CTIME: 40,
|
|
355
|
+
// float64 - creation/change time (ms since epoch)
|
|
356
|
+
ATIME: 48,
|
|
357
|
+
// float64 - last access time (ms since epoch)
|
|
358
|
+
UID: 56,
|
|
359
|
+
// uint32 - owner
|
|
360
|
+
GID: 60
|
|
361
|
+
// uint32 - group
|
|
362
|
+
};
|
|
296
363
|
var INODE_TYPE = {
|
|
364
|
+
FREE: 0,
|
|
297
365
|
FILE: 1,
|
|
298
366
|
DIRECTORY: 2,
|
|
299
367
|
SYMLINK: 3
|
|
300
368
|
};
|
|
369
|
+
var DEFAULT_FILE_MODE = 33188;
|
|
370
|
+
var DEFAULT_DIR_MODE = 16877;
|
|
371
|
+
var DEFAULT_SYMLINK_MODE = 41471;
|
|
372
|
+
var DEFAULT_UMASK = 18;
|
|
373
|
+
var S_IFMT = 61440;
|
|
374
|
+
var MAX_SYMLINK_DEPTH = 40;
|
|
375
|
+
var INITIAL_PATH_TABLE_SIZE = 256 * 1024;
|
|
376
|
+
var INITIAL_DATA_BLOCKS = 1024;
|
|
377
|
+
function calculateLayout(inodeCount = DEFAULT_INODE_COUNT, blockSize = DEFAULT_BLOCK_SIZE, totalBlocks = INITIAL_DATA_BLOCKS) {
|
|
378
|
+
const inodeTableOffset = SUPERBLOCK.SIZE;
|
|
379
|
+
const inodeTableSize = inodeCount * INODE_SIZE;
|
|
380
|
+
const pathTableOffset = inodeTableOffset + inodeTableSize;
|
|
381
|
+
const pathTableSize = INITIAL_PATH_TABLE_SIZE;
|
|
382
|
+
const bitmapOffset = pathTableOffset + pathTableSize;
|
|
383
|
+
const bitmapSize = Math.ceil(totalBlocks / 8);
|
|
384
|
+
const dataOffset = Math.ceil((bitmapOffset + bitmapSize) / blockSize) * blockSize;
|
|
385
|
+
const totalSize = dataOffset + totalBlocks * blockSize;
|
|
386
|
+
return {
|
|
387
|
+
inodeTableOffset,
|
|
388
|
+
inodeTableSize,
|
|
389
|
+
pathTableOffset,
|
|
390
|
+
pathTableSize,
|
|
391
|
+
bitmapOffset,
|
|
392
|
+
bitmapSize,
|
|
393
|
+
dataOffset,
|
|
394
|
+
totalSize,
|
|
395
|
+
totalBlocks
|
|
396
|
+
};
|
|
397
|
+
}
|
|
301
398
|
|
|
302
399
|
// src/stats.ts
|
|
303
400
|
function decodeStats(data) {
|
|
@@ -345,13 +442,13 @@ function decodeStats(data) {
|
|
|
345
442
|
function decodeDirents(data) {
|
|
346
443
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
347
444
|
const count = view.getUint32(0, true);
|
|
348
|
-
const
|
|
445
|
+
const decoder9 = new TextDecoder();
|
|
349
446
|
const entries = [];
|
|
350
447
|
let offset = 4;
|
|
351
448
|
for (let i = 0; i < count; i++) {
|
|
352
449
|
const nameLen = view.getUint16(offset, true);
|
|
353
450
|
offset += 2;
|
|
354
|
-
const name =
|
|
451
|
+
const name = decoder9.decode(data.subarray(offset, offset + nameLen));
|
|
355
452
|
offset += nameLen;
|
|
356
453
|
const type = data[offset++];
|
|
357
454
|
const isFile = type === INODE_TYPE.FILE;
|
|
@@ -373,13 +470,13 @@ function decodeDirents(data) {
|
|
|
373
470
|
function decodeNames(data) {
|
|
374
471
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
375
472
|
const count = view.getUint32(0, true);
|
|
376
|
-
const
|
|
473
|
+
const decoder9 = new TextDecoder();
|
|
377
474
|
const names = [];
|
|
378
475
|
let offset = 4;
|
|
379
476
|
for (let i = 0; i < count; i++) {
|
|
380
477
|
const nameLen = view.getUint16(offset, true);
|
|
381
478
|
offset += 2;
|
|
382
|
-
names.push(
|
|
479
|
+
names.push(decoder9.decode(data.subarray(offset, offset + nameLen)));
|
|
383
480
|
offset += nameLen;
|
|
384
481
|
}
|
|
385
482
|
return names;
|
|
@@ -939,33 +1036,233 @@ function format(obj) {
|
|
|
939
1036
|
}
|
|
940
1037
|
|
|
941
1038
|
// src/methods/watch.ts
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1039
|
+
var watchers = /* @__PURE__ */ new Set();
|
|
1040
|
+
var fileWatchers = /* @__PURE__ */ new Map();
|
|
1041
|
+
var bc = null;
|
|
1042
|
+
var bcRefCount = 0;
|
|
1043
|
+
function ensureBc() {
|
|
1044
|
+
if (bc) {
|
|
1045
|
+
bcRefCount++;
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
bc = new BroadcastChannel("vfs-watch");
|
|
1049
|
+
bcRefCount = 1;
|
|
1050
|
+
bc.onmessage = onBroadcast;
|
|
1051
|
+
}
|
|
1052
|
+
function releaseBc() {
|
|
1053
|
+
if (--bcRefCount <= 0 && bc) {
|
|
1054
|
+
bc.close();
|
|
1055
|
+
bc = null;
|
|
1056
|
+
bcRefCount = 0;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function onBroadcast(event) {
|
|
1060
|
+
const { eventType, path: mutatedPath } = event.data;
|
|
1061
|
+
for (const entry of watchers) {
|
|
1062
|
+
const filename = matchWatcher(entry, mutatedPath);
|
|
1063
|
+
if (filename !== null) {
|
|
1064
|
+
try {
|
|
1065
|
+
entry.listener(eventType, filename);
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const fileSet = fileWatchers.get(mutatedPath);
|
|
1071
|
+
if (fileSet) {
|
|
1072
|
+
for (const entry of fileSet) {
|
|
1073
|
+
triggerWatchFile(entry);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function matchWatcher(entry, mutatedPath) {
|
|
1078
|
+
const { absPath, recursive } = entry;
|
|
1079
|
+
if (mutatedPath === absPath) {
|
|
1080
|
+
return basename(mutatedPath);
|
|
1081
|
+
}
|
|
1082
|
+
if (!mutatedPath.startsWith(absPath) || mutatedPath.charAt(absPath.length) !== "/") {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
const relativePath = mutatedPath.substring(absPath.length + 1);
|
|
1086
|
+
if (recursive) return relativePath;
|
|
1087
|
+
return relativePath.indexOf("/") === -1 ? relativePath : null;
|
|
1088
|
+
}
|
|
1089
|
+
function watch(filePath, options, listener) {
|
|
1090
|
+
const opts = typeof options === "string" ? { } : options ?? {};
|
|
1091
|
+
const cb = listener ?? (() => {
|
|
1092
|
+
});
|
|
1093
|
+
const absPath = resolve(filePath);
|
|
1094
|
+
const signal = opts.signal;
|
|
1095
|
+
const entry = {
|
|
1096
|
+
absPath,
|
|
1097
|
+
recursive: opts.recursive ?? false,
|
|
1098
|
+
listener: cb,
|
|
1099
|
+
signal
|
|
1100
|
+
};
|
|
1101
|
+
ensureBc();
|
|
1102
|
+
watchers.add(entry);
|
|
1103
|
+
if (signal) {
|
|
1104
|
+
const onAbort = () => {
|
|
1105
|
+
watchers.delete(entry);
|
|
1106
|
+
releaseBc();
|
|
1107
|
+
signal.removeEventListener("abort", onAbort);
|
|
1108
|
+
};
|
|
1109
|
+
if (signal.aborted) {
|
|
1110
|
+
onAbort();
|
|
1111
|
+
} else {
|
|
1112
|
+
signal.addEventListener("abort", onAbort);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
945
1115
|
const watcher = {
|
|
946
|
-
close
|
|
947
|
-
|
|
948
|
-
|
|
1116
|
+
close() {
|
|
1117
|
+
watchers.delete(entry);
|
|
1118
|
+
releaseBc();
|
|
1119
|
+
},
|
|
1120
|
+
ref() {
|
|
1121
|
+
return watcher;
|
|
1122
|
+
},
|
|
1123
|
+
unref() {
|
|
1124
|
+
return watcher;
|
|
1125
|
+
}
|
|
949
1126
|
};
|
|
950
1127
|
return watcher;
|
|
951
1128
|
}
|
|
952
|
-
|
|
953
|
-
let
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1129
|
+
function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
1130
|
+
let opts;
|
|
1131
|
+
let cb;
|
|
1132
|
+
if (typeof optionsOrListener === "function") {
|
|
1133
|
+
cb = optionsOrListener;
|
|
1134
|
+
opts = {};
|
|
1135
|
+
} else {
|
|
1136
|
+
opts = optionsOrListener ?? {};
|
|
1137
|
+
cb = listener;
|
|
1138
|
+
}
|
|
1139
|
+
if (!cb) return;
|
|
1140
|
+
const absPath = resolve(filePath);
|
|
1141
|
+
const interval = opts.interval ?? 5007;
|
|
1142
|
+
let prevStats = null;
|
|
1143
|
+
try {
|
|
1144
|
+
prevStats = statSync(syncRequest, absPath);
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
const entry = {
|
|
1148
|
+
absPath,
|
|
1149
|
+
listener: cb,
|
|
1150
|
+
interval,
|
|
1151
|
+
prevStats,
|
|
1152
|
+
syncRequest,
|
|
1153
|
+
timerId: null
|
|
1154
|
+
};
|
|
1155
|
+
ensureBc();
|
|
1156
|
+
let set = fileWatchers.get(absPath);
|
|
1157
|
+
if (!set) {
|
|
1158
|
+
set = /* @__PURE__ */ new Set();
|
|
1159
|
+
fileWatchers.set(absPath, set);
|
|
1160
|
+
}
|
|
1161
|
+
set.add(entry);
|
|
1162
|
+
entry.timerId = setInterval(() => triggerWatchFile(entry), interval);
|
|
1163
|
+
}
|
|
1164
|
+
function unwatchFile(filePath, listener) {
|
|
1165
|
+
const absPath = resolve(filePath);
|
|
1166
|
+
const set = fileWatchers.get(absPath);
|
|
1167
|
+
if (!set) return;
|
|
1168
|
+
if (listener) {
|
|
1169
|
+
for (const entry of set) {
|
|
1170
|
+
if (entry.listener === listener) {
|
|
1171
|
+
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1172
|
+
set.delete(entry);
|
|
1173
|
+
releaseBc();
|
|
1174
|
+
break;
|
|
963
1175
|
}
|
|
1176
|
+
}
|
|
1177
|
+
if (set.size === 0) fileWatchers.delete(absPath);
|
|
1178
|
+
} else {
|
|
1179
|
+
for (const entry of set) {
|
|
1180
|
+
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1181
|
+
releaseBc();
|
|
1182
|
+
}
|
|
1183
|
+
fileWatchers.delete(absPath);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function triggerWatchFile(entry) {
|
|
1187
|
+
let currStats = null;
|
|
1188
|
+
try {
|
|
1189
|
+
currStats = statSync(entry.syncRequest, entry.absPath);
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
const prev = entry.prevStats ?? emptyStats();
|
|
1193
|
+
const curr = currStats ?? emptyStats();
|
|
1194
|
+
if (prev.mtimeMs !== curr.mtimeMs || prev.size !== curr.size || prev.ino !== curr.ino) {
|
|
1195
|
+
entry.prevStats = currStats;
|
|
1196
|
+
try {
|
|
1197
|
+
entry.listener(curr, prev);
|
|
964
1198
|
} catch {
|
|
965
|
-
yield { eventType: "rename", filename: basename(filePath) };
|
|
966
|
-
return;
|
|
967
1199
|
}
|
|
968
|
-
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function emptyStats() {
|
|
1203
|
+
const zero = /* @__PURE__ */ new Date(0);
|
|
1204
|
+
return {
|
|
1205
|
+
isFile: () => false,
|
|
1206
|
+
isDirectory: () => false,
|
|
1207
|
+
isBlockDevice: () => false,
|
|
1208
|
+
isCharacterDevice: () => false,
|
|
1209
|
+
isSymbolicLink: () => false,
|
|
1210
|
+
isFIFO: () => false,
|
|
1211
|
+
isSocket: () => false,
|
|
1212
|
+
dev: 0,
|
|
1213
|
+
ino: 0,
|
|
1214
|
+
mode: 0,
|
|
1215
|
+
nlink: 0,
|
|
1216
|
+
uid: 0,
|
|
1217
|
+
gid: 0,
|
|
1218
|
+
rdev: 0,
|
|
1219
|
+
size: 0,
|
|
1220
|
+
blksize: 4096,
|
|
1221
|
+
blocks: 0,
|
|
1222
|
+
atimeMs: 0,
|
|
1223
|
+
mtimeMs: 0,
|
|
1224
|
+
ctimeMs: 0,
|
|
1225
|
+
birthtimeMs: 0,
|
|
1226
|
+
atime: zero,
|
|
1227
|
+
mtime: zero,
|
|
1228
|
+
ctime: zero,
|
|
1229
|
+
birthtime: zero
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function* watchAsync(_asyncRequest, filePath, options) {
|
|
1233
|
+
const absPath = resolve(filePath);
|
|
1234
|
+
const recursive = options?.recursive ?? false;
|
|
1235
|
+
const signal = options?.signal;
|
|
1236
|
+
const queue = [];
|
|
1237
|
+
let resolve2 = null;
|
|
1238
|
+
const entry = {
|
|
1239
|
+
absPath,
|
|
1240
|
+
recursive,
|
|
1241
|
+
listener: (eventType, filename) => {
|
|
1242
|
+
queue.push({ eventType, filename });
|
|
1243
|
+
if (resolve2) {
|
|
1244
|
+
resolve2();
|
|
1245
|
+
resolve2 = null;
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
signal
|
|
1249
|
+
};
|
|
1250
|
+
ensureBc();
|
|
1251
|
+
watchers.add(entry);
|
|
1252
|
+
try {
|
|
1253
|
+
while (!signal?.aborted) {
|
|
1254
|
+
if (queue.length === 0) {
|
|
1255
|
+
await new Promise((r) => {
|
|
1256
|
+
resolve2 = r;
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
while (queue.length > 0) {
|
|
1260
|
+
yield queue.shift();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} finally {
|
|
1264
|
+
watchers.delete(entry);
|
|
1265
|
+
releaseBc();
|
|
969
1266
|
}
|
|
970
1267
|
}
|
|
971
1268
|
|
|
@@ -1233,9 +1530,9 @@ var VFSFileSystem = class {
|
|
|
1233
1530
|
}
|
|
1234
1531
|
};
|
|
1235
1532
|
mc.port1.start();
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1533
|
+
const bc2 = new BroadcastChannel("vfs-leader-change");
|
|
1534
|
+
bc2.postMessage({ type: "leader-changed" });
|
|
1535
|
+
bc2.close();
|
|
1239
1536
|
}).catch((err) => {
|
|
1240
1537
|
console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
|
|
1241
1538
|
});
|
|
@@ -1501,11 +1798,13 @@ var VFSFileSystem = class {
|
|
|
1501
1798
|
}
|
|
1502
1799
|
// ---- Watch methods ----
|
|
1503
1800
|
watch(filePath, options, listener) {
|
|
1504
|
-
return watch();
|
|
1801
|
+
return watch(filePath, options, listener);
|
|
1505
1802
|
}
|
|
1506
1803
|
watchFile(filePath, optionsOrListener, listener) {
|
|
1804
|
+
watchFile(this._sync, filePath, optionsOrListener, listener);
|
|
1507
1805
|
}
|
|
1508
1806
|
unwatchFile(filePath, listener) {
|
|
1807
|
+
unwatchFile(filePath, listener);
|
|
1509
1808
|
}
|
|
1510
1809
|
// ---- Stream methods ----
|
|
1511
1810
|
createReadStream(filePath, options) {
|
|
@@ -1664,6 +1963,1451 @@ var VFSPromises = class {
|
|
|
1664
1963
|
}
|
|
1665
1964
|
};
|
|
1666
1965
|
|
|
1966
|
+
// src/vfs/engine.ts
|
|
1967
|
+
var encoder10 = new TextEncoder();
|
|
1968
|
+
var decoder8 = new TextDecoder();
|
|
1969
|
+
var VFSEngine = class {
|
|
1970
|
+
handle;
|
|
1971
|
+
pathIndex = /* @__PURE__ */ new Map();
|
|
1972
|
+
// path → inode index
|
|
1973
|
+
inodeCount = 0;
|
|
1974
|
+
blockSize = DEFAULT_BLOCK_SIZE;
|
|
1975
|
+
totalBlocks = 0;
|
|
1976
|
+
freeBlocks = 0;
|
|
1977
|
+
inodeTableOffset = 0;
|
|
1978
|
+
pathTableOffset = 0;
|
|
1979
|
+
pathTableUsed = 0;
|
|
1980
|
+
pathTableSize = 0;
|
|
1981
|
+
bitmapOffset = 0;
|
|
1982
|
+
dataOffset = 0;
|
|
1983
|
+
umask = DEFAULT_UMASK;
|
|
1984
|
+
processUid = 0;
|
|
1985
|
+
processGid = 0;
|
|
1986
|
+
strictPermissions = false;
|
|
1987
|
+
debug = false;
|
|
1988
|
+
// File descriptor table
|
|
1989
|
+
fdTable = /* @__PURE__ */ new Map();
|
|
1990
|
+
nextFd = 3;
|
|
1991
|
+
// 0=stdin, 1=stdout, 2=stderr reserved
|
|
1992
|
+
// Reusable buffers to avoid allocations
|
|
1993
|
+
inodeBuf = new Uint8Array(INODE_SIZE);
|
|
1994
|
+
inodeView = new DataView(this.inodeBuf.buffer);
|
|
1995
|
+
// In-memory inode cache — eliminates disk reads for hot inodes
|
|
1996
|
+
inodeCache = /* @__PURE__ */ new Map();
|
|
1997
|
+
superblockBuf = new Uint8Array(SUPERBLOCK.SIZE);
|
|
1998
|
+
superblockView = new DataView(this.superblockBuf.buffer);
|
|
1999
|
+
// In-memory bitmap cache — eliminates bitmap reads from OPFS
|
|
2000
|
+
bitmap = null;
|
|
2001
|
+
bitmapDirtyLo = Infinity;
|
|
2002
|
+
// lowest dirty byte index
|
|
2003
|
+
bitmapDirtyHi = -1;
|
|
2004
|
+
// highest dirty byte index (inclusive)
|
|
2005
|
+
superblockDirty = false;
|
|
2006
|
+
// Free inode hint — skip O(n) scan
|
|
2007
|
+
freeInodeHint = 0;
|
|
2008
|
+
init(handle, opts) {
|
|
2009
|
+
this.handle = handle;
|
|
2010
|
+
this.processUid = opts?.uid ?? 0;
|
|
2011
|
+
this.processGid = opts?.gid ?? 0;
|
|
2012
|
+
this.umask = opts?.umask ?? DEFAULT_UMASK;
|
|
2013
|
+
this.strictPermissions = opts?.strictPermissions ?? false;
|
|
2014
|
+
this.debug = opts?.debug ?? false;
|
|
2015
|
+
const size = handle.getSize();
|
|
2016
|
+
if (size === 0) {
|
|
2017
|
+
this.format();
|
|
2018
|
+
} else {
|
|
2019
|
+
this.mount();
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/** Release the sync access handle (call on fatal error or shutdown) */
|
|
2023
|
+
closeHandle() {
|
|
2024
|
+
try {
|
|
2025
|
+
this.handle?.close();
|
|
2026
|
+
} catch (_) {
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/** Format a fresh VFS */
|
|
2030
|
+
format() {
|
|
2031
|
+
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
2032
|
+
this.inodeCount = DEFAULT_INODE_COUNT;
|
|
2033
|
+
this.blockSize = DEFAULT_BLOCK_SIZE;
|
|
2034
|
+
this.totalBlocks = layout.totalBlocks;
|
|
2035
|
+
this.freeBlocks = layout.totalBlocks;
|
|
2036
|
+
this.inodeTableOffset = layout.inodeTableOffset;
|
|
2037
|
+
this.pathTableOffset = layout.pathTableOffset;
|
|
2038
|
+
this.pathTableSize = layout.pathTableSize;
|
|
2039
|
+
this.pathTableUsed = 0;
|
|
2040
|
+
this.bitmapOffset = layout.bitmapOffset;
|
|
2041
|
+
this.dataOffset = layout.dataOffset;
|
|
2042
|
+
this.handle.truncate(layout.totalSize);
|
|
2043
|
+
this.writeSuperblock();
|
|
2044
|
+
const zeroBuf = new Uint8Array(layout.inodeTableSize);
|
|
2045
|
+
this.handle.write(zeroBuf, { at: this.inodeTableOffset });
|
|
2046
|
+
this.bitmap = new Uint8Array(layout.bitmapSize);
|
|
2047
|
+
this.handle.write(this.bitmap, { at: this.bitmapOffset });
|
|
2048
|
+
this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
|
|
2049
|
+
this.handle.flush();
|
|
2050
|
+
}
|
|
2051
|
+
/** Mount an existing VFS from disk — validates superblock integrity */
|
|
2052
|
+
mount() {
|
|
2053
|
+
const fileSize = this.handle.getSize();
|
|
2054
|
+
if (fileSize < SUPERBLOCK.SIZE) {
|
|
2055
|
+
throw new Error(`Corrupt VFS: file too small (${fileSize} bytes, need at least ${SUPERBLOCK.SIZE})`);
|
|
2056
|
+
}
|
|
2057
|
+
this.handle.read(this.superblockBuf, { at: 0 });
|
|
2058
|
+
const v = this.superblockView;
|
|
2059
|
+
const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
|
|
2060
|
+
if (magic !== VFS_MAGIC) {
|
|
2061
|
+
throw new Error(`Corrupt VFS: bad magic 0x${magic.toString(16)} (expected 0x${VFS_MAGIC.toString(16)})`);
|
|
2062
|
+
}
|
|
2063
|
+
const version = v.getUint32(SUPERBLOCK.VERSION, true);
|
|
2064
|
+
if (version !== VFS_VERSION) {
|
|
2065
|
+
throw new Error(`Corrupt VFS: unsupported version ${version} (expected ${VFS_VERSION})`);
|
|
2066
|
+
}
|
|
2067
|
+
const inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
2068
|
+
const blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
2069
|
+
const totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
2070
|
+
const freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
|
|
2071
|
+
const inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
2072
|
+
const pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
2073
|
+
const dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
2074
|
+
const bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
2075
|
+
const pathUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
2076
|
+
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0) {
|
|
2077
|
+
throw new Error(`Corrupt VFS: invalid block size ${blockSize} (must be power of 2)`);
|
|
2078
|
+
}
|
|
2079
|
+
if (inodeCount === 0) {
|
|
2080
|
+
throw new Error("Corrupt VFS: inode count is 0");
|
|
2081
|
+
}
|
|
2082
|
+
if (freeBlocks > totalBlocks) {
|
|
2083
|
+
throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
|
|
2084
|
+
}
|
|
2085
|
+
if (inodeTableOffset !== SUPERBLOCK.SIZE) {
|
|
2086
|
+
throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
|
|
2087
|
+
}
|
|
2088
|
+
const expectedPathOffset = inodeTableOffset + inodeCount * INODE_SIZE;
|
|
2089
|
+
if (pathTableOffset !== expectedPathOffset) {
|
|
2090
|
+
throw new Error(`Corrupt VFS: path table offset ${pathTableOffset} (expected ${expectedPathOffset})`);
|
|
2091
|
+
}
|
|
2092
|
+
if (bitmapOffset <= pathTableOffset) {
|
|
2093
|
+
throw new Error(`Corrupt VFS: bitmap offset ${bitmapOffset} must be after path table ${pathTableOffset}`);
|
|
2094
|
+
}
|
|
2095
|
+
if (dataOffset <= bitmapOffset) {
|
|
2096
|
+
throw new Error(`Corrupt VFS: data offset ${dataOffset} must be after bitmap ${bitmapOffset}`);
|
|
2097
|
+
}
|
|
2098
|
+
const pathTableSize = bitmapOffset - pathTableOffset;
|
|
2099
|
+
if (pathUsed > pathTableSize) {
|
|
2100
|
+
throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
|
|
2101
|
+
}
|
|
2102
|
+
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
2103
|
+
if (fileSize < expectedMinSize) {
|
|
2104
|
+
throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
|
|
2105
|
+
}
|
|
2106
|
+
this.inodeCount = inodeCount;
|
|
2107
|
+
this.blockSize = blockSize;
|
|
2108
|
+
this.totalBlocks = totalBlocks;
|
|
2109
|
+
this.freeBlocks = freeBlocks;
|
|
2110
|
+
this.inodeTableOffset = inodeTableOffset;
|
|
2111
|
+
this.pathTableOffset = pathTableOffset;
|
|
2112
|
+
this.dataOffset = dataOffset;
|
|
2113
|
+
this.bitmapOffset = bitmapOffset;
|
|
2114
|
+
this.pathTableUsed = pathUsed;
|
|
2115
|
+
this.pathTableSize = pathTableSize;
|
|
2116
|
+
const bitmapSize = Math.ceil(this.totalBlocks / 8);
|
|
2117
|
+
this.bitmap = new Uint8Array(bitmapSize);
|
|
2118
|
+
this.handle.read(this.bitmap, { at: this.bitmapOffset });
|
|
2119
|
+
this.rebuildIndex();
|
|
2120
|
+
if (!this.pathIndex.has("/")) {
|
|
2121
|
+
throw new Error('Corrupt VFS: root directory "/" not found in inode table');
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
writeSuperblock() {
|
|
2125
|
+
const v = this.superblockView;
|
|
2126
|
+
v.setUint32(SUPERBLOCK.MAGIC, VFS_MAGIC, true);
|
|
2127
|
+
v.setUint32(SUPERBLOCK.VERSION, VFS_VERSION, true);
|
|
2128
|
+
v.setUint32(SUPERBLOCK.INODE_COUNT, this.inodeCount, true);
|
|
2129
|
+
v.setUint32(SUPERBLOCK.BLOCK_SIZE, this.blockSize, true);
|
|
2130
|
+
v.setUint32(SUPERBLOCK.TOTAL_BLOCKS, this.totalBlocks, true);
|
|
2131
|
+
v.setUint32(SUPERBLOCK.FREE_BLOCKS, this.freeBlocks, true);
|
|
2132
|
+
v.setFloat64(SUPERBLOCK.INODE_OFFSET, this.inodeTableOffset, true);
|
|
2133
|
+
v.setFloat64(SUPERBLOCK.PATH_OFFSET, this.pathTableOffset, true);
|
|
2134
|
+
v.setFloat64(SUPERBLOCK.DATA_OFFSET, this.dataOffset, true);
|
|
2135
|
+
v.setFloat64(SUPERBLOCK.BITMAP_OFFSET, this.bitmapOffset, true);
|
|
2136
|
+
v.setUint32(SUPERBLOCK.PATH_USED, this.pathTableUsed, true);
|
|
2137
|
+
this.handle.write(this.superblockBuf, { at: 0 });
|
|
2138
|
+
}
|
|
2139
|
+
/** Flush pending bitmap and superblock writes to disk (one write each) */
|
|
2140
|
+
markBitmapDirty(lo, hi) {
|
|
2141
|
+
if (lo < this.bitmapDirtyLo) this.bitmapDirtyLo = lo;
|
|
2142
|
+
if (hi > this.bitmapDirtyHi) this.bitmapDirtyHi = hi;
|
|
2143
|
+
}
|
|
2144
|
+
commitPending() {
|
|
2145
|
+
if (this.bitmapDirtyHi >= 0) {
|
|
2146
|
+
const lo = this.bitmapDirtyLo;
|
|
2147
|
+
const hi = this.bitmapDirtyHi;
|
|
2148
|
+
this.handle.write(this.bitmap.subarray(lo, hi + 1), { at: this.bitmapOffset + lo });
|
|
2149
|
+
this.bitmapDirtyLo = Infinity;
|
|
2150
|
+
this.bitmapDirtyHi = -1;
|
|
2151
|
+
}
|
|
2152
|
+
if (this.superblockDirty) {
|
|
2153
|
+
this.writeSuperblock();
|
|
2154
|
+
this.superblockDirty = false;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
/** Rebuild in-memory path→inode index from disk */
|
|
2158
|
+
rebuildIndex() {
|
|
2159
|
+
this.pathIndex.clear();
|
|
2160
|
+
for (let i = 0; i < this.inodeCount; i++) {
|
|
2161
|
+
const inode = this.readInode(i);
|
|
2162
|
+
if (inode.type === INODE_TYPE.FREE) continue;
|
|
2163
|
+
const path = this.readPath(inode.pathOffset, inode.pathLength);
|
|
2164
|
+
this.pathIndex.set(path, i);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// ========== Low-level inode I/O ==========
|
|
2168
|
+
readInode(idx) {
|
|
2169
|
+
const cached = this.inodeCache.get(idx);
|
|
2170
|
+
if (cached) return cached;
|
|
2171
|
+
const offset = this.inodeTableOffset + idx * INODE_SIZE;
|
|
2172
|
+
this.handle.read(this.inodeBuf, { at: offset });
|
|
2173
|
+
const v = this.inodeView;
|
|
2174
|
+
const inode = {
|
|
2175
|
+
type: v.getUint8(INODE.TYPE),
|
|
2176
|
+
pathOffset: v.getUint32(INODE.PATH_OFFSET, true),
|
|
2177
|
+
pathLength: v.getUint16(INODE.PATH_LENGTH, true),
|
|
2178
|
+
mode: v.getUint32(INODE.MODE, true),
|
|
2179
|
+
size: v.getFloat64(INODE.SIZE, true),
|
|
2180
|
+
firstBlock: v.getUint32(INODE.FIRST_BLOCK, true),
|
|
2181
|
+
blockCount: v.getUint32(INODE.BLOCK_COUNT, true),
|
|
2182
|
+
mtime: v.getFloat64(INODE.MTIME, true),
|
|
2183
|
+
ctime: v.getFloat64(INODE.CTIME, true),
|
|
2184
|
+
atime: v.getFloat64(INODE.ATIME, true),
|
|
2185
|
+
uid: v.getUint32(INODE.UID, true),
|
|
2186
|
+
gid: v.getUint32(INODE.GID, true)
|
|
2187
|
+
};
|
|
2188
|
+
this.inodeCache.set(idx, inode);
|
|
2189
|
+
return inode;
|
|
2190
|
+
}
|
|
2191
|
+
writeInode(idx, inode) {
|
|
2192
|
+
if (inode.type === INODE_TYPE.FREE) {
|
|
2193
|
+
this.inodeCache.delete(idx);
|
|
2194
|
+
} else {
|
|
2195
|
+
this.inodeCache.set(idx, inode);
|
|
2196
|
+
}
|
|
2197
|
+
const v = this.inodeView;
|
|
2198
|
+
v.setUint8(INODE.TYPE, inode.type);
|
|
2199
|
+
v.setUint8(INODE.FLAGS, 0);
|
|
2200
|
+
v.setUint8(INODE.FLAGS + 1, 0);
|
|
2201
|
+
v.setUint8(INODE.FLAGS + 2, 0);
|
|
2202
|
+
v.setUint32(INODE.PATH_OFFSET, inode.pathOffset, true);
|
|
2203
|
+
v.setUint16(INODE.PATH_LENGTH, inode.pathLength, true);
|
|
2204
|
+
v.setUint16(INODE.RESERVED_10, 0, true);
|
|
2205
|
+
v.setUint32(INODE.MODE, inode.mode, true);
|
|
2206
|
+
v.setFloat64(INODE.SIZE, inode.size, true);
|
|
2207
|
+
v.setUint32(INODE.FIRST_BLOCK, inode.firstBlock, true);
|
|
2208
|
+
v.setUint32(INODE.BLOCK_COUNT, inode.blockCount, true);
|
|
2209
|
+
v.setFloat64(INODE.MTIME, inode.mtime, true);
|
|
2210
|
+
v.setFloat64(INODE.CTIME, inode.ctime, true);
|
|
2211
|
+
v.setFloat64(INODE.ATIME, inode.atime, true);
|
|
2212
|
+
v.setUint32(INODE.UID, inode.uid, true);
|
|
2213
|
+
v.setUint32(INODE.GID, inode.gid, true);
|
|
2214
|
+
const offset = this.inodeTableOffset + idx * INODE_SIZE;
|
|
2215
|
+
this.handle.write(this.inodeBuf, { at: offset });
|
|
2216
|
+
}
|
|
2217
|
+
// ========== Path table I/O ==========
|
|
2218
|
+
readPath(offset, length) {
|
|
2219
|
+
const buf = new Uint8Array(length);
|
|
2220
|
+
this.handle.read(buf, { at: this.pathTableOffset + offset });
|
|
2221
|
+
return decoder8.decode(buf);
|
|
2222
|
+
}
|
|
2223
|
+
appendPath(path) {
|
|
2224
|
+
const bytes = encoder10.encode(path);
|
|
2225
|
+
const offset = this.pathTableUsed;
|
|
2226
|
+
if (offset + bytes.byteLength > this.pathTableSize) {
|
|
2227
|
+
this.growPathTable(offset + bytes.byteLength);
|
|
2228
|
+
}
|
|
2229
|
+
this.handle.write(bytes, { at: this.pathTableOffset + offset });
|
|
2230
|
+
this.pathTableUsed += bytes.byteLength;
|
|
2231
|
+
this.superblockDirty = true;
|
|
2232
|
+
return { offset, length: bytes.byteLength };
|
|
2233
|
+
}
|
|
2234
|
+
growPathTable(needed) {
|
|
2235
|
+
const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
|
|
2236
|
+
const growth = newSize - this.pathTableSize;
|
|
2237
|
+
const dataSize = this.totalBlocks * this.blockSize;
|
|
2238
|
+
const dataBuf = new Uint8Array(dataSize);
|
|
2239
|
+
this.handle.read(dataBuf, { at: this.dataOffset });
|
|
2240
|
+
const newTotalSize = this.handle.getSize() + growth;
|
|
2241
|
+
this.handle.truncate(newTotalSize);
|
|
2242
|
+
const newBitmapOffset = this.bitmapOffset + growth;
|
|
2243
|
+
const newDataOffset = this.dataOffset + growth;
|
|
2244
|
+
this.handle.write(dataBuf, { at: newDataOffset });
|
|
2245
|
+
this.handle.write(this.bitmap, { at: newBitmapOffset });
|
|
2246
|
+
this.pathTableSize = newSize;
|
|
2247
|
+
this.bitmapOffset = newBitmapOffset;
|
|
2248
|
+
this.dataOffset = newDataOffset;
|
|
2249
|
+
this.superblockDirty = true;
|
|
2250
|
+
}
|
|
2251
|
+
// ========== Bitmap I/O ==========
|
|
2252
|
+
allocateBlocks(count) {
|
|
2253
|
+
if (count === 0) return 0;
|
|
2254
|
+
const bitmap = this.bitmap;
|
|
2255
|
+
let run = 0;
|
|
2256
|
+
let start = 0;
|
|
2257
|
+
for (let i = 0; i < this.totalBlocks; i++) {
|
|
2258
|
+
const byteIdx = i >>> 3;
|
|
2259
|
+
const bitIdx = i & 7;
|
|
2260
|
+
const used = bitmap[byteIdx] >>> bitIdx & 1;
|
|
2261
|
+
if (used) {
|
|
2262
|
+
run = 0;
|
|
2263
|
+
start = i + 1;
|
|
2264
|
+
} else {
|
|
2265
|
+
run++;
|
|
2266
|
+
if (run === count) {
|
|
2267
|
+
for (let j = start; j <= i; j++) {
|
|
2268
|
+
const bj = j >>> 3;
|
|
2269
|
+
const bi = j & 7;
|
|
2270
|
+
bitmap[bj] |= 1 << bi;
|
|
2271
|
+
}
|
|
2272
|
+
this.markBitmapDirty(start >>> 3, i >>> 3);
|
|
2273
|
+
this.freeBlocks -= count;
|
|
2274
|
+
this.superblockDirty = true;
|
|
2275
|
+
return start;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return this.growAndAllocate(count);
|
|
2280
|
+
}
|
|
2281
|
+
growAndAllocate(count) {
|
|
2282
|
+
const oldTotal = this.totalBlocks;
|
|
2283
|
+
const newTotal = Math.max(oldTotal * 2, oldTotal + count);
|
|
2284
|
+
const addedBlocks = newTotal - oldTotal;
|
|
2285
|
+
const newFileSize = this.dataOffset + newTotal * this.blockSize;
|
|
2286
|
+
this.handle.truncate(newFileSize);
|
|
2287
|
+
const newBitmapSize = Math.ceil(newTotal / 8);
|
|
2288
|
+
const newBitmap = new Uint8Array(newBitmapSize);
|
|
2289
|
+
newBitmap.set(this.bitmap);
|
|
2290
|
+
this.bitmap = newBitmap;
|
|
2291
|
+
this.totalBlocks = newTotal;
|
|
2292
|
+
this.freeBlocks += addedBlocks;
|
|
2293
|
+
const start = oldTotal;
|
|
2294
|
+
for (let j = start; j < start + count; j++) {
|
|
2295
|
+
const bj = j >>> 3;
|
|
2296
|
+
const bi = j & 7;
|
|
2297
|
+
this.bitmap[bj] |= 1 << bi;
|
|
2298
|
+
}
|
|
2299
|
+
this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
|
|
2300
|
+
this.freeBlocks -= count;
|
|
2301
|
+
this.superblockDirty = true;
|
|
2302
|
+
return start;
|
|
2303
|
+
}
|
|
2304
|
+
freeBlockRange(start, count) {
|
|
2305
|
+
if (count === 0) return;
|
|
2306
|
+
const bitmap = this.bitmap;
|
|
2307
|
+
for (let i = start; i < start + count; i++) {
|
|
2308
|
+
const byteIdx = i >>> 3;
|
|
2309
|
+
const bitIdx = i & 7;
|
|
2310
|
+
bitmap[byteIdx] &= ~(1 << bitIdx);
|
|
2311
|
+
}
|
|
2312
|
+
this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
|
|
2313
|
+
this.freeBlocks += count;
|
|
2314
|
+
this.superblockDirty = true;
|
|
2315
|
+
}
|
|
2316
|
+
// updateSuperblockFreeBlocks is no longer needed — superblock writes are coalesced via commitPending()
|
|
2317
|
+
// ========== Inode allocation ==========
|
|
2318
|
+
findFreeInode() {
|
|
2319
|
+
for (let i = this.freeInodeHint; i < this.inodeCount; i++) {
|
|
2320
|
+
if (this.inodeCache.has(i)) continue;
|
|
2321
|
+
const offset = this.inodeTableOffset + i * INODE_SIZE;
|
|
2322
|
+
const typeBuf = new Uint8Array(1);
|
|
2323
|
+
this.handle.read(typeBuf, { at: offset });
|
|
2324
|
+
if (typeBuf[0] === INODE_TYPE.FREE) {
|
|
2325
|
+
this.freeInodeHint = i + 1;
|
|
2326
|
+
return i;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const idx = this.growInodeTable();
|
|
2330
|
+
this.freeInodeHint = idx + 1;
|
|
2331
|
+
return idx;
|
|
2332
|
+
}
|
|
2333
|
+
growInodeTable() {
|
|
2334
|
+
const oldCount = this.inodeCount;
|
|
2335
|
+
const newCount = oldCount * 2;
|
|
2336
|
+
const growth = (newCount - oldCount) * INODE_SIZE;
|
|
2337
|
+
const afterInodeOffset = this.inodeTableOffset + oldCount * INODE_SIZE;
|
|
2338
|
+
const afterSize = this.handle.getSize() - afterInodeOffset;
|
|
2339
|
+
const afterBuf = new Uint8Array(afterSize);
|
|
2340
|
+
this.handle.read(afterBuf, { at: afterInodeOffset });
|
|
2341
|
+
this.handle.truncate(this.handle.getSize() + growth);
|
|
2342
|
+
this.handle.write(afterBuf, { at: afterInodeOffset + growth });
|
|
2343
|
+
const zeroes = new Uint8Array(growth);
|
|
2344
|
+
this.handle.write(zeroes, { at: afterInodeOffset });
|
|
2345
|
+
this.pathTableOffset += growth;
|
|
2346
|
+
this.bitmapOffset += growth;
|
|
2347
|
+
this.dataOffset += growth;
|
|
2348
|
+
this.inodeCount = newCount;
|
|
2349
|
+
this.superblockDirty = true;
|
|
2350
|
+
return oldCount;
|
|
2351
|
+
}
|
|
2352
|
+
// ========== Data I/O ==========
|
|
2353
|
+
readData(firstBlock, blockCount, size) {
|
|
2354
|
+
const buf = new Uint8Array(size);
|
|
2355
|
+
const offset = this.dataOffset + firstBlock * this.blockSize;
|
|
2356
|
+
this.handle.read(buf, { at: offset });
|
|
2357
|
+
return buf;
|
|
2358
|
+
}
|
|
2359
|
+
writeData(firstBlock, data) {
|
|
2360
|
+
const offset = this.dataOffset + firstBlock * this.blockSize;
|
|
2361
|
+
this.handle.write(data, { at: offset });
|
|
2362
|
+
}
|
|
2363
|
+
// ========== Path resolution ==========
|
|
2364
|
+
resolvePath(path, depth = 0) {
|
|
2365
|
+
if (depth > MAX_SYMLINK_DEPTH) return void 0;
|
|
2366
|
+
const idx = this.pathIndex.get(path);
|
|
2367
|
+
if (idx === void 0) {
|
|
2368
|
+
return this.resolvePathComponents(path, true, depth);
|
|
2369
|
+
}
|
|
2370
|
+
const inode = this.readInode(idx);
|
|
2371
|
+
if (inode.type === INODE_TYPE.SYMLINK) {
|
|
2372
|
+
const target = decoder8.decode(this.readData(inode.firstBlock, inode.blockCount, inode.size));
|
|
2373
|
+
const resolved = target.startsWith("/") ? target : this.resolveRelative(path, target);
|
|
2374
|
+
return this.resolvePath(resolved, depth + 1);
|
|
2375
|
+
}
|
|
2376
|
+
return idx;
|
|
2377
|
+
}
|
|
2378
|
+
/** Resolve symlinks in intermediate path components */
|
|
2379
|
+
resolvePathComponents(path, followLast = true, depth = 0) {
|
|
2380
|
+
if (depth > MAX_SYMLINK_DEPTH) return void 0;
|
|
2381
|
+
const parts = path.split("/").filter(Boolean);
|
|
2382
|
+
let current = "/";
|
|
2383
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2384
|
+
const isLast = i === parts.length - 1;
|
|
2385
|
+
current = current === "/" ? "/" + parts[i] : current + "/" + parts[i];
|
|
2386
|
+
const idx = this.pathIndex.get(current);
|
|
2387
|
+
if (idx === void 0) return void 0;
|
|
2388
|
+
const inode = this.readInode(idx);
|
|
2389
|
+
if (inode.type === INODE_TYPE.SYMLINK && (!isLast || followLast)) {
|
|
2390
|
+
const target = decoder8.decode(this.readData(inode.firstBlock, inode.blockCount, inode.size));
|
|
2391
|
+
const resolved = target.startsWith("/") ? target : this.resolveRelative(current, target);
|
|
2392
|
+
if (isLast) {
|
|
2393
|
+
return this.resolvePathComponents(resolved, true, depth + 1);
|
|
2394
|
+
}
|
|
2395
|
+
const remaining = parts.slice(i + 1).join("/");
|
|
2396
|
+
const newPath = resolved + (remaining ? "/" + remaining : "");
|
|
2397
|
+
return this.resolvePathComponents(newPath, followLast, depth + 1);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return this.pathIndex.get(current);
|
|
2401
|
+
}
|
|
2402
|
+
resolveRelative(from, target) {
|
|
2403
|
+
const dir = from.substring(0, from.lastIndexOf("/")) || "/";
|
|
2404
|
+
const parts = (dir + "/" + target).split("/").filter(Boolean);
|
|
2405
|
+
const resolved = [];
|
|
2406
|
+
for (const p of parts) {
|
|
2407
|
+
if (p === ".") continue;
|
|
2408
|
+
if (p === "..") {
|
|
2409
|
+
resolved.pop();
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
resolved.push(p);
|
|
2413
|
+
}
|
|
2414
|
+
return "/" + resolved.join("/");
|
|
2415
|
+
}
|
|
2416
|
+
// ========== Core inode creation helper ==========
|
|
2417
|
+
createInode(path, type, mode, size, data) {
|
|
2418
|
+
const idx = this.findFreeInode();
|
|
2419
|
+
const { offset: pathOff, length: pathLen } = this.appendPath(path);
|
|
2420
|
+
const now = Date.now();
|
|
2421
|
+
let firstBlock = 0;
|
|
2422
|
+
let blockCount = 0;
|
|
2423
|
+
if (data && data.byteLength > 0) {
|
|
2424
|
+
blockCount = Math.ceil(data.byteLength / this.blockSize);
|
|
2425
|
+
firstBlock = this.allocateBlocks(blockCount);
|
|
2426
|
+
this.writeData(firstBlock, data);
|
|
2427
|
+
}
|
|
2428
|
+
const inode = {
|
|
2429
|
+
type,
|
|
2430
|
+
pathOffset: pathOff,
|
|
2431
|
+
pathLength: pathLen,
|
|
2432
|
+
mode,
|
|
2433
|
+
size,
|
|
2434
|
+
firstBlock,
|
|
2435
|
+
blockCount,
|
|
2436
|
+
mtime: now,
|
|
2437
|
+
ctime: now,
|
|
2438
|
+
atime: now,
|
|
2439
|
+
uid: this.processUid,
|
|
2440
|
+
gid: this.processGid
|
|
2441
|
+
};
|
|
2442
|
+
this.writeInode(idx, inode);
|
|
2443
|
+
this.pathIndex.set(path, idx);
|
|
2444
|
+
return idx;
|
|
2445
|
+
}
|
|
2446
|
+
// ========== Public API — called by server worker dispatch ==========
|
|
2447
|
+
/** Normalize a path: ensure leading /, resolve . and .. */
|
|
2448
|
+
normalizePath(p) {
|
|
2449
|
+
if (p.charCodeAt(0) !== 47) p = "/" + p;
|
|
2450
|
+
if (p.length === 1) return p;
|
|
2451
|
+
if (p.indexOf("/.") === -1 && p.indexOf("//") === -1 && p.charCodeAt(p.length - 1) !== 47) {
|
|
2452
|
+
return p;
|
|
2453
|
+
}
|
|
2454
|
+
const parts = p.split("/").filter(Boolean);
|
|
2455
|
+
const resolved = [];
|
|
2456
|
+
for (const part of parts) {
|
|
2457
|
+
if (part === ".") continue;
|
|
2458
|
+
if (part === "..") {
|
|
2459
|
+
resolved.pop();
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
resolved.push(part);
|
|
2463
|
+
}
|
|
2464
|
+
return "/" + resolved.join("/");
|
|
2465
|
+
}
|
|
2466
|
+
// ---- READ ----
|
|
2467
|
+
read(path) {
|
|
2468
|
+
const t0 = this.debug ? performance.now() : 0;
|
|
2469
|
+
path = this.normalizePath(path);
|
|
2470
|
+
let idx = this.pathIndex.get(path);
|
|
2471
|
+
if (idx !== void 0) {
|
|
2472
|
+
const inode2 = this.inodeCache.get(idx);
|
|
2473
|
+
if (inode2) {
|
|
2474
|
+
if (inode2.type === INODE_TYPE.SYMLINK) {
|
|
2475
|
+
idx = this.resolvePathComponents(path, true);
|
|
2476
|
+
} else if (inode2.type === INODE_TYPE.DIRECTORY) {
|
|
2477
|
+
return { status: CODE_TO_STATUS.EISDIR, data: null };
|
|
2478
|
+
} else {
|
|
2479
|
+
const data2 = inode2.size > 0 ? this.readData(inode2.firstBlock, inode2.blockCount, inode2.size) : new Uint8Array(0);
|
|
2480
|
+
if (this.debug) {
|
|
2481
|
+
const t1 = performance.now();
|
|
2482
|
+
console.log(`[VFS read] path=${path} size=${inode2.size} TOTAL=${(t1 - t0).toFixed(3)}ms (fast)`);
|
|
2483
|
+
}
|
|
2484
|
+
return { status: 0, data: data2 };
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (idx === void 0) idx = this.resolvePathComponents(path, true);
|
|
2489
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2490
|
+
const inode = this.readInode(idx);
|
|
2491
|
+
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR, data: null };
|
|
2492
|
+
const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
2493
|
+
if (this.debug) {
|
|
2494
|
+
const t1 = performance.now();
|
|
2495
|
+
console.log(`[VFS read] path=${path} size=${inode.size} TOTAL=${(t1 - t0).toFixed(3)}ms (slow path)`);
|
|
2496
|
+
}
|
|
2497
|
+
return { status: 0, data };
|
|
2498
|
+
}
|
|
2499
|
+
// ---- WRITE ----
|
|
2500
|
+
write(path, data, flags = 0) {
|
|
2501
|
+
const t0 = this.debug ? performance.now() : 0;
|
|
2502
|
+
path = this.normalizePath(path);
|
|
2503
|
+
const t1 = this.debug ? performance.now() : 0;
|
|
2504
|
+
const parentStatus = this.ensureParent(path);
|
|
2505
|
+
if (parentStatus !== 0) return { status: parentStatus };
|
|
2506
|
+
const t2 = this.debug ? performance.now() : 0;
|
|
2507
|
+
const existingIdx = this.resolvePathComponents(path, true);
|
|
2508
|
+
const t3 = this.debug ? performance.now() : 0;
|
|
2509
|
+
let tAlloc = t3, tData = t3, tInode = t3;
|
|
2510
|
+
if (existingIdx !== void 0) {
|
|
2511
|
+
const inode = this.readInode(existingIdx);
|
|
2512
|
+
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
2513
|
+
const neededBlocks = Math.ceil(data.byteLength / this.blockSize);
|
|
2514
|
+
if (neededBlocks <= inode.blockCount) {
|
|
2515
|
+
tAlloc = this.debug ? performance.now() : 0;
|
|
2516
|
+
this.writeData(inode.firstBlock, data);
|
|
2517
|
+
tData = this.debug ? performance.now() : 0;
|
|
2518
|
+
if (neededBlocks < inode.blockCount) {
|
|
2519
|
+
this.freeBlockRange(inode.firstBlock + neededBlocks, inode.blockCount - neededBlocks);
|
|
2520
|
+
}
|
|
2521
|
+
} else {
|
|
2522
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2523
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
2524
|
+
tAlloc = this.debug ? performance.now() : 0;
|
|
2525
|
+
this.writeData(newFirst, data);
|
|
2526
|
+
tData = this.debug ? performance.now() : 0;
|
|
2527
|
+
inode.firstBlock = newFirst;
|
|
2528
|
+
}
|
|
2529
|
+
inode.size = data.byteLength;
|
|
2530
|
+
inode.blockCount = neededBlocks;
|
|
2531
|
+
inode.mtime = Date.now();
|
|
2532
|
+
this.writeInode(existingIdx, inode);
|
|
2533
|
+
tInode = this.debug ? performance.now() : 0;
|
|
2534
|
+
} else {
|
|
2535
|
+
const mode = DEFAULT_FILE_MODE & ~(this.umask & 511);
|
|
2536
|
+
this.createInode(path, INODE_TYPE.FILE, mode, data.byteLength, data);
|
|
2537
|
+
tAlloc = this.debug ? performance.now() : 0;
|
|
2538
|
+
tData = tAlloc;
|
|
2539
|
+
tInode = tAlloc;
|
|
2540
|
+
}
|
|
2541
|
+
if (flags & 1) {
|
|
2542
|
+
this.commitPending();
|
|
2543
|
+
this.handle.flush();
|
|
2544
|
+
}
|
|
2545
|
+
const tFlush = this.debug ? performance.now() : 0;
|
|
2546
|
+
if (this.debug) {
|
|
2547
|
+
const existing = existingIdx !== void 0;
|
|
2548
|
+
console.log(`[VFS write] path=${path} size=${data.byteLength} ${existing ? "UPDATE" : "CREATE"} normalize=${(t1 - t0).toFixed(3)}ms parent=${(t2 - t1).toFixed(3)}ms resolve=${(t3 - t2).toFixed(3)}ms alloc=${(tAlloc - t3).toFixed(3)}ms data=${(tData - tAlloc).toFixed(3)}ms inode=${(tInode - tData).toFixed(3)}ms flush=${(tFlush - tInode).toFixed(3)}ms TOTAL=${(tFlush - t0).toFixed(3)}ms`);
|
|
2549
|
+
}
|
|
2550
|
+
return { status: 0 };
|
|
2551
|
+
}
|
|
2552
|
+
// ---- APPEND ----
|
|
2553
|
+
append(path, data) {
|
|
2554
|
+
path = this.normalizePath(path);
|
|
2555
|
+
const existingIdx = this.resolvePathComponents(path, true);
|
|
2556
|
+
if (existingIdx === void 0) {
|
|
2557
|
+
return this.write(path, data);
|
|
2558
|
+
}
|
|
2559
|
+
const inode = this.readInode(existingIdx);
|
|
2560
|
+
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
2561
|
+
const existing = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
2562
|
+
const combined = new Uint8Array(existing.byteLength + data.byteLength);
|
|
2563
|
+
combined.set(existing);
|
|
2564
|
+
combined.set(data, existing.byteLength);
|
|
2565
|
+
const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
|
|
2566
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2567
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
2568
|
+
this.writeData(newFirst, combined);
|
|
2569
|
+
inode.firstBlock = newFirst;
|
|
2570
|
+
inode.blockCount = neededBlocks;
|
|
2571
|
+
inode.size = combined.byteLength;
|
|
2572
|
+
inode.mtime = Date.now();
|
|
2573
|
+
this.writeInode(existingIdx, inode);
|
|
2574
|
+
this.commitPending();
|
|
2575
|
+
return { status: 0 };
|
|
2576
|
+
}
|
|
2577
|
+
// ---- UNLINK ----
|
|
2578
|
+
unlink(path) {
|
|
2579
|
+
path = this.normalizePath(path);
|
|
2580
|
+
const idx = this.pathIndex.get(path);
|
|
2581
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2582
|
+
const inode = this.readInode(idx);
|
|
2583
|
+
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
2584
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2585
|
+
inode.type = INODE_TYPE.FREE;
|
|
2586
|
+
this.writeInode(idx, inode);
|
|
2587
|
+
this.pathIndex.delete(path);
|
|
2588
|
+
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
2589
|
+
this.commitPending();
|
|
2590
|
+
return { status: 0 };
|
|
2591
|
+
}
|
|
2592
|
+
// ---- STAT ----
|
|
2593
|
+
stat(path) {
|
|
2594
|
+
path = this.normalizePath(path);
|
|
2595
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2596
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2597
|
+
return this.encodeStatResponse(idx);
|
|
2598
|
+
}
|
|
2599
|
+
// ---- LSTAT (no symlink follow) ----
|
|
2600
|
+
lstat(path) {
|
|
2601
|
+
path = this.normalizePath(path);
|
|
2602
|
+
const idx = this.pathIndex.get(path);
|
|
2603
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2604
|
+
return this.encodeStatResponse(idx);
|
|
2605
|
+
}
|
|
2606
|
+
encodeStatResponse(idx) {
|
|
2607
|
+
const inode = this.readInode(idx);
|
|
2608
|
+
const buf = new Uint8Array(49);
|
|
2609
|
+
const view = new DataView(buf.buffer);
|
|
2610
|
+
view.setUint8(0, inode.type);
|
|
2611
|
+
view.setUint32(1, inode.mode, true);
|
|
2612
|
+
view.setFloat64(5, inode.size, true);
|
|
2613
|
+
view.setFloat64(13, inode.mtime, true);
|
|
2614
|
+
view.setFloat64(21, inode.ctime, true);
|
|
2615
|
+
view.setFloat64(29, inode.atime, true);
|
|
2616
|
+
view.setUint32(37, inode.uid, true);
|
|
2617
|
+
view.setUint32(41, inode.gid, true);
|
|
2618
|
+
view.setUint32(45, idx, true);
|
|
2619
|
+
return { status: 0, data: buf };
|
|
2620
|
+
}
|
|
2621
|
+
// ---- MKDIR ----
|
|
2622
|
+
mkdir(path, flags = 0) {
|
|
2623
|
+
path = this.normalizePath(path);
|
|
2624
|
+
const recursive = (flags & 1) !== 0;
|
|
2625
|
+
if (recursive) {
|
|
2626
|
+
return this.mkdirRecursive(path);
|
|
2627
|
+
}
|
|
2628
|
+
if (this.pathIndex.has(path)) return { status: CODE_TO_STATUS.EEXIST, data: null };
|
|
2629
|
+
const parentStatus = this.ensureParent(path);
|
|
2630
|
+
if (parentStatus !== 0) return { status: parentStatus, data: null };
|
|
2631
|
+
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
2632
|
+
this.createInode(path, INODE_TYPE.DIRECTORY, mode, 0);
|
|
2633
|
+
this.commitPending();
|
|
2634
|
+
const pathBytes = encoder10.encode(path);
|
|
2635
|
+
return { status: 0, data: pathBytes };
|
|
2636
|
+
}
|
|
2637
|
+
mkdirRecursive(path) {
|
|
2638
|
+
const parts = path.split("/").filter(Boolean);
|
|
2639
|
+
let current = "";
|
|
2640
|
+
let firstCreated = null;
|
|
2641
|
+
for (const part of parts) {
|
|
2642
|
+
current += "/" + part;
|
|
2643
|
+
if (this.pathIndex.has(current)) {
|
|
2644
|
+
const idx = this.pathIndex.get(current);
|
|
2645
|
+
const inode = this.readInode(idx);
|
|
2646
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) {
|
|
2647
|
+
return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
2648
|
+
}
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
2652
|
+
this.createInode(current, INODE_TYPE.DIRECTORY, mode, 0);
|
|
2653
|
+
if (!firstCreated) firstCreated = current;
|
|
2654
|
+
}
|
|
2655
|
+
this.commitPending();
|
|
2656
|
+
const result = firstCreated ? encoder10.encode(firstCreated) : void 0;
|
|
2657
|
+
return { status: 0, data: result ?? null };
|
|
2658
|
+
}
|
|
2659
|
+
// ---- RMDIR ----
|
|
2660
|
+
rmdir(path, flags = 0) {
|
|
2661
|
+
path = this.normalizePath(path);
|
|
2662
|
+
const recursive = (flags & 1) !== 0;
|
|
2663
|
+
const idx = this.pathIndex.get(path);
|
|
2664
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2665
|
+
const inode = this.readInode(idx);
|
|
2666
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
|
|
2667
|
+
const children = this.getDirectChildren(path);
|
|
2668
|
+
if (children.length > 0) {
|
|
2669
|
+
if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
|
|
2670
|
+
for (const child of this.getAllDescendants(path)) {
|
|
2671
|
+
const childIdx = this.pathIndex.get(child);
|
|
2672
|
+
const childInode = this.readInode(childIdx);
|
|
2673
|
+
this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
|
|
2674
|
+
childInode.type = INODE_TYPE.FREE;
|
|
2675
|
+
this.writeInode(childIdx, childInode);
|
|
2676
|
+
this.pathIndex.delete(child);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
inode.type = INODE_TYPE.FREE;
|
|
2680
|
+
this.writeInode(idx, inode);
|
|
2681
|
+
this.pathIndex.delete(path);
|
|
2682
|
+
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
2683
|
+
this.commitPending();
|
|
2684
|
+
return { status: 0 };
|
|
2685
|
+
}
|
|
2686
|
+
// ---- READDIR ----
|
|
2687
|
+
readdir(path, flags = 0) {
|
|
2688
|
+
path = this.normalizePath(path);
|
|
2689
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2690
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2691
|
+
const inode = this.readInode(idx);
|
|
2692
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
2693
|
+
const withFileTypes = (flags & 1) !== 0;
|
|
2694
|
+
const children = this.getDirectChildren(path);
|
|
2695
|
+
if (withFileTypes) {
|
|
2696
|
+
let totalSize2 = 4;
|
|
2697
|
+
const entries = [];
|
|
2698
|
+
for (const childPath of children) {
|
|
2699
|
+
const name = childPath.substring(childPath.lastIndexOf("/") + 1);
|
|
2700
|
+
const nameBytes = encoder10.encode(name);
|
|
2701
|
+
const childIdx = this.pathIndex.get(childPath);
|
|
2702
|
+
const childInode = this.readInode(childIdx);
|
|
2703
|
+
entries.push({ name: nameBytes, type: childInode.type });
|
|
2704
|
+
totalSize2 += 2 + nameBytes.byteLength + 1;
|
|
2705
|
+
}
|
|
2706
|
+
const buf2 = new Uint8Array(totalSize2);
|
|
2707
|
+
const view2 = new DataView(buf2.buffer);
|
|
2708
|
+
view2.setUint32(0, entries.length, true);
|
|
2709
|
+
let offset2 = 4;
|
|
2710
|
+
for (const entry of entries) {
|
|
2711
|
+
view2.setUint16(offset2, entry.name.byteLength, true);
|
|
2712
|
+
offset2 += 2;
|
|
2713
|
+
buf2.set(entry.name, offset2);
|
|
2714
|
+
offset2 += entry.name.byteLength;
|
|
2715
|
+
buf2[offset2++] = entry.type;
|
|
2716
|
+
}
|
|
2717
|
+
return { status: 0, data: buf2 };
|
|
2718
|
+
}
|
|
2719
|
+
let totalSize = 4;
|
|
2720
|
+
const nameEntries = [];
|
|
2721
|
+
for (const childPath of children) {
|
|
2722
|
+
const name = childPath.substring(childPath.lastIndexOf("/") + 1);
|
|
2723
|
+
const nameBytes = encoder10.encode(name);
|
|
2724
|
+
nameEntries.push(nameBytes);
|
|
2725
|
+
totalSize += 2 + nameBytes.byteLength;
|
|
2726
|
+
}
|
|
2727
|
+
const buf = new Uint8Array(totalSize);
|
|
2728
|
+
const view = new DataView(buf.buffer);
|
|
2729
|
+
view.setUint32(0, nameEntries.length, true);
|
|
2730
|
+
let offset = 4;
|
|
2731
|
+
for (const nameBytes of nameEntries) {
|
|
2732
|
+
view.setUint16(offset, nameBytes.byteLength, true);
|
|
2733
|
+
offset += 2;
|
|
2734
|
+
buf.set(nameBytes, offset);
|
|
2735
|
+
offset += nameBytes.byteLength;
|
|
2736
|
+
}
|
|
2737
|
+
return { status: 0, data: buf };
|
|
2738
|
+
}
|
|
2739
|
+
// ---- RENAME ----
|
|
2740
|
+
rename(oldPath, newPath) {
|
|
2741
|
+
oldPath = this.normalizePath(oldPath);
|
|
2742
|
+
newPath = this.normalizePath(newPath);
|
|
2743
|
+
const idx = this.pathIndex.get(oldPath);
|
|
2744
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2745
|
+
const parentStatus = this.ensureParent(newPath);
|
|
2746
|
+
if (parentStatus !== 0) return { status: parentStatus };
|
|
2747
|
+
const existingIdx = this.pathIndex.get(newPath);
|
|
2748
|
+
if (existingIdx !== void 0) {
|
|
2749
|
+
const existingInode = this.readInode(existingIdx);
|
|
2750
|
+
this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
|
|
2751
|
+
existingInode.type = INODE_TYPE.FREE;
|
|
2752
|
+
this.writeInode(existingIdx, existingInode);
|
|
2753
|
+
this.pathIndex.delete(newPath);
|
|
2754
|
+
}
|
|
2755
|
+
const inode = this.readInode(idx);
|
|
2756
|
+
const { offset: pathOff, length: pathLen } = this.appendPath(newPath);
|
|
2757
|
+
inode.pathOffset = pathOff;
|
|
2758
|
+
inode.pathLength = pathLen;
|
|
2759
|
+
inode.mtime = Date.now();
|
|
2760
|
+
this.writeInode(idx, inode);
|
|
2761
|
+
this.pathIndex.delete(oldPath);
|
|
2762
|
+
this.pathIndex.set(newPath, idx);
|
|
2763
|
+
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
2764
|
+
const prefix = oldPath === "/" ? "/" : oldPath + "/";
|
|
2765
|
+
const toRename = [];
|
|
2766
|
+
for (const [p, i] of this.pathIndex) {
|
|
2767
|
+
if (p.startsWith(prefix)) {
|
|
2768
|
+
toRename.push([p, i]);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
for (const [p, i] of toRename) {
|
|
2772
|
+
const suffix = p.substring(oldPath.length);
|
|
2773
|
+
const childNewPath = newPath + suffix;
|
|
2774
|
+
const childInode = this.readInode(i);
|
|
2775
|
+
const { offset: cpo, length: cpl } = this.appendPath(childNewPath);
|
|
2776
|
+
childInode.pathOffset = cpo;
|
|
2777
|
+
childInode.pathLength = cpl;
|
|
2778
|
+
this.writeInode(i, childInode);
|
|
2779
|
+
this.pathIndex.delete(p);
|
|
2780
|
+
this.pathIndex.set(childNewPath, i);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
this.commitPending();
|
|
2784
|
+
return { status: 0 };
|
|
2785
|
+
}
|
|
2786
|
+
// ---- EXISTS ----
|
|
2787
|
+
exists(path) {
|
|
2788
|
+
path = this.normalizePath(path);
|
|
2789
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2790
|
+
const buf = new Uint8Array(1);
|
|
2791
|
+
buf[0] = idx !== void 0 ? 1 : 0;
|
|
2792
|
+
return { status: 0, data: buf };
|
|
2793
|
+
}
|
|
2794
|
+
// ---- TRUNCATE ----
|
|
2795
|
+
truncate(path, len = 0) {
|
|
2796
|
+
path = this.normalizePath(path);
|
|
2797
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2798
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2799
|
+
const inode = this.readInode(idx);
|
|
2800
|
+
if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
2801
|
+
if (len === 0) {
|
|
2802
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2803
|
+
inode.firstBlock = 0;
|
|
2804
|
+
inode.blockCount = 0;
|
|
2805
|
+
inode.size = 0;
|
|
2806
|
+
} else if (len < inode.size) {
|
|
2807
|
+
const neededBlocks = Math.ceil(len / this.blockSize);
|
|
2808
|
+
if (neededBlocks < inode.blockCount) {
|
|
2809
|
+
this.freeBlockRange(inode.firstBlock + neededBlocks, inode.blockCount - neededBlocks);
|
|
2810
|
+
}
|
|
2811
|
+
inode.blockCount = neededBlocks;
|
|
2812
|
+
inode.size = len;
|
|
2813
|
+
} else if (len > inode.size) {
|
|
2814
|
+
const neededBlocks = Math.ceil(len / this.blockSize);
|
|
2815
|
+
if (neededBlocks > inode.blockCount) {
|
|
2816
|
+
const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
|
|
2817
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2818
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
2819
|
+
const newData = new Uint8Array(len);
|
|
2820
|
+
newData.set(oldData);
|
|
2821
|
+
this.writeData(newFirst, newData);
|
|
2822
|
+
inode.firstBlock = newFirst;
|
|
2823
|
+
}
|
|
2824
|
+
inode.blockCount = neededBlocks;
|
|
2825
|
+
inode.size = len;
|
|
2826
|
+
}
|
|
2827
|
+
inode.mtime = Date.now();
|
|
2828
|
+
this.writeInode(idx, inode);
|
|
2829
|
+
this.commitPending();
|
|
2830
|
+
return { status: 0 };
|
|
2831
|
+
}
|
|
2832
|
+
// ---- COPY ----
|
|
2833
|
+
copy(srcPath, destPath, flags = 0) {
|
|
2834
|
+
srcPath = this.normalizePath(srcPath);
|
|
2835
|
+
destPath = this.normalizePath(destPath);
|
|
2836
|
+
const srcIdx = this.resolvePathComponents(srcPath, true);
|
|
2837
|
+
if (srcIdx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2838
|
+
const srcInode = this.readInode(srcIdx);
|
|
2839
|
+
if (srcInode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
|
|
2840
|
+
if (flags & 1 && this.pathIndex.has(destPath)) {
|
|
2841
|
+
return { status: CODE_TO_STATUS.EEXIST };
|
|
2842
|
+
}
|
|
2843
|
+
const data = srcInode.size > 0 ? this.readData(srcInode.firstBlock, srcInode.blockCount, srcInode.size) : new Uint8Array(0);
|
|
2844
|
+
return this.write(destPath, data);
|
|
2845
|
+
}
|
|
2846
|
+
// ---- ACCESS ----
|
|
2847
|
+
access(path, mode = 0) {
|
|
2848
|
+
path = this.normalizePath(path);
|
|
2849
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2850
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2851
|
+
if (mode === 0) return { status: 0 };
|
|
2852
|
+
if (!this.strictPermissions) return { status: 0 };
|
|
2853
|
+
const inode = this.readInode(idx);
|
|
2854
|
+
const filePerm = this.getEffectivePermission(inode);
|
|
2855
|
+
if (mode & 4 && !(filePerm & 4)) return { status: CODE_TO_STATUS.EACCES };
|
|
2856
|
+
if (mode & 2 && !(filePerm & 2)) return { status: CODE_TO_STATUS.EACCES };
|
|
2857
|
+
if (mode & 1 && !(filePerm & 1)) return { status: CODE_TO_STATUS.EACCES };
|
|
2858
|
+
return { status: 0 };
|
|
2859
|
+
}
|
|
2860
|
+
getEffectivePermission(inode) {
|
|
2861
|
+
const modeBits = inode.mode & 511;
|
|
2862
|
+
if (this.processUid === inode.uid) return modeBits >>> 6 & 7;
|
|
2863
|
+
if (this.processGid === inode.gid) return modeBits >>> 3 & 7;
|
|
2864
|
+
return modeBits & 7;
|
|
2865
|
+
}
|
|
2866
|
+
// ---- REALPATH ----
|
|
2867
|
+
realpath(path) {
|
|
2868
|
+
path = this.normalizePath(path);
|
|
2869
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2870
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2871
|
+
const inode = this.readInode(idx);
|
|
2872
|
+
const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
|
|
2873
|
+
return { status: 0, data: encoder10.encode(resolvedPath) };
|
|
2874
|
+
}
|
|
2875
|
+
// ---- CHMOD ----
|
|
2876
|
+
chmod(path, mode) {
|
|
2877
|
+
path = this.normalizePath(path);
|
|
2878
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2879
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2880
|
+
const inode = this.readInode(idx);
|
|
2881
|
+
inode.mode = inode.mode & S_IFMT | mode & 4095;
|
|
2882
|
+
inode.ctime = Date.now();
|
|
2883
|
+
this.writeInode(idx, inode);
|
|
2884
|
+
return { status: 0 };
|
|
2885
|
+
}
|
|
2886
|
+
// ---- CHOWN ----
|
|
2887
|
+
chown(path, uid, gid) {
|
|
2888
|
+
path = this.normalizePath(path);
|
|
2889
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2890
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2891
|
+
const inode = this.readInode(idx);
|
|
2892
|
+
inode.uid = uid;
|
|
2893
|
+
inode.gid = gid;
|
|
2894
|
+
inode.ctime = Date.now();
|
|
2895
|
+
this.writeInode(idx, inode);
|
|
2896
|
+
return { status: 0 };
|
|
2897
|
+
}
|
|
2898
|
+
// ---- UTIMES ----
|
|
2899
|
+
utimes(path, atime, mtime) {
|
|
2900
|
+
path = this.normalizePath(path);
|
|
2901
|
+
const idx = this.resolvePathComponents(path, true);
|
|
2902
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
|
|
2903
|
+
const inode = this.readInode(idx);
|
|
2904
|
+
inode.atime = atime;
|
|
2905
|
+
inode.mtime = mtime;
|
|
2906
|
+
inode.ctime = Date.now();
|
|
2907
|
+
this.writeInode(idx, inode);
|
|
2908
|
+
return { status: 0 };
|
|
2909
|
+
}
|
|
2910
|
+
// ---- SYMLINK ----
|
|
2911
|
+
symlink(target, linkPath) {
|
|
2912
|
+
linkPath = this.normalizePath(linkPath);
|
|
2913
|
+
if (this.pathIndex.has(linkPath)) return { status: CODE_TO_STATUS.EEXIST };
|
|
2914
|
+
const parentStatus = this.ensureParent(linkPath);
|
|
2915
|
+
if (parentStatus !== 0) return { status: parentStatus };
|
|
2916
|
+
const targetBytes = encoder10.encode(target);
|
|
2917
|
+
this.createInode(linkPath, INODE_TYPE.SYMLINK, DEFAULT_SYMLINK_MODE, targetBytes.byteLength, targetBytes);
|
|
2918
|
+
this.commitPending();
|
|
2919
|
+
return { status: 0 };
|
|
2920
|
+
}
|
|
2921
|
+
// ---- READLINK ----
|
|
2922
|
+
readlink(path) {
|
|
2923
|
+
path = this.normalizePath(path);
|
|
2924
|
+
const idx = this.pathIndex.get(path);
|
|
2925
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2926
|
+
const inode = this.readInode(idx);
|
|
2927
|
+
if (inode.type !== INODE_TYPE.SYMLINK) return { status: CODE_TO_STATUS.EINVAL, data: null };
|
|
2928
|
+
const target = this.readData(inode.firstBlock, inode.blockCount, inode.size);
|
|
2929
|
+
return { status: 0, data: target };
|
|
2930
|
+
}
|
|
2931
|
+
// ---- LINK (hard link — copies the file) ----
|
|
2932
|
+
link(existingPath, newPath) {
|
|
2933
|
+
return this.copy(existingPath, newPath);
|
|
2934
|
+
}
|
|
2935
|
+
// ---- OPEN (file descriptor) ----
|
|
2936
|
+
open(path, flags, tabId) {
|
|
2937
|
+
path = this.normalizePath(path);
|
|
2938
|
+
const hasCreate = (flags & 64) !== 0;
|
|
2939
|
+
const hasTrunc = (flags & 512) !== 0;
|
|
2940
|
+
const hasExcl = (flags & 128) !== 0;
|
|
2941
|
+
let idx = this.resolvePathComponents(path, true);
|
|
2942
|
+
if (idx === void 0) {
|
|
2943
|
+
if (!hasCreate) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
2944
|
+
const mode = DEFAULT_FILE_MODE & ~(this.umask & 511);
|
|
2945
|
+
idx = this.createInode(path, INODE_TYPE.FILE, mode, 0);
|
|
2946
|
+
} else if (hasExcl && hasCreate) {
|
|
2947
|
+
return { status: CODE_TO_STATUS.EEXIST, data: null };
|
|
2948
|
+
}
|
|
2949
|
+
if (hasTrunc) {
|
|
2950
|
+
this.truncate(path, 0);
|
|
2951
|
+
}
|
|
2952
|
+
const fd = this.nextFd++;
|
|
2953
|
+
this.fdTable.set(fd, { tabId, inodeIdx: idx, position: 0, flags });
|
|
2954
|
+
const buf = new Uint8Array(4);
|
|
2955
|
+
new DataView(buf.buffer).setUint32(0, fd, true);
|
|
2956
|
+
return { status: 0, data: buf };
|
|
2957
|
+
}
|
|
2958
|
+
// ---- CLOSE ----
|
|
2959
|
+
close(fd) {
|
|
2960
|
+
if (!this.fdTable.has(fd)) return { status: CODE_TO_STATUS.EBADF };
|
|
2961
|
+
this.fdTable.delete(fd);
|
|
2962
|
+
return { status: 0 };
|
|
2963
|
+
}
|
|
2964
|
+
// ---- FREAD ----
|
|
2965
|
+
fread(fd, length, position) {
|
|
2966
|
+
const entry = this.fdTable.get(fd);
|
|
2967
|
+
if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
|
|
2968
|
+
const inode = this.readInode(entry.inodeIdx);
|
|
2969
|
+
const pos = position ?? entry.position;
|
|
2970
|
+
const readLen = Math.min(length, inode.size - pos);
|
|
2971
|
+
if (readLen <= 0) return { status: 0, data: new Uint8Array(0) };
|
|
2972
|
+
const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
|
|
2973
|
+
const buf = new Uint8Array(readLen);
|
|
2974
|
+
this.handle.read(buf, { at: dataOffset });
|
|
2975
|
+
if (position === null) {
|
|
2976
|
+
entry.position += readLen;
|
|
2977
|
+
}
|
|
2978
|
+
return { status: 0, data: buf };
|
|
2979
|
+
}
|
|
2980
|
+
// ---- FWRITE ----
|
|
2981
|
+
fwrite(fd, data, position) {
|
|
2982
|
+
const entry = this.fdTable.get(fd);
|
|
2983
|
+
if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
|
|
2984
|
+
const inode = this.readInode(entry.inodeIdx);
|
|
2985
|
+
const isAppend = (entry.flags & 1024) !== 0;
|
|
2986
|
+
const pos = isAppend ? inode.size : position ?? entry.position;
|
|
2987
|
+
const endPos = pos + data.byteLength;
|
|
2988
|
+
if (endPos > inode.size) {
|
|
2989
|
+
const neededBlocks = Math.ceil(endPos / this.blockSize);
|
|
2990
|
+
if (neededBlocks > inode.blockCount) {
|
|
2991
|
+
const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
2992
|
+
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
2993
|
+
const newFirst = this.allocateBlocks(neededBlocks);
|
|
2994
|
+
const newBuf = new Uint8Array(endPos);
|
|
2995
|
+
newBuf.set(oldData);
|
|
2996
|
+
newBuf.set(data, pos);
|
|
2997
|
+
this.writeData(newFirst, newBuf);
|
|
2998
|
+
inode.firstBlock = newFirst;
|
|
2999
|
+
inode.blockCount = neededBlocks;
|
|
3000
|
+
} else {
|
|
3001
|
+
const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
|
|
3002
|
+
this.handle.write(data, { at: dataOffset });
|
|
3003
|
+
}
|
|
3004
|
+
inode.size = endPos;
|
|
3005
|
+
} else {
|
|
3006
|
+
const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
|
|
3007
|
+
this.handle.write(data, { at: dataOffset });
|
|
3008
|
+
}
|
|
3009
|
+
inode.mtime = Date.now();
|
|
3010
|
+
this.writeInode(entry.inodeIdx, inode);
|
|
3011
|
+
if (position === null) {
|
|
3012
|
+
entry.position = endPos;
|
|
3013
|
+
}
|
|
3014
|
+
this.commitPending();
|
|
3015
|
+
const buf = new Uint8Array(4);
|
|
3016
|
+
new DataView(buf.buffer).setUint32(0, data.byteLength, true);
|
|
3017
|
+
return { status: 0, data: buf };
|
|
3018
|
+
}
|
|
3019
|
+
// ---- FSTAT ----
|
|
3020
|
+
fstat(fd) {
|
|
3021
|
+
const entry = this.fdTable.get(fd);
|
|
3022
|
+
if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
|
|
3023
|
+
return this.encodeStatResponse(entry.inodeIdx);
|
|
3024
|
+
}
|
|
3025
|
+
// ---- FTRUNCATE ----
|
|
3026
|
+
ftruncate(fd, len = 0) {
|
|
3027
|
+
const entry = this.fdTable.get(fd);
|
|
3028
|
+
if (!entry) return { status: CODE_TO_STATUS.EBADF };
|
|
3029
|
+
const inode = this.readInode(entry.inodeIdx);
|
|
3030
|
+
const path = this.readPath(inode.pathOffset, inode.pathLength);
|
|
3031
|
+
return this.truncate(path, len);
|
|
3032
|
+
}
|
|
3033
|
+
// ---- FSYNC ----
|
|
3034
|
+
fsync() {
|
|
3035
|
+
this.commitPending();
|
|
3036
|
+
this.handle.flush();
|
|
3037
|
+
return { status: 0 };
|
|
3038
|
+
}
|
|
3039
|
+
// ---- OPENDIR ----
|
|
3040
|
+
opendir(path, tabId) {
|
|
3041
|
+
path = this.normalizePath(path);
|
|
3042
|
+
const idx = this.resolvePathComponents(path, true);
|
|
3043
|
+
if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
|
|
3044
|
+
const inode = this.readInode(idx);
|
|
3045
|
+
if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
|
|
3046
|
+
const fd = this.nextFd++;
|
|
3047
|
+
this.fdTable.set(fd, { tabId, inodeIdx: idx, position: 0, flags: 0 });
|
|
3048
|
+
const buf = new Uint8Array(4);
|
|
3049
|
+
new DataView(buf.buffer).setUint32(0, fd, true);
|
|
3050
|
+
return { status: 0, data: buf };
|
|
3051
|
+
}
|
|
3052
|
+
// ---- MKDTEMP ----
|
|
3053
|
+
mkdtemp(prefix) {
|
|
3054
|
+
const suffix = Math.random().toString(36).substring(2, 8);
|
|
3055
|
+
const path = this.normalizePath(prefix + suffix);
|
|
3056
|
+
const parentStatus = this.ensureParent(path);
|
|
3057
|
+
if (parentStatus !== 0) {
|
|
3058
|
+
const parentPath = path.substring(0, path.lastIndexOf("/"));
|
|
3059
|
+
if (parentPath) {
|
|
3060
|
+
this.mkdirRecursive(parentPath);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
|
|
3064
|
+
this.createInode(path, INODE_TYPE.DIRECTORY, mode, 0);
|
|
3065
|
+
this.commitPending();
|
|
3066
|
+
return { status: 0, data: encoder10.encode(path) };
|
|
3067
|
+
}
|
|
3068
|
+
// ========== Helpers ==========
|
|
3069
|
+
getDirectChildren(dirPath) {
|
|
3070
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
3071
|
+
const children = [];
|
|
3072
|
+
for (const path of this.pathIndex.keys()) {
|
|
3073
|
+
if (path === dirPath) continue;
|
|
3074
|
+
if (!path.startsWith(prefix)) continue;
|
|
3075
|
+
const rest = path.substring(prefix.length);
|
|
3076
|
+
if (!rest.includes("/")) {
|
|
3077
|
+
children.push(path);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return children.sort();
|
|
3081
|
+
}
|
|
3082
|
+
getAllDescendants(dirPath) {
|
|
3083
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
3084
|
+
const descendants = [];
|
|
3085
|
+
for (const path of this.pathIndex.keys()) {
|
|
3086
|
+
if (path.startsWith(prefix)) descendants.push(path);
|
|
3087
|
+
}
|
|
3088
|
+
return descendants.sort((a, b) => {
|
|
3089
|
+
const da = a.split("/").length;
|
|
3090
|
+
const db = b.split("/").length;
|
|
3091
|
+
return db - da;
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
ensureParent(path) {
|
|
3095
|
+
const lastSlash = path.lastIndexOf("/");
|
|
3096
|
+
if (lastSlash <= 0) return 0;
|
|
3097
|
+
const parentPath = path.substring(0, lastSlash);
|
|
3098
|
+
const parentIdx = this.pathIndex.get(parentPath);
|
|
3099
|
+
if (parentIdx === void 0) return CODE_TO_STATUS.ENOENT;
|
|
3100
|
+
const parentInode = this.readInode(parentIdx);
|
|
3101
|
+
if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
|
|
3102
|
+
return 0;
|
|
3103
|
+
}
|
|
3104
|
+
/** Clean up all fds owned by a tab */
|
|
3105
|
+
cleanupTab(tabId) {
|
|
3106
|
+
for (const [fd, entry] of this.fdTable) {
|
|
3107
|
+
if (entry.tabId === tabId) {
|
|
3108
|
+
this.fdTable.delete(fd);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/** Get all file paths and their data for OPFS sync */
|
|
3113
|
+
getAllFiles() {
|
|
3114
|
+
const files = [];
|
|
3115
|
+
for (const [path, idx] of this.pathIndex) {
|
|
3116
|
+
files.push({ path, idx });
|
|
3117
|
+
}
|
|
3118
|
+
return files;
|
|
3119
|
+
}
|
|
3120
|
+
/** Get file path for a file descriptor (used by OPFS sync for FD-based ops) */
|
|
3121
|
+
getPathForFd(fd) {
|
|
3122
|
+
const entry = this.fdTable.get(fd);
|
|
3123
|
+
if (!entry) return null;
|
|
3124
|
+
const inode = this.readInode(entry.inodeIdx);
|
|
3125
|
+
return this.readPath(inode.pathOffset, inode.pathLength);
|
|
3126
|
+
}
|
|
3127
|
+
/** Get file data by inode index */
|
|
3128
|
+
getInodeData(idx) {
|
|
3129
|
+
const inode = this.readInode(idx);
|
|
3130
|
+
const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
3131
|
+
return { type: inode.type, data, mtime: inode.mtime };
|
|
3132
|
+
}
|
|
3133
|
+
/** Export all files/dirs/symlinks from the VFS */
|
|
3134
|
+
exportAll() {
|
|
3135
|
+
const result = [];
|
|
3136
|
+
for (const [path, idx] of this.pathIndex) {
|
|
3137
|
+
const inode = this.readInode(idx);
|
|
3138
|
+
let data = null;
|
|
3139
|
+
if (inode.type === INODE_TYPE.FILE || inode.type === INODE_TYPE.SYMLINK) {
|
|
3140
|
+
data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
3141
|
+
}
|
|
3142
|
+
result.push({ path, type: inode.type, data, mode: inode.mode, mtime: inode.mtime });
|
|
3143
|
+
}
|
|
3144
|
+
result.sort((a, b) => {
|
|
3145
|
+
if (a.type === INODE_TYPE.DIRECTORY && b.type !== INODE_TYPE.DIRECTORY) return -1;
|
|
3146
|
+
if (a.type !== INODE_TYPE.DIRECTORY && b.type === INODE_TYPE.DIRECTORY) return 1;
|
|
3147
|
+
return a.path.localeCompare(b.path);
|
|
3148
|
+
});
|
|
3149
|
+
return result;
|
|
3150
|
+
}
|
|
3151
|
+
flush() {
|
|
3152
|
+
this.handle.flush();
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
|
|
3156
|
+
// src/helpers.ts
|
|
3157
|
+
async function navigateToRoot(root) {
|
|
3158
|
+
let dir = await navigator.storage.getDirectory();
|
|
3159
|
+
if (root && root !== "/") {
|
|
3160
|
+
for (const seg of root.split("/").filter(Boolean)) {
|
|
3161
|
+
dir = await dir.getDirectoryHandle(seg, { create: true });
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return dir;
|
|
3165
|
+
}
|
|
3166
|
+
async function ensureParentDirs(rootDir, path) {
|
|
3167
|
+
const parts = path.split("/").filter(Boolean);
|
|
3168
|
+
parts.pop();
|
|
3169
|
+
let dir = rootDir;
|
|
3170
|
+
for (const part of parts) {
|
|
3171
|
+
dir = await dir.getDirectoryHandle(part, { create: true });
|
|
3172
|
+
}
|
|
3173
|
+
return dir;
|
|
3174
|
+
}
|
|
3175
|
+
function basename2(path) {
|
|
3176
|
+
const parts = path.split("/").filter(Boolean);
|
|
3177
|
+
return parts[parts.length - 1] || "";
|
|
3178
|
+
}
|
|
3179
|
+
async function writeOPFSFile(rootDir, path, data) {
|
|
3180
|
+
const parentDir = await ensureParentDirs(rootDir, path);
|
|
3181
|
+
const name = basename2(path);
|
|
3182
|
+
const fileHandle = await parentDir.getFileHandle(name, { create: true });
|
|
3183
|
+
const syncHandle = await fileHandle.createSyncAccessHandle();
|
|
3184
|
+
try {
|
|
3185
|
+
syncHandle.truncate(0);
|
|
3186
|
+
if (data.byteLength > 0) {
|
|
3187
|
+
syncHandle.write(data, { at: 0 });
|
|
3188
|
+
}
|
|
3189
|
+
syncHandle.flush();
|
|
3190
|
+
} finally {
|
|
3191
|
+
syncHandle.close();
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
async function clearDirectory(dir, skip) {
|
|
3195
|
+
const entries = [];
|
|
3196
|
+
for await (const name of dir.keys()) {
|
|
3197
|
+
if (!skip.has(name)) entries.push(name);
|
|
3198
|
+
}
|
|
3199
|
+
for (const name of entries) {
|
|
3200
|
+
await dir.removeEntry(name, { recursive: true });
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function readOPFSRecursive(dir, prefix, skip) {
|
|
3204
|
+
const result = [];
|
|
3205
|
+
for await (const [name, handle] of dir.entries()) {
|
|
3206
|
+
if (prefix === "" && skip.has(name)) continue;
|
|
3207
|
+
const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
|
|
3208
|
+
if (handle.kind === "directory") {
|
|
3209
|
+
result.push({ path: fullPath, type: "directory" });
|
|
3210
|
+
const children = await readOPFSRecursive(handle, fullPath, skip);
|
|
3211
|
+
result.push(...children);
|
|
3212
|
+
} else {
|
|
3213
|
+
const file = await handle.getFile();
|
|
3214
|
+
const data = await file.arrayBuffer();
|
|
3215
|
+
result.push({ path: fullPath, type: "file", data });
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return result;
|
|
3219
|
+
}
|
|
3220
|
+
async function unpackToOPFS(root = "/") {
|
|
3221
|
+
const rootDir = await navigateToRoot(root);
|
|
3222
|
+
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
|
|
3223
|
+
const handle = await vfsFileHandle.createSyncAccessHandle();
|
|
3224
|
+
let entries;
|
|
3225
|
+
try {
|
|
3226
|
+
const engine = new VFSEngine();
|
|
3227
|
+
engine.init(handle);
|
|
3228
|
+
entries = engine.exportAll();
|
|
3229
|
+
} finally {
|
|
3230
|
+
handle.close();
|
|
3231
|
+
}
|
|
3232
|
+
await clearDirectory(rootDir, /* @__PURE__ */ new Set([".vfs.bin"]));
|
|
3233
|
+
let files = 0;
|
|
3234
|
+
let directories = 0;
|
|
3235
|
+
for (const entry of entries) {
|
|
3236
|
+
if (entry.path === "/") continue;
|
|
3237
|
+
if (entry.type === INODE_TYPE.DIRECTORY) {
|
|
3238
|
+
await ensureParentDirs(rootDir, entry.path + "/dummy");
|
|
3239
|
+
const name = basename2(entry.path);
|
|
3240
|
+
const parent = await ensureParentDirs(rootDir, entry.path);
|
|
3241
|
+
await parent.getDirectoryHandle(name, { create: true });
|
|
3242
|
+
directories++;
|
|
3243
|
+
} else if (entry.type === INODE_TYPE.FILE) {
|
|
3244
|
+
await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
|
|
3245
|
+
files++;
|
|
3246
|
+
} else if (entry.type === INODE_TYPE.SYMLINK) {
|
|
3247
|
+
await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
|
|
3248
|
+
files++;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
return { files, directories };
|
|
3252
|
+
}
|
|
3253
|
+
async function loadFromOPFS(root = "/") {
|
|
3254
|
+
const rootDir = await navigateToRoot(root);
|
|
3255
|
+
const opfsEntries = await readOPFSRecursive(rootDir, "", /* @__PURE__ */ new Set([".vfs.bin"]));
|
|
3256
|
+
try {
|
|
3257
|
+
await rootDir.removeEntry(".vfs.bin");
|
|
3258
|
+
} catch (_) {
|
|
3259
|
+
}
|
|
3260
|
+
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
3261
|
+
const handle = await vfsFileHandle.createSyncAccessHandle();
|
|
3262
|
+
try {
|
|
3263
|
+
const engine = new VFSEngine();
|
|
3264
|
+
engine.init(handle);
|
|
3265
|
+
const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
|
|
3266
|
+
let files = 0;
|
|
3267
|
+
let directories = 0;
|
|
3268
|
+
for (const dir of dirs) {
|
|
3269
|
+
engine.mkdir(dir.path, 16877);
|
|
3270
|
+
directories++;
|
|
3271
|
+
}
|
|
3272
|
+
const fileEntries = opfsEntries.filter((e) => e.type === "file");
|
|
3273
|
+
for (const file of fileEntries) {
|
|
3274
|
+
engine.write(file.path, new Uint8Array(file.data));
|
|
3275
|
+
files++;
|
|
3276
|
+
}
|
|
3277
|
+
engine.flush();
|
|
3278
|
+
return { files, directories };
|
|
3279
|
+
} finally {
|
|
3280
|
+
handle.close();
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function repairVFS(root = "/") {
|
|
3284
|
+
const rootDir = await navigateToRoot(root);
|
|
3285
|
+
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
|
|
3286
|
+
const file = await vfsFileHandle.getFile();
|
|
3287
|
+
const raw = new Uint8Array(await file.arrayBuffer());
|
|
3288
|
+
const fileSize = raw.byteLength;
|
|
3289
|
+
if (fileSize < SUPERBLOCK.SIZE) {
|
|
3290
|
+
throw new Error(`VFS file too small to repair (${fileSize} bytes)`);
|
|
3291
|
+
}
|
|
3292
|
+
const view = new DataView(raw.buffer);
|
|
3293
|
+
let inodeCount;
|
|
3294
|
+
let blockSize;
|
|
3295
|
+
let totalBlocks;
|
|
3296
|
+
let inodeTableOffset;
|
|
3297
|
+
let pathTableOffset;
|
|
3298
|
+
let dataOffset;
|
|
3299
|
+
const magic = view.getUint32(SUPERBLOCK.MAGIC, true);
|
|
3300
|
+
const version = view.getUint32(SUPERBLOCK.VERSION, true);
|
|
3301
|
+
const superblockValid = magic === VFS_MAGIC && version === VFS_VERSION;
|
|
3302
|
+
if (superblockValid) {
|
|
3303
|
+
inodeCount = view.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
3304
|
+
blockSize = view.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
3305
|
+
totalBlocks = view.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
3306
|
+
inodeTableOffset = view.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
3307
|
+
pathTableOffset = view.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
3308
|
+
view.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
3309
|
+
dataOffset = view.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
3310
|
+
view.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
3311
|
+
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0 || inodeCount === 0 || inodeTableOffset >= fileSize || pathTableOffset >= fileSize || dataOffset >= fileSize) {
|
|
3312
|
+
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3313
|
+
inodeCount = DEFAULT_INODE_COUNT;
|
|
3314
|
+
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3315
|
+
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3316
|
+
inodeTableOffset = layout.inodeTableOffset;
|
|
3317
|
+
pathTableOffset = layout.pathTableOffset;
|
|
3318
|
+
dataOffset = layout.dataOffset;
|
|
3319
|
+
}
|
|
3320
|
+
} else {
|
|
3321
|
+
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3322
|
+
inodeCount = DEFAULT_INODE_COUNT;
|
|
3323
|
+
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3324
|
+
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3325
|
+
inodeTableOffset = layout.inodeTableOffset;
|
|
3326
|
+
pathTableOffset = layout.pathTableOffset;
|
|
3327
|
+
dataOffset = layout.dataOffset;
|
|
3328
|
+
}
|
|
3329
|
+
const decoder9 = new TextDecoder();
|
|
3330
|
+
const recovered = [];
|
|
3331
|
+
let lost = 0;
|
|
3332
|
+
const maxInodes = Math.min(inodeCount, Math.floor((fileSize - inodeTableOffset) / INODE_SIZE));
|
|
3333
|
+
for (let i = 0; i < maxInodes; i++) {
|
|
3334
|
+
const off = inodeTableOffset + i * INODE_SIZE;
|
|
3335
|
+
if (off + INODE_SIZE > fileSize) break;
|
|
3336
|
+
const type = raw[off + INODE.TYPE];
|
|
3337
|
+
if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) continue;
|
|
3338
|
+
const inodeView = new DataView(raw.buffer, off, INODE_SIZE);
|
|
3339
|
+
const pathOffset = inodeView.getUint32(INODE.PATH_OFFSET, true);
|
|
3340
|
+
const pathLength = inodeView.getUint16(INODE.PATH_LENGTH, true);
|
|
3341
|
+
const size = inodeView.getFloat64(INODE.SIZE, true);
|
|
3342
|
+
const firstBlock = inodeView.getUint32(INODE.FIRST_BLOCK, true);
|
|
3343
|
+
inodeView.getUint32(INODE.BLOCK_COUNT, true);
|
|
3344
|
+
const absPathOffset = pathTableOffset + pathOffset;
|
|
3345
|
+
if (pathLength === 0 || pathLength > 4096 || absPathOffset + pathLength > fileSize) {
|
|
3346
|
+
lost++;
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
let path;
|
|
3350
|
+
try {
|
|
3351
|
+
path = decoder9.decode(raw.subarray(absPathOffset, absPathOffset + pathLength));
|
|
3352
|
+
} catch {
|
|
3353
|
+
lost++;
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
if (!path.startsWith("/") || path.includes("\0")) {
|
|
3357
|
+
lost++;
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
if (type === INODE_TYPE.DIRECTORY) {
|
|
3361
|
+
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
if (size < 0 || size > fileSize || !isFinite(size)) {
|
|
3365
|
+
lost++;
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
const dataStart = dataOffset + firstBlock * blockSize;
|
|
3369
|
+
if (dataStart + size > fileSize || firstBlock >= totalBlocks) {
|
|
3370
|
+
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3371
|
+
lost++;
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
const data = raw.slice(dataStart, dataStart + size);
|
|
3375
|
+
recovered.push({ path, type, data });
|
|
3376
|
+
}
|
|
3377
|
+
await rootDir.removeEntry(".vfs.bin");
|
|
3378
|
+
const newFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
3379
|
+
const handle = await newFileHandle.createSyncAccessHandle();
|
|
3380
|
+
try {
|
|
3381
|
+
const engine = new VFSEngine();
|
|
3382
|
+
engine.init(handle);
|
|
3383
|
+
const dirs = recovered.filter((e) => e.type === INODE_TYPE.DIRECTORY && e.path !== "/").sort((a, b) => a.path.localeCompare(b.path));
|
|
3384
|
+
const files = recovered.filter((e) => e.type === INODE_TYPE.FILE);
|
|
3385
|
+
const symlinks = recovered.filter((e) => e.type === INODE_TYPE.SYMLINK);
|
|
3386
|
+
for (const dir of dirs) {
|
|
3387
|
+
const result = engine.mkdir(dir.path, 16877);
|
|
3388
|
+
if (result.status !== 0) lost++;
|
|
3389
|
+
}
|
|
3390
|
+
for (const file2 of files) {
|
|
3391
|
+
const result = engine.write(file2.path, file2.data);
|
|
3392
|
+
if (result.status !== 0) lost++;
|
|
3393
|
+
}
|
|
3394
|
+
for (const sym of symlinks) {
|
|
3395
|
+
const target = decoder9.decode(sym.data);
|
|
3396
|
+
const result = engine.symlink(target, sym.path);
|
|
3397
|
+
if (result.status !== 0) lost++;
|
|
3398
|
+
}
|
|
3399
|
+
engine.flush();
|
|
3400
|
+
} finally {
|
|
3401
|
+
handle.close();
|
|
3402
|
+
}
|
|
3403
|
+
const entries = recovered.filter((e) => e.path !== "/").map((e) => ({
|
|
3404
|
+
path: e.path,
|
|
3405
|
+
type: e.type === INODE_TYPE.FILE ? "file" : e.type === INODE_TYPE.DIRECTORY ? "directory" : "symlink",
|
|
3406
|
+
size: e.data.byteLength
|
|
3407
|
+
}));
|
|
3408
|
+
return { recovered: entries.length, lost, entries };
|
|
3409
|
+
}
|
|
3410
|
+
|
|
1667
3411
|
// src/index.ts
|
|
1668
3412
|
function createFS(config) {
|
|
1669
3413
|
return new VFSFileSystem(config);
|
|
@@ -1677,6 +3421,6 @@ function init() {
|
|
|
1677
3421
|
return getDefaultFS().init();
|
|
1678
3422
|
}
|
|
1679
3423
|
|
|
1680
|
-
export { FSError, VFSFileSystem, constants, createError, createFS, getDefaultFS, init, path_exports as path, statusToError };
|
|
3424
|
+
export { FSError, VFSFileSystem, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path_exports as path, repairVFS, statusToError, unpackToOPFS };
|
|
1681
3425
|
//# sourceMappingURL=index.js.map
|
|
1682
3426
|
//# sourceMappingURL=index.js.map
|