@volter/browser-wali 0.1.9 → 0.1.11

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 (113) hide show
  1. package/dist/bulk-memory.d.ts +23 -0
  2. package/dist/bulk-memory.d.ts.map +1 -0
  3. package/dist/bulk-memory.js +50 -0
  4. package/dist/bulk-memory.js.map +1 -0
  5. package/dist/coherent-file-locks.d.ts +4 -1
  6. package/dist/coherent-file-locks.d.ts.map +1 -1
  7. package/dist/coherent-file-locks.js +24 -1
  8. package/dist/coherent-file-locks.js.map +1 -1
  9. package/dist/descriptors.d.ts +18 -1
  10. package/dist/descriptors.d.ts.map +1 -1
  11. package/dist/descriptors.js +51 -6
  12. package/dist/descriptors.js.map +1 -1
  13. package/dist/filesystem-host.d.ts +81 -0
  14. package/dist/filesystem-host.d.ts.map +1 -0
  15. package/dist/filesystem-host.js +849 -0
  16. package/dist/filesystem-host.js.map +1 -0
  17. package/dist/filesystem-protocol.d.ts +49 -1
  18. package/dist/filesystem-protocol.d.ts.map +1 -1
  19. package/dist/filesystem-protocol.js +17 -0
  20. package/dist/filesystem-protocol.js.map +1 -1
  21. package/dist/filesystem-remote.d.ts +72 -18
  22. package/dist/filesystem-remote.d.ts.map +1 -1
  23. package/dist/filesystem-remote.js +271 -40
  24. package/dist/filesystem-remote.js.map +1 -1
  25. package/dist/filesystem-worker.js +8 -502
  26. package/dist/filesystem-worker.js.map +1 -1
  27. package/dist/filesystem.d.ts +9 -0
  28. package/dist/filesystem.d.ts.map +1 -1
  29. package/dist/filesystem.js +209 -61
  30. package/dist/filesystem.js.map +1 -1
  31. package/dist/host-idle.d.ts +29 -0
  32. package/dist/host-idle.d.ts.map +1 -0
  33. package/dist/host-idle.js +68 -0
  34. package/dist/host-idle.js.map +1 -0
  35. package/dist/host.d.ts +1 -0
  36. package/dist/host.d.ts.map +1 -1
  37. package/dist/host.js +1 -3
  38. package/dist/host.js.map +1 -1
  39. package/dist/memory.d.ts.map +1 -1
  40. package/dist/memory.js +79 -18
  41. package/dist/memory.js.map +1 -1
  42. package/dist/module-layout.d.ts +6 -0
  43. package/dist/module-layout.d.ts.map +1 -1
  44. package/dist/module-layout.js +26 -3
  45. package/dist/module-layout.js.map +1 -1
  46. package/dist/monotonic-clock.d.ts +9 -0
  47. package/dist/monotonic-clock.d.ts.map +1 -1
  48. package/dist/monotonic-clock.js +11 -0
  49. package/dist/monotonic-clock.js.map +1 -1
  50. package/dist/network-worker.js +7 -2
  51. package/dist/network-worker.js.map +1 -1
  52. package/dist/network.d.ts.map +1 -1
  53. package/dist/network.js +11 -8
  54. package/dist/network.js.map +1 -1
  55. package/dist/process-bridge.d.ts.map +1 -1
  56. package/dist/process-bridge.js +25 -5
  57. package/dist/process-bridge.js.map +1 -1
  58. package/dist/program.d.ts +5 -1
  59. package/dist/program.d.ts.map +1 -1
  60. package/dist/program.js +13 -12
  61. package/dist/program.js.map +1 -1
  62. package/dist/runtime.d.ts.map +1 -1
  63. package/dist/runtime.js +75 -9
  64. package/dist/runtime.js.map +1 -1
  65. package/dist/shared-file-layer.d.ts +69 -0
  66. package/dist/shared-file-layer.d.ts.map +1 -0
  67. package/dist/shared-file-layer.js +299 -0
  68. package/dist/shared-file-layer.js.map +1 -0
  69. package/dist/shared-pipes.d.ts +7 -0
  70. package/dist/shared-pipes.d.ts.map +1 -1
  71. package/dist/shared-pipes.js +56 -17
  72. package/dist/shared-pipes.js.map +1 -1
  73. package/dist/signals.d.ts +2 -0
  74. package/dist/signals.d.ts.map +1 -1
  75. package/dist/signals.js +5 -1
  76. package/dist/signals.js.map +1 -1
  77. package/dist/snapshot-filesystem.d.ts +36 -3
  78. package/dist/snapshot-filesystem.d.ts.map +1 -1
  79. package/dist/snapshot-filesystem.js +204 -19
  80. package/dist/snapshot-filesystem.js.map +1 -1
  81. package/dist/stdin.d.ts.map +1 -1
  82. package/dist/stdin.js +3 -2
  83. package/dist/stdin.js.map +1 -1
  84. package/dist/symlink.d.ts.map +1 -1
  85. package/dist/symlink.js +91 -1
  86. package/dist/symlink.js.map +1 -1
  87. package/dist/system.d.ts.map +1 -1
  88. package/dist/system.js +17 -6
  89. package/dist/system.js.map +1 -1
  90. package/dist/thread-coordinator-worker.d.ts +1 -25
  91. package/dist/thread-coordinator-worker.d.ts.map +1 -1
  92. package/dist/thread-coordinator-worker.js +29 -2
  93. package/dist/thread-coordinator-worker.js.map +1 -1
  94. package/dist/thread-worker.js +13 -8
  95. package/dist/thread-worker.js.map +1 -1
  96. package/dist/threads.d.ts +4 -0
  97. package/dist/threads.d.ts.map +1 -1
  98. package/dist/threads.js +72 -43
  99. package/dist/threads.js.map +1 -1
  100. package/dist/types.d.ts +10 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/websocket-peer.d.ts.map +1 -1
  103. package/dist/websocket-peer.js +2 -1
  104. package/dist/websocket-peer.js.map +1 -1
  105. package/dist/worker-program-protocol.d.ts +26 -0
  106. package/dist/worker-program-protocol.d.ts.map +1 -1
  107. package/dist/worker-program-worker.js +26 -8
  108. package/dist/worker-program-worker.js.map +1 -1
  109. package/dist/worker-program.d.ts +43 -0
  110. package/dist/worker-program.d.ts.map +1 -1
  111. package/dist/worker-program.js +198 -24
  112. package/dist/worker-program.js.map +1 -1
  113. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { realtimeMilliseconds } from "./monotonic-clock.js";
