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

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 +24 -5
  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 +2 -2
  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 +145 -6
  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 +2868 -494
  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 +78 -19
  22. package/dist/isolate-runtime/setup-fs-facade.js +62 -23
  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 +122 -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 +1393 -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 +312 -0
  52. package/dist/kernel/socket-table.js +1188 -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 +349 -22
  70. package/dist/shared/bridge-contract.js +62 -5
  71. package/dist/shared/console-formatter.js +8 -4
  72. package/dist/shared/global-exposure.js +364 -19
  73. package/dist/shared/in-memory-fs.d.ts +33 -11
  74. package/dist/shared/in-memory-fs.js +439 -130
  75. package/dist/shared/permissions.d.ts +4 -6
  76. package/dist/shared/permissions.js +19 -39
  77. package/dist/types.d.ts +8 -159
  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 -670
  84. package/dist/bridge/fs.d.ts +0 -281
  85. package/dist/bridge/fs.js +0 -2235
  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 -308
  90. package/dist/bridge/network.d.ts +0 -350
  91. package/dist/bridge/network.js +0 -2050
  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 -1015
  98. package/dist/bridge.js +0 -12496
  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
+ });
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
+ });
50
124
  }
51
125
  }
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
- }
126
+ for (const [linkPath, link] of this.symlinks.entries()) {
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: link.ino,
136
+ });
58
137
  }
59
138
  }
60
- return Array.from(entries.entries()).map(([name, isDirectory]) => ({
61
- name,
62
- isDirectory,
63
- }));
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,54 @@ 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
+ this.ensureDirectory(current);
193
+ }
194
+ const inode = this.allocateFileInode();
195
+ this.files.set(resolved, inode.ino);
196
+ this.fileContents.set(inode.ino, new Uint8Array(0));
197
+ this.updateFileMetadata(inode.ino, 0);
198
+ created = true;
199
+ }
200
+ if (hasTrunc) {
201
+ if (this.dirs.has(resolved)) {
202
+ throw new KernelError("EISDIR", `illegal operation on a directory, open '${normalized}'`);
203
+ }
204
+ const truncateIno = this.files.get(resolved);
205
+ if (truncateIno === undefined) {
206
+ throw new KernelError("ENOENT", `no such file or directory, open '${normalized}'`);
207
+ }
208
+ this.fileContents.set(truncateIno, new Uint8Array(0));
209
+ this.updateFileMetadata(truncateIno, 0);
210
+ }
211
+ return created;
88
212
  }
89
213
  async createDir(path) {
90
214
  const normalized = normalizePath(path);
@@ -92,59 +216,57 @@ export class InMemoryFileSystem {
92
216
  if (!this.dirs.has(parent)) {
93
217
  throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
94
218
  }
95
- this.dirs.add(normalized);
219
+ this.ensureDirectory(normalized);
96
220
  }
97
- async mkdir(path) {
221
+ async mkdir(path, _options) {
98
222
  const parts = splitPath(path);
99
223
  let current = "";
100
224
  for (const part of parts) {
101
225
  current += `/${part}`;
102
- if (!this.dirs.has(current)) {
103
- this.dirs.add(current);
104
- }
226
+ this.ensureDirectory(current);
105
227
  }
106
228
  }
229
+ resolveIfSymlink(normalized) {
230
+ return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
231
+ }
107
232
  resolveSymlink(normalized, maxDepth = 16) {
108
233
  let current = normalized;
109
234
  for (let i = 0; i < maxDepth; i++) {
110
- const target = this.symlinks.get(current);
111
- if (!target)
235
+ const link = this.symlinks.get(current);
236
+ if (!link)
112
237
  return current;
113
- current = target.startsWith("/") ? normalizePath(target) : normalizePath(`${dirname(current)}/${target}`);
238
+ current = link.target.startsWith("/")
239
+ ? normalizePath(link.target)
240
+ : normalizePath(`${dirname(current)}/${link.target}`);
114
241
  }
115
242
  throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
116
243
  }
244
+ statForInode(inode) {
245
+ const isDirectory = (inode.mode & 0o170000) === S_IFDIR;
246
+ const isSymbolicLink = (inode.mode & 0o170000) === S_IFLNK;
247
+ return {
248
+ mode: inode.mode,
249
+ size: isDirectory ? 4096 : inode.size,
250
+ isDirectory,
251
+ isSymbolicLink,
252
+ atimeMs: inode.atime.getTime(),
253
+ mtimeMs: inode.mtime.getTime(),
254
+ ctimeMs: inode.ctime.getTime(),
255
+ birthtimeMs: inode.birthtime.getTime(),
256
+ ino: inode.ino,
257
+ nlink: inode.nlink,
258
+ uid: inode.uid,
259
+ gid: inode.gid,
260
+ };
261
+ }
117
262
  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
- };
136
- }
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
- };
263
+ const fileIno = this.files.get(normalized);
264
+ if (fileIno !== undefined) {
265
+ return this.statByInode(fileIno);
266
+ }
267
+ const dirIno = this.dirs.get(normalized);
268
+ if (dirIno !== undefined) {
269
+ return this.statByInode(dirIno);
148
270
  }
