@volter/browser-wali 0.1.166 → 0.1.167

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 (215) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +9 -0
  3. package/dist/browser-wali-command.d.ts +7 -0
  4. package/dist/browser-wali-command.d.ts.map +1 -1
  5. package/dist/browser-wali-command.js +70 -18
  6. package/dist/browser-wali-command.js.map +1 -1
  7. package/dist/bulk-memory.d.ts +4 -2
  8. package/dist/bulk-memory.d.ts.map +1 -1
  9. package/dist/bulk-memory.js +32 -9
  10. package/dist/bulk-memory.js.map +1 -1
  11. package/dist/capabilities.d.ts +1 -1
  12. package/dist/capabilities.d.ts.map +1 -1
  13. package/dist/capabilities.js +5 -2
  14. package/dist/capabilities.js.map +1 -1
  15. package/dist/child-processes.d.ts +2 -0
  16. package/dist/child-processes.d.ts.map +1 -1
  17. package/dist/child-processes.js +9 -1
  18. package/dist/child-processes.js.map +1 -1
  19. package/dist/clocks.d.ts +3 -0
  20. package/dist/clocks.d.ts.map +1 -0
  21. package/dist/clocks.js +44 -0
  22. package/dist/clocks.js.map +1 -0
  23. package/dist/coherent-file-locks.d.ts.map +1 -1
  24. package/dist/coherent-file-locks.js +2 -1
  25. package/dist/coherent-file-locks.js.map +1 -1
  26. package/dist/credentials.d.ts +4 -0
  27. package/dist/credentials.d.ts.map +1 -0
  28. package/dist/credentials.js +46 -0
  29. package/dist/credentials.js.map +1 -0
  30. package/dist/descriptors.d.ts +14 -0
  31. package/dist/descriptors.d.ts.map +1 -1
  32. package/dist/descriptors.js +45 -5
  33. package/dist/descriptors.js.map +1 -1
  34. package/dist/environment.d.ts +4 -0
  35. package/dist/environment.d.ts.map +1 -0
  36. package/dist/environment.js +23 -0
  37. package/dist/environment.js.map +1 -0
  38. package/dist/epoll-ready.d.ts +27 -0
  39. package/dist/epoll-ready.d.ts.map +1 -0
  40. package/dist/epoll-ready.js +88 -0
  41. package/dist/epoll-ready.js.map +1 -0
  42. package/dist/events.d.ts +1 -0
  43. package/dist/events.d.ts.map +1 -1
  44. package/dist/events.js +55 -24
  45. package/dist/events.js.map +1 -1
  46. package/dist/filesystem-host-session.d.ts +65 -0
  47. package/dist/filesystem-host-session.d.ts.map +1 -0
  48. package/dist/filesystem-host-session.js +951 -0
  49. package/dist/filesystem-host-session.js.map +1 -0
  50. package/dist/filesystem-host.d.ts +13 -81
  51. package/dist/filesystem-host.d.ts.map +1 -1
  52. package/dist/filesystem-host.js +14 -882
  53. package/dist/filesystem-host.js.map +1 -1
  54. package/dist/filesystem-protocol.d.ts +3 -1
  55. package/dist/filesystem-protocol.d.ts.map +1 -1
  56. package/dist/filesystem-remote.d.ts +8 -3
  57. package/dist/filesystem-remote.d.ts.map +1 -1
  58. package/dist/filesystem-remote.js +42 -20
  59. package/dist/filesystem-remote.js.map +1 -1
  60. package/dist/filesystem-service-client.d.ts +4 -0
  61. package/dist/filesystem-service-client.d.ts.map +1 -0
  62. package/dist/filesystem-service-client.js +16 -0
  63. package/dist/filesystem-service-client.js.map +1 -0
  64. package/dist/filesystem.d.ts +25 -4
  65. package/dist/filesystem.d.ts.map +1 -1
  66. package/dist/filesystem.js +548 -169
  67. package/dist/filesystem.js.map +1 -1
  68. package/dist/fork-memory.d.ts +23 -0
  69. package/dist/fork-memory.d.ts.map +1 -0
  70. package/dist/fork-memory.js +93 -0
  71. package/dist/fork-memory.js.map +1 -0
  72. package/dist/fork-worker.js +32 -11
  73. package/dist/fork-worker.js.map +1 -1
  74. package/dist/jumps.d.ts +7 -0
  75. package/dist/jumps.d.ts.map +1 -1
  76. package/dist/jumps.js +19 -1
  77. package/dist/jumps.js.map +1 -1
  78. package/dist/kernel-filesystem-worker.d.ts +2 -0
  79. package/dist/kernel-filesystem-worker.d.ts.map +1 -0
  80. package/dist/kernel-filesystem-worker.js +28 -0
  81. package/dist/kernel-filesystem-worker.js.map +1 -0
  82. package/dist/kernel-filesystem.d.ts +12 -0
  83. package/dist/kernel-filesystem.d.ts.map +1 -0
  84. package/dist/kernel-filesystem.js +45 -0
  85. package/dist/kernel-filesystem.js.map +1 -0
  86. package/dist/kernel-network-worker.d.ts +2 -0
  87. package/dist/kernel-network-worker.d.ts.map +1 -0
  88. package/dist/kernel-network-worker.js +14 -0
  89. package/dist/kernel-network-worker.js.map +1 -0
  90. package/dist/kernel-network.d.ts +14 -0
  91. package/dist/kernel-network.d.ts.map +1 -0
  92. package/dist/kernel-network.js +50 -0
  93. package/dist/kernel-network.js.map +1 -0
  94. package/dist/kernel-process-client.d.ts +6 -1
  95. package/dist/kernel-process-client.d.ts.map +1 -1
  96. package/dist/kernel-process-client.js +10 -1
  97. package/dist/kernel-process-client.js.map +1 -1
  98. package/dist/module-layout.d.ts +8 -0
  99. package/dist/module-layout.d.ts.map +1 -1
  100. package/dist/module-layout.js +263 -2
  101. package/dist/module-layout.js.map +1 -1
  102. package/dist/network-host.d.ts +6 -0
  103. package/dist/network-host.d.ts.map +1 -0
  104. package/dist/network-host.js +1582 -0
  105. package/dist/network-host.js.map +1 -0
  106. package/dist/network-protocol.d.ts +9 -2
  107. package/dist/network-protocol.d.ts.map +1 -1
  108. package/dist/network-protocol.js +3 -0
  109. package/dist/network-protocol.js.map +1 -1
  110. package/dist/network-service-client.d.ts +4 -0
  111. package/dist/network-service-client.d.ts.map +1 -0
  112. package/dist/network-service-client.js +16 -0
  113. package/dist/network-service-client.js.map +1 -0
  114. package/dist/network-worker.js +5 -1150
  115. package/dist/network-worker.js.map +1 -1
  116. package/dist/network.d.ts +13 -1
  117. package/dist/network.d.ts.map +1 -1
  118. package/dist/network.js +175 -48
  119. package/dist/network.js.map +1 -1
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +97 -0
  123. package/dist/paths.js.map +1 -0
  124. package/dist/process-bridge.d.ts +12 -4
  125. package/dist/process-bridge.d.ts.map +1 -1
  126. package/dist/process-bridge.js +53 -17
  127. package/dist/process-bridge.js.map +1 -1
  128. package/dist/program.d.ts.map +1 -1
  129. package/dist/program.js +8 -0
  130. package/dist/program.js.map +1 -1
  131. package/dist/runtime.d.ts.map +1 -1
  132. package/dist/runtime.js +150 -92
  133. package/dist/runtime.js.map +1 -1
  134. package/dist/shared-buffer-pool.d.ts +18 -5
  135. package/dist/shared-buffer-pool.d.ts.map +1 -1
  136. package/dist/shared-buffer-pool.js +86 -14
  137. package/dist/shared-buffer-pool.js.map +1 -1
  138. package/dist/shared-file-layer.d.ts +3 -1
  139. package/dist/shared-file-layer.d.ts.map +1 -1
  140. package/dist/shared-file-layer.js +150 -35
  141. package/dist/shared-file-layer.js.map +1 -1
  142. package/dist/shared-pipes.d.ts +3 -1
  143. package/dist/shared-pipes.d.ts.map +1 -1
  144. package/dist/shared-pipes.js +105 -33
  145. package/dist/shared-pipes.js.map +1 -1
  146. package/dist/signal-interruption.d.ts +9 -0
  147. package/dist/signal-interruption.d.ts.map +1 -0
  148. package/dist/signal-interruption.js +6 -0
  149. package/dist/signal-interruption.js.map +1 -0
  150. package/dist/signals.d.ts +17 -6
  151. package/dist/signals.d.ts.map +1 -1
  152. package/dist/signals.js +101 -22
  153. package/dist/signals.js.map +1 -1
  154. package/dist/snapshot-filesystem.d.ts +1 -0
  155. package/dist/snapshot-filesystem.d.ts.map +1 -1
  156. package/dist/snapshot-filesystem.js +3 -0
  157. package/dist/snapshot-filesystem.js.map +1 -1
  158. package/dist/socket-flags.d.ts +3 -0
  159. package/dist/socket-flags.d.ts.map +1 -0
  160. package/dist/socket-flags.js +9 -0
  161. package/dist/socket-flags.js.map +1 -0
  162. package/dist/special-files.d.ts +6 -0
  163. package/dist/special-files.d.ts.map +1 -0
  164. package/dist/special-files.js +56 -0
  165. package/dist/special-files.js.map +1 -0
  166. package/dist/stdin.d.ts.map +1 -1
  167. package/dist/stdin.js +13 -5
  168. package/dist/stdin.js.map +1 -1
  169. package/dist/symlink.d.ts +2 -1
  170. package/dist/symlink.d.ts.map +1 -1
  171. package/dist/symlink.js +25 -93
  172. package/dist/symlink.js.map +1 -1
  173. package/dist/system.d.ts +2 -4
  174. package/dist/system.d.ts.map +1 -1
  175. package/dist/system.js +46 -29
  176. package/dist/system.js.map +1 -1
  177. package/dist/thread-coordinator-worker.js +58 -24
  178. package/dist/thread-coordinator-worker.js.map +1 -1
  179. package/dist/thread-lifecycle.d.ts +9 -0
  180. package/dist/thread-lifecycle.d.ts.map +1 -0
  181. package/dist/thread-lifecycle.js +30 -0
  182. package/dist/thread-lifecycle.js.map +1 -0
  183. package/dist/thread-worker.js +125 -44
  184. package/dist/thread-worker.js.map +1 -1
  185. package/dist/threads.d.ts +6 -0
  186. package/dist/threads.d.ts.map +1 -1
  187. package/dist/threads.js +46 -6
  188. package/dist/threads.js.map +1 -1
  189. package/dist/tty-job-control.d.ts +13 -0
  190. package/dist/tty-job-control.d.ts.map +1 -0
  191. package/dist/tty-job-control.js +56 -0
  192. package/dist/tty-job-control.js.map +1 -0
  193. package/dist/tty-settings.d.ts +3 -0
  194. package/dist/tty-settings.d.ts.map +1 -0
  195. package/dist/tty-settings.js +25 -0
  196. package/dist/tty-settings.js.map +1 -0
  197. package/dist/types.d.ts +12 -0
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/wait-state.d.ts +8 -0
  200. package/dist/wait-state.d.ts.map +1 -1
  201. package/dist/wait-state.js +15 -0
  202. package/dist/wait-state.js.map +1 -1
  203. package/dist/worker-program-protocol.d.ts +22 -2
  204. package/dist/worker-program-protocol.d.ts.map +1 -1
  205. package/dist/worker-program-worker.js +31 -9
  206. package/dist/worker-program-worker.js.map +1 -1
  207. package/dist/worker-program.d.ts +12 -2
  208. package/dist/worker-program.d.ts.map +1 -1
  209. package/dist/worker-program.js +214 -36
  210. package/dist/worker-program.js.map +1 -1
  211. package/dist/write-vector.d.ts +3 -0
  212. package/dist/write-vector.d.ts.map +1 -0
  213. package/dist/write-vector.js +35 -0
  214. package/dist/write-vector.js.map +1 -0
  215. package/package.json +1 -1
