@secure-exec/core 0.2.0-rc.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/generated/isolate-runtime.d.ts +2 -2
  2. package/dist/generated/isolate-runtime.js +2 -2
  3. package/dist/index.d.ts +17 -4
  4. package/dist/index.js +10 -2
  5. package/dist/isolate-runtime/require-setup.js +1489 -239
  6. package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
  7. package/dist/kernel/device-backend.d.ts +14 -0
  8. package/dist/kernel/device-backend.js +251 -0
  9. package/dist/kernel/device-layer.js +9 -0
  10. package/dist/kernel/file-lock.js +2 -3
  11. package/dist/kernel/index.d.ts +4 -4
  12. package/dist/kernel/index.js +3 -3
  13. package/dist/kernel/kernel.js +141 -122
  14. package/dist/kernel/mount-table.d.ts +75 -0
  15. package/dist/kernel/mount-table.js +353 -0
  16. package/dist/kernel/permissions.d.ts +9 -0
  17. package/dist/kernel/permissions.js +33 -1
  18. package/dist/kernel/proc-backend.d.ts +30 -0
  19. package/dist/kernel/proc-backend.js +428 -0
  20. package/dist/kernel/proc-layer.js +6 -0
  21. package/dist/kernel/process-table.d.ts +3 -1
  22. package/dist/kernel/process-table.js +23 -3
  23. package/dist/kernel/pty.d.ts +3 -2
  24. package/dist/kernel/pty.js +13 -2
  25. package/dist/kernel/socket-table.d.ts +7 -0
  26. package/dist/kernel/socket-table.js +99 -35
  27. package/dist/kernel/types.d.ts +45 -4
  28. package/dist/kernel/types.js +9 -0
  29. package/dist/kernel/vfs.d.ts +30 -2
  30. package/dist/kernel/vfs.js +19 -2
  31. package/dist/shared/api-types.d.ts +6 -0
  32. package/dist/shared/bridge-contract.d.ts +21 -3
  33. package/dist/shared/bridge-contract.js +2 -0
  34. package/dist/shared/console-formatter.js +8 -8
  35. package/dist/shared/global-exposure.js +95 -0
  36. package/dist/shared/in-memory-fs.d.ts +14 -59
  37. package/dist/shared/in-memory-fs.js +97 -597
  38. package/dist/shared/permissions.js +5 -0
  39. package/dist/test/block-store-conformance.d.ts +34 -0
  40. package/dist/test/block-store-conformance.js +251 -0
  41. package/dist/test/metadata-store-conformance.d.ts +37 -0
  42. package/dist/test/metadata-store-conformance.js +646 -0
  43. package/dist/test/vfs-conformance.d.ts +65 -0
  44. package/dist/test/vfs-conformance.js +842 -0
  45. package/dist/types.d.ts +1 -0
  46. package/dist/vfs/chunked-vfs.d.ts +66 -0
  47. package/dist/vfs/chunked-vfs.js +1290 -0
  48. package/dist/vfs/host-block-store.d.ts +19 -0
  49. package/dist/vfs/host-block-store.js +97 -0
  50. package/dist/vfs/memory-block-store.d.ts +16 -0
  51. package/dist/vfs/memory-block-store.js +45 -0
  52. package/dist/vfs/memory-metadata.d.ts +75 -0
  53. package/dist/vfs/memory-metadata.js +528 -0
  54. package/dist/vfs/sqlite-metadata.d.ts +91 -0
  55. package/dist/vfs/sqlite-metadata.js +582 -0
  56. package/dist/vfs/types.d.ts +210 -0
  57. package/dist/vfs/types.js +8 -0
  58. package/package.json +20 -1
  59. package/dist/kernel/inode-table.d.ts +0 -43
  60. package/dist/kernel/inode-table.js +0 -85
@@ -1,615 +1,115 @@
1
- import { InodeTable } from "../kernel/inode-table.js";
1
+ /**
2
+ * Factory for creating an in-memory VirtualFileSystem backed by ChunkedVFS.
3
+ *
4
+ * Replaces the old monolithic InMemoryFileSystem with
5
+ * ChunkedVFS(InMemoryMetadataStore + InMemoryBlockStore).
6
+ */
2
7
  import { KernelError, O_CREAT, O_EXCL, O_TRUNC } from "../kernel/types.js";
