@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.
- package/dist/bulk-memory.d.ts +23 -0
- package/dist/bulk-memory.d.ts.map +1 -0
- package/dist/bulk-memory.js +50 -0
- package/dist/bulk-memory.js.map +1 -0
- package/dist/coherent-file-locks.d.ts +4 -1
- package/dist/coherent-file-locks.d.ts.map +1 -1
- package/dist/coherent-file-locks.js +24 -1
- package/dist/coherent-file-locks.js.map +1 -1
- package/dist/descriptors.d.ts +18 -1
- package/dist/descriptors.d.ts.map +1 -1
- package/dist/descriptors.js +51 -6
- package/dist/descriptors.js.map +1 -1
- package/dist/filesystem-host.d.ts +81 -0
- package/dist/filesystem-host.d.ts.map +1 -0
- package/dist/filesystem-host.js +849 -0
- package/dist/filesystem-host.js.map +1 -0
- package/dist/filesystem-protocol.d.ts +49 -1
- package/dist/filesystem-protocol.d.ts.map +1 -1
- package/dist/filesystem-protocol.js +17 -0
- package/dist/filesystem-protocol.js.map +1 -1
- package/dist/filesystem-remote.d.ts +72 -18
- package/dist/filesystem-remote.d.ts.map +1 -1
- package/dist/filesystem-remote.js +271 -40
- package/dist/filesystem-remote.js.map +1 -1
- package/dist/filesystem-worker.js +8 -502
- package/dist/filesystem-worker.js.map +1 -1
- package/dist/filesystem.d.ts +9 -0
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +209 -61
- package/dist/filesystem.js.map +1 -1
- package/dist/host-idle.d.ts +29 -0
- package/dist/host-idle.d.ts.map +1 -0
- package/dist/host-idle.js +68 -0
- package/dist/host-idle.js.map +1 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +1 -3
- package/dist/host.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +79 -18
- package/dist/memory.js.map +1 -1
- package/dist/module-layout.d.ts +6 -0
- package/dist/module-layout.d.ts.map +1 -1
- package/dist/module-layout.js +26 -3
- package/dist/module-layout.js.map +1 -1
- package/dist/monotonic-clock.d.ts +9 -0
- package/dist/monotonic-clock.d.ts.map +1 -1
- package/dist/monotonic-clock.js +11 -0
- package/dist/monotonic-clock.js.map +1 -1
- package/dist/network-worker.js +7 -2
- package/dist/network-worker.js.map +1 -1
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +11 -8
- package/dist/network.js.map +1 -1
- package/dist/process-bridge.d.ts.map +1 -1
- package/dist/process-bridge.js +25 -5
- package/dist/process-bridge.js.map +1 -1
- package/dist/program.d.ts +5 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +13 -12
- package/dist/program.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +75 -9
- package/dist/runtime.js.map +1 -1
- package/dist/shared-file-layer.d.ts +69 -0
- package/dist/shared-file-layer.d.ts.map +1 -0
- package/dist/shared-file-layer.js +299 -0
- package/dist/shared-file-layer.js.map +1 -0
- package/dist/shared-pipes.d.ts +7 -0
- package/dist/shared-pipes.d.ts.map +1 -1
- package/dist/shared-pipes.js +56 -17
- package/dist/shared-pipes.js.map +1 -1
- package/dist/signals.d.ts +2 -0
- package/dist/signals.d.ts.map +1 -1
- package/dist/signals.js +5 -1
- package/dist/signals.js.map +1 -1
- package/dist/snapshot-filesystem.d.ts +36 -3
- package/dist/snapshot-filesystem.d.ts.map +1 -1
- package/dist/snapshot-filesystem.js +204 -19
- package/dist/snapshot-filesystem.js.map +1 -1
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +3 -2
- package/dist/stdin.js.map +1 -1
- package/dist/symlink.d.ts.map +1 -1
- package/dist/symlink.js +91 -1
- package/dist/symlink.js.map +1 -1
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +17 -6
- package/dist/system.js.map +1 -1
- package/dist/thread-coordinator-worker.d.ts +1 -25
- package/dist/thread-coordinator-worker.d.ts.map +1 -1
- package/dist/thread-coordinator-worker.js +29 -2
- package/dist/thread-coordinator-worker.js.map +1 -1
- package/dist/thread-worker.js +13 -8
- package/dist/thread-worker.js.map +1 -1
- package/dist/threads.d.ts +4 -0
- package/dist/threads.d.ts.map +1 -1
- package/dist/threads.js +72 -43
- package/dist/threads.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/websocket-peer.d.ts.map +1 -1
- package/dist/websocket-peer.js +2 -1
- package/dist/websocket-peer.js.map +1 -1
- package/dist/worker-program-protocol.d.ts +26 -0
- package/dist/worker-program-protocol.d.ts.map +1 -1
- package/dist/worker-program-worker.js +26 -8
- package/dist/worker-program-worker.js.map +1 -1
- package/dist/worker-program.d.ts +43 -0
- package/dist/worker-program.d.ts.map +1 -1
- package/dist/worker-program.js +198 -24
- package/dist/worker-program.js.map +1 -1
- package/package.json +1 -1
package/dist/filesystem.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
368
|
-
|
|
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
|
|
434
|
+
const directory = stat.isDirectory();
|
|
371
435
|
this.files.set(fd, {
|
|
372
436
|
path,
|
|
373
437
|
flags,
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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,
|
|
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?.(
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
view
|
|
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.
|
|
1243
|
-
view
|
|
1244
|
-
view
|
|
1245
|
-
view
|
|
1246
|
-
|
|
1247
|
-
writeStatTime(view, pointer +
|
|
1248
|
-
writeStatTime(view, pointer +
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
view
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1293
|
-
const
|
|
1294
|
-
? ("errnoName" in error ? String(error.errnoName) : "code" in error ? String(error.code)
|
|
1295
|
-
|
|
1296
|
-
|
|
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)
|