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

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 (102) hide show
  1. package/dist/esm-compiler.d.ts +5 -1
  2. package/dist/esm-compiler.js +5 -1
  3. package/dist/fs-helpers.d.ts +1 -1
  4. package/dist/generated/isolate-runtime.d.ts +15 -15
  5. package/dist/generated/isolate-runtime.js +15 -15
  6. package/dist/index.d.ts +25 -6
  7. package/dist/index.js +23 -3
  8. package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
  9. package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +10 -8
  10. package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
  11. package/dist/isolate-runtime/bridge-attach.js +2 -2
  12. package/dist/isolate-runtime/bridge-initial-globals.js +3 -3
  13. package/dist/isolate-runtime/eval-script-result.js +1 -1
  14. package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
  15. package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
  16. package/dist/isolate-runtime/override-process-cwd.js +1 -1
  17. package/dist/isolate-runtime/override-process-env.js +1 -1
  18. package/dist/isolate-runtime/require-setup.js +2236 -19
  19. package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
  20. package/dist/isolate-runtime/set-stdin-data.js +1 -1
  21. package/dist/isolate-runtime/setup-dynamic-import.js +47 -15
  22. package/dist/isolate-runtime/setup-fs-facade.js +2 -2
  23. package/dist/kernel/command-registry.d.ts +44 -0
  24. package/dist/kernel/command-registry.js +114 -0
  25. package/dist/kernel/device-layer.d.ts +12 -0
  26. package/dist/kernel/device-layer.js +262 -0
  27. package/dist/kernel/dns-cache.d.ts +29 -0
  28. package/dist/kernel/dns-cache.js +52 -0
  29. package/dist/kernel/fd-table.d.ts +84 -0
  30. package/dist/kernel/fd-table.js +278 -0
  31. package/dist/kernel/file-lock.d.ts +34 -0
  32. package/dist/kernel/file-lock.js +123 -0
  33. package/dist/kernel/host-adapter.d.ts +50 -0
  34. package/dist/kernel/host-adapter.js +8 -0
  35. package/dist/kernel/index.d.ts +36 -0
  36. package/dist/kernel/index.js +34 -0
  37. package/dist/kernel/inode-table.d.ts +43 -0
  38. package/dist/kernel/inode-table.js +85 -0
  39. package/dist/kernel/kernel.d.ts +9 -0
  40. package/dist/kernel/kernel.js +1396 -0
  41. package/dist/kernel/permissions.d.ts +27 -0
  42. package/dist/kernel/permissions.js +118 -0
  43. package/dist/kernel/pipe-manager.d.ts +64 -0
  44. package/dist/kernel/pipe-manager.js +267 -0
  45. package/dist/kernel/proc-layer.d.ts +11 -0
  46. package/dist/kernel/proc-layer.js +501 -0
  47. package/dist/kernel/process-table.d.ts +124 -0
  48. package/dist/kernel/process-table.js +631 -0
  49. package/dist/kernel/pty.d.ts +108 -0
  50. package/dist/kernel/pty.js +541 -0
  51. package/dist/kernel/socket-table.d.ts +305 -0
  52. package/dist/kernel/socket-table.js +1124 -0
  53. package/dist/kernel/timer-table.d.ts +54 -0
  54. package/dist/kernel/timer-table.js +108 -0
  55. package/dist/kernel/types.d.ts +500 -0
  56. package/dist/kernel/types.js +89 -0
  57. package/dist/kernel/user.d.ts +29 -0
  58. package/dist/kernel/user.js +35 -0
  59. package/dist/kernel/vfs.d.ts +54 -0
  60. package/dist/kernel/vfs.js +8 -0
  61. package/dist/kernel/wait.d.ts +45 -0
  62. package/dist/kernel/wait.js +112 -0
  63. package/dist/kernel/wstatus.d.ts +21 -0
  64. package/dist/kernel/wstatus.js +33 -0
  65. package/dist/module-resolver.d.ts +4 -0
  66. package/dist/module-resolver.js +4 -0
  67. package/dist/package-bundler.d.ts +6 -1
  68. package/dist/runtime-driver.d.ts +3 -1
  69. package/dist/shared/bridge-contract.d.ts +529 -94
  70. package/dist/shared/bridge-contract.js +86 -3
  71. package/dist/shared/console-formatter.js +4 -0
  72. package/dist/shared/global-exposure.js +345 -0
  73. package/dist/shared/in-memory-fs.d.ts +30 -11
  74. package/dist/shared/in-memory-fs.js +383 -109
  75. package/dist/shared/permissions.d.ts +4 -6
  76. package/dist/shared/permissions.js +24 -28
  77. package/dist/types.d.ts +20 -130
  78. package/dist/types.js +5 -0
  79. package/package.json +12 -22
  80. package/dist/bridge/active-handles.d.ts +0 -22
  81. package/dist/bridge/active-handles.js +0 -55
  82. package/dist/bridge/child-process.d.ts +0 -99
  83. package/dist/bridge/child-process.js +0 -656
  84. package/dist/bridge/fs.d.ts +0 -281
  85. package/dist/bridge/fs.js +0 -2231
  86. package/dist/bridge/index.d.ts +0 -10
  87. package/dist/bridge/index.js +0 -41
  88. package/dist/bridge/module.d.ts +0 -75
  89. package/dist/bridge/module.js +0 -299
  90. package/dist/bridge/network.d.ts +0 -250
  91. package/dist/bridge/network.js +0 -1433
  92. package/dist/bridge/os.d.ts +0 -13
  93. package/dist/bridge/os.js +0 -256
  94. package/dist/bridge/polyfills.d.ts +0 -2
  95. package/dist/bridge/polyfills.js +0 -11
  96. package/dist/bridge/process.d.ts +0 -89
  97. package/dist/bridge/process.js +0 -994
  98. package/dist/bridge.js +0 -11766
  99. package/dist/python-runtime.d.ts +0 -16
  100. package/dist/python-runtime.js +0 -45
  101. package/dist/runtime.d.ts +0 -31
  102. package/dist/runtime.js +0 -69
