@oh-my-pi/pi-natives 9.3.1 → 9.6.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.
package/src/wasix.ts DELETED
@@ -1,1745 +0,0 @@
1
- /**
2
- * WASI Preview 1 Implementation
3
- *
4
- * A non-sandboxed, spec-accurate WASI implementation for TypeScript/Bun.
5
- * Preopens "/" (Unix) or all drive letters (Windows) for full filesystem access.
6
- *
7
- * Reference: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md
8
- */
9
-
10
- import * as crypto from "node:crypto";
11
- import * as fs from "node:fs";
12
- import * as os from "node:os";
13
- import * as path from "node:path";
14
- import * as tty from "node:tty";
15
-
16
- // =============================================================================
17
- // WASI Error Codes (errno)
18
- // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-errno-variant
19
- // =============================================================================
20
-
21
- export const enum Errno {
22
- /** No error occurred. System call completed successfully. */
23
- SUCCESS = 0,
24
- /** Argument list too long. */
25
- E2BIG = 1,
26
- /** Permission denied. */
27
- EACCES = 2,
28
- /** Address in use. */
29
- EADDRINUSE = 3,
30
- /** Address not available. */
31
- EADDRNOTAVAIL = 4,
32
- /** Address family not supported. */
33
- EAFNOSUPPORT = 5,
34
- /** Resource unavailable, or operation would block. */
35
- EAGAIN = 6,
36
- /** Connection already in progress. */
37
- EALREADY = 7,
38
- /** Bad file descriptor. */
39
- EBADF = 8,
40
- /** Bad message. */
41
- EBADMSG = 9,
42
- /** Device or resource busy. */
43
- EBUSY = 10,
44
- /** Operation canceled. */
45
- ECANCELED = 11,
46
- /** No child processes. */
47
- ECHILD = 12,
48
- /** Connection aborted. */
49
- ECONNABORTED = 13,
50
- /** Connection refused. */
51
- ECONNREFUSED = 14,
52
- /** Connection reset. */
53
- ECONNRESET = 15,
54
- /** Resource deadlock would occur. */
55
- EDEADLK = 16,
56
- /** Destination address required. */
57
- EDESTADDRREQ = 17,
58
- /** Mathematics argument out of domain of function. */
59
- EDOM = 18,
60
- /** Reserved. */
61
- EDQUOT = 19,
62
- /** File exists. */
63
- EEXIST = 20,
64
- /** Bad address. */
65
- EFAULT = 21,
66
- /** File too large. */
67
- EFBIG = 22,
68
- /** Host is unreachable. */
69
- EHOSTUNREACH = 23,
70
- /** Identifier removed. */
71
- EIDRM = 24,
72
- /** Illegal byte sequence. */
73
- EILSEQ = 25,
74
- /** Operation in progress. */
75
- EINPROGRESS = 26,
76
- /** Interrupted function. */
77
- EINTR = 27,
78
- /** Invalid argument. */
79
- EINVAL = 28,
80
- /** I/O error. */
81
- EIO = 29,
82
- /** Socket is connected. */
83
- EISCONN = 30,
84
- /** Is a directory. */
85
- EISDIR = 31,
86
- /** Too many levels of symbolic links. */
87
- ELOOP = 32,
88
- /** File descriptor value too large. */
89
- EMFILE = 33,
90
- /** Too many links. */
91
- EMLINK = 34,
92
- /** Message too large. */
93
- EMSGSIZE = 35,
94
- /** Reserved. */
95
- EMULTIHOP = 36,
96
- /** Filename too long. */
97
- ENAMETOOLONG = 37,
98
- /** Network is down. */
99
- ENETDOWN = 38,
100
- /** Connection aborted by network. */
101
- ENETRESET = 39,
102
- /** Network unreachable. */
103
- ENETUNREACH = 40,
104
- /** Too many files open in system. */
105
- ENFILE = 41,
106
- /** No buffer space available. */
107
- ENOBUFS = 42,
108
- /** No such device. */
109
- ENODEV = 43,
110
- /** No such file or directory. */
111
- ENOENT = 44,
112
- /** Executable file format error. */
113
- ENOEXEC = 45,
114
- /** No locks available. */
115
- ENOLCK = 46,
116
- /** Reserved. */
117
- ENOLINK = 47,
118
- /** Not enough space. */
119
- ENOMEM = 48,
120
- /** No message of the desired type. */
121
- ENOMSG = 49,
122
- /** Protocol not available. */
123
- ENOPROTOOPT = 50,
124
- /** No space left on device. */
125
- ENOSPC = 51,
126
- /** Function not supported. */
127
- ENOSYS = 52,
128
- /** The socket is not connected. */
129
- ENOTCONN = 53,
130
- /** Not a directory or a symbolic link to a directory. */
131
- ENOTDIR = 54,
132
- /** Directory not empty. */
133
- ENOTEMPTY = 55,
134
- /** State not recoverable. */
135
- ENOTRECOVERABLE = 56,
136
- /** Not a socket. */
137
- ENOTSOCK = 57,
138
- /** Not supported, or operation not supported on socket. */
139
- ENOTSUP = 58,
140
- /** Inappropriate I/O control operation. */
141
- ENOTTY = 59,
142
- /** No such device or address. */
143
- ENXIO = 60,
144
- /** Value too large to be stored in data type. */
145
- EOVERFLOW = 61,
146
- /** Previous owner died. */
147
- EOWNERDEAD = 62,
148
- /** Operation not permitted. */
149
- EPERM = 63,
150
- /** Broken pipe. */
151
- EPIPE = 64,
152
- /** Protocol error. */
153
- EPROTO = 65,
154
- /** Protocol not supported. */
155
- EPROTONOSUPPORT = 66,
156
- /** Protocol wrong type for socket. */
157
- EPROTOTYPE = 67,
158
- /** Result too large. */
159
- ERANGE = 68,
160
- /** Read-only file system. */
161
- EROFS = 69,
162
- /** Invalid seek. */
163
- ESPIPE = 70,
164
- /** No such process. */
165
- ESRCH = 71,
166
- /** Reserved. */
167
- ESTALE = 72,
168
- /** Connection timed out. */
169
- ETIMEDOUT = 73,
170
- /** Text file busy. */
171
- ETXTBSY = 74,
172
- /** Cross-device link. */
173
- EXDEV = 75,
174
- /** Extension: Capabilities insufficient. */
175
- ENOTCAPABLE = 76,
176
- }
177
-
178
- /** Map Node.js error codes to WASI errno */
179
- const NODE_ERROR_MAP: Record<string, Errno> = {
180
- E2BIG: Errno.E2BIG,
181
- EACCES: Errno.EACCES,
182
- EADDRINUSE: Errno.EADDRINUSE,
183
- EADDRNOTAVAIL: Errno.EADDRNOTAVAIL,
184
- EAFNOSUPPORT: Errno.EAFNOSUPPORT,
185
- EAGAIN: Errno.EAGAIN,
186
- EALREADY: Errno.EALREADY,
187
- EBADF: Errno.EBADF,
188
- EBADMSG: Errno.EBADMSG,
189
- EBUSY: Errno.EBUSY,
190
- ECANCELED: Errno.ECANCELED,
191
- ECHILD: Errno.ECHILD,
192
- ECONNABORTED: Errno.ECONNABORTED,
193
- ECONNREFUSED: Errno.ECONNREFUSED,
194
- ECONNRESET: Errno.ECONNRESET,
195
- EDEADLK: Errno.EDEADLK,
196
- EDEADLOCK: Errno.EDEADLK,
197
- EDESTADDRREQ: Errno.EDESTADDRREQ,
198
- EDOM: Errno.EDOM,
199
- EDQUOT: Errno.EDQUOT,
200
- EEXIST: Errno.EEXIST,
201
- EFAULT: Errno.EFAULT,
202
- EFBIG: Errno.EFBIG,
203
- EHOSTDOWN: Errno.EHOSTUNREACH,
204
- EHOSTUNREACH: Errno.EHOSTUNREACH,
205
- EIDRM: Errno.EIDRM,
206
- EILSEQ: Errno.EILSEQ,
207
- EINPROGRESS: Errno.EINPROGRESS,
208
- EINTR: Errno.EINTR,
209
- EINVAL: Errno.EINVAL,
210
- EIO: Errno.EIO,
211
- EISCONN: Errno.EISCONN,
212
- EISDIR: Errno.EISDIR,
213
- ELOOP: Errno.ELOOP,
214
- EMFILE: Errno.EMFILE,
215
- EMLINK: Errno.EMLINK,
216
- EMSGSIZE: Errno.EMSGSIZE,
217
- EMULTIHOP: Errno.EMULTIHOP,
218
- ENAMETOOLONG: Errno.ENAMETOOLONG,
219
- ENETDOWN: Errno.ENETDOWN,
220
- ENETRESET: Errno.ENETRESET,
221
- ENETUNREACH: Errno.ENETUNREACH,
222
- ENFILE: Errno.ENFILE,
223
- ENOBUFS: Errno.ENOBUFS,
224
- ENODEV: Errno.ENODEV,
225
- ENOENT: Errno.ENOENT,
226
- ENOEXEC: Errno.ENOEXEC,
227
- ENOLCK: Errno.ENOLCK,
228
- ENOLINK: Errno.ENOLINK,
229
- ENOMEM: Errno.ENOMEM,
230
- ENOMSG: Errno.ENOMSG,
231
- ENOPROTOOPT: Errno.ENOPROTOOPT,
232
- ENOSPC: Errno.ENOSPC,
233
- ENOSYS: Errno.ENOSYS,
234
- ENOTCONN: Errno.ENOTCONN,
235
- ENOTDIR: Errno.ENOTDIR,
236
- ENOTEMPTY: Errno.ENOTEMPTY,
237
- ENOTRECOVERABLE: Errno.ENOTRECOVERABLE,
238
- ENOTSOCK: Errno.ENOTSOCK,
239
- ENOTSUP: Errno.ENOTSUP,
240
- ENOTTY: Errno.ENOTTY,
241
- ENXIO: Errno.ENXIO,
242
- EOPNOTSUPP: Errno.ENOTSUP,
243
- EOVERFLOW: Errno.EOVERFLOW,
244
- EOWNERDEAD: Errno.EOWNERDEAD,
245
- EPERM: Errno.EPERM,
246
- EPIPE: Errno.EPIPE,
247
- EPROTO: Errno.EPROTO,
248
- EPROTONOSUPPORT: Errno.EPROTONOSUPPORT,
249
- EPROTOTYPE: Errno.EPROTOTYPE,
250
- ERANGE: Errno.ERANGE,
251
- EROFS: Errno.EROFS,
252
- ESPIPE: Errno.ESPIPE,
253
- ESRCH: Errno.ESRCH,
254
- ESTALE: Errno.ESTALE,
255
- ETIMEDOUT: Errno.ETIMEDOUT,
256
- ETXTBSY: Errno.ETXTBSY,
257
- EWOULDBLOCK: Errno.EAGAIN,
258
- EXDEV: Errno.EXDEV,
259
- };
260
-
261
- const INVERSE_ERROR_MAP: string[] = [];
262
- for (const [key, value] of Object.entries(NODE_ERROR_MAP)) {
263
- INVERSE_ERROR_MAP[value as number] = key;
264
- }
265
-
266
- function getErrnoName(errno: Errno): string {
267
- if (errno < 0 || errno > INVERSE_ERROR_MAP.length) {
268
- return "UNKNOWN";
269
- }
270
- return INVERSE_ERROR_MAP[errno];
271
- }
272
-
273
- // =============================================================================
274
- // WASI Clock IDs
275
- // =============================================================================
276
-
277
- export const enum ClockId {
278
- /** Wall clock time. */
279
- REALTIME = 0,
280
- /** Monotonic clock for measuring elapsed time. */
281
- MONOTONIC = 1,
282
- /** CPU-time clock for the current process. */
283
- PROCESS_CPUTIME_ID = 2,
284
- /** CPU-time clock for the current thread. */
285
- THREAD_CPUTIME_ID = 3,
286
- }
287
-
288
- // =============================================================================
289
- // WASI File Types
290
- // =============================================================================
291
-
292
- export const enum FileType {
293
- /** The type of the file descriptor is unknown. */
294
- UNKNOWN = 0,
295
- /** The file descriptor refers to a block device. */
296
- BLOCK_DEVICE = 1,
297
- /** The file descriptor refers to a character device. */
298
- CHARACTER_DEVICE = 2,
299
- /** The file descriptor refers to a directory. */
300
- DIRECTORY = 3,
301
- /** The file descriptor refers to a regular file. */
302
- REGULAR_FILE = 4,
303
- /** The file descriptor refers to a datagram socket. */
304
- SOCKET_DGRAM = 5,
305
- /** The file descriptor refers to a stream socket. */
306
- SOCKET_STREAM = 6,
307
- /** The file descriptor refers to a symbolic link. */
308
- SYMBOLIC_LINK = 7,
309
- }
310
-
311
- // =============================================================================
312
- // WASI FD Flags
313
- // =============================================================================
314
-
315
- export const enum FdFlags {
316
- /** Append mode: Data written to the file is always appended. */
317
- APPEND = 1 << 0,
318
- /** Write according to synchronized I/O data integrity completion. */
319
- DSYNC = 1 << 1,
320
- /** Non-blocking mode. */
321
- NONBLOCK = 1 << 2,
322
- /** Synchronized read I/O operations. */
323
- RSYNC = 1 << 3,
324
- /** Write according to synchronized I/O file integrity completion. */
325
- SYNC = 1 << 4,
326
- }
327
-
328
- // =============================================================================
329
- // WASI Rights
330
- // =============================================================================
331
-
332
- export const Rights = {
333
- FD_DATASYNC: 1n << 0n,
334
- FD_READ: 1n << 1n,
335
- FD_SEEK: 1n << 2n,
336
- FD_FDSTAT_SET_FLAGS: 1n << 3n,
337
- FD_SYNC: 1n << 4n,
338
- FD_TELL: 1n << 5n,
339
- FD_WRITE: 1n << 6n,
340
- FD_ADVISE: 1n << 7n,
341
- FD_ALLOCATE: 1n << 8n,
342
- PATH_CREATE_DIRECTORY: 1n << 9n,
343
- PATH_CREATE_FILE: 1n << 10n,
344
- PATH_LINK_SOURCE: 1n << 11n,
345
- PATH_LINK_TARGET: 1n << 12n,
346
- PATH_OPEN: 1n << 13n,
347
- FD_READDIR: 1n << 14n,
348
- PATH_READLINK: 1n << 15n,
349
- PATH_RENAME_SOURCE: 1n << 16n,
350
- PATH_RENAME_TARGET: 1n << 17n,
351
- PATH_FILESTAT_GET: 1n << 18n,
352
- PATH_FILESTAT_SET_SIZE: 1n << 19n,
353
- PATH_FILESTAT_SET_TIMES: 1n << 20n,
354
- FD_FILESTAT_GET: 1n << 21n,
355
- FD_FILESTAT_SET_SIZE: 1n << 22n,
356
- FD_FILESTAT_SET_TIMES: 1n << 23n,
357
- PATH_SYMLINK: 1n << 24n,
358
- PATH_REMOVE_DIRECTORY: 1n << 25n,
359
- PATH_UNLINK_FILE: 1n << 26n,
360
- POLL_FD_READWRITE: 1n << 27n,
361
- SOCK_SHUTDOWN: 1n << 28n,
362
- SOCK_ACCEPT: 1n << 29n,
363
- } as const;
364
-
365
- /** All rights combined */
366
- const RIGHTS_ALL = Object.values(Rights).reduce((a, b) => a | b, 0n);
367
-
368
- /** Rights for regular files */
369
- const RIGHTS_FILE_BASE =
370
- Rights.FD_DATASYNC |
371
- Rights.FD_READ |
372
- Rights.FD_SEEK |
373
- Rights.FD_FDSTAT_SET_FLAGS |
374
- Rights.FD_SYNC |
375
- Rights.FD_TELL |
376
- Rights.FD_WRITE |
377
- Rights.FD_ADVISE |
378
- Rights.FD_ALLOCATE |
379
- Rights.FD_FILESTAT_GET |
380
- Rights.FD_FILESTAT_SET_SIZE |
381
- Rights.FD_FILESTAT_SET_TIMES |
382
- Rights.POLL_FD_READWRITE;
383
-
384
- /** Rights for directories */
385
- const RIGHTS_DIRECTORY_BASE =
386
- Rights.FD_FDSTAT_SET_FLAGS |
387
- Rights.FD_SYNC |
388
- Rights.FD_ADVISE |
389
- Rights.PATH_CREATE_DIRECTORY |
390
- Rights.PATH_CREATE_FILE |
391
- Rights.PATH_LINK_SOURCE |
392
- Rights.PATH_LINK_TARGET |
393
- Rights.PATH_OPEN |
394
- Rights.FD_READDIR |
395
- Rights.PATH_READLINK |
396
- Rights.PATH_RENAME_SOURCE |
397
- Rights.PATH_RENAME_TARGET |
398
- Rights.PATH_FILESTAT_GET |
399
- Rights.PATH_FILESTAT_SET_SIZE |
400
- Rights.PATH_FILESTAT_SET_TIMES |
401
- Rights.FD_FILESTAT_GET |
402
- Rights.FD_FILESTAT_SET_TIMES |
403
- Rights.PATH_SYMLINK |
404
- Rights.PATH_REMOVE_DIRECTORY |
405
- Rights.PATH_UNLINK_FILE |
406
- Rights.POLL_FD_READWRITE;
407
-
408
- const RIGHTS_DIRECTORY_INHERITING = RIGHTS_DIRECTORY_BASE | RIGHTS_FILE_BASE;
409
-
410
- /** Rights for TTY/character devices */
411
- const RIGHTS_TTY =
412
- Rights.FD_READ | Rights.FD_FDSTAT_SET_FLAGS | Rights.FD_WRITE | Rights.FD_FILESTAT_GET | Rights.POLL_FD_READWRITE;
413
-
414
- // =============================================================================
415
- // WASI Open Flags (oflags)
416
- // =============================================================================
417
-
418
- export const enum OFlags {
419
- /** Create file if it does not exist. */
420
- CREAT = 1 << 0,
421
- /** Fail if not a directory. */
422
- DIRECTORY = 1 << 1,
423
- /** Fail if file already exists. */
424
- EXCL = 1 << 2,
425
- /** Truncate file to size 0. */
426
- TRUNC = 1 << 3,
427
- }
428
-
429
- // =============================================================================
430
- // WASI Lookup Flags
431
- // =============================================================================
432
-
433
- export const enum LookupFlags {
434
- /** Follow symlinks. */
435
- SYMLINK_FOLLOW = 1 << 0,
436
- }
437
-
438
- // =============================================================================
439
- // WASI Whence (for seek)
440
- // =============================================================================
441
-
442
- export const enum Whence {
443
- /** Seek relative to start of file. */
444
- SET = 0,
445
- /** Seek relative to current position. */
446
- CUR = 1,
447
- /** Seek relative to end of file. */
448
- END = 2,
449
- }
450
-
451
- // =============================================================================
452
- // WASI Filestat Set Flags (fstflags)
453
- // =============================================================================
454
-
455
- export const enum FstFlags {
456
- /** Adjust the last data access timestamp to the provided value. */
457
- ATIM = 1 << 0,
458
- /** Adjust the last data access timestamp to the current time. */
459
- ATIM_NOW = 1 << 1,
460
- /** Adjust the last data modification timestamp to the provided value. */
461
- MTIM = 1 << 2,
462
- /** Adjust the last data modification timestamp to the current time. */
463
- MTIM_NOW = 1 << 3,
464
- }
465
-
466
- // =============================================================================
467
- // WASI Event Types (for poll_oneoff)
468
- // =============================================================================
469
-
470
- export const enum EventType {
471
- /** Clock event. */
472
- CLOCK = 0,
473
- /** File descriptor read event. */
474
- FD_READ = 1,
475
- /** File descriptor write event. */
476
- FD_WRITE = 2,
477
- }
478
-
479
- // =============================================================================
480
- // WASI Subscription Clock Flags
481
- // =============================================================================
482
-
483
- export const enum SubClockFlags {
484
- /** Clock is absolute (vs relative). */
485
- ABSTIME = 1 << 0,
486
- }
487
-
488
- // =============================================================================
489
- // WASI Signals
490
- // =============================================================================
491
-
492
- export const enum Signal {
493
- NONE = 0,
494
- HUP = 1,
495
- INT = 2,
496
- QUIT = 3,
497
- ILL = 4,
498
- TRAP = 5,
499
- ABRT = 6,
500
- BUS = 7,
501
- FPE = 8,
502
- KILL = 9,
503
- USR1 = 10,
504
- SEGV = 11,
505
- USR2 = 12,
506
- PIPE = 13,
507
- ALRM = 14,
508
- TERM = 15,
509
- CHLD = 16,
510
- CONT = 17,
511
- STOP = 18,
512
- TSTP = 19,
513
- TTIN = 20,
514
- TTOU = 21,
515
- URG = 22,
516
- XCPU = 23,
517
- XFSZ = 24,
518
- VTALRM = 25,
519
- PROF = 26,
520
- WINCH = 27,
521
- POLL = 28,
522
- PWR = 29,
523
- SYS = 30,
524
- }
525
-
526
- const SIGNAL_MAP: Record<number, NodeJS.Signals> = {
527
- [Signal.HUP]: "SIGHUP",
528
- [Signal.INT]: "SIGINT",
529
- [Signal.QUIT]: "SIGQUIT",
530
- [Signal.ILL]: "SIGILL",
531
- [Signal.TRAP]: "SIGTRAP",
532
- [Signal.ABRT]: "SIGABRT",
533
- [Signal.BUS]: "SIGBUS",
534
- [Signal.FPE]: "SIGFPE",
535
- [Signal.KILL]: "SIGKILL",
536
- [Signal.USR1]: "SIGUSR1",
537
- [Signal.SEGV]: "SIGSEGV",
538
- [Signal.USR2]: "SIGUSR2",
539
- [Signal.PIPE]: "SIGPIPE",
540
- [Signal.ALRM]: "SIGALRM",
541
- [Signal.TERM]: "SIGTERM",
542
- [Signal.CHLD]: "SIGCHLD",
543
- [Signal.CONT]: "SIGCONT",
544
- [Signal.STOP]: "SIGSTOP",
545
- [Signal.TSTP]: "SIGTSTP",
546
- [Signal.TTIN]: "SIGTTIN",
547
- [Signal.TTOU]: "SIGTTOU",
548
- [Signal.URG]: "SIGURG",
549
- [Signal.XCPU]: "SIGXCPU",
550
- [Signal.XFSZ]: "SIGXFSZ",
551
- [Signal.VTALRM]: "SIGVTALRM",
552
- };
553
-
554
- // =============================================================================
555
- // WASI Preopentype
556
- // =============================================================================
557
-
558
- export const enum PreopenType {
559
- /** A pre-opened directory. */
560
- DIR = 0,
561
- }
562
-
563
- // =============================================================================
564
- // WASI Advice (for fd_advise)
565
- // =============================================================================
566
-
567
- export const enum Advice {
568
- NORMAL = 0,
569
- SEQUENTIAL = 1,
570
- RANDOM = 2,
571
- WILLNEED = 3,
572
- DONTNEED = 4,
573
- NOREUSE = 5,
574
- }
575
-
576
- // =============================================================================
577
- // Custom Error Classes
578
- // =============================================================================
579
-
580
- export class WASIError extends Error {
581
- constructor(public errno: Errno) {
582
- super(`WASI error: ${getErrnoName(errno)} (${errno})`);
583
- this.name = "WASIError";
584
- }
585
- }
586
-
587
- export class WASIExitError extends Error {
588
- constructor(public code: number) {
589
- super(`WASI exit: ${code}`);
590
- this.name = "WASIExitError";
591
- }
592
- }
593
-
594
- // =============================================================================
595
- // File Descriptor Entry
596
- // =============================================================================
597
-
598
- interface FdEntry {
599
- /** The real OS file descriptor or handle */
600
- fd: number;
601
- /** WASI file type */
602
- fileType: FileType;
603
- /** Base rights */
604
- rightsBase: bigint;
605
- /** Inheriting rights */
606
- rightsInheriting: bigint;
607
- /** Current file offset (for regular files) */
608
- offset?: bigint;
609
- /** Path on the real filesystem (for preopens/opened paths) */
610
- realPath?: string;
611
- /** Virtual path (for preopens) */
612
- preopenPath?: string;
613
- /** FD flags */
614
- fdFlags: number;
615
- }
616
-
617
- // =============================================================================
618
- // WASI Configuration
619
- // =============================================================================
620
-
621
- export interface WASIOptions {
622
- /** Command-line arguments */
623
- args?: string[];
624
- /** Environment variables */
625
- env?: Record<string, string>;
626
- /** Preopened directories: map of virtual path -> real path */
627
- preopens?: Record<string, string>;
628
- }
629
-
630
- // =============================================================================
631
- // Utility Functions
632
- // =============================================================================
633
-
634
- /** Convert milliseconds to nanoseconds (as bigint) */
635
- function msToNs(ms: number): bigint {
636
- return BigInt(Math.trunc(ms * 1_000_000));
637
- }
638
-
639
- /** Convert nanoseconds to milliseconds */
640
- function nsToMs(ns: bigint): number {
641
- return Number(ns / 1_000_000n);
642
- }
643
-
644
- /** Get current time in nanoseconds for a given clock */
645
- function clockTimeNs(clockId: ClockId): bigint | null {
646
- switch (clockId) {
647
- case ClockId.REALTIME:
648
- return msToNs(Date.now());
649
- case ClockId.MONOTONIC:
650
- return process.hrtime.bigint();
651
- case ClockId.PROCESS_CPUTIME_ID:
652
- case ClockId.THREAD_CPUTIME_ID:
653
- // Use hrtime as approximation for CPU time
654
- return process.hrtime.bigint();
655
- default:
656
- return null;
657
- }
658
- }
659
-
660
- /** Convert Node.js fs.Stats to WASI FileType */
661
- function statsToFileType(stats: fs.Stats): FileType {
662
- if (stats.isFile()) return FileType.REGULAR_FILE;
663
- if (stats.isDirectory()) return FileType.DIRECTORY;
664
- if (stats.isSymbolicLink()) return FileType.SYMBOLIC_LINK;
665
- if (stats.isCharacterDevice()) return FileType.CHARACTER_DEVICE;
666
- if (stats.isBlockDevice()) return FileType.BLOCK_DEVICE;
667
- if (stats.isFIFO()) return FileType.SOCKET_STREAM;
668
- if (stats.isSocket()) return FileType.SOCKET_STREAM;
669
- return FileType.UNKNOWN;
670
- }
671
-
672
- /** Get rights for a file type */
673
- function rightsForFileType(fileType: FileType, isTTY: boolean): { base: bigint; inheriting: bigint } {
674
- switch (fileType) {
675
- case FileType.REGULAR_FILE:
676
- return { base: RIGHTS_FILE_BASE, inheriting: 0n };
677
- case FileType.DIRECTORY:
678
- return { base: RIGHTS_DIRECTORY_BASE, inheriting: RIGHTS_DIRECTORY_INHERITING };
679
- case FileType.CHARACTER_DEVICE:
680
- if (isTTY) {
681
- return { base: RIGHTS_TTY, inheriting: 0n };
682
- }
683
- return { base: RIGHTS_ALL, inheriting: RIGHTS_ALL };
684
- case FileType.BLOCK_DEVICE:
685
- return { base: RIGHTS_ALL, inheriting: RIGHTS_ALL };
686
- case FileType.SOCKET_STREAM:
687
- case FileType.SOCKET_DGRAM:
688
- return { base: RIGHTS_ALL, inheriting: RIGHTS_ALL };
689
- default:
690
- return { base: RIGHTS_ALL, inheriting: RIGHTS_ALL };
691
- }
692
- }
693
-
694
- /** Wrap a syscall function to catch errors and return errno */
695
- function wrap<T extends (...args: any[]) => number>(fn: T): T {
696
- return ((...args: Parameters<T>): number => {
697
- try {
698
- return fn(...args);
699
- } catch (err: any) {
700
- if (err instanceof WASIError) {
701
- return err.errno;
702
- }
703
- if (err instanceof WASIExitError) {
704
- throw err;
705
- }
706
- // Map Node.js errors
707
- let e = err;
708
- while (e?.cause) e = e.cause;
709
- if (e?.code && typeof e.code === "string" && e.code in NODE_ERROR_MAP) {
710
- return NODE_ERROR_MAP[e.code];
711
- }
712
- console.error("WASI unexpected error:", err);
713
- return Errno.EIO;
714
- }
715
- }) as T;
716
- }
717
-
718
- /** Read a null-terminated string from memory */
719
- function readString(memory: DataView, ptr: number, len: number): string {
720
- const bytes = new Uint8Array(memory.buffer, ptr, len);
721
- return new TextDecoder().decode(bytes);
722
- }
723
-
724
- /** Write a string to memory */
725
- function writeString(memory: DataView, ptr: number, str: string): number {
726
- const bytes = new TextEncoder().encode(str);
727
- new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
728
- return bytes.length;
729
- }
730
-
731
- // =============================================================================
732
- // WASI Class
733
- // =============================================================================
734
-
735
- const FD_INIT = [
736
- { fd: 0, fileType: FileType.CHARACTER_DEVICE, rightsBase: RIGHTS_TTY, rightsInheriting: 0n, fdFlags: 0 },
737
- { fd: 1, fileType: FileType.CHARACTER_DEVICE, rightsBase: RIGHTS_TTY, rightsInheriting: 0n, fdFlags: 0 },
738
- { fd: 2, fileType: FileType.CHARACTER_DEVICE, rightsBase: RIGHTS_TTY, rightsInheriting: 0n, fdFlags: 0 },
739
- ];
740
-
741
- export class WASI1 {
742
- #memory!: WebAssembly.Memory; /** WebAssembly memory */
743
- #args: string[]; /** Program arguments */
744
- #env: Record<string, string>; /** Environment variables */
745
- #fds = new Map<number, FdEntry>(); /** Map of WASI fd -> FdEntry */
746
- #nextFd = 3; /** Next free WASI fd */
747
- #wasiImport: ReturnType<WASI1["buildImports"]>; /** WASI imports object */
748
-
749
- constructor(options: WASIOptions = {}) {
750
- this.#args = options.args ?? [];
751
- this.#env = options.env ?? {};
752
-
753
- // Initialize stdin/stdout/stderr
754
- for (let i = 0; i < FD_INIT.length; i++) {
755
- this.#fds.set(FD_INIT[i].fd, FD_INIT[i]);
756
- }
757
-
758
- // Set up preopens
759
- const preopens = options.preopens ?? this.getDefaultPreopens();
760
- for (const [virtualPath, realPath] of Object.entries(preopens)) {
761
- this.addPreopen(virtualPath, realPath);
762
- }
763
-
764
- // Build the WASI imports
765
- this.#wasiImport = this.buildImports();
766
- }
767
-
768
- /** Get default preopens based on platform */
769
- private getDefaultPreopens(): Record<string, string> {
770
- if (os.platform() === "win32") {
771
- // On Windows, preopen all available drive letters
772
- const preopens: Record<string, string> = {};
773
- for (let i = 65; i <= 90; i++) {
774
- // A-Z
775
- const letter = String.fromCharCode(i);
776
- const drivePath = `${letter}:\\`;
777
- try {
778
- fs.accessSync(drivePath);
779
- preopens[`/${letter.toLowerCase()}`] = drivePath;
780
- } catch {
781
- // Drive doesn't exist, skip
782
- }
783
- }
784
- return preopens;
785
- } else {
786
- // On Unix, preopen root
787
- return { "/": "/" };
788
- }
789
- }
790
-
791
- /** Add a preopen directory */
792
- private addPreopen(virtualPath: string, realPath: string): void {
793
- try {
794
- const fd = fs.openSync(realPath, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
795
- const wasiFd = this.#nextFd++;
796
- this.#fds.set(wasiFd, {
797
- fd,
798
- fileType: FileType.DIRECTORY,
799
- rightsBase: RIGHTS_DIRECTORY_BASE,
800
- rightsInheriting: RIGHTS_DIRECTORY_INHERITING,
801
- realPath,
802
- preopenPath: virtualPath,
803
- fdFlags: 0,
804
- });
805
- } catch (err) {
806
- console.warn(`Failed to preopen ${virtualPath} -> ${realPath}:`, err);
807
- }
808
- }
809
-
810
- #view() {
811
- return new DataView(this.#memory.buffer);
812
- }
813
-
814
- #mem(ptr: number, len: number) {
815
- return new Uint8Array(this.#memory.buffer, ptr, len);
816
- }
817
-
818
- /** Get an FD entry, throwing if invalid */
819
- private getFd(fd: number): FdEntry {
820
- const entry = this.#fds.get(fd);
821
- if (!entry) {
822
- throw new WASIError(Errno.EBADF);
823
- }
824
- return entry;
825
- }
826
-
827
- /** Check rights on an FD */
828
- private checkRights(fd: number, rights: bigint): FdEntry {
829
- const entry = this.getFd(fd);
830
- if ((entry.rightsBase & rights) !== rights) {
831
- throw new WASIError(Errno.ENOTCAPABLE);
832
- }
833
- return entry;
834
- }
835
-
836
- /** Allocate a new WASI fd */
837
- private allocateFd(): number {
838
- return this.#nextFd++;
839
- }
840
-
841
- /** Read iovecs from memory */
842
- private readIovecs(iovsPtr: number, iovsLen: number): Uint8Array[] {
843
- const v = this.#view();
844
- const buffers: Uint8Array[] = [];
845
- for (let i = 0; i < iovsLen; i++) {
846
- const ptr = v.getUint32(iovsPtr + i * 8, true);
847
- const len = v.getUint32(iovsPtr + i * 8 + 4, true);
848
- buffers.push(this.#mem(ptr, len));
849
- }
850
- return buffers;
851
- }
852
-
853
- /** Resolve a path relative to a directory fd */
854
- private resolvePath(dirFd: number, pathPtr: number, pathLen: number): string {
855
- const entry = this.getFd(dirFd);
856
- if (entry.fileType !== FileType.DIRECTORY) {
857
- throw new WASIError(Errno.ENOTDIR);
858
- }
859
- const relativePath = readString(this.#view(), pathPtr, pathLen);
860
- if (!entry.realPath) {
861
- throw new WASIError(Errno.EINVAL);
862
- }
863
- return path.resolve(entry.realPath, relativePath);
864
- }
865
-
866
- /** Build all WASI imports */
867
- private buildImports() {
868
- return {
869
- // =========================================================================
870
- // Arguments
871
- // =========================================================================
872
-
873
- args_get: wrap((argvPtr: number, argvBufPtr: number): number => {
874
- const v = this.#view();
875
- let bufOffset = argvBufPtr;
876
- for (let i = 0; i < this.#args.length; i++) {
877
- v.setUint32(argvPtr + i * 4, bufOffset, true);
878
- bufOffset += writeString(v, bufOffset, `${this.#args[i]}\0`);
879
- }
880
- return Errno.SUCCESS;
881
- }),
882
-
883
- args_sizes_get: wrap((argcPtr: number, argvBufSizePtr: number): number => {
884
- const v = this.#view();
885
- v.setUint32(argcPtr, this.#args.length, true);
886
- const bufSize = this.#args.reduce((sum, arg) => sum + new TextEncoder().encode(arg).length + 1, 0);
887
- v.setUint32(argvBufSizePtr, bufSize, true);
888
- return Errno.SUCCESS;
889
- }),
890
-
891
- // =========================================================================
892
- // Environment
893
- // =========================================================================
894
-
895
- environ_get: wrap((environPtr: number, environBufPtr: number): number => {
896
- const v = this.#view();
897
- const entries = Object.entries(this.#env);
898
- let bufOffset = environBufPtr;
899
- for (let i = 0; i < entries.length; i++) {
900
- const [key, value] = entries[i];
901
- v.setUint32(environPtr + i * 4, bufOffset, true);
902
- bufOffset += writeString(v, bufOffset, `${key}=${value}\0`);
903
- }
904
- return Errno.SUCCESS;
905
- }),
906
-
907
- environ_sizes_get: wrap((environCountPtr: number, environBufSizePtr: number): number => {
908
- const v = this.#view();
909
- const entries = Object.entries(this.#env);
910
- v.setUint32(environCountPtr, entries.length, true);
911
- const bufSize = entries.reduce((sum, [k, v]) => sum + new TextEncoder().encode(`${k}=${v}`).length + 1, 0);
912
- v.setUint32(environBufSizePtr, bufSize, true);
913
- return Errno.SUCCESS;
914
- }),
915
-
916
- // =========================================================================
917
- // Clock
918
- // =========================================================================
919
-
920
- clock_res_get: wrap((clockId: number, resPtr: number): number => {
921
- const v = this.#view();
922
- let res: bigint;
923
- switch (clockId) {
924
- case ClockId.REALTIME:
925
- res = 1_000_000n; // 1ms
926
- break;
927
- case ClockId.MONOTONIC:
928
- case ClockId.PROCESS_CPUTIME_ID:
929
- case ClockId.THREAD_CPUTIME_ID:
930
- res = 1n; // 1ns
931
- break;
932
- default:
933
- return Errno.EINVAL;
934
- }
935
- v.setBigUint64(resPtr, res, true);
936
- return Errno.SUCCESS;
937
- }),
938
-
939
- clock_time_get: wrap((clockId: number, _precision: bigint, timePtr: number): number => {
940
- const v = this.#view();
941
- const time = clockTimeNs(clockId);
942
- if (time === null) {
943
- return Errno.EINVAL;
944
- }
945
- v.setBigUint64(timePtr, time, true);
946
- return Errno.SUCCESS;
947
- }),
948
-
949
- // =========================================================================
950
- // File Descriptor Operations
951
- // =========================================================================
952
-
953
- fd_advise: wrap((fd: number, _offset: bigint, _len: bigint, _advice: number): number => {
954
- this.checkRights(fd, Rights.FD_ADVISE);
955
- // Advisory only, no-op is valid
956
- return Errno.SUCCESS;
957
- }),
958
-
959
- fd_allocate: wrap((fd: number, offset: bigint, len: bigint): number => {
960
- const entry = this.checkRights(fd, Rights.FD_ALLOCATE);
961
- // Extend file if needed
962
- const stats = fs.fstatSync(entry.fd);
963
- const newSize = Number(offset + len);
964
- if (newSize > stats.size) {
965
- fs.ftruncateSync(entry.fd, newSize);
966
- }
967
- return Errno.SUCCESS;
968
- }),
969
-
970
- fd_close: wrap((fd: number): number => {
971
- const entry = this.getFd(fd);
972
- // Don't close stdin/stdout/stderr
973
- if (fd > 2) {
974
- fs.closeSync(entry.fd);
975
- }
976
- this.#fds.delete(fd);
977
- return Errno.SUCCESS;
978
- }),
979
-
980
- fd_datasync: wrap((fd: number): number => {
981
- const entry = this.checkRights(fd, Rights.FD_DATASYNC);
982
- fs.fdatasyncSync(entry.fd);
983
- return Errno.SUCCESS;
984
- }),
985
-
986
- fd_fdstat_get: wrap((fd: number, statPtr: number): number => {
987
- const v = this.#view();
988
- const entry = this.getFd(fd);
989
-
990
- // fdstat structure:
991
- // u8 fs_filetype
992
- // u16 fs_flags
993
- // u64 fs_rights_base
994
- // u64 fs_rights_inheriting
995
- v.setUint8(statPtr, entry.fileType);
996
- v.setUint16(statPtr + 2, entry.fdFlags, true);
997
- v.setBigUint64(statPtr + 8, entry.rightsBase, true);
998
- v.setBigUint64(statPtr + 16, entry.rightsInheriting, true);
999
- return Errno.SUCCESS;
1000
- }),
1001
-
1002
- fd_fdstat_set_flags: wrap((fd: number, flags: number): number => {
1003
- const entry = this.checkRights(fd, Rights.FD_FDSTAT_SET_FLAGS);
1004
- entry.fdFlags = flags;
1005
- // Note: Most flags don't have direct OS equivalents in sync I/O
1006
- return Errno.SUCCESS;
1007
- }),
1008
-
1009
- fd_fdstat_set_rights: wrap((fd: number, rightsBase: bigint, rightsInheriting: bigint): number => {
1010
- const entry = this.getFd(fd);
1011
- // Can only remove rights, not add
1012
- if ((rightsBase & ~entry.rightsBase) !== 0n) {
1013
- return Errno.ENOTCAPABLE;
1014
- }
1015
- if ((rightsInheriting & ~entry.rightsInheriting) !== 0n) {
1016
- return Errno.ENOTCAPABLE;
1017
- }
1018
- entry.rightsBase = rightsBase;
1019
- entry.rightsInheriting = rightsInheriting;
1020
- return Errno.SUCCESS;
1021
- }),
1022
-
1023
- fd_filestat_get: wrap((fd: number, statPtr: number): number => {
1024
- const v = this.#view();
1025
- const entry = this.checkRights(fd, Rights.FD_FILESTAT_GET);
1026
- const stats = fs.fstatSync(entry.fd);
1027
-
1028
- // filestat structure (64 bytes):
1029
- // u64 dev, u64 ino, u8 filetype, u64 nlink, u64 size,
1030
- // u64 atim, u64 mtim, u64 ctim
1031
- v.setBigUint64(statPtr, BigInt(stats.dev), true);
1032
- v.setBigUint64(statPtr + 8, BigInt(stats.ino), true);
1033
- v.setUint8(statPtr + 16, statsToFileType(stats));
1034
- v.setBigUint64(statPtr + 24, BigInt(stats.nlink), true);
1035
- v.setBigUint64(statPtr + 32, BigInt(stats.size), true);
1036
- v.setBigUint64(statPtr + 40, msToNs(stats.atimeMs), true);
1037
- v.setBigUint64(statPtr + 48, msToNs(stats.mtimeMs), true);
1038
- v.setBigUint64(statPtr + 56, msToNs(stats.ctimeMs), true);
1039
- return Errno.SUCCESS;
1040
- }),
1041
-
1042
- fd_filestat_set_size: wrap((fd: number, size: bigint): number => {
1043
- const entry = this.checkRights(fd, Rights.FD_FILESTAT_SET_SIZE);
1044
- fs.ftruncateSync(entry.fd, Number(size));
1045
- return Errno.SUCCESS;
1046
- }),
1047
-
1048
- fd_filestat_set_times: wrap((fd: number, atim: bigint, mtim: bigint, fstFlags: number): number => {
1049
- const entry = this.checkRights(fd, Rights.FD_FILESTAT_SET_TIMES);
1050
- const stats = fs.fstatSync(entry.fd);
1051
-
1052
- let atime: Date;
1053
- let mtime: Date;
1054
- const now = new Date();
1055
-
1056
- if (fstFlags & FstFlags.ATIM_NOW) {
1057
- atime = now;
1058
- } else if (fstFlags & FstFlags.ATIM) {
1059
- atime = new Date(nsToMs(atim));
1060
- } else {
1061
- atime = stats.atime;
1062
- }
1063
-
1064
- if (fstFlags & FstFlags.MTIM_NOW) {
1065
- mtime = now;
1066
- } else if (fstFlags & FstFlags.MTIM) {
1067
- mtime = new Date(nsToMs(mtim));
1068
- } else {
1069
- mtime = stats.mtime;
1070
- }
1071
-
1072
- fs.futimesSync(entry.fd, atime, mtime);
1073
- return Errno.SUCCESS;
1074
- }),
1075
-
1076
- fd_pread: wrap((fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nreadPtr: number): number => {
1077
- const entry = this.checkRights(fd, Rights.FD_READ | Rights.FD_SEEK);
1078
- const buffers = this.readIovecs(iovsPtr, iovsLen);
1079
-
1080
- let totalRead = 0;
1081
- let currentOffset = Number(offset);
1082
- for (const buf of buffers) {
1083
- const bytesRead = fs.readSync(entry.fd, buf, 0, buf.length, currentOffset);
1084
- totalRead += bytesRead;
1085
- currentOffset += bytesRead;
1086
- if (bytesRead < buf.length) break;
1087
- }
1088
-
1089
- const v = this.#view();
1090
- v.setUint32(nreadPtr, totalRead, true);
1091
- return Errno.SUCCESS;
1092
- }),
1093
-
1094
- fd_prestat_get: wrap((fd: number, prestatPtr: number): number => {
1095
- const v = this.#view();
1096
- const entry = this.getFd(fd);
1097
- if (!entry.preopenPath) {
1098
- return Errno.EBADF;
1099
- }
1100
-
1101
- // prestat structure:
1102
- // u8 tag (0 = dir)
1103
- // u32 name_len
1104
- v.setUint8(prestatPtr, PreopenType.DIR);
1105
- const pathBytes = new TextEncoder().encode(entry.preopenPath);
1106
- v.setUint32(prestatPtr + 4, pathBytes.length, true);
1107
- return Errno.SUCCESS;
1108
- }),
1109
-
1110
- fd_prestat_dir_name: wrap((fd: number, pathPtr: number, pathLen: number): number => {
1111
- const entry = this.getFd(fd);
1112
- if (!entry.preopenPath) {
1113
- return Errno.EBADF;
1114
- }
1115
-
1116
- const pathBytes = new TextEncoder().encode(entry.preopenPath);
1117
- if (pathLen < pathBytes.length) {
1118
- return Errno.ENOBUFS;
1119
- }
1120
- this.#mem(pathPtr, pathBytes.length).set(pathBytes);
1121
- return Errno.SUCCESS;
1122
- }),
1123
-
1124
- fd_pwrite: wrap(
1125
- (fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nwrittenPtr: number): number => {
1126
- const entry = this.checkRights(fd, Rights.FD_WRITE | Rights.FD_SEEK);
1127
- const buffers = this.readIovecs(iovsPtr, iovsLen);
1128
-
1129
- let totalWritten = 0;
1130
- let currentOffset = Number(offset);
1131
- for (const buf of buffers) {
1132
- const bytesWritten = fs.writeSync(entry.fd, buf, 0, buf.length, currentOffset);
1133
- totalWritten += bytesWritten;
1134
- currentOffset += bytesWritten;
1135
- }
1136
-
1137
- this.#view().setUint32(nwrittenPtr, totalWritten, true);
1138
- return Errno.SUCCESS;
1139
- },
1140
- ),
1141
-
1142
- fd_read: wrap((fd: number, iovsPtr: number, iovsLen: number, nreadPtr: number): number => {
1143
- const entry = this.checkRights(fd, Rights.FD_READ);
1144
- const buffers = this.readIovecs(iovsPtr, iovsLen);
1145
-
1146
- let totalRead = 0;
1147
- for (const buf of buffers) {
1148
- const position = entry.offset !== undefined ? Number(entry.offset) : null;
1149
- const bytesRead = fs.readSync(entry.fd, buf, 0, buf.length, position);
1150
- totalRead += bytesRead;
1151
- if (entry.offset !== undefined) {
1152
- entry.offset += BigInt(bytesRead);
1153
- }
1154
- if (bytesRead < buf.length) break;
1155
- }
1156
-
1157
- const v = this.#view();
1158
- v.setUint32(nreadPtr, totalRead, true);
1159
- return Errno.SUCCESS;
1160
- }),
1161
-
1162
- fd_readdir: wrap((fd: number, bufPtr: number, bufLen: number, cookie: bigint, bufUsedPtr: number): number => {
1163
- const v = this.#view();
1164
- const entry = this.checkRights(fd, Rights.FD_READDIR);
1165
- if (!entry.realPath) {
1166
- return Errno.EINVAL;
1167
- }
1168
-
1169
- const dirents = fs.readdirSync(entry.realPath, { withFileTypes: true });
1170
- let offset = bufPtr;
1171
- const startPtr = bufPtr;
1172
- const cookieNum = Number(cookie);
1173
-
1174
- for (let i = cookieNum; i < dirents.length; i++) {
1175
- const dirent = dirents[i];
1176
- const nameBytes = new TextEncoder().encode(dirent.name);
1177
-
1178
- // dirent structure:
1179
- // u64 d_next (cookie of next entry)
1180
- // u64 d_ino
1181
- // u32 d_namlen
1182
- // u8 d_type
1183
- // char d_name[]
1184
- const direntSize = 24 + nameBytes.length;
1185
-
1186
- if (offset - startPtr + direntSize > bufLen) {
1187
- break;
1188
- }
1189
-
1190
- // Get inode
1191
- let ino = 0n;
1192
- try {
1193
- const stats = fs.statSync(path.join(entry.realPath!, dirent.name));
1194
- ino = BigInt(stats.ino);
1195
- } catch {}
1196
-
1197
- v.setBigUint64(offset, BigInt(i + 1), true);
1198
- v.setBigUint64(offset + 8, ino, true);
1199
- v.setUint32(offset + 16, nameBytes.length, true);
1200
-
1201
- let fileType = FileType.UNKNOWN;
1202
- if (dirent.isFile()) fileType = FileType.REGULAR_FILE;
1203
- else if (dirent.isDirectory()) fileType = FileType.DIRECTORY;
1204
- else if (dirent.isSymbolicLink()) fileType = FileType.SYMBOLIC_LINK;
1205
- else if (dirent.isCharacterDevice()) fileType = FileType.CHARACTER_DEVICE;
1206
- else if (dirent.isBlockDevice()) fileType = FileType.BLOCK_DEVICE;
1207
- else if (dirent.isFIFO()) fileType = FileType.SOCKET_STREAM;
1208
- else if (dirent.isSocket()) fileType = FileType.SOCKET_STREAM;
1209
-
1210
- v.setUint8(offset + 20, fileType);
1211
- this.#mem(offset + 24, nameBytes.length).set(nameBytes);
1212
-
1213
- offset += direntSize;
1214
- }
1215
-
1216
- v.setUint32(bufUsedPtr, offset - startPtr, true);
1217
- return Errno.SUCCESS;
1218
- }),
1219
-
1220
- fd_renumber: wrap((from: number, to: number): number => {
1221
- const fromEntry = this.getFd(from);
1222
- const toEntry = this.#fds.get(to);
1223
-
1224
- // Close the target if it exists
1225
- if (toEntry && to > 2) {
1226
- fs.closeSync(toEntry.fd);
1227
- }
1228
-
1229
- this.#fds.set(to, fromEntry);
1230
- this.#fds.delete(from);
1231
- return Errno.SUCCESS;
1232
- }),
1233
-
1234
- fd_seek: wrap((fd: number, offset: bigint, whence: number, newOffsetPtr: number): number => {
1235
- const v = this.#view();
1236
- const entry = this.checkRights(fd, Rights.FD_SEEK);
1237
-
1238
- if (entry.offset === undefined) {
1239
- entry.offset = 0n;
1240
- }
1241
-
1242
- let newOffset: bigint;
1243
- switch (whence) {
1244
- case Whence.SET:
1245
- newOffset = offset;
1246
- break;
1247
- case Whence.CUR:
1248
- newOffset = entry.offset + offset;
1249
- break;
1250
- case Whence.END: {
1251
- const stats = fs.fstatSync(entry.fd);
1252
- newOffset = BigInt(stats.size) + offset;
1253
- break;
1254
- }
1255
- default:
1256
- return Errno.EINVAL;
1257
- }
1258
-
1259
- if (newOffset < 0n) {
1260
- return Errno.EINVAL;
1261
- }
1262
-
1263
- entry.offset = newOffset;
1264
- v.setBigUint64(newOffsetPtr, newOffset, true);
1265
- return Errno.SUCCESS;
1266
- }),
1267
-
1268
- fd_sync: wrap((fd: number): number => {
1269
- const entry = this.checkRights(fd, Rights.FD_SYNC);
1270
- fs.fsyncSync(entry.fd);
1271
- return Errno.SUCCESS;
1272
- }),
1273
-
1274
- fd_tell: wrap((fd: number, offsetPtr: number): number => {
1275
- const v = this.#view();
1276
- const entry = this.checkRights(fd, Rights.FD_TELL);
1277
- const offset = entry.offset ?? 0n;
1278
- v.setBigUint64(offsetPtr, offset, true);
1279
- return Errno.SUCCESS;
1280
- }),
1281
-
1282
- fd_write: wrap((fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number => {
1283
- const entry = this.checkRights(fd, Rights.FD_WRITE);
1284
- const buffers = this.readIovecs(iovsPtr, iovsLen);
1285
-
1286
- let totalWritten = 0;
1287
- for (const buf of buffers) {
1288
- if (buf.length === 0) continue;
1289
- const position = entry.offset !== undefined ? Number(entry.offset) : null;
1290
- const bytesWritten = fs.writeSync(entry.fd, buf, 0, buf.length, position);
1291
- totalWritten += bytesWritten;
1292
- if (entry.offset !== undefined) {
1293
- entry.offset += BigInt(bytesWritten);
1294
- }
1295
- }
1296
-
1297
- const v = this.#view();
1298
- v.setUint32(nwrittenPtr, totalWritten, true);
1299
- return Errno.SUCCESS;
1300
- }),
1301
-
1302
- // =========================================================================
1303
- // Path Operations
1304
- // =========================================================================
1305
-
1306
- path_create_directory: wrap((fd: number, pathPtr: number, pathLen: number): number => {
1307
- this.checkRights(fd, Rights.PATH_CREATE_DIRECTORY);
1308
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1309
- fs.mkdirSync(fullPath);
1310
- return Errno.SUCCESS;
1311
- }),
1312
-
1313
- path_filestat_get: wrap(
1314
- (fd: number, flags: number, pathPtr: number, pathLen: number, statPtr: number): number => {
1315
- const v = this.#view();
1316
- this.checkRights(fd, Rights.PATH_FILESTAT_GET);
1317
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1318
-
1319
- const stats = flags & LookupFlags.SYMLINK_FOLLOW ? fs.statSync(fullPath) : fs.lstatSync(fullPath);
1320
-
1321
- v.setBigUint64(statPtr, BigInt(stats.dev), true);
1322
- v.setBigUint64(statPtr + 8, BigInt(stats.ino), true);
1323
- v.setUint8(statPtr + 16, statsToFileType(stats));
1324
- v.setBigUint64(statPtr + 24, BigInt(stats.nlink), true);
1325
- v.setBigUint64(statPtr + 32, BigInt(stats.size), true);
1326
- v.setBigUint64(statPtr + 40, msToNs(stats.atimeMs), true);
1327
- v.setBigUint64(statPtr + 48, msToNs(stats.mtimeMs), true);
1328
- v.setBigUint64(statPtr + 56, msToNs(stats.ctimeMs), true);
1329
- return Errno.SUCCESS;
1330
- },
1331
- ),
1332
-
1333
- path_filestat_set_times: wrap(
1334
- (
1335
- fd: number,
1336
- flags: number,
1337
- pathPtr: number,
1338
- pathLen: number,
1339
- atim: bigint,
1340
- mtim: bigint,
1341
- fstFlags: number,
1342
- ): number => {
1343
- this.checkRights(fd, Rights.PATH_FILESTAT_SET_TIMES);
1344
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1345
-
1346
- const stats = flags & LookupFlags.SYMLINK_FOLLOW ? fs.statSync(fullPath) : fs.lstatSync(fullPath);
1347
-
1348
- let atime: Date;
1349
- let mtime: Date;
1350
- const now = new Date();
1351
-
1352
- if (fstFlags & FstFlags.ATIM_NOW) {
1353
- atime = now;
1354
- } else if (fstFlags & FstFlags.ATIM) {
1355
- atime = new Date(nsToMs(atim));
1356
- } else {
1357
- atime = stats.atime;
1358
- }
1359
-
1360
- if (fstFlags & FstFlags.MTIM_NOW) {
1361
- mtime = now;
1362
- } else if (fstFlags & FstFlags.MTIM) {
1363
- mtime = new Date(nsToMs(mtim));
1364
- } else {
1365
- mtime = stats.mtime;
1366
- }
1367
-
1368
- fs.utimesSync(fullPath, atime, mtime);
1369
- return Errno.SUCCESS;
1370
- },
1371
- ),
1372
-
1373
- path_link: wrap(
1374
- (
1375
- oldFd: number,
1376
- _oldFlags: number,
1377
- oldPathPtr: number,
1378
- oldPathLen: number,
1379
- newFd: number,
1380
- newPathPtr: number,
1381
- newPathLen: number,
1382
- ): number => {
1383
- this.checkRights(oldFd, Rights.PATH_LINK_SOURCE);
1384
- this.checkRights(newFd, Rights.PATH_LINK_TARGET);
1385
- const oldPath = this.resolvePath(oldFd, oldPathPtr, oldPathLen);
1386
- const newPath = this.resolvePath(newFd, newPathPtr, newPathLen);
1387
- fs.linkSync(oldPath, newPath);
1388
- return Errno.SUCCESS;
1389
- },
1390
- ),
1391
-
1392
- path_open: wrap(
1393
- (
1394
- dirFd: number,
1395
- _dirFlags: number,
1396
- pathPtr: number,
1397
- pathLen: number,
1398
- oflags: number,
1399
- fsRightsBase: bigint,
1400
- fsRightsInheriting: bigint,
1401
- fdFlags: number,
1402
- fdPtr: number,
1403
- ): number => {
1404
- const v = this.#view();
1405
- const fullPath = this.resolvePath(dirFd, pathPtr, pathLen);
1406
-
1407
- // Build Node.js open flags
1408
- let nodeFlags = 0;
1409
-
1410
- // Determine read/write mode from requested rights
1411
- const wantsRead = (fsRightsBase & Rights.FD_READ) !== 0n;
1412
- const wantsWrite =
1413
- (fsRightsBase & (Rights.FD_WRITE | Rights.FD_ALLOCATE | Rights.FD_FILESTAT_SET_SIZE)) !== 0n;
1414
-
1415
- if (wantsRead && wantsWrite) {
1416
- nodeFlags |= fs.constants.O_RDWR;
1417
- } else if (wantsWrite) {
1418
- nodeFlags |= fs.constants.O_WRONLY;
1419
- } else {
1420
- nodeFlags |= fs.constants.O_RDONLY;
1421
- }
1422
-
1423
- // Handle oflags
1424
- if (oflags & OFlags.CREAT) {
1425
- nodeFlags |= fs.constants.O_CREAT;
1426
- }
1427
- if (oflags & OFlags.DIRECTORY) {
1428
- nodeFlags |= fs.constants.O_DIRECTORY;
1429
- }
1430
- if (oflags & OFlags.EXCL) {
1431
- nodeFlags |= fs.constants.O_EXCL;
1432
- }
1433
- if (oflags & OFlags.TRUNC) {
1434
- nodeFlags |= fs.constants.O_TRUNC;
1435
- }
1436
-
1437
- // Handle fdFlags
1438
- if (fdFlags & FdFlags.APPEND) {
1439
- nodeFlags |= fs.constants.O_APPEND;
1440
- }
1441
- if (fdFlags & FdFlags.DSYNC) {
1442
- nodeFlags |= fs.constants.O_DSYNC || fs.constants.O_SYNC;
1443
- }
1444
- if (fdFlags & FdFlags.NONBLOCK) {
1445
- nodeFlags |= fs.constants.O_NONBLOCK;
1446
- }
1447
- if (fdFlags & FdFlags.SYNC) {
1448
- nodeFlags |= fs.constants.O_SYNC;
1449
- }
1450
-
1451
- // Check if it's a directory that we need to open read-only
1452
- let isDir = false;
1453
- try {
1454
- isDir = fs.statSync(fullPath).isDirectory();
1455
- } catch {}
1456
-
1457
- if (isDir && !wantsWrite) {
1458
- nodeFlags = fs.constants.O_RDONLY | fs.constants.O_DIRECTORY;
1459
- }
1460
-
1461
- const realFd = fs.openSync(fullPath, nodeFlags);
1462
- const stats = fs.fstatSync(realFd);
1463
- const fileType = statsToFileType(stats);
1464
- const { base, inheriting } = rightsForFileType(fileType, tty.isatty(realFd));
1465
-
1466
- const wasiFd = this.allocateFd();
1467
- this.#fds.set(wasiFd, {
1468
- fd: realFd,
1469
- fileType,
1470
- rightsBase: fsRightsBase & base,
1471
- rightsInheriting: fsRightsInheriting & inheriting,
1472
- offset: fileType === FileType.REGULAR_FILE ? 0n : undefined,
1473
- realPath: fullPath,
1474
- fdFlags,
1475
- });
1476
-
1477
- v.setUint32(fdPtr, wasiFd, true);
1478
- return Errno.SUCCESS;
1479
- },
1480
- ),
1481
-
1482
- path_readlink: wrap(
1483
- (
1484
- fd: number,
1485
- pathPtr: number,
1486
- pathLen: number,
1487
- bufPtr: number,
1488
- bufLen: number,
1489
- bufUsedPtr: number,
1490
- ): number => {
1491
- const v = this.#view();
1492
- this.checkRights(fd, Rights.PATH_READLINK);
1493
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1494
- const target = fs.readlinkSync(fullPath);
1495
- const targetBytes = new TextEncoder().encode(target);
1496
- const len = Math.min(targetBytes.length, bufLen);
1497
- this.#mem(bufPtr, len).set(targetBytes.subarray(0, len));
1498
- v.setUint32(bufUsedPtr, len, true);
1499
- return Errno.SUCCESS;
1500
- },
1501
- ),
1502
-
1503
- path_remove_directory: wrap((fd: number, pathPtr: number, pathLen: number): number => {
1504
- this.checkRights(fd, Rights.PATH_REMOVE_DIRECTORY);
1505
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1506
- fs.rmdirSync(fullPath);
1507
- return Errno.SUCCESS;
1508
- }),
1509
-
1510
- path_rename: wrap(
1511
- (
1512
- oldFd: number,
1513
- oldPathPtr: number,
1514
- oldPathLen: number,
1515
- newFd: number,
1516
- newPathPtr: number,
1517
- newPathLen: number,
1518
- ): number => {
1519
- this.checkRights(oldFd, Rights.PATH_RENAME_SOURCE);
1520
- this.checkRights(newFd, Rights.PATH_RENAME_TARGET);
1521
- const oldPath = this.resolvePath(oldFd, oldPathPtr, oldPathLen);
1522
- const newPath = this.resolvePath(newFd, newPathPtr, newPathLen);
1523
- fs.renameSync(oldPath, newPath);
1524
- return Errno.SUCCESS;
1525
- },
1526
- ),
1527
-
1528
- path_symlink: wrap(
1529
- (oldPathPtr: number, oldPathLen: number, fd: number, newPathPtr: number, newPathLen: number): number => {
1530
- this.checkRights(fd, Rights.PATH_SYMLINK);
1531
- const oldPath = readString(this.#view(), oldPathPtr, oldPathLen);
1532
- const newPath = this.resolvePath(fd, newPathPtr, newPathLen);
1533
- fs.symlinkSync(oldPath, newPath);
1534
- return Errno.SUCCESS;
1535
- },
1536
- ),
1537
-
1538
- path_unlink_file: wrap((fd: number, pathPtr: number, pathLen: number): number => {
1539
- this.checkRights(fd, Rights.PATH_UNLINK_FILE);
1540
- const fullPath = this.resolvePath(fd, pathPtr, pathLen);
1541
- fs.unlinkSync(fullPath);
1542
- return Errno.SUCCESS;
1543
- }),
1544
-
1545
- // =========================================================================
1546
- // Polling
1547
- // =========================================================================
1548
-
1549
- poll_oneoff: wrap((inPtr: number, outPtr: number, nsubscriptions: number, neventsPtr: number): number => {
1550
- const v = this.#view();
1551
-
1552
- let nevents = 0;
1553
- let maxWaitNs = 0n;
1554
-
1555
- for (let i = 0; i < nsubscriptions; i++) {
1556
- const subPtr = inPtr + i * 48;
1557
- const userdata = v.getBigUint64(subPtr, true);
1558
- const tag = v.getUint8(subPtr + 8);
1559
-
1560
- const eventPtr = outPtr + nevents * 32;
1561
-
1562
- switch (tag) {
1563
- case EventType.CLOCK: {
1564
- const clockId = v.getUint32(subPtr + 16, true);
1565
- const timeout = v.getBigUint64(subPtr + 24, true);
1566
- const _precision = v.getBigUint64(subPtr + 32, true);
1567
- const flags = v.getUint16(subPtr + 40, true);
1568
-
1569
- const isAbsolute = (flags & SubClockFlags.ABSTIME) !== 0;
1570
- const now = clockTimeNs(clockId);
1571
-
1572
- if (now === null) {
1573
- v.setBigUint64(eventPtr, userdata, true);
1574
- v.setUint16(eventPtr + 8, Errno.EINVAL, true);
1575
- v.setUint8(eventPtr + 10, EventType.CLOCK);
1576
- } else {
1577
- let waitNs = isAbsolute ? timeout - now : timeout;
1578
- if (waitNs < 0n) waitNs = 0n;
1579
- if (waitNs > maxWaitNs) maxWaitNs = waitNs;
1580
-
1581
- v.setBigUint64(eventPtr, userdata, true);
1582
- v.setUint16(eventPtr + 8, Errno.SUCCESS, true);
1583
- v.setUint8(eventPtr + 10, EventType.CLOCK);
1584
- }
1585
- nevents++;
1586
- break;
1587
- }
1588
-
1589
- case EventType.FD_READ:
1590
- case EventType.FD_WRITE: {
1591
- const _fd = v.getUint32(subPtr + 16, true);
1592
-
1593
- // For simplicity, we just return immediately with success
1594
- // A full implementation would use select/poll/epoll
1595
- v.setBigUint64(eventPtr, userdata, true);
1596
- v.setUint16(eventPtr + 8, Errno.SUCCESS, true);
1597
- v.setUint8(eventPtr + 10, tag);
1598
- // nbytes and flags at eventPtr + 16 and eventPtr + 24
1599
- v.setBigUint64(eventPtr + 16, 0n, true);
1600
- v.setUint16(eventPtr + 24, 0, true);
1601
- nevents++;
1602
- break;
1603
- }
1604
-
1605
- default:
1606
- return Errno.EINVAL;
1607
- }
1608
- }
1609
-
1610
- // Sleep if there's a timeout
1611
- if (maxWaitNs > 0n && nevents > 0) {
1612
- const sleepMs = Number(maxWaitNs / 1_000_000n);
1613
- if (sleepMs > 0) {
1614
- // Use Bun.sleepSync if available, otherwise busy-wait (not ideal)
1615
- if (typeof Bun !== "undefined" && Bun.sleepSync) {
1616
- Bun.sleepSync(sleepMs);
1617
- } else {
1618
- const end = Date.now() + sleepMs;
1619
- while (Date.now() < end) {
1620
- // Busy wait - not ideal but works
1621
- }
1622
- }
1623
- }
1624
- }
1625
-
1626
- v.setUint32(neventsPtr, nevents, true);
1627
- return Errno.SUCCESS;
1628
- }),
1629
-
1630
- // =========================================================================
1631
- // Process
1632
- // =========================================================================
1633
-
1634
- proc_exit: (code: number): never => {
1635
- throw new WASIExitError(code);
1636
- },
1637
-
1638
- proc_raise: wrap((sig: number): number => {
1639
- const signal = SIGNAL_MAP[sig];
1640
- if (!signal) {
1641
- return Errno.EINVAL;
1642
- }
1643
- process.kill(process.pid, signal);
1644
- return Errno.SUCCESS;
1645
- }),
1646
-
1647
- // =========================================================================
1648
- // Random
1649
- // =========================================================================
1650
-
1651
- random_get: wrap((bufPtr: number, bufLen: number): number => {
1652
- const buf = this.#mem(bufPtr, bufLen);
1653
- crypto.getRandomValues(buf);
1654
- return Errno.SUCCESS;
1655
- }),
1656
-
1657
- // =========================================================================
1658
- // Scheduler
1659
- // =========================================================================
1660
-
1661
- sched_yield: wrap((): number => {
1662
- // No-op in single-threaded JS
1663
- return Errno.SUCCESS;
1664
- }),
1665
-
1666
- // =========================================================================
1667
- // Sockets (stubs - return ENOSYS)
1668
- // =========================================================================
1669
-
1670
- sock_accept: wrap((_fd: number, _flags: number, _fdPtr: number): number => {
1671
- return Errno.ENOSYS;
1672
- }),
1673
-
1674
- sock_recv: wrap(
1675
- (
1676
- _fd: number,
1677
- _iovs: number,
1678
- _iovsLen: number,
1679
- _flags: number,
1680
- _nreadPtr: number,
1681
- _flagsPtr: number,
1682
- ): number => {
1683
- return Errno.ENOSYS;
1684
- },
1685
- ),
1686
-
1687
- sock_send: wrap(
1688
- (_fd: number, _iovs: number, _iovsLen: number, _flags: number, _nwrittenPtr: number): number => {
1689
- return Errno.ENOSYS;
1690
- },
1691
- ),
1692
-
1693
- sock_shutdown: wrap((_fd: number, _how: number): number => {
1694
- return Errno.ENOSYS;
1695
- }),
1696
- };
1697
- }
1698
-
1699
- /** Get imports for a WebAssembly module */
1700
- getImportObject() {
1701
- return {
1702
- wasi_snapshot_preview1: this.#wasiImport,
1703
- wasi_unstable: this.#wasiImport,
1704
- };
1705
- }
1706
-
1707
- /** Initialize WASI with a WebAssembly instance */
1708
- initialize(instance: WebAssembly.Instance): void {
1709
- const exports = instance.exports;
1710
- if (exports.memory instanceof WebAssembly.Memory) {
1711
- this.#memory = exports.memory;
1712
- } else {
1713
- throw new Error("WebAssembly instance must export memory");
1714
- }
1715
- }
1716
-
1717
- /** Start the WASI program */
1718
- start(instance: WebAssembly.Instance): number {
1719
- this.initialize(instance);
1720
- const exports = instance.exports;
1721
-
1722
- if (typeof exports._start === "function") {
1723
- try {
1724
- (exports._start as () => void)();
1725
- return 0;
1726
- } catch (err) {
1727
- if (err instanceof WASIExitError) {
1728
- return err.code;
1729
- }
1730
- throw err;
1731
- }
1732
- } else if (typeof exports._initialize === "function") {
1733
- (exports._initialize as () => void)();
1734
- return 0;
1735
- } else {
1736
- throw new Error("WebAssembly instance must export _start or _initialize");
1737
- }
1738
- }
1739
- }
1740
-
1741
- // =============================================================================
1742
- // Default Export
1743
- // =============================================================================
1744
-
1745
- export default WASI1;