1
2
  import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
2
3
  import { createProcessFilesystemMutationGuard } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
3
4
  import { BufferedWaliStdin } from "./stdin.js";
@@ -16,6 +17,7 @@ const O_DIRECTORY = 0x1_0000;
16
17
  const SEEK_SET = 0;
17
18
  const SEEK_CUR = 1;
18
19
  const SEEK_END = 2;
20
+ const O_CLOEXEC_FLAG = 0x80000;
19
21
  const F_GETLK = 5;
20
22
  const F_SETLK = 6;
21
23
  const F_SETLKW = 7;
@@ -80,6 +82,11 @@ export class WaliFileDescriptorTable {
80
82
  readFileSync: fs.readFileSync.bind(fs), readFile: fs.readFile.bind(fs),
81
83
  readdirSync: fs.readdirSync.bind(fs),
82
84
  writeFileSync: (path, bytes) => fs.writeFileSync(this.assertWritable(path), bytes),
85
+ ...(fs.readRangeSync ? { readRangeSync: fs.readRangeSync.bind(fs) } : {}),
86
+ ...(fs.linkTargetSync ? { linkTargetSync: fs.linkTargetSync.bind(fs) } : {}),
87
+ ...(fs.viewRangeSync ? { viewRangeSync: fs.viewRangeSync.bind(fs) } : {}),
88
+ ...(fs.writeRangeSync ? { writeRangeSync: (path, offset, data) => fs.writeRangeSync(this.assertWritable(path), offset, data) } : {}),
89
+ ...(fs.truncateSync ? { truncateSync: (path, length) => fs.truncateSync(this.assertWritable(path), length) } : {}),
83
90
  mkdirSync: (path, settings) => fs.mkdirSync(this.assertWritable(path), settings),
84
91
  unlinkSync: (path) => fs.unlinkSync(this.assertWritable(path)),
85
92
  rmdirSync: (path) => fs.rmdirSync(this.assertWritable(path)),
@@ -97,6 +104,8 @@ export class WaliFileDescriptorTable {
97
104
  this.specialDescriptors.set(fd, { stdio: fd });
98
105
  }
99
106
  }