3
- const S_IFREG = 0o100000;
4
- const S_IFDIR = 0o040000;
5
- const S_IFLNK = 0o120000;
6
- const S_IFSOCK = 0o140000;
7
- function normalizePath(path) {
8
- if (!path)
9
- return "/";
10
- let normalized = path.startsWith("/") ? path : `/${path}`;
11
- normalized = normalized.replace(/\/+/g, "/");
12
- if (normalized.length > 1 && normalized.endsWith("/")) {
13
- normalized = normalized.slice(0, -1);
14
- }
15
- return normalized;
16
- }
17
- function splitPath(path) {
18
- const normalized = normalizePath(path);
19
- return normalized === "/" ? [] : normalized.slice(1).split("/");
20
- }
21
- function dirname(path) {
22
- const parts = splitPath(path);
23
- if (parts.length <= 1)
24
- return "/";
25
- return `/${parts.slice(0, -1).join("/")}`;
26
- }
8
+ import { createChunkedVfs } from "../vfs/chunked-vfs.js";
9
+ import { InMemoryMetadataStore } from "../vfs/memory-metadata.js";
10
+ import { InMemoryBlockStore } from "../vfs/memory-block-store.js";
27
11
  /**
28
- * A fully in-memory VirtualFileSystem backed by inode-aware Maps.
29
- * Used as the default filesystem for the browser sandbox and for tests.
30
- * Paths are always POSIX-style (forward slashes, rooted at "/").
12
+ * Create an in-memory VirtualFileSystem using the chunked storage architecture.
13
+ *
14
+ * The returned VFS stores all data in memory via InMemoryMetadataStore and
15
+ * InMemoryBlockStore, composed through ChunkedVFS. It also includes a
16
+ * synchronous `prepareOpenSync` method used by the kernel for O_CREAT/O_EXCL/O_TRUNC
17
+ * handling during fdOpen.
31
18
  */
