@bytecodealliance/preview3-shim 0.1.0-rc.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.
Files changed (67) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +14 -0
  3. package/lib/nodejs/cli.js +127 -0
  4. package/lib/nodejs/clocks.js +100 -0
  5. package/lib/nodejs/filesystem/descriptor.js +997 -0
  6. package/lib/nodejs/filesystem/error.js +92 -0
  7. package/lib/nodejs/filesystem/utils.js +19 -0
  8. package/lib/nodejs/filesystem.js +2 -0
  9. package/lib/nodejs/finalization.js +33 -0
  10. package/lib/nodejs/future.js +187 -0
  11. package/lib/nodejs/http/client.js +146 -0
  12. package/lib/nodejs/http/error.js +71 -0
  13. package/lib/nodejs/http/fields.js +359 -0
  14. package/lib/nodejs/http/request.js +589 -0
  15. package/lib/nodejs/http/response.js +212 -0
  16. package/lib/nodejs/http/server.js +157 -0
  17. package/lib/nodejs/http.js +34 -0
  18. package/lib/nodejs/index.js +8 -0
  19. package/lib/nodejs/random.js +14 -0
  20. package/lib/nodejs/sockets/address.js +146 -0
  21. package/lib/nodejs/sockets/error.js +123 -0
  22. package/lib/nodejs/sockets/ip-name-lookup.js +93 -0
  23. package/lib/nodejs/sockets/tcp.js +776 -0
  24. package/lib/nodejs/sockets/udp.js +516 -0
  25. package/lib/nodejs/sockets.js +14 -0
  26. package/lib/nodejs/stream.js +423 -0
  27. package/lib/nodejs/workers/cli-worker.js +49 -0
  28. package/lib/nodejs/workers/filesystem-worker.js +119 -0
  29. package/lib/nodejs/workers/http-worker.js +387 -0
  30. package/lib/nodejs/workers/resource-worker.js +151 -0
  31. package/lib/nodejs/workers/tcp-worker.js +283 -0
  32. package/lib/nodejs/workers/udp-worker.js +194 -0
  33. package/package.json +57 -0
  34. package/types/cli.d.ts +12 -0
  35. package/types/clocks.d.ts +2 -0
  36. package/types/filesystem.d.ts +2 -0
  37. package/types/http.d.ts +4 -0
  38. package/types/index.d.ts +6 -0
  39. package/types/interfaces/wasi-cli-environment.d.ts +21 -0
  40. package/types/interfaces/wasi-cli-exit.d.ts +6 -0
  41. package/types/interfaces/wasi-cli-run.d.ts +5 -0
  42. package/types/interfaces/wasi-cli-stderr.d.ts +14 -0
  43. package/types/interfaces/wasi-cli-stdin.d.ts +19 -0
  44. package/types/interfaces/wasi-cli-stdout.d.ts +14 -0
  45. package/types/interfaces/wasi-cli-terminal-input.d.ts +8 -0
  46. package/types/interfaces/wasi-cli-terminal-output.d.ts +8 -0
  47. package/types/interfaces/wasi-cli-terminal-stderr.d.ts +7 -0
  48. package/types/interfaces/wasi-cli-terminal-stdin.d.ts +7 -0
  49. package/types/interfaces/wasi-cli-terminal-stdout.d.ts +7 -0
  50. package/types/interfaces/wasi-cli-types.d.ts +15 -0
  51. package/types/interfaces/wasi-clocks-monotonic-clock.d.ts +33 -0
  52. package/types/interfaces/wasi-clocks-system-clock.d.ts +38 -0
  53. package/types/interfaces/wasi-clocks-types.d.ts +5 -0
  54. package/types/interfaces/wasi-filesystem-preopens.d.ts +6 -0
  55. package/types/interfaces/wasi-filesystem-types.d.ts +780 -0
  56. package/types/interfaces/wasi-http-client.d.ts +9 -0
  57. package/types/interfaces/wasi-http-handler.d.ts +9 -0
  58. package/types/interfaces/wasi-http-types.d.ts +619 -0
  59. package/types/interfaces/wasi-random-insecure-seed.d.ts +21 -0
  60. package/types/interfaces/wasi-random-insecure.d.ts +26 -0
  61. package/types/interfaces/wasi-random-random.d.ts +31 -0
  62. package/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +77 -0
  63. package/types/interfaces/wasi-sockets-types.d.ts +830 -0
  64. package/types/random.d.ts +3 -0
  65. package/types/sockets.d.ts +2 -0
  66. package/types/wasi-cli-command.d.ts +23 -0
  67. package/types/wasi-http-service.d.ts +14 -0