@@ -1,6 +1,9 @@
1
+ import { InodeTable } from "../kernel/inode-table.js";
2
+ import { KernelError, O_CREAT, O_EXCL, O_TRUNC } from "../kernel/types.js";
1
3
  const S_IFREG = 0o100000;
2
4
  const S_IFDIR = 0o040000;
3
5
  const S_IFLNK = 0o120000;
6
+ const S_IFSOCK = 0o140000;
4
7
  function normalizePath(path) {
5
8
  if (!path)
6
9
  return "/";
@@ -22,53 +25,127 @@ function dirname(path) {
22
25
  return `/${parts.slice(0, -1).join("/")}`;
23
26
  }
24
27
  /**
25
- * A fully in-memory VirtualFileSystem backed by Maps.
28
+ * A fully in-memory VirtualFileSystem backed by inode-aware Maps.
26
29
  * Used as the default filesystem for the browser sandbox and for tests.
27
30
  * Paths are always POSIX-style (forward slashes, rooted at "/").
28
31
  */
29
32
  export class InMemoryFileSystem {
33
+ inodeTable;
30
34
  files = new Map();
31
- dirs = new Set(["/"]);
35
+ fileContents = new Map();
36
+ dirs = new Map();
32
37
  symlinks = new Map();
33
- modes = new Map();
34
- owners = new Map();
35
- timestamps = new Map();
36
- hardLinks = new Map(); // newPath → originalPath
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
+ }
37
78
  listDirEntries(path) {
38
79
  const normalized = normalizePath(path);
39
- if (!this.dirs.has(normalized)) {
80
+ const dirIno = this.dirs.get(normalized);
81
+ if (dirIno === undefined) {
40
82
  throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);
41
83
  }
42
84
  const prefix = normalized === "/" ? "/" : `${normalized}/`;
43
85
  const entries = new Map();
44
- for (const filePath of this.files.keys()) {
45
- if (filePath.startsWith(prefix)) {
46
- const rest = filePath.slice(prefix.length);
47
- if (rest && !rest.includes("/")) {
48
- entries.set(rest, false);
49
- }
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
+ });
50
111
  }
51
112
  }
52
- for (const dirPath of this.dirs.values()) {
53
- if (dirPath.startsWith(prefix)) {
54
- const rest = dirPath.slice(prefix.length);
55
- if (rest && !rest.includes("/")) {
56
- entries.set(rest, true);
57
- }
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
+ });
58
124
  }
59
125
  }
60
- return Array.from(entries.entries()).map(([name, isDirectory]) => ({
61
- name,
62
- isDirectory,
63
- }));
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());
64
140
  }