@@ -1,8 +1,16 @@
1
+ // File-call rules translated from The Emscripten Authors' WasmFS syscalls.cpp
2
+ // (Copyright 2021, MIT), 862dc0d48acc4134a0088d27ac8f5dd18baba1cd; see ../LICENSE.
3
+ // Storage, shared descriptors and process state remain owned by this kernel.
4
+ import { createWaliDevice, waliDeviceStat } from "./special-files.js";
1
5
  import { realtimeMilliseconds } from "./monotonic-clock.js";
6
+ import { gatherWaliWriteVector } from "./write-vector.js";
7
+ import { setBrowserFileTimes } from "@volter/browser-runtime/browser-file-times.js";
2
8
  import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
9
+ import { WaliSharedPipeHost } from "./shared-pipes.js";
3
10
  import { createProcessFilesystemMutationGuard } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
4
11
  import { BufferedWaliStdin } from "./stdin.js";
5
12
  import { readWaliSymlink, resolveWaliSymlinkPath, writeWaliSymlink } from "./symlink.js";
13
+ import { absoluteWaliPath, normalizeWaliPath } from "./paths.js";
6
14
  import { PR_WALI, PR_WALI_TREE_ADOPT, PR_WALI_TREE_CONFIRM, PR_WALI_TREE_DECLINE, PR_WALI_TREE_RELEASE, PRIVATE_TREE_INODES, PRIVATE_TREE_WINDOW_BASE, PRIVATE_TREE_WINDOW_SIZE, decodePrivateTree, encodePrivateTree, inPrivateTreeWindow, } from "./private-tree.js";
7
15
  let reportedDroppedEnvironment = false;
8
16
  const AT_FDCWD = -100;
@@ -15,6 +23,10 @@ const O_TRUNC = 0x200;
15
23
  const O_APPEND = 0x400;
16
24
  const O_NONBLOCK = 0x800;
17
25
  const O_DIRECTORY = 0x1_0000;
26
+ const O_NOFOLLOW = 0x2_0000;
27
+ const AT_EMPTY_PATH = 0x1000;
28
+ const AT_NO_AUTOMOUNT = 0x800;
29
+ const AT_EACCESS = 0x200;
18
30
  const SEEK_SET = 0;
19
31
  const SEEK_CUR = 1;
20
32
  const SEEK_END = 2;
