@polyengine/wasi 0.1.0-pre.g633468a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,795 @@
1
+ // INTERNAL shared core for the real `wasi:filesystem` impls
2
+ // (filesystem_node.ts, filesystem_web.ts) — not a package export. The
3
+ // backend seam (`FsBackend`) is the sockets_platform.ts move applied to
4
+ // files: everything WIT-shaped (value mapping, path validation, error
5
+ // shaping, stream plumbing, resource classes) lives here once; a backend
6
+ // supplies raw handle operations and an error mapper.
7
+ //
8
+ // TRACKS. Both impls serve `@0.2` (WASI 0.2.12 WIT) and `@0.3` (WASI
9
+ // 0.3.1). The same descriptor method NAMES carry different signatures
10
+ // across tracks (`read-via-stream(offset) -> input-stream` vs
11
+ // `read-via-stream(offset) -> tuple<stream<u8>, future<...>>`), so each
12
+ // track gets its OWN resource class per `makeFilesystem` call — a guest
13
+ // links one track and never mixes instances.
14
+ //
15
+ // ERROR SHAPES (the A10 family, and the reason this file exists twice
16
+ // over): 0.2's `error-code` is an ENUM — the err payload is the bare
17
+ // kebab-case string ("no-entry") — while 0.3's is a VARIANT (it grew
18
+ // `other(option<string>)`) — the payload is `{ kind: "no-entry" }`.
19
+ // Backends throw raw platform errors; `mapError` names the code and the
20
+ // per-track guards shape it. A branded `ComponentException` from inner
21
+ // code passes through untouched; an unmapped throw would be a trap, so
22
+ // the guards map everything.
23
+ //
24
+ // SYNC vs PARKING (A14). 0.2 descriptor methods are sync WIT functions.
25
+ // A sync backend (node) returns plain values from every op — no parking,
26
+ // callback-mode guests work untouched. An async backend (OPFS) returns
27
+ // promises, so every backend-touching 0.2 method is wrapped `suspending`
28
+ // on the freshly-minted class prototype (per-call classes are what make
29
+ // the marking per-backend rather than global). The 0.3 track needs no
30
+ // marks: its methods are `async func` in WIT, and async-typed imports
31
+ // accept thenables.
32
+ //
33
+ // READ-ONLY BY DEFAULT (`makeFilesystem(..., { writable })`). Write
34
+ // access is a PACKAGE-LEVEL opt-in: one flag for the whole
35
+ // implementation, never per-preopen. The rationale is a proof
36
+ // obligation, not ergonomics. Per-preopen permissions form a lattice,
37
+ // and the two-descriptor operations (`link-at`, `rename-at`) are edges
38
+ // between its cells: each is a place where the check can be attached to
39
+ // the wrong descriptor, letting a guest bridge from a read-only preopen
40
+ // into a writable one — wasmtime-wasi shipped a vulnerability of exactly
41
+ // that shape. With a single global flag there is no lattice to bridge,
42
+ // so the obligation collapses to a closed enumeration ("every mutating
43
+ // leaf refuses"), checkable against the WIT method list rather than
44
+ // requiring per-path reasoning. Enforcement therefore lives HERE, in the
45
+ // provider, and nowhere else: both backends and both tracks inherit it
46
+ // from one site. Refusals use the WIT `read-only` error code.
47
+ //
48
+ // Two DISTINCT concerns, deliberately not merged:
49
+ // * per-descriptor flags (`requireWrite`) -> `bad-descriptor`: this
50
+ // descriptor was not opened for writing / directory mutation;
51
+ // * the global grant (`requireWritable`) -> `read-only`: this
52
+ // filesystem is read-only, whatever the descriptor says.
53
+ // The global check runs FIRST on every mutating leaf, so a read-only
54
+ // package answers `read-only` uniformly rather than leaking descriptor
55
+ // bookkeeping.
56
+ //
57
+ // PATHS. Guest paths are resolved TEXTUALLY: split on "/", drop "." and
58
+ // empty segments, ".." pops (underflow = `not-permitted`), absolute
59
+ // paths and NUL rejected. Backends receive clean, non-escaping segment
60
+ // lists. SECURITY: containment here is a CORRECTNESS mechanism, not a
61
+ // security boundary — see docs/security.md. This layer confines lookups
62
+ // TEXTUALLY only — it does
63
+ // not chase symlinks per-component (no openat2/RESOLVE_BENEATH analogue
64
+ // in node or OPFS). PHYSICAL containment is the backend's job: the node
65
+ // backend realpaths every op against the preopen root before the OS call
66
+ // (filesystem_node.ts header, issue #177), so guest-created and
67
+ // pre-existing escaping symlinks alike are refused with `not-permitted`;
68
+ // OPFS has no symlinks, so the web backend is immune by construction.
69
+ import { ComponentException, Stream, suspending } from "@polyengine/runtime/embedder";
70
+ import { FedInputStream, IoError, OutputStream, Pollable, SinkOutputStream } from "../io.js";
71
+ const OK03 = { kind: "ok" };
72
+ // --- errors ----------------------------------------------------------------------
73
+ /**
74
+ * The io `error` resource minted by filesystem STREAM failures, carrying
75
+ * the error-code so 0.2's `filesystem-error-code(borrow<error>)` can
76
+ * downcast it (SinkOutputStream preserves IoError subclasses).
77
+ */
78
+ export class FsIoError extends IoError {
79
+ code;
80
+ constructor(code, message) {
81
+ super(message);
82
+ this.code = code;
83
+ }
84
+ }
85
+ function message(e) {
86
+ return e instanceof Error ? e.message : String(e);
87
+ }
88
+ function err02(code) {
89
+ return new ComponentException(code);
90
+ }
91
+ function err03(code) {
92
+ return new ComponentException({ kind: code });
93
+ }
94
+ /** Chain over MaybeAsync without forcing sync backends through a tick. */
95
+ function chain(v, f) {
96
+ return v instanceof Promise ? v.then(f) : f(v);
97
+ }
98
+ /** Run `fn`, mapping raw throws/rejections to the track's error shape;
99
+ * branded ComponentExceptions pass through. */
100
+ function guarded(map, shape, fn) {
101
+ // The explicit annotation is what lets TS's flow analysis treat the
102
+ // catch-arm call as never-returning.
103
+ const rethrow = (e) => {
104
+ if (e instanceof ComponentException)
105
+ throw e;
106
+ throw shape(map(e));
107
+ };
108
+ try {
109
+ const r = fn();
110
+ return r instanceof Promise ? r.catch(rethrow) : r;
111
+ }
112
+ catch (e) {
113
+ return rethrow(e);
114
+ }
115
+ }
116
+ // --- paths -----------------------------------------------------------------------
117
+ /**
118
+ * Validate and normalize a guest path to non-escaping segments (module
119
+ * header). `shape` picks the track's error payload.
120
+ */
121
+ export function parsePath(path, shape) {
122
+ if (path.includes("\0"))
123
+ throw shape("invalid");
124
+ if (path.startsWith("/"))
125
+ throw shape("not-permitted");
126
+ const out = [];
127
+ for (const seg of path.split("/")) {
128
+ if (seg === "" || seg === ".")
129
+ continue;
130
+ if (seg === "..") {
131
+ if (out.length === 0)
132
+ throw shape("not-permitted");
133
+ out.pop();
134
+ continue;
135
+ }
136
+ out.push(seg);
137
+ }
138
+ return out;
139
+ }
140
+ /** Ops that name an entry (create/remove/unlink/rename/link/symlink)
141
+ * need a final component; "" or "." resolve to none. */
142
+ function requireFinal(segments, shape) {
143
+ if (segments.length === 0)
144
+ throw shape("invalid");
145
+ return segments;
146
+ }
147
+ // --- conversions -----------------------------------------------------------------
148
+ const NS_PER_SEC = 1000000000n;
149
+ function nsToDatetime(ns) {
150
+ return { seconds: ns / NS_PER_SEC, nanoseconds: Number(ns % NS_PER_SEC) };
151
+ }
152
+ function newTimestampToSpec(v) {
153
+ switch (v.kind) {
154
+ case "no-change":
155
+ case "now":
156
+ return { kind: v.kind };
157
+ case "timestamp":
158
+ return {
159
+ kind: "timestamp",
160
+ ns: v.value.seconds * NS_PER_SEC + BigInt(v.value.nanoseconds),
161
+ };
162
+ }
163
+ }
164
+ function statValue(st) {
165
+ return {
166
+ type: st.type,
167
+ linkCount: st.linkCount,
168
+ size: st.size,
169
+ ...(st.atimeNs === undefined ? {} : { dataAccessTimestamp: nsToDatetime(st.atimeNs) }),
170
+ ...(st.mtimeNs === undefined ? {} : { dataModificationTimestamp: nsToDatetime(st.mtimeNs) }),
171
+ ...(st.ctimeNs === undefined ? {} : { statusChangeTimestamp: nsToDatetime(st.ctimeNs) }),
172
+ };
173
+ }
174
+ // FNV-1a 64-bit over the identity words: a deterministic, per-object
175
+ // metadata-hash (the WIT contract is only "same object + same hash input
176
+ // => same value"; wasmtime likewise hashes host metadata).
177
+ const FNV_OFFSET = 0xcbf29ce484222325n;
178
+ const FNV_PRIME = 0x100000001b3n;
179
+ const U64 = 0xffffffffffffffffn;
180
+ function fnv64(words) {
181
+ let h = FNV_OFFSET;
182
+ for (const w of words) {
183
+ for (let i = 0n; i < 8n; i++) {
184
+ h ^= (w >> (i * 8n)) & 0xffn;
185
+ h = (h * FNV_PRIME) & U64;
186
+ }
187
+ }
188
+ return h;
189
+ }
190
+ function hashIdentity(id) {
191
+ return {
192
+ lower: fnv64([id.a, id.b]),
193
+ upper: fnv64([id.b ^ 0x9e3779b97f4a7c15n, id.a]),
194
+ };
195
+ }
196
+ const READ_CHUNK = 65536;
197
+ /** 0.2 methods wrapped `suspending` for async backends (A14; module
198
+ * header). Everything that touches the backend — stream CONSTRUCTION
199
+ * stays plain (the streams themselves park via io.ts's marks). */
200
+ const PARKED_02 = [
201
+ "advise",
202
+ "syncData",
203
+ "setSize",
204
+ "setTimes",
205
+ "read",
206
+ "write",
207
+ "readDirectory",
208
+ "sync",
209
+ "createDirectoryAt",
210
+ "stat",
211
+ "statAt",
212
+ "setTimesAt",
213
+ "linkAt",
214
+ "openAt",
215
+ "readlinkAt",
216
+ "removeDirectoryAt",
217
+ "renameAt",
218
+ "symlinkAt",
219
+ "unlinkFileAt",
220
+ "isSameObject",
221
+ "metadataHash",
222
+ "metadataHashAt",
223
+ ];
224
+ /**
225
+ * Build the two-track `wasi:filesystem` import fragment over a backend.
226
+ * `preopens`: directory handles with their guest names, served (as fresh
227
+ * per-call descriptors) by both tracks' `preopens#get-directories`.
228
+ * `access.writable` (default false) is the package-level write grant.
229
+ */
230
+ export function makeFilesystem(backend, preopens, access = {}) {
231
+ const writable = access.writable === true;
232
+ const map = (e) => backend.mapError(e);
233
+ const g02 = (fn) => guarded(map, err02, fn);
234
+ const g03 = (fn) => guarded(map, err03, fn);
235
+ /** A stream-facing sink/source error: an IoError subclass carrying the
236
+ * code, so 0.2 stream failures downcast via filesystem-error-code. */
237
+ const streamError = (e) => e instanceof FsIoError ? e : new FsIoError(map(e), message(e));
238
+ const decodeOpen = (pf, of, df) => ({
239
+ follow: pf.symlinkFollow === true,
240
+ create: of.create === true,
241
+ directory: of.directory === true,
242
+ exclusive: of.exclusive === true,
243
+ truncate: of.truncate === true,
244
+ read: df.read === true,
245
+ write: df.write === true || df.mutateDirectory === true,
246
+ });
247
+ /** Descriptor flags as VALUES, masked by the package grant: a
248
+ * read-only package never advertises `write`/`mutate-directory`, on
249
+ * preopens or on anything `open-at` mints, so a guest that checks
250
+ * flags before acting sees the same story the operations tell. */
251
+ const flagsValue = (df) => ({
252
+ read: df.read === true,
253
+ write: writable && df.write === true,
254
+ fileIntegritySync: df.fileIntegritySync === true,
255
+ dataIntegritySync: df.dataIntegritySync === true,
256
+ requestedWriteSync: df.requestedWriteSync === true,
257
+ mutateDirectory: writable && df.mutateDirectory === true,
258
+ });
259
+ const PREOPEN_FLAGS = flagsValue({ read: true, write: true, mutateDirectory: true });
260
+ /** The package-level grant. Refuses with the WIT `read-only` code —
261
+ * distinct from `requireWrite`'s per-descriptor `bad-descriptor`
262
+ * (module header). Called FIRST by every mutating leaf. */
263
+ const requireWritable = (shape) => {
264
+ if (!writable)
265
+ throw shape("read-only");
266
+ };
267
+ /** `open-at` is mutating exactly when it asks for write access or for
268
+ * an open-flag that creates/truncates: read-only means a guest cannot
269
+ * bring a file into existence either. */
270
+ const requireOpenAllowed = (of, df, shape) => {
271
+ if (writable)
272
+ return;
273
+ if (df.write === true || df.mutateDirectory === true ||
274
+ of.create === true || of.truncate === true || of.exclusive === true) {
275
+ throw shape("read-only");
276
+ }
277
+ };
278
+ /** An async pull over positional reads: the byte source for both
279
+ * tracks' read-via-stream on any backend. */
280
+ async function* readFrom(h, offset) {
281
+ let at = offset;
282
+ for (;;) {
283
+ const bytes = await backend.read(h, READ_CHUNK, at);
284
+ if (bytes.length === 0)
285
+ return;
286
+ at += bytes.length;
287
+ yield bytes;
288
+ }
289
+ }
290
+ // --- 0.2 sync streams (sync backends only: plain values, never park) --------
291
+ class SyncFileInputStream {
292
+ #h;
293
+ #cursor;
294
+ #closed = false;
295
+ constructor(h, offset) {
296
+ this.#h = h;
297
+ this.#cursor = offset;
298
+ }
299
+ read(len) {
300
+ if (this.#closed)
301
+ throw new ComponentException({ kind: "closed" });
302
+ const n = Number(len);
303
+ let bytes;
304
+ try {
305
+ bytes = backend.read(this.#h, n, this.#cursor);
306
+ }
307
+ catch (e) {
308
+ throw new ComponentException({
309
+ kind: "last-operation-failed",
310
+ value: streamError(e),
311
+ });
312
+ }
313
+ this.#cursor += bytes.length;
314
+ if (n > 0 && bytes.length === 0) {
315
+ throw new ComponentException({ kind: "closed" }); // EOF
316
+ }
317
+ return bytes;
318
+ }
319
+ blockingRead(len) {
320
+ return this.read(len);
321
+ }
322
+ skip(len) {
323
+ return BigInt(this.read(len).length);
324
+ }
325
+ blockingSkip(len) {
326
+ return this.skip(len);
327
+ }
328
+ subscribe() {
329
+ return new Pollable(); // file bytes are always "ready"
330
+ }
331
+ [Symbol.dispose]() {
332
+ this.#closed = true;
333
+ }
334
+ }
335
+ /** Sync positional/append writes ride the buffer-backed OutputStream
336
+ * base; the sink converts raw failures to stream-errors. */
337
+ const syncWriteStream = (write) => new OutputStream((chunk) => {
338
+ try {
339
+ write(chunk);
340
+ }
341
+ catch (e) {
342
+ throw new ComponentException({
343
+ kind: "last-operation-failed",
344
+ value: streamError(e),
345
+ });
346
+ }
347
+ });
348
+ /** Async sinks for SinkOutputStream: failures carry the code. */
349
+ const asyncSink = (write) => async (chunk) => {
350
+ try {
351
+ await write(chunk);
352
+ }
353
+ catch (e) {
354
+ throw streamError(e);
355
+ }
356
+ };
357
+ const requireFile = (c, shape) => {
358
+ if (c.type === "directory")
359
+ throw shape("is-directory");
360
+ };
361
+ const requireDir = (c, shape) => {
362
+ if (c.type !== "directory")
363
+ throw shape("not-directory");
364
+ };
365
+ const requireRead = (c, shape) => {
366
+ if (!c.flags.read)
367
+ throw shape("bad-descriptor");
368
+ };
369
+ const requireWrite = (c, shape) => {
370
+ if (!c.flags.write && !c.flags.mutateDirectory)
371
+ throw shape("bad-descriptor");
372
+ };
373
+ // --- the 0.2 track ---------------------------------------------------------
374
+ class DirectoryEntryStream02 {
375
+ #entries;
376
+ #at = 0;
377
+ constructor(entries) {
378
+ this.#entries = entries;
379
+ }
380
+ readDirectoryEntry() {
381
+ return this.#at < this.#entries.length ? this.#entries[this.#at++] : undefined;
382
+ }
383
+ [Symbol.dispose]() {
384
+ this.#at = this.#entries.length;
385
+ }
386
+ }
387
+ class Descriptor02 {
388
+ core;
389
+ constructor(h, type, flags) {
390
+ this.core = { h, type, flags };
391
+ }
392
+ readViaStream(offset) {
393
+ return g02(() => {
394
+ requireFile(this.core, err02);
395
+ requireRead(this.core, err02);
396
+ return backend.isSync
397
+ ? new SyncFileInputStream(this.core.h, Number(offset))
398
+ : new FedInputStream(readFrom(this.core.h, Number(offset)));
399
+ });
400
+ }
401
+ writeViaStream(offset) {
402
+ return g02(() => {
403
+ requireWritable(err02);
404
+ requireFile(this.core, err02);
405
+ requireWrite(this.core, err02);
406
+ let cursor = Number(offset);
407
+ if (backend.isSync) {
408
+ return syncWriteStream((chunk) => {
409
+ cursor += backend.write(this.core.h, chunk, cursor);
410
+ });
411
+ }
412
+ return new SinkOutputStream(asyncSink(async (chunk) => {
413
+ cursor += await backend.write(this.core.h, chunk, cursor);
414
+ }));
415
+ });
416
+ }
417
+ appendViaStream() {
418
+ return g02(() => {
419
+ requireWritable(err02);
420
+ requireFile(this.core, err02);
421
+ requireWrite(this.core, err02);
422
+ if (backend.isSync) {
423
+ return syncWriteStream((chunk) => void backend.append(this.core.h, chunk));
424
+ }
425
+ return new SinkOutputStream(asyncSink(async (chunk) => {
426
+ await backend.append(this.core.h, chunk);
427
+ }));
428
+ });
429
+ }
430
+ advise(_offset, _length, _advice) {
431
+ return g02(() => requireFile(this.core, err02)); // advisory: accept and ignore
432
+ }
433
+ syncData() {
434
+ return g02(() => backend.syncData(this.core.h));
435
+ }
436
+ getFlags() {
437
+ return { ...this.core.flags };
438
+ }
439
+ getType() {
440
+ return this.core.type;
441
+ }
442
+ setSize(size) {
443
+ return g02(() => {
444
+ requireWritable(err02);
445
+ requireWrite(this.core, err02);
446
+ return backend.setSize(this.core.h, Number(size));
447
+ });
448
+ }
449
+ setTimes(atime, mtime) {
450
+ return g02(() => {
451
+ requireWritable(err02);
452
+ requireWrite(this.core, err02);
453
+ return backend.setTimes(this.core.h, newTimestampToSpec(atime), newTimestampToSpec(mtime));
454
+ });
455
+ }
456
+ read(length, offset) {
457
+ return g02(() => {
458
+ requireFile(this.core, err02);
459
+ requireRead(this.core, err02);
460
+ const n = Number(length);
461
+ return chain(backend.read(this.core.h, n, Number(offset)), (bytes) => [bytes, n > 0 && bytes.length === 0]);
462
+ });
463
+ }
464
+ write(buffer, offset) {
465
+ return g02(() => {
466
+ requireWritable(err02);
467
+ requireFile(this.core, err02);
468
+ requireWrite(this.core, err02);
469
+ return chain(backend.write(this.core.h, buffer, Number(offset)), BigInt);
470
+ });
471
+ }
472
+ readDirectory() {
473
+ return g02(() => {
474
+ requireDir(this.core, err02);
475
+ return chain(backend.readDirectory(this.core.h), (entries) => new DirectoryEntryStream02(entries));
476
+ });
477
+ }
478
+ sync() {
479
+ return g02(() => backend.syncAll(this.core.h));
480
+ }
481
+ createDirectoryAt(path) {
482
+ return g02(() => {
483
+ requireWritable(err02);
484
+ requireWrite(this.core, err02);
485
+ return backend.createDirectoryAt(this.core.h, requireFinal(parsePath(path, err02), err02));
486
+ });
487
+ }
488
+ stat() {
489
+ return g02(() => chain(backend.stat(this.core.h), statValue));
490
+ }
491
+ statAt(pathFlags, path) {
492
+ return g02(() => chain(backend.statAt(this.core.h, parsePath(path, err02), pathFlags.symlinkFollow === true), statValue));
493
+ }
494
+ setTimesAt(pathFlags, path, atime, mtime) {
495
+ return g02(() => {
496
+ requireWritable(err02);
497
+ requireWrite(this.core, err02);
498
+ return backend.setTimesAt(this.core.h, parsePath(path, err02), pathFlags.symlinkFollow === true, newTimestampToSpec(atime), newTimestampToSpec(mtime));
499
+ });
500
+ }
501
+ linkAt(oldPathFlags, oldPath, newDescriptor, newPath) {
502
+ return g02(() => {
503
+ requireWritable(err02);
504
+ if (backend.linkAt === undefined)
505
+ throw err02("unsupported");
506
+ // Both ends: a two-descriptor op checked on one side only is the
507
+ // classic bridge bug (module header).
508
+ requireWrite(this.core, err02);
509
+ requireWrite(newDescriptor.core, err02);
510
+ return backend.linkAt(this.core.h, requireFinal(parsePath(oldPath, err02), err02), oldPathFlags.symlinkFollow === true, newDescriptor.core.h, requireFinal(parsePath(newPath, err02), err02));
511
+ });
512
+ }
513
+ openAt(pathFlags, path, openFlags, flags) {
514
+ return g02(() => {
515
+ requireOpenAllowed(openFlags, flags, err02);
516
+ return chain(backend.openAt(this.core.h, parsePath(path, err02), decodeOpen(pathFlags, openFlags, flags)), ({ handle, type }) => new Descriptor02(handle, type, flagsValue(flags)));
517
+ });
518
+ }
519
+ readlinkAt(path) {
520
+ return g02(() => {
521
+ if (backend.readlinkAt === undefined)
522
+ throw err02("unsupported");
523
+ return backend.readlinkAt(this.core.h, requireFinal(parsePath(path, err02), err02));
524
+ });
525
+ }
526
+ removeDirectoryAt(path) {
527
+ return g02(() => {
528
+ requireWritable(err02);
529
+ requireWrite(this.core, err02);
530
+ return backend.removeDirectoryAt(this.core.h, requireFinal(parsePath(path, err02), err02));
531
+ });
532
+ }
533
+ renameAt(oldPath, newDescriptor, newPath) {
534
+ return g02(() => {
535
+ requireWritable(err02);
536
+ // Both ends (see link-at).
537
+ requireWrite(this.core, err02);
538
+ requireWrite(newDescriptor.core, err02);
539
+ return backend.renameAt(this.core.h, requireFinal(parsePath(oldPath, err02), err02), newDescriptor.core.h, requireFinal(parsePath(newPath, err02), err02));
540
+ });
541
+ }
542
+ symlinkAt(oldPath, newPath) {
543
+ return g02(() => {
544
+ requireWritable(err02);
545
+ if (backend.symlinkAt === undefined)
546
+ throw err02("unsupported");
547
+ requireWrite(this.core, err02);
548
+ // old-path is the link CONTENTS (never validated as a lookup path).
549
+ return backend.symlinkAt(oldPath, this.core.h, requireFinal(parsePath(newPath, err02), err02));
550
+ });
551
+ }
552
+ unlinkFileAt(path) {
553
+ return g02(() => {
554
+ requireWritable(err02);
555
+ requireWrite(this.core, err02);
556
+ return backend.unlinkFileAt(this.core.h, requireFinal(parsePath(path, err02), err02));
557
+ });
558
+ }
559
+ /** Returns bool, not result: backend failures TRAP (unguarded). */
560
+ isSameObject(other) {
561
+ return backend.isSame(this.core.h, other.core.h);
562
+ }
563
+ metadataHash() {
564
+ return g02(() => chain(backend.identity(this.core.h), hashIdentity));
565
+ }
566
+ metadataHashAt(pathFlags, path) {
567
+ return g02(() => chain(backend.identityAt(this.core.h, parsePath(path, err02), pathFlags.symlinkFollow === true), hashIdentity));
568
+ }
569
+ [Symbol.dispose]() {
570
+ backend.close(this.core.h);
571
+ }
572
+ }
573
+ // --- the 0.3 track ---------------------------------------------------------
574
+ class Descriptor03 {
575
+ core;
576
+ constructor(h, type, flags) {
577
+ this.core = { h, type, flags };
578
+ }
579
+ /** tuple<stream<u8>, future<result<_, error-code>>> */
580
+ readViaStream(offset) {
581
+ requireFile(this.core, err03);
582
+ requireRead(this.core, err03);
583
+ let settle;
584
+ const done = new Promise((r) => (settle = r));
585
+ const h = this.core.h;
586
+ const source = (async function* () {
587
+ try {
588
+ yield* readFrom(h, Number(offset));
589
+ settle(OK03);
590
+ }
591
+ catch (e) {
592
+ settle({ kind: "err", value: { kind: map(e) } });
593
+ }
594
+ finally {
595
+ settle(OK03); // reader dropped early: no-op if already settled
596
+ }
597
+ })();
598
+ return [source, done];
599
+ }
600
+ /** The promise IS the future source (A12): drain the guest's stream. */
601
+ async writeViaStream(data, offset) {
602
+ try {
603
+ requireWritable(err03);
604
+ requireFile(this.core, err03);
605
+ requireWrite(this.core, err03);
606
+ let cursor = Number(offset);
607
+ for await (const chunk of data) {
608
+ const bytes = chunk instanceof Uint8Array ? chunk : Uint8Array.from(chunk);
609
+ cursor += await backend.write(this.core.h, bytes, cursor);
610
+ }
611
+ return OK03;
612
+ }
613
+ catch (e) {
614
+ if (data instanceof Stream)
615
+ data.drop(); // the guest's writer must not hang
616
+ return {
617
+ kind: "err",
618
+ value: { kind: e instanceof ComponentException ? e.payload.kind : map(e) },
619
+ };
620
+ }
621
+ }
622
+ async appendViaStream(data) {
623
+ try {
624
+ requireWritable(err03);
625
+ requireFile(this.core, err03);
626
+ requireWrite(this.core, err03);
627
+ for await (const chunk of data) {
628
+ const bytes = chunk instanceof Uint8Array ? chunk : Uint8Array.from(chunk);
629
+ await backend.append(this.core.h, bytes);
630
+ }
631
+ return OK03;
632
+ }
633
+ catch (e) {
634
+ if (data instanceof Stream)
635
+ data.drop();
636
+ return {
637
+ kind: "err",
638
+ value: { kind: e instanceof ComponentException ? e.payload.kind : map(e) },
639
+ };
640
+ }
641
+ }
642
+ advise(_offset, _length, _advice) {
643
+ return g03(() => requireFile(this.core, err03));
644
+ }
645
+ syncData() {
646
+ return g03(() => backend.syncData(this.core.h));
647
+ }
648
+ getFlags() {
649
+ return { ...this.core.flags };
650
+ }
651
+ getType() {
652
+ return this.core.type;
653
+ }
654
+ setSize(size) {
655
+ return g03(() => {
656
+ requireWritable(err03);
657
+ requireWrite(this.core, err03);
658
+ return backend.setSize(this.core.h, Number(size));
659
+ });
660
+ }
661
+ setTimes(atime, mtime) {
662
+ return g03(() => {
663
+ requireWritable(err03);
664
+ requireWrite(this.core, err03);
665
+ return backend.setTimes(this.core.h, newTimestampToSpec(atime), newTimestampToSpec(mtime));
666
+ });
667
+ }
668
+ /** tuple<stream<directory-entry>, future<result<_, error-code>>> */
669
+ readDirectory() {
670
+ return g03(() => {
671
+ requireDir(this.core, err03);
672
+ return chain(backend.readDirectory(this.core.h), (entries) => [
673
+ entries,
674
+ Promise.resolve(OK03),
675
+ ]);
676
+ });
677
+ }
678
+ sync() {
679
+ return g03(() => backend.syncAll(this.core.h));
680
+ }
681
+ createDirectoryAt(path) {
682
+ return g03(() => {
683
+ requireWritable(err03);
684
+ requireWrite(this.core, err03);
685
+ return backend.createDirectoryAt(this.core.h, requireFinal(parsePath(path, err03), err03));
686
+ });
687
+ }
688
+ stat() {
689
+ return g03(() => chain(backend.stat(this.core.h), statValue));
690
+ }
691
+ statAt(pathFlags, path) {
692
+ return g03(() => chain(backend.statAt(this.core.h, parsePath(path, err03), pathFlags.symlinkFollow === true), statValue));
693
+ }
694
+ setTimesAt(pathFlags, path, atime, mtime) {
695
+ return g03(() => {
696
+ requireWritable(err03);
697
+ requireWrite(this.core, err03);
698
+ return backend.setTimesAt(this.core.h, parsePath(path, err03), pathFlags.symlinkFollow === true, newTimestampToSpec(atime), newTimestampToSpec(mtime));
699
+ });
700
+ }
701
+ linkAt(oldPathFlags, oldPath, newDescriptor, newPath) {
702
+ return g03(() => {
703
+ requireWritable(err03);
704
+ if (backend.linkAt === undefined)
705
+ throw err03("unsupported");
706
+ // Both ends: a two-descriptor op checked on one side only is the
707
+ // classic bridge bug (module header).
708
+ requireWrite(this.core, err03);
709
+ requireWrite(newDescriptor.core, err03);
710
+ return backend.linkAt(this.core.h, requireFinal(parsePath(oldPath, err03), err03), oldPathFlags.symlinkFollow === true, newDescriptor.core.h, requireFinal(parsePath(newPath, err03), err03));
711
+ });
712
+ }
713
+ openAt(pathFlags, path, openFlags, flags) {
714
+ return g03(() => {
715
+ requireOpenAllowed(openFlags, flags, err03);
716
+ return chain(backend.openAt(this.core.h, parsePath(path, err03), decodeOpen(pathFlags, openFlags, flags)), ({ handle, type }) => new Descriptor03(handle, type, flagsValue(flags)));
717
+ });
718
+ }
719
+ readlinkAt(path) {
720
+ return g03(() => {
721
+ if (backend.readlinkAt === undefined)
722
+ throw err03("unsupported");
723
+ return backend.readlinkAt(this.core.h, requireFinal(parsePath(path, err03), err03));
724
+ });
725
+ }
726
+ removeDirectoryAt(path) {
727
+ return g03(() => {
728
+ requireWritable(err03);
729
+ requireWrite(this.core, err03);
730
+ return backend.removeDirectoryAt(this.core.h, requireFinal(parsePath(path, err03), err03));
731
+ });
732
+ }
733
+ renameAt(oldPath, newDescriptor, newPath) {
734
+ return g03(() => {
735
+ requireWritable(err03);
736
+ // Both ends (see link-at).
737
+ requireWrite(this.core, err03);
738
+ requireWrite(newDescriptor.core, err03);
739
+ return backend.renameAt(this.core.h, requireFinal(parsePath(oldPath, err03), err03), newDescriptor.core.h, requireFinal(parsePath(newPath, err03), err03));
740
+ });
741
+ }
742
+ symlinkAt(oldPath, newPath) {
743
+ return g03(() => {
744
+ requireWritable(err03);
745
+ if (backend.symlinkAt === undefined)
746
+ throw err03("unsupported");
747
+ requireWrite(this.core, err03);
748
+ return backend.symlinkAt(oldPath, this.core.h, requireFinal(parsePath(newPath, err03), err03));
749
+ });
750
+ }
751
+ unlinkFileAt(path) {
752
+ return g03(() => {
753
+ requireWritable(err03);
754
+ requireWrite(this.core, err03);
755
+ return backend.unlinkFileAt(this.core.h, requireFinal(parsePath(path, err03), err03));
756
+ });
757
+ }
758
+ isSameObject(other) {
759
+ return backend.isSame(this.core.h, other.core.h); // bool, not result: raw throw = trap
760
+ }
761
+ metadataHash() {
762
+ return g03(() => chain(backend.identity(this.core.h), hashIdentity));
763
+ }
764
+ metadataHashAt(pathFlags, path) {
765
+ return g03(() => chain(backend.identityAt(this.core.h, parsePath(path, err03), pathFlags.symlinkFollow === true), hashIdentity));
766
+ }
767
+ [Symbol.dispose]() {
768
+ backend.close(this.core.h);
769
+ }
770
+ }
771
+ // Async backends: mark the 0.2 track's backend-touching methods
772
+ // park-capable on the freshly-minted prototype (module header; A14).
773
+ if (!backend.isSync) {
774
+ const proto = Descriptor02.prototype;
775
+ for (const name of PARKED_02) {
776
+ proto[name] = suspending(proto[name]);
777
+ }
778
+ }
779
+ const getDirectories02 = () => preopens.map(([h, name]) => [new Descriptor02(h, "directory", PREOPEN_FLAGS), name]);
780
+ const getDirectories03 = () => preopens.map(([h, name]) => [new Descriptor03(h, "directory", PREOPEN_FLAGS), name]);
781
+ return {
782
+ imports: {
783
+ "wasi:filesystem/types@0.2": {
784
+ Descriptor: Descriptor02,
785
+ DirectoryEntryStream: DirectoryEntryStream02,
786
+ // `filesystem-error-code(err: borrow<error>) -> option<error-code>`:
787
+ // downcast succeeds exactly for the io errors OUR streams minted.
788
+ filesystemErrorCode: (err) => err instanceof FsIoError ? err.code : undefined,
789
+ },
790
+ "wasi:filesystem/preopens@0.2": { getDirectories: getDirectories02 },
791
+ "wasi:filesystem/types@0.3": { Descriptor: Descriptor03 },
792
+ "wasi:filesystem/preopens@0.3": { getDirectories: getDirectories03 },
793
+ },
794
+ };
795
+ }