149
271
  throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
150
272
  }
@@ -152,8 +274,8 @@ export class InMemoryFileSystem {
152
274
  const normalized = normalizePath(path);
153
275
  if (this.symlinks.has(normalized)) {
154
276
  try {
155
- this.resolveSymlink(normalized);
156
- return true;
277
+ const resolved = this.resolveSymlink(normalized);
278
+ return this.files.has(resolved) || this.dirs.has(resolved);
157
279
  }
158
280
  catch {
159
281
  return false;
@@ -168,9 +290,26 @@ export class InMemoryFileSystem {
168
290
  }
169
291
  async removeFile(path) {
170
292
  const normalized = normalizePath(path);
171
- if (!this.files.delete(normalized)) {
293
+ const symlink = this.symlinks.get(normalized);
294
+ if (symlink) {
295
+ this.symlinks.delete(normalized);
296
+ this.inodeTable.decrementLinks(symlink.ino);
297
+ if (this.inodeTable.shouldDelete(symlink.ino)) {
298
+ this.inodeTable.delete(symlink.ino);
299
+ }
300
+ return;
301
+ }
302
+ const resolved = this.resolveSymlink(normalized);
303
+ const ino = this.files.get(resolved);
304
+ if (ino === undefined) {
172
305
  throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
173
306
  }
307
+ this.files.delete(resolved);
308
+ this.inodeTable.decrementLinks(ino);
309
+ if (this.inodeTable.shouldDelete(ino)) {
310
+ this.deleteInodeData(ino);
311
+ this.inodeTable.delete(ino);
312
+ }
174
313
  }
175
314
  async removeDir(path) {
176
315
  const normalized = normalizePath(path);
@@ -186,12 +325,24 @@ export class InMemoryFileSystem {
186
325
  throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
187
326
  }
188
327
  }
189
- for (const dirPath of this.dirs.values()) {
328
+ for (const dirPath of this.dirs.keys()) {
190
329
  if (dirPath !== normalized && dirPath.startsWith(prefix)) {
191
330
  throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
192
331
  }
193
332
  }
333
+ for (const linkPath of this.symlinks.keys()) {
334
+ if (linkPath.startsWith(prefix)) {
335
+ throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
336
+ }
337
+ }
338
+ const ino = this.dirs.get(normalized);
194
339
  this.dirs.delete(normalized);
340
+ this.inodeTable.decrementLinks(ino);
341
+ this.inodeTable.decrementLinks(ino);
342
+ this.adjustParentDirectoryLinkCount(normalized, -1);
343
+ if (this.inodeTable.shouldDelete(ino)) {
344
+ this.inodeTable.delete(ino);
345
+ }
195
346
  }
196
347
  async rename(oldPath, newPath) {
197
348
  const oldNormalized = normalizePath(oldPath);
@@ -206,9 +357,23 @@ export class InMemoryFileSystem {
206
357
  if (this.dirs.has(newNormalized)) {
207
358
  throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
208
359
  }
209
- const content = this.files.get(oldNormalized);
210
- this.files.set(newNormalized, content);
360
+ if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
361
+ throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
362
+ }
363
+ const ino = this.files.get(oldNormalized);
211
364
  this.files.delete(oldNormalized);
365
+ this.files.set(newNormalized, ino);
366
+ return;
367
+ }
368
+ if (this.symlinks.has(oldNormalized)) {
369
+ if (this.files.has(newNormalized) ||
370
+ this.dirs.has(newNormalized) ||
371
+ this.symlinks.has(newNormalized)) {
372
+ throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
373
+ }
374
+ const target = this.symlinks.get(oldNormalized);
375
+ this.symlinks.delete(oldNormalized);
376
+ this.symlinks.set(newNormalized, target);
212
377
  return;
213
378
  }
214
379
  if (!this.dirs.has(oldNormalized)) {
@@ -220,119 +385,263 @@ export class InMemoryFileSystem {
220
385
  if (newNormalized.startsWith(`${oldNormalized}/`)) {
221
386
  throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
222
387
  }
223
- if (this.dirs.has(newNormalized) || this.files.has(newNormalized)) {
388
+ if (this.dirs.has(newNormalized) ||
389
+ this.files.has(newNormalized) ||
390
+ this.symlinks.has(newNormalized)) {
224
391
  throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
225
392
  }
226
393
  const sourcePrefix = `${oldNormalized}/`;
227
394
  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) {
395
+ const dirEntries = Array.from(this.dirs.entries())
396
+ .filter(([path]) => path === oldNormalized || path.startsWith(sourcePrefix))
397
+ .sort(([a], [b]) => a.length - b.length);
398
+ const fileEntries = Array.from(this.files.entries()).filter(([path]) => path.startsWith(sourcePrefix));
399
+ const symlinkEntries = Array.from(this.symlinks.entries()).filter(([path]) => path.startsWith(sourcePrefix));
400
+ for (const [path] of dirEntries)
233
401
  this.dirs.delete(path);
234
- }
235
- for (const path of filePaths) {
236
- const content = this.files.get(path);
402
+ for (const [path] of fileEntries)
237
403
  this.files.delete(path);
238
- this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, content);
404
+ for (const [path] of symlinkEntries)
405
+ this.symlinks.delete(path);
406
+ for (const [path, ino] of dirEntries) {
407
+ const nextPath = path === oldNormalized
408
+ ? newNormalized
409
+ : `${targetPrefix}${path.slice(sourcePrefix.length)}`;
410
+ this.dirs.set(nextPath, ino);
239
411
  }
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)}`);
412
+ for (const [path, ino] of fileEntries) {
413
+ this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, ino);
414
+ }
415
+ for (const [path, target] of symlinkEntries) {
416
+ this.symlinks.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, target);
417
+ }
418
+ if (dirname(oldNormalized) !== dirname(newNormalized)) {
419
+ this.adjustParentDirectoryLinkCount(oldNormalized, -1);
420
+ this.adjustParentDirectoryLinkCount(newNormalized, 1);
246
421
  }
247
422
  }
248
423
  async symlink(target, linkPath) {
249
424
  const normalized = normalizePath(linkPath);
250
- if (this.files.has(normalized) || this.dirs.has(normalized) || this.symlinks.has(normalized)) {
425
+ if (this.files.has(normalized) ||
426
+ this.dirs.has(normalized) ||
427
+ this.symlinks.has(normalized)) {
251
428
  throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
252
429
  }
253
430
  await this.mkdir(dirname(normalized));
254
- this.symlinks.set(normalized, target);
431
+ const inode = this.allocateSymlinkInode(target);
432
+ this.symlinks.set(normalized, { target, ino: inode.ino });
255
433
  }
256
434
  async readlink(path) {
257
435
  const normalized = normalizePath(path);
258
- const target = this.symlinks.get(normalized);
259
- if (target === undefined) {
436
+ const link = this.symlinks.get(normalized);
437
+ if (link === undefined) {
260
438
  throw new Error(`EINVAL: invalid argument, readlink '${normalized}'`);
261
439
  }
262
- return target;
440
+ return link.target;
263
441
  }
264
442
  async lstat(path) {
265
443
  const normalized = normalizePath(path);
266
- const target = this.symlinks.get(normalized);
267
- if (target !== undefined) {
268
- const now = Date.now();
269
- return {
270
- mode: S_IFLNK | 0o777,
271
- size: new TextEncoder().encode(target).byteLength,
272
- isDirectory: false,
273
- isSymbolicLink: true,
274
- atimeMs: now,
275
- mtimeMs: now,
276
- ctimeMs: now,
277
- birthtimeMs: now,
278
- };
444
+ const link = this.symlinks.get(normalized);
445
+ if (link !== undefined) {
446
+ return this.statForInode(this.requireInode(link.ino));
279
447
  }
280
448
  return this.statEntry(normalized);
281
449
  }
282
450
  async link(oldPath, newPath) {
283
451
  const oldNormalized = normalizePath(oldPath);
284
452
  const newNormalized = normalizePath(newPath);
285
- const file = this.files.get(oldNormalized);
286
- if (!file) {
453
+ const resolved = this.resolveSymlink(oldNormalized);
454
+ const ino = this.files.get(resolved);
455
+ if (ino === undefined) {
287
456
  throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
288
457
  }
289
- if (this.files.has(newNormalized) || this.dirs.has(newNormalized)) {
458
+ if (this.files.has(newNormalized) ||
459
+ this.dirs.has(newNormalized) ||
460
+ this.symlinks.has(newNormalized)) {
290
461
  throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
291
462
  }
292
463
  await this.mkdir(dirname(newNormalized));
293
- this.files.set(newNormalized, file);
294
- this.hardLinks.set(newNormalized, oldNormalized);
464
+ this.files.set(newNormalized, ino);
465
+ this.inodeTable.incrementLinks(ino);
295
466
  }
296
467
  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}'`);
468
+ const inode = this.requirePathInode(path, "chmod");
469
+ const callerTypeBits = mode & 0o170000;
470
+ if (callerTypeBits !== 0) {
471
+ inode.mode = mode;
472
+ }
473
+ else {
474
+ const existingTypeBits = inode.mode & 0o170000;
475
+ inode.mode = existingTypeBits | (mode & 0o7777);
301
476
  }
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));
477
+ inode.ctime = new Date();
305
478
  }