65
141
  async readFile(path) {
66
142
  const normalized = normalizePath(path);
67
- const data = this.files.get(normalized);
68
- if (!data) {
143
+ const resolved = this.resolveSymlink(normalized);
144
+ const ino = this.files.get(resolved);
145
+ if (ino === undefined) {
69
146
  throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
70
147
  }
71
- return data;
148
+ return this.readFileByInode(ino);
72
149
  }
73
150
  async readTextFile(path) {
74
151
  const data = await this.readFile(path);
@@ -84,7 +161,56 @@ export class InMemoryFileSystem {
84
161
  const normalized = normalizePath(path);
85
162
  await this.mkdir(dirname(normalized));
86
163
  const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
87
- this.files.set(normalized, data);
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;
178
+ const hasCreate = (flags & O_CREAT) !== 0;
179
+ const hasExcl = (flags & O_EXCL) !== 0;
180
+ 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);
183
+ if (hasCreate && hasExcl && exists) {
184
+ throw new KernelError("EEXIST", `file already exists, open '${normalized}'`);
185
+ }
186
+ 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);
194
+ }
195
+ }
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;
88
214
  }
89
215
  async createDir(path) {
90
216
  const normalized = normalizePath(path);
@@ -92,59 +218,60 @@ export class InMemoryFileSystem {
92
218
  if (!this.dirs.has(parent)) {
93
219
  throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
94
220
  }
95
- this.dirs.add(normalized);
221
+ if (!this.dirs.has(normalized)) {
222
+ this.dirs.set(normalized, this.allocateDirectoryInode().ino);
223
+ }
96
224
  }
97
- async mkdir(path) {
225
+ async mkdir(path, _options) {
98
226
  const parts = splitPath(path);
99
227
  let current = "";
100
228
  for (const part of parts) {
101
229
  current += `/${part}`;
102
230
  if (!this.dirs.has(current)) {
103
- this.dirs.add(current);
231
+ this.dirs.set(current, this.allocateDirectoryInode().ino);
104
232
  }
105
233
  }
106
234
  }
235
+ resolveIfSymlink(normalized) {
236
+ return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
237
+ }
107
238
  resolveSymlink(normalized, maxDepth = 16) {
108
239
  let current = normalized;
109
240
  for (let i = 0; i < maxDepth; i++) {
110
241
  const target = this.symlinks.get(current);
111
242
  if (!target)
112
243
  return current;
113
- current = target.startsWith("/") ? normalizePath(target) : normalizePath(`${dirname(current)}/${target}`);
244
+ current = target.startsWith("/")
245
+ ? normalizePath(target)
246
+ : normalizePath(`${dirname(current)}/${target}`);
114
247
  }
115
248
  throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
116
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
+ }
117
267
  statEntry(normalized) {
118
- const now = Date.now();
119
- const ts = this.timestamps.get(normalized);
120
- const owner = this.owners.get(normalized);
121
- const customMode = this.modes.get(normalized);
122
- const atimeMs = ts?.atimeMs ?? now;
123
- const mtimeMs = ts?.mtimeMs ?? now;
124
- const file = this.files.get(normalized);
125
- if (file) {
126
- return {
127
- mode: customMode ?? (S_IFREG | 0o644),
128
- size: file.byteLength,
129
- isDirectory: false,
130
- isSymbolicLink: false,
131
- atimeMs,
132
- mtimeMs,
133
- ctimeMs: now,
134
- birthtimeMs: now,
135
- };
268
+ const fileIno = this.files.get(normalized);
269
+ if (fileIno !== undefined) {
270
+ return this.statByInode(fileIno);
136
271
  }
137
- if (this.dirs.has(normalized)) {
138
- return {
139
- mode: customMode ?? (S_IFDIR | 0o755),
140
- size: 4096,
141
- isDirectory: true,
142
- isSymbolicLink: false,
143
- atimeMs,
144
- mtimeMs,
145
- ctimeMs: now,
146
- birthtimeMs: now,
147
- };
272
+ const dirIno = this.dirs.get(normalized);
273
+ if (dirIno !== undefined) {
274
+ return this.statByInode(dirIno);
148
275
  }
149
276
  throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
150
277
  }
@@ -152,8 +279,8 @@ export class InMemoryFileSystem {
152
279
  const normalized = normalizePath(path);
153
280
  if (this.symlinks.has(normalized)) {
154
281
  try {
155
- this.resolveSymlink(normalized);
156
- return true;
282
+ const resolved = this.resolveSymlink(normalized);
283
+ return this.files.has(resolved) || this.dirs.has(resolved);
157
284
  }
158
285
  catch {
159
286
  return false;
@@ -168,9 +295,20 @@ export class InMemoryFileSystem {
168
295
  }
169
296
  async removeFile(path) {
170
297
  const normalized = normalizePath(path);
171
- if (!this.files.delete(normalized)) {
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) {
172
304
  throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
173
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
+ }
174
312
  }
175
313
  async removeDir(path) {
176
314
  const normalized = normalizePath(path);
@@ -186,12 +324,22 @@ export class InMemoryFileSystem {
186
324
  throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
187
325
  }
188
326
  }
189
- for (const dirPath of this.dirs.values()) {
327
+ for (const dirPath of this.dirs.keys()) {
190
328
  if (dirPath !== normalized && dirPath.startsWith(prefix)) {
191
329
  throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
192
330
  }
193
331
  }
332
+ for (const linkPath of this.symlinks.keys()) {
333
+ if (linkPath.startsWith(prefix)) {
334
+ throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
335
+ }
336
+ }
337
+ const ino = this.dirs.get(normalized);
194
338
  this.dirs.delete(normalized);
339
+ this.inodeTable.decrementLinks(ino);
340
+ if (this.inodeTable.shouldDelete(ino)) {
341
+ this.inodeTable.delete(ino);
342
+ }
195
343
  }
196
344
  async rename(oldPath, newPath) {
197
345
  const oldNormalized = normalizePath(oldPath);
@@ -206,9 +354,23 @@ export class InMemoryFileSystem {
206
354
  if (this.dirs.has(newNormalized)) {
207
355
  throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
208
356
  }
209
- const content = this.files.get(oldNormalized);
210
- this.files.set(newNormalized, content);
357
+ if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
358
+ throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
359
+ }
360
+ const ino = this.files.get(oldNormalized);
211
361
  this.files.delete(oldNormalized);
362
+ this.files.set(newNormalized, ino);
363
+ return;
364
+ }
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);
212
374
  return;
213
375
  }
214
376
  if (!this.dirs.has(oldNormalized)) {
@@ -220,34 +382,42 @@ export class InMemoryFileSystem {
220
382
  if (newNormalized.startsWith(`${oldNormalized}/`)) {
221
383
  throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
222
384
  }
223
- if (this.dirs.has(newNormalized) || this.files.has(newNormalized)) {
385
+ if (this.dirs.has(newNormalized) ||
386
+ this.files.has(newNormalized) ||
387
+ this.symlinks.has(newNormalized)) {
224
388
  throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
225
389
  }
226
390
  const sourcePrefix = `${oldNormalized}/`;
227
391
  const targetPrefix = `${newNormalized}/`;
228
- const dirPaths = Array.from(this.dirs.values())
229
- .filter((path) => path === oldNormalized || path.startsWith(sourcePrefix))
230
- .sort((a, b) => a.length - b.length);
231
- const filePaths = Array.from(this.files.keys()).filter((path) => path.startsWith(sourcePrefix));
232
- for (const path of dirPaths) {
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)
233
398
  this.dirs.delete(path);
234
- }
235
- for (const path of filePaths) {
236
- const content = this.files.get(path);
399
+ for (const [path] of fileEntries)
237
400
  this.files.delete(path);
238
- this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, content);
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);
239
408
  }
240
- this.dirs.add(newNormalized);
241
- for (const path of dirPaths) {
242
- if (path === oldNormalized) {
243
- continue;
244
- }
245
- this.dirs.add(`${targetPrefix}${path.slice(sourcePrefix.length)}`);
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);
246
414
  }
247
415
  }
248
416
  async symlink(target, linkPath) {
249
417
  const normalized = normalizePath(linkPath);
250
- if (this.files.has(normalized) || this.dirs.has(normalized) || this.symlinks.has(normalized)) {
418
+ if (this.files.has(normalized) ||
419
+ this.dirs.has(normalized) ||
420
+ this.symlinks.has(normalized)) {
251
421
  throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
252
422
  }
253
423
  await this.mkdir(dirname(normalized));
@@ -275,6 +445,10 @@ export class InMemoryFileSystem {
275
445
  mtimeMs: now,
276
446
  ctimeMs: now,
277
447
  birthtimeMs: now,
448
+ ino: 0,
449
+ nlink: 1,
450
+ uid: 0,
451
+ gid: 0,
278
452
  };
279
453
  }
280
454
  return this.statEntry(normalized);
@@ -282,58 +456,158 @@ export class InMemoryFileSystem {
282
456
  async link(oldPath, newPath) {
283
457
  const oldNormalized = normalizePath(oldPath);
284
458
  const newNormalized = normalizePath(newPath);
285
- const file = this.files.get(oldNormalized);
286
- if (!file) {
459
+ const resolved = this.resolveSymlink(oldNormalized);
460
+ const ino = this.files.get(resolved);
461
+ if (ino === undefined) {
287
462
  throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
288
463
  }
289
- if (this.files.has(newNormalized) || this.dirs.has(newNormalized)) {
464
+ if (this.files.has(newNormalized) ||
465
+ this.dirs.has(newNormalized) ||
466
+ this.symlinks.has(newNormalized)) {
290
467
  throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
291
468
  }
292
469
  await this.mkdir(dirname(newNormalized));
293
- this.files.set(newNormalized, file);
294
- this.hardLinks.set(newNormalized, oldNormalized);
470
+ this.files.set(newNormalized, ino);
471
+ this.inodeTable.incrementLinks(ino);
295
472
  }
296
473
  async chmod(path, mode) {
297
- const normalized = normalizePath(path);
298
- const resolved = this.resolveSymlink(normalized);
299
- if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
300
- throw new Error(`ENOENT: no such file or directory, chmod '${normalized}'`);
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);
301
482
  }
302
- const existing = this.modes.get(resolved);
303
- const typeBits = existing ? (existing & 0o170000) : (this.files.has(resolved) ? S_IFREG : S_IFDIR);
304
- this.modes.set(resolved, typeBits | (mode & 0o7777));
483
+ inode.ctime = new Date();
305
484
  }
306
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) {
307
498
  const normalized = normalizePath(path);
308
499
  const resolved = this.resolveSymlink(normalized);
309
500
  if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
310
- throw new Error(`ENOENT: no such file or directory, chown '${normalized}'`);
501
+ throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
311
502
  }
312
- this.owners.set(resolved, { uid, gid });
503
+ return resolved;
313
504
  }
314
- async utimes(path, atime, mtime) {
505
+ async pread(path, offset, length) {
315
506
  const normalized = normalizePath(path);
316
507
  const resolved = this.resolveSymlink(normalized);
317
- if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
318
- throw new Error(`ENOENT: no such file or directory, utimes '${normalized}'`);
508
+ const ino = this.files.get(resolved);
509
+ if (ino === undefined) {
510
+ throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
319
511
  }
320
- this.timestamps.set(resolved, { atimeMs: atime * 1000, mtimeMs: mtime * 1000 });
512
+ return this.preadByInode(ino, offset, length);
321
513
  }
322
514
  async truncate(path, length) {
323
515
  const normalized = normalizePath(path);
324
516
  const resolved = this.resolveSymlink(normalized);
325
- const file = this.files.get(resolved);
326
- if (!file) {
517
+ const ino = this.files.get(resolved);
518
+ if (ino === undefined) {
327
519
  throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
328
520
  }
329
- if (length >= file.byteLength) {
330
- const padded = new Uint8Array(length);
331
- padded.set(file);
332
- this.files.set(resolved, padded);
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
+ }
333
559
  }
334
- else {
335
- this.files.set(resolved, file.slice(0, length));
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`);
336
609
  }
610
+ return inode;
337
611
  }
338
612
  }
339
613
  export function createInMemoryFileSystem() {
@@ -5,7 +5,9 @@
5
5
  * checks that throw EACCES on denial. When no permission callback is provided
6
6
  * for a category, guarded operations in that category are denied by default.
7
7
  */
8
- import type { CommandExecutor, EnvAccessRequest, NetworkAdapter, Permissions, VirtualFileSystem } from "../types.js";
8
+ import type { EnvAccessRequest, Permissions } from "../kernel/types.js";
9
+ import type { VirtualFileSystem } from "../kernel/vfs.js";
10
+ import type { CommandExecutor, NetworkAdapter } from "../types.js";
9
11
  export declare const allowAllFs: Pick<Permissions, "fs">;
10
12
  export declare const allowAllNetwork: Pick<Permissions, "network">;
11
13
  export declare const allowAllChildProcess: Pick<Permissions, "childProcess">;
@@ -16,11 +18,7 @@ export declare const allowAll: Permissions;
16
18
  * Throws EACCES if the permission callback denies or is absent.
17
19
  */
18
20
  export declare function wrapFileSystem(fs: VirtualFileSystem, permissions?: Permissions): VirtualFileSystem;
19
- /**
20
- * Wrap a NetworkAdapter so externally-originating operations (`listen`, `fetch`,
21
- * `dns`, `http`) pass through the network permission check.
22
- * `httpServerClose` is forwarded as-is.
23
- */
21
+ /** Wrap a NetworkAdapter so external client operations pass through the network permission check. */
24
22
  export declare function wrapNetworkAdapter(adapter: NetworkAdapter, permissions?: Permissions): NetworkAdapter;
25
23
  /** Wrap a CommandExecutor so spawn passes through the childProcess permission check. */
26
24
  export declare function wrapCommandExecutor(executor: CommandExecutor, permissions?: Permissions): CommandExecutor;