lilac-wasm-bin 0.2.0

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.
@@ -0,0 +1,705 @@
1
+ // WASI preview1 implementation (in-memory) for mruby-wasm-js.
2
+ //
3
+ // `createWasiPreview1({ env, args, stdin, fs })` returns a fresh,
4
+ // independent WASI implementation with its own state — env vars, argv,
5
+ // stdin buffer, virtual filesystem, open-fd table. Used as the default
6
+ // `wasi_snapshot_preview1` import object by index.js's `createVM`,
7
+ // but also exportable for direct use.
8
+ //
9
+ // Internal layout:
10
+ // - File / Directory: tree-VFS node classes (module-level)
11
+ // - constants: WASI errno / oflags / fdflags / filetype values
12
+ // - pure tree walkers: pathSegments / lookupFull / lookupNode /
13
+ // ensureParent / walkFiles / resolveRelative — pass `root` as arg
14
+ // - createFsFacade(root): Map-compatible facade over a Directory tree
15
+ // - createWasiPreview1(options): orchestrator that holds per-VM state
16
+ // (open fds, stdout buffer, instance ref) and returns the WASI
17
+ // imports object plus VFS handles.
18
+
19
+ import { debug } from "./debug.js";
20
+ import { createMemoryHelpers, decoder, encoder } from "./_memory.js";
21
+
22
+ // --- Module-level types ---------------------------------------------------
23
+
24
+ /** A regular-file node in the VFS. Holds raw bytes; no path stored. */
25
+ export class File {
26
+ constructor(data = new Uint8Array(0)) {
27
+ this.data = data;
28
+ }
29
+ }
30
+
31
+ /** A directory node in the VFS. `entries` maps name → File | Directory. */
32
+ export class Directory {
33
+ constructor(entries = {}) {
34
+ this.entries = entries;
35
+ }
36
+ }
37
+
38
+ // --- Constants ------------------------------------------------------------
39
+
40
+ // path_open oflags
41
+ const O_CREAT = 1;
42
+ const O_DIRECTORY = 2;
43
+ const O_EXCL = 4;
44
+ const O_TRUNC = 8;
45
+ // path_open / fd_fdstat fdflags
46
+ const FD_APPEND = 1;
47
+ // fd_seek whence
48
+ const WHENCE_SET = 0;
49
+ const WHENCE_CUR = 1;
50
+ const WHENCE_END = 2;
51
+ // preview1 errno values we actually return
52
+ const E_BADF = 8;
53
+ const E_EXIST = 20;
54
+ const E_INVAL = 28;
55
+ const E_ISDIR = 31;
56
+ const E_NOENT = 44;
57
+ const E_NOTDIR = 54;
58
+ const E_NOTEMPTY = 55;
59
+ // filetype values written into filestat records
60
+ const FILETYPE_CHARACTER_DEVICE = 2;
61
+ const FILETYPE_DIRECTORY = 3;
62
+ const FILETYPE_REGULAR_FILE = 4;
63
+
64
+ const PREOPEN_FD = 3;
65
+ const PREOPEN_PATH = "/";
66
+
67
+ // --- Pure tree walkers ----------------------------------------------------
68
+ // All take `root` (or another Directory) as an explicit argument so they
69
+ // can be unit-tested independently of WASI imports / VM state.
70
+
71
+ // Normalise an absolute path to an array of segments. Empty + "."
72
+ // segments are dropped, ".." pops the previous segment.
73
+ function pathSegments(absPath) {
74
+ const out = [];
75
+ for (const seg of absPath.split("/")) {
76
+ if (seg === "" || seg === ".") continue;
77
+ if (seg === "..") out.pop();
78
+ else out.push(seg);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function resolvePathToAbs(rel) {
84
+ return (PREOPEN_PATH + "/" + rel).replace(/\/+/g, "/");
85
+ }
86
+
87
+ // Walk an absolute path. Returns one of:
88
+ // { parent, name, node } — node is the resolved File|Directory or null if missing
89
+ // { parent: null, name: "", node: root } — root itself
90
+ // null — traversal hit a File mid-path (caller maps to E_NOTDIR)
91
+ function lookupFull(root, absPath) {
92
+ const segs = pathSegments(absPath);
93
+ if (segs.length === 0) return { parent: null, name: "", node: root };
94
+ let dir = root;
95
+ for (let i = 0; i < segs.length - 1; i++) {
96
+ const next = dir.entries[segs[i]];
97
+ if (next == null) return { parent: dir, name: segs[segs.length - 1], node: null };
98
+ if (!(next instanceof Directory)) return null;
99
+ dir = next;
100
+ }
101
+ const leaf = segs[segs.length - 1];
102
+ return { parent: dir, name: leaf, node: dir.entries[leaf] ?? null };
103
+ }
104
+
105
+ function lookupNode(root, absPath) {
106
+ const r = lookupFull(root, absPath);
107
+ return r ? r.node : null;
108
+ }
109
+
110
+ // Walk to (or create) the parent Directory of absPath. Auto-creates
111
+ // intermediate Directory nodes; throws if any intermediate is a File.
112
+ function ensureParent(root, absPath) {
113
+ const segs = pathSegments(absPath);
114
+ if (segs.length === 0) throw new Error("cannot ensure parent of root");
115
+ let dir = root;
116
+ for (let i = 0; i < segs.length - 1; i++) {
117
+ const name = segs[i];
118
+ let next = dir.entries[name];
119
+ if (next == null) {
120
+ next = new Directory();
121
+ dir.entries[name] = next;
122
+ } else if (!(next instanceof Directory)) {
123
+ throw new Error(`cannot create '${absPath}': '${name}' is a file`);
124
+ }
125
+ dir = next;
126
+ }
127
+ return { parent: dir, leaf: segs[segs.length - 1] };
128
+ }
129
+
130
+ // Walk all File leaves yielding [absolutePath, bytes] pairs.
131
+ function* walkFiles(prefix, dir) {
132
+ for (const [name, node] of Object.entries(dir.entries)) {
133
+ const path = prefix + "/" + name;
134
+ if (node instanceof File) yield [path, node.data];
135
+ else yield* walkFiles(path, node);
136
+ }
137
+ }
138
+
139
+ // Walk relPath from baseDir; "." skipped, ".." treated as "stay put"
140
+ // (we don't track parent pointers — good enough for readdir's fstatat).
141
+ function resolveRelative(baseDir, relPath) {
142
+ const segs = relPath.split("/").filter((s) => s.length > 0 && s !== ".");
143
+ let node = baseDir;
144
+ for (const seg of segs) {
145
+ if (seg === "..") continue;
146
+ if (!(node instanceof Directory)) return null;
147
+ node = node.entries[seg] ?? null;
148
+ if (!node) return null;
149
+ }
150
+ return node;
151
+ }
152
+
153
+ // --- Pure record writers --------------------------------------------------
154
+
155
+ // Write a 64-byte WASI filestat record. Only fields we care about
156
+ // (filetype, nlink, size) are filled; timestamps and dev/ino stay 0.
157
+ function writeFilestat(view, ptr, filetype, size) {
158
+ for (let i = 0; i < 64; i++) view.setUint8(ptr + i, 0);
159
+ view.setUint8(ptr + 16, filetype);
160
+ view.setBigUint64(ptr + 24, 1n, true);
161
+ view.setBigUint64(ptr + 32, BigInt(size), true);
162
+ }
163
+
164
+ // Sum the byte length across an iovec array (uint32 length lives at
165
+ // offset 4 of each 8-byte slot — ptr at offset 0, len at offset 4).
166
+ function iovsTotalLen(view, iovsPtr, iovsLen) {
167
+ let total = 0;
168
+ for (let i = 0; i < iovsLen; i++) total += view.getUint32(iovsPtr + i * 8 + 4, true);
169
+ return total;
170
+ }
171
+
172
+ // --- Public: fs Map facade factory ----------------------------------------
173
+
174
+ /**
175
+ * Build a Map-compatible facade over a `Directory` tree. Exposes
176
+ * set / get / has / delete / iteration / clear / size / Symbol.iterator
177
+ * plus `populate(dir)` and `root`. Iteration yields only File leaves
178
+ * in tree-traversal order. `set` auto-creates intermediate Directory
179
+ * nodes on demand.
180
+ */
181
+ export function createFsFacade(root) {
182
+ return {
183
+ set(path, bytes) {
184
+ const { parent, leaf } = ensureParent(root, path);
185
+ const existing = parent.entries[leaf];
186
+ if (existing instanceof Directory) {
187
+ throw new Error(`cannot set '${path}': it's a directory`);
188
+ }
189
+ if (existing instanceof File) existing.data = bytes;
190
+ else parent.entries[leaf] = new File(bytes);
191
+ return this;
192
+ },
193
+ get(path) {
194
+ const node = lookupNode(root, path);
195
+ return node instanceof File ? node.data : undefined;
196
+ },
197
+ has(path) {
198
+ return lookupNode(root, path) instanceof File;
199
+ },
200
+ delete(path) {
201
+ const r = lookupFull(root, path);
202
+ if (!r || !(r.node instanceof File) || !r.parent) return false;
203
+ delete r.parent.entries[r.name];
204
+ return true;
205
+ },
206
+ *entries() { yield* walkFiles("", root); },
207
+ *keys() { for (const [p] of this.entries()) yield p; },
208
+ *values() { for (const [, v] of this.entries()) yield v; },
209
+ [Symbol.iterator]() { return this.entries(); },
210
+ get size() { let n = 0; for (const _ of this.entries()) n++; return n; },
211
+ clear() { root.entries = {}; },
212
+ populate(dir) {
213
+ if (!(dir instanceof Directory)) throw new TypeError("fs.populate expects a Directory");
214
+ root.entries = dir.entries;
215
+ },
216
+ get root() { return root; },
217
+ };
218
+ }
219
+
220
+ // --- IO helpers (module-level, state-injected) ----------------------------
221
+ // Each helper takes an `io` state object containing exactly the mutable
222
+ // references it touches. Hoisting these out of the factory mirrors the
223
+ // "tree walker" treatment for read/write code paths and lets unit tests
224
+ // drive them with synthetic state ({ fs: mockFs, ... }) without
225
+ // instantiating wasm.
226
+
227
+ // fd_write helper: write iovec contents into the in-memory file backing
228
+ // `f`. Grows `io.fs` entry if needed; advances f.pos for non-append fds.
229
+ function writeToOpenFile({ fs }, f, view, memory, iovsPtr, iovsLen) {
230
+ const data = fs.get(f.path);
231
+ const needed = iovsTotalLen(view, iovsPtr, iovsLen);
232
+ const writeStart = f.append ? data.length : f.pos;
233
+ const newSize = Math.max(data.length, writeStart + needed);
234
+ let target = data;
235
+ if (newSize > data.length) {
236
+ target = new Uint8Array(newSize);
237
+ target.set(data);
238
+ fs.set(f.path, target);
239
+ }
240
+ let pos = writeStart;
241
+ let total = 0;
242
+ for (let i = 0; i < iovsLen; i++) {
243
+ const ptr = view.getUint32(iovsPtr + i * 8, true);
244
+ const len = view.getUint32(iovsPtr + i * 8 + 4, true);
245
+ target.set(memory.subarray(ptr, ptr + len), pos);
246
+ pos += len;
247
+ total += len;
248
+ }
249
+ if (!f.append) f.pos = pos;
250
+ return total;
251
+ }
252
+
253
+ // fd_read helper: drain bytes from the JS-side stdin buffer into iovec
254
+ // slots. Returns 0 (EOF) when buffer is empty.
255
+ function readFromStdin({ stdin }, view, memory, iovsPtr, iovsLen) {
256
+ let total = 0;
257
+ for (let i = 0; i < iovsLen; i++) {
258
+ const ptr = view.getUint32(iovsPtr + i * 8, true);
259
+ const len = view.getUint32(iovsPtr + i * 8 + 4, true);
260
+ const remaining = stdin.bytes.length;
261
+ if (remaining <= 0) break;
262
+ const n = Math.min(len, remaining);
263
+ memory.set(stdin.bytes.subarray(0, n), ptr);
264
+ stdin.bytes = stdin.bytes.subarray(n);
265
+ total += n;
266
+ }
267
+ return total;
268
+ }
269
+
270
+ // fd_read helper: drain bytes from an open file's backing array into
271
+ // iovec slots. Advances f.pos. Returns -1 if the path was removed under
272
+ // us (caller maps to E_BADF).
273
+ function readFromOpenFile({ fs }, f, view, memory, iovsPtr, iovsLen) {
274
+ const data = fs.get(f.path);
275
+ if (!data) return -1;
276
+ let total = 0;
277
+ for (let i = 0; i < iovsLen; i++) {
278
+ const ptr = view.getUint32(iovsPtr + i * 8, true);
279
+ const len = view.getUint32(iovsPtr + i * 8 + 4, true);
280
+ const remaining = data.length - f.pos;
281
+ if (remaining <= 0) break;
282
+ const n = Math.min(len, remaining);
283
+ memory.set(data.subarray(f.pos, f.pos + n), ptr);
284
+ f.pos += n;
285
+ total += n;
286
+ }
287
+ return total;
288
+ }
289
+
290
+ // fd_write helper: drain iovec contents into a line-buffered console.
291
+ // fd 1/2 (and any unknown fd that isn't a tracked file) lands here so
292
+ // `puts` from mruby reaches console.log without spawning a real tty.
293
+ function writeToStdio({ stdoutBuffer }, view, memory, iovsPtr, iovsLen) {
294
+ let total = 0;
295
+ for (let i = 0; i < iovsLen; i++) {
296
+ const ptr = view.getUint32(iovsPtr + i * 8, true);
297
+ const len = view.getUint32(iovsPtr + i * 8 + 4, true);
298
+ const str = decoder.decode(memory.subarray(ptr, ptr + len));
299
+ stdoutBuffer.push(str);
300
+ if (str.includes("\n")) {
301
+ const joined = stdoutBuffer.join("");
302
+ stdoutBuffer.length = 0;
303
+ for (const line of joined.split("\n")) {
304
+ if (line.length > 0) console.log("[mruby]", line);
305
+ }
306
+ }
307
+ total += len;
308
+ }
309
+ return total;
310
+ }
311
+
312
+ // --- Stdin helper (module-level) ------------------------------------------
313
+
314
+ function makeStdin(initial) {
315
+ let bytes;
316
+ if (initial == null) bytes = new Uint8Array(0);
317
+ else if (typeof initial === "string") bytes = encoder.encode(initial);
318
+ else if (initial instanceof Uint8Array) bytes = initial;
319
+ else throw new TypeError("stdin must be a string, Uint8Array, or undefined");
320
+ return {
321
+ bytes,
322
+ pushText(text) {
323
+ const add = encoder.encode(text);
324
+ const merged = new Uint8Array(this.bytes.length + add.length);
325
+ merged.set(this.bytes);
326
+ merged.set(add, this.bytes.length);
327
+ this.bytes = merged;
328
+ },
329
+ };
330
+ }
331
+
332
+ // --- WASI factory ---------------------------------------------------------
333
+
334
+ /**
335
+ * Build a fresh WASI preview1 implementation. Each call gets independent
336
+ * state (env, args, stdin, fs, open-fd table, stdout buffering).
337
+ *
338
+ * @returns {{
339
+ * imports: object,
340
+ * bindInstance: (inst: WebAssembly.Instance) => void,
341
+ * env: Record<string, string>,
342
+ * args: string[],
343
+ * stdin: { bytes: Uint8Array, pushText: (s: string) => void },
344
+ * fs: object,
345
+ * }}
346
+ */
347
+ export function createWasiPreview1(options = {}) {
348
+ const env = { ...(options.env ?? {}) };
349
+ const args = [...(options.args ?? ["mruby-wasm-js"])];
350
+ const stdin = makeStdin(options.stdin);
351
+ const root = options.fs instanceof Directory ? options.fs : new Directory();
352
+ const fs = createFsFacade(root);
353
+
354
+ let nextFileFd = 4;
355
+ // fd → { type: 'file', path, pos, append } | { type: 'dir', node }
356
+ const openFiles = new Map();
357
+ const stdoutBuffer = [];
358
+ let instance = null;
359
+
360
+ const { readUtf8 } = createMemoryHelpers(() => instance);
361
+
362
+ // Single state bag passed to module-level IO helpers below. One
363
+ // allocation up-front instead of per-call object literals.
364
+ const io = { fs, stdin, stdoutBuffer };
365
+
366
+ // --- WASI imports ------------------------------------------------------
367
+ const imports = {
368
+ fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
369
+ const view = new DataView(instance.exports.memory.buffer);
370
+ const memory = new Uint8Array(instance.exports.memory.buffer);
371
+ const f = openFiles.get(fd);
372
+ if (f && f.type === "dir") return E_ISDIR;
373
+ const total = f
374
+ ? writeToOpenFile(io, f, view, memory, iovsPtr, iovsLen)
375
+ : writeToStdio(io, view, memory, iovsPtr, iovsLen);
376
+ view.setUint32(nwrittenPtr, total, true);
377
+ return 0;
378
+ },
379
+ fd_close(fd) {
380
+ if (openFiles.has(fd)) openFiles.delete(fd);
381
+ return 0;
382
+ },
383
+ fd_seek(fd, offset, whence, newOffsetPtr) {
384
+ const f = openFiles.get(fd);
385
+ if (!f || f.type !== "file") return E_BADF;
386
+ const data = fs.get(f.path);
387
+ if (!data) return E_BADF;
388
+ const off = Number(offset);
389
+ let newPos;
390
+ switch (whence) {
391
+ case WHENCE_SET: newPos = off; break;
392
+ case WHENCE_CUR: newPos = f.pos + off; break;
393
+ case WHENCE_END: newPos = data.length + off; break;
394
+ default: return E_INVAL;
395
+ }
396
+ if (newPos < 0) return E_INVAL;
397
+ f.pos = newPos;
398
+ const view = new DataView(instance.exports.memory.buffer);
399
+ view.setBigUint64(newOffsetPtr, BigInt(newPos), true);
400
+ return 0;
401
+ },
402
+ fd_tell(fd, ptr) {
403
+ const f = openFiles.get(fd);
404
+ if (!f || f.type !== "file") return E_BADF;
405
+ const view = new DataView(instance.exports.memory.buffer);
406
+ view.setBigUint64(ptr, BigInt(f.pos), true);
407
+ return 0;
408
+ },
409
+ fd_fdstat_get(_fd, fdstatPtr) {
410
+ const view = new DataView(instance.exports.memory.buffer);
411
+ for (let off = 0; off < 24; off++) view.setUint8(fdstatPtr + off, 0);
412
+ return 0;
413
+ },
414
+ fd_fdstat_set_flags(_fd, _flags) { return 0; },
415
+ fd_filestat_get(fd, ptr) {
416
+ const view = new DataView(instance.exports.memory.buffer);
417
+ const f = openFiles.get(fd);
418
+ if (f && f.type === "file") {
419
+ const data = fs.get(f.path);
420
+ if (!data) return E_BADF;
421
+ writeFilestat(view, ptr, FILETYPE_REGULAR_FILE, data.length);
422
+ return 0;
423
+ }
424
+ if (f && f.type === "dir") {
425
+ writeFilestat(view, ptr, FILETYPE_DIRECTORY, 0);
426
+ return 0;
427
+ }
428
+ if (fd === 0 || fd === 1 || fd === 2) {
429
+ writeFilestat(view, ptr, FILETYPE_CHARACTER_DEVICE, 0);
430
+ return 0;
431
+ }
432
+ return E_BADF;
433
+ },
434
+ fd_prestat_get(fd, ptr) {
435
+ if (fd !== PREOPEN_FD) return E_BADF;
436
+ const view = new DataView(instance.exports.memory.buffer);
437
+ const nameBytes = encoder.encode(PREOPEN_PATH);
438
+ view.setUint8(ptr, 0);
439
+ view.setUint32(ptr + 4, nameBytes.length, true);
440
+ return 0;
441
+ },
442
+ fd_prestat_dir_name(fd, ptr, len) {
443
+ if (fd !== PREOPEN_FD) return E_BADF;
444
+ const memory = new Uint8Array(instance.exports.memory.buffer);
445
+ const nameBytes = encoder.encode(PREOPEN_PATH);
446
+ const n = Math.min(nameBytes.length, len);
447
+ memory.set(nameBytes.subarray(0, n), ptr);
448
+ return 0;
449
+ },
450
+ fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
451
+ if (debug.trace) console.log(`[wasi] fd_read fd=${fd} iovsLen=${iovsLen}`);
452
+ const view = new DataView(instance.exports.memory.buffer);
453
+ const memory = new Uint8Array(instance.exports.memory.buffer);
454
+ let total;
455
+ if (fd === 0) {
456
+ total = readFromStdin(io, view, memory, iovsPtr, iovsLen);
457
+ } else {
458
+ const f = openFiles.get(fd);
459
+ if (!f || f.type !== "file") return E_BADF;
460
+ total = readFromOpenFile(io, f, view, memory, iovsPtr, iovsLen);
461
+ if (total < 0) return E_BADF;
462
+ }
463
+ view.setUint32(nreadPtr, total, true);
464
+ return 0;
465
+ },
466
+ fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr) {
467
+ const f = openFiles.get(fd);
468
+ if (!f || f.type !== "dir") return E_BADF;
469
+ const view = new DataView(instance.exports.memory.buffer);
470
+ const memory = new Uint8Array(instance.exports.memory.buffer);
471
+ const realNames = Object.keys(f.node.entries);
472
+ const all = [".", "..", ...realNames];
473
+ let bufPos = 0;
474
+ for (let i = Number(cookie); i < all.length; i++) {
475
+ const name = all[i];
476
+ const nameBytes = encoder.encode(name);
477
+ const recordSize = 24 + nameBytes.length;
478
+ if (bufPos + recordSize > bufLen) break;
479
+ for (let j = 0; j < 24; j++) view.setUint8(bufPtr + bufPos + j, 0);
480
+ view.setBigUint64(bufPtr + bufPos, BigInt(i + 1), true);
481
+ view.setBigUint64(bufPtr + bufPos + 8, BigInt(i + 1), true);
482
+ view.setUint32(bufPtr + bufPos + 16, nameBytes.length, true);
483
+ const child = i < 2 ? f.node : f.node.entries[name];
484
+ view.setUint8(bufPtr + bufPos + 20,
485
+ child instanceof Directory ? FILETYPE_DIRECTORY : FILETYPE_REGULAR_FILE);
486
+ memory.set(nameBytes, bufPtr + bufPos + 24);
487
+ bufPos += recordSize;
488
+ }
489
+ view.setUint32(bufusedPtr, bufPos, true);
490
+ return 0;
491
+ },
492
+ path_open(dirfd, _dirflags, pathPtr, pathLen,
493
+ oflags, _rightsBase, _rightsInh, fdflags, fdPtr) {
494
+ if (debug.trace) console.log(`[wasi] path_open dirfd=${dirfd} path="${readUtf8(pathPtr, pathLen)}" oflags=${oflags} fdflags=${fdflags}`);
495
+ if (dirfd !== PREOPEN_FD) return E_BADF;
496
+ const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
497
+ const directory = !!(oflags & O_DIRECTORY);
498
+ const create = !!(oflags & O_CREAT);
499
+ const excl = !!(oflags & O_EXCL);
500
+ const trunc = !!(oflags & O_TRUNC);
501
+ const append = !!(fdflags & FD_APPEND);
502
+ const node = lookupNode(root, fullPath);
503
+
504
+ if (directory) {
505
+ if (!node) return E_NOENT;
506
+ if (!(node instanceof Directory)) return E_NOTDIR;
507
+ const fd = nextFileFd++;
508
+ openFiles.set(fd, { type: "dir", node });
509
+ const view = new DataView(instance.exports.memory.buffer);
510
+ view.setUint32(fdPtr, fd, true);
511
+ return 0;
512
+ }
513
+
514
+ if (node instanceof Directory) return E_ISDIR;
515
+ const exists = node instanceof File;
516
+ if (excl && exists) return E_EXIST;
517
+ if (!exists && !create) return E_NOENT;
518
+ if (!exists || trunc) fs.set(fullPath, new Uint8Array(0));
519
+
520
+ const data = fs.get(fullPath);
521
+ const fd = nextFileFd++;
522
+ openFiles.set(fd, { type: "file", path: fullPath, pos: append ? data.length : 0, append });
523
+ const view = new DataView(instance.exports.memory.buffer);
524
+ view.setUint32(fdPtr, fd, true);
525
+ return 0;
526
+ },
527
+ path_filestat_get(dirfd, _flags, pathPtr, pathLen, ptr) {
528
+ let baseDir;
529
+ if (dirfd === PREOPEN_FD) {
530
+ baseDir = root;
531
+ } else {
532
+ const f = openFiles.get(dirfd);
533
+ if (!f || f.type !== "dir") return E_BADF;
534
+ baseDir = f.node;
535
+ }
536
+ const node = resolveRelative(baseDir, readUtf8(pathPtr, pathLen));
537
+ if (!node) return E_NOENT;
538
+ const view = new DataView(instance.exports.memory.buffer);
539
+ if (node instanceof Directory) writeFilestat(view, ptr, FILETYPE_DIRECTORY, 0);
540
+ else writeFilestat(view, ptr, FILETYPE_REGULAR_FILE, node.data.length);
541
+ return 0;
542
+ },
543
+ proc_exit(code) {
544
+ console.log("[mruby] proc_exit", code);
545
+ },
546
+ environ_sizes_get(countPtr, sizesPtr) {
547
+ const view = new DataView(instance.exports.memory.buffer);
548
+ const entries = Object.entries(env);
549
+ let totalSize = 0;
550
+ for (const [k, v] of entries) totalSize += encoder.encode(`${k}=${v}\0`).length;
551
+ view.setUint32(countPtr, entries.length, true);
552
+ view.setUint32(sizesPtr, totalSize, true);
553
+ return 0;
554
+ },
555
+ environ_get(envPtr, bufPtr) {
556
+ const view = new DataView(instance.exports.memory.buffer);
557
+ const memory = new Uint8Array(instance.exports.memory.buffer);
558
+ let offset = bufPtr;
559
+ Object.entries(env).forEach(([k, v], i) => {
560
+ view.setUint32(envPtr + i * 4, offset, true);
561
+ const bytes = encoder.encode(`${k}=${v}\0`);
562
+ memory.set(bytes, offset);
563
+ offset += bytes.length;
564
+ });
565
+ return 0;
566
+ },
567
+ args_sizes_get(countPtr, sizesPtr) {
568
+ const view = new DataView(instance.exports.memory.buffer);
569
+ let totalSize = 0;
570
+ for (const a of args) totalSize += encoder.encode(`${a}\0`).length;
571
+ view.setUint32(countPtr, args.length, true);
572
+ view.setUint32(sizesPtr, totalSize, true);
573
+ return 0;
574
+ },
575
+ args_get(argvPtr, bufPtr) {
576
+ const view = new DataView(instance.exports.memory.buffer);
577
+ const memory = new Uint8Array(instance.exports.memory.buffer);
578
+ let offset = bufPtr;
579
+ args.forEach((a, i) => {
580
+ view.setUint32(argvPtr + i * 4, offset, true);
581
+ const bytes = encoder.encode(`${a}\0`);
582
+ memory.set(bytes, offset);
583
+ offset += bytes.length;
584
+ });
585
+ return 0;
586
+ },
587
+ clock_time_get(id, _precision, ptr) {
588
+ const view = new DataView(instance.exports.memory.buffer);
589
+ let nanos;
590
+ if (id === 0) nanos = BigInt(Math.floor(Date.now() * 1e6));
591
+ else {
592
+ const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
593
+ nanos = BigInt(Math.floor(now * 1e6));
594
+ }
595
+ view.setBigUint64(ptr, nanos, true);
596
+ return 0;
597
+ },
598
+ clock_res_get(_id, ptr) {
599
+ const view = new DataView(instance.exports.memory.buffer);
600
+ view.setBigUint64(ptr, 1_000_000n, true);
601
+ return 0;
602
+ },
603
+ random_get(ptr, len) {
604
+ const memory = new Uint8Array(instance.exports.memory.buffer, ptr, len);
605
+ for (let off = 0; off < len; off += 65536) {
606
+ const slice = memory.subarray(off, Math.min(off + 65536, len));
607
+ crypto.getRandomValues(slice);
608
+ }
609
+ return 0;
610
+ },
611
+ fd_filestat_set_size(fd, size) {
612
+ const f = openFiles.get(fd);
613
+ if (!f || f.type !== "file") return E_BADF;
614
+ const data = fs.get(f.path);
615
+ if (!data) return E_BADF;
616
+ const newSize = Number(size);
617
+ if (newSize < 0) return E_INVAL;
618
+ if (newSize < data.length) fs.set(f.path, data.slice(0, newSize));
619
+ else if (newSize > data.length) {
620
+ const grown = new Uint8Array(newSize);
621
+ grown.set(data);
622
+ fs.set(f.path, grown);
623
+ }
624
+ return 0;
625
+ },
626
+ fd_pwrite(fd, iovsPtr, iovsLen, offset, nwrittenPtr) {
627
+ const f = openFiles.get(fd);
628
+ if (!f || f.type !== "file") return E_BADF;
629
+ const view = new DataView(instance.exports.memory.buffer);
630
+ const memory = new Uint8Array(instance.exports.memory.buffer);
631
+ const pseudo = { path: f.path, pos: Number(offset), append: false };
632
+ const total = writeToOpenFile(io, pseudo, view, memory, iovsPtr, iovsLen);
633
+ view.setUint32(nwrittenPtr, total, true);
634
+ return 0;
635
+ },
636
+ path_unlink_file(dirfd, pathPtr, pathLen) {
637
+ if (dirfd !== PREOPEN_FD) return E_BADF;
638
+ const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
639
+ const node = lookupNode(root, fullPath);
640
+ if (!node) return E_NOENT;
641
+ if (node instanceof Directory) return E_ISDIR;
642
+ fs.delete(fullPath);
643
+ return 0;
644
+ },
645
+ path_create_directory(dirfd, pathPtr, pathLen) {
646
+ if (dirfd !== PREOPEN_FD) return E_BADF;
647
+ const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
648
+ const segs = pathSegments(fullPath);
649
+ if (segs.length === 0) return E_EXIST;
650
+ let dir = root;
651
+ for (let i = 0; i < segs.length - 1; i++) {
652
+ const next = dir.entries[segs[i]];
653
+ if (next == null) return E_NOENT;
654
+ if (!(next instanceof Directory)) return E_NOTDIR;
655
+ dir = next;
656
+ }
657
+ const leaf = segs[segs.length - 1];
658
+ if (dir.entries[leaf] != null) return E_EXIST;
659
+ dir.entries[leaf] = new Directory();
660
+ return 0;
661
+ },
662
+ path_remove_directory(dirfd, pathPtr, pathLen) {
663
+ if (dirfd !== PREOPEN_FD) return E_BADF;
664
+ const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
665
+ const r = lookupFull(root, fullPath);
666
+ if (!r || r.node == null) return E_NOENT;
667
+ if (!(r.node instanceof Directory)) return E_NOTDIR;
668
+ if (!r.parent) return E_INVAL;
669
+ if (Object.keys(r.node.entries).length > 0) return E_NOTEMPTY;
670
+ delete r.parent.entries[r.name];
671
+ return 0;
672
+ },
673
+ fd_filestat_set_times(_fd, _atim, _mtim, _flags) { return E_INVAL; },
674
+ fd_pread(_fd, _iovs, _iovsLen, _offset, _nreadPtr) { return E_INVAL; },
675
+ fd_renumber(_from, _to) { return E_INVAL; },
676
+ fd_sync(_fd) { return 0; },
677
+ fd_advise(_fd, _offset, _len, _advice) { return 0; },
678
+ fd_allocate(_fd, _offset, _len) { return E_INVAL; },
679
+ fd_datasync(_fd) { return 0; },
680
+ path_filestat_set_times() { return E_INVAL; },
681
+ path_link() { return E_INVAL; },
682
+ path_readlink() { return E_INVAL; },
683
+ path_rename(oldDirfd, oldPathPtr, oldPathLen, newDirfd, newPathPtr, newPathLen) {
684
+ if (oldDirfd !== PREOPEN_FD || newDirfd !== PREOPEN_FD) return E_BADF;
685
+ const oldPath = resolvePathToAbs(readUtf8(oldPathPtr, oldPathLen));
686
+ const newPath = resolvePathToAbs(readUtf8(newPathPtr, newPathLen));
687
+ if (!fs.has(oldPath)) return E_NOENT;
688
+ fs.set(newPath, fs.get(oldPath));
689
+ fs.delete(oldPath);
690
+ return 0;
691
+ },
692
+ path_symlink() { return E_INVAL; },
693
+ poll_oneoff() { return E_INVAL; },
694
+ sched_yield() { return 0; },
695
+ };
696
+
697
+ return {
698
+ imports,
699
+ bindInstance(inst) { instance = inst; },
700
+ env,
701
+ args,
702
+ stdin,
703
+ fs,
704
+ };
705
+ }