107
+ /** Whether this table holds `fd` as an open file or directory of its own: no pipe, socket, special or stdio stream. */
108
+ ownsFile(fd) { return this.files.has(fd); }
100
109
  imports() {
101
110
  return {
102
111
  SYS_open: (path, flags, mode) => this.open(this.readCString(Number(path)), Number(flags), Number(mode)),
@@ -142,7 +151,13 @@ export class WaliFileDescriptorTable {
142
151
  SYS_readlinkat: (dirfd, path, pointer, size) => this.readlink(this.readCString(Number(path)), Number(pointer), Number(size), Number(dirfd)),
143
152
  SYS_dup: (fd) => this.dup(Number(fd)),
144
153
  SYS_dup2: (fd, target) => this.dup(Number(fd), Number(target)),
145
- SYS_dup3: (fd, target) => this.dup(Number(fd), Number(target)),
154
+ // As Linux: EINVAL for any flag but O_CLOEXEC and for dup3(fd, fd); O_CLOEXEC marks the new descriptor.
155
+ SYS_dup3: (fd, target, flags) => {
156
+ const requested = Number(flags);
157
+ if ((requested & ~O_CLOEXEC_FLAG) !== 0 || Number(fd) === Number(target))
158
+ return -22n;
159
+ return this.markCloseOnExec(this.dup(Number(fd), Number(target)), requested);
160
+ },
146
161
  // fcntl's third parameter is variadic. WALI transports the ABI register
147
162
  // as i64, so its upper 32 bits are unspecified for integer arguments.
148
163
  SYS_fcntl: (fd, command, argument) => this.fcntl(Number(fd), Number(command), lowSigned32(argument)),
@@ -196,7 +211,10 @@ export class WaliFileDescriptorTable {
196
211
  const file = this.files.get(fd);
197
212
  if (!file || file.directory || offset < 0 || length < 0)
198
213
  return;
199
- return this.fs().readFileSync(file.path).slice(offset, offset + length);
214
+ // The range, not the whole file sliced: a mapping is read in pieces, and
215
+ // each piece copied the file entire (2026-09-27, Blender reading a `.blend`).
216
+ const fs = this.fs();
217
+ return fs.readRangeSync ? fs.readRangeSync(file.path, offset, length) : fs.readFileSync(file.path).slice(offset, offset + length);
200
218
  }
201
219
  mappingPath(fd) {
202
220
  const file = this.files.get(fd);
@@ -220,6 +238,17 @@ export class WaliFileDescriptorTable {
220
238
  return false;
221
239
  }
222
240
  }
241
+ /** `fd` marked close-on-exec when `flags` carry O_CLOEXEC (open's and dup3's flags); `fd` returned. */
242
+ markCloseOnExecNumber(fd, flags) {
243
+ if (flags & O_CLOEXEC_FLAG)
244
+ this.descriptors.entries[fd].flags = 1;
245
+ return fd;
246
+ }
247
+ markCloseOnExec(result, flags) {
248
+ if (result >= 0n)
249
+ this.markCloseOnExecNumber(Number(result), flags);
250
+ return result;
251
+ }
223
252
  allocateSpecial(descriptor, minimum = 3) {
224
253
  const fd = this.allocateFd(Math.max(minimum, this.options.specialDescriptorMinimum ?? 3));
225
254
  this.specialDescriptors.set(fd, descriptor);
@@ -272,8 +301,22 @@ export class WaliFileDescriptorTable {
272
301
  throw errno("ENOSYS");
273
302
  return this.guestFilesystem;
274
303
  }
275
- memoryBytes() { return new Uint8Array(this.options.memory().buffer); }
276
- memoryView() { return new DataView(this.options.memory().buffer); }
304
+ // The views over the program's memory, made again only when its buffer is
305
+ // another (a grow): two new views per syscall, and more for a stat, were
306
+ // allocated for the same bytes (2026-09-28).
307
+ viewedBuffer;
308
+ viewedBytes = new Uint8Array();
309
+ viewedData = new DataView(new ArrayBuffer(0));
310
+ memoryBytes() { this.viewMemory(); return this.viewedBytes; }
311
+ memoryView() { this.viewMemory(); return this.viewedData; }
312
+ viewMemory() {
313
+ const buffer = this.options.memory().buffer;
314
+ if (buffer === this.viewedBuffer)
315
+ return;
316
+ this.viewedBuffer = buffer;
317
+ this.viewedBytes = new Uint8Array(buffer);
318
+ this.viewedData = new DataView(buffer);
319
+ }
277
320
  copyOut(pointer, length) {
278
321
  assertRange(this.options.memory(), pointer, length);
279
322
  return this.memoryBytes().slice(pointer, pointer + length);
@@ -289,10 +332,20 @@ export class WaliFileDescriptorTable {
289
332
  const end = bytes.indexOf(0, pointer);
290
333
  if (end < 0 || end - pointer > 1_048_576)
291
334
  throw new WebAssembly.RuntimeError("WALI string is not null terminated.");
292
- // Browser TextDecoder deliberately rejects views backed by
293
- // SharedArrayBuffer. Copying the short C string also gives the decoder a
294
- // stable snapshot while another pthread may write process memory.
295
- return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes.subarray(pointer, end)));
335
+ // A path is almost always ASCII, read here a code unit at a time; any
336
+ // other goes through one decoder, over a copy, since a browser's
337
+ // TextDecoder rejects views backed by SharedArrayBuffer and the copy is a
338
+ // stable snapshot while another pthread may write process memory. A new
339
+ // decoder and an element-wise copy for every path were a measurable part
340
+ // of every path syscall (2026-09-27).
341
+ let text = "";
342
+ for (let index = pointer; index < end; index += 1) {
343
+ const unit = bytes[index];
344
+ if (unit >= 0x80)
345
+ return strictDecoder.decode(bytes.slice(pointer, end));
346
+ text += String.fromCharCode(unit);
347
+ }
348
+ return text;
296
349
  }
297
350
  resolve(path) { return normalizePath(this.cwd, path); }
298
351
  resolveAt(dirfd, path) {
@@ -322,16 +375,19 @@ export class WaliFileDescriptorTable {
322
375
  }
323
376
  }
324
377
  openResolved(path, flags, mode) {
378
+ // O_CLOEXEC is recorded here for every kind of descriptor open makes: musl
379
+ // no longer follows it with F_SETFD (musl 0003), which was the only thing
380
+ // marking these special files (review of dfd87b6).
325
381
  if (path === "/dev/null") {
326
- return this.allocateSpecial({
382
+ return this.markCloseOnExecNumber(this.allocateSpecial({
327
383
  read: () => new Uint8Array(),
328
384
  write: (value) => BigInt(value.byteLength),
329
385
  poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
330
386
  fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
331
- });
387
+ }), flags);
332
388
  }
333
389
  if (path === "/dev/urandom" || path === "/dev/random") {
334
- return this.allocateSpecial({
390
+ return this.markCloseOnExecNumber(this.allocateSpecial({
335
391
  read: (length) => {
336
392
  if (!Number.isSafeInteger(length) || length < 0)
337
393
  return -22n;
@@ -344,10 +400,12 @@ export class WaliFileDescriptorTable {
344
400
  write: (value) => BigInt(value.byteLength),
345
401
  poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
346
402
  fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
347
- });
403
+ }), flags);
348
404
  }
349
405
  const fs = this.fs();
350
- const exists = fs.existsSync(path);
406
+ // One lookup where the store answers ENOENT from statSync (a snapshot store's); exists-then-stat resolved it twice.
407
+ let found = fs.linkTargetSync ? statIfPresent(fs, path) : fs.existsSync(path) ? fs.statSync(path) : undefined;
408
+ const exists = found !== undefined;
351
409
  if ((flags & O_ACCMODE) !== 0 || (!exists && (flags & O_CREAT)))
352
410
  this.assertWritable(path);
353
411
  if (!exists && !(flags & O_CREAT))
@@ -358,21 +416,28 @@ export class WaliFileDescriptorTable {
358
416
  fs.writeFileSync(path, new Uint8Array());
359
417
  fs.chmodSync?.(path, mode & 0o7777);
360
418
  this.options.fileLocks?.created?.(path);
419
+ found = fs.statSync(path);
361
420
  }
362
- const stat = fs.statSync(path);
421
+ const stat = found;
363
422
  if ((flags & O_DIRECTORY) && !stat.isDirectory())
364
423
  throw errno("ENOTDIR");
365
424
  if (stat.isDirectory() && (flags & O_ACCMODE) !== 0)
366
425
  throw errno("EISDIR");
367
- if ((flags & O_TRUNC) && (flags & O_ACCMODE) !== 0 && stat.isFile())
368
- fs.writeFileSync(path, new Uint8Array());
426
+ const truncated = (flags & O_TRUNC) !== 0 && (flags & O_ACCMODE) !== 0 && stat.isFile();
427
+ if (truncated) {
428
+ if (fs.truncateSync)
429
+ fs.truncateSync(path, 0);
430
+ else
431
+ fs.writeFileSync(path, new Uint8Array());
432
+ }
369
433
  const fd = this.allocateFd();
370
- const size = stat.isFile() ? fs.statSync(path).size : 0;
434
+ const directory = stat.isDirectory();
371
435
  this.files.set(fd, {
372
436
  path,
373
437
  flags,
374
- position: flags & O_APPEND ? size : 0,
375
- directory: stat.isDirectory(),
438
+ // The stat above is the file's size unless this open emptied it.
439
+ position: flags & O_APPEND && !truncated && stat.isFile() ? stat.size : 0,
440
+ directory,
376
441
  description: this.nextDescription++,
377
442
  });
378
443
  this.descriptors.entries[fd].flags = flags & 0x80000 ? 1 : 0;
@@ -419,9 +484,21 @@ export class WaliFileDescriptorTable {
419
484
  const file = this.files.get(fd);
420
485
  if (!file)
421
486
  return -9n;
487
+ // The open description this descriptor names, which a dup shares: its
488
+ // locks go when no descriptor of this process names it any more. Asked of
489
+ // the table's count, not by walking every descriptor slot: the table's slots run to
490
+ // the highest descriptor (60,000 and up for a thread's local ones), and
491
+ // that walk was 104 ms of 184 in 1,000 small-file opens in Blender's
492
+ // Python (2026-09-27).
493
+ const shared = this.descriptors.get(fd);
422
494
  this.files.delete(fd);
423
495
  this.releasePosixLocks(file.path);
424
- if (![...this.files.values()].some((candidate) => candidate.description === file.description)) {
496
+ // This process's own descriptors, as before: the lock owners are recorded
497
+ // per process, so a count across processes (a fork's child holding the
498
+ // same description) left the owner held by nobody (review of f066d0a).
499
+ // Nothing to name when this process holds no owner's lock, which is every
500
+ // close of a file never locked: the owners' strings were most of a close.
501
+ if (this.lockingOwners.size > 0 && (!shared || !this.descriptors.names(shared.id))) {
425
502
  this.releaseLockOwner(this.lockOwner("ofd", file));
426
503
  this.releaseLockOwner(this.lockOwner("flock", file));
427
504
  }
@@ -457,9 +534,12 @@ export class WaliFileDescriptorTable {
457
534
  if ((file.flags & O_ACCMODE) === O_WRONLY)
458
535
  throw errno("EBADF");
459
536
  const fs = this.fs();
460
- const chunk = fs.readRangeSync
461
- ? fs.readRangeSync(file.path, file.position, length)
462
- : fs.readFileSync(file.path).subarray(file.position, file.position + length);
537
+ // A view where the filesystem offers one: copied into the program's memory at once, so one copy, not two.
538
+ const chunk = fs.viewRangeSync
539
+ ? fs.viewRangeSync(file.path, file.position, length)
540
+ : fs.readRangeSync
541
+ ? fs.readRangeSync(file.path, file.position, length)
542
+ : fs.readFileSync(file.path).subarray(file.position, file.position + length);
463
543
  this.copyIn(pointer, chunk);
464
544
  file.position += chunk.byteLength;
465
545
  return BigInt(chunk.byteLength);
@@ -470,13 +550,12 @@ export class WaliFileDescriptorTable {
470
550
  }
471
551
  write(fd, pointer, length) {
472
552
  try {
473
- const chunk = this.copyOut(pointer, length);
474
553
  const stdio = this.stdioFd(fd);
475
554
  if (stdio === 1 || stdio === 2)
476
- return this.options.emit(stdio, chunk);
555
+ return this.options.emit(stdio, this.copyOut(pointer, length));
477
556
  const special = this.specialDescriptors.get(fd);
478
557
  if (special)
479
- return special.write?.(chunk) ?? -9n;
558
+ return special.write?.(this.copyOut(pointer, length)) ?? -9n;
480
559
  const file = this.files.get(fd);
481
560
  if (!file)
482
561
  throw errno("EBADF");
@@ -488,10 +567,13 @@ export class WaliFileDescriptorTable {
488
567
  if (fs.writeRangeSync) {
489
568
  if (file.flags & O_APPEND)
490
569
  file.position = fs.statSync(file.path).size;
491
- fs.writeRangeSync(file.path, file.position, chunk);
492
- file.position += chunk.byteLength;
493
- return BigInt(chunk.byteLength);
570
+ // A view, not a copy: the file copies what it keeps (2026-09-27, one copy of every write fewer).
571
+ assertRange(this.options.memory(), pointer, length);
572
+ fs.writeRangeSync(file.path, file.position, this.memoryBytes().subarray(pointer, pointer + length));
573
+ file.position += length;
574
+ return BigInt(length);
494
575
  }
576
+ const chunk = this.copyOut(pointer, length);
495
577
  const current = fs.readFileSync(file.path);
496
578
  if (file.flags & O_APPEND)
497
579
  file.position = current.byteLength;
@@ -633,7 +715,13 @@ export class WaliFileDescriptorTable {
633
715
  throw errno("EBADF");
634
716
  if (!Number.isSafeInteger(length) || length < 0)
635
717
  throw errno("EINVAL");
636
- const current = this.fs().readFileSync(file.path);
718
+ const fs = this.fs();
719
+ if (fs.truncateSync) {
720
+ if (fs.statSync(file.path).size !== length)
721
+ fs.truncateSync(file.path, length);
722
+ return 0n;
723
+ }
724
+ const current = fs.readFileSync(file.path);
637
725
  if (current.byteLength === length)
638
726
  return 0n;
639
727
  const resized = new Uint8Array(length);
@@ -649,7 +737,8 @@ export class WaliFileDescriptorTable {
649
737
  try {
650
738
  const resolved = this.resolveFilesystemPath(this.resolve(path), !lstat);
651
739
  const fs = this.fs();
652
- if (!fs.existsSync(resolved))
740
+ // A snapshot store's statSync answers ENOENT itself: asking existsSync first resolved the path twice.
741
+ if (!fs.linkTargetSync && !fs.existsSync(resolved))
653
742
  throw errno("ENOENT");
654
743
  const target = lstat ? this.readSymlink(resolved) : undefined;
655
744
  writeStat(this.memoryView(), pointer, target === undefined
@@ -668,7 +757,7 @@ export class WaliFileDescriptorTable {
668
757
  const noFollow = Boolean(flags & AT_SYMLINK_NOFOLLOW);
669
758
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), !noFollow);
670
759
  const fs = this.fs();
671
- if (!fs.existsSync(resolved))
760
+ if (!fs.linkTargetSync && !fs.existsSync(resolved))
672
761
  throw errno("ENOENT");
673
762
  const target = noFollow ? this.readSymlink(resolved) : undefined;
674
763
  writeStat(this.memoryView(), pointer, target === undefined ? fs.statSync(resolved) : syntheticSymlinkStat(target));
@@ -859,7 +948,7 @@ export class WaliFileDescriptorTable {
859
948
  const path = this.resolveFilesystemPath(unresolved, !(flags & AT_SYMLINK_NOFOLLOW));
860
949
  const filesystem = this.fs();
861
950
  const stat = filesystem.statSync(path);
862
- const now = Date.now();
951
+ const now = realtimeMilliseconds();
863
952
  const readTime = (index, fallback) => {
864
953
  if (timesPointer === 0)
865
954
  return now;
@@ -1198,6 +1287,9 @@ export class WaliFileDescriptorTable {
1198
1287
  function normalizePath(cwd, path) {
1199
1288
  if (!path)
1200
1289
  return cwd;
1290
+ // Most paths a program names are already normal: itself, without the split.
1291
+ if (isNormalPath(path))
1292
+ return path;
1201
1293
  const components = [];
1202
1294
  const value = path.startsWith("/") ? path : `${cwd}/${path}`;
1203
1295
  for (const component of value.replaceAll("\\", "/").split("/")) {
@@ -1210,6 +1302,28 @@ function normalizePath(cwd, path) {
1210
1302
  }
1211
1303
  return `/${components.join("/")}`;
1212
1304
  }
1305
+ /** Absolute, no empty, `.` or `..` component, no trailing slash, and no backslash (which `normalizePath` turns into one). */
1306
+ function isNormalPath(path) {
1307
+ if (path.charCodeAt(0) !== 47)
1308
+ return false;
1309
+ if (path.length === 1)
1310
+ return true;
1311
+ let start = 1;
1312
+ for (let index = 1; index <= path.length; index += 1) {
1313
+ const code = index < path.length ? path.charCodeAt(index) : 47;
1314
+ if (code === 92)
1315
+ return false;
1316
+ if (code !== 47)
1317
+ continue;
1318
+ const length = index - start;
1319
+ if (length === 0)
1320
+ return false;
1321
+ if (path.charCodeAt(start) === 46 && (length === 1 || (length === 2 && path.charCodeAt(start + 1) === 46)))
1322
+ return false;
1323
+ start = index + 1;
1324
+ }
1325
+ return true;
1326
+ }
1213
1327
  function alignTo(value, alignment) {
1214
1328
  return Math.ceil(value / alignment) * alignment;
1215
1329
  }
@@ -1227,25 +1341,46 @@ function assertRange(memory, pointer, length) {
1227
1341
  || pointer + length > memory.buffer.byteLength)
1228
1342
  throw new WebAssembly.RuntimeError("WALI filesystem memory access is out of bounds.");
1229
1343
  }
1344
+ /** The stat of `path`, or undefined when the store says ENOENT; any other failure is thrown. */
1345
+ function statIfPresent(fs, path) {
1346
+ try {
1347
+ return fs.statSync(path);
1348
+ }
1349
+ catch (error) {
1350
+ if (/\bENOENT\b/u.test(error instanceof Error ? error.message : String(error)))
1351
+ return undefined;
1352
+ throw error;
1353
+ }
1354
+ }
1230
1355
  function writeStat(view, pointer, stat) {
1231
1356
  if (pointer < 0 || pointer + 144 > view.byteLength)
1232
1357
  throw new WebAssembly.RuntimeError("WALI stat pointer is out of bounds.");
1233
- for (let offset = 0; offset < 144; offset += 4)
1234
- view.setUint32(pointer + offset, 0, true);
1235
- view.setBigUint64(pointer, BigInt(stat.dev ?? 1), true);
1236
- view.setBigUint64(pointer + 8, BigInt(stat.ino ?? 1), true);
1237
- view.setBigUint64(pointer + 16, BigInt(stat.nlink ?? 1), true);
1358
+ // Every field written once, and the 64-bit ones as two words: a stat is
1359
+ // one of a program's commonest calls, and zeroing the struct a word at a
1360
+ // time and then allocating a BigInt per 64-bit field was a third of it
1361
+ // (2026-09-28). The padding and the reserved tail are the only bytes zeroed.
1362
+ setUint64(view, pointer, stat.dev ?? 1);
1363
+ setUint64(view, pointer + 8, stat.ino ?? 1);
1364
+ setUint64(view, pointer + 16, stat.nlink ?? 1);
1238
1365
  const kind = stat.isSymbolicLink?.() ? S_IFLNK : stat.isDirectory() ? S_IFDIR : S_IFREG;
1239
1366
  view.setUint32(pointer + 24, kind | (stat.mode ?? (stat.isDirectory() ? 0o755 : 0o644)), true);
1240
1367
  view.setUint32(pointer + 28, stat.uid ?? 1000, true);
1241
1368
  view.setUint32(pointer + 32, stat.gid ?? 1000, true);
1242
- view.setBigUint64(pointer + 40, BigInt(stat.rdev ?? 0), true);
1243
- view.setBigInt64(pointer + 48, BigInt(stat.size), true);
1244
- view.setBigInt64(pointer + 56, BigInt(stat.blksize ?? 4096), true);
1245
- view.setBigInt64(pointer + 64, BigInt(stat.blocks ?? Math.ceil(stat.size / 512)), true);
1246
- writeStatTime(view, pointer + 72, stat.atimeMs ?? stat.mtimeMs ?? Date.now());
1247
- writeStatTime(view, pointer + 88, stat.mtimeMs ?? Date.now());
1248
- writeStatTime(view, pointer + 104, stat.ctimeMs ?? stat.mtimeMs ?? Date.now());
1369
+ view.setUint32(pointer + 36, 0, true);
1370
+ setUint64(view, pointer + 40, stat.rdev ?? 0);
1371
+ setUint64(view, pointer + 48, stat.size);
1372
+ setUint64(view, pointer + 56, stat.blksize ?? 4096);
1373
+ setUint64(view, pointer + 64, stat.blocks ?? Math.ceil(stat.size / 512));
1374
+ writeStatTime(view, pointer + 72, stat.atimeMs ?? stat.mtimeMs ?? realtimeMilliseconds());
1375
+ writeStatTime(view, pointer + 88, stat.mtimeMs ?? realtimeMilliseconds());
1376
+ writeStatTime(view, pointer + 104, stat.ctimeMs ?? stat.mtimeMs ?? realtimeMilliseconds());
1377
+ for (let offset = 120; offset < 144; offset += 4)
1378
+ view.setUint32(pointer + offset, 0, true);
1379
+ }
1380
+ /** A 64-bit little-endian field from a safe integer, as two words: two's complement for a negative one, as setBigInt64 wrote it. */
1381
+ function setUint64(view, pointer, value) {
1382
+ view.setUint32(pointer, value >>> 0, true);
1383
+ view.setUint32(pointer + 4, Math.floor(value / 0x1_0000_0000) >>> 0, true);
1249
1384
  }
1250
1385
  function syntheticSymlinkStat(target) {
1251
1386
  return {
@@ -1272,9 +1407,10 @@ function writeStatfs(view, pointer) {
1272
1407
  view.setBigUint64(pointer + 72, 4096n, true);
1273
1408
  }
1274
1409
  function writeStatTime(view, pointer, milliseconds) {
1275
- const nanoseconds = BigInt(Math.floor(milliseconds * 1_000_000));
1276
- view.setBigInt64(pointer, nanoseconds / 1000000000n, true);
1277
- view.setBigInt64(pointer + 8, nanoseconds % 1000000000n, true);
1410
+ // Seconds by floor and nanoseconds in [0, 1e9), as a timespec is, before 1970 too.
1411
+ const seconds = Math.floor(milliseconds / 1_000);
1412
+ setUint64(view, pointer, seconds);
1413
+ setUint64(view, pointer + 8, Math.min(999_999_999, Math.floor((milliseconds - seconds * 1_000) * 1_000_000)));
1278
1414
  }
1279
1415
  function syntheticTerminalStat() {
1280
1416
  return {
@@ -1282,24 +1418,36 @@ function syntheticTerminalStat() {
1282
1418
  isDirectory: () => false,
1283
1419
  size: 0,
1284
1420
  mode: 0o020666,
1285
- mtimeMs: Date.now(),
1421
+ mtimeMs: realtimeMilliseconds(),
1286
1422
  };
1287
1423
  }
1424
+ /**
1425
+ * One error per errno, made once: a syscall's error is thrown to be caught
1426
+ * and turned into its number, and building an Error (its stack captured) for
1427
+ * every missing path Python probed was 34 ms of Blender's start (2026-09-27).
1428
+ */
1429
+ const strictDecoder = new TextDecoder("utf-8", { fatal: true });
1430
+ const errnoErrors = new Map();
1288
1431
  function errno(errnoName) {
1289
- return Object.assign(new Error(errnoName), { errnoName });
1432
+ let error = errnoErrors.get(errnoName);
1433
+ if (!error) {
1434
+ error = Object.assign(new Error(errnoName), { errnoName });
1435
+ errnoErrors.set(errnoName, error);
1436
+ }
1437
+ return error;
1290
1438
  }
1439
+ const ERRNO_NUMBERS = {
1440
+ EPERM: 1n, ENOENT: 2n, EIO: 5n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1441
+ EFAULT: 14n, EBUSY: 16n, EEXIST: 17n, EXDEV: 18n, ENOTDIR: 20n, EISDIR: 21n,
1442
+ EINVAL: 22n, EMFILE: 24n, ENOSPC: 28n, ESPIPE: 29n, EROFS: 30n, ERANGE: 34n,
1443
+ ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n,
1444
+ };
1291
1445
  function negativeErrno(error) {
1292
- const message = error instanceof Error ? error.message : String(error);
1293
- const name = typeof error === "object" && error !== null
1294
- ? ("errnoName" in error ? String(error.errnoName) : "code" in error ? String(error.code)
1295
- : /\b(E[A-Z]+)\b/u.exec(message)?.[1] ?? "")
1296
- : /\b(E[A-Z]+)\b/u.exec(message)?.[1] ?? "";
1297
- const code = {
1298
- EPERM: 1n, ENOENT: 2n, EIO: 5n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1299
- EFAULT: 14n, EBUSY: 16n, EEXIST: 17n, EXDEV: 18n, ENOTDIR: 20n, EISDIR: 21n,
1300
- EINVAL: 22n, EMFILE: 24n, ENOSPC: 28n, ESPIPE: 29n, EROFS: 30n, ERANGE: 34n,
1301
- ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n,
1302
- };
1446
+ // The errno's own name first, and the message parsed only for an error that has none.
1447
+ const named = typeof error === "object" && error !== null
1448
+ ? ("errnoName" in error ? String(error.errnoName) : "code" in error ? String(error.code) : undefined) : undefined;
1449
+ const name = named ?? /\b(E[A-Z]+)\b/u.exec(error instanceof Error ? error.message : String(error))?.[1] ?? "";
1450
+ const code = ERRNO_NUMBERS;
1303
1451
  if (name in code)
1304
1452
  return -code[name];
1305
1453
  if (error instanceof WebAssembly.RuntimeError || error instanceof RangeError)