@bytecodealliance/preview3-shim 0.1.2 → 0.2.1

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