@@ -0,0 +1,997 @@
1
+ import fs from "node:fs/promises";
2
+ import process from "node:process";
3
+
4
+ import { StreamReader, readableByteStreamFromReader } from "../stream.js";
5
+ import { FutureReader } from "../future.js";
6
+ import { ResourceWorker } from "../workers/resource-worker.js";
7
+ import { earlyDispose, registerDispose } from "../finalization.js";
8
+
9
+ import { FSError } from "./error.js";
10
+ import { wasiTypeFromDirent } from "./utils.js";
11
+
12
+ const symbolDispose = Symbol.dispose || Symbol.for("dispose");
13
+
14
+ let WORKER = null;
15
+ function worker() {
16
+ return (WORKER ??= new ResourceWorker(
17
+ new URL("../workers/filesystem-worker.js", import.meta.url),
18
+ ));
19
+ }
20
+
21
+ class Descriptor {
22
+ /** Node.js file handle for file operations (FileHandle from fs.promises.open) */
23
+ #handle;
24
+ /** Descriptor flags */
25
+ #mode;
26
+ /** Absolute filesystem path to the file or directory */
27
+ #fullPath;
28
+ /** Cleanup finalizer */
29
+ #finalizer;
30
+ /** Host filesystem path for preopened directories */
31
+ #hostPreopen;
32
+
33
+ static _create(handle, mode, fullPath) {
34
+ const {
35
+ read = false,
36
+ write = false,
37
+ fileIntegritySync = false,
38
+ dataIntegritySync = false,
39
+ requestedWriteSync = false,
40
+ mutateDirectory = false,
41
+ } = mode;
42
+
43
+ const merged = {
44
+ read,
45
+ write,
46
+ fileIntegritySync,
47
+ dataIntegritySync,
48
+ requestedWriteSync,
49
+ mutateDirectory,
50
+ };
51
+
52
+ const desc = new Descriptor();
53
+ desc.#handle = handle;
54
+ desc.#fullPath = fullPath;
55
+ desc.#mode = merged;
56
+ desc.#finalizer = registerDispose(desc, null, handle, (handle) => handle.close());
57
+
58
+ return desc;
59
+ }
60
+
61
+ static _createPreopen(hostPreopen) {
62
+ const desc = new Descriptor();
63
+
64
+ if (hostPreopen.endsWith("/")) {
65
+ desc.#hostPreopen = hostPreopen.slice(0, -1) || "/";
66
+ } else {
67
+ desc.#hostPreopen = hostPreopen;
68
+ }
69
+
70
+ // Windows requires UNC paths at minimum
71
+ if (process.platform === "win32") {
72
+ desc.#hostPreopen = desc.#hostPreopen.replace(/\\/g, "/");
73
+ if (desc.#hostPreopen === "/") {
74
+ desc.#hostPreopen = "//";
75
+ }
76
+ }
77
+ return desc;
78
+ }
79
+
80
+ [symbolDispose]() {
81
+ if (this.#finalizer) {
82
+ earlyDispose(this.#finalizer);
83
+ this.#finalizer = null;
84
+ }
85
+ this.#handle = null;
86
+ }
87
+
88
+ /**
89
+ * Return a stream for reading from a file.
90
+ * WIT:
91
+ * ```
92
+ * read-via-stream: func(offset: filesize) -> tuple<stream<u8>, future<result<_, error-code>>>
93
+ * ```
94
+ *
95
+ * @param {bigint} offset The offset within the file.
96
+ * @returns {{stream: StreamReader, future: FutureReader}}
97
+ * A tuple: a readable byte stream and a future that resolves to an error code.
98
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
99
+ */
100
+ readViaStream(offset) {
101
+ this.#ensureHandle();
102
+ const transform = new TransformStream();
103
+ const promise = worker()
104
+ .run(
105
+ {
106
+ op: "read",
107
+ fd: this.#handle.fd,
108
+ offset,
109
+ stream: transform.writable,
110
+ },
111
+ [transform.writable],
112
+ )
113
+ .catch((err) => {
114
+ throw FSError.from(err);
115
+ });
116
+
117
+ return [new StreamReader(transform.readable), new FutureReader(promise)];
118
+ }
119
+
120
+ /**
121
+ * Write to a file via a byte stream.
122
+ * WIT:
123
+ * ```
124
+ * write-via-stream: async func(data: stream<u8>, offset: filesize) -> result<_, error-code>
125
+ * ```
126
+ *
127
+ * @async
128
+ * @param {object} data A readable byte stream.
129
+ * @param {bigint} offset The offset within the file.
130
+ * @returns {Promise<void>}
131
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
132
+ */
133
+ async writeViaStream(data, offset) {
134
+ this.#ensureWritable(data);
135
+ const stream = readableByteStreamFromReader(data, { name: "file write data" });
136
+
137
+ try {
138
+ await worker().run({ op: "write", fd: this.#handle.fd, offset, stream }, [stream]);
139
+ } catch (err) {
140
+ throw FSError.from(err);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Append to a file via a byte stream.
146
+ * WIT:
147
+ * ```
148
+ * append-via-stream: async func(data: stream<u8>) -> result<_, error-code>
149
+ * ```
150
+ *
151
+ * @async
152
+ * @param {object} data A readable byte stream.
153
+ * @returns {Promise<void>}
154
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
155
+ */
156
+ async appendViaStream(data) {
157
+ this.#ensureWritable(data);
158
+ const offset = await this.#handle.stat().then((s) => s.size);
159
+ return this.writeViaStream(data, offset);
160
+ }
161
+
162
+ /**
163
+ * Advisory on file access patterns.
164
+ * WIT:
165
+ * ```
166
+ * advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>
167
+ * ```
168
+ *
169
+ * @async
170
+ * @param {bigint} offset Start of the region.
171
+ * @param {bigint} length Length of the region.
172
+ * @param {string} advice One of the Wasi `advice` values.
173
+ * @returns {Promise<void>}
174
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
175
+ */
176
+ async advise(_offset, _length, _advice) {
177
+ // TODO: This not directly suported on Node.js:
178
+ // https://github.com/bytecodealliance/jco/issues/718
179
+ if ((await this.getType()).tag === "directory") {
180
+ throw new FSError("bad-descriptor");
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Synchronize file data to disk.
186
+ * WIT:
187
+ * ```
188
+ * sync-data: async func() -> result<_, error-code>
189
+ * ```
190
+ *
191
+ * @async
192
+ * @returns {Promise<void>}
193
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
194
+ */
195
+ async syncData() {
196
+ this.#ensureHandle();
197
+ try {
198
+ await this.#handle.datasync();
199
+ } catch (e) {
200
+ // On windows, `sync_data` uses `FileFlushBuffers` which fails with `EPERM` if
201
+ // the file is not upen for writing. Ignore this error, for POSIX compatibility.
202
+ if (process.platform === "win32" && e.code === "EPERM") {
203
+ return;
204
+ }
205
+ throw FSError.from(e);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Get flags associated with a descriptor.
211
+ * WIT:
212
+ * ```
213
+ * get-flags: async func() -> result<descriptor-flags, error-code>
214
+ * ```
215
+ *
216
+ * @async
217
+ * @returns {Promise<object>} The descriptor flags object.
218
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
219
+ */
220
+ async getFlags() {
221
+ return this.#mode;
222
+ }
223
+
224
+ /**
225
+ * Get the dynamic type of a descriptor.
226
+ * WIT:
227
+ * ```
228
+ * get-type: async func() -> result<descriptor-type, error-code>
229
+ * ```
230
+ *
231
+ * @async
232
+ * @returns {Promise<string>} The descriptor type.
233
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
234
+ */
235
+ async getType() {
236
+ if (this.#hostPreopen) {
237
+ return { tag: "directory" };
238
+ }
239
+ try {
240
+ const stats = await this.#handle.stat();
241
+ return wasiTypeFromDirent(stats);
242
+ } catch (e) {
243
+ throw FSError.from(e);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Adjust the size of an open file.
249
+ * WIT:
250
+ * ```
251
+ * set-size: async func(size: filesize) -> result<_, error-code>
252
+ * ```
253
+ *
254
+ * @async
255
+ * @param {bigint} size The new size.
256
+ * @returns {Promise<void>}
257
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
258
+ */
259
+ async setSize(size) {
260
+ this.#ensureHandle();
261
+
262
+ if (size > Number.MAX_SAFE_INTEGER) {
263
+ throw new FSError("invalid");
264
+ }
265
+
266
+ try {
267
+ await this.#handle.truncate(Number(size));
268
+ } catch (e) {
269
+ if (process.platform === "win32" && e.code === "EPERM") {
270
+ throw new FSError("access");
271
+ }
272
+ throw FSError.from(e);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Adjust the timestamps of an open file or directory.
278
+ * WIT:
279
+ * ```
280
+ * set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>
281
+ * ```
282
+ *
283
+ * @async
284
+ * @param {object} atimeDesc new-timestamp descriptor
285
+ * @param {object} mtimeDesc new-timestamp descriptor
286
+ * @returns {Promise<void>}
287
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
288
+ */
289
+ async setTimes(atimeDesc, mtimeDesc) {
290
+ this.#ensureHandle();
291
+
292
+ const { atime, mtime } = await this.#computeTimestamps(atimeDesc, mtimeDesc);
293
+
294
+ try {
295
+ await this.#handle.utimes(atime, mtime);
296
+ } catch (e) {
297
+ throw FSError.from(e);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Read directory entries via a stream.
303
+ * WIT:
304
+ * ```
305
+ * read-directory: async func() -> tuple<stream<directory-entry>, future<result<_, error-code>>>
306
+ * ```
307
+ *
308
+ * @async
309
+ * @returns {{stream: StreamReader, future: FutureReader}}
310
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
311
+ */
312
+ readDirectory() {
313
+ const fullPath = this.#fullPath ?? this.#hostPreopen;
314
+ if (!fullPath) {
315
+ throw new FSError("invalid");
316
+ }
317
+
318
+ const preopens = preopenEntries.map(([desc, _virtualPath]) => desc.#hostPreopen);
319
+
320
+ const transform = new TransformStream();
321
+ const promise = worker()
322
+ .run(
323
+ {
324
+ op: "readDir",
325
+ fullPath,
326
+ stream: transform.writable,
327
+ preopens,
328
+ },
329
+ [transform.writable],
330
+ )
331
+ .catch((err) => {
332
+ throw FSError.from(err);
333
+ });
334
+
335
+ return [new StreamReader(transform.readable), new FutureReader(promise)];
336
+ }
337
+
338
+ /**
339
+ * Synchronize data & metadata to disk.
340
+ * WIT:
341
+ * ```
342
+ * sync: async func() -> result<_, error-code>
343
+ * ```
344
+ *
345
+ * @async
346
+ * @returns {Promise<void>}
347
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
348
+ */
349
+ async sync() {
350
+ return this.#hostPreopen ? this.#syncDir() : this.#syncFile();
351
+ }
352
+
353
+ /**
354
+ * Create a directory.
355
+ * WIT:
356
+ * ```
357
+ * create-directory-at: async func(path: string) -> result<_, error-code>
358
+ * ```
359
+ *
360
+ * @async
361
+ * @param {string} path Relative path.
362
+ * @returns {Promise<void>}
363
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
364
+ */
365
+ async createDirectoryAt(path) {
366
+ const full = this.#getFullPath(path, false);
367
+ try {
368
+ await fs.mkdir(full);
369
+ } catch (e) {
370
+ throw FSError.from(e);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Return attributes of an open file or directory.
376
+ * WIT:
377
+ * ```
378
+ * stat: async func() -> result<descriptor-stat, error-code>
379
+ * ```
380
+ *
381
+ * @async
382
+ * @returns {Promise<object>} File statistics.
383
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
384
+ */
385
+ async stat() {
386
+ try {
387
+ let s;
388
+ if (this.#hostPreopen) {
389
+ s = await fs.stat(this.#hostPreopen, { bigint: true });
390
+ } else {
391
+ this.#ensureHandle();
392
+ s = await this.#handle.stat({ bigint: true });
393
+ }
394
+
395
+ return {
396
+ type: wasiTypeFromDirent(s),
397
+ linkCount: s.nlink,
398
+ size: s.size,
399
+ dataAccessTimestamp: nsToDateTime(s.atimeNs),
400
+ dataModificationTimestamp: nsToDateTime(s.mtimeNs),
401
+ statusChangeTimestamp: nsToDateTime(s.ctimeNs),
402
+ };
403
+ } catch (e) {
404
+ throw FSError.from(e);
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Adjust timestamps on a file or symlink.
410
+ * WIT:
411
+ * ```
412
+ * set-times-at: async func(
413
+ * path-flags: path-flags,
414
+ * path: string,
415
+ * data-access-timestamp: new-timestamp,
416
+ * data-modification-timestamp: new-timestamp
417
+ * ) -> result<_, error-code>
418
+ * ```
419
+ *
420
+ * @async
421
+ * @param {object} flags Path flags.
422
+ * @param {string} path Relative path.
423
+ * @param {object} atimeDesc New access timestamp.
424
+ * @param {object} mtimeDesc New modification timestamp.
425
+ * @returns {Promise<void>}
426
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
427
+ */
428
+ async statAt(flags, path) {
429
+ const full = this.#getFullPath(path, flags.symlinkFollow);
430
+ try {
431
+ const statFn = flags.symlinkFollow ? fs.stat : fs.lstat;
432
+ const s = await statFn(full, { bigint: true });
433
+ return {
434
+ type: wasiTypeFromDirent(s),
435
+ linkCount: s.nlink,
436
+ size: s.size,
437
+ dataAccessTimestamp: nsToDateTime(s.atimeNs),
438
+ dataModificationTimestamp: nsToDateTime(s.mtimeNs),
439
+ statusChangeTimestamp: nsToDateTime(s.ctimeNs),
440
+ };
441
+ } catch (e) {
442
+ throw FSError.from(e);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Adjust timestamps on a file or symlink.
448
+ * WIT:
449
+ * ```
450
+ * set-times-at: async func(
451
+ * path-flags: path-flags,
452
+ * path: string,
453
+ * data-access-timestamp: new-timestamp,
454
+ * data-modification-timestamp: new-timestamp
455
+ * ) -> result<_, error-code>
456
+ * ```
457
+ *
458
+ * @async
459
+ * @param {object} flags Path flags.
460
+ * @param {string} path Relative path.
461
+ * @param {object} atimeDesc New access timestamp.
462
+ * @param {object} mtimeDesc New modification timestamp.
463
+ * @returns {Promise<void>}
464
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
465
+ */
466
+ async setTimesAt(flags, path, atimeDesc, mtimeDesc) {
467
+ const { atime, mtime } = await this.#computeTimestamps(atimeDesc, mtimeDesc, path);
468
+
469
+ if (!flags.symlinkFollow && !fs.lutimes) {
470
+ throw new FSError("unsupported");
471
+ }
472
+
473
+ const full = this.#getFullPath(path, flags.symlinkFollow);
474
+
475
+ try {
476
+ const utimesFn = flags.symlinkFollow ? fs.utimes : fs.lutimes;
477
+ await utimesFn(full, atime, mtime);
478
+ } catch (e) {
479
+ throw FSError.from(e);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Create a hard link.
485
+ * WIT:
486
+ * ```
487
+ * link-at: async func(
488
+ * old-path-flags: path-flags,
489
+ * old-path: string,
490
+ * new-descriptor: borrow<descriptor>,
491
+ * new-path: string
492
+ * ) -> result<_, error-code>
493
+ * ```
494
+ *
495
+ * @async
496
+ * @param {object} oldFlags Old path flags.
497
+ * @param {string} oldPath Source path.
498
+ * @param {Descriptor} newDesc Target descriptor.
499
+ * @param {string} newPath Destination path.
500
+ * @returns {Promise<void>}
501
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
502
+ */
503
+ async linkAt(oldFlags, oldPath, newDesc, newPath) {
504
+ const src = this.#getFullPath(oldPath, oldFlags.symlinkFollow);
505
+ const dst = newDesc.#getFullPath(newPath, false);
506
+
507
+ // On Windows, a trailing '/' means the path is treated as a
508
+ // directory and hard links to directory are not supported
509
+ if (process.platform === "win32" && dst.endsWith("/")) {
510
+ throw new FSError("no-entry");
511
+ }
512
+
513
+ try {
514
+ await fs.link(src, dst);
515
+ } catch (e) {
516
+ throw FSError.from(e);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Open a file or directory.
522
+ * WIT:
523
+ * ```
524
+ * open-at: async func(
525
+ * path-flags: path-flags,
526
+ * path: string,
527
+ * open-flags: open-flags,
528
+ * %flags: descriptor-flags
529
+ * ) -> result<descriptor, error-code>
530
+ * ```
531
+ *
532
+ * @async
533
+ * @param {object} pf - path flags
534
+ * @param {string} path - relative path
535
+ * @param {object} of - open flags
536
+ * @param {object} df - descriptor flags
537
+ * @returns {Promise<Descriptor>}
538
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
539
+ */
540
+ async openAt(pf, path, of, df) {
541
+ if (!preopenEntries.length) {
542
+ throw new FSError("access");
543
+ }
544
+
545
+ const fullPath = this.#getFullPath(path, pf.symlinkFollow);
546
+ const target = stripTrailingSlash(fullPath);
547
+
548
+ const makeFsFlags = () => {
549
+ let fsFlags = 0;
550
+ if (of.create) {
551
+ fsFlags |= fs.constants.O_CREAT;
552
+ }
553
+ if (of.directory) {
554
+ fsFlags |= fs.constants.O_DIRECTORY;
555
+ }
556
+ if (of.exclusive) {
557
+ fsFlags |= fs.constants.O_EXCL;
558
+ }
559
+ if (of.truncate) {
560
+ fsFlags |= fs.constants.O_TRUNC;
561
+ }
562
+ if (df.read && df.write) {
563
+ fsFlags |= fs.constants.O_RDWR;
564
+ } else if (df.write) {
565
+ fsFlags |= fs.constants.O_WRONLY;
566
+ } else if (df.read) {
567
+ fsFlags |= fs.constants.O_RDONLY;
568
+ }
569
+ if (df.fileIntegritySync) {
570
+ fsFlags |= fs.constants.O_SYNC;
571
+ }
572
+ if (df.requestedWriteSync) {
573
+ fsFlags |= fs.constants.O_SYNC;
574
+ }
575
+ if (df.dataIntegritySync) {
576
+ fsFlags |= fs.constants.O_DSYNC;
577
+ }
578
+ if (!pf.symlinkFollow) {
579
+ fsFlags |= fs.constants.O_NOFOLLOW;
580
+ }
581
+ return fsFlags;
582
+ };
583
+
584
+ const fsFlags = makeFsFlags();
585
+
586
+ if (process.platform === "win32") {
587
+ if (!pf.symlinkFollow && !of.create) {
588
+ const isSymlink = await fs
589
+ .lstat(fullPath)
590
+ .then((p) => p.isSymbolicLink())
591
+ .catch(() => false);
592
+
593
+ if (isSymlink) {
594
+ const tag = of.directory ? "not-directory" : "loop";
595
+ throw new FSError(tag);
596
+ }
597
+ }
598
+
599
+ if (pf.symlinkFollow && of.directory) {
600
+ const isFile = await fs
601
+ .stat(fullPath)
602
+ .then((p) => !p.isDirectory())
603
+ .catch(() => false);
604
+
605
+ if (isFile) {
606
+ throw new FSError("not-directory");
607
+ }
608
+ }
609
+ }
610
+
611
+ try {
612
+ const handle = await fs.open(target, fsFlags);
613
+ const desc = descriptorCreate(handle, df, fullPath);
614
+ const isDir = (await desc.getType()).tag === "directory";
615
+
616
+ if (fullPath.endsWith("/") && !isDir) {
617
+ desc[symbolDispose]();
618
+ throw new FSError("not-directory");
619
+ }
620
+ return desc;
621
+ } catch (err) {
622
+ if (err.code === "ERR_INVALID_ARG_VALUE") {
623
+ throw new FSError(process.platform === "win32" ? "no-entry" : "invalid");
624
+ }
625
+ throw FSError.from(err);
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Read the contents of a symbolic link.
631
+ * WIT:
632
+ * ```
633
+ * readlink-at: async func(path: string) -> result<string, error-code>
634
+ * ```
635
+ *
636
+ * @async
637
+ * @param {string} path Relative path.
638
+ * @returns {Promise<string>}
639
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
640
+ */
641
+ async readlinkAt(path) {
642
+ const full = this.#getFullPath(path, false);
643
+ try {
644
+ return await fs.readlink(full);
645
+ } catch (e) {
646
+ throw FSError.from(e);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Remove a directory.
652
+ * WIT:
653
+ * ```
654
+ * remove-directory-at: async func(path: string) -> result<_, error-code>
655
+ * ```
656
+ *
657
+ * @async
658
+ * @param {string} path Relative path.
659
+ * @returns {Promise<void>}
660
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
661
+ */
662
+ async removeDirectoryAt(path) {
663
+ const full = this.#getFullPath(path, false);
664
+ try {
665
+ await fs.rmdir(full);
666
+ } catch (e) {
667
+ if (process.platform === "win32" && e.code === "ENOENT") {
668
+ throw new FSError("not-directory");
669
+ }
670
+ throw FSError.from(e);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Rename a filesystem object.
676
+ * WIT:
677
+ * ```
678
+ * rename-at: async func(old-path: string, new-descriptor: borrow<descriptor>, new-path: string) -> result<_, error-code>
679
+ * ```
680
+ *
681
+ * @async
682
+ * @param {string} oldPath Source path.
683
+ * @param {Descriptor} newDesc Target descriptor.
684
+ * @param {string} newPath Destination path.
685
+ * @returns {Promise<void>}
686
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
687
+ */
688
+ async renameAt(oldPath, newDesc, newPath) {
689
+ const src = this.#getFullPath(oldPath, false);
690
+ const dst = newDesc.#getFullPath(newPath, false);
691
+ try {
692
+ await fs.rename(src, dst);
693
+ } catch (e) {
694
+ if (process.platform === "win32" && e.code === "EPERM") {
695
+ throw new FSError("access");
696
+ }
697
+ throw FSError.from(e);
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Create a symbolic link.
703
+ * WIT:
704
+ * ```
705
+ * symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>
706
+ * ```
707
+ *
708
+ * @async
709
+ * @param {string} target Link target.
710
+ * @param {string} path Destination path.
711
+ * @returns {Promise<void>}
712
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
713
+ */
714
+ async symlinkAt(target, path) {
715
+ if (target.startsWith("/")) {
716
+ throw new FSError("not-permitted");
717
+ }
718
+ const full = this.#getFullPath(path, false);
719
+
720
+ try {
721
+ await fs.symlink(target, full);
722
+ } catch (e) {
723
+ if (full.endsWith("/") && e.code === "EEXIST") {
724
+ const isDir = (await fs.stat(full)).isDirectory();
725
+ const isWin = process.platform === "win32";
726
+ if (!isDir) {
727
+ const tag = isWin ? "no-entry" : "not-directory";
728
+ throw new FSError(tag);
729
+ }
730
+ }
731
+
732
+ if (process.platform === "win32" && ["EPERM", "EEXIST"].includes(e.code)) {
733
+ throw new FSError("no-entry");
734
+ }
735
+
736
+ throw FSError.from(e);
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Unlink a filesystem object that is not a directory.
742
+ * WIT:
743
+ * ```
744
+ * unlink-file-at: async func(path: string) -> result<_, error-code>
745
+ * ```
746
+ *
747
+ * @async
748
+ * @param {string} path Relative path.
749
+ * @returns {Promise<void>}
750
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
751
+ */
752
+ async unlinkFileAt(path) {
753
+ const full = this.#getFullPath(path, false);
754
+ if (full.endsWith("/")) {
755
+ const isDir = (await fs.stat(full)).isDirectory();
756
+ if (isDir) {
757
+ if (process.platform === "win32") {
758
+ throw new FSError("access");
759
+ } else if (process.platform === "darwin") {
760
+ throw new FSError("not-permitted");
761
+ } else {
762
+ throw new FSError("is-directory");
763
+ }
764
+ }
765
+ }
766
+
767
+ try {
768
+ await fs.unlink(full);
769
+ } catch (err) {
770
+ if (process.platform === "win32" && err.code === "EPERM") {
771
+ throw new FSError("access");
772
+ } else {
773
+ throw FSError.from(err);
774
+ }
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Test whether two descriptors refer to the same object.
780
+ * WIT:
781
+ * ```
782
+ * is-same-object: async func(other: borrow<descriptor>) -> bool
783
+ * ```
784
+ *
785
+ * @async
786
+ * @param {Descriptor} other Another descriptor.
787
+ * @returns {Promise<boolean>}
788
+ */
789
+ async isSameObject(other) {
790
+ return other === this;
791
+ }
792
+
793
+ /**
794
+ * Return a hash of the metadata associated with this descriptor.
795
+ * WIT:
796
+ * ```
797
+ * metadata-hash: async func() -> result<metadata-hash-value, error-code>
798
+ * ```
799
+ *
800
+ * @async
801
+ * @returns {Promise<{upper: bigint, lower: bigint}>}
802
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
803
+ */
804
+ async metadataHash() {
805
+ if (this.#hostPreopen) {
806
+ return { upper: 0n, lower: BigInt(this._id) };
807
+ }
808
+ try {
809
+ const s = await this.#handle.stat();
810
+ return { upper: s.mtimeNs, lower: s.ino };
811
+ } catch (e) {
812
+ throw FSError.from(e);
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Return a hash of the metadata via path.
818
+ * WIT:
819
+ * ```
820
+ * metadata-hash-at: async func(path-flags: path-flags, path: string) -> result<metadata-hash-value, error-code>
821
+ * ```
822
+ *
823
+ * @async
824
+ * @param {object} flags Path flags.
825
+ * @param {string} path Relative path.
826
+ * @returns {Promise<{upper: bigint, lower: bigint}>}
827
+ * @throws {FSError} `payload.tag` contains mapped WASI error code.
828
+ */
829
+ async metadataHashAt(flags, path) {
830
+ const full = this.#getFullPath(path, false);
831
+ try {
832
+ const statFn = flags.symlinkFollow ? fs.stat : fs.lstat;
833
+ const s = await statFn(full);
834
+ return { upper: s.mtimeNs, lower: s.ino };
835
+ } catch (e) {
836
+ throw FSError.from(e);
837
+ }
838
+ }
839
+
840
+ async #syncDir() {
841
+ try {
842
+ const handle = await fs.open(this.#hostPreopen, fs.constants.O_RDONLY);
843
+ try {
844
+ await handle.sync();
845
+ } finally {
846
+ await handle.close();
847
+ }
848
+ } catch (e) {
849
+ throw FSError.from(e);
850
+ }
851
+ }
852
+
853
+ async #syncFile() {
854
+ this.#ensureHandle();
855
+ try {
856
+ await this.#handle.sync();
857
+ } catch (e) {
858
+ // On windows, `sync_data` uses `FileFlushBuffers` which fails with `EPERM` if
859
+ // the file is not upen for writing. Ignore this error, for POSIX compatibility.
860
+ if (process.platform === "win32" && e.code === "EPERM") {
861
+ return;
862
+ }
863
+ throw FSError.from(e);
864
+ }
865
+ }
866
+
867
+ #getNewTimestamp(newTimestamp, oldTimestamp) {
868
+ switch (newTimestamp.tag) {
869
+ case "now":
870
+ return Math.floor(Date.now() / 1e3);
871
+ case "no-change":
872
+ return timestampToSec(oldTimestamp);
873
+ case "timestamp":
874
+ return timestampToSec(newTimestamp.val);
875
+ default:
876
+ throw new FSError("invalid", `Unknown new-timestamp tag: ${newTimestamp.tag}`);
877
+ }
878
+ }
879
+
880
+ async #computeTimestamps(atimeDesc, mtimeDesc, path = null) {
881
+ let stats;
882
+ if (atimeDesc.tag === "no-change" || mtimeDesc.tag === "no-change") {
883
+ const flags = { symlinkFollow: true };
884
+ const p = path === null ? this.stat() : this.statAt(flags, path);
885
+ stats = await p;
886
+ }
887
+
888
+ const atime = this.#getNewTimestamp(atimeDesc, stats?.dataAccessTimestamp);
889
+ const mtime = this.#getNewTimestamp(mtimeDesc, stats?.dataModificationTimestamp);
890
+ return { atime, mtime };
891
+ }
892
+
893
+ /**
894
+ * Resolve a relative subpath against this descriptor.
895
+ */
896
+ #getFullPath(subpath, _followSymlinks) {
897
+ subpath = subpath.replaceAll("\\", "/").replace(/\/\/+/g, "/");
898
+
899
+ if (subpath.startsWith("/")) {
900
+ throw new FSError("not-permitted");
901
+ }
902
+
903
+ const segments = [];
904
+ for (const seg of subpath.split("/")) {
905
+ if (seg === "" || seg === ".") {
906
+ continue;
907
+ }
908
+ if (seg === "..") {
909
+ if (segments.length === 0) {
910
+ throw new FSError("not-permitted");
911
+ }
912
+ segments.pop();
913
+ } else {
914
+ segments.push(seg);
915
+ }
916
+ }
917
+
918
+ const base = this.#hostPreopen ?? this.#fullPath;
919
+ if (segments.length === 0) {
920
+ return base;
921
+ }
922
+
923
+ const baseNormalized = stripTrailingSlash(base);
924
+ return `${baseNormalized}/${segments.join("/")}`;
925
+ }
926
+
927
+ #ensureHandle() {
928
+ if (!this.#handle) {
929
+ throw new FSError("bad-descriptor");
930
+ }
931
+ }
932
+
933
+ #ensureWritable(data) {
934
+ this.#ensureHandle();
935
+ if (!this.#mode.write) {
936
+ data?.[symbolDispose]?.();
937
+ data?.close?.();
938
+ throw new FSError("bad-descriptor");
939
+ }
940
+ }
941
+ }
942
+
943
+ const descriptorCreatePreopen = Descriptor._createPreopen;
944
+ delete Descriptor._createPreopen;
945
+
946
+ const descriptorCreate = Descriptor._create;
947
+ delete Descriptor._create;
948
+
949
+ const NS_PER_SEC = 1_000_000_000n;
950
+ function nsToDateTime(ns) {
951
+ const seconds = ns / NS_PER_SEC;
952
+ const nanoseconds = Number(ns % NS_PER_SEC);
953
+ return { seconds, nanoseconds };
954
+ }
955
+
956
+ function timestampToSec({ seconds, nanoseconds }) {
957
+ return Number(seconds) + nanoseconds / 1e9;
958
+ }
959
+
960
+ function stripTrailingSlash(path) {
961
+ if (path === "/") {
962
+ return "/";
963
+ }
964
+ return path.replace(/\/+$/, "");
965
+ }
966
+
967
+ const preopenEntries = [];
968
+ export const preopens = {
969
+ Descriptor,
970
+ getDirectories: () => preopenEntries,
971
+ };
972
+
973
+ export const types = {
974
+ Descriptor,
975
+ };
976
+
977
+ /**
978
+ * Set the preopen table.
979
+ *
980
+ * @param {Record<string, string>} entries
981
+ */
982
+ export function _setPreopens(entries) {
983
+ preopenEntries.length = 0;
984
+ Object.entries(entries).forEach(([virtualPath, hostPreopen]) =>
985
+ _addPreopen(virtualPath, hostPreopen),
986
+ );
987
+ }
988
+
989
+ /**
990
+ * Add a new preopen mapping.
991
+ *
992
+ * @param {string} virtualPath
993
+ * @param {string} hostPreopen
994
+ */
995
+ export function _addPreopen(virtualPath, hostPreopen) {
996
+ preopenEntries.push([descriptorCreatePreopen(hostPreopen), virtualPath]);
997
+ }