@bytecodealliance/preview3-shim 0.1.2 → 0.2.0

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