@@ -53,9 +65,9 @@ export class WaliFileDescriptorTable {
53
65
  files;
54
66
  specialDescriptors;
55
67
  stdioAliases;
56
- descriptorStatusFlags = new Map();
57
68
  stdin;
58
69
  nextDescription = 1;
70
+ fileCreationMask;
59
71
  cwd;
60
72
  guestFilesystem;
61
73
  assertWritable;
@@ -64,6 +76,7 @@ export class WaliFileDescriptorTable {
64
76
  privateTreeDeclined = false;
65
77
  constructor(options) {
66
78
  this.options = options;
79
+ this.fileCreationMask = (options.fileCreationMask ?? 0o022) & 0o777;
67
80
  this.descriptors = options.descriptors ?? new WaliProcessDescriptorTable(new WaliDescriptionRegistry());
68
81
  this.files = this.descriptors.view("file");
69
82
  this.specialDescriptors = this.descriptors.view("special", (value) => value.close?.());
@@ -101,7 +114,7 @@ export class WaliFileDescriptorTable {
101
114
  ...(fs.restoreNodeSync ? { restoreNodeSync: (path, node) => fs.restoreNodeSync(this.assertWritable(path), node) } : {}),
102
115
  } : fs;
103
116
  this.stdin = "read" in options.stdin ? options.stdin : new BufferedWaliStdin(options.stdin);
104
- if (!options.descriptors) {
117
+ if (!options.descriptors || options.initializeStdio) {
105
118
  for (const fd of [0, 1, 2])
106
119
  this.specialDescriptors.set(fd, { stdio: fd });
107
120
  }
@@ -117,6 +130,7 @@ export class WaliFileDescriptorTable {
117
130
  SYS_open: (path, flags, mode) => this.open(this.readCString(Number(path)), Number(flags), Number(mode)),
118
131
  SYS_openat: (dirfd, path, flags, mode) => this.openAt(Number(dirfd), this.readCString(Number(path)), Number(flags), Number(mode)),
119
132
  SYS_close: (fd) => this.close(Number(fd)),
133
+ SYS_ioctl: (fd, request, argument) => this.ioctl(Number(fd), Number(request), Number(argument)),
120
134
  SYS_read: (fd, pointer, length) => this.read(Number(fd), Number(pointer), Number(length)),
121
135
  SYS_pread64: (fd, pointer, length, offset) => this.pread(Number(fd), Number(pointer), Number(length), Number(offset)),
122
136
  SYS_write: (fd, pointer, length) => this.write(Number(fd), Number(pointer), Number(length)),
@@ -128,13 +142,19 @@ export class WaliFileDescriptorTable {
128
142
  SYS_pwritev: (fd, iov, count, offset, high) => this.pwritev(Number(fd), Number(iov), Number(count), vectorOffset(offset, high)),
129
143
  SYS_lseek: (fd, offset, whence) => this.lseek(Number(fd), Number(offset), Number(whence)),
130
144
  SYS_ftruncate: (fd, length) => this.ftruncate(Number(fd), Number(length)),
145
+ SYS_truncate: (path, length) => this.truncate(this.readCString(Number(path)), Number(length)),
131
146
  SYS_fstat: (fd, stat) => this.fstat(Number(fd), Number(stat)),
132
147
  SYS_stat: (path, stat) => this.stat(this.readCString(Number(path)), Number(stat), false),
133
148
  SYS_lstat: (path, stat) => this.stat(this.readCString(Number(path)), Number(stat), true),
134
149
  SYS_newfstatat: (dirfd, path, stat, flags) => this.statAt(Number(dirfd), this.readCString(Number(path)), Number(stat), Number(flags)),
135
- SYS_access: (path) => this.access(this.readCString(Number(path))),
136
- SYS_faccessat: (dirfd, path) => this.accessAt(Number(dirfd), this.readCString(Number(path))),
137
- SYS_faccessat2: (dirfd, path) => this.accessAt(Number(dirfd), this.readCString(Number(path))),
150
+ SYS_access: (path, mode) => this.accessAt(AT_FDCWD, this.readCString(Number(path)), Number(mode), 0),
151
+ SYS_faccessat: (dirfd, path, mode) => this.accessAt(Number(dirfd), this.readCString(Number(path)), Number(mode), 0),
152
+ SYS_faccessat2: (dirfd, path, mode, flags) => this.accessAt(Number(dirfd), this.readCString(Number(path)), Number(mode), Number(flags)),
153
+ SYS_umask: (mask) => {
154
+ const previous = this.fileCreationMask;
155
+ this.fileCreationMask = Number(mask) & 0o777;
156
+ return BigInt(previous);
157
+ },
138
158
  SYS_chmod: (path, mode) => this.chmod(this.readCString(Number(path)), Number(mode)),
139
159
  SYS_fchmod: (fd, mode) => this.fchmod(Number(fd), Number(mode)),
140
160
  SYS_fchmodat: (dirfd, path, mode, flags) => this.chmodAt(Number(dirfd), this.readCString(Number(path)), Number(mode), Number(flags)),
@@ -149,10 +169,11 @@ export class WaliFileDescriptorTable {
149
169
  SYS_inotify_init1: () => this.inotifyInit(),
150
170
  SYS_inotify_add_watch: (fd, path) => this.inotifyAddWatch(Number(fd), this.readCString(Number(path))),
151
171
  SYS_inotify_rm_watch: (fd) => this.specialDescriptors.has(Number(fd)) ? 0n : -9n,
152
- SYS_rmdir: (path) => this.mutate(() => this.fs().rmdirSync(this.resolveFilesystemPath(this.resolve(this.readCString(Number(path))), false))),
153
- SYS_unlink: (path) => this.mutate(() => this.fs().unlinkSync(this.resolveFilesystemPath(this.resolve(this.readCString(Number(path))), false))),
172
+ SYS_rmdir: (path) => this.unlinkAt(AT_FDCWD, this.readCString(Number(path)), AT_REMOVEDIR),
173
+ SYS_unlink: (path) => this.unlinkAt(AT_FDCWD, this.readCString(Number(path)), 0),
154
174
  SYS_unlinkat: (dirfd, path, flags) => this.unlinkAt(Number(dirfd), this.readCString(Number(path)), Number(flags)),
155
175
  SYS_rename: (from, to) => this.rename(this.readCString(Number(from)), this.readCString(Number(to))),
176
+ SYS_renameat: (fromDirfd, from, toDirfd, to) => this.rename(this.readCString(Number(from)), this.readCString(Number(to)), Number(fromDirfd), Number(toDirfd)),
156
177
  SYS_utimensat: (dirfd, path, times, flags) => this.utimensat(Number(dirfd), Number(path), Number(times), Number(flags)),
157
178
  SYS_symlink: (target, link) => this.symlink(this.readCString(Number(target)), this.readCString(Number(link)), AT_FDCWD),
158
179
  SYS_symlinkat: (target, dirfd, link) => this.symlink(this.readCString(Number(target)), this.readCString(Number(link)), Number(dirfd)),
@@ -190,15 +211,17 @@ export class WaliFileDescriptorTable {
190
211
  this.options.filesystem.mkdirSync("/tmp", { recursive: true });
191
212
  }
192
213
  catch { /* already present */ }
193
- // The env file is one `name=value` per line, so it cannot carry a value
194
- // with a line break or a name that is not an identifier. A shell's own
214
+ // Legacy artifacts read one `name=value` per line. The owned linker now
215
+ // supplies direct environment imports; rebuilt programs never use this
216
+ // compatibility path. Old artifacts still cannot carry a line break.
217
+ // A shell's own
195
218
  // parameters (`?`, a newline-valued IFS) reach here through every lane's
196
219
  // spawn; refusing the whole spawn for them is what killed a script's
197
220
  // `spawn("ffmpeg")`. They are dropped and named once.
198
221
  const dropped = [];
199
222
  const lines = [];
200
223
  for (const [name, value] of Object.entries(env)) {
201
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || /[\u0000\r\n]/u.test(value)) {
224
+ if (/[=\u0000\r\n]/u.test(name) || /[\u0000\r\n]/u.test(value)) {
202
225
  dropped.push(name);
203
226
  continue;
204
227
  }
@@ -270,7 +293,7 @@ export class WaliFileDescriptorTable {
270
293
  return this.specialDescriptors.get(fd)?.browserProcessPipe;
271
294
  }
272
295
  copyForFork(memory) {
273
- return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors: this.descriptors.copy() });
296
+ return new WaliFileDescriptorTable({ ...this.options, initializeStdio: false, fileCreationMask: this.fileCreationMask, cwd: this.cwd, memory, descriptors: this.descriptors.copy() });
274
297
  }
275
298
  copyForExec(memory, stdio) {
276
299
  const descriptors = this.descriptors.copy();
@@ -282,17 +305,45 @@ export class WaliFileDescriptorTable {
282
305
  });
283
306
  }
284
307
  descriptors.exec();
285
- return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors });
308
+ return new WaliFileDescriptorTable({ ...this.options, initializeStdio: false, fileCreationMask: this.fileCreationMask, cwd: this.cwd, memory, descriptors });
286
309
  }
287
310
  currentWorkingDirectory() { return this.cwd; }
288
- ioctl(fd, request) {
311
+ creationMask() { return this.fileCreationMask; }
312
+ // Translated from Emscripten libsyscall.js and wasmfs/syscalls.cpp (MIT),
313
+ // 862dc0d48acc4134a0088d27ac8f5dd18baba1cd. The kernel owns the description;
314
+ // terminal settings are dispatched separately to the attached tty.
315
+ ioctl(fd, request, argument) {
289
316
  if (!this.hasFd(fd))
290
- return;
291
- // FIONBIO/FIOCLEX/FIONCLEX only mutate descriptor flags. Browser pipes
292
- // are already nonblocking and close-on-exec is handled by the spawn ABI.
293
- if (request === 0x5421 || request === 0x5451 || request === 0x5450)
317
+ return -9n;
318
+ const operation = request >>> 0;
319
+ if (operation === 0x5451 || operation === 0x5450) {
320
+ return this.fcntl(fd, 2, operation === 0x5451 ? 1 : 0);
321
+ }
322
+ if (operation !== 0x5421 && operation !== 0x541b)
323
+ return -25n;
324
+ const file = this.files.get(fd);
325
+ if (operation === 0x541b && (!file || file.directory))
326
+ return -25n;
327
+ const memory = this.options.memory();
328
+ if (!Number.isSafeInteger(argument) || argument <= 0 || argument + 4 > memory.buffer.byteLength)
329
+ return -14n;
330
+ if (operation === 0x5421) {
331
+ const nonblocking = this.memoryView().getInt32(argument, true) !== 0;
332
+ const flags = this.fcntl(fd, 3, 0);
333
+ if (flags < 0n)
334
+ return flags;
335
+ return this.fcntl(fd, 4, nonblocking ? Number(flags) | O_NONBLOCK : Number(flags) & ~O_NONBLOCK);
336
+ }
337
+ if (!file)
338
+ return -25n;
339
+ try {
340
+ const remaining = Math.max(0, this.fs().statSync(file.path).size - file.position);
341
+ this.memoryView().setInt32(argument, remaining, true);
294
342
  return 0n;
295
- return;
343
+ }
344
+ catch (error) {
345
+ return negativeErrno(error);
346
+ }
296
347
  }
297
348
  pollDescriptor(fd, events) {
298
349
  if (fd < 0 || !this.hasFd(fd))
@@ -302,9 +353,12 @@ export class WaliFileDescriptorTable {
302
353
  return special.poll?.(events) ?? 0;
303
354
  const stdio = this.stdioFd(fd);
304
355
  let returned = 0;
305
- if ((events & 0x1) && stdio === 0 && this.stdin.poll())
356
+ const terminal = this.terminalFd(fd);
357
+ const input = terminal ? this.options.terminalStdin : this.stdin;
358
+ const mode = this.statusFlags(fd) & O_ACCMODE;
359
+ if ((events & 0x1) && stdio === 0 && (!terminal || mode !== O_WRONLY) && input?.poll())
306
360
  returned |= 0x1;
307
- if ((events & 0x4) && stdio !== 0)
361
+ if ((events & 0x4) && (stdio !== 0 || (terminal && mode !== 0)))
308
362
  returned |= 0x4;
309
363
  return returned;
310
364
  }
@@ -359,20 +413,22 @@ export class WaliFileDescriptorTable {
359
413
  }
360
414
  return text;
361
415
  }
362
- resolve(path) { return normalizePath(this.cwd, path); }
416
+ resolve(path) { return absoluteWaliPath(this.cwd, path); }
363
417
  resolveAt(dirfd, path) {
364
418
  if (path.startsWith("/"))
365
- return normalizePath("/", path);
419
+ return path;
366
420
  if (dirfd === AT_FDCWD)
367
421
  return this.resolve(path);
368
422
  const directory = this.files.get(dirfd);
369
- if (!directory?.directory)
423
+ if (!directory)
424
+ throw errno(this.hasFd(dirfd) ? "ENOTDIR" : "EBADF");
425
+ if (!directory.directory)
370
426
  throw errno("ENOTDIR");
371
- return normalizePath(directory.path, path);
427
+ return absoluteWaliPath(directory.path, path);
372
428
  }
373
429
  open(path, flags, mode) {
374
430
  try {
375
- return BigInt(this.openResolved(this.resolveFilesystemPath(this.resolve(path)), flags, mode));
431
+ return BigInt(this.openPath(this.resolve(path), flags, mode));
376
432
  }
377
433
  catch (error) {
378
434
  return negativeErrno(error);
@@ -380,45 +436,65 @@ export class WaliFileDescriptorTable {
380
436
  }
381
437
  openAt(dirfd, path, flags, mode) {
382
438
  try {
383
- return BigInt(this.openResolved(this.resolveFilesystemPath(this.resolveAt(dirfd, path)), flags, mode));
439
+ return BigInt(this.openPath(this.resolveAt(dirfd, path), flags, mode));
384
440
  }
385
441
  catch (error) {
386
442
  return negativeErrno(error);
387
443
  }
388
444
  }
445
+ openPath(path, flags, mode) {
446
+ if ((flags & O_ACCMODE) === O_ACCMODE)
447
+ throw errno("EINVAL");
448
+ // Linux >=6.4 rejects this pair instead of creating a regular file and
449
+ // then failing O_DIRECTORY (the old behaviour still in pinned WasmFS).
450
+ if ((flags & O_CREAT) && (flags & O_DIRECTORY))
451
+ throw errno("EINVAL");
452
+ const exclusive = Boolean((flags & O_CREAT) && (flags & O_EXCL));
453
+ const resolved = this.resolveFilesystemPath(path, !(exclusive || (flags & O_NOFOLLOW)));
454
+ const target = exclusive || (flags & O_NOFOLLOW) ? this.virtualLink(resolved) ?? this.readSymlink(resolved) : undefined;
455
+ const entry = exclusive || (flags & O_NOFOLLOW) ? statIfPresent(this.fs(), resolved, true) : undefined;
456
+ const link = target !== undefined || entry?.isSymbolicLink?.();
457
+ // Linux O_EXCL checks the link itself, even when its target is absent.
458
+ if (exclusive && (link || entry !== undefined
459
+ || ["/dev/null", "/dev/random", "/dev/urandom"].includes(resolved)))
460
+ throw errno("EEXIST");
461
+ if ((flags & O_NOFOLLOW) && link)
462
+ throw errno("ELOOP");
463
+ checkFileName(resolved);
464
+ const descriptor = this.procDescriptor(resolved);
465
+ if (descriptor !== undefined)
466
+ return this.reopenDescriptor(descriptor, flags, mode);
467
+ return this.openResolved(resolved, flags, mode);
468
+ }
389
469
  openResolved(path, flags, mode) {
390
470
  // O_CLOEXEC is recorded here for every kind of descriptor open makes: musl
391
471
  // no longer follows it with F_SETFD (musl 0003), which was the only thing
392
472
  // marking these special files (review of dfd87b6).
393
- if (path === "/dev/null") {
394
- return this.markCloseOnExecNumber(this.allocateSpecial({
395
- read: () => new Uint8Array(),
396
- write: (value) => BigInt(value.byteLength),
397
- poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
398
- fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
399
- }), flags);
400
- }
401
- if (path === "/dev/urandom" || path === "/dev/random") {
402
- return this.markCloseOnExecNumber(this.allocateSpecial({
403
- read: (length) => {
404
- if (!Number.isSafeInteger(length) || length < 0)
405
- return -22n;
406
- const value = new Uint8Array(length);
407
- for (let offset = 0; offset < value.byteLength; offset += 65_536) {
408
- crypto.getRandomValues(value.subarray(offset, Math.min(offset + 65_536, value.byteLength)));
409
- }
410
- return value;
411
- },
412
- write: (value) => BigInt(value.byteLength),
413
- poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
414
- fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
415
- }), flags);
473
+ if (path === "/dev/tty") {
474
+ if (flags & O_DIRECTORY)
475
+ throw errno("ENOTDIR");
476
+ const fd = this.allocateFd();
477
+ this.specialDescriptors.set(fd, { stdio: 0, terminal: true, stat: () => waliDeviceStat(path) });
478
+ Atomics.store(this.descriptors.registry.pipeMetadata, this.descriptors.entries[fd].description * 3 + 1, flags & ~O_CLOEXEC_FLAG);
479
+ // The calling realm validates controlling-session membership through
480
+ // ttyopen before returning this fd. The filesystem owner owns the fd;
481
+ // terminal input/output stay on the caller's attached transport.
482
+ return this.markCloseOnExecNumber(fd, flags);
483
+ }
484
+ const device = createWaliDevice(path, flags);
485
+ if (device) {
486
+ if (flags & O_DIRECTORY)
487
+ throw errno("ENOTDIR");
488
+ const fd = this.allocateFd();
489
+ this.specialDescriptors.set(fd, device);
490
+ return this.markCloseOnExecNumber(fd, flags);
416
491
  }
492
+ const fd = this.allocateFd();
417
493
  const fs = this.fs();
418
494
  // One lookup where the store answers ENOENT from statSync (a snapshot store's); exists-then-stat resolved it twice.
419
- let found = fs.linkTargetSync ? statIfPresent(fs, path) : fs.existsSync(path) ? fs.statSync(path) : undefined;
495
+ let found = this.virtualStat(path, false) ?? (fs.linkTargetSync ? statIfPresent(fs, path) : fs.existsSync(path) ? fs.statSync(path) : undefined);
420
496
  const exists = found !== undefined;
421
- if ((flags & O_ACCMODE) !== 0 || (!exists && (flags & O_CREAT)))
497
+ if ((flags & O_ACCMODE) !== 0 || (flags & O_TRUNC) || (!exists && (flags & O_CREAT)))
422
498
  this.assertWritable(path);
423
499
  if (!exists && !(flags & O_CREAT))
424
500
  throw errno("ENOENT");
@@ -426,30 +502,40 @@ export class WaliFileDescriptorTable {
426
502
  throw errno("EEXIST");
427
503
  if (!exists) {
428
504
  fs.writeFileSync(path, new Uint8Array());
429
- fs.chmodSync?.(path, mode & 0o7777);
505
+ fs.chmodSync?.(path, mode & 0o7777 & ~this.fileCreationMask);
430
506
  this.options.fileLocks?.created?.(path);
431
507
  found = fs.statSync(path);
432
508
  }
433
509
  const stat = found;
434
510
  if ((flags & O_DIRECTORY) && !stat.isDirectory())
435
511
  throw errno("ENOTDIR");
436
- if (stat.isDirectory() && (flags & O_ACCMODE) !== 0)
512
+ if (stat.isDirectory() && ((flags & O_ACCMODE) !== 0 || (flags & O_CREAT) || (flags & O_TRUNC)))
437
513
  throw errno("EISDIR");
438
- const truncated = (flags & O_TRUNC) !== 0 && (flags & O_ACCMODE) !== 0 && stat.isFile();
514
+ // doOpen does not recheck the mode of the file it just created: creating
515
+ // mode 0400 with O_RDWR still returns a writable open description.
516
+ if (exists) {
517
+ if ((flags & O_ACCMODE) !== O_WRONLY)
518
+ checkFilePermission(stat, 4);
519
+ if ((flags & O_ACCMODE) !== 0 || (flags & O_TRUNC))
520
+ checkFilePermission(stat, 2);
521
+ }
522
+ const truncated = (flags & O_TRUNC) !== 0 && stat.isFile();
439
523
  if (truncated) {
440
524
  if (fs.truncateSync)
441
525
  fs.truncateSync(path, 0);
442
526
  else
443
527
  fs.writeFileSync(path, new Uint8Array());
444
528
  }
445
- const fd = this.allocateFd();
446
529
  const directory = stat.isDirectory();
530
+ const virtualDirectory = ["/proc", "/proc/self", "/proc/self/fd"].includes(path);
531
+ const entries = directory && !virtualDirectory ? this.directoryEntries(path) : undefined;
447
532
  this.files.set(fd, {
448
533
  path,
449
534
  flags,
450
- // The stat above is the file's size unless this open emptied it.
451
- position: flags & O_APPEND && !truncated && stat.isFile() ? stat.size : 0,
535
+ // O_APPEND chooses EOF on each write; open itself starts at zero.
536
+ position: 0,
452
537
  directory,
538
+ ...(entries ? { entries } : {}),
453
539
  description: this.nextDescription++,
454
540
  });
455
541
  this.descriptors.entries[fd].flags = flags & 0x80000 ? 1 : 0;
@@ -471,8 +557,8 @@ export class WaliFileDescriptorTable {
471
557
  *
472
558
  * Scanning costs one probe per open descriptor, and a process has a handful.
473
559
  */
474
- allocateFd(minimum = 3) {
475
- const maximum = this.options.descriptorMaximum ?? 65_535;
560
+ allocateFd(minimum = 0) {
561
+ const maximum = Math.min(this.options.descriptorMaximum ?? 65_535, this.descriptors.descriptorLimit - 1);
476
562
  for (let fd = Math.max(minimum, 0); fd <= maximum; fd += 1) {
477
563
  // A private tree's numbers are its libc's, held or given back (ADR-0047).
478
564
  if (inPrivateTreeWindow(fd)) {
@@ -489,7 +575,6 @@ export class WaliFileDescriptorTable {
489
575
  }
490
576
  close(fd) {
491
577
  if (this.stdioAliases.delete(fd)) {
492
- this.descriptorStatusFlags.delete(fd);
493
578
  this.setSharedStdioAlias(fd, undefined);
494
579
  return 0n;
495
580
  }
@@ -526,9 +611,14 @@ export class WaliFileDescriptorTable {
526
611
  if (length < 0)
527
612
  throw errno("EINVAL");
528
613
  if (this.stdioFd(fd) === 0) {
529
- if ((this.statusFlags(fd) & O_NONBLOCK) && !this.stdin.poll())
614
+ if (this.terminalFd(fd) && (this.statusFlags(fd) & O_ACCMODE) === O_WRONLY)
615
+ return -9n;
616
+ const input = this.terminalFd(fd) ? this.options.terminalStdin : this.stdin;
617
+ if (!input)
618
+ return -95n; // No independent terminal transport was supplied.
619
+ if ((this.statusFlags(fd) & O_NONBLOCK) && !input.poll())
530
620
  return -11n;
531
- const chunk = this.stdin.read(length);
621
+ const chunk = input.read(length);
532
622
  this.copyIn(pointer, chunk);
533
623
  return BigInt(chunk.byteLength);
534
624
  }
@@ -566,13 +656,20 @@ export class WaliFileDescriptorTable {
566
656
  }
567
657
  }
568
658
  write(fd, pointer, length) {
659
+ return this.writeBytes(fd, pointer, length);
660
+ }
661
+ /** One vector is one destination write and one shared-offset advance. */
662
+ writeBytes(fd, pointer, length, gathered) {
569
663
  try {
570
664
  const stdio = this.stdioFd(fd);
665
+ if (this.terminalFd(fd))
666
+ return (this.statusFlags(fd) & O_ACCMODE) === 0 ? -9n
667
+ : this.options.emitTerminal?.(gathered ?? this.copyOut(pointer, length)) ?? -95n;
571
668
  if (stdio === 1 || stdio === 2)
572
- return this.options.emit(stdio, this.copyOut(pointer, length));
669
+ return this.options.emit(stdio, gathered ?? this.copyOut(pointer, length));
573
670
  const special = this.specialDescriptors.get(fd);
574
671
  if (special)
575
- return special.write?.(this.copyOut(pointer, length)) ?? -9n;
672
+ return special.write?.(gathered ?? this.copyOut(pointer, length)) ?? -9n;
576
673
  const file = this.files.get(fd);
577
674
  if (!file)
578
675
  throw errno("EBADF");
@@ -585,28 +682,32 @@ export class WaliFileDescriptorTable {
585
682
  if (file.flags & O_APPEND)
586
683
  file.position = fs.statSync(file.path).size;
587
684
  // A view, not a copy: the file copies what it keeps (2026-09-27, one copy of every write fewer).
588
- assertRange(this.options.memory(), pointer, length);
589
- fs.writeRangeSync(file.path, file.position, this.memoryBytes().subarray(pointer, pointer + length));
685
+ if (!gathered)
686
+ assertRange(this.options.memory(), pointer, length);
687
+ fs.writeRangeSync(file.path, file.position, gathered ?? this.memoryBytes().subarray(pointer, pointer + length));
590
688
  file.position += length;
591
689
  return BigInt(length);
592
690
  }
593
- const chunk = this.copyOut(pointer, length);
691
+ const value = gathered ?? this.copyOut(pointer, length);
594
692
  const current = fs.readFileSync(file.path);
595
693
  if (file.flags & O_APPEND)
596
694
  file.position = current.byteLength;
597
- const size = Math.max(current.byteLength, file.position + chunk.byteLength);
695
+ const size = Math.max(current.byteLength, file.position + value.byteLength);
598
696
  const next = new Uint8Array(size);
599
697
  next.set(current);
600
- next.set(chunk, file.position);
698
+ next.set(value, file.position);
601
699
  fs.writeFileSync(file.path, next);
602
- file.position += chunk.byteLength;
603
- return BigInt(chunk.byteLength);
700
+ file.position += value.byteLength;
701
+ return BigInt(value.byteLength);
604
702
  }
605
703
  catch (error) {
606
704
  return negativeErrno(error);
607
705
  }
608
706
  }
609
707
  pread(fd, pointer, length, offset) {
708
+ const special = this.specialDescriptors.get(fd);
709
+ if (special)
710
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.read(fd, pointer, length);
610
711
  const file = this.files.get(fd);
611
712
  if (!file)
612
713
  return -9n;
@@ -622,6 +723,9 @@ export class WaliFileDescriptorTable {
622
723
  }
623
724
  }
624
725
  pwrite(fd, pointer, length, offset) {
726
+ const special = this.specialDescriptors.get(fd);
727
+ if (special)
728
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.write(fd, pointer, length);
625
729
  const file = this.files.get(fd);
626
730
  if (!file)
627
731
  return -9n;
@@ -641,6 +745,9 @@ export class WaliFileDescriptorTable {
641
745
  }
642
746
  /** pread over a vector: the same answers, each buffer in turn at the offset, the position unmoved. */
643
747
  preadv(fd, pointer, count, offset) {
748
+ const special = this.specialDescriptors.get(fd);
749
+ if (special)
750
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.readv(fd, pointer, count);
644
751
  const file = this.files.get(fd);
645
752
  if (!file)
646
753
  return -9n;
@@ -657,6 +764,9 @@ export class WaliFileDescriptorTable {
657
764
  }
658
765
  /** pwrite over a vector, as pwrite: at the offset whatever O_APPEND says, the position unmoved. */
659
766
  pwritev(fd, pointer, count, offset) {
767
+ const special = this.specialDescriptors.get(fd);
768
+ if (special)
769
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.writev(fd, pointer, count);
660
770
  const file = this.files.get(fd);
661
771
  if (!file)
662
772
  return -9n;
@@ -689,29 +799,45 @@ export class WaliFileDescriptorTable {
689
799
  return total;
690
800
  }
691
801
  writev(fd, pointer, count) {
692
- let total = 0n;
693
- for (let index = 0; index < count; index += 1) {
694
- const offset = pointer + index * 8;
695
- assertRange(this.options.memory(), offset, 8);
696
- const result = this.write(fd, this.memoryView().getUint32(offset, true), this.memoryView().getUint32(offset + 4, true));
697
- if (result < 0n)
698
- return total > 0n ? total : result;
699
- total += result;
700
- }
701
- return total;
802
+ const value = gatherWaliWriteVector(this.options.memory(), pointer, count);
803
+ return typeof value === "bigint" ? value : this.writeBytes(fd, 0, value.byteLength, value);
804
+ }
805
+ // OpenFileState::create caches dirents in Emscripten file_table.cpp/h
806
+ // (Copyright 2021, MIT/NCSA), pinned at 862dc0d4. Each process retains the
807
+ // same description on dup/fork; kernel storage supplies the entry metadata.
808
+ directoryEntries(path) {
809
+ const fs = this.fs();
810
+ const entries = [];
811
+ const virtual = path === "/proc" ? ["self"] : path === "/proc/self" ? ["fd"]
812
+ : path === "/proc/self/fd" ? this.descriptors.entries.flatMap((entry, fd) => entry ? [String(fd)] : []) : undefined;
813
+ for (const name of [".", "..", ...(virtual ?? fs.readdirSync(path))]) {
814
+ const entryPath = name === "." ? path : name === ".."
815
+ ? normalizePath(path, "..") : normalizePath(path, name);
816
+ const stat = this.virtualStat(entryPath, true) ?? statIfPresent(fs, entryPath, true);
817
+ if (!stat)
818
+ continue; // A backend entry removed during enumeration.
819
+ const link = this.virtualLink(entryPath) ?? this.readSymlink(entryPath);
820
+ const type = link !== undefined || stat.isSymbolicLink?.() ? 10
821
+ : (stat.mode ?? 0) & 0o170000 ? ((stat.mode & 0o170000) >>> 12)
822
+ : stat.isDirectory() ? 4 : 8;
823
+ entries.push({ name, inode: stat.ino ?? 1, type });
824
+ }
825
+ return entries;
702
826
  }
703
827
  getdents64(fd, pointer, count) {
704
828
  try {
829
+ if (!Number.isSafeInteger(count) || count < 24)
830
+ throw errno("EINVAL");
705
831
  const directory = this.files.get(fd);
706
832
  if (!directory)
707
833
  throw errno("EBADF");
708
834
  if (!directory.directory)
709
835
  throw errno("ENOTDIR");
710
- const entries = [".", "..", ...this.fs().readdirSync(directory.path)];
836
+ const entries = directory.entries ??= this.directoryEntries(directory.path);
711
837
  let written = 0;
712
838
  while (directory.position < entries.length) {
713
- const name = entries[directory.position];
714
- const nameBytes = new TextEncoder().encode(name);
839
+ const entry = entries[directory.position];
840
+ const nameBytes = new TextEncoder().encode(entry.name);
715
841
  const recordLength = alignTo(19 + nameBytes.byteLength + 1, 8);
716
842
  if (recordLength > count - written) {
717
843
  if (written === 0)
@@ -720,13 +846,10 @@ export class WaliFileDescriptorTable {
720
846
  }
721
847
  const record = pointer + written;
722
848
  assertRange(this.options.memory(), record, recordLength);
723
- const entryPath = name === "." ? directory.path : name === ".."
724
- ? normalizePath(directory.path, "..") : normalizePath(directory.path, name);
725
- const stat = this.fs().statSync(entryPath);
726
- this.memoryView().setBigUint64(record, BigInt(directory.position + 1), true);
849
+ this.memoryView().setBigUint64(record, BigInt(entry.inode), true);
727
850
  this.memoryView().setBigInt64(record + 8, BigInt(directory.position + 1), true);
728
851
  this.memoryView().setUint16(record + 16, recordLength, true);
729
- this.memoryView().setUint8(record + 18, stat.isDirectory() ? 4 : 8);
852
+ this.memoryView().setUint8(record + 18, entry.type);
730
853
  this.memoryBytes().fill(0, record + 19, record + recordLength);
731
854
  this.memoryBytes().set(nameBytes, record + 19);
732
855
  directory.position += 1;
@@ -740,13 +863,14 @@ export class WaliFileDescriptorTable {
740
863
  }
741
864
  lseek(fd, offset, whence) {
742
865
  try {
866
+ const special = this.specialDescriptors.get(fd);
867
+ if (special)
868
+ return special.seek?.(offset, whence) ?? -29n;
743
869
  const file = this.files.get(fd);
744
870
  if (!file)
745
871
  throw errno("EBADF");
746
- if (file.directory)
747
- throw errno("EINVAL");
748
872
  const base = whence === SEEK_SET ? 0 : whence === SEEK_CUR ? file.position
749
- : whence === SEEK_END ? this.fs().statSync(file.path).size : undefined;
873
+ : whence === SEEK_END && !file.directory ? this.fs().statSync(file.path).size : undefined;
750
874
  // An offset or a position past 2^53 - 1 is not one a double holds (musl 0005's lseek says the same).
751
875
  if (base === undefined || !Number.isSafeInteger(offset) || base + offset < 0 || !Number.isSafeInteger(base + offset))
752
876
  throw errno("EINVAL");
@@ -759,6 +883,9 @@ export class WaliFileDescriptorTable {
759
883
  }
760
884
  ftruncate(fd, length) {
761
885
  try {
886
+ const special = this.specialDescriptors.get(fd);
887
+ if (special)
888
+ return special.truncate?.(length) ?? -22n;
762
889
  const file = this.files.get(fd);
763
890
  if (!file)
764
891
  throw errno("EBADF");
@@ -786,9 +913,44 @@ export class WaliFileDescriptorTable {
786
913
  return negativeErrno(error);
787
914
  }
788
915
  }
916
+ truncate(path, length) {
917
+ try {
918
+ const resolved = this.resolveFilesystemPath(this.resolve(path));
919
+ if (waliDeviceStat(resolved))
920
+ throw errno("EINVAL");
921
+ const fs = this.fs();
922
+ const stat = fs.statSync(resolved);
923
+ if (!stat.isFile())
924
+ throw errno("EISDIR");
925
+ checkFilePermission(stat, 2);
926
+ if (!Number.isSafeInteger(length) || length < 0)
927
+ throw errno("EINVAL");
928
+ if (fs.truncateSync)
929
+ fs.truncateSync(resolved, length);
930
+ else {
931
+ const resized = new Uint8Array(length);
932
+ resized.set(fs.readFileSync(resolved).subarray(0, length));
933
+ fs.writeFileSync(resolved, resized);
934
+ }
935
+ return 0n;
936
+ }
937
+ catch (error) {
938
+ return negativeErrno(error);
939
+ }
940
+ }
789
941
  stat(path, pointer, lstat) {
790
942
  try {
791
943
  const resolved = this.resolveFilesystemPath(this.resolve(path), !lstat);
944
+ const virtual = this.virtualStat(resolved, lstat);
945
+ if (virtual) {
946
+ writeStat(this.memoryView(), pointer, virtual);
947
+ return 0n;
948
+ }
949
+ const device = waliDeviceStat(resolved);
950
+ if (device) {
951
+ writeStat(this.memoryView(), pointer, device);
952
+ return 0n;
953
+ }
792
954
  const fs = this.fs();
793
955
  // A snapshot store's statSync answers ENOENT itself: asking existsSync first resolved the path twice.
794
956
  if (!fs.linkTargetSync && !fs.existsSync(resolved))
@@ -805,15 +967,31 @@ export class WaliFileDescriptorTable {
805
967
  }
806
968
  statAt(dirfd, path, pointer, flags) {
807
969
  try {
808
- if (flags & ~AT_SYMLINK_NOFOLLOW)
970
+ if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH | AT_NO_AUTOMOUNT))
809
971
  throw errno("EINVAL");
972
+ // getFileAt with AT_EMPTY_PATH addresses the open description directly,
973
+ // including a non-directory fd; AT_FDCWD means the current directory.
974
+ if (!path && (flags & AT_EMPTY_PATH)) {
975
+ return dirfd === AT_FDCWD ? this.stat(this.cwd, pointer, false) : this.fstat(dirfd, pointer);
976
+ }
810
977
  const noFollow = Boolean(flags & AT_SYMLINK_NOFOLLOW);
811
978
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), !noFollow);
979
+ const virtual = this.virtualStat(resolved, noFollow);
980
+ if (virtual) {
981
+ writeStat(this.memoryView(), pointer, virtual);
982
+ return 0n;
983
+ }
984
+ const device = waliDeviceStat(resolved);
985
+ if (device) {
986
+ writeStat(this.memoryView(), pointer, device);
987
+ return 0n;
988
+ }
812
989
  const fs = this.fs();
813
990
  if (!fs.linkTargetSync && !fs.existsSync(resolved))
814
991
  throw errno("ENOENT");
815
992
  const target = noFollow ? this.readSymlink(resolved) : undefined;
816
- writeStat(this.memoryView(), pointer, target === undefined ? fs.statSync(resolved) : syntheticSymlinkStat(target));
993
+ writeStat(this.memoryView(), pointer, target === undefined
994
+ ? (noFollow && fs.lstatSync ? fs.lstatSync(resolved) : fs.statSync(resolved)) : syntheticSymlinkStat(target));
817
995
  return 0n;
818
996
  }
819
997
  catch (error) {
@@ -826,27 +1004,38 @@ export class WaliFileDescriptorTable {
826
1004
  writeStat(this.memoryView(), pointer, syntheticTerminalStat());
827
1005
  return 0n;
828
1006
  }
1007
+ const special = this.specialDescriptors.get(fd);
1008
+ const stat = special?.stat?.();
1009
+ if (stat) {
1010
+ writeStat(this.memoryView(), pointer, stat);
1011
+ return 0n;
1012
+ }
829
1013
  const file = this.files.get(fd);
830
1014
  if (!file)
831
1015
  throw errno("EBADF");
832
- writeStat(this.memoryView(), pointer, this.fs().statSync(file.path));
1016
+ writeStat(this.memoryView(), pointer, this.virtualStat(file.path, false) ?? this.fs().statSync(file.path));
833
1017
  return 0n;
834
1018
  }
835
1019
  catch (error) {
836
1020
  return negativeErrno(error);
837
1021
  }
838
1022
  }
839
- access(path) {
840
- try {
841
- return this.fs().existsSync(this.resolveFilesystemPath(this.resolve(path))) ? 0n : -2n;
842
- }
843
- catch (error) {
844
- return negativeErrno(error);
845
- }
846
- }
847
- accessAt(dirfd, path) {
1023
+ accessAt(dirfd, path, mode, flags) {
848
1024
  try {
849
- return this.fs().existsSync(this.resolveFilesystemPath(this.resolveAt(dirfd, path))) ? 0n : -2n;
1025
+ if (mode & ~7 || flags & ~(AT_EACCESS | AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))
1026
+ throw errno("EINVAL");
1027
+ const noFollow = Boolean(flags & AT_SYMLINK_NOFOLLOW);
1028
+ const resolved = !path && (flags & AT_EMPTY_PATH)
1029
+ ? dirfd === AT_FDCWD ? this.cwd : this.files.get(dirfd)?.path
1030
+ : this.resolveFilesystemPath(this.resolveAt(dirfd, path), !noFollow);
1031
+ if (resolved === undefined)
1032
+ throw errno("EBADF");
1033
+ const fs = this.fs();
1034
+ const target = noFollow ? this.readSymlink(resolved) : undefined;
1035
+ const stat = this.virtualStat(resolved, noFollow) ?? waliDeviceStat(resolved) ?? (target !== undefined ? syntheticSymlinkStat(target)
1036
+ : noFollow && fs.lstatSync ? fs.lstatSync(resolved) : fs.statSync(resolved));
1037
+ checkFilePermission(stat, mode);
1038
+ return 0n;
850
1039
  }
851
1040
  catch (error) {
852
1041
  return negativeErrno(error);
@@ -914,24 +1103,34 @@ export class WaliFileDescriptorTable {
914
1103
  return 0n;
915
1104
  }
916
1105
  getcwd(pointer, size) {
917
- const encoded = new TextEncoder().encode(`${this.cwd}\0`);
918
- if (encoded.byteLength > size)
919
- return -34n;
920
- this.copyIn(pointer, encoded);
921
- return BigInt(encoded.byteLength);
1106
+ try {
1107
+ if (!pointer && size > 0)
1108
+ throw errno("EFAULT");
1109
+ if (size === 0)
1110
+ throw errno("EINVAL");
1111
+ if (this.cwd !== "/" && !this.fs().existsSync(this.cwd))
1112
+ throw errno("ENOENT");
1113
+ const encoded = new TextEncoder().encode(`${this.cwd}\0`);
1114
+ if (encoded.byteLength > size)
1115
+ throw errno("ERANGE");
1116
+ this.copyIn(pointer, encoded);
1117
+ return BigInt(encoded.byteLength);
1118
+ }
1119
+ catch (error) {
1120
+ return negativeErrno(error);
1121
+ }
922
1122
  }
923
1123
  mkdir(path, mode) {
924
- return this.mutate(() => {
925
- const resolved = this.resolveFilesystemPath(this.resolve(path), false);
926
- this.fs().mkdirSync(resolved);
927
- this.fs().chmodSync?.(resolved, mode & 0o7777);
928
- });
1124
+ return this.mkdirAt(AT_FDCWD, path, mode);
929
1125
  }
930
1126
  mkdirAt(dirfd, path, mode) {
931
1127
  try {
932
- const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false);
1128
+ const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false, true);
1129
+ checkFileName(resolved);
1130
+ if (statIfPresent(this.fs(), resolved, true))
1131
+ throw errno("EEXIST");
933
1132
  this.fs().mkdirSync(resolved);
934
- this.fs().chmodSync?.(resolved, mode & 0o7777);
1133
+ this.fs().chmodSync?.(resolved, mode & 0o7777 & ~this.fileCreationMask);
935
1134
  return 0n;
936
1135
  }
937
1136
  catch (error) {
@@ -976,21 +1175,38 @@ export class WaliFileDescriptorTable {
976
1175
  try {
977
1176
  if (flags & ~AT_REMOVEDIR)
978
1177
  throw errno("EINVAL");
1178
+ const removeDirectory = Boolean(flags & AT_REMOVEDIR);
1179
+ const leaf = path.replace(/\/+$/u, "").split("/").at(-1);
1180
+ if (removeDirectory && leaf === ".")
1181
+ throw errno("EINVAL");
979
1182
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false);
980
- if (flags & AT_REMOVEDIR)
981
- this.fs().rmdirSync(resolved);
1183
+ const fs = this.fs();
1184
+ const stat = fs.lstatSync?.(resolved) ?? fs.statSync(resolved);
1185
+ if (resolved === "/")
1186
+ throw errno("EBUSY");
1187
+ if (stat.isDirectory()) {
1188
+ if (!removeDirectory)
1189
+ throw errno("EISDIR");
1190
+ if (fs.readdirSync(resolved).length > 0)
1191
+ throw errno("ENOTEMPTY");
1192
+ }
1193
+ else if (removeDirectory)
1194
+ throw errno("ENOTDIR");
1195
+ checkFilePermission(fs.statSync(resolved.slice(0, resolved.lastIndexOf("/")) || "/"), 2);
1196
+ if (removeDirectory)
1197
+ fs.rmdirSync(resolved);
982
1198
  else
983
- this.fs().unlinkSync(resolved);
1199
+ fs.unlinkSync(resolved);
984
1200
  return 0n;
985
1201
  }
986
1202
  catch (error) {
987
1203
  return negativeErrno(error);
988
1204
  }
989
1205
  }
990
- rename(from, to) {
1206
+ rename(from, to, fromDirfd = AT_FDCWD, toDirfd = AT_FDCWD) {
991
1207
  return this.mutate(() => {
992
- const source = this.resolveFilesystemPath(this.resolve(from), false);
993
- const destination = this.resolveFilesystemPath(this.resolve(to), false);
1208
+ const source = this.resolveFilesystemPath(this.resolveAt(fromDirfd, from), false);
1209
+ const destination = this.resolveFilesystemPath(this.resolveAt(toDirfd, to), false);
994
1210
  // Moving a directory the held tree is under moves the tree: the libc's to release first (ADR-0047).
995
1211
  if (this.privateTree && (underRoot(this.privateTree.root, source) || underRoot(this.privateTree.root, destination)))
996
1212
  throw errno("EREMOTE");
@@ -1003,11 +1219,14 @@ export class WaliFileDescriptorTable {
1003
1219
  if (fs.existsSync(source)) {
1004
1220
  if (source === destination)
1005
1221
  return;
1006
- const directory = fs.statSync(source).isDirectory();
1222
+ if (source === "/" || destination === "/")
1223
+ throw errno("EBUSY");
1224
+ checkFileName(destination);
1225
+ const directory = (fs.lstatSync?.(source) ?? fs.statSync(source)).isDirectory();
1007
1226
  if (directory && destination.startsWith(`${source}/`))
1008
1227
  throw errno("EINVAL");
1009
1228
  if (fs.existsSync(destination)) {
1010
- const into = fs.statSync(destination).isDirectory();
1229
+ const into = (fs.lstatSync?.(destination) ?? fs.statSync(destination)).isDirectory();
1011
1230
  if (directory && !into)
1012
1231
  throw errno("ENOTDIR");
1013
1232
  if (!directory && into)
@@ -1051,8 +1270,8 @@ export class WaliFileDescriptorTable {
1051
1270
  };
1052
1271
  const atimeMs = readTime(0, stat.atimeMs ?? stat.mtimeMs ?? now);
1053
1272
  const mtimeMs = readTime(1, stat.mtimeMs ?? now);
1054
- if (filesystem.utimesSync)
1055
- filesystem.utimesSync(path, new Date(atimeMs), new Date(mtimeMs));
1273
+ if (filesystem.utimesSync || filesystem.utimesMsSync)
1274
+ setBrowserFileTimes(filesystem, path, atimeMs, mtimeMs);
1056
1275
  else if (stat.isFile())
1057
1276
  filesystem.writeFileSync(path, filesystem.readFileSync(path));
1058
1277
  return 0n;
@@ -1063,12 +1282,12 @@ export class WaliFileDescriptorTable {
1063
1282
  }
1064
1283
  readlink(path, pointer, size, dirfd) {
1065
1284
  try {
1066
- if (!Number.isSafeInteger(size) || size < 0)
1285
+ if (!Number.isSafeInteger(size) || size <= 0)
1067
1286
  throw errno("EINVAL");
1068
- const resolved = this.resolveAt(dirfd, path);
1287
+ let resolved = this.resolveAt(dirfd, path);
1069
1288
  if (this.privateTree && underRoot(resolved, this.privateTree.root))
1070
1289
  throw errno("EREMOTE");
1071
- let target;
1290
+ let target = this.virtualLink(resolved);
1072
1291
  if (resolved === "/proc/self/exe")
1073
1292
  target = this.options.executablePath ?? "/usr/bin/wali-program";
1074
1293
  const descriptorMatch = /^\/proc\/self\/fd\/(\d+)$/u.exec(resolved);
@@ -1077,12 +1296,14 @@ export class WaliFileDescriptorTable {
1077
1296
  if (descriptor)
1078
1297
  target = descriptor.path;
1079
1298
  }
1299
+ if (target === undefined)
1300
+ resolved = this.resolveFilesystemPath(resolved, false);
1080
1301
  target ??= this.readSymlink(resolved);
1081
1302
  // musl realpath(3) probes every path component with readlink(2).
1082
1303
  // Existing non-symlinks must report EINVAL; ENOENT means the component
1083
1304
  // itself is missing and makes canonicalization fail.
1084
1305
  if (!target)
1085
- return this.fs().existsSync(resolved) ? -22n : -2n;
1306
+ return this.virtualStat(resolved, false) || waliDeviceStat(resolved) || this.fs().existsSync(resolved) ? -22n : -2n;
1086
1307
  const encoded = new TextEncoder().encode(target).subarray(0, size);
1087
1308
  this.copyIn(pointer, encoded);
1088
1309
  return BigInt(encoded.byteLength);
@@ -1094,6 +1315,7 @@ export class WaliFileDescriptorTable {
1094
1315
  symlink(target, link, dirfd) {
1095
1316
  try {
1096
1317
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, link), false);
1318
+ checkFileName(resolved);
1097
1319
  writeWaliSymlink(this.fs(), target, resolved);
1098
1320
  return 0n;
1099
1321
  }
@@ -1101,8 +1323,21 @@ export class WaliFileDescriptorTable {
1101
1323
  return negativeErrno(error);
1102
1324
  }
1103
1325
  }
1104
- resolveFilesystemPath(path, followFinal = true) {
1105
- const resolved = resolveWaliSymlinkPath(this.fs(), path, followFinal);
1326
+ resolveFilesystemPath(path, followFinal = true, allowMissingDirectory = false) {
1327
+ const resolved = resolveWaliSymlinkPath(this.fs(), path, followFinal, allowMissingDirectory, candidate => {
1328
+ const target = this.virtualLink(candidate, false);
1329
+ if (target !== undefined)
1330
+ return { directory: false, target };
1331
+ if (["/proc", "/proc/self", "/proc/self/fd"].includes(candidate))
1332
+ return { directory: true };
1333
+ const fd = this.procDescriptor(candidate);
1334
+ if (fd === undefined || !this.hasFd(fd))
1335
+ return;
1336
+ const file = this.files.get(fd);
1337
+ // Directory magic links must support descendants (fd/N/name). Files
1338
+ // remain magic leaves so reopen applies the caller's requested flags.
1339
+ return file?.directory ? { directory: false, target: file.path } : { directory: false };
1340
+ });
1106
1341
  // A held private tree is the libc's: a path that reaches it here (a symlink
1107
1342
  // elsewhere, a directory descriptor) is EREMOTE, and the libc releases the
1108
1343
  // tree and asks again (ADR-0047 §3).
@@ -1110,11 +1345,119 @@ export class WaliFileDescriptorTable {
1110
1345
  throw errno("EREMOTE");
1111
1346
  return resolved;
1112
1347
  }
1348
+ procDescriptor(path) {
1349
+ const match = /^\/proc\/self\/fd\/(\d+)$/u.exec(path);
1350
+ return match ? Number(match[1]) : undefined;
1351
+ }
1352
+ virtualLink(path, descriptors = true) {
1353
+ if (path === "/dev/fd")
1354
+ return "/proc/self/fd";
1355
+ if (path === "/dev/stdin")
1356
+ return "/proc/self/fd/0";
1357
+ if (path === "/dev/stdout")
1358
+ return "/proc/self/fd/1";
1359
+ if (path === "/dev/stderr")
1360
+ return "/proc/self/fd/2";
1361
+ if (!descriptors)
1362
+ return;
1363
+ const fd = this.procDescriptor(path);
1364
+ if (fd === undefined)
1365
+ return;
1366
+ const description = this.descriptors.get(fd);
1367
+ if (!description)
1368
+ throw errno("ENOENT");
1369
+ const file = this.files.get(fd);
1370
+ if (file)
1371
+ return file.path;
1372
+ if (description.kind === "pipe")
1373
+ return `pipe:[${description.value.pipe + 1}]`;
1374
+ const special = this.specialDescriptors.get(fd);
1375
+ const rdev = special?.stat?.().rdev;
1376
+ if (rdev !== undefined)
1377
+ return rdev === 0x103 ? "/dev/null" : rdev === 0x108 ? "/dev/random"
1378
+ : rdev === 0x109 ? "/dev/urandom" : "/dev/tty";
1379
+ if (special?.stdio !== undefined)
1380
+ return "/dev/tty";
1381
+ return `anon_inode:[${description.kind}]`;
1382
+ }
1383
+ virtualStat(path, noFollow) {
1384
+ if (noFollow) {
1385
+ const target = this.virtualLink(path);
1386
+ if (target !== undefined)
1387
+ return syntheticSymlinkStat(target);
1388
+ }
1389
+ if (["/proc", "/proc/self", "/proc/self/fd"].includes(path))
1390
+ return {
1391
+ isFile: () => false, isDirectory: () => true, size: 0, mode: 0o040555,
1392
+ };
1393
+ const fd = this.procDescriptor(path);
1394
+ if (fd === undefined)
1395
+ return;
1396
+ const description = this.descriptors.get(fd);
1397
+ if (!description)
1398
+ throw errno("ENOENT");
1399
+ const file = this.files.get(fd);
1400
+ if (file)
1401
+ return this.fs().statSync(file.path);
1402
+ if (description.kind === "pipe")
1403
+ return {
1404
+ isFile: () => false, isDirectory: () => false, size: 0, mode: 0o010666,
1405
+ ino: description.value.pipe + 1,
1406
+ };
1407
+ return this.specialDescriptors.get(fd)?.stat?.() ?? syntheticTerminalStat();
1408
+ }
1409
+ reopenDescriptor(fd, flags, mode) {
1410
+ const description = this.descriptors.get(fd);
1411
+ if (!description)
1412
+ throw errno("ENOENT");
1413
+ const stat = this.virtualStat(`/proc/self/fd/${fd}`, false);
1414
+ if ((flags & O_DIRECTORY) && !stat.isDirectory())
1415
+ throw errno("ENOTDIR");
1416
+ if ((flags & O_ACCMODE) !== O_WRONLY)
1417
+ checkFilePermission(stat, 4);
1418
+ if ((flags & O_ACCMODE) !== 0)
1419
+ checkFilePermission(stat, 2);
1420
+ const file = this.files.get(fd);
1421
+ if (file)
1422
+ return this.openResolved(file.path, flags & ~(O_CREAT | O_EXCL), mode);
1423
+ if (description.kind === "pipe" && this.options.descriptorState) {
1424
+ const host = new WaliSharedPipeHost(this.options.memory, { ...this.options.descriptorState,
1425
+ table: this.descriptors.sharedEntries.buffer }, this.descriptors);
1426
+ const result = host.reopen(fd, flags);
1427
+ if (result < 0n)
1428
+ throw errno(result === -24n ? "EMFILE" : "EOPNOTSUPP");
1429
+ return Number(result);
1430
+ }
1431
+ const target = this.virtualLink(`/proc/self/fd/${fd}`);
1432
+ if (target === "/dev/tty" && this.specialDescriptors.get(fd)?.terminal)
1433
+ return this.openResolved(target, flags, mode);
1434
+ const device = target && createWaliDevice(target, flags);
1435
+ if (device) {
1436
+ const next = this.allocateFd();
1437
+ this.specialDescriptors.set(next, device);
1438
+ return this.markCloseOnExecNumber(next, flags);
1439
+ }
1440
+ const special = this.specialDescriptors.get(fd);
1441
+ if (special?.stdio !== undefined) {
1442
+ // Host streams currently expose one direction rather than an inode
1443
+ // that can be reopened in either direction. Do not return a descriptor
1444
+ // whose access mode promises I/O its transport cannot perform.
1445
+ if ((flags & O_ACCMODE) !== (special.stdio === 0 ? 0 : O_WRONLY))
1446
+ throw errno("EOPNOTSUPP");
1447
+ const next = this.allocateFd();
1448
+ this.specialDescriptors.set(next, { stdio: special.stdio });
1449
+ Atomics.store(this.descriptors.registry.pipeMetadata, this.descriptors.entries[next].description * 3 + 1, flags & ~O_CLOEXEC_FLAG);
1450
+ return this.markCloseOnExecNumber(next, flags);
1451
+ }
1452
+ throw errno("EOPNOTSUPP");
1453
+ }
1113
1454
  readSymlink(path) {
1114
1455
  return readWaliSymlink(this.fs(), path);
1115
1456
  }
1116
- dup(fd, target, minimum = 3) {
1117
- const maximum = this.options.descriptorMaximum ?? 65_535;
1457
+ dup(fd, target, minimum = 0) {
1458
+ if (target === fd && this.hasFd(fd))
1459
+ return BigInt(fd);
1460
+ const maximum = Math.min(this.options.descriptorMaximum ?? 65_535, this.descriptors.descriptorLimit - 1);
1118
1461
  const stdio = this.stdioFd(fd);
1119
1462
  if (stdio !== undefined) {
1120
1463
  const next = target ?? this.allocateFd(minimum);
@@ -1122,8 +1465,8 @@ export class WaliFileDescriptorTable {
1122
1465
  return -9n;
1123
1466
  if (next === fd)
1124
1467
  return BigInt(next);
1468
+ this.close(next);
1125
1469
  this.descriptors.set(next, this.descriptors.get(fd));
1126
- this.descriptorStatusFlags.set(next, this.statusFlags(fd));
1127
1470
  this.setSharedStdioAlias(next, stdio);
1128
1471
  return BigInt(next);
1129
1472
  }
@@ -1147,7 +1490,7 @@ export class WaliFileDescriptorTable {
1147
1490
  // wasmfs/syscalls.cpp __syscall_fcntl64 F_DUPFD: every open file,
1148
1491
  // including special files, shares its description through the table.
1149
1492
  // Our kernel retains process ownership and FD_CLOEXEC semantics.
1150
- if (argument < 0 || argument > (this.options.descriptorMaximum ?? 65_535))
1493
+ if (argument < 0 || argument >= this.descriptors.descriptorLimit || argument > (this.options.descriptorMaximum ?? 65_535))
1151
1494
  return -22n;
1152
1495
  if (special?.duplicate)
1153
1496
  return special.duplicate(argument, command === 1030);
@@ -1176,13 +1519,18 @@ export class WaliFileDescriptorTable {
1176
1519
  if (command === 3)
1177
1520
  return BigInt(this.statusFlags(fd));
1178
1521
  if (command === 4) {
1522
+ // libsyscall F_SETFL: change only mutable status bits. Access mode,
1523
+ // creation flags and descriptor CLOEXEC are not replaced by the argument.
1524
+ const mutable = O_APPEND | O_NONBLOCK | 0x2000 /* O_ASYNC */ | 0x4000 /* O_DIRECT */ | 0x40000 /* O_NOATIME */;
1179
1525
  const stdio = this.stdioFd(fd);
1180
- if (stdio !== undefined)
1181
- this.descriptorStatusFlags.set(fd, argument);
1526
+ if (stdio !== undefined) {
1527
+ const id = this.descriptors.entries[fd].description;
1528
+ Atomics.store(this.descriptors.registry.pipeMetadata, id * 3 + 1, (this.statusFlags(fd) & ~mutable) | (argument & mutable));
1529
+ }
1182
1530
  else {
1183
1531
  const file = this.files.get(fd);
1184
1532
  if (file)
1185
- file.flags = (file.flags & O_ACCMODE) | (argument & ~O_ACCMODE);
1533
+ file.flags = (file.flags & ~mutable) | (argument & mutable);
1186
1534
  }
1187
1535
  return 0n;
1188
1536
  }
@@ -1347,6 +1695,9 @@ export class WaliFileDescriptorTable {
1347
1695
  return BigInt(scan());
1348
1696
  }
1349
1697
  hasFd(fd) {
1698
+ if (this.options.descriptorState && !this.options.descriptors)
1699
+ return Number.isInteger(fd) && fd >= 0 && fd < 65_536
1700
+ && Atomics.load(new Int32Array(this.options.descriptorState.table), fd) !== 0;
1350
1701
  return this.descriptors.get(fd) !== undefined;
1351
1702
  }
1352
1703
  /** Descriptor aliases retain the identity of their terminal stream. */
@@ -1355,24 +1706,46 @@ export class WaliFileDescriptorTable {
1355
1706
  const state = this.options.descriptorState;
1356
1707
  if (fd < 0 || fd >= 65_536)
1357
1708
  return;
1358
- const id = Atomics.load(new Int32Array(state.table), fd);
1359
- const stream = Atomics.load(new Int32Array(state.descriptions), id * 3 + 2) - 1;
1709
+ const id = Atomics.load(this.options.descriptors ? this.descriptors.sharedEntries : new Int32Array(state.table), fd);
1710
+ const encoded = Atomics.load(new Int32Array(state.descriptions), id * 3 + 2);
1711
+ if (encoded === 4)
1712
+ return 0;
1713
+ const stream = encoded - 1;
1360
1714
  return stream >= 0 && stream <= 2 ? stream : undefined;
1361
1715
  }
1362
1716
  return this.specialDescriptors.get(fd)?.stdio;
1363
1717
  }
1718
+ terminalFd(fd) {
1719
+ const state = this.options.descriptorState;
1720
+ if (!state)
1721
+ return this.specialDescriptors.get(fd)?.terminal === true;
1722
+ if (fd < 0 || fd >= 65_536)
1723
+ return false;
1724
+ const id = Atomics.load(this.options.descriptors ? this.descriptors.sharedEntries : new Int32Array(state.table), fd);
1725
+ return Atomics.load(new Int32Array(state.descriptions), id * 3 + 2) === 4;
1726
+ }
1727
+ terminalAvailable(pointer) {
1728
+ if (!this.options.terminalStdin)
1729
+ return -95n;
1730
+ if (pointer <= 0 || pointer + 4 > this.options.memory().buffer.byteLength)
1731
+ return -14n;
1732
+ this.memoryView().setInt32(pointer, this.options.terminalStdin.available?.() ?? 0, true);
1733
+ return 0n;
1734
+ }
1364
1735
  setSharedStdioAlias(fd, target) {
1365
1736
  if (fd < 0 || fd >= (this.options.stdioAliasRegistry?.length ?? 0))
1366
1737
  return;
1367
1738
  Atomics.store(this.options.stdioAliasRegistry, fd, target ?? -1);
1368
1739
  }
1369
1740
  statusFlags(fd) {
1370
- const stored = this.descriptorStatusFlags.get(fd);
1371
- if (stored !== undefined)
1372
- return stored;
1373
1741
  const stdio = this.stdioFd(fd);
1374
- if (stdio !== undefined)
1375
- return stdio === 0 ? 0 : O_WRONLY;
1742
+ if (stdio !== undefined) {
1743
+ const state = this.options.descriptorState;
1744
+ const table = state && !this.options.descriptors ? new Int32Array(state.table) : this.descriptors.sharedEntries;
1745
+ const metadata = state ? new Int32Array(state.descriptions) : this.descriptors.registry.pipeMetadata;
1746
+ const id = Atomics.load(table, fd);
1747
+ return Atomics.load(metadata, id * 3 + 1) | (stdio === 0 || this.terminalFd(fd) ? 0 : O_WRONLY);
1748
+ }
1376
1749
  return this.files.get(fd)?.flags ?? O_RDWR;
1377
1750
  }
1378
1751
  mutate(operation) {
@@ -1409,7 +1782,7 @@ export class WaliFileDescriptorTable {
1409
1782
  else
1410
1783
  fs.writeFileSync(node.path, node.content);
1411
1784
  fs.chmodSync?.(node.path, node.mode & 0o7777);
1412
- fs.utimesSync?.(node.path, new Date(node.atimeMs), new Date(node.mtimeMs));
1785
+ setBrowserFileTimes(fs, node.path, node.atimeMs, node.mtimeMs);
1413
1786
  }
1414
1787
  }
1415
1788
  this.privateTree = undefined;
@@ -1546,7 +1919,7 @@ export class WaliFileDescriptorTable {
1546
1919
  else
1547
1920
  fs.writeFileSync(node.path, node.content);
1548
1921
  fs.chmodSync?.(node.path, node.mode & 0o7777);
1549
- fs.utimesSync?.(node.path, new Date(node.atimeMs), new Date(node.mtimeMs));
1922
+ setBrowserFileTimes(fs, node.path, node.atimeMs, node.mtimeMs);
1550
1923
  }
1551
1924
  }
1552
1925
  for (const descriptor of descriptors) {
@@ -1555,7 +1928,9 @@ export class WaliFileDescriptorTable {
1555
1928
  if (descriptor.group === descriptor.fd) {
1556
1929
  this.files.set(descriptor.fd, {
1557
1930
  path: descriptor.path, flags: descriptor.flags, position: descriptor.position,
1558
- directory: descriptor.directory, description: this.nextDescription++,
1931
+ directory: descriptor.directory,
1932
+ ...(descriptor.directory ? { entries: this.directoryEntries(descriptor.path) } : {}),
1933
+ description: this.nextDescription++,
1559
1934
  });
1560
1935
  }
1561
1936
  else {
@@ -1570,6 +1945,19 @@ export class WaliFileDescriptorTable {
1570
1945
  return 0n;
1571
1946
  }
1572
1947
  }
1948
+ /** WasmFS's single-user mode checks; identity/credential policy is a separate seam. */
1949
+ function checkFilePermission(stat, requested) {
1950
+ const mode = stat.mode ?? (stat.isDirectory() ? 0o777 : 0o666);
1951
+ if (((requested & 4) && !(mode & 0o444))
1952
+ || ((requested & 2) && !(mode & 0o222))
1953
+ || ((requested & 1) && !(mode & 0o111)))
1954
+ throw errno("EACCES");
1955
+ }
1956
+ function checkFileName(path) {
1957
+ // WASMFS_NAME_MAX is Linux's 255-byte component limit, not a work budget.
1958
+ if (new TextEncoder().encode(path.slice(path.lastIndexOf("/") + 1)).byteLength > 255)
1959
+ throw errno("ENAMETOOLONG");
1960
+ }
1573
1961
  /** preadv's offset: one 64-bit argument, or two 32-bit halves from a libc that splits it (musl's own preadv.c). */
1574
1962
  function vectorOffset(offset, high) {
1575
1963
  if (high === undefined)
@@ -1585,19 +1973,11 @@ function normalizePath(cwd, path) {
1585
1973
  // Most paths a program names are already normal: itself, without the split.
1586
1974
  if (isNormalPath(path))
1587
1975
  return path;
1588
- const components = [];
1589
1976
  const value = path.startsWith("/") ? path : `${cwd}/${path}`;
1590
- for (const component of value.replaceAll("\\", "/").split("/")) {
1591
- if (!component || component === ".")
1592
- continue;
1593
- if (component === "..")
1594
- components.pop();
1595
- else
1596
- components.push(component);
1597
- }
1598
- return `/${components.join("/")}`;
1977
+ const normalized = normalizeWaliPath(value);
1978
+ return normalized === "/" ? "/" : normalized.replace(/\/$/u, "");
1599
1979
  }
1600
- /** Absolute, no empty, `.` or `..` component, no trailing slash, and no backslash (which `normalizePath` turns into one). */
1980
+ /** Absolute with no empty/dot component or trailing slash; backslash is a literal character. */
1601
1981
  function isNormalPath(path) {
1602
1982
  if (path.charCodeAt(0) !== 47)
1603
1983
  return false;
@@ -1606,8 +1986,6 @@ function isNormalPath(path) {
1606
1986
  let start = 1;
1607
1987
  for (let index = 1; index <= path.length; index += 1) {
1608
1988
  const code = index < path.length ? path.charCodeAt(index) : 47;
1609
- if (code === 92)
1610
- return false;
1611
1989
  if (code !== 47)
1612
1990
  continue;
1613
1991
  const length = index - start;
@@ -1637,9 +2015,9 @@ function assertRange(memory, pointer, length) {
1637
2015
  throw new WebAssembly.RuntimeError("WALI filesystem memory access is out of bounds.");
1638
2016
  }
1639
2017
  /** The stat of `path`, or undefined when the store says ENOENT; any other failure is thrown. */
1640
- function statIfPresent(fs, path) {
2018
+ function statIfPresent(fs, path, noFollow = false) {
1641
2019
  try {
1642
- return fs.statSync(path);
2020
+ return noFollow && fs.lstatSync ? fs.lstatSync(path) : fs.statSync(path);
1643
2021
  }
1644
2022
  catch (error) {
1645
2023
  if (/\bENOENT\b/u.test(error instanceof Error ? error.message : String(error)))
@@ -1657,8 +2035,9 @@ function writeStat(view, pointer, stat) {
1657
2035
  setUint64(view, pointer, stat.dev ?? 1);
1658
2036
  setUint64(view, pointer + 8, stat.ino ?? 1);
1659
2037
  setUint64(view, pointer + 16, stat.nlink ?? 1);
1660
- const kind = stat.isSymbolicLink?.() ? S_IFLNK : stat.isDirectory() ? S_IFDIR : S_IFREG;
1661
- view.setUint32(pointer + 24, kind | (stat.mode ?? (stat.isDirectory() ? 0o755 : 0o644)), true);
2038
+ // A device supplies S_IFCHR; ORing S_IFREG into it made it S_IFLNK.
2039
+ const kind = (stat.mode ?? 0) & 0o170000 || (stat.isSymbolicLink?.() ? S_IFLNK : stat.isDirectory() ? S_IFDIR : S_IFREG);
2040
+ view.setUint32(pointer + 24, kind | ((stat.mode ?? (stat.isDirectory() ? 0o755 : 0o644)) & 0o7777), true);
1662
2041
  view.setUint32(pointer + 28, stat.uid ?? 1000, true);
1663
2042
  view.setUint32(pointer + 32, stat.gid ?? 1000, true);
1664
2043
  view.setUint32(pointer + 36, 0, true);
@@ -1732,10 +2111,10 @@ function errno(errnoName) {
1732
2111
  return error;
1733
2112
  }
1734
2113
  const ERRNO_NUMBERS = {
1735
- EPERM: 1n, ENOENT: 2n, EIO: 5n, EBADF: 9n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
2114
+ EPERM: 1n, ENOENT: 2n, EINTR: 4n, EIO: 5n, ENXIO: 6n, EBADF: 9n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1736
2115
  EFAULT: 14n, EBUSY: 16n, EEXIST: 17n, EXDEV: 18n, ENOTDIR: 20n, EISDIR: 21n,
1737
2116
  EINVAL: 22n, EMFILE: 24n, ENOSPC: 28n, ESPIPE: 29n, EROFS: 30n, ERANGE: 34n,
1738
- ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n, EREMOTE: 66n,
2117
+ ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n, EREMOTE: 66n, EOPNOTSUPP: 95n,
1739
2118
  };
1740
2119
  function negativeErrno(error) {
1741
2120
  // The errno's own name first, and the message parsed only for an error that has none.