@nimbus-sh/worker 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +24 -24
- package/dist/facets/cirrus-real.js +3 -3
- package/dist/facets/compose.d.ts +3 -0
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +10 -3
- package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
- package/dist/facets/esbuild-bundle-pool.js +3 -3
- package/dist/facets/esbuild-transform.d.ts +6 -2
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +8 -4
- package/dist/facets/manager.d.ts +54 -15
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +227 -144
- package/dist/facets/real-vite-hmr.d.ts +2 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +3 -2
- package/dist/git/commands.d.ts +17 -1
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +73 -9
- package/dist/git/network-facet.d.ts +2 -0
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +10 -5
- package/dist/hosted/commands.d.ts.map +1 -1
- package/dist/hosted/commands.js +28 -39
- package/dist/hosted/runtime.d.ts +24 -2
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +70 -13
- package/dist/hosted/services.d.ts +14 -2
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +15 -2
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +4 -4
- package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
- package/dist/loaders/pre-bundle-preamble.js +13 -8
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/npm/cache.d.ts +6 -1
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +8 -6
- package/dist/npm/install-batch-facet.d.ts +2 -0
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +29 -15
- package/dist/npm/installer.d.ts +8 -4
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +173 -113
- package/dist/npm/npx-install.d.ts +3 -1
- package/dist/npm/npx-install.d.ts.map +1 -1
- package/dist/npm/npx-install.js +4 -1
- package/dist/npm/placement.d.ts +16 -0
- package/dist/npm/placement.d.ts.map +1 -0
- package/dist/npm/placement.js +33 -0
- package/dist/npm/r2-cache.d.ts +10 -6
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +32 -16
- package/dist/npm/resolve-one-facet.d.ts +3 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +1 -0
- package/dist/npm/resolver.d.ts +11 -6
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +12 -3
- package/dist/runtime/bash-repl.d.ts +5 -3
- package/dist/runtime/bash-repl.d.ts.map +1 -1
- package/dist/runtime/bash-repl.js +8 -3
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +2 -10
- package/dist/runtime/esbuild-wasm-bytes.d.ts +25 -18
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +55 -30
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +0 -9
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +853 -181
- package/dist/runtime/python-repl.d.ts +4 -3
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +10 -39
- package/dist/runtime/ruby-repl.d.ts +3 -2
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +7 -6
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +5 -16
- package/dist/runtime/runtime-catalog.js +1 -1
- package/dist/runtime/wasi/preamble.d.ts +0 -22
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +160 -1445
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/hibernation.d.ts +45 -2
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +78 -1
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +1 -0
- package/dist/session/nimbus-session.d.ts +16 -23
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +25 -36
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +1 -0
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +11 -1
- package/dist/session/programmatic.js +2 -2
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +4 -2
- package/dist/session/rpc.d.ts +7 -44
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +17 -67
- package/dist/session/supervisor-op.d.ts +8 -3
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +10 -45
- package/dist/session/supervisor-rpc.d.ts +47 -22
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +64 -26
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +4 -2
- package/dist/workspace-host.d.ts +2 -1
- package/dist/workspace-host.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.js +3 -0
- package/package.json +30 -4
- package/public/_assets/esbuild-0.24.2.js +2380 -0
- package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-7b4dd7a76d95d21e.js} +852 -180
- package/scripts/bundle-esbuild-wasm.mjs +55 -39
- package/scripts/bundle-facet-workers.mjs +45 -7
- package/scripts/bundle-runtime.mjs +35 -8
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { installAuthorityFilesystem, WASI_ACCEPTED_PATH_PREFIX, WASI_LISTEN_PATH_PREFIX, WASI_TCP_PATH_PREFIX, } from '@nimbus-sh/core/runtime/wasi/filesystem.js';
|
|
2
|
+
import { supervisorFilesystem } from '@nimbus-sh/core/runtime/vfs-supervisor.js';
|
|
3
|
+
import { WASI_RESIDENT_FILE_CAP_BYTES } from '@nimbus-sh/core/constants.js';
|
|
1
4
|
// errno constants
|
|
2
5
|
const __WASI_ESUCCESS = 0;
|
|
3
6
|
const __WASI_EAGAIN = 6;
|
|
@@ -65,21 +68,6 @@ const __WASI_EVENTTYPE_CLOCK = 0;
|
|
|
65
68
|
const __WASI_EVENTTYPE_FD_READ = 1;
|
|
66
69
|
const __WASI_EVENTTYPE_FD_WRITE = 2;
|
|
67
70
|
const __WASI_SUBCLOCKFLAGS_ABSTIME = 1; // SUBSCRIPTION_CLOCK_ABSTIME
|
|
68
|
-
// WASI socket and polling support B7: synthetic-path prefix recognised by path_open as a request
|
|
69
|
-
// to open a TCP socket. Mirrors bash's /dev/tcp/<host>/<port> convention
|
|
70
|
-
// (https://www.gnu.org/software/bash/manual/html_node/Redirections.html).
|
|
71
|
-
const __WASI_TCP_PATH_PREFIX = '/dev/tcp/';
|
|
72
|
-
// The accept half of the same synthetic-socket family: binds a connection the
|
|
73
|
-
// virtual socket kernel has already accepted to a file descriptor. It has to go
|
|
74
|
-
// through path_open like the dial half - guests layered over wasi-vfs
|
|
75
|
-
// (ruby.wasm) resolve descriptors through their own fd table, so a descriptor
|
|
76
|
-
// handed to them out of band is not one they can use.
|
|
77
|
-
const __WASI_ACCEPTED_PATH_PREFIX = '/dev/nimbus/socket/';
|
|
78
|
-
// A listening socket, as a descriptor. Reading it is accept(2): the read
|
|
79
|
-
// suspends until a connection is queued and yields that connection's id, so a
|
|
80
|
-
// server's accept loop is an ordinary blocking read and needs no cooperative
|
|
81
|
-
// pump driving it from outside.
|
|
82
|
-
const __WASI_LISTEN_PATH_PREFIX = '/dev/nimbus/listen/';
|
|
83
71
|
// WASI socket and polling support B7: resolved at preamble module-init via dynamic import.
|
|
84
72
|
// CF docs: "TCP sockets cannot be created in global scope and shared
|
|
85
73
|
// across requests" — so we only IMPORT the module here (cheap symbol
|
|
@@ -100,49 +88,28 @@ catch (__e) {
|
|
|
100
88
|
class __WasiExit {
|
|
101
89
|
constructor(code) { this.code = code | 0; }
|
|
102
90
|
}
|
|
103
|
-
//
|
|
91
|
+
// The device a pathless descriptor (stdio, a socket) reports in fd_filestat_get.
|
|
92
|
+
// Above any device the authority can hand out, so a fd-numbered inode on it
|
|
93
|
+
// never collides with a file's (dev, ino).
|
|
94
|
+
const __WASI_STREAM_DEV = 1n << 32n;
|
|
95
|
+
// ─── Filesystem state ───────────────────────────────────────────────────
|
|
104
96
|
//
|
|
105
|
-
// __wasiInitFS({ root, preopens,
|
|
106
|
-
//
|
|
107
|
-
// root string — canonical session root, e.g. 'home/user
|
|
97
|
+
// __wasiInitFS({ root, preopens, residentFileCap? }) installs a per-call
|
|
98
|
+
// filesystem view:
|
|
99
|
+
// root string — canonical session root, e.g. 'home/user'.
|
|
108
100
|
// preopens Array<{ wasiPath, vfsPath }> — fd>=3 preopens (in order).
|
|
109
|
-
// files Record<vfsPath, base64 string> — initial file contents.
|
|
110
|
-
// dirs Array<vfsPath> — initial directory list.
|
|
111
|
-
// modes Record<vfsPath, effective rwx bits> — access already resolved
|
|
112
|
-
// for the invoking credential by the supervisor.
|
|
113
|
-
// times Record<vfsPath, {mtime, atime, ctime}> | undefined
|
|
114
|
-
// — per-path nanosecond timestamps (WASI socket and polling support B1). Optional;
|
|
115
|
-
// paths without entries default to "now" at init time.
|
|
116
|
-
// Values are decimal strings to survive JSON.serialize
|
|
117
|
-
// (BigInt is not JSON-safe); converted to BigInt internally.
|
|
118
|
-
// symlinks Record<vfsPath, targetPath> | undefined
|
|
119
|
-
// — WASI socket and polling support B3 in-memory symlink table. Optional; defaults
|
|
120
|
-
// to empty. Target paths are canonicalized at insertion.
|
|
121
101
|
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
102
|
+
// There is no in-facet copy of the filesystem. Every file and directory
|
|
103
|
+
// syscall is answered by the authority codec (wasi/filesystem.ts) through the
|
|
104
|
+
// supervisor stub __wasiAdoptSupervisor installs, reading and writing the same
|
|
105
|
+
// inodes the shell does. What this module keeps is the descriptor table's
|
|
106
|
+
// non-file kinds: stdio, sockets, listeners and the preopen roots.
|
|
125
107
|
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// them. Eight other places DID check, and answered null/0/false, so the same
|
|
130
|
-
// pre-init read was a TypeError down one path and a plausible wrong answer down
|
|
131
|
-
// another. wasm-runner worked around it by calling __wasiInitFS with an empty
|
|
132
|
-
// seed purely so "__wasiFS isn't null when WASI fns are called".
|
|
133
|
-
//
|
|
134
|
-
// Starting empty makes the invariant structural instead of a caller's duty: a
|
|
135
|
-
// syscall before init now answers ENOENT against an empty filesystem, which is
|
|
136
|
-
// true, rather than trapping the guest or being individually guarded 113 times.
|
|
108
|
+
// Starting empty rather than null keeps the invariant structural: a syscall
|
|
109
|
+
// before init answers EBADF against an empty descriptor table, which is true,
|
|
110
|
+
// rather than trapping the guest.
|
|
137
111
|
function __wasiEmptyFS() {
|
|
138
|
-
return {
|
|
139
|
-
root: '',
|
|
140
|
-
files: new Map(), dirs: new Set(), times: new Map(), symlinks: new Map(),
|
|
141
|
-
modes: new Map(), sizes: new Map(), origFiles: new Map(),
|
|
142
|
-
residentFileCap: 8 * 1024 * 1024,
|
|
143
|
-
enumeratedRoots: [],
|
|
144
|
-
revision: null,
|
|
145
|
-
};
|
|
112
|
+
return { root: '', residentFileCap: WASI_RESIDENT_FILE_CAP_BYTES };
|
|
146
113
|
}
|
|
147
114
|
let __wasiFS = __wasiEmptyFS();
|
|
148
115
|
let __wasiPreopens = [];
|
|
@@ -157,186 +124,21 @@ let __wasiPreopens = [];
|
|
|
157
124
|
let __wasiThreads = null;
|
|
158
125
|
// ── Live backing store ───────────────────────────────────────────────────
|
|
159
126
|
//
|
|
160
|
-
// The supervisor stub
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
127
|
+
// The supervisor stub. Every filesystem call the guest makes is answered by
|
|
128
|
+
// the authority codec (wasi/filesystem.ts) through it; absent (unit tests,
|
|
129
|
+
// compute-only instances) the guest has stdio, sockets and clocks but no
|
|
130
|
+
// files, and a file syscall answers EBADF.
|
|
164
131
|
let __wasiSup = null;
|
|
165
|
-
const __wasiPersistQ = { pending: [], tail: Promise.resolve(), failures: [] };
|
|
166
132
|
// Adopting is idempotent and never downgrades. A resident process re-enters
|
|
167
133
|
// through routed fetch/handleHttpRequest hops that resolve the entrypoint
|
|
168
134
|
// WITHOUT a supervisor in env; clearing the live stub on those hops would
|
|
169
|
-
// strand the process
|
|
135
|
+
// strand the process.
|
|
170
136
|
export function __wasiAdoptSupervisor(sup) {
|
|
171
137
|
if (sup)
|
|
172
138
|
__wasiSup = sup;
|
|
173
139
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
* supervisor to back it. Only manifestVfs stamps a revision, so this is
|
|
177
|
-
* exactly "the producer expected this filesystem to be live" — as opposed to
|
|
178
|
-
* a sealed instance, whose seed IS the whole filesystem by design.
|
|
179
|
-
*/
|
|
180
|
-
function __wasiExpectsLiveBacking() {
|
|
181
|
-
return __wasiFS.revision !== null && __wasiFS.revision !== undefined;
|
|
182
|
-
}
|
|
183
|
-
/** Mirror one mutation to the session VFS. Cache is already updated. */
|
|
184
|
-
function __wasiEnqueue(op, run) {
|
|
185
|
-
if (!__wasiSup) {
|
|
186
|
-
// A sealed instance keeps its mutations in memory on purpose. One that was
|
|
187
|
-
// seeded as a CACHE and has no supervisor cannot: the write is already
|
|
188
|
-
// gone. Recording it makes the next drain throw, which is the whole reason
|
|
189
|
-
// the exit-time diff could be deleted — silence here would put the data
|
|
190
|
-
// loss back, just without the second implementation.
|
|
191
|
-
if (__wasiExpectsLiveBacking()) {
|
|
192
|
-
__wasiPersistQ.failures.push(op + ': no supervisor adopted for a live-backed filesystem');
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
// Captured HERE rather than read when the continuation runs. __wasiInitFS
|
|
197
|
-
// nulls __wasiSup and resets the queue tail, but resetting the tail does not
|
|
198
|
-
// cancel continuations already chained to the old one — so a pooled isolate
|
|
199
|
-
// that re-inits with work in flight used to hand the previous program's op a
|
|
200
|
-
// null stub, lose that mutation, and then throw the resulting TypeError out
|
|
201
|
-
// of the NEXT program's drain. An op belongs to the process that queued it.
|
|
202
|
-
const sup = __wasiSup;
|
|
203
|
-
const entry = { op };
|
|
204
|
-
__wasiPersistQ.pending.push(entry);
|
|
205
|
-
__wasiPersistQ.tail = __wasiPersistQ.tail.then(async () => {
|
|
206
|
-
try {
|
|
207
|
-
await run(sup);
|
|
208
|
-
}
|
|
209
|
-
catch (e) {
|
|
210
|
-
// A failed write-back is data loss and must be visible, not swallowed.
|
|
211
|
-
__wasiPersistQ.failures.push(op + ': ' + ((e && e.message) || String(e)));
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
const i = __wasiPersistQ.pending.indexOf(entry);
|
|
215
|
-
if (i >= 0)
|
|
216
|
-
__wasiPersistQ.pending.splice(i, 1);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
// Paths with a write-back already queued. The queued op reads the cache when
|
|
221
|
-
// it runs, so a burst of fd_writes to one file coalesces into one round trip
|
|
222
|
-
// instead of one per write.
|
|
223
|
-
const __wasiDirty = new Set();
|
|
224
|
-
// Paths a live stat has already reported absent. Cleared whenever anything is
|
|
225
|
-
// created or the cache is revalidated, so a negative never outlives the fact.
|
|
226
|
-
const __wasiNegative = new Set();
|
|
227
|
-
function __wasiPersistFile(vfsPath) {
|
|
228
|
-
// No supervisor check here: __wasiEnqueue is the one place that decides what
|
|
229
|
-
// a missing one means. Marking the path dirty first also means a burst of
|
|
230
|
-
// writes to it reports at most once.
|
|
231
|
-
if (__wasiDirty.has(vfsPath))
|
|
232
|
-
return;
|
|
233
|
-
__wasiDirty.add(vfsPath);
|
|
234
|
-
// The bytes are read when the op RUNS, which is what coalesces a burst of
|
|
235
|
-
// fd_writes into one round trip. The filesystem they are read from is pinned
|
|
236
|
-
// here, though: reading the global instead meant a re-init between the queue
|
|
237
|
-
// and the drain looked up the path in the NEXT program's empty cache, found
|
|
238
|
-
// nothing, and returned successfully having written nothing at all.
|
|
239
|
-
const fs = __wasiFS;
|
|
240
|
-
__wasiEnqueue('writeFile ' + vfsPath, async (sup) => {
|
|
241
|
-
__wasiDirty.delete(vfsPath);
|
|
242
|
-
const bytes = fs && fs.files.get(vfsPath);
|
|
243
|
-
if (!bytes)
|
|
244
|
-
return;
|
|
245
|
-
await sup.writeFile(vfsPath, bytes);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Mirror an explicit utimensat. Deliberately NOT reached from the implicit
|
|
250
|
-
* mtime bump every write does — writeFile already sets it, and a round trip
|
|
251
|
-
* per fd_write would be one syscall's cost paid by all of them.
|
|
252
|
-
*/
|
|
253
|
-
function __wasiPersistTimes(vfsPath) {
|
|
254
|
-
const t = __wasiFS.times.get(vfsPath);
|
|
255
|
-
if (!t)
|
|
256
|
-
return;
|
|
257
|
-
const atimeMs = Number(t.atime / 1000000n);
|
|
258
|
-
const mtimeMs = Number(t.mtime / 1000000n);
|
|
259
|
-
__wasiEnqueue('utimes ' + vfsPath, (sup) => sup.utimes(vfsPath, atimeMs, mtimeMs));
|
|
260
|
-
}
|
|
261
|
-
function __wasiPersistSymlink(target, vfsPath) {
|
|
262
|
-
__wasiEnqueue('symlink ' + vfsPath, (sup) => sup.symlink(target, vfsPath));
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Drop cached content that matches what the supervisor already has, turning
|
|
266
|
-
* those inodes back into manifest entries so the next read refetches. Content
|
|
267
|
-
* with a write still queued is never dropped. Returns the paths evicted.
|
|
268
|
-
*
|
|
269
|
-
* This is what makes the cache a cache: memory pressure and staleness are both
|
|
270
|
-
* answered by forgetting, never by serving something known to be old.
|
|
271
|
-
*/
|
|
272
|
-
function __wasiEvictCleanContent() {
|
|
273
|
-
if (!__wasiFS || !__wasiSup)
|
|
274
|
-
return [];
|
|
275
|
-
const evicted = [];
|
|
276
|
-
for (const [path, bytes] of [...__wasiFS.files]) {
|
|
277
|
-
if (__wasiDirty.has(path))
|
|
278
|
-
continue;
|
|
279
|
-
const orig = __wasiFS.origFiles.get(path);
|
|
280
|
-
if (!orig || orig.length !== bytes.length)
|
|
281
|
-
continue;
|
|
282
|
-
let same = true;
|
|
283
|
-
for (let i = 0; i < orig.length; i++) {
|
|
284
|
-
if (orig[i] !== bytes[i]) {
|
|
285
|
-
same = false;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
if (!same)
|
|
290
|
-
continue;
|
|
291
|
-
__wasiFS.files.delete(path);
|
|
292
|
-
__wasiFS.sizes.set(path, bytes.length);
|
|
293
|
-
evicted.push(path);
|
|
294
|
-
}
|
|
295
|
-
return evicted;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Re-sync the cache with the session VFS. A resident process (a server) must
|
|
299
|
-
* see files that other processes created or changed after it spawned, so the
|
|
300
|
-
* cached content is dropped and metadata re-read on next access.
|
|
301
|
-
*/
|
|
302
|
-
export async function __wasiRevalidateFS() {
|
|
303
|
-
if (!__wasiSup || !__wasiFS)
|
|
304
|
-
return [];
|
|
305
|
-
await __wasiDrainPersist();
|
|
306
|
-
// One round trip decides it. An unchanged subtree means the cache is still
|
|
307
|
-
// exactly right, so a resident server parking between requests pays a single
|
|
308
|
-
// revision check rather than re-reading everything it had already loaded.
|
|
309
|
-
if (typeof __wasiSup.fsRevision === 'function') {
|
|
310
|
-
let revision;
|
|
311
|
-
try {
|
|
312
|
-
revision = await __wasiSup.fsRevision(__wasiFS.root);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
revision = null;
|
|
316
|
-
}
|
|
317
|
-
if (revision !== null && revision === __wasiFS.revision)
|
|
318
|
-
return [];
|
|
319
|
-
__wasiFS.revision = revision;
|
|
320
|
-
}
|
|
321
|
-
__wasiNegative.clear();
|
|
322
|
-
return __wasiEvictCleanContent();
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Await every queued mutation. Callers drain before returning a result to the
|
|
326
|
-
* supervisor and before any live read, so the supervisor's view always
|
|
327
|
-
* includes this process's own writes.
|
|
328
|
-
*/
|
|
329
|
-
export async function __wasiDrainPersist() {
|
|
330
|
-
await __wasiPersistQ.tail;
|
|
331
|
-
if (__wasiPersistQ.failures.length > 0) {
|
|
332
|
-
const failures = __wasiPersistQ.failures.slice();
|
|
333
|
-
__wasiPersistQ.failures.length = 0;
|
|
334
|
-
throw new Error('wasi persist failed: ' + failures.join('; '));
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// All paths are stored in canonical form: no leading '/', no '..', no
|
|
338
|
-
// double slashes. The wasm program sees '/foo/bar.txt' (with leading
|
|
339
|
-
// slash) but the VFS keys are 'home/user/.../foo/bar.txt'.
|
|
140
|
+
// A guest-visible path in canonical form: no leading '/', no '..', no double
|
|
141
|
+
// slashes. The socket path prefixes are matched against it.
|
|
340
142
|
function __wasiCanonicalize(p) {
|
|
341
143
|
if (!p)
|
|
342
144
|
return '';
|
|
@@ -354,122 +156,20 @@ function __wasiCanonicalize(p) {
|
|
|
354
156
|
}
|
|
355
157
|
return out.join('/');
|
|
356
158
|
}
|
|
357
|
-
// Wall-clock at init, used as the default mtime/atime/ctime for any path
|
|
358
|
-
// that arrives without an explicit times entry. Captured once per call so
|
|
359
|
-
// all "default-init" timestamps within a call share a value (mtime ==
|
|
360
|
-
// atime == ctime), matching what a real cold-load would produce.
|
|
361
|
-
// One device for this filesystem, and a stable distinct inode per path.
|
|
362
|
-
//
|
|
363
|
-
// A guest is entitled to treat (st_dev, st_ino) as an identity: LLVM's
|
|
364
|
-
// FileManager keys its directory cache on exactly that pair, GNU make and
|
|
365
|
-
// find -samefile compare it, and rsync uses it to detect hardlinks. Emitting
|
|
366
|
-
// a constant zero collapses every inode into one — clang then searched a
|
|
367
|
-
// single include directory and reported every system header missing. FNV-1a
|
|
368
|
-
// over the canonical path is stable across calls within a run and across
|
|
369
|
-
// processes seeded from the same tree, which is what those callers need.
|
|
370
|
-
const __WASI_DEV = 1n;
|
|
371
|
-
const __WASI_INO_MASK = 0xffffffffffffffffn;
|
|
372
|
-
function __wasiInode(path) {
|
|
373
|
-
let h = 14695981039346656037n;
|
|
374
|
-
const p = String(path === undefined || path === null ? '' : path);
|
|
375
|
-
for (let i = 0; i < p.length; i++) {
|
|
376
|
-
h = (h ^ BigInt(p.charCodeAt(i) & 0xff)) * 1099511628211n & __WASI_INO_MASK;
|
|
377
|
-
}
|
|
378
|
-
// 0 is the "no inode" value a caller may test for; never hand it out.
|
|
379
|
-
return h === 0n ? 1n : h;
|
|
380
|
-
}
|
|
381
|
-
function __wasiNowNs() {
|
|
382
|
-
// Date.now() is ms since epoch; multiply by 1e6 → ns.
|
|
383
|
-
return BigInt(Date.now()) * 1000000n;
|
|
384
|
-
}
|
|
385
159
|
export function __wasiInitFS(opts) {
|
|
386
|
-
// A fresh init is a fresh process: no supervisor adopted yet
|
|
387
|
-
// the previous tenant's
|
|
388
|
-
//
|
|
389
|
-
// this would let one program's state answer another program's syscalls.
|
|
160
|
+
// A fresh init is a fresh process: no supervisor adopted yet. Pools reuse an
|
|
161
|
+
// isolate across calls, so the previous tenant's stub must not answer the
|
|
162
|
+
// next program's syscalls.
|
|
390
163
|
__wasiSup = null;
|
|
391
|
-
__wasiDirty.clear();
|
|
392
|
-
__wasiNegative.clear();
|
|
393
|
-
__wasiPersistQ.pending.length = 0;
|
|
394
|
-
__wasiPersistQ.failures.length = 0;
|
|
395
|
-
__wasiPersistQ.tail = Promise.resolve();
|
|
396
|
-
const files = new Map(); // canonicalVfsPath → Uint8Array
|
|
397
|
-
const dirs = new Set(); // canonicalVfsPath
|
|
398
|
-
// WASI socket and polling support B1: parallel timestamp + symlink maps.
|
|
399
|
-
const times = new Map(); // canonicalVfsPath → {mtime: BigInt, atime: BigInt, ctime: BigInt}
|
|
400
|
-
const symlinks = new Map(); // canonicalVfsPath → targetPath (canonical)
|
|
401
|
-
const modes = new Map(); // canonicalVfsPath → effective rwx bits
|
|
402
|
-
// Mirror of the content the seed carried (and of anything demand-loaded),
|
|
403
|
-
// so the evictor can tell a cached-but-unchanged file from a dirty one.
|
|
404
|
-
const origFiles = new Map();
|
|
405
|
-
const nowNs = __wasiNowNs();
|
|
406
|
-
for (const [path, b64] of Object.entries(opts.files || {})) {
|
|
407
|
-
const canon = __wasiCanonicalize(path);
|
|
408
|
-
const bin = atob(b64);
|
|
409
|
-
const u8 = new Uint8Array(bin.length);
|
|
410
|
-
for (let i = 0; i < bin.length; i++)
|
|
411
|
-
u8[i] = bin.charCodeAt(i);
|
|
412
|
-
files.set(canon, u8);
|
|
413
|
-
origFiles.set(canon, u8.slice()); // copy so subsequent mutations detect change
|
|
414
|
-
// Default timestamps (overwritten below if opts.times has this path).
|
|
415
|
-
times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
|
|
416
|
-
}
|
|
417
|
-
for (const path of opts.dirs || []) {
|
|
418
|
-
const canon = __wasiCanonicalize(path);
|
|
419
|
-
dirs.add(canon);
|
|
420
|
-
if (!times.has(canon))
|
|
421
|
-
times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
|
|
422
|
-
}
|
|
423
|
-
for (const [path, mode] of Object.entries(opts.modes)) {
|
|
424
|
-
modes.set(__wasiCanonicalize(path), Number(mode) & 7);
|
|
425
|
-
}
|
|
426
|
-
// WASI socket and polling support B1: load explicit per-path timestamps if supervisor provided them.
|
|
427
|
-
// Values arrive as decimal strings (BigInt → JSON.stringify safe) or numbers.
|
|
428
|
-
for (const [path, t] of Object.entries(opts.times || {})) {
|
|
429
|
-
const canon = __wasiCanonicalize(path);
|
|
430
|
-
const cur = times.get(canon);
|
|
431
|
-
if (!cur)
|
|
432
|
-
continue; // path not yet in files/dirs; skip
|
|
433
|
-
const mtime = (t && t.mtime !== undefined) ? BigInt(t.mtime) : cur.mtime;
|
|
434
|
-
const atime = (t && t.atime !== undefined) ? BigInt(t.atime) : cur.atime;
|
|
435
|
-
const ctime = (t && t.ctime !== undefined) ? BigInt(t.ctime) : cur.ctime;
|
|
436
|
-
times.set(canon, { mtime, atime, ctime });
|
|
437
|
-
}
|
|
438
|
-
// WASI socket and polling support B3: load explicit symlinks if supervisor provided them.
|
|
439
|
-
for (const [path, target] of Object.entries(opts.symlinks || {})) {
|
|
440
|
-
const canon = __wasiCanonicalize(path);
|
|
441
|
-
symlinks.set(canon, String(target));
|
|
442
|
-
}
|
|
443
|
-
// Metadata-only entries: the manifest knows the file and its size, the seed
|
|
444
|
-
// did not carry its bytes. Content arrives on first read via the supervisor.
|
|
445
|
-
// A path here but absent from the files map is a cache miss, never ENOENT.
|
|
446
|
-
const sizes = new Map();
|
|
447
|
-
for (const [path, size] of Object.entries(opts.sizes || {})) {
|
|
448
|
-
const canon = __wasiCanonicalize(path);
|
|
449
|
-
sizes.set(canon, Number(size));
|
|
450
|
-
if (!times.has(canon))
|
|
451
|
-
times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
|
|
452
|
-
}
|
|
453
164
|
__wasiFS = {
|
|
454
165
|
root: __wasiCanonicalize(opts.root || ''),
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// Files at or above this size are never held whole; reads window through
|
|
458
|
-
// the supervisor instead. Keeps a 200 MiB blob from ending the isolate.
|
|
459
|
-
residentFileCap: Number(opts.residentFileCap ?? (8 * 1024 * 1024)),
|
|
460
|
-
// Roots the seed claims to have listed COMPLETELY. Only a producer that
|
|
461
|
-
// walked a subtree without exclusions may claim one. Inside such a root a
|
|
462
|
-
// path the manifest lacks is genuinely absent, so the miss is answered
|
|
463
|
-
// here instead of costing a round trip — which is most of the traffic a
|
|
464
|
-
// language runtime generates ($LOAD_PATH / sys.path probing walks
|
|
465
|
-
// candidate names that mostly do not exist).
|
|
466
|
-
enumeratedRoots: (opts.enumeratedRoots || []).map(__wasiCanonicalize),
|
|
467
|
-
// Supervisor revision the seed was built against; a park re-checks it and
|
|
468
|
-
// only then drops cached content. Without this the cache could serve
|
|
469
|
-
// content another process has since changed.
|
|
470
|
-
revision: opts.revision ?? null,
|
|
166
|
+
// Largest regular file the codec answers from a resident copy.
|
|
167
|
+
residentFileCap: Number(opts.residentFileCap ?? WASI_RESIDENT_FILE_CAP_BYTES),
|
|
471
168
|
};
|
|
472
169
|
// Reset fd table baseline; install preopens as fd 3, 4, 5, ...
|
|
170
|
+
// Emptied rather than replaced: the authority codec and the socket helpers
|
|
171
|
+
// are handed this Map, and a swap would leave half of them writing into a
|
|
172
|
+
// table nothing reads.
|
|
473
173
|
__wasiPreopens = [];
|
|
474
174
|
fdTable.clear();
|
|
475
175
|
fdTable.set(0, { kind: 'stdin' });
|
|
@@ -477,14 +177,10 @@ export function __wasiInitFS(opts) {
|
|
|
477
177
|
fdTable.set(2, { kind: 'stderr' });
|
|
478
178
|
nextFd = 3;
|
|
479
179
|
for (const po of (opts.preopens || [])) {
|
|
480
|
-
const fd =
|
|
180
|
+
const fd = __wasiAllocateFd();
|
|
481
181
|
const vfsPath = __wasiCanonicalize(po.vfsPath);
|
|
482
182
|
fdTable.set(fd, { kind: 'preopen', wasiPath: po.wasiPath, vfsPath });
|
|
483
183
|
__wasiPreopens.push({ fd, wasiPath: po.wasiPath, vfsPath });
|
|
484
|
-
if (!dirs.has(vfsPath))
|
|
485
|
-
dirs.add(vfsPath);
|
|
486
|
-
if (!times.has(vfsPath))
|
|
487
|
-
times.set(vfsPath, { mtime: nowNs, atime: nowNs, ctime: nowNs });
|
|
488
184
|
}
|
|
489
185
|
}
|
|
490
186
|
// The guest-visible absolute path a path_* call names. wasi-libc resolves an
|
|
@@ -565,7 +261,7 @@ function __wasiConnectRemote(host, port) {
|
|
|
565
261
|
// Returns a WASI errno and writes the new fd to fdOutPtr.
|
|
566
262
|
function __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
|
|
567
263
|
// pathArg shape: "/dev/tcp/<host>/<port>".
|
|
568
|
-
const tail = pathArg.substring(
|
|
264
|
+
const tail = pathArg.substring(WASI_TCP_PATH_PREFIX.length);
|
|
569
265
|
const slashIdx = tail.lastIndexOf('/');
|
|
570
266
|
if (slashIdx <= 0 || slashIdx === tail.length - 1)
|
|
571
267
|
return __WASI_EINVAL;
|
|
@@ -593,7 +289,7 @@ function __wasiKernelOrNull(what) {
|
|
|
593
289
|
// Open a listening socket as a descriptor. The port must already be bound; the
|
|
594
290
|
// descriptor is the accept queue, not the bind.
|
|
595
291
|
function __wasiOpenListener(pathArg, fdflags, fdOutPtr, writeU32LE) {
|
|
596
|
-
const port = parseInt(pathArg.substring(
|
|
292
|
+
const port = parseInt(pathArg.substring(WASI_LISTEN_PATH_PREFIX.length), 10);
|
|
597
293
|
if (!Number.isInteger(port) || port <= 0 || port >= 65536)
|
|
598
294
|
return __WASI_EINVAL;
|
|
599
295
|
const kernel = __wasiKernelOrNull('ports cannot be listened on');
|
|
@@ -620,7 +316,7 @@ function __wasiOpenListener(pathArg, fdflags, fdOutPtr, writeU32LE) {
|
|
|
620
316
|
if (typeof announce === 'function')
|
|
621
317
|
announce(port);
|
|
622
318
|
}
|
|
623
|
-
const fd =
|
|
319
|
+
const fd = __wasiAllocateFd();
|
|
624
320
|
fdTable.set(fd, { kind: 'listener', port, fdflags: fdflags | 0 });
|
|
625
321
|
writeU32LE(fdOutPtr, fd);
|
|
626
322
|
return __WASI_ESUCCESS;
|
|
@@ -670,7 +366,7 @@ async function __wasiAcceptRead(entry, iovsPtr, iovsLen, nreadPtr, writeU32LE, v
|
|
|
670
366
|
// Bind an already-accepted kernel connection to a file descriptor, so a
|
|
671
367
|
// server's accepted socket is the same kind of fd as a client's dialed one.
|
|
672
368
|
function __wasiOpenAcceptedSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
|
|
673
|
-
const id = parseInt(pathArg.substring(
|
|
369
|
+
const id = parseInt(pathArg.substring(WASI_ACCEPTED_PATH_PREFIX.length), 10);
|
|
674
370
|
if (!Number.isInteger(id) || id <= 0)
|
|
675
371
|
return __WASI_EINVAL;
|
|
676
372
|
const kernel = globalThis.__nimbusVirtualSockets;
|
|
@@ -695,7 +391,7 @@ function __wasiOpenAcceptedSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
|
|
|
695
391
|
// connection, or an accepted one. Every socket fd in the table comes from here,
|
|
696
392
|
// which is why nothing downstream has to tell them apart.
|
|
697
393
|
function __wasiAdoptSocket(socket, fdflags) {
|
|
698
|
-
const fd =
|
|
394
|
+
const fd = __wasiAllocateFd();
|
|
699
395
|
fdTable.set(fd, {
|
|
700
396
|
kind: 'socket',
|
|
701
397
|
socket,
|
|
@@ -710,203 +406,26 @@ function __wasiAdoptSocket(socket, fdflags) {
|
|
|
710
406
|
});
|
|
711
407
|
return fd;
|
|
712
408
|
}
|
|
713
|
-
// WASI socket and polling support B1+B2 helpers: update tracked timestamps for a path. Idempotent.
|
|
714
|
-
// Caller passes nanosecond BigInt(s); pass null for fields to keep unchanged.
|
|
715
|
-
function __wasiTouchTimes(canonPath, mtimeNs, atimeNs, ctimeNs) {
|
|
716
|
-
const cur = __wasiFS.times.get(canonPath);
|
|
717
|
-
if (cur) {
|
|
718
|
-
if (mtimeNs !== null)
|
|
719
|
-
cur.mtime = mtimeNs;
|
|
720
|
-
if (atimeNs !== null)
|
|
721
|
-
cur.atime = atimeNs;
|
|
722
|
-
if (ctimeNs !== null)
|
|
723
|
-
cur.ctime = ctimeNs;
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
const now = __wasiNowNs();
|
|
727
|
-
__wasiFS.times.set(canonPath, {
|
|
728
|
-
mtime: mtimeNs !== null ? mtimeNs : now,
|
|
729
|
-
atime: atimeNs !== null ? atimeNs : now,
|
|
730
|
-
ctime: ctimeNs !== null ? ctimeNs : now,
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// Convenience: bump mtime + ctime to "now" on a write/create.
|
|
735
|
-
function __wasiBumpMtime(canonPath) {
|
|
736
|
-
const now = __wasiNowNs();
|
|
737
|
-
__wasiTouchTimes(canonPath, now, null, now);
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Read named files back out as base64.
|
|
741
|
-
*
|
|
742
|
-
* A process with a supervisor has already written everything through as it
|
|
743
|
-
* happened and never calls this. A SEALED one — no supervisor, by design,
|
|
744
|
-
* because it must not be able to reach the session VFS at all — has no other
|
|
745
|
-
* channel, so its caller names the paths it wants and gets exactly those.
|
|
746
|
-
* A path with no file is simply absent from the result.
|
|
747
|
-
*/
|
|
748
|
-
export function __wasiReadFilesB64(paths) {
|
|
749
|
-
const out = {};
|
|
750
|
-
for (const path of paths) {
|
|
751
|
-
const canon = __wasiCanonicalize(path);
|
|
752
|
-
const bytes = __wasiFS.files.get(canon);
|
|
753
|
-
if (!bytes)
|
|
754
|
-
continue;
|
|
755
|
-
const CHUNK = 0x8000;
|
|
756
|
-
let s = '';
|
|
757
|
-
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
758
|
-
s += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(i + CHUNK, bytes.length)));
|
|
759
|
-
}
|
|
760
|
-
out[path] = btoa(s);
|
|
761
|
-
}
|
|
762
|
-
return out;
|
|
763
|
-
}
|
|
764
409
|
// ─── fd table ──────────────────────────────────────────────────────────
|
|
765
410
|
//
|
|
766
|
-
// Entry shapes:
|
|
411
|
+
// Entry shapes this module owns:
|
|
767
412
|
// { kind: 'stdin' | 'stdout' | 'stderr' }
|
|
768
413
|
// { kind: 'preopen', wasiPath, vfsPath, rights? }
|
|
769
|
-
// { kind: '
|
|
770
|
-
//
|
|
771
|
-
// { kind: '
|
|
414
|
+
// { kind: 'socket' | 'listener', socket, reader, writer, readBuf, ... }
|
|
415
|
+
// and the authority codec's (wasi/filesystem.ts), which shares the table:
|
|
416
|
+
// { kind: 'authority', handle, type, rights, ... } a live descriptor
|
|
417
|
+
// { kind: 'resident', stat, bytes, position, ... } a read-only copy
|
|
772
418
|
//
|
|
773
419
|
// WASI socket and polling support B6: 'rights' is an optional BigInt mask. When set, fd_fdstat_get
|
|
774
420
|
// returns it (instead of the wide-open default). fd_fdstat_set_rights
|
|
775
|
-
// writes to it. The mask is advisory
|
|
776
|
-
// rights
|
|
421
|
+
// writes to it. The mask is advisory for this module's own kinds; the codec
|
|
422
|
+
// enforces rights on its descriptors.
|
|
777
423
|
export const fdTable = new Map();
|
|
778
424
|
let nextFd = 3;
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
// wasi-path-fix: a single preopen { wasiPath: '/', vfsPath: 'home/user' }
|
|
784
|
-
// exposes the user's home as the wasm-side root. wasi-libc strips '/'
|
|
785
|
-
// from absolute paths and hands the rest to path_open. For "/tmp/x" the
|
|
786
|
-
// stripped path "tmp/x" + vfsPath "home/user" yields "home/user/tmp/x"
|
|
787
|
-
// (correct). For "/home/user/x" the stripped path "home/user/x" +
|
|
788
|
-
// vfsPath "home/user" yields "home/user/home/user/x" — the file lands
|
|
789
|
-
// at the wrong location. The user mental model is that "/home/user/x"
|
|
790
|
-
// IS the user's file at home/user/x in the VFS.
|
|
791
|
-
//
|
|
792
|
-
// Fix: in __wasiResolvePath, when baseFd is a preopen AND the supplied
|
|
793
|
-
// pathStr already starts with the preopen's vfsPath (followed by '/' or
|
|
794
|
-
// end-of-string), strip the vfsPath prefix BEFORE prepending it. This
|
|
795
|
-
// makes "/home/user/x" → wasi-libc strip → "home/user/x" → resolver
|
|
796
|
-
// strip vfsPath → "x" → final canonical "home/user/x" ✓. Paths that
|
|
797
|
-
// don't share the vfsPath prefix (like "tmp/x") follow the legacy path.
|
|
798
|
-
//
|
|
799
|
-
// Skip the strip when vfsPath is empty (no chroot mismatch possible).
|
|
800
|
-
// Be careful with segment boundaries: vfsPath "home/user" must NOT
|
|
801
|
-
// strip from path "home/userfoo/bar" (it's a different directory).
|
|
802
|
-
function __wasiResolvePath(baseFd, pathStr) {
|
|
803
|
-
const entry = fdTable.get(baseFd);
|
|
804
|
-
if (!entry)
|
|
805
|
-
return null;
|
|
806
|
-
let baseVfs;
|
|
807
|
-
if (entry.kind === 'preopen' || entry.kind === 'dir')
|
|
808
|
-
baseVfs = entry.vfsPath;
|
|
809
|
-
else
|
|
810
|
-
return null;
|
|
811
|
-
// Strip leading './' or '/' segments (POSIX legacy from wasi-libc).
|
|
812
|
-
let trimmed = pathStr.replace(/^\.?\/+/, '').replace(/^\/+/, '');
|
|
813
|
-
// wasi-path-fix: chroot-collision strip. Only applies to preopens (not
|
|
814
|
-
// 'dir' fds — those are opened via path_open with already-resolved
|
|
815
|
-
// paths). Only when baseVfs is non-empty AND trimmed starts with
|
|
816
|
-
// baseVfs at a segment boundary.
|
|
817
|
-
if (entry.kind === 'preopen' && baseVfs.length > 0) {
|
|
818
|
-
if (trimmed === baseVfs) {
|
|
819
|
-
trimmed = '';
|
|
820
|
-
}
|
|
821
|
-
else if (trimmed.length > baseVfs.length &&
|
|
822
|
-
trimmed.charCodeAt(baseVfs.length) === 47 /* '/' */ &&
|
|
823
|
-
trimmed.substring(0, baseVfs.length) === baseVfs) {
|
|
824
|
-
trimmed = trimmed.substring(baseVfs.length + 1);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return __wasiCanonicalize(baseVfs + '/' + trimmed);
|
|
828
|
-
}
|
|
829
|
-
// WASI socket and polling support B3: resolve a path WITH optional symlink-follow + POSIX loop
|
|
830
|
-
// detection. Returns {path: canonical, isSymlink: bool, err?: errno}.
|
|
831
|
-
// - If followFlag is true (default — matches LOOKUPFLAGS_SYMLINK_FOLLOW),
|
|
832
|
-
// walks the symlink chain up to __WASI_SYMLOOP_MAX depth. ELOOP if exceeded.
|
|
833
|
-
// - If followFlag is false, returns the bare resolved path; if the path
|
|
834
|
-
// itself is in __wasiFS.symlinks, isSymlink=true so callers know to
|
|
835
|
-
// open it as a symlink fd rather than a regular file.
|
|
836
|
-
// Errors return {path: '', isSymlink: false, err: __WASI_E...}.
|
|
837
|
-
function __wasiResolvePathFull(baseFd, pathStr, followFlag) {
|
|
838
|
-
let p = __wasiResolvePath(baseFd, pathStr);
|
|
839
|
-
if (p === null)
|
|
840
|
-
return { path: '', isSymlink: false, err: __WASI_EBADF };
|
|
841
|
-
if (!followFlag) {
|
|
842
|
-
return { path: p, isSymlink: __wasiFS.symlinks.has(p), err: __WASI_ESUCCESS };
|
|
843
|
-
}
|
|
844
|
-
// Follow symlinks with bounded loop counter.
|
|
845
|
-
let depth = 0;
|
|
846
|
-
while (__wasiFS.symlinks.has(p)) {
|
|
847
|
-
if (depth++ >= __WASI_SYMLOOP_MAX) {
|
|
848
|
-
return { path: p, isSymlink: true, err: __WASI_ELOOP };
|
|
849
|
-
}
|
|
850
|
-
const target = __wasiFS.symlinks.get(p);
|
|
851
|
-
// Target may be relative or absolute. Absolute targets (leading '/')
|
|
852
|
-
// resolve against the same preopen root as the original path; relative
|
|
853
|
-
// targets resolve against the directory containing the symlink.
|
|
854
|
-
let next;
|
|
855
|
-
if (target.startsWith('/')) {
|
|
856
|
-
// Resolve against the preopen of baseFd.
|
|
857
|
-
const e = fdTable.get(baseFd);
|
|
858
|
-
const baseVfs = (e && (e.kind === 'preopen' || e.kind === 'dir')) ? e.vfsPath : '';
|
|
859
|
-
next = __wasiCanonicalize(baseVfs + '/' + target);
|
|
860
|
-
}
|
|
861
|
-
else {
|
|
862
|
-
const lastSlash = p.lastIndexOf('/');
|
|
863
|
-
const parent = lastSlash >= 0 ? p.substring(0, lastSlash) : '';
|
|
864
|
-
next = __wasiCanonicalize(parent + '/' + target);
|
|
865
|
-
}
|
|
866
|
-
p = next;
|
|
867
|
-
}
|
|
868
|
-
return { path: p, isSymlink: false, err: __WASI_ESUCCESS };
|
|
869
|
-
}
|
|
870
|
-
function __wasiEffectiveMode(path) {
|
|
871
|
-
const mode = __wasiFS.modes.get(path);
|
|
872
|
-
if (mode !== undefined)
|
|
873
|
-
return mode;
|
|
874
|
-
return __wasiInodeExists(path) ? 0 : 7;
|
|
875
|
-
}
|
|
876
|
-
function __wasiInodeExists(path) {
|
|
877
|
-
return (__wasiFS.files.has(path) ||
|
|
878
|
-
__wasiFS.dirs.has(path) ||
|
|
879
|
-
__wasiFS.symlinks.has(path) ||
|
|
880
|
-
__wasiFS.modes.has(path));
|
|
881
|
-
}
|
|
882
|
-
function __wasiCheckTraversal(baseFd, resolved) {
|
|
883
|
-
const entry = fdTable.get(baseFd);
|
|
884
|
-
if (!entry || (entry.kind !== 'preopen' && entry.kind !== 'dir'))
|
|
885
|
-
return __WASI_EBADF;
|
|
886
|
-
const base = entry.vfsPath;
|
|
887
|
-
if (resolved === base)
|
|
888
|
-
return __WASI_ESUCCESS;
|
|
889
|
-
const prefix = base ? base + '/' : '';
|
|
890
|
-
const relative = resolved.startsWith(prefix) ? resolved.substring(prefix.length) : resolved;
|
|
891
|
-
const parts = relative.split('/').filter(Boolean);
|
|
892
|
-
let ancestor = base;
|
|
893
|
-
for (let i = 0; i < parts.length; i++) {
|
|
894
|
-
if (!__wasiFS.dirs.has(ancestor)) {
|
|
895
|
-
return __wasiInodeExists(ancestor) ? __WASI_ENOTDIR : __WASI_ENOENT;
|
|
896
|
-
}
|
|
897
|
-
if ((__wasiEffectiveMode(ancestor) & 1) === 0)
|
|
898
|
-
return __WASI_EACCES;
|
|
899
|
-
if (i === parts.length - 1)
|
|
900
|
-
break;
|
|
901
|
-
ancestor = ancestor ? ancestor + '/' + parts[i] : parts[i];
|
|
902
|
-
}
|
|
903
|
-
return __WASI_ESUCCESS;
|
|
904
|
-
}
|
|
905
|
-
function __wasiCheckMode(path, requested) {
|
|
906
|
-
return (__wasiEffectiveMode(path) & requested) === requested
|
|
907
|
-
? __WASI_ESUCCESS
|
|
908
|
-
: __WASI_EACCES;
|
|
909
|
-
}
|
|
425
|
+
// The only source of descriptor numbers. A second counter — one for files, one
|
|
426
|
+
// for sockets — hands the same number out twice and the later open silently
|
|
427
|
+
// destroys the earlier fd's entry.
|
|
428
|
+
function __wasiAllocateFd() { return nextFd++; }
|
|
910
429
|
// ─── makeImports ────────────────────────────────────────────────────────
|
|
911
430
|
// ── Park watchdog ────────────────────────────────────────────────────────
|
|
912
431
|
//
|
|
@@ -1009,35 +528,6 @@ export function __wasiMakeImports(opts) {
|
|
|
1009
528
|
const bytes = u8().subarray(ptr, ptr + len);
|
|
1010
529
|
return utf8dec.decode(bytes);
|
|
1011
530
|
}
|
|
1012
|
-
// The single filestat emitter for both ABIs. preview1 is 64 bytes with an
|
|
1013
|
-
// eight-byte nlink at +24; preview0 is 56 with a four-byte nlink at +20, so
|
|
1014
|
-
// every field from nlink onward shifts. A caller decoding the wrong layout
|
|
1015
|
-
// reads st_size out of the nlink slot and sees 1 — silently, since the
|
|
1016
|
-
// function signature is identical either way.
|
|
1017
|
-
function writeFilestat(statPtr, path, ftype, size, t) {
|
|
1018
|
-
const dv = view();
|
|
1019
|
-
writeU64LE(statPtr, __WASI_DEV);
|
|
1020
|
-
writeU64LE(statPtr + 8, __wasiInode(path));
|
|
1021
|
-
dv.setUint8(statPtr + 16, ftype);
|
|
1022
|
-
if (preview0) {
|
|
1023
|
-
for (let i = 17; i < 20; i++)
|
|
1024
|
-
dv.setUint8(statPtr + i, 0);
|
|
1025
|
-
dv.setUint32(statPtr + 20, 1, true); // nlink u32
|
|
1026
|
-
writeU64LE(statPtr + 24, size);
|
|
1027
|
-
writeU64LE(statPtr + 32, t.atime);
|
|
1028
|
-
writeU64LE(statPtr + 40, t.mtime);
|
|
1029
|
-
writeU64LE(statPtr + 48, t.ctime);
|
|
1030
|
-
return __WASI_ESUCCESS;
|
|
1031
|
-
}
|
|
1032
|
-
for (let i = 17; i < 24; i++)
|
|
1033
|
-
dv.setUint8(statPtr + i, 0);
|
|
1034
|
-
writeU64LE(statPtr + 24, 1n); // nlink u64
|
|
1035
|
-
writeU64LE(statPtr + 32, size);
|
|
1036
|
-
writeU64LE(statPtr + 40, t.atime);
|
|
1037
|
-
writeU64LE(statPtr + 48, t.mtime);
|
|
1038
|
-
writeU64LE(statPtr + 56, t.ctime);
|
|
1039
|
-
return __WASI_ESUCCESS;
|
|
1040
|
-
}
|
|
1041
531
|
let stdoutBuf = '';
|
|
1042
532
|
let stderrBuf = '';
|
|
1043
533
|
// A sink and the readable buffer are mutually exclusive: with a sink the
|
|
@@ -1059,65 +549,6 @@ export function __wasiMakeImports(opts) {
|
|
|
1059
549
|
stderrBuf += s;
|
|
1060
550
|
}
|
|
1061
551
|
}
|
|
1062
|
-
// ── Helpers operating on the VFS state ───────────────────────────────
|
|
1063
|
-
function getFile(vfsPath) {
|
|
1064
|
-
return __wasiFS.files.get(vfsPath) || null;
|
|
1065
|
-
}
|
|
1066
|
-
/** Known to exist — resident content or a manifest entry. */
|
|
1067
|
-
function fileKnown(vfsPath) {
|
|
1068
|
-
return __wasiFS.files.has(vfsPath) || __wasiFS.sizes.has(vfsPath);
|
|
1069
|
-
}
|
|
1070
|
-
/** Size without forcing a load. */
|
|
1071
|
-
function fileSize(vfsPath) {
|
|
1072
|
-
const resident = __wasiFS.files.get(vfsPath);
|
|
1073
|
-
if (resident)
|
|
1074
|
-
return resident.length;
|
|
1075
|
-
return __wasiFS.sizes.get(vfsPath) ?? 0;
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Bytes for [offset, offset+length) of a file. Returns synchronously when
|
|
1079
|
-
* the content is resident — JSPI passes a plain value through with no
|
|
1080
|
-
* suspender, so sync-only guests are unaffected by the live path existing.
|
|
1081
|
-
* Returns a Promise only on a genuine cache miss.
|
|
1082
|
-
*/
|
|
1083
|
-
function readRange(vfsPath, offset, length) {
|
|
1084
|
-
const resident = __wasiFS.files.get(vfsPath);
|
|
1085
|
-
if (resident) {
|
|
1086
|
-
const start = Math.min(offset, resident.length);
|
|
1087
|
-
return resident.subarray(start, Math.min(resident.length, start + length));
|
|
1088
|
-
}
|
|
1089
|
-
const size = __wasiFS.sizes.get(vfsPath);
|
|
1090
|
-
if (size === undefined)
|
|
1091
|
-
return new Uint8Array(0);
|
|
1092
|
-
// A manifest entry says "this file exists and has N bytes"; the bytes live
|
|
1093
|
-
// in the session VFS. With no supervisor there is no way to get them, and
|
|
1094
|
-
// answering with zero bytes is not a degraded read — it is a DIFFERENT
|
|
1095
|
-
// FILE, reported as a success. Ruby's require took that empty content,
|
|
1096
|
-
// defined nothing, and failed later as an undefined constant with nothing
|
|
1097
|
-
// pointing back here. Callers turn null into EIO.
|
|
1098
|
-
if (!__wasiSup)
|
|
1099
|
-
return null;
|
|
1100
|
-
// Queued writes must be visible to our own live read.
|
|
1101
|
-
return (async () => {
|
|
1102
|
-
await __wasiDrainPersist();
|
|
1103
|
-
if (size <= __wasiFS.residentFileCap) {
|
|
1104
|
-
const whole = await __wasiSup.fsReadRange(vfsPath, 0, size);
|
|
1105
|
-
const bytes = whole instanceof Uint8Array ? whole : new Uint8Array(whole);
|
|
1106
|
-
__wasiFS.files.set(vfsPath, bytes);
|
|
1107
|
-
// The diff-back mirror must agree, or a demand-loaded file would be
|
|
1108
|
-
// reported as newly written by every runner still using the diff.
|
|
1109
|
-
__wasiFS.origFiles.set(vfsPath, bytes.slice());
|
|
1110
|
-
const start = Math.min(offset, bytes.length);
|
|
1111
|
-
return bytes.subarray(start, Math.min(bytes.length, start + length));
|
|
1112
|
-
}
|
|
1113
|
-
// Too large to hold: window straight through, nothing cached.
|
|
1114
|
-
const want = Math.max(0, Math.min(length, size - offset));
|
|
1115
|
-
if (want === 0)
|
|
1116
|
-
return new Uint8Array(0);
|
|
1117
|
-
const win = await __wasiSup.fsReadRange(vfsPath, offset, want);
|
|
1118
|
-
return win instanceof Uint8Array ? win : new Uint8Array(win);
|
|
1119
|
-
})();
|
|
1120
|
-
}
|
|
1121
552
|
/** Scatter the given bytes across the iovec list; returns bytes consumed. */
|
|
1122
553
|
function scatterIovs(bytes, iovsPtr, iovsLen, dv, memU8) {
|
|
1123
554
|
let done = 0;
|
|
@@ -1135,49 +566,6 @@ export function __wasiMakeImports(opts) {
|
|
|
1135
566
|
}
|
|
1136
567
|
return done;
|
|
1137
568
|
}
|
|
1138
|
-
/**
|
|
1139
|
-
* Resolve a path the manifest does not know. Files created after this
|
|
1140
|
-
* process spawned are invisible to a seed, so a miss asks the supervisor
|
|
1141
|
-
* once and records the answer — including the negative, so a program that
|
|
1142
|
-
* probes the same absent path in a loop pays a single round trip.
|
|
1143
|
-
*/
|
|
1144
|
-
/** True when the seed listed this path's subtree exhaustively. */
|
|
1145
|
-
function underEnumeratedRoot(vfsPath) {
|
|
1146
|
-
for (const root of __wasiFS.enumeratedRoots) {
|
|
1147
|
-
if (root === '' || vfsPath === root || vfsPath.startsWith(root + '/'))
|
|
1148
|
-
return true;
|
|
1149
|
-
}
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
function statLive(vfsPath) {
|
|
1153
|
-
if (!__wasiSup || __wasiNegative.has(vfsPath))
|
|
1154
|
-
return false;
|
|
1155
|
-
// The manifest is authoritative inside a root it fully enumerated, so an
|
|
1156
|
-
// absent path there is absent — no round trip. Revalidation at a park is
|
|
1157
|
-
// what lets files created after spawn become visible.
|
|
1158
|
-
if (underEnumeratedRoot(vfsPath))
|
|
1159
|
-
return false;
|
|
1160
|
-
return (async () => {
|
|
1161
|
-
await __wasiDrainPersist();
|
|
1162
|
-
const st = await __wasiSup.stat(vfsPath);
|
|
1163
|
-
if (!st) {
|
|
1164
|
-
__wasiNegative.add(vfsPath);
|
|
1165
|
-
return false;
|
|
1166
|
-
}
|
|
1167
|
-
ensureParentDirs(vfsPath);
|
|
1168
|
-
if (st.type === 'directory') {
|
|
1169
|
-
__wasiFS.dirs.add(vfsPath);
|
|
1170
|
-
__wasiFS.modes.set(vfsPath, 7);
|
|
1171
|
-
}
|
|
1172
|
-
else {
|
|
1173
|
-
__wasiFS.sizes.set(vfsPath, Number(st.size ?? 0));
|
|
1174
|
-
__wasiFS.modes.set(vfsPath, 6);
|
|
1175
|
-
}
|
|
1176
|
-
const ns = BigInt(st.mtime ?? Date.now()) * 1000000n;
|
|
1177
|
-
__wasiFS.times.set(vfsPath, { mtime: ns, atime: ns, ctime: ns });
|
|
1178
|
-
return true;
|
|
1179
|
-
})();
|
|
1180
|
-
}
|
|
1181
569
|
/** Total capacity of an iovec list. */
|
|
1182
570
|
function iovsCapacity(iovsPtr, iovsLen, dv) {
|
|
1183
571
|
let total = 0;
|
|
@@ -1185,89 +573,6 @@ export function __wasiMakeImports(opts) {
|
|
|
1185
573
|
total += dv.getUint32(iovsPtr + i * 8 + 4, true);
|
|
1186
574
|
return total;
|
|
1187
575
|
}
|
|
1188
|
-
function hasDir(vfsPath) {
|
|
1189
|
-
if (__wasiFS.dirs.has(vfsPath))
|
|
1190
|
-
return true;
|
|
1191
|
-
// Root preopens implicitly exist as dirs.
|
|
1192
|
-
return false;
|
|
1193
|
-
}
|
|
1194
|
-
function setFile(vfsPath, bytes) {
|
|
1195
|
-
__wasiFS.files.set(vfsPath, bytes);
|
|
1196
|
-
__wasiFS.sizes.delete(vfsPath);
|
|
1197
|
-
__wasiNegative.delete(vfsPath);
|
|
1198
|
-
if (!__wasiFS.modes.has(vfsPath))
|
|
1199
|
-
__wasiFS.modes.set(vfsPath, 6);
|
|
1200
|
-
// WASI socket and polling support B1: bump mtime+ctime on every write. atime stays as-is
|
|
1201
|
-
// (read paths bump atime explicitly via touchAccess()).
|
|
1202
|
-
__wasiBumpMtime(vfsPath);
|
|
1203
|
-
__wasiPersistFile(vfsPath);
|
|
1204
|
-
}
|
|
1205
|
-
function unsetFile(vfsPath) {
|
|
1206
|
-
__wasiFS.files.delete(vfsPath);
|
|
1207
|
-
__wasiFS.sizes.delete(vfsPath);
|
|
1208
|
-
__wasiFS.times.delete(vfsPath);
|
|
1209
|
-
__wasiFS.modes.delete(vfsPath);
|
|
1210
|
-
__wasiEnqueue('unlink ' + vfsPath, (sup) => sup.unlink(vfsPath));
|
|
1211
|
-
}
|
|
1212
|
-
function touchAccess(vfsPath) {
|
|
1213
|
-
// WASI socket and polling support B1: bump atime on a read. mtime/ctime unchanged.
|
|
1214
|
-
const cur = __wasiFS.times.get(vfsPath);
|
|
1215
|
-
if (cur)
|
|
1216
|
-
cur.atime = __wasiNowNs();
|
|
1217
|
-
}
|
|
1218
|
-
function ensureParentDirs(vfsPath) {
|
|
1219
|
-
// Ensure all ancestor dirs of vfsPath exist (they should, but mkdir -p
|
|
1220
|
-
// semantics are handled in path_create_directory by callers).
|
|
1221
|
-
const parts = vfsPath.split('/');
|
|
1222
|
-
if (parts.length <= 1)
|
|
1223
|
-
return;
|
|
1224
|
-
let p = '';
|
|
1225
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1226
|
-
p = p ? (p + '/' + parts[i]) : parts[i];
|
|
1227
|
-
__wasiFS.dirs.add(p);
|
|
1228
|
-
if (!__wasiFS.modes.has(p))
|
|
1229
|
-
__wasiFS.modes.set(p, 7);
|
|
1230
|
-
// WASI socket and polling support B1: ensure ancestor dirs have a times entry.
|
|
1231
|
-
if (!__wasiFS.times.has(p)) {
|
|
1232
|
-
const now = __wasiNowNs();
|
|
1233
|
-
__wasiFS.times.set(p, { mtime: now, atime: now, ctime: now });
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
function readdirChildren(vfsPath) {
|
|
1238
|
-
const out = [];
|
|
1239
|
-
const seen = new Set();
|
|
1240
|
-
const prefix = vfsPath === '' ? '' : vfsPath + '/';
|
|
1241
|
-
// Files
|
|
1242
|
-
for (const path of __wasiFS.files.keys()) {
|
|
1243
|
-
if (path.startsWith(prefix)) {
|
|
1244
|
-
const rest = path.substring(prefix.length);
|
|
1245
|
-
if (rest && rest.indexOf('/') === -1) {
|
|
1246
|
-
out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });
|
|
1247
|
-
seen.add(rest);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
// Dirs
|
|
1252
|
-
for (const path of __wasiFS.dirs) {
|
|
1253
|
-
if (path !== vfsPath && path.startsWith(prefix)) {
|
|
1254
|
-
const rest = path.substring(prefix.length);
|
|
1255
|
-
if (rest && rest.indexOf('/') === -1) {
|
|
1256
|
-
out.push({ name: rest, type: __WASI_FT_DIRECTORY });
|
|
1257
|
-
seen.add(rest);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
for (const path of __wasiFS.modes.keys()) {
|
|
1262
|
-
if (path.startsWith(prefix)) {
|
|
1263
|
-
const rest = path.substring(prefix.length);
|
|
1264
|
-
if (rest && rest.indexOf('/') === -1 && !seen.has(rest)) {
|
|
1265
|
-
out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
return out;
|
|
1270
|
-
}
|
|
1271
576
|
const imports = {
|
|
1272
577
|
// ── args / env ──
|
|
1273
578
|
args_get(argvPtr, argvBufPtr) {
|
|
@@ -1353,32 +658,9 @@ export function __wasiMakeImports(opts) {
|
|
|
1353
658
|
if (entry.kind === 'listener') {
|
|
1354
659
|
return __wasiAcceptRead(entry, iovsPtr, iovsLen, nreadPtr, writeU32LE, view, u8);
|
|
1355
660
|
}
|
|
1356
|
-
if (entry.kind === '
|
|
661
|
+
if (entry.kind === 'preopen')
|
|
1357
662
|
return __WASI_EISDIR;
|
|
1358
|
-
|
|
1359
|
-
return __WASI_EBADF;
|
|
1360
|
-
if (!fileKnown(entry.vfsPath))
|
|
1361
|
-
return __WASI_ENOENT;
|
|
1362
|
-
const want = iovsCapacity(iovsPtr, iovsLen, view());
|
|
1363
|
-
const take = Math.min(want, Math.max(0, fileSize(entry.vfsPath) - entry.offset));
|
|
1364
|
-
if (take === 0) {
|
|
1365
|
-
writeU32LE(nreadPtr, 0);
|
|
1366
|
-
return __WASI_ESUCCESS;
|
|
1367
|
-
}
|
|
1368
|
-
const chunk = readRange(entry.vfsPath, entry.offset, take);
|
|
1369
|
-
// Re-read memory views after a possible suspension: the guest may have
|
|
1370
|
-
// grown its memory while we were parked, detaching the old buffer.
|
|
1371
|
-
const deliver = (bytes) => {
|
|
1372
|
-
// Null is "the content exists but is unreachable" — never zero bytes,
|
|
1373
|
-
// which the guest cannot tell from an empty file.
|
|
1374
|
-
if (bytes === null)
|
|
1375
|
-
return __WASI_EIO;
|
|
1376
|
-
const n = scatterIovs(bytes, iovsPtr, iovsLen, view(), u8());
|
|
1377
|
-
entry.offset += n;
|
|
1378
|
-
writeU32LE(nreadPtr, n);
|
|
1379
|
-
return __WASI_ESUCCESS;
|
|
1380
|
-
};
|
|
1381
|
-
return (chunk && typeof chunk.then === 'function') ? chunk.then(deliver) : deliver(chunk);
|
|
663
|
+
return __WASI_EBADF;
|
|
1382
664
|
},
|
|
1383
665
|
fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
|
|
1384
666
|
// wasi-libc maps write(2) to fd_write for every fd kind; a socket fd
|
|
@@ -1387,6 +669,8 @@ export function __wasiMakeImports(opts) {
|
|
|
1387
669
|
if (sockEntry && sockEntry.kind === 'socket') {
|
|
1388
670
|
return __rawSockSend(fd, iovsPtr, iovsLen, 0, nwrittenPtr);
|
|
1389
671
|
}
|
|
672
|
+
if (fd !== 1 && fd !== 2)
|
|
673
|
+
return __WASI_EBADF;
|
|
1390
674
|
const dv = view();
|
|
1391
675
|
const memU8 = u8();
|
|
1392
676
|
// Gather all iov bytes
|
|
@@ -1413,79 +697,27 @@ export function __wasiMakeImports(opts) {
|
|
|
1413
697
|
off += p.length;
|
|
1414
698
|
}
|
|
1415
699
|
}
|
|
1416
|
-
|
|
1417
|
-
appendStream(fd === 1 ? 'stdout' : 'stderr', combined);
|
|
1418
|
-
writeU32LE(nwrittenPtr, total);
|
|
1419
|
-
return __WASI_ESUCCESS;
|
|
1420
|
-
}
|
|
1421
|
-
const entry = fdTable.get(fd);
|
|
1422
|
-
if (!entry || entry.kind !== 'file')
|
|
1423
|
-
return __WASI_EBADF;
|
|
1424
|
-
const file = getFile(entry.vfsPath) || new Uint8Array(0);
|
|
1425
|
-
// O_APPEND: each write atomically seeks to EOF, ignoring the current
|
|
1426
|
-
// offset, then advances the offset past the written bytes (POSIX).
|
|
1427
|
-
const appendMode = (entry.fdflags & __WASI_FDFLAGS_APPEND) !== 0;
|
|
1428
|
-
const writeAt = appendMode ? file.length : entry.offset;
|
|
1429
|
-
const newLen = Math.max(file.length, writeAt + total);
|
|
1430
|
-
const next = new Uint8Array(newLen);
|
|
1431
|
-
next.set(file, 0);
|
|
1432
|
-
next.set(combined, writeAt);
|
|
1433
|
-
setFile(entry.vfsPath, next);
|
|
1434
|
-
entry.offset = writeAt + total;
|
|
700
|
+
appendStream(fd === 1 ? 'stdout' : 'stderr', combined);
|
|
1435
701
|
writeU32LE(nwrittenPtr, total);
|
|
1436
702
|
return __WASI_ESUCCESS;
|
|
1437
703
|
},
|
|
1438
|
-
fd_seek(fd,
|
|
1439
|
-
//
|
|
1440
|
-
// (
|
|
1441
|
-
|
|
1442
|
-
if (fd === 0 || fd === 1 || fd === 2) {
|
|
1443
|
-
// Seeking a pipe/tty is ESPIPE — lets guests detect non-seekable
|
|
1444
|
-
// stdio (POSIX lseek on a pipe).
|
|
704
|
+
fd_seek(fd, _offsetArg, _whence, _newOffsetPtr) {
|
|
705
|
+
// Seeking a pipe/tty is ESPIPE — lets guests detect non-seekable
|
|
706
|
+
// stdio (POSIX lseek on a pipe). A socket is a non-seekable stream too.
|
|
707
|
+
if (fd === 0 || fd === 1 || fd === 2)
|
|
1445
708
|
return __WASI_ESPIPE;
|
|
1446
|
-
}
|
|
1447
709
|
const entry = fdTable.get(fd);
|
|
1448
|
-
// A socket is a non-seekable stream, same as stdio: ESPIPE, not EBADF.
|
|
1449
710
|
if (entry && (entry.kind === 'socket' || entry.kind === 'listener'))
|
|
1450
711
|
return __WASI_ESPIPE;
|
|
1451
|
-
|
|
1452
|
-
return __WASI_EBADF;
|
|
1453
|
-
const delta = typeof offsetArg === 'bigint' ? offsetArg : BigInt(offsetArg | 0);
|
|
1454
|
-
const cur = BigInt(entry.offset);
|
|
1455
|
-
// fileSize, not the resident buffer: a manifest entry is a file whose
|
|
1456
|
-
// bytes have not been demand-loaded yet, and measuring it by what
|
|
1457
|
-
// happens to be in memory calls it empty. SEEK_END then lands at 0, and
|
|
1458
|
-
// every guest that sizes a file by seeking to its end — zipimport does
|
|
1459
|
-
// exactly that before looking for the end-of-central-directory record —
|
|
1460
|
-
// reads the head of the file and concludes it is not an archive.
|
|
1461
|
-
const fileLen = BigInt(fileSize(entry.vfsPath));
|
|
1462
|
-
let next;
|
|
1463
|
-
if (whence === WHENCE_SET)
|
|
1464
|
-
next = delta;
|
|
1465
|
-
else if (whence === WHENCE_CUR)
|
|
1466
|
-
next = cur + delta;
|
|
1467
|
-
else if (whence === WHENCE_END)
|
|
1468
|
-
next = fileLen + delta;
|
|
1469
|
-
else
|
|
1470
|
-
return __WASI_EINVAL;
|
|
1471
|
-
if (next < 0n)
|
|
1472
|
-
return __WASI_EINVAL;
|
|
1473
|
-
entry.offset = Number(next);
|
|
1474
|
-
writeU64LE(newOffsetPtr, next);
|
|
1475
|
-
return __WASI_ESUCCESS;
|
|
712
|
+
return __WASI_EBADF;
|
|
1476
713
|
},
|
|
1477
|
-
fd_tell(fd,
|
|
1478
|
-
if (fd === 0 || fd === 1 || fd === 2)
|
|
1479
|
-
// ftell on a pipe/tty is ESPIPE, same non-seekable rule as fd_seek.
|
|
714
|
+
fd_tell(fd, _offsetPtr) {
|
|
715
|
+
if (fd === 0 || fd === 1 || fd === 2)
|
|
1480
716
|
return __WASI_ESPIPE;
|
|
1481
|
-
}
|
|
1482
717
|
const entry = fdTable.get(fd);
|
|
1483
718
|
if (entry && (entry.kind === 'socket' || entry.kind === 'listener'))
|
|
1484
719
|
return __WASI_ESPIPE;
|
|
1485
|
-
|
|
1486
|
-
return __WASI_EBADF;
|
|
1487
|
-
writeU64LE(offsetPtr, BigInt(entry.offset));
|
|
1488
|
-
return __WASI_ESUCCESS;
|
|
720
|
+
return __WASI_EBADF;
|
|
1489
721
|
},
|
|
1490
722
|
fd_fdstat_get(fd, statPtr) {
|
|
1491
723
|
const entry = fdTable.get(fd);
|
|
@@ -1496,12 +728,9 @@ export function __wasiMakeImports(opts) {
|
|
|
1496
728
|
if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
|
|
1497
729
|
ftype = __WASI_FT_CHARACTER_DEVICE;
|
|
1498
730
|
}
|
|
1499
|
-
else if (entry.kind === 'preopen'
|
|
731
|
+
else if (entry.kind === 'preopen') {
|
|
1500
732
|
ftype = __WASI_FT_DIRECTORY;
|
|
1501
733
|
}
|
|
1502
|
-
else if (entry.kind === 'file') {
|
|
1503
|
-
ftype = __WASI_FT_REGULAR_FILE;
|
|
1504
|
-
}
|
|
1505
734
|
else if (entry.kind === 'socket' || entry.kind === 'listener') {
|
|
1506
735
|
ftype = __WASI_FT_SOCKET_STREAM; // WASI socket and polling support B7
|
|
1507
736
|
}
|
|
@@ -1525,7 +754,7 @@ export function __wasiMakeImports(opts) {
|
|
|
1525
754
|
return __WASI_EBADF;
|
|
1526
755
|
// Sockets track the mask too: guests set O_NONBLOCK on a socket and
|
|
1527
756
|
// then read it back through fd_fdstat_get.
|
|
1528
|
-
if (entry.kind === '
|
|
757
|
+
if (entry.kind === 'socket' || entry.kind === 'listener')
|
|
1529
758
|
entry.fdflags = flags;
|
|
1530
759
|
return __WASI_ESUCCESS;
|
|
1531
760
|
},
|
|
@@ -1579,399 +808,42 @@ export function __wasiMakeImports(opts) {
|
|
|
1579
808
|
// WASI socket and polling support B7: synthetic /dev/tcp/<host>/<port> path — open a TCP
|
|
1580
809
|
// socket (remote via cloudflare:sockets, loopback via the virtual
|
|
1581
810
|
// socket kernel). Bash-like convention.
|
|
1582
|
-
// Intercept BEFORE resolution because /dev/tcp/* doesn't exist in
|
|
1583
|
-
// the in-memory FS — __wasiResolvePath would canonicalize it but
|
|
1584
|
-
// not find any entry.
|
|
1585
811
|
const guestPath = __wasiGuestPath(baseFd, pathArg);
|
|
1586
|
-
if (guestPath.startsWith(
|
|
812
|
+
if (guestPath.startsWith(WASI_TCP_PATH_PREFIX)) {
|
|
1587
813
|
return __wasiOpenTcpSocket(guestPath, fdflags, fdOutPtr, writeU32LE);
|
|
1588
814
|
}
|
|
1589
|
-
if (guestPath.startsWith(
|
|
815
|
+
if (guestPath.startsWith(WASI_ACCEPTED_PATH_PREFIX)) {
|
|
1590
816
|
return __wasiOpenAcceptedSocket(guestPath, fdflags, fdOutPtr, writeU32LE);
|
|
1591
817
|
}
|
|
1592
|
-
if (guestPath.startsWith(
|
|
818
|
+
if (guestPath.startsWith(WASI_LISTEN_PATH_PREFIX)) {
|
|
1593
819
|
return __wasiOpenListener(guestPath, fdflags, fdOutPtr, writeU32LE);
|
|
1594
820
|
}
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
const follow = (dirflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
|
|
1599
|
-
const rp = __wasiResolvePathFull(baseFd, pathArg, follow);
|
|
1600
|
-
if (rp.err === __WASI_EBADF)
|
|
1601
|
-
return __WASI_EBADF;
|
|
1602
|
-
if (rp.err === __WASI_ELOOP)
|
|
1603
|
-
return __WASI_ELOOP;
|
|
1604
|
-
const resolved = rp.path;
|
|
1605
|
-
const traversal = __wasiCheckTraversal(baseFd, resolved);
|
|
1606
|
-
if (traversal !== __WASI_ESUCCESS)
|
|
1607
|
-
return traversal;
|
|
1608
|
-
const isCreate = (oflags & __WASI_O_CREAT) !== 0;
|
|
1609
|
-
const isDirectory = (oflags & __WASI_O_DIRECTORY) !== 0;
|
|
1610
|
-
const isExcl = (oflags & __WASI_O_EXCL) !== 0;
|
|
1611
|
-
const isTrunc = (oflags & __WASI_O_TRUNC) !== 0;
|
|
1612
|
-
// O_NOFOLLOW (symlink_follow cleared) on a trailing symlink: POSIX
|
|
1613
|
-
// open() fails with ELOOP rather than opening the link itself.
|
|
1614
|
-
if (!follow && rp.isSymlink) {
|
|
1615
|
-
return __WASI_ELOOP;
|
|
1616
|
-
}
|
|
1617
|
-
const fileExists = __wasiFS.files.has(resolved) || __wasiFS.sizes.has(resolved) || (__wasiFS.modes.has(resolved) &&
|
|
1618
|
-
!__wasiFS.dirs.has(resolved) &&
|
|
1619
|
-
!__wasiFS.symlinks.has(resolved));
|
|
1620
|
-
const dirExists = __wasiFS.dirs.has(resolved);
|
|
1621
|
-
if (isDirectory) {
|
|
1622
|
-
if (!dirExists)
|
|
1623
|
-
return __WASI_ENOENT;
|
|
1624
|
-
const access = __wasiCheckMode(resolved, 1);
|
|
1625
|
-
if (access !== __WASI_ESUCCESS)
|
|
1626
|
-
return access;
|
|
1627
|
-
const fd = nextFd++;
|
|
1628
|
-
fdTable.set(fd, { kind: 'dir', vfsPath: resolved, readdirEntries: null, cookie: 0n, oflags, fdflags });
|
|
1629
|
-
writeU32LE(fdOutPtr, fd);
|
|
1630
|
-
return __WASI_ESUCCESS;
|
|
1631
|
-
}
|
|
1632
|
-
if (dirExists) {
|
|
1633
|
-
// Opening a directory without O_DIRECTORY: WASI returns EISDIR.
|
|
1634
|
-
return __WASI_EISDIR;
|
|
1635
|
-
}
|
|
1636
|
-
if (!fileExists) {
|
|
1637
|
-
if (!isCreate)
|
|
1638
|
-
return __WASI_ENOENT;
|
|
1639
|
-
const parentSlash = resolved.lastIndexOf('/');
|
|
1640
|
-
const parent = parentSlash < 0 ? '' : resolved.substring(0, parentSlash);
|
|
1641
|
-
const access = __wasiCheckMode(parent, 3);
|
|
1642
|
-
if (access !== __WASI_ESUCCESS)
|
|
1643
|
-
return access;
|
|
1644
|
-
// create empty file
|
|
1645
|
-
ensureParentDirs(resolved);
|
|
1646
|
-
setFile(resolved, new Uint8Array(0));
|
|
1647
|
-
}
|
|
1648
|
-
else if (isExcl) {
|
|
1649
|
-
return __WASI_EEXIST;
|
|
1650
|
-
}
|
|
1651
|
-
else if (isTrunc) {
|
|
1652
|
-
const access = __wasiCheckMode(resolved, 2);
|
|
1653
|
-
if (access !== __WASI_ESUCCESS)
|
|
1654
|
-
return access;
|
|
1655
|
-
setFile(resolved, new Uint8Array(0));
|
|
1656
|
-
}
|
|
1657
|
-
const requested = typeof rightsBase === 'bigint' ? rightsBase : BigInt(rightsBase >>> 0);
|
|
1658
|
-
let requiredMode = 0;
|
|
1659
|
-
if ((requested & __WASI_RIGHT_FD_READ) !== 0n)
|
|
1660
|
-
requiredMode |= 4;
|
|
1661
|
-
if ((requested & __WASI_RIGHT_FD_WRITE) !== 0n)
|
|
1662
|
-
requiredMode |= 2;
|
|
1663
|
-
if (requiredMode !== 0) {
|
|
1664
|
-
const access = __wasiCheckMode(resolved, requiredMode);
|
|
1665
|
-
if (access !== __WASI_ESUCCESS)
|
|
1666
|
-
return access;
|
|
1667
|
-
}
|
|
1668
|
-
const fd = nextFd++;
|
|
1669
|
-
fdTable.set(fd, { kind: 'file', vfsPath: resolved, offset: 0, oflags, fdflags });
|
|
1670
|
-
writeU32LE(fdOutPtr, fd);
|
|
1671
|
-
return __WASI_ESUCCESS;
|
|
821
|
+
// Every other path is the authority codec's; without a supervisor there
|
|
822
|
+
// is no filesystem to open it on.
|
|
823
|
+
return __WASI_EBADF;
|
|
1672
824
|
},
|
|
1673
825
|
// ── path_create_directory ──
|
|
1674
|
-
path_create_directory(
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
if (resolved === null)
|
|
1678
|
-
return __WASI_EBADF;
|
|
1679
|
-
if (__wasiFS.dirs.has(resolved))
|
|
1680
|
-
return __WASI_EEXIST;
|
|
1681
|
-
if (__wasiFS.files.has(resolved))
|
|
1682
|
-
return __WASI_EEXIST;
|
|
1683
|
-
ensureParentDirs(resolved);
|
|
1684
|
-
__wasiFS.dirs.add(resolved);
|
|
1685
|
-
__wasiFS.modes.set(resolved, 7);
|
|
1686
|
-
__wasiNegative.delete(resolved);
|
|
1687
|
-
// WASI socket and polling support B1: seed times for the new dir.
|
|
1688
|
-
const now = __wasiNowNs();
|
|
1689
|
-
__wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
|
|
1690
|
-
__wasiEnqueue('mkdir ' + resolved, (sup) => sup.mkdir(resolved));
|
|
1691
|
-
return __WASI_ESUCCESS;
|
|
1692
|
-
},
|
|
1693
|
-
path_remove_directory(baseFd, pathPtr, pathLen) {
|
|
1694
|
-
const path = readPath(pathPtr, pathLen);
|
|
1695
|
-
const resolved = __wasiResolvePath(baseFd, path);
|
|
1696
|
-
if (resolved === null)
|
|
1697
|
-
return __WASI_EBADF;
|
|
1698
|
-
if (!__wasiFS.dirs.has(resolved))
|
|
1699
|
-
return __WASI_ENOENT;
|
|
1700
|
-
// Must be empty
|
|
1701
|
-
const children = readdirChildren(resolved);
|
|
1702
|
-
if (children.length > 0)
|
|
1703
|
-
return __WASI_ENOTEMPTY;
|
|
1704
|
-
__wasiFS.dirs.delete(resolved);
|
|
1705
|
-
__wasiFS.modes.delete(resolved);
|
|
1706
|
-
__wasiEnqueue('rmdir ' + resolved, (sup) => sup.rmdir(resolved));
|
|
1707
|
-
return __WASI_ESUCCESS;
|
|
1708
|
-
},
|
|
1709
|
-
path_unlink_file(baseFd, pathPtr, pathLen) {
|
|
1710
|
-
const path = readPath(pathPtr, pathLen);
|
|
1711
|
-
const resolved = __wasiResolvePath(baseFd, path);
|
|
1712
|
-
if (resolved === null)
|
|
1713
|
-
return __WASI_EBADF;
|
|
1714
|
-
if (__wasiFS.dirs.has(resolved))
|
|
1715
|
-
return __WASI_EISDIR;
|
|
1716
|
-
// WASI socket and polling support B3: unlink also removes a symlink at this path. We
|
|
1717
|
-
// operate on the unfollowed path (POSIX unlink semantics — removes
|
|
1718
|
-
// the directory entry itself, not the symlink target).
|
|
1719
|
-
if (__wasiFS.symlinks.has(resolved)) {
|
|
1720
|
-
__wasiFS.symlinks.delete(resolved);
|
|
1721
|
-
__wasiFS.times.delete(resolved);
|
|
1722
|
-
__wasiFS.modes.delete(resolved);
|
|
1723
|
-
return __WASI_ESUCCESS;
|
|
1724
|
-
}
|
|
1725
|
-
if (!fileKnown(resolved))
|
|
1726
|
-
return __WASI_ENOENT;
|
|
1727
|
-
unsetFile(resolved);
|
|
1728
|
-
return __WASI_ESUCCESS;
|
|
1729
|
-
},
|
|
826
|
+
path_create_directory(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
|
|
827
|
+
path_remove_directory(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
|
|
828
|
+
path_unlink_file(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
|
|
1730
829
|
// ── path_rename (atomic; overwrites destination) ──
|
|
1731
|
-
path_rename(
|
|
1732
|
-
const srcPath = readPath(srcPathPtr, srcPathLen);
|
|
1733
|
-
const dstPath = readPath(dstPathPtr, dstPathLen);
|
|
1734
|
-
const src = __wasiResolvePath(srcFd, srcPath);
|
|
1735
|
-
const dst = __wasiResolvePath(dstFd, dstPath);
|
|
1736
|
-
if (src === null || dst === null)
|
|
1737
|
-
return __WASI_EBADF;
|
|
1738
|
-
// rename operates on the entry itself (no symlink-follow on the final
|
|
1739
|
-
// component, per POSIX). src may be a file, a directory, or a symlink.
|
|
1740
|
-
const srcFile = __wasiFS.files.get(src);
|
|
1741
|
-
const srcIsDir = __wasiFS.dirs.has(src);
|
|
1742
|
-
const srcIsSymlink = __wasiFS.symlinks.has(src);
|
|
1743
|
-
// A manifest-only file is a file: its bytes just are not resident yet.
|
|
1744
|
-
const srcIsLazy = srcFile === undefined && __wasiFS.sizes.has(src);
|
|
1745
|
-
if (srcFile === undefined && !srcIsLazy && !srcIsDir && !srcIsSymlink)
|
|
1746
|
-
return __WASI_ENOENT;
|
|
1747
|
-
if (src === dst)
|
|
1748
|
-
return __WASI_ESUCCESS; // rename to itself is a no-op
|
|
1749
|
-
// Move the path's timestamps with it, bumping ctime (metadata change).
|
|
1750
|
-
const moveTimes = (from, to) => {
|
|
1751
|
-
const t = __wasiFS.times.get(from);
|
|
1752
|
-
__wasiFS.times.delete(from);
|
|
1753
|
-
const now = __wasiNowNs();
|
|
1754
|
-
__wasiFS.times.set(to, t
|
|
1755
|
-
? { mtime: t.mtime, atime: t.atime, ctime: now }
|
|
1756
|
-
: { mtime: now, atime: now, ctime: now });
|
|
1757
|
-
};
|
|
1758
|
-
const moveMode = (from, to) => {
|
|
1759
|
-
const mode = __wasiFS.modes.get(from);
|
|
1760
|
-
__wasiFS.modes.delete(from);
|
|
1761
|
-
if (mode !== undefined)
|
|
1762
|
-
__wasiFS.modes.set(to, mode);
|
|
1763
|
-
};
|
|
1764
|
-
// Pre-remove the destination of any kind (atomic overwrite).
|
|
1765
|
-
__wasiFS.files.delete(dst);
|
|
1766
|
-
__wasiFS.sizes.delete(dst);
|
|
1767
|
-
__wasiFS.dirs.delete(dst);
|
|
1768
|
-
__wasiFS.symlinks.delete(dst);
|
|
1769
|
-
__wasiFS.times.delete(dst);
|
|
1770
|
-
__wasiFS.modes.delete(dst);
|
|
1771
|
-
if (srcIsLazy) {
|
|
1772
|
-
__wasiFS.sizes.set(dst, __wasiFS.sizes.get(src));
|
|
1773
|
-
__wasiFS.sizes.delete(src);
|
|
1774
|
-
moveTimes(src, dst);
|
|
1775
|
-
moveMode(src, dst);
|
|
1776
|
-
}
|
|
1777
|
-
else if (srcIsSymlink) {
|
|
1778
|
-
__wasiFS.symlinks.set(dst, __wasiFS.symlinks.get(src));
|
|
1779
|
-
__wasiFS.symlinks.delete(src);
|
|
1780
|
-
moveTimes(src, dst);
|
|
1781
|
-
moveMode(src, dst);
|
|
1782
|
-
}
|
|
1783
|
-
else if (srcFile !== undefined) {
|
|
1784
|
-
__wasiFS.files.set(dst, srcFile);
|
|
1785
|
-
__wasiFS.files.delete(src);
|
|
1786
|
-
moveTimes(src, dst);
|
|
1787
|
-
moveMode(src, dst);
|
|
1788
|
-
}
|
|
1789
|
-
else {
|
|
1790
|
-
__wasiFS.dirs.delete(src);
|
|
1791
|
-
__wasiFS.dirs.add(dst);
|
|
1792
|
-
moveTimes(src, dst);
|
|
1793
|
-
moveMode(src, dst);
|
|
1794
|
-
// Rebase every descendant key (files, dirs, symlinks) + its times.
|
|
1795
|
-
const srcPrefix = src + '/';
|
|
1796
|
-
const rebase = (key) => dst + '/' + key.substring(srcPrefix.length);
|
|
1797
|
-
for (const key of [...__wasiFS.files.keys()]) {
|
|
1798
|
-
if (key.startsWith(srcPrefix)) {
|
|
1799
|
-
const nk = rebase(key);
|
|
1800
|
-
__wasiFS.files.set(nk, __wasiFS.files.get(key));
|
|
1801
|
-
__wasiFS.files.delete(key);
|
|
1802
|
-
moveTimes(key, nk);
|
|
1803
|
-
moveMode(key, nk);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
for (const key of [...__wasiFS.dirs]) {
|
|
1807
|
-
if (key.startsWith(srcPrefix)) {
|
|
1808
|
-
const nk = rebase(key);
|
|
1809
|
-
__wasiFS.dirs.add(nk);
|
|
1810
|
-
__wasiFS.dirs.delete(key);
|
|
1811
|
-
moveTimes(key, nk);
|
|
1812
|
-
moveMode(key, nk);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
for (const key of [...__wasiFS.symlinks.keys()]) {
|
|
1816
|
-
if (key.startsWith(srcPrefix)) {
|
|
1817
|
-
const nk = rebase(key);
|
|
1818
|
-
__wasiFS.symlinks.set(nk, __wasiFS.symlinks.get(key));
|
|
1819
|
-
__wasiFS.symlinks.delete(key);
|
|
1820
|
-
moveTimes(key, nk);
|
|
1821
|
-
moveMode(key, nk);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
for (const key of [...__wasiFS.sizes.keys()]) {
|
|
1825
|
-
if (key.startsWith(srcPrefix)) {
|
|
1826
|
-
__wasiFS.sizes.set(rebase(key), __wasiFS.sizes.get(key));
|
|
1827
|
-
__wasiFS.sizes.delete(key);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
// One op: the supervisor renames the whole subtree atomically, which a
|
|
1832
|
-
// per-descendant replay could not.
|
|
1833
|
-
__wasiEnqueue('rename ' + src + ' -> ' + dst, (sup) => sup.rename(src, dst));
|
|
1834
|
-
return __WASI_ESUCCESS;
|
|
1835
|
-
},
|
|
830
|
+
path_rename(_srcFd, _srcPathPtr, _srcPathLen, _dstFd, _dstPathPtr, _dstPathLen) { return __WASI_EBADF; },
|
|
1836
831
|
// ── path_filestat_get ──
|
|
1837
|
-
path_filestat_get(
|
|
1838
|
-
const path = readPath(pathPtr, pathLen);
|
|
1839
|
-
// Follow symlinks only when SYMLINK_FOLLOW is set (stat); flags=0 is lstat.
|
|
1840
|
-
const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
|
|
1841
|
-
const rp = __wasiResolvePathFull(baseFd, path, follow);
|
|
1842
|
-
if (rp.err === __WASI_EBADF)
|
|
1843
|
-
return __WASI_EBADF;
|
|
1844
|
-
if (rp.err === __WASI_ELOOP)
|
|
1845
|
-
return __WASI_ELOOP;
|
|
1846
|
-
const resolved = rp.path;
|
|
1847
|
-
const traversal = __wasiCheckTraversal(baseFd, resolved);
|
|
1848
|
-
if (traversal !== __WASI_ESUCCESS)
|
|
1849
|
-
return traversal;
|
|
1850
|
-
const classify = () => {
|
|
1851
|
-
if (!follow && rp.isSymlink) {
|
|
1852
|
-
return [__WASI_FT_SYMBOLIC_LINK,
|
|
1853
|
-
BigInt(new TextEncoder().encode(__wasiFS.symlinks.get(resolved)).length)];
|
|
1854
|
-
}
|
|
1855
|
-
if (__wasiFS.files.has(resolved) || __wasiFS.sizes.has(resolved)) {
|
|
1856
|
-
return [__WASI_FT_REGULAR_FILE, BigInt(fileSize(resolved))];
|
|
1857
|
-
}
|
|
1858
|
-
if (__wasiFS.dirs.has(resolved))
|
|
1859
|
-
return [__WASI_FT_DIRECTORY, 0n];
|
|
1860
|
-
if (__wasiFS.modes.has(resolved))
|
|
1861
|
-
return [__WASI_FT_REGULAR_FILE, 0n];
|
|
1862
|
-
return null;
|
|
1863
|
-
};
|
|
1864
|
-
let kind = classify();
|
|
1865
|
-
if (!kind) {
|
|
1866
|
-
const live = statLive(resolved);
|
|
1867
|
-
if (live && typeof live.then === 'function') {
|
|
1868
|
-
return live.then((found) => {
|
|
1869
|
-
// classify() is re-run because the live stat is what populated the
|
|
1870
|
-
// maps. It can still answer null: the stat and this callback are
|
|
1871
|
-
// separated by a suspension, and anything that unlinks the path in
|
|
1872
|
-
// between — another thread, another request on a resident server —
|
|
1873
|
-
// leaves nothing to classify. Spreading that null threw "is not
|
|
1874
|
-
// iterable" from inside a Suspending import, which reaches the
|
|
1875
|
-
// guest as a trap carrying no errno. The path is gone, so ENOENT is
|
|
1876
|
-
// the answer the guest can act on.
|
|
1877
|
-
const settled = found ? classify() : null;
|
|
1878
|
-
return settled ? emitStat(...settled) : __WASI_ENOENT;
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
return __WASI_ENOENT;
|
|
1882
|
-
}
|
|
1883
|
-
return emitStat(...kind);
|
|
1884
|
-
// Hoisted: reached from both the resident and the post-live-stat path.
|
|
1885
|
-
function emitStat(ftype, size) {
|
|
1886
|
-
// WASI socket and polling support B1: emit real timestamps.
|
|
1887
|
-
const t = __wasiFS.times.get(resolved) || { mtime: 0n, atime: 0n, ctime: 0n };
|
|
1888
|
-
return writeFilestat(statPtr, resolved, ftype, size, t);
|
|
1889
|
-
}
|
|
1890
|
-
},
|
|
832
|
+
path_filestat_get(_baseFd, _lookupflags, _pathPtr, _pathLen, _statPtr) { return __WASI_EBADF; },
|
|
1891
833
|
// WASI socket and polling support B2: real path_filestat_set_times. Honors ATIM/ATIM_NOW/
|
|
1892
834
|
// MTIM/MTIM_NOW flags. Spec: atim_ns and mtim_ns are absolute
|
|
1893
835
|
// nanosecond timestamps; flags select which fields to update + whether
|
|
1894
836
|
// to clamp to "now". ENOENT if path doesn't exist.
|
|
1895
|
-
path_filestat_set_times(
|
|
1896
|
-
const path = readPath(pathPtr, pathLen);
|
|
1897
|
-
const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
|
|
1898
|
-
const rp = __wasiResolvePathFull(baseFd, path, follow);
|
|
1899
|
-
if (rp.err === __WASI_EBADF)
|
|
1900
|
-
return __WASI_EBADF;
|
|
1901
|
-
if (rp.err === __WASI_ELOOP)
|
|
1902
|
-
return __WASI_ELOOP;
|
|
1903
|
-
const resolved = rp.path;
|
|
1904
|
-
const isSymlinkOnly = !follow && rp.isSymlink;
|
|
1905
|
-
if (!isSymlinkOnly && !__wasiFS.files.has(resolved) && !__wasiFS.dirs.has(resolved)) {
|
|
1906
|
-
return __WASI_ENOENT;
|
|
1907
|
-
}
|
|
1908
|
-
// Decode flags. Mutually-exclusive pairs ATIM vs ATIM_NOW (similarly
|
|
1909
|
-
// for MTIM) — caller error if both set; we accept and prefer _NOW.
|
|
1910
|
-
const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;
|
|
1911
|
-
const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;
|
|
1912
|
-
const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;
|
|
1913
|
-
const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;
|
|
1914
|
-
if ((setAtim && setAtimNow) || (setMtim && setMtimNow))
|
|
1915
|
-
return __WASI_EINVAL;
|
|
1916
|
-
const now = __wasiNowNs();
|
|
1917
|
-
const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
|
|
1918
|
-
const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
|
|
1919
|
-
__wasiTouchTimes(resolved, mtimNs, atimNs, now);
|
|
1920
|
-
__wasiPersistTimes(resolved);
|
|
1921
|
-
return __WASI_ESUCCESS;
|
|
1922
|
-
},
|
|
837
|
+
path_filestat_set_times(_baseFd, _lookupflags, _pathPtr, _pathLen, _atimArg, _mtimArg, _fstflags) { return __WASI_EBADF; },
|
|
1923
838
|
// WASI socket and polling support B3: read the target string of a symlink. Spec:
|
|
1924
839
|
// path_readlink(fd, path, path_len, buf, buf_len, *bufused)
|
|
1925
840
|
// Truncates to buf_len; writes actual bytes to *bufused.
|
|
1926
|
-
path_readlink(
|
|
1927
|
-
const path = readPath(pathPtr, pathLen);
|
|
1928
|
-
// readlink NEVER follows symlinks on the last component (POSIX).
|
|
1929
|
-
const rp = __wasiResolvePathFull(baseFd, path, false);
|
|
1930
|
-
if (rp.err === __WASI_EBADF)
|
|
1931
|
-
return __WASI_EBADF;
|
|
1932
|
-
if (rp.err === __WASI_ELOOP)
|
|
1933
|
-
return __WASI_ELOOP;
|
|
1934
|
-
const resolved = rp.path;
|
|
1935
|
-
if (!__wasiFS.symlinks.has(resolved))
|
|
1936
|
-
return __WASI_EINVAL; // not a symlink
|
|
1937
|
-
const target = __wasiFS.symlinks.get(resolved);
|
|
1938
|
-
const enc = new TextEncoder();
|
|
1939
|
-
const targetBytes = enc.encode(target);
|
|
1940
|
-
const n = Math.min(targetBytes.length, bufLen);
|
|
1941
|
-
u8().set(targetBytes.subarray(0, n), bufPtr);
|
|
1942
|
-
writeU32LE(bufUsedPtr, n);
|
|
1943
|
-
// POSIX: readlink doesn't update atime per most filesystems; but
|
|
1944
|
-
// some (ext4 with strictatime) do. We follow Linux default: no
|
|
1945
|
-
// atime bump on readlink.
|
|
1946
|
-
return __WASI_ESUCCESS;
|
|
1947
|
-
},
|
|
841
|
+
path_readlink(_baseFd, _pathPtr, _pathLen, _bufPtr, _bufLen, _bufUsedPtr) { return __WASI_EBADF; },
|
|
1948
842
|
// WASI socket and polling support B3: create a symlink. Spec:
|
|
1949
843
|
// path_symlink(old_path, fd, new_path)
|
|
1950
844
|
// old_path is the symlink's TARGET (stored verbatim); fd+new_path
|
|
1951
845
|
// is the location where the symlink itself is created.
|
|
1952
|
-
path_symlink(
|
|
1953
|
-
const oldPath = readPath(oldPathPtr, oldPathLen); // target string
|
|
1954
|
-
const newPath = readPath(newPathPtr, newPathLen);
|
|
1955
|
-
const resolved = __wasiResolvePath(newFd, newPath);
|
|
1956
|
-
if (resolved === null)
|
|
1957
|
-
return __WASI_EBADF;
|
|
1958
|
-
if (__wasiFS.files.has(resolved))
|
|
1959
|
-
return __WASI_EEXIST;
|
|
1960
|
-
if (__wasiFS.dirs.has(resolved))
|
|
1961
|
-
return __WASI_EEXIST;
|
|
1962
|
-
if (__wasiFS.symlinks.has(resolved))
|
|
1963
|
-
return __WASI_EEXIST;
|
|
1964
|
-
// Store the target verbatim — symlinks per POSIX are dumb strings;
|
|
1965
|
-
// resolution happens at lookup time.
|
|
1966
|
-
__wasiFS.symlinks.set(resolved, oldPath);
|
|
1967
|
-
__wasiFS.modes.set(resolved, 7);
|
|
1968
|
-
ensureParentDirs(resolved);
|
|
1969
|
-
const now = __wasiNowNs();
|
|
1970
|
-
__wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
|
|
1971
|
-
__wasiNegative.delete(resolved);
|
|
1972
|
-
__wasiPersistSymlink(oldPath, resolved);
|
|
1973
|
-
return __WASI_ESUCCESS;
|
|
1974
|
-
},
|
|
846
|
+
path_symlink(_oldPathPtr, _oldPathLen, _newFd, _newPathPtr, _newPathLen) { return __WASI_EBADF; },
|
|
1975
847
|
// WASI socket and polling support B3: hardlink. Spec:
|
|
1976
848
|
// path_link(old_fd, old_flags, old_path, new_fd, new_path)
|
|
1977
849
|
// Implements as a shared-buffer alias. POSIX semantics: both names
|
|
@@ -1982,54 +854,20 @@ export function __wasiMakeImports(opts) {
|
|
|
1982
854
|
// don't propagate. Matches link(2) at the WASI-layer (link itself
|
|
1983
855
|
// succeeds; concurrent-mutation semantics are filesystem-dependent
|
|
1984
856
|
// and our shim is a single-call sandbox).
|
|
1985
|
-
path_link(
|
|
1986
|
-
const oldPath = readPath(oldPathPtr, oldPathLen);
|
|
1987
|
-
const newPath = readPath(newPathPtr, newPathLen);
|
|
1988
|
-
const follow = (oldFlags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
|
|
1989
|
-
const rpOld = __wasiResolvePathFull(oldFd, oldPath, follow);
|
|
1990
|
-
if (rpOld.err === __WASI_EBADF)
|
|
1991
|
-
return __WASI_EBADF;
|
|
1992
|
-
if (rpOld.err === __WASI_ELOOP)
|
|
1993
|
-
return __WASI_ELOOP;
|
|
1994
|
-
const src = rpOld.path;
|
|
1995
|
-
const dst = __wasiResolvePath(newFd, newPath);
|
|
1996
|
-
if (dst === null)
|
|
1997
|
-
return __WASI_EBADF;
|
|
1998
|
-
if (!__wasiFS.files.has(src))
|
|
1999
|
-
return __WASI_ENOENT;
|
|
2000
|
-
if (__wasiFS.files.has(dst) || __wasiFS.dirs.has(dst) || __wasiFS.symlinks.has(dst)) {
|
|
2001
|
-
return __WASI_EEXIST;
|
|
2002
|
-
}
|
|
2003
|
-
__wasiFS.files.set(dst, __wasiFS.files.get(src)); // shared reference
|
|
2004
|
-
__wasiFS.modes.set(dst, __wasiEffectiveMode(src));
|
|
2005
|
-
ensureParentDirs(dst);
|
|
2006
|
-
const now = __wasiNowNs();
|
|
2007
|
-
__wasiFS.times.set(dst, { mtime: now, atime: now, ctime: now });
|
|
2008
|
-
__wasiNegative.delete(dst);
|
|
2009
|
-
// The session VFS has no hardlinks; the new name is persisted as its own
|
|
2010
|
-
// file with the same bytes, which is what link(2) already degrades to
|
|
2011
|
-
// here (fd_write REPLACES the buffer rather than mutating it in place).
|
|
2012
|
-
__wasiPersistFile(dst);
|
|
2013
|
-
return __WASI_ESUCCESS;
|
|
2014
|
-
},
|
|
857
|
+
path_link(_oldFd, _oldFlags, _oldPathPtr, _oldPathLen, _newFd, _newPathPtr, _newPathLen) { return __WASI_EBADF; },
|
|
2015
858
|
// ── fd_filestat_get / fd_filestat_set_size ──
|
|
2016
859
|
fd_filestat_get(fd, statPtr) {
|
|
2017
860
|
const entry = fdTable.get(fd);
|
|
2018
861
|
if (!entry)
|
|
2019
862
|
return __WASI_EBADF;
|
|
863
|
+
// Files and directories are the authority codec's. A descriptor with no
|
|
864
|
+
// path (stdio, a socket) is still its own object and must not share an
|
|
865
|
+
// inode with every other one, so it is named by its fd — on a device of
|
|
866
|
+
// its own, because (dev, ino) is the identity and the authority's
|
|
867
|
+
// devices already use the small numbers (the kernel namespace is 0,
|
|
868
|
+
// SQLite devices count from 1, and their inodes from 1 too).
|
|
2020
869
|
let ftype = __WASI_FT_UNKNOWN;
|
|
2021
|
-
|
|
2022
|
-
let timesPath = null;
|
|
2023
|
-
if (entry.kind === 'file') {
|
|
2024
|
-
ftype = __WASI_FT_REGULAR_FILE;
|
|
2025
|
-
size = BigInt(fileSize(entry.vfsPath));
|
|
2026
|
-
timesPath = entry.vfsPath;
|
|
2027
|
-
}
|
|
2028
|
-
else if (entry.kind === 'dir' || entry.kind === 'preopen') {
|
|
2029
|
-
ftype = __WASI_FT_DIRECTORY;
|
|
2030
|
-
timesPath = entry.vfsPath;
|
|
2031
|
-
}
|
|
2032
|
-
else if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
|
|
870
|
+
if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
|
|
2033
871
|
ftype = __WASI_FT_CHARACTER_DEVICE;
|
|
2034
872
|
}
|
|
2035
873
|
else if (entry.kind === 'socket' || entry.kind === 'listener') {
|
|
@@ -2037,111 +875,33 @@ export function __wasiMakeImports(opts) {
|
|
|
2037
875
|
// IO layer keys buffering and seekability off exactly this).
|
|
2038
876
|
ftype = __WASI_FT_SOCKET_STREAM;
|
|
2039
877
|
}
|
|
2040
|
-
|
|
2041
|
-
// A path names the inode when there is one. An fd with no path — stdio,
|
|
2042
|
-
// a socket — is still its own object and must not share an inode with
|
|
2043
|
-
// every other one, so it is named by the fd instead.
|
|
2044
|
-
return writeFilestat(statPtr, timesPath === null ? '\0fd/' + fd : timesPath, ftype, size, t);
|
|
2045
|
-
},
|
|
2046
|
-
fd_filestat_set_size(fd, size) {
|
|
2047
|
-
const entry = fdTable.get(fd);
|
|
2048
|
-
if (!entry || entry.kind !== 'file')
|
|
2049
|
-
return __WASI_EBADF;
|
|
2050
|
-
const cur = getFile(entry.vfsPath) || new Uint8Array(0);
|
|
2051
|
-
const newSize = Number(size);
|
|
2052
|
-
const next = new Uint8Array(newSize);
|
|
2053
|
-
next.set(cur.subarray(0, Math.min(cur.length, newSize)), 0);
|
|
2054
|
-
setFile(entry.vfsPath, next);
|
|
2055
|
-
return __WASI_ESUCCESS;
|
|
2056
|
-
},
|
|
2057
|
-
// WASI socket and polling support B2: real fd_filestat_set_times.
|
|
2058
|
-
fd_filestat_set_times(fd, atimArg, mtimArg, fstflags) {
|
|
2059
|
-
const entry = fdTable.get(fd);
|
|
2060
|
-
if (!entry)
|
|
878
|
+
else
|
|
2061
879
|
return __WASI_EBADF;
|
|
2062
|
-
|
|
2063
|
-
|
|
880
|
+
const dv = view();
|
|
881
|
+
dv.setUint8(statPtr + 16, ftype);
|
|
882
|
+
writeU64LE(statPtr, __WASI_STREAM_DEV);
|
|
883
|
+
writeU64LE(statPtr + 8, BigInt(fd) + 1n);
|
|
884
|
+
if (preview0) {
|
|
885
|
+
for (let i = 17; i < 20; i++)
|
|
886
|
+
dv.setUint8(statPtr + i, 0);
|
|
887
|
+
dv.setUint32(statPtr + 20, 1, true); // nlink u32
|
|
888
|
+
for (let off = 24; off < 56; off += 8)
|
|
889
|
+
writeU64LE(statPtr + off, 0n);
|
|
2064
890
|
return __WASI_ESUCCESS;
|
|
2065
891
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
return __WASI_EINVAL;
|
|
2072
|
-
const now = __wasiNowNs();
|
|
2073
|
-
const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
|
|
2074
|
-
const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
|
|
2075
|
-
__wasiTouchTimes(entry.vfsPath, mtimNs, atimNs, now);
|
|
2076
|
-
__wasiPersistTimes(entry.vfsPath);
|
|
892
|
+
for (let i = 17; i < 24; i++)
|
|
893
|
+
dv.setUint8(statPtr + i, 0);
|
|
894
|
+
writeU64LE(statPtr + 24, 1n); // nlink u64
|
|
895
|
+
for (let off = 32; off < 64; off += 8)
|
|
896
|
+
writeU64LE(statPtr + off, 0n);
|
|
2077
897
|
return __WASI_ESUCCESS;
|
|
2078
898
|
},
|
|
899
|
+
fd_filestat_set_size(_fd, _size) { return __WASI_EBADF; },
|
|
900
|
+
// WASI socket and polling support B2: real fd_filestat_set_times.
|
|
901
|
+
fd_filestat_set_times(_fd, _atimArg, _mtimArg, _fstflags) { return __WASI_EBADF; },
|
|
2079
902
|
// ── fd_pread / fd_pwrite (offset-explicit) ──
|
|
2080
|
-
fd_pread(
|
|
2081
|
-
|
|
2082
|
-
if (!entry || entry.kind !== 'file')
|
|
2083
|
-
return __WASI_EBADF;
|
|
2084
|
-
if (!fileKnown(entry.vfsPath))
|
|
2085
|
-
return __WASI_ENOENT;
|
|
2086
|
-
const offset = typeof offsetArg === 'bigint'
|
|
2087
|
-
? Number(offsetArg)
|
|
2088
|
-
: (offsetArg >>> 0);
|
|
2089
|
-
const want = iovsCapacity(iovsPtr, iovsLen, view());
|
|
2090
|
-
const take = Math.min(want, Math.max(0, fileSize(entry.vfsPath) - offset));
|
|
2091
|
-
if (take === 0) {
|
|
2092
|
-
writeU32LE(nreadPtr, 0);
|
|
2093
|
-
return __WASI_ESUCCESS;
|
|
2094
|
-
}
|
|
2095
|
-
const chunk = readRange(entry.vfsPath, offset, take);
|
|
2096
|
-
const deliver = (bytes) => {
|
|
2097
|
-
if (bytes === null)
|
|
2098
|
-
return __WASI_EIO;
|
|
2099
|
-
writeU32LE(nreadPtr, scatterIovs(bytes, iovsPtr, iovsLen, view(), u8()));
|
|
2100
|
-
return __WASI_ESUCCESS;
|
|
2101
|
-
};
|
|
2102
|
-
return (chunk && typeof chunk.then === 'function') ? chunk.then(deliver) : deliver(chunk);
|
|
2103
|
-
},
|
|
2104
|
-
fd_pwrite(fd, iovsPtr, iovsLen, offsetArg, nwrittenPtr) {
|
|
2105
|
-
const entry = fdTable.get(fd);
|
|
2106
|
-
if (!entry || entry.kind !== 'file')
|
|
2107
|
-
return __WASI_EBADF;
|
|
2108
|
-
let offset = typeof offsetArg === 'bigint'
|
|
2109
|
-
? Number(offsetArg)
|
|
2110
|
-
: (offsetArg >>> 0);
|
|
2111
|
-
const dv = view();
|
|
2112
|
-
const memU8 = u8();
|
|
2113
|
-
let total = 0;
|
|
2114
|
-
const parts = [];
|
|
2115
|
-
for (let i = 0; i < iovsLen; i++) {
|
|
2116
|
-
const iov = iovsPtr + i * 8;
|
|
2117
|
-
const bufPtr = dv.getUint32(iov, true);
|
|
2118
|
-
const bufLen = dv.getUint32(iov + 4, true);
|
|
2119
|
-
if (bufLen > 0)
|
|
2120
|
-
parts.push(memU8.slice(bufPtr, bufPtr + bufLen));
|
|
2121
|
-
total += bufLen;
|
|
2122
|
-
}
|
|
2123
|
-
let combined;
|
|
2124
|
-
if (parts.length === 0)
|
|
2125
|
-
combined = new Uint8Array(0);
|
|
2126
|
-
else if (parts.length === 1)
|
|
2127
|
-
combined = parts[0];
|
|
2128
|
-
else {
|
|
2129
|
-
combined = new Uint8Array(total);
|
|
2130
|
-
let off = 0;
|
|
2131
|
-
for (const p of parts) {
|
|
2132
|
-
combined.set(p, off);
|
|
2133
|
-
off += p.length;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
const cur = getFile(entry.vfsPath) || new Uint8Array(0);
|
|
2137
|
-
const newLen = Math.max(cur.length, offset + total);
|
|
2138
|
-
const next = new Uint8Array(newLen);
|
|
2139
|
-
next.set(cur, 0);
|
|
2140
|
-
next.set(combined, offset);
|
|
2141
|
-
setFile(entry.vfsPath, next);
|
|
2142
|
-
writeU32LE(nwrittenPtr, total);
|
|
2143
|
-
return __WASI_ESUCCESS;
|
|
2144
|
-
},
|
|
903
|
+
fd_pread(_fd, _iovsPtr, _iovsLen, _offsetArg, _nreadPtr) { return __WASI_EBADF; },
|
|
904
|
+
fd_pwrite(_fd, _iovsPtr, _iovsLen, _offsetArg, _nwrittenPtr) { return __WASI_EBADF; },
|
|
2145
905
|
// ── fd_readdir ──
|
|
2146
906
|
//
|
|
2147
907
|
// WASI dirent layout (24 bytes per entry):
|
|
@@ -2151,72 +911,14 @@ export function __wasiMakeImports(opts) {
|
|
|
2151
911
|
// d_type u8 @ 20
|
|
2152
912
|
// pad 21..23
|
|
2153
913
|
// followed by name bytes (variable).
|
|
2154
|
-
fd_readdir(
|
|
2155
|
-
|
|
2156
|
-
if (!entry || (entry.kind !== 'dir' && entry.kind !== 'preopen'))
|
|
2157
|
-
return __WASI_EBADF;
|
|
2158
|
-
const access = __wasiCheckMode(entry.vfsPath, 4);
|
|
2159
|
-
if (access !== __WASI_ESUCCESS)
|
|
2160
|
-
return access;
|
|
2161
|
-
if (!entry.readdirEntries) {
|
|
2162
|
-
const kids = readdirChildren(entry.vfsPath);
|
|
2163
|
-
entry.readdirEntries = [
|
|
2164
|
-
{ name: '.', type: __WASI_FT_DIRECTORY },
|
|
2165
|
-
{ name: '..', type: __WASI_FT_DIRECTORY },
|
|
2166
|
-
...kids,
|
|
2167
|
-
];
|
|
2168
|
-
}
|
|
2169
|
-
let startCookie = typeof cookieArg === 'bigint'
|
|
2170
|
-
? Number(cookieArg)
|
|
2171
|
-
: (cookieArg >>> 0);
|
|
2172
|
-
let written = 0;
|
|
2173
|
-
const dv = view();
|
|
2174
|
-
const memU8 = u8();
|
|
2175
|
-
for (let i = startCookie; i < entry.readdirEntries.length; i++) {
|
|
2176
|
-
const e = entry.readdirEntries[i];
|
|
2177
|
-
const nameBytes = utf8enc.encode(e.name);
|
|
2178
|
-
const recordSize = 24 + nameBytes.length;
|
|
2179
|
-
if (written + 24 > bufLen)
|
|
2180
|
-
break;
|
|
2181
|
-
// d_next = i+1 (next cookie)
|
|
2182
|
-
writeU64LE(bufPtr + written, BigInt(i + 1));
|
|
2183
|
-
writeU64LE(bufPtr + written + 8, 0n); // d_ino
|
|
2184
|
-
dv.setUint32(bufPtr + written + 16, nameBytes.length, true);
|
|
2185
|
-
dv.setUint8(bufPtr + written + 20, e.type);
|
|
2186
|
-
for (let j = 21; j < 24; j++)
|
|
2187
|
-
dv.setUint8(bufPtr + written + j, 0);
|
|
2188
|
-
written += 24;
|
|
2189
|
-
// Write as much of the name as fits.
|
|
2190
|
-
const nameRoom = Math.min(nameBytes.length, bufLen - written);
|
|
2191
|
-
memU8.set(nameBytes.subarray(0, nameRoom), bufPtr + written);
|
|
2192
|
-
written += nameRoom;
|
|
2193
|
-
if (nameRoom < nameBytes.length)
|
|
2194
|
-
break;
|
|
2195
|
-
}
|
|
2196
|
-
writeU32LE(bufusedPtr, written);
|
|
2197
|
-
return __WASI_ESUCCESS;
|
|
2198
|
-
},
|
|
2199
|
-
fd_advise() { return __WASI_ESUCCESS; },
|
|
914
|
+
fd_readdir(_fd, _bufPtr, _bufLen, _cookieArg, _bufusedPtr) { return __WASI_EBADF; },
|
|
915
|
+
fd_advise(_fd, _offset, _len, _advice) { return __WASI_ESUCCESS; },
|
|
2200
916
|
// WASI socket and polling support B4: real fd_allocate. Extends the file's byte buffer with
|
|
2201
917
|
// zeros so [offset, offset+len) is allocated. POSIX posix_fallocate
|
|
2202
918
|
// semantics. ENOSPC is not reachable in our in-memory FS (the
|
|
2203
919
|
// 32 MiB snapshot cap is enforced at supervisor level; in-call we
|
|
2204
920
|
// just allocate the JS Uint8Array).
|
|
2205
|
-
fd_allocate(
|
|
2206
|
-
const entry = fdTable.get(fd);
|
|
2207
|
-
if (!entry || entry.kind !== 'file')
|
|
2208
|
-
return __WASI_EBADF;
|
|
2209
|
-
const offset = typeof offsetArg === 'bigint' ? Number(offsetArg) : (offsetArg >>> 0);
|
|
2210
|
-
const len = typeof lenArg === 'bigint' ? Number(lenArg) : (lenArg >>> 0);
|
|
2211
|
-
const cur = getFile(entry.vfsPath) || new Uint8Array(0);
|
|
2212
|
-
const needed = offset + len;
|
|
2213
|
-
if (needed <= cur.length)
|
|
2214
|
-
return __WASI_ESUCCESS; // already big enough
|
|
2215
|
-
const next = new Uint8Array(needed);
|
|
2216
|
-
next.set(cur, 0); // [cur.length, needed) is implicitly zero-initialised
|
|
2217
|
-
setFile(entry.vfsPath, next);
|
|
2218
|
-
return __WASI_ESUCCESS;
|
|
2219
|
-
},
|
|
921
|
+
fd_allocate(_fd, _offsetArg, _lenArg) { return __WASI_EBADF; },
|
|
2220
922
|
fd_datasync() { return __WASI_ESUCCESS; },
|
|
2221
923
|
fd_sync() { return __WASI_ESUCCESS; },
|
|
2222
924
|
// dup2(2), and the only way to reach it: preview1 has no dup. Renumbering
|
|
@@ -2428,17 +1130,15 @@ export function __wasiMakeImports(opts) {
|
|
|
2428
1130
|
});
|
|
2429
1131
|
}
|
|
2430
1132
|
// Regular files, dirs, stdio: always ready (POSIX: regular files
|
|
2431
|
-
// never block — read returns immediately even if at EOF).
|
|
2432
|
-
|
|
2433
|
-
|
|
1133
|
+
// never block — read returns immediately even if at EOF). Files and
|
|
1134
|
+
// directories are the authority's descriptors; leaving them out made
|
|
1135
|
+
// polling one EBADF, which a guest reads as "this fd is gone".
|
|
1136
|
+
if (entry.kind === 'preopen' || entry.kind === 'authority' || entry.kind === 'resident' ||
|
|
2434
1137
|
entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
|
|
2435
1138
|
let nbytes = 0n;
|
|
2436
|
-
if (entry.kind === '
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2439
|
-
const remain = f.length - (entry.offset || 0);
|
|
2440
|
-
nbytes = remain > 0 ? BigInt(remain) : 0n;
|
|
2441
|
-
}
|
|
1139
|
+
if (entry.kind === 'resident' && s.tag === __WASI_EVENTTYPE_FD_READ) {
|
|
1140
|
+
const remain = entry.bytes.length - entry.position;
|
|
1141
|
+
nbytes = remain > 0 ? BigInt(remain) : 0n;
|
|
2442
1142
|
}
|
|
2443
1143
|
else if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {
|
|
2444
1144
|
// Writable: report large available capacity. Most user code
|
|
@@ -2843,6 +1543,15 @@ export function __wasiMakeImports(opts) {
|
|
|
2843
1543
|
}
|
|
2844
1544
|
},
|
|
2845
1545
|
};
|
|
1546
|
+
installAuthorityFilesystem(imports, {
|
|
1547
|
+
fs: () => __wasiSup ? supervisorFilesystem(__wasiSup, opts.parking === 'none' ? __wasiSup.synchronous : undefined) : null,
|
|
1548
|
+
memory: opts.getMemory,
|
|
1549
|
+
fds: fdTable,
|
|
1550
|
+
allocateFd: __wasiAllocateFd,
|
|
1551
|
+
abi: opts.abi,
|
|
1552
|
+
synchronous: opts.parking === 'none',
|
|
1553
|
+
residentBytes: __wasiFS.residentFileCap,
|
|
1554
|
+
});
|
|
2846
1555
|
// Raw async socket bodies, captured BEFORE JSPI-wrapping so fd_read /
|
|
2847
1556
|
// fd_write can route socket fds through them (wasi-libc maps read(2)/
|
|
2848
1557
|
// write(2) to fd_read/fd_write for every fd kind, sockets included).
|
|
@@ -2858,7 +1567,13 @@ export function __wasiMakeImports(opts) {
|
|
|
2858
1567
|
// Every import that can park.
|
|
2859
1568
|
const parkable = [
|
|
2860
1569
|
'sock_send', 'sock_recv', 'sock_shutdown', 'sock_accept', 'poll_oneoff',
|
|
2861
|
-
'fd_read', 'fd_write', 'fd_pread', 'path_filestat_get',
|
|
1570
|
+
'fd_read', 'fd_write', 'fd_pread', 'fd_pwrite', 'path_filestat_get',
|
|
1571
|
+
'path_open', 'fd_close', 'fd_renumber', 'fd_seek', 'fd_tell', 'fd_filestat_get',
|
|
1572
|
+
'fd_fdstat_set_flags', 'fd_fdstat_set_rights', 'fd_filestat_set_size',
|
|
1573
|
+
'fd_sync', 'fd_datasync', 'fd_allocate', 'fd_advise', 'fd_readdir',
|
|
1574
|
+
'path_create_directory', 'path_remove_directory', 'path_unlink_file',
|
|
1575
|
+
'path_rename', 'path_symlink', 'path_readlink', 'path_link',
|
|
1576
|
+
'fd_filestat_set_times', 'path_filestat_set_times',
|
|
2862
1577
|
];
|
|
2863
1578
|
// Applied before Suspending wraps them.
|
|
2864
1579
|
for (const name of parkable) {
|
|
@@ -2908,33 +1623,35 @@ export function __wasiMakeImports(opts) {
|
|
|
2908
1623
|
imports.sock_recv = new WebAssembly.Suspending(imports.sock_recv);
|
|
2909
1624
|
imports.sock_shutdown = new WebAssembly.Suspending(imports.sock_shutdown);
|
|
2910
1625
|
imports.sock_accept = new WebAssembly.Suspending(imports.sock_accept);
|
|
2911
|
-
// WASI socket and polling support B8: poll_oneoff also needs JSPI to support CLOCK
|
|
2912
|
-
// subscriptions (await setTimeout) and socket-fd readiness
|
|
2913
|
-
// (await reader.read()). Same Suspending shape as sock_*.
|
|
2914
1626
|
imports.poll_oneoff = new WebAssembly.Suspending(imports.poll_oneoff);
|
|
2915
|
-
// fd_read/fd_write become suspending too: a socket fd routes to the
|
|
2916
|
-
// async sock bodies (Promise → JSPI suspends). File/stdio ops return a
|
|
2917
|
-
// plain errno number, which JSPI passes through UNDER AN ACTIVE
|
|
2918
|
-
// SUSPENDER. There is no passthrough without one: V8 traps ANY call
|
|
2919
|
-
// into a Suspending import off a non-promising stack with SuspendError
|
|
2920
|
-
// "trying to suspend without WebAssembly.promising", even when the
|
|
2921
|
-
// import returns a plain number (measured on workerd, compat
|
|
2922
|
-
// 2026-04-01). Every export that can reach these imports must be
|
|
2923
|
-
// entered via WebAssembly.promising — assuming sync callers were
|
|
2924
|
-
// unaffected is exactly how the Ruby VM boot went dark.
|
|
2925
1627
|
imports.fd_read = new WebAssembly.Suspending(imports.fd_read);
|
|
2926
1628
|
imports.fd_write = new WebAssembly.Suspending(imports.fd_write);
|
|
2927
|
-
// fd_pread reaches the same file bodies as fd_read and so can equally
|
|
2928
|
-
// land on a cache miss that must go to the supervisor. Leaving it
|
|
2929
|
-
// unwrapped would trap the guest the first time a demand load happened
|
|
2930
|
-
// to arrive through pread(2) rather than read(2).
|
|
2931
1629
|
imports.fd_pread = new WebAssembly.Suspending(imports.fd_pread);
|
|
2932
|
-
|
|
2933
|
-
// asking the supervisor, so it too can return a Promise.
|
|
1630
|
+
imports.fd_pwrite = new WebAssembly.Suspending(imports.fd_pwrite);
|
|
2934
1631
|
imports.path_filestat_get = new WebAssembly.Suspending(imports.path_filestat_get);
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
1632
|
+
imports.path_open = new WebAssembly.Suspending(imports.path_open);
|
|
1633
|
+
imports.fd_close = new WebAssembly.Suspending(imports.fd_close);
|
|
1634
|
+
imports.fd_renumber = new WebAssembly.Suspending(imports.fd_renumber);
|
|
1635
|
+
imports.fd_seek = new WebAssembly.Suspending(imports.fd_seek);
|
|
1636
|
+
imports.fd_tell = new WebAssembly.Suspending(imports.fd_tell);
|
|
1637
|
+
imports.fd_filestat_get = new WebAssembly.Suspending(imports.fd_filestat_get);
|
|
1638
|
+
imports.fd_fdstat_set_flags = new WebAssembly.Suspending(imports.fd_fdstat_set_flags);
|
|
1639
|
+
imports.fd_fdstat_set_rights = new WebAssembly.Suspending(imports.fd_fdstat_set_rights);
|
|
1640
|
+
imports.fd_filestat_set_size = new WebAssembly.Suspending(imports.fd_filestat_set_size);
|
|
1641
|
+
imports.fd_sync = new WebAssembly.Suspending(imports.fd_sync);
|
|
1642
|
+
imports.fd_datasync = new WebAssembly.Suspending(imports.fd_datasync);
|
|
1643
|
+
imports.fd_allocate = new WebAssembly.Suspending(imports.fd_allocate);
|
|
1644
|
+
imports.fd_advise = new WebAssembly.Suspending(imports.fd_advise);
|
|
1645
|
+
imports.fd_readdir = new WebAssembly.Suspending(imports.fd_readdir);
|
|
1646
|
+
imports.path_create_directory = new WebAssembly.Suspending(imports.path_create_directory);
|
|
1647
|
+
imports.path_remove_directory = new WebAssembly.Suspending(imports.path_remove_directory);
|
|
1648
|
+
imports.path_unlink_file = new WebAssembly.Suspending(imports.path_unlink_file);
|
|
1649
|
+
imports.path_rename = new WebAssembly.Suspending(imports.path_rename);
|
|
1650
|
+
imports.path_symlink = new WebAssembly.Suspending(imports.path_symlink);
|
|
1651
|
+
imports.path_readlink = new WebAssembly.Suspending(imports.path_readlink);
|
|
1652
|
+
imports.path_link = new WebAssembly.Suspending(imports.path_link);
|
|
1653
|
+
imports.fd_filestat_set_times = new WebAssembly.Suspending(imports.fd_filestat_set_times);
|
|
1654
|
+
imports.path_filestat_set_times = new WebAssembly.Suspending(imports.path_filestat_set_times);
|
|
2938
1655
|
if (opts.threads)
|
|
2939
1656
|
imports.sched_yield = new WebAssembly.Suspending(imports.sched_yield);
|
|
2940
1657
|
}
|
|
@@ -3006,6 +1723,4 @@ export async function __wasiRunStartAsync(instance, ctx) {
|
|
|
3006
1723
|
// already follow) because a direct reference to a preamble-only symbol will
|
|
3007
1724
|
// not typecheck in the supervisor bundle the body is authored in.
|
|
3008
1725
|
globalThis.__wasiAdoptSupervisor = __wasiAdoptSupervisor;
|
|
3009
|
-
globalThis.__wasiDrainPersist = __wasiDrainPersist;
|
|
3010
|
-
globalThis.__wasiRevalidateFS = __wasiRevalidateFS;
|
|
3011
1726
|
// ── END: wasi-instance preamble ─────────────────────────────────────────
|