32
- export class InMemoryFileSystem {
33
- inodeTable;
34
- files = new Map();
35
- fileContents = new Map();
36
- dirs = new Map();
37
- symlinks = new Map();
38
- constructor(inodeTable = new InodeTable()) {
39
- this.inodeTable = inodeTable;
40
- this.dirs.set("/", this.allocateDirectoryInode().ino);
41
- }
42
- // Rebind the filesystem to the kernel's shared inode table.
43
- setInodeTable(inodeTable) {
44
- if (this.inodeTable === inodeTable)
45
- return;
46
- const oldTable = this.inodeTable;
47
- this.inodeTable = inodeTable;
48
- this.reindexInodes(oldTable);
49
- }
50
- getInodeForPath(path) {
51
- const normalized = normalizePath(path);
52
- const resolved = this.resolveSymlink(normalized);
53
- return this.files.get(resolved) ?? this.dirs.get(resolved) ?? null;
54
- }
55
- readFileByInode(ino) {
56
- const data = this.fileContents.get(ino);
57
- if (!data) {
58
- throw new Error(`ENOENT: inode ${ino} has no file data`);
59
- }
60
- this.requireInode(ino).atime = new Date();
61
- return data;
62
- }
63
- writeFileByInode(ino, content) {
64
- this.requireFileInode(ino);
65
- this.fileContents.set(ino, content);
66
- this.updateFileMetadata(ino, content.byteLength);
67
- }
68
- preadByInode(ino, offset, length) {
69
- const data = this.readFileByInode(ino);
70
- return data.slice(offset, offset + length);
71
- }
72
- statByInode(ino) {
73
- return this.statForInode(this.requireInode(ino));
74
- }
75
- deleteInodeData(ino) {
76
- this.fileContents.delete(ino);
77
- }
78
- listDirEntries(path) {
79
- const normalized = normalizePath(path);
80
- const dirIno = this.dirs.get(normalized);
81
- if (dirIno === undefined) {
82
- throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);
83
- }
84
- const prefix = normalized === "/" ? "/" : `${normalized}/`;
85
- const entries = new Map();
86
- const parentPath = normalized === "/" ? "/" : dirname(normalized);
87
- const parentIno = this.dirs.get(parentPath) ?? dirIno;
88
- entries.set(".", {
89
- name: ".",
90
- isDirectory: true,
91
- isSymbolicLink: false,
92
- ino: dirIno,
93
- });
94
- entries.set("..", {
95
- name: "..",
96
- isDirectory: true,
97
- isSymbolicLink: false,
98
- ino: parentIno,
99
- });
100
- for (const [filePath, ino] of this.files.entries()) {
101
- if (!filePath.startsWith(prefix))
102
- continue;
103
- const rest = filePath.slice(prefix.length);
104
- if (rest && !rest.includes("/")) {
105
- entries.set(rest, {
106
- name: rest,
107
- isDirectory: false,
108
- isSymbolicLink: false,
109
- ino,
110
- });
111
- }
112
- }
113
- for (const [dirPath, ino] of this.dirs.entries()) {
114
- if (!dirPath.startsWith(prefix))
115
- continue;
116
- const rest = dirPath.slice(prefix.length);
117
- if (rest && !rest.includes("/")) {
118
- entries.set(rest, {
119
- name: rest,
120
- isDirectory: true,
121
- isSymbolicLink: false,
122
- ino,
123
- });
124
- }
125
- }
126
- for (const linkPath of this.symlinks.keys()) {
127
- if (!linkPath.startsWith(prefix))
128
- continue;
129
- const rest = linkPath.slice(prefix.length);
130
- if (rest && !rest.includes("/")) {
131
- entries.set(rest, {
132
- name: rest,
133
- isDirectory: false,
134
- isSymbolicLink: true,
135
- ino: 0,
136
- });
137
- }
138
- }
139
- return Array.from(entries.values());
140
- }
141
- async readFile(path) {
142
- const normalized = normalizePath(path);
143
- const resolved = this.resolveSymlink(normalized);
144
- const ino = this.files.get(resolved);
145
- if (ino === undefined) {
146
- throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
147
- }
148
- return this.readFileByInode(ino);
149
- }
150
- async readTextFile(path) {
151
- const data = await this.readFile(path);
152
- return new TextDecoder().decode(data);
153
- }
154
- async readDir(path) {
155
- return this.listDirEntries(path).map((entry) => entry.name);
156
- }
157
- async readDirWithTypes(path) {
158
- return this.listDirEntries(path);
159
- }
160
- async writeFile(path, content) {
161
- const normalized = normalizePath(path);
162
- await this.mkdir(dirname(normalized));
163
- const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
164
- const resolved = this.resolveIfSymlink(normalized) ?? normalized;
165
- const existing = this.files.get(resolved);
166
- if (existing !== undefined) {
167
- this.writeFileByInode(existing, data);
168
- return;
169
- }
170
- const inode = this.allocateFileInode();
171
- this.files.set(resolved, inode.ino);
172
- this.fileContents.set(inode.ino, data);
173
- this.updateFileMetadata(inode.ino, data.byteLength);
174
- }
175
- prepareOpenSync(path, flags) {
176
- const normalized = normalizePath(path);
177
- const resolved = this.resolveIfSymlink(normalized) ?? normalized;
19
+ export function createInMemoryFileSystem() {
20
+ const metadata = new InMemoryMetadataStore();
21
+ const blocks = new InMemoryBlockStore();
22
+ const vfs = createChunkedVfs({ metadata, blocks });
23
+ // The kernel's fdOpen calls prepareOpenSync synchronously for O_CREAT,
24
+ // O_EXCL, and O_TRUNC flags. Since InMemoryMetadataStore is backed by
25
+ // synchronous Maps, we use its synchronous accessor methods directly.
26
+ function prepareOpenSync(path, flags) {
178
27
  const hasCreate = (flags & O_CREAT) !== 0;
179
28
  const hasExcl = (flags & O_EXCL) !== 0;
180
29
  const hasTrunc = (flags & O_TRUNC) !== 0;
181
- const fileIno = this.files.get(resolved);
182
- const exists = fileIno !== undefined || this.dirs.has(resolved) || this.symlinks.has(normalized);
30
+ // Check if path exists via synchronous resolution.
31
+ let resolvedIno;
32
+ try {
33
+ resolvedIno = metadata.resolvePathSync(path);
34
+ }
35
+ catch {
36
+ // ENOENT is expected when the file doesn't exist yet.
37
+ }
38
+ const exists = resolvedIno !== undefined;
183
39
  if (hasCreate && hasExcl && exists) {
184
- throw new KernelError("EEXIST", `file already exists, open '${normalized}'`);
40
+ throw new KernelError("EEXIST", `file already exists, open '${path}'`);
185
41
  }
186
42
  let created = false;
187
- if (fileIno === undefined && hasCreate) {
188
- const parts = splitPath(dirname(resolved));
189
- let current = "";
190
- for (const part of parts) {
191
- current += `/${part}`;
192
- if (!this.dirs.has(current)) {
193
- this.dirs.set(current, this.allocateDirectoryInode().ino);
43
+ if (!exists && hasCreate) {
44
+ // Create parent directories and the file synchronously.
45
+ const parts = path.replace(/\/+/g, "/").replace(/\/$/, "").split("/").filter(Boolean);
46
+ let parentIno = 1; // root
47
+ for (let i = 0; i < parts.length - 1; i++) {
48
+ const childIno = metadata.lookupSync(parentIno, parts[i]);
49
+ if (childIno === null) {
50
+ const newIno = metadata.createInodeSync({
51
+ type: "directory",
52
+ mode: 0o755,
53
+ uid: 0,
54
+ gid: 0,
55
+ });
56
+ metadata.updateInodeSync(newIno, { nlink: 2 });
57
+ metadata.createDentrySync(parentIno, parts[i], newIno, "directory");
58
+ // Increment parent nlink for subdirectory
59
+ const parentMeta = metadata.getInodeSync(parentIno);
60
+ if (parentMeta) {
61
+ metadata.updateInodeSync(parentIno, { nlink: parentMeta.nlink + 1 });
62
+ }
63
+ parentIno = newIno;
64
+ }
65
+ else {
66
+ parentIno = childIno;
194
67
  }
195
68
  }
196
- const inode = this.allocateFileInode();
197
- this.files.set(resolved, inode.ino);
198
- this.fileContents.set(inode.ino, new Uint8Array(0));
199
- this.updateFileMetadata(inode.ino, 0);
200
- created = true;
201
- }
202
- if (hasTrunc) {
203
- if (this.dirs.has(resolved)) {
204
- throw new KernelError("EISDIR", `illegal operation on a directory, open '${normalized}'`);
205
- }
206
- const truncateIno = this.files.get(resolved);
207
- if (truncateIno === undefined) {
208
- throw new KernelError("ENOENT", `no such file or directory, open '${normalized}'`);
209
- }
210
- this.fileContents.set(truncateIno, new Uint8Array(0));
211
- this.updateFileMetadata(truncateIno, 0);
212
- }
213
- return created;
214
- }
215
- async createDir(path) {
216
- const normalized = normalizePath(path);
217
- const parent = dirname(normalized);
218
- if (!this.dirs.has(parent)) {
219
- throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
220
- }
221
- if (!this.dirs.has(normalized)) {
222
- this.dirs.set(normalized, this.allocateDirectoryInode().ino);
223
- }
224
- }
225
- async mkdir(path, _options) {
226
- const parts = splitPath(path);
227
- let current = "";
228
- for (const part of parts) {
229
- current += `/${part}`;
230
- if (!this.dirs.has(current)) {
231
- this.dirs.set(current, this.allocateDirectoryInode().ino);
232
- }
233
- }
234
- }
235
- resolveIfSymlink(normalized) {
236
- return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
237
- }
238
- resolveSymlink(normalized, maxDepth = 16) {
239
- let current = normalized;
240
- for (let i = 0; i < maxDepth; i++) {
241
- const target = this.symlinks.get(current);
242
- if (!target)
243
- return current;
244
- current = target.startsWith("/")
245
- ? normalizePath(target)
246
- : normalizePath(`${dirname(current)}/${target}`);
247
- }
248
- throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
249
- }
250
- statForInode(inode) {
251
- const isDirectory = (inode.mode & 0o170000) === S_IFDIR;
252
- return {
253
- mode: inode.mode,
254
- size: isDirectory ? 4096 : inode.size,
255
- isDirectory,
256
- isSymbolicLink: false,
257
- atimeMs: inode.atime.getTime(),
258
- mtimeMs: inode.mtime.getTime(),
259
- ctimeMs: inode.ctime.getTime(),
260
- birthtimeMs: inode.birthtime.getTime(),
261
- ino: inode.ino,
262
- nlink: inode.nlink,
263
- uid: inode.uid,
264
- gid: inode.gid,
265
- };
266
- }
267
- statEntry(normalized) {
268
- const fileIno = this.files.get(normalized);
269
- if (fileIno !== undefined) {
270
- return this.statByInode(fileIno);
271
- }
272
- const dirIno = this.dirs.get(normalized);
273
- if (dirIno !== undefined) {
274
- return this.statByInode(dirIno);
275
- }
276
- throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
277
- }
278
- async exists(path) {
279
- const normalized = normalizePath(path);
280
- if (this.symlinks.has(normalized)) {
281
- try {
282
- const resolved = this.resolveSymlink(normalized);
283
- return this.files.has(resolved) || this.dirs.has(resolved);
284
- }
285
- catch {
286
- return false;
287
- }
288
- }
289
- return this.files.has(normalized) || this.dirs.has(normalized);
290
- }
291
- async stat(path) {
292
- const normalized = normalizePath(path);
293
- const resolved = this.resolveSymlink(normalized);
294
- return this.statEntry(resolved);
295
- }
296
- async removeFile(path) {
297
- const normalized = normalizePath(path);
298
- if (this.symlinks.delete(normalized)) {
299
- return;
300
- }
301
- const resolved = this.resolveSymlink(normalized);
302
- const ino = this.files.get(resolved);
303
- if (ino === undefined) {
304
- throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
305
- }
306
- this.files.delete(resolved);
307
- this.inodeTable.decrementLinks(ino);
308
- if (this.inodeTable.shouldDelete(ino)) {
309
- this.deleteInodeData(ino);
310
- this.inodeTable.delete(ino);
311
- }
312
- }
313
- async removeDir(path) {
314
- const normalized = normalizePath(path);
315
- if (normalized === "/") {
316
- throw new Error("EPERM: operation not permitted, rmdir '/'");
317
- }
318
- if (!this.dirs.has(normalized)) {
319
- throw new Error(`ENOENT: no such file or directory, rmdir '${normalized}'`);
320
- }
321
- const prefix = normalized.endsWith("/") ? normalized : `${normalized}/`;
322
- for (const filePath of this.files.keys()) {
323
- if (filePath.startsWith(prefix)) {
324
- throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
325
- }
326
- }
327
- for (const dirPath of this.dirs.keys()) {
328
- if (dirPath !== normalized && dirPath.startsWith(prefix)) {
329
- throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
330
- }
331
- }
332
- for (const linkPath of this.symlinks.keys()) {
333
- if (linkPath.startsWith(prefix)) {
334
- throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
69
+ // Create the file inode.
70
+ const fileName = parts[parts.length - 1];
71
+ if (fileName) {
72
+ const fileIno = metadata.createInodeSync({
73
+ type: "file",
74
+ mode: 0o644,
75
+ uid: 0,
76
+ gid: 0,
77
+ });
78
+ metadata.updateInodeSync(fileIno, {
79
+ nlink: 1,
80
+ size: 0,
81
+ storageMode: "inline",
82
+ inlineContent: new Uint8Array(0),
83
+ });
84
+ try {
85
+ metadata.createDentrySync(parentIno, fileName, fileIno, "file");
86
+ created = true;
87
+ }
88
+ catch {
89
+ // EEXIST from race condition, ignore.
90
+ }
335
91
  }
336
92
  }
337
- const ino = this.dirs.get(normalized);
338
- this.dirs.delete(normalized);
339
- this.inodeTable.decrementLinks(ino);
340
- if (this.inodeTable.shouldDelete(ino)) {
341
- this.inodeTable.delete(ino);
342
- }
343
- }
344
- async rename(oldPath, newPath) {
345
- const oldNormalized = normalizePath(oldPath);
346
- const newNormalized = normalizePath(newPath);
347
- if (oldNormalized === newNormalized) {
348
- return;
349
- }
350
- if (!this.dirs.has(dirname(newNormalized))) {
351
- throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
352
- }
353
- if (this.files.has(oldNormalized)) {
354
- if (this.dirs.has(newNormalized)) {
355
- throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
93
+ if (hasTrunc && resolvedIno !== undefined) {
94
+ // Check that the target is a file, not a directory.
95
+ const meta = metadata.getInodeSync(resolvedIno);
96
+ if (meta && meta.type === "directory") {
97
+ throw new KernelError("EISDIR", `illegal operation on a directory, open '${path}'`);
356
98
  }
357
- if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
358
- throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
99
+ // Truncate file to 0 bytes.
100
+ metadata.updateInodeSync(resolvedIno, {
101
+ size: 0,
102
+ storageMode: "inline",
103
+ inlineContent: new Uint8Array(0),
104
+ });
105
+ // Delete any existing chunks synchronously.
106
+ const keys = metadata.deleteAllChunksSync(resolvedIno);
107
+ if (keys.length > 0) {
108
+ // Fire-and-forget async block deletion. Blocks are in memory so this resolves immediately.
109
+ void blocks.deleteMany(keys);
359
110
  }
360
- const ino = this.files.get(oldNormalized);
361
- this.files.delete(oldNormalized);
362
- this.files.set(newNormalized, ino);
363
- return;
364
111
  }
365
- if (this.symlinks.has(oldNormalized)) {
366
- if (this.files.has(newNormalized) ||
367
- this.dirs.has(newNormalized) ||
368
- this.symlinks.has(newNormalized)) {
369
- throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
370
- }
371
- const target = this.symlinks.get(oldNormalized);
372
- this.symlinks.delete(oldNormalized);
373
- this.symlinks.set(newNormalized, target);
374
- return;
375
- }
376
- if (!this.dirs.has(oldNormalized)) {
377
- throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
378
- }
379
- if (oldNormalized === "/") {
380
- throw new Error(`EPERM: operation not permitted, rename '${oldNormalized}'`);
381
- }
382
- if (newNormalized.startsWith(`${oldNormalized}/`)) {
383
- throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
384
- }
385
- if (this.dirs.has(newNormalized) ||
386
- this.files.has(newNormalized) ||
387
- this.symlinks.has(newNormalized)) {
388
- throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
389
- }
390
- const sourcePrefix = `${oldNormalized}/`;
391
- const targetPrefix = `${newNormalized}/`;
392
- const dirEntries = Array.from(this.dirs.entries())
393
- .filter(([path]) => path === oldNormalized || path.startsWith(sourcePrefix))
394
- .sort(([a], [b]) => a.length - b.length);
395
- const fileEntries = Array.from(this.files.entries()).filter(([path]) => path.startsWith(sourcePrefix));
396
- const symlinkEntries = Array.from(this.symlinks.entries()).filter(([path]) => path.startsWith(sourcePrefix));
397
- for (const [path] of dirEntries)
398
- this.dirs.delete(path);
399
- for (const [path] of fileEntries)
400
- this.files.delete(path);
401
- for (const [path] of symlinkEntries)
402
- this.symlinks.delete(path);
403
- for (const [path, ino] of dirEntries) {
404
- const nextPath = path === oldNormalized
405
- ? newNormalized
406
- : `${targetPrefix}${path.slice(sourcePrefix.length)}`;
407
- this.dirs.set(nextPath, ino);
408
- }
409
- for (const [path, ino] of fileEntries) {
410
- this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, ino);
411
- }
412
- for (const [path, target] of symlinkEntries) {
413
- this.symlinks.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, target);
414
- }
415
- }
416
- async symlink(target, linkPath) {
417
- const normalized = normalizePath(linkPath);
418
- if (this.files.has(normalized) ||
419
- this.dirs.has(normalized) ||
420
- this.symlinks.has(normalized)) {
421
- throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
422
- }
423
- await this.mkdir(dirname(normalized));
424
- this.symlinks.set(normalized, target);
425
- }
426
- async readlink(path) {
427
- const normalized = normalizePath(path);
428
- const target = this.symlinks.get(normalized);
429
- if (target === undefined) {
430
- throw new Error(`EINVAL: invalid argument, readlink '${normalized}'`);
431
- }
432
- return target;
433
- }
434
- async lstat(path) {
435
- const normalized = normalizePath(path);
436
- const target = this.symlinks.get(normalized);
437
- if (target !== undefined) {
438
- const now = Date.now();
439
- return {
440
- mode: S_IFLNK | 0o777,
441
- size: new TextEncoder().encode(target).byteLength,
442
- isDirectory: false,
443
- isSymbolicLink: true,
444
- atimeMs: now,
445
- mtimeMs: now,
446
- ctimeMs: now,
447
- birthtimeMs: now,
448
- ino: 0,
449
- nlink: 1,
450
- uid: 0,
451
- gid: 0,
452
- };
453
- }
454
- return this.statEntry(normalized);
455
- }
456
- async link(oldPath, newPath) {
457
- const oldNormalized = normalizePath(oldPath);
458
- const newNormalized = normalizePath(newPath);
459
- const resolved = this.resolveSymlink(oldNormalized);
460
- const ino = this.files.get(resolved);
461
- if (ino === undefined) {
462
- throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
463
- }
464
- if (this.files.has(newNormalized) ||
465
- this.dirs.has(newNormalized) ||
466
- this.symlinks.has(newNormalized)) {
467
- throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
468
- }
469
- await this.mkdir(dirname(newNormalized));
470
- this.files.set(newNormalized, ino);
471
- this.inodeTable.incrementLinks(ino);
472
- }
473
- async chmod(path, mode) {
474
- const inode = this.requirePathInode(path, "chmod");
475
- const callerTypeBits = mode & 0o170000;
476
- if (callerTypeBits !== 0) {
477
- inode.mode = mode;
478
- }
479
- else {
480
- const existingTypeBits = inode.mode & 0o170000;
481
- inode.mode = existingTypeBits | (mode & 0o7777);
482
- }
483
- inode.ctime = new Date();
484
- }
485
- async chown(path, uid, gid) {
486
- const inode = this.requirePathInode(path, "chown");
487
- inode.uid = uid;
488
- inode.gid = gid;
489
- inode.ctime = new Date();
490
- }
491
- async utimes(path, atime, mtime) {
492
- const inode = this.requirePathInode(path, "utimes");
493
- inode.atime = new Date(atime * 1000);
494
- inode.mtime = new Date(mtime * 1000);
495
- inode.ctime = new Date();
496
- }
497
- async realpath(path) {
498
- const normalized = normalizePath(path);
499
- const resolved = this.resolveSymlink(normalized);
500
- if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
501
- throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
502
- }
503
- return resolved;
504
- }
505
- async pread(path, offset, length) {
506
- const normalized = normalizePath(path);
507
- const resolved = this.resolveSymlink(normalized);
508
- const ino = this.files.get(resolved);
509
- if (ino === undefined) {
510
- throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
511
- }
512
- return this.preadByInode(ino, offset, length);
513
- }
514
- async truncate(path, length) {
515
- const normalized = normalizePath(path);
516
- const resolved = this.resolveSymlink(normalized);
517
- const ino = this.files.get(resolved);
518
- if (ino === undefined) {
519
- throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
520
- }
521
- const file = this.readFileByInode(ino);
522
- const next = length >= file.byteLength
523
- ? (() => {
524
- const padded = new Uint8Array(length);
525
- padded.set(file);
526
- return padded;
527
- })()
528
- : file.slice(0, length);
529
- this.fileContents.set(ino, next);
530
- this.updateFileMetadata(ino, next.byteLength);
531
- }
532
- reindexInodes(oldTable) {
533
- const oldContents = new Map(this.fileContents);
534
- const oldFiles = new Map(this.files);
535
- const oldDirs = Array.from(this.dirs.entries()).sort(([a], [b]) => a.length - b.length);
536
- const inoMap = new Map();
537
- this.files = new Map();
538
- this.fileContents = new Map();
539
- this.dirs = new Map();
540
- for (const [dirPath, oldIno] of oldDirs) {
541
- const ino = this.cloneInode(oldIno, oldTable, S_IFDIR | 0o755).ino;
542
- this.dirs.set(dirPath, ino);
543
- }
544
- if (!this.dirs.has("/")) {
545
- this.dirs.set("/", this.allocateDirectoryInode().ino);
546
- }
547
- for (const [path, oldIno] of oldFiles) {
548
- const mapped = inoMap.get(oldIno) ?? (() => {
549
- const inode = this.cloneInode(oldIno, oldTable, S_IFREG | 0o644);
550
- inoMap.set(oldIno, inode.ino);
551
- return inode.ino;
552
- })();
553
- this.files.set(path, mapped);
554
- const content = oldContents.get(oldIno);
555
- if (content) {
556
- this.fileContents.set(mapped, content);
557
- this.requireInode(mapped).size = content.byteLength;
558
- }
559
- }
560
- }
561
- cloneInode(oldIno, oldTable, fallbackMode) {
562
- const source = oldTable.get(oldIno);
563
- const inode = this.inodeTable.allocate(source?.mode ?? fallbackMode, source?.uid ?? 0, source?.gid ?? 0);
564
- inode.nlink = source?.nlink ?? 1;
565
- inode.openRefCount = 0;
566
- inode.size = source?.size ?? 0;
567
- inode.atime = source?.atime ? new Date(source.atime) : new Date();
568
- inode.mtime = source?.mtime ? new Date(source.mtime) : new Date();
569
- inode.ctime = source?.ctime ? new Date(source.ctime) : new Date();
570
- inode.birthtime = source?.birthtime ? new Date(source.birthtime) : new Date();
571
- return inode;
572
- }
573
- allocateFileInode() {
574
- return this.inodeTable.allocate(S_IFREG | 0o644, 0, 0);
575
- }
576
- allocateDirectoryInode() {
577
- const inode = this.inodeTable.allocate(S_IFDIR | 0o755, 0, 0);
578
- inode.size = 4096;
579
- return inode;
580
- }
581
- updateFileMetadata(ino, size) {
582
- const inode = this.requireFileInode(ino);
583
- const now = new Date();
584
- inode.size = size;
585
- inode.atime = now;
586
- inode.mtime = now;
587
- inode.ctime = now;
588
- }
589
- requirePathInode(path, op) {
590
- const normalized = normalizePath(path);
591
- const resolved = this.resolveSymlink(normalized);
592
- const ino = this.files.get(resolved) ?? this.dirs.get(resolved);
593
- if (ino === undefined) {
594
- throw new Error(`ENOENT: no such file or directory, ${op} '${normalized}'`);
595
- }
596
- return this.requireInode(ino);
597
- }
598
- requireFileInode(ino) {
599
- const inode = this.requireInode(ino);
600
- if ((inode.mode & 0o170000) !== S_IFREG && (inode.mode & 0o170000) !== S_IFSOCK) {
601
- throw new Error(`EINVAL: inode ${ino} is not a regular file`);
602
- }
603
- return inode;
604
- }
605
- requireInode(ino) {
606
- const inode = this.inodeTable.get(ino);
607
- if (!inode) {
608
- throw new Error(`ENOENT: inode ${ino} not found`);
609
- }
610
- return inode;
112
+ return created;
611
113
  }
612
- }
613
- export function createInMemoryFileSystem() {
614
- return new InMemoryFileSystem();
114
+ return Object.assign(vfs, { prepareOpenSync });
615
115
  }