306
479
  async chown(path, uid, gid) {
480
+ const inode = this.requirePathInode(path, "chown");
481
+ inode.uid = uid;
482
+ inode.gid = gid;
483
+ inode.ctime = new Date();
484
+ }
485
+ async utimes(path, atime, mtime) {
486
+ const inode = this.requirePathInode(path, "utimes");
487
+ inode.atime = new Date(atime * 1000);
488
+ inode.mtime = new Date(mtime * 1000);
489
+ inode.ctime = new Date();
490
+ }
491
+ async realpath(path) {
307
492
  const normalized = normalizePath(path);
308
493
  const resolved = this.resolveSymlink(normalized);
309
494
  if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
310
- throw new Error(`ENOENT: no such file or directory, chown '${normalized}'`);
495
+ throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
311
496
  }
312
- this.owners.set(resolved, { uid, gid });
497
+ return resolved;
313
498
  }
314
- async utimes(path, atime, mtime) {
499
+ async pread(path, offset, length) {
315
500
  const normalized = normalizePath(path);
316
501
  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}'`);
502
+ const ino = this.files.get(resolved);
503
+ if (ino === undefined) {
504
+ throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
319
505
  }
320
- this.timestamps.set(resolved, { atimeMs: atime * 1000, mtimeMs: mtime * 1000 });
506
+ return this.preadByInode(ino, offset, length);
321
507
  }
322
508
  async truncate(path, length) {
323
509
  const normalized = normalizePath(path);
324
510
  const resolved = this.resolveSymlink(normalized);
325
- const file = this.files.get(resolved);
326
- if (!file) {
511
+ const ino = this.files.get(resolved);
512
+ if (ino === undefined) {
327
513
  throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
328
514
  }
329
- if (length >= file.byteLength) {
330
- const padded = new Uint8Array(length);
331
- padded.set(file);
332
- this.files.set(resolved, padded);
515
+ const file = this.readFileByInode(ino);
516
+ const next = length >= file.byteLength
517
+ ? (() => {
518
+ const padded = new Uint8Array(length);
519
+ padded.set(file);
520
+ return padded;
521
+ })()
522
+ : file.slice(0, length);
523
+ this.fileContents.set(ino, next);
524
+ this.updateFileMetadata(ino, next.byteLength);
525
+ }
526
+ reindexInodes(oldTable) {
527
+ const oldContents = new Map(this.fileContents);
528
+ const oldFiles = new Map(this.files);
529
+ const oldSymlinks = new Map(this.symlinks);
530
+ const oldDirs = Array.from(this.dirs.entries()).sort(([a], [b]) => a.length - b.length);
531
+ const inoMap = new Map();
532
+ this.files = new Map();
533
+ this.fileContents = new Map();
534
+ this.dirs = new Map();
535
+ this.symlinks = new Map();
536
+ for (const [dirPath, oldIno] of oldDirs) {
537
+ const ino = this.cloneInode(oldIno, oldTable, S_IFDIR | 0o755).ino;
538
+ this.dirs.set(dirPath, ino);
539
+ }
540
+ if (!this.dirs.has("/")) {
541
+ this.dirs.set("/", this.allocateDirectoryInode().ino);
542
+ }
543
+ for (const [path, oldIno] of oldFiles) {
544
+ const mapped = inoMap.get(oldIno) ?? (() => {
545
+ const inode = this.cloneInode(oldIno, oldTable, S_IFREG | 0o644);
546
+ inoMap.set(oldIno, inode.ino);
547
+ return inode.ino;
548
+ })();
549
+ this.files.set(path, mapped);
550
+ const content = oldContents.get(oldIno);
551
+ if (content) {
552
+ this.fileContents.set(mapped, content);
553
+ this.requireInode(mapped).size = content.byteLength;
554
+ }
555
+ }
556
+ for (const [path, link] of oldSymlinks) {
557
+ const mapped = this.cloneInode(link.ino, oldTable, S_IFLNK | 0o777).ino;
558
+ this.symlinks.set(path, { target: link.target, ino: mapped });
559
+ this.requireInode(mapped).size = new TextEncoder().encode(link.target).byteLength;
560
+ }
561
+ }
562
+ cloneInode(oldIno, oldTable, fallbackMode) {
563
+ const source = oldTable.get(oldIno);
564
+ const inode = this.inodeTable.allocate(source?.mode ?? fallbackMode, source?.uid ?? 0, source?.gid ?? 0);
565
+ inode.nlink = source?.nlink ?? 1;
566
+ inode.openRefCount = 0;
567
+ inode.size = source?.size ?? 0;
568
+ inode.atime = source?.atime ? new Date(source.atime) : new Date();
569
+ inode.mtime = source?.mtime ? new Date(source.mtime) : new Date();
570
+ inode.ctime = source?.ctime ? new Date(source.ctime) : new Date();
571
+ inode.birthtime = source?.birthtime ? new Date(source.birthtime) : new Date();
572
+ return inode;
573
+ }
574
+ allocateFileInode() {
575
+ return this.inodeTable.allocate(S_IFREG | 0o644, 0, 0);
576
+ }
577
+ allocateDirectoryInode() {
578
+ const inode = this.inodeTable.allocate(S_IFDIR | 0o755, 0, 0);
579
+ inode.nlink = 2;
580
+ inode.size = 4096;
581
+ return inode;
582
+ }
583
+ allocateSymlinkInode(target) {
584
+ const inode = this.inodeTable.allocate(S_IFLNK | 0o777, 0, 0);
585
+ inode.size = new TextEncoder().encode(target).byteLength;
586
+ return inode;
587
+ }
588
+ updateFileMetadata(ino, size) {
589
+ const inode = this.requireFileInode(ino);
590
+ const now = new Date();
591
+ inode.size = size;
592
+ inode.atime = now;
593
+ inode.mtime = now;
594
+ inode.ctime = now;
595
+ }
596
+ requirePathInode(path, op) {
597
+ const normalized = normalizePath(path);
598
+ const resolved = this.resolveSymlink(normalized);
599
+ const ino = this.files.get(resolved) ?? this.dirs.get(resolved);
600
+ if (ino === undefined) {
601
+ throw new Error(`ENOENT: no such file or directory, ${op} '${normalized}'`);
602
+ }
603
+ return this.requireInode(ino);
604
+ }
605
+ requireFileInode(ino) {
606
+ const inode = this.requireInode(ino);
607
+ if ((inode.mode & 0o170000) !== S_IFREG && (inode.mode & 0o170000) !== S_IFSOCK) {
608
+ throw new Error(`EINVAL: inode ${ino} is not a regular file`);
609
+ }
610
+ return inode;
611
+ }
612
+ requireInode(ino) {
613
+ const inode = this.inodeTable.get(ino);
614
+ if (!inode) {
615
+ throw new Error(`ENOENT: inode ${ino} not found`);
616
+ }
617
+ return inode;
618
+ }
619
+ ensureDirectory(path) {
620
+ const normalized = normalizePath(path);
621
+ if (normalized === "/")
622
+ return;
623
+ if (this.dirs.has(normalized))
624
+ return;
625
+ const parent = dirname(normalized);
626
+ if (!this.dirs.has(parent)) {
627
+ throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
628
+ }
629
+ this.dirs.set(normalized, this.allocateDirectoryInode().ino);
630
+ this.adjustParentDirectoryLinkCount(normalized, 1);
631
+ }
632
+ adjustParentDirectoryLinkCount(path, delta) {
633
+ const normalized = normalizePath(path);
634
+ if (normalized === "/")
635
+ return;
636
+ const parent = dirname(normalized);
637
+ const parentIno = this.dirs.get(parent);
638
+ if (parentIno === undefined)
639
+ return;
640
+ if (delta > 0) {
641
+ this.inodeTable.incrementLinks(parentIno);
333
642
  }
334
643
  else {
335
- this.files.set(resolved, file.slice(0, length));
644
+ this.inodeTable.decrementLinks(parentIno);
336
645
  }
337
646
  }
338
647
  }