@secure-exec/wasmvm 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/browser-driver.d.ts +68 -0
- package/dist/browser-driver.d.ts.map +1 -0
- package/dist/browser-driver.js +293 -0
- package/dist/browser-driver.js.map +1 -0
- package/dist/driver.d.ts +43 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +1420 -0
- package/dist/driver.js.map +1 -0
- package/dist/fd-table.d.ts +67 -0
- package/dist/fd-table.d.ts.map +1 -0
- package/dist/fd-table.js +171 -0
- package/dist/fd-table.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel-worker.d.ts +14 -0
- package/dist/kernel-worker.d.ts.map +1 -0
- package/dist/kernel-worker.js +1205 -0
- package/dist/kernel-worker.js.map +1 -0
- package/dist/module-cache.d.ts +21 -0
- package/dist/module-cache.d.ts.map +1 -0
- package/dist/module-cache.js +51 -0
- package/dist/module-cache.js.map +1 -0
- package/dist/permission-check.d.ts +36 -0
- package/dist/permission-check.d.ts.map +1 -0
- package/dist/permission-check.js +84 -0
- package/dist/permission-check.js.map +1 -0
- package/dist/ring-buffer.d.ts +64 -0
- package/dist/ring-buffer.d.ts.map +1 -0
- package/dist/ring-buffer.js +160 -0
- package/dist/ring-buffer.js.map +1 -0
- package/dist/syscall-rpc.d.ts +95 -0
- package/dist/syscall-rpc.d.ts.map +1 -0
- package/dist/syscall-rpc.js +48 -0
- package/dist/syscall-rpc.js.map +1 -0
- package/dist/user.d.ts +58 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +143 -0
- package/dist/user.js.map +1 -0
- package/dist/wasi-constants.d.ts +77 -0
- package/dist/wasi-constants.d.ts.map +1 -0
- package/dist/wasi-constants.js +122 -0
- package/dist/wasi-constants.js.map +1 -0
- package/dist/wasi-file-io.d.ts +50 -0
- package/dist/wasi-file-io.d.ts.map +1 -0
- package/dist/wasi-file-io.js +10 -0
- package/dist/wasi-file-io.js.map +1 -0
- package/dist/wasi-polyfill.d.ts +368 -0
- package/dist/wasi-polyfill.d.ts.map +1 -0
- package/dist/wasi-polyfill.js +1438 -0
- package/dist/wasi-polyfill.js.map +1 -0
- package/dist/wasi-process-io.d.ts +35 -0
- package/dist/wasi-process-io.d.ts.map +1 -0
- package/dist/wasi-process-io.js +11 -0
- package/dist/wasi-process-io.js.map +1 -0
- package/dist/wasi-types.d.ts +175 -0
- package/dist/wasi-types.d.ts.map +1 -0
- package/dist/wasi-types.js +68 -0
- package/dist/wasi-types.js.map +1 -0
- package/dist/wasm-magic.d.ts +12 -0
- package/dist/wasm-magic.d.ts.map +1 -0
- package/dist/wasm-magic.js +53 -0
- package/dist/wasm-magic.js.map +1 -0
- package/dist/worker-adapter.d.ts +32 -0
- package/dist/worker-adapter.d.ts.map +1 -0
- package/dist/worker-adapter.js +147 -0
- package/dist/worker-adapter.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASI polyfill for wasi_snapshot_preview1.
|
|
3
|
+
*
|
|
4
|
+
* Implements all 46 wasi_snapshot_preview1 functions:
|
|
5
|
+
* - Core fd and prestat operations (US-007)
|
|
6
|
+
* - Path, directory, and filestat operations (US-008)
|
|
7
|
+
* - Args, env, clock, random, proc_exit, and remaining stubs (US-009)
|
|
8
|
+
*/
|
|
9
|
+
import { FILETYPE_UNKNOWN, FILETYPE_REGULAR_FILE, FILETYPE_DIRECTORY, FILETYPE_CHARACTER_DEVICE, FILETYPE_SYMBOLIC_LINK, RIGHT_FD_DATASYNC, RIGHT_FD_READ, RIGHT_FD_SEEK, RIGHT_FD_FDSTAT_SET_FLAGS, RIGHT_FD_SYNC, RIGHT_FD_TELL, RIGHT_FD_WRITE, RIGHT_FD_ADVISE, RIGHT_FD_ALLOCATE, RIGHT_FD_READDIR, RIGHT_FD_FILESTAT_GET, RIGHT_FD_FILESTAT_SET_SIZE, RIGHT_FD_FILESTAT_SET_TIMES, RIGHT_PATH_CREATE_DIRECTORY, RIGHT_PATH_CREATE_FILE, RIGHT_PATH_LINK_SOURCE, RIGHT_PATH_LINK_TARGET, RIGHT_PATH_OPEN, RIGHT_PATH_READLINK, RIGHT_PATH_RENAME_SOURCE, RIGHT_PATH_RENAME_TARGET, RIGHT_PATH_FILESTAT_GET, RIGHT_PATH_FILESTAT_SET_SIZE, RIGHT_PATH_FILESTAT_SET_TIMES, RIGHT_PATH_SYMLINK, RIGHT_PATH_REMOVE_DIRECTORY, RIGHT_PATH_UNLINK_FILE, RIGHT_POLL_FD_READWRITE, ERRNO_SUCCESS, ERRNO_EBADF, ERRNO_EINVAL, } from './wasi-constants.js';
|
|
10
|
+
import { VfsError } from './wasi-types.js';
|
|
11
|
+
// Additional WASI errno codes
|
|
12
|
+
export const ERRNO_ESPIPE = 70;
|
|
13
|
+
export const ERRNO_EISDIR = 31;
|
|
14
|
+
export const ERRNO_ENOMEM = 48;
|
|
15
|
+
export const ERRNO_ENOSYS = 52;
|
|
16
|
+
export const ERRNO_ENOENT = 44;
|
|
17
|
+
export const ERRNO_EEXIST = 20;
|
|
18
|
+
export const ERRNO_ENOTDIR = 54;
|
|
19
|
+
export const ERRNO_ENOTEMPTY = 55;
|
|
20
|
+
export const ERRNO_ELOOP = 36;
|
|
21
|
+
export const ERRNO_EACCES = 2;
|
|
22
|
+
export const ERRNO_EPERM = 63;
|
|
23
|
+
export const ERRNO_EIO = 29;
|
|
24
|
+
// Map VfsError codes to WASI errno numbers
|
|
25
|
+
const ERRNO_MAP = {
|
|
26
|
+
ENOENT: 44,
|
|
27
|
+
EEXIST: 20,
|
|
28
|
+
ENOTDIR: 54,
|
|
29
|
+
EISDIR: 31,
|
|
30
|
+
ENOTEMPTY: 55,
|
|
31
|
+
EACCES: 2,
|
|
32
|
+
EBADF: 8,
|
|
33
|
+
EINVAL: 28,
|
|
34
|
+
EPERM: 63,
|
|
35
|
+
};
|
|
36
|
+
/** Map a caught error to a WASI errno. VfsError maps via code; unknown errors → EIO. */
|
|
37
|
+
function vfsErrorToErrno(e) {
|
|
38
|
+
if (e instanceof VfsError) {
|
|
39
|
+
return ERRNO_MAP[e.code] ?? ERRNO_EIO;
|
|
40
|
+
}
|
|
41
|
+
return ERRNO_EIO;
|
|
42
|
+
}
|
|
43
|
+
// Re-export for convenience
|
|
44
|
+
export { ERRNO_SUCCESS, ERRNO_EBADF, ERRNO_EINVAL };
|
|
45
|
+
// WASI seek whence values
|
|
46
|
+
const WHENCE_SET = 0;
|
|
47
|
+
const WHENCE_CUR = 1;
|
|
48
|
+
const WHENCE_END = 2;
|
|
49
|
+
// WASI lookup flags
|
|
50
|
+
const LOOKUP_SYMLINK_FOLLOW = 1;
|
|
51
|
+
// WASI open flags (oflags)
|
|
52
|
+
const OFLAG_CREAT = 1;
|
|
53
|
+
const OFLAG_DIRECTORY = 2;
|
|
54
|
+
const OFLAG_EXCL = 4;
|
|
55
|
+
const OFLAG_TRUNC = 8;
|
|
56
|
+
// WASI fstflags (for set_times)
|
|
57
|
+
const FSTFLAG_ATIM = 1;
|
|
58
|
+
const FSTFLAG_ATIM_NOW = 2;
|
|
59
|
+
const FSTFLAG_MTIM = 4;
|
|
60
|
+
const FSTFLAG_MTIM_NOW = 8;
|
|
61
|
+
// WASI preopentype
|
|
62
|
+
const PREOPENTYPE_DIR = 0;
|
|
63
|
+
// WASI clock IDs
|
|
64
|
+
const CLOCKID_REALTIME = 0;
|
|
65
|
+
const CLOCKID_MONOTONIC = 1;
|
|
66
|
+
const CLOCKID_PROCESS_CPUTIME_ID = 2;
|
|
67
|
+
const CLOCKID_THREAD_CPUTIME_ID = 3;
|
|
68
|
+
// WASI subscription/event types for poll_oneoff
|
|
69
|
+
const EVENTTYPE_CLOCK = 0;
|
|
70
|
+
const EVENTTYPE_FD_READ = 1;
|
|
71
|
+
const EVENTTYPE_FD_WRITE = 2;
|
|
72
|
+
/** Normalize a POSIX path — resolve `.` and `..`, collapse slashes. */
|
|
73
|
+
function normalizePath(path) {
|
|
74
|
+
const parts = path.split('/');
|
|
75
|
+
const resolved = [];
|
|
76
|
+
for (const p of parts) {
|
|
77
|
+
if (p === '' || p === '.')
|
|
78
|
+
continue;
|
|
79
|
+
if (p === '..') {
|
|
80
|
+
resolved.pop();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
resolved.push(p);
|
|
84
|
+
}
|
|
85
|
+
return '/' + resolved.join('/');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Exception thrown by proc_exit to terminate WASM execution.
|
|
89
|
+
* Callers should catch this to extract the exit code.
|
|
90
|
+
*/
|
|
91
|
+
export class WasiProcExit extends Error {
|
|
92
|
+
exitCode;
|
|
93
|
+
constructor(exitCode) {
|
|
94
|
+
super(`proc_exit(${exitCode})`);
|
|
95
|
+
this.exitCode = exitCode;
|
|
96
|
+
this.name = 'WasiProcExit';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// All rights for files
|
|
100
|
+
const RIGHTS_FILE_BASE = RIGHT_FD_DATASYNC | RIGHT_FD_READ | RIGHT_FD_SEEK |
|
|
101
|
+
RIGHT_FD_FDSTAT_SET_FLAGS | RIGHT_FD_SYNC | RIGHT_FD_TELL | RIGHT_FD_WRITE |
|
|
102
|
+
RIGHT_FD_ADVISE | RIGHT_FD_ALLOCATE | RIGHT_FD_FILESTAT_GET |
|
|
103
|
+
RIGHT_FD_FILESTAT_SET_SIZE | RIGHT_FD_FILESTAT_SET_TIMES |
|
|
104
|
+
RIGHT_POLL_FD_READWRITE;
|
|
105
|
+
// All rights for directories
|
|
106
|
+
const RIGHTS_DIR_BASE = RIGHT_FD_FDSTAT_SET_FLAGS | RIGHT_FD_SYNC |
|
|
107
|
+
RIGHT_FD_READDIR | RIGHT_PATH_CREATE_DIRECTORY | RIGHT_PATH_CREATE_FILE |
|
|
108
|
+
RIGHT_PATH_LINK_SOURCE | RIGHT_PATH_LINK_TARGET | RIGHT_PATH_OPEN |
|
|
109
|
+
RIGHT_PATH_READLINK | RIGHT_PATH_RENAME_SOURCE | RIGHT_PATH_RENAME_TARGET |
|
|
110
|
+
RIGHT_PATH_FILESTAT_GET | RIGHT_PATH_FILESTAT_SET_SIZE |
|
|
111
|
+
RIGHT_PATH_FILESTAT_SET_TIMES | RIGHT_PATH_SYMLINK |
|
|
112
|
+
RIGHT_PATH_REMOVE_DIRECTORY | RIGHT_PATH_UNLINK_FILE |
|
|
113
|
+
RIGHT_FD_FILESTAT_GET | RIGHT_FD_FILESTAT_SET_TIMES;
|
|
114
|
+
// Files opened from a pre-opened directory can inherit these rights
|
|
115
|
+
const RIGHTS_DIR_INHERITING = RIGHTS_FILE_BASE | RIGHTS_DIR_BASE;
|
|
116
|
+
/**
|
|
117
|
+
* Concatenate multiple Uint8Array chunks into one.
|
|
118
|
+
*/
|
|
119
|
+
function concatBytes(arrays) {
|
|
120
|
+
if (arrays.length === 0)
|
|
121
|
+
return new Uint8Array(0);
|
|
122
|
+
if (arrays.length === 1)
|
|
123
|
+
return arrays[0];
|
|
124
|
+
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
125
|
+
const result = new Uint8Array(total);
|
|
126
|
+
let offset = 0;
|
|
127
|
+
for (const a of arrays) {
|
|
128
|
+
result.set(a, offset);
|
|
129
|
+
offset += a.length;
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* WASI polyfill implementing wasi_snapshot_preview1.
|
|
135
|
+
*
|
|
136
|
+
* Phase 1: Core fd and prestat operations (US-007).
|
|
137
|
+
* Additional operations added in US-008, US-009.
|
|
138
|
+
*/
|
|
139
|
+
export class WasiPolyfill {
|
|
140
|
+
fdTable;
|
|
141
|
+
vfs;
|
|
142
|
+
args;
|
|
143
|
+
env;
|
|
144
|
+
memory;
|
|
145
|
+
exitCode;
|
|
146
|
+
_fileIO;
|
|
147
|
+
_processIO;
|
|
148
|
+
_stdinData;
|
|
149
|
+
_stdinOffset;
|
|
150
|
+
_stdinReader;
|
|
151
|
+
_stdoutWriter;
|
|
152
|
+
_stderrWriter;
|
|
153
|
+
_sleepHook;
|
|
154
|
+
_stdoutChunks;
|
|
155
|
+
_stderrChunks;
|
|
156
|
+
_preopens;
|
|
157
|
+
constructor(fdTable, vfs, options) {
|
|
158
|
+
this.fdTable = fdTable;
|
|
159
|
+
this.vfs = vfs;
|
|
160
|
+
this._fileIO = options.fileIO;
|
|
161
|
+
this.args = options.args ?? [];
|
|
162
|
+
this.env = options.env ?? {};
|
|
163
|
+
this._processIO = options.processIO;
|
|
164
|
+
this.memory = options.memory ?? null;
|
|
165
|
+
this.exitCode = null;
|
|
166
|
+
// Stdin
|
|
167
|
+
if (typeof options.stdin === 'string') {
|
|
168
|
+
this._stdinData = new TextEncoder().encode(options.stdin);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this._stdinData = options.stdin ?? null;
|
|
172
|
+
}
|
|
173
|
+
this._stdinOffset = 0;
|
|
174
|
+
// Streaming I/O callbacks (for parallel pipelines with ring buffers)
|
|
175
|
+
this._stdinReader = null;
|
|
176
|
+
this._stdoutWriter = null;
|
|
177
|
+
this._stderrWriter = null;
|
|
178
|
+
this._sleepHook = null;
|
|
179
|
+
// Collected output
|
|
180
|
+
this._stdoutChunks = [];
|
|
181
|
+
this._stderrChunks = [];
|
|
182
|
+
// Pre-opened directories: fd -> path
|
|
183
|
+
this._preopens = new Map();
|
|
184
|
+
this._setupPreopens();
|
|
185
|
+
}
|
|
186
|
+
_setupPreopens() {
|
|
187
|
+
const fd = this.fdTable.open({ type: 'preopen', path: '/' }, {
|
|
188
|
+
filetype: FILETYPE_DIRECTORY,
|
|
189
|
+
rightsBase: RIGHTS_DIR_BASE,
|
|
190
|
+
rightsInheriting: RIGHTS_DIR_INHERITING,
|
|
191
|
+
fdflags: 0,
|
|
192
|
+
path: '/',
|
|
193
|
+
});
|
|
194
|
+
this._preopens.set(fd, '/');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Set the WASM memory reference (call after WebAssembly.instantiate).
|
|
198
|
+
*/
|
|
199
|
+
setMemory(memory) {
|
|
200
|
+
this.memory = memory;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set a blocking stdin reader for parallel pipeline mode.
|
|
204
|
+
* The reader function should have signature: (buf, offset, length) => bytesRead
|
|
205
|
+
* Returns 0 on EOF.
|
|
206
|
+
*/
|
|
207
|
+
setStdinReader(reader) {
|
|
208
|
+
this._stdinReader = reader;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set a blocking stdout writer for parallel pipeline mode.
|
|
212
|
+
* The writer function should have signature: (buf, offset, length) => void
|
|
213
|
+
*/
|
|
214
|
+
setStdoutWriter(writer) {
|
|
215
|
+
this._stdoutWriter = writer;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Set a blocking stderr writer for streaming mode.
|
|
219
|
+
* The writer function should have signature: (buf, offset, length) => void
|
|
220
|
+
*/
|
|
221
|
+
setStderrWriter(writer) {
|
|
222
|
+
this._stderrWriter = writer;
|
|
223
|
+
}
|
|
224
|
+
/** Set a hook to run while clock sleeps block in poll_oneoff. */
|
|
225
|
+
setSleepHook(hook) {
|
|
226
|
+
this._sleepHook = hook;
|
|
227
|
+
}
|
|
228
|
+
/** Append raw data to the stdout collection (used by inline child execution). */
|
|
229
|
+
appendStdout(data) {
|
|
230
|
+
if (data.length > 0) {
|
|
231
|
+
this._stdoutChunks.push(data.slice());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Append raw data to the stderr collection (used by inline child execution). */
|
|
235
|
+
appendStderr(data) {
|
|
236
|
+
if (data.length > 0) {
|
|
237
|
+
this._stderrChunks.push(data.slice());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Get collected stdout as Uint8Array. */
|
|
241
|
+
get stdout() {
|
|
242
|
+
return concatBytes(this._stdoutChunks);
|
|
243
|
+
}
|
|
244
|
+
/** Get collected stderr as Uint8Array. */
|
|
245
|
+
get stderr() {
|
|
246
|
+
return concatBytes(this._stderrChunks);
|
|
247
|
+
}
|
|
248
|
+
/** Get collected stdout as string. */
|
|
249
|
+
get stdoutString() {
|
|
250
|
+
return new TextDecoder().decode(this.stdout);
|
|
251
|
+
}
|
|
252
|
+
/** Get collected stderr as string. */
|
|
253
|
+
get stderrString() {
|
|
254
|
+
return new TextDecoder().decode(this.stderr);
|
|
255
|
+
}
|
|
256
|
+
// --- Memory helpers ---
|
|
257
|
+
_view() {
|
|
258
|
+
return new DataView(this.memory.buffer);
|
|
259
|
+
}
|
|
260
|
+
_bytes() {
|
|
261
|
+
return new Uint8Array(this.memory.buffer);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read an array of iovec structs from WASM memory.
|
|
265
|
+
* Each iovec is { buf: u32, buf_len: u32 } = 8 bytes.
|
|
266
|
+
*/
|
|
267
|
+
_readIovecs(iovs_ptr, iovs_len) {
|
|
268
|
+
const view = this._view();
|
|
269
|
+
const iovecs = [];
|
|
270
|
+
for (let i = 0; i < iovs_len; i++) {
|
|
271
|
+
const base = iovs_ptr + i * 8;
|
|
272
|
+
iovecs.push({
|
|
273
|
+
buf: view.getUint32(base, true),
|
|
274
|
+
buf_len: view.getUint32(base + 4, true),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return iovecs;
|
|
278
|
+
}
|
|
279
|
+
// --- Core FD operations ---
|
|
280
|
+
/**
|
|
281
|
+
* Read from a file descriptor into iovec buffers.
|
|
282
|
+
* Handles stdio (stdin), VFS files, and pipes.
|
|
283
|
+
*/
|
|
284
|
+
fd_read(fd, iovs_ptr, iovs_len, nread_ptr) {
|
|
285
|
+
const entry = this.fdTable.get(fd);
|
|
286
|
+
if (!entry)
|
|
287
|
+
return ERRNO_EBADF;
|
|
288
|
+
if (!(entry.rightsBase & RIGHT_FD_READ))
|
|
289
|
+
return ERRNO_EBADF;
|
|
290
|
+
const iovecs = this._readIovecs(iovs_ptr, iovs_len);
|
|
291
|
+
const mem = this._bytes();
|
|
292
|
+
let totalRead = 0;
|
|
293
|
+
const resource = entry.resource;
|
|
294
|
+
if (resource.type === 'stdio' && resource.name === 'stdin') {
|
|
295
|
+
if (this._stdinReader) {
|
|
296
|
+
// Streaming mode: read from ring buffer (blocks via Atomics.wait)
|
|
297
|
+
for (const iov of iovecs) {
|
|
298
|
+
if (iov.buf_len === 0)
|
|
299
|
+
continue;
|
|
300
|
+
const tmpBuf = new Uint8Array(iov.buf_len);
|
|
301
|
+
const n = this._stdinReader(tmpBuf, 0, iov.buf_len);
|
|
302
|
+
if (n <= 0)
|
|
303
|
+
break; // EOF
|
|
304
|
+
mem.set(tmpBuf.subarray(0, n), iov.buf);
|
|
305
|
+
totalRead += n;
|
|
306
|
+
if (n < iov.buf_len)
|
|
307
|
+
break; // Short read -- don't block further
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Buffered mode: read from pre-loaded stdin data
|
|
312
|
+
if (!this._stdinData || this._stdinOffset >= this._stdinData.length) {
|
|
313
|
+
this._view().setUint32(nread_ptr, 0, true);
|
|
314
|
+
return ERRNO_SUCCESS;
|
|
315
|
+
}
|
|
316
|
+
for (const iov of iovecs) {
|
|
317
|
+
const remaining = this._stdinData.length - this._stdinOffset;
|
|
318
|
+
if (remaining <= 0)
|
|
319
|
+
break;
|
|
320
|
+
const n = Math.min(iov.buf_len, remaining);
|
|
321
|
+
mem.set(this._stdinData.subarray(this._stdinOffset, this._stdinOffset + n), iov.buf);
|
|
322
|
+
this._stdinOffset += n;
|
|
323
|
+
totalRead += n;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (resource.type === 'vfsFile') {
|
|
328
|
+
// Delegate to kernel file I/O bridge
|
|
329
|
+
const totalRequested = iovecs.reduce((sum, iov) => sum + iov.buf_len, 0);
|
|
330
|
+
const result = this._fileIO.fdRead(fd, totalRequested);
|
|
331
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
332
|
+
return result.errno;
|
|
333
|
+
// Scatter data into iovecs
|
|
334
|
+
let offset = 0;
|
|
335
|
+
for (const iov of iovecs) {
|
|
336
|
+
const remaining = result.data.length - offset;
|
|
337
|
+
if (remaining <= 0)
|
|
338
|
+
break;
|
|
339
|
+
const n = Math.min(iov.buf_len, remaining);
|
|
340
|
+
mem.set(result.data.subarray(offset, offset + n), iov.buf);
|
|
341
|
+
offset += n;
|
|
342
|
+
totalRead += n;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (resource.type === 'pipe') {
|
|
346
|
+
const pipe = resource.pipe;
|
|
347
|
+
if (pipe && pipe.buffer) {
|
|
348
|
+
// Assert: only one reader may consume from a pipe's read end.
|
|
349
|
+
// If a different fd already claimed this pipe, throw to prevent
|
|
350
|
+
// silent data corruption from double-consumption.
|
|
351
|
+
if (pipe._readerId !== undefined && pipe._readerId !== fd) {
|
|
352
|
+
throw new Error(`Pipe read end consumed by multiple readers (fd ${pipe._readerId} and fd ${fd})`);
|
|
353
|
+
}
|
|
354
|
+
pipe._readerId = fd;
|
|
355
|
+
for (const iov of iovecs) {
|
|
356
|
+
const remaining = pipe.writeOffset - pipe.readOffset;
|
|
357
|
+
if (remaining <= 0)
|
|
358
|
+
break;
|
|
359
|
+
const n = Math.min(iov.buf_len, remaining);
|
|
360
|
+
mem.set(pipe.buffer.subarray(pipe.readOffset, pipe.readOffset + n), iov.buf);
|
|
361
|
+
pipe.readOffset += n;
|
|
362
|
+
totalRead += n;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
return ERRNO_EBADF;
|
|
368
|
+
}
|
|
369
|
+
this._view().setUint32(nread_ptr, totalRead, true);
|
|
370
|
+
return ERRNO_SUCCESS;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Write from iovec buffers to a file descriptor.
|
|
374
|
+
* Handles stdio (stdout/stderr collection), VFS files, and pipes.
|
|
375
|
+
*/
|
|
376
|
+
fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) {
|
|
377
|
+
const entry = this.fdTable.get(fd);
|
|
378
|
+
if (!entry)
|
|
379
|
+
return ERRNO_EBADF;
|
|
380
|
+
if (!(entry.rightsBase & RIGHT_FD_WRITE))
|
|
381
|
+
return ERRNO_EBADF;
|
|
382
|
+
const iovecs = this._readIovecs(iovs_ptr, iovs_len);
|
|
383
|
+
const mem = this._bytes();
|
|
384
|
+
let totalWritten = 0;
|
|
385
|
+
const resource = entry.resource;
|
|
386
|
+
if (resource.type === 'stdio') {
|
|
387
|
+
for (const iov of iovecs) {
|
|
388
|
+
if (iov.buf_len === 0)
|
|
389
|
+
continue;
|
|
390
|
+
const chunk = mem.slice(iov.buf, iov.buf + iov.buf_len);
|
|
391
|
+
if (resource.name === 'stdout') {
|
|
392
|
+
if (this._stdoutWriter) {
|
|
393
|
+
// Streaming mode: write to ring buffer (blocks via Atomics.wait)
|
|
394
|
+
this._stdoutWriter(chunk, 0, chunk.length);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this._stdoutChunks.push(chunk);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (resource.name === 'stderr') {
|
|
401
|
+
if (this._stderrWriter) {
|
|
402
|
+
this._stderrWriter(chunk, 0, chunk.length);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
this._stderrChunks.push(chunk);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
totalWritten += iov.buf_len;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (resource.type === 'vfsFile') {
|
|
412
|
+
// Collect all write data, then delegate to kernel file I/O bridge
|
|
413
|
+
const chunks = [];
|
|
414
|
+
for (const iov of iovecs) {
|
|
415
|
+
if (iov.buf_len === 0)
|
|
416
|
+
continue;
|
|
417
|
+
chunks.push(mem.slice(iov.buf, iov.buf + iov.buf_len));
|
|
418
|
+
totalWritten += iov.buf_len;
|
|
419
|
+
}
|
|
420
|
+
if (totalWritten > 0) {
|
|
421
|
+
const writeData = concatBytes(chunks);
|
|
422
|
+
const result = this._fileIO.fdWrite(fd, writeData);
|
|
423
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
424
|
+
return result.errno;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (resource.type === 'pipe') {
|
|
428
|
+
const pipe = resource.pipe;
|
|
429
|
+
for (const iov of iovecs) {
|
|
430
|
+
if (iov.buf_len === 0)
|
|
431
|
+
continue;
|
|
432
|
+
const chunk = mem.slice(iov.buf, iov.buf + iov.buf_len);
|
|
433
|
+
if (pipe) {
|
|
434
|
+
const needed = pipe.writeOffset + chunk.length;
|
|
435
|
+
if (needed > pipe.buffer.length) {
|
|
436
|
+
const newBuf = new Uint8Array(Math.max(needed, pipe.buffer.length * 2));
|
|
437
|
+
newBuf.set(pipe.buffer);
|
|
438
|
+
pipe.buffer = newBuf;
|
|
439
|
+
}
|
|
440
|
+
pipe.buffer.set(chunk, pipe.writeOffset);
|
|
441
|
+
pipe.writeOffset += chunk.length;
|
|
442
|
+
}
|
|
443
|
+
totalWritten += iov.buf_len;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
return ERRNO_EBADF;
|
|
448
|
+
}
|
|
449
|
+
this._view().setUint32(nwritten_ptr, totalWritten, true);
|
|
450
|
+
return ERRNO_SUCCESS;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Seek within a file descriptor. Delegates to kernel file I/O bridge.
|
|
454
|
+
*/
|
|
455
|
+
fd_seek(fd, offset, whence, newoffset_ptr) {
|
|
456
|
+
const entry = this.fdTable.get(fd);
|
|
457
|
+
if (!entry)
|
|
458
|
+
return ERRNO_EBADF;
|
|
459
|
+
if (entry.filetype !== FILETYPE_REGULAR_FILE)
|
|
460
|
+
return ERRNO_ESPIPE;
|
|
461
|
+
if (!(entry.rightsBase & RIGHT_FD_SEEK))
|
|
462
|
+
return ERRNO_EBADF;
|
|
463
|
+
const offsetBig = typeof offset === 'bigint' ? offset : BigInt(offset);
|
|
464
|
+
const result = this._fileIO.fdSeek(fd, offsetBig, whence);
|
|
465
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
466
|
+
return result.errno;
|
|
467
|
+
// Sync local cursor so fd_tell returns consistent values
|
|
468
|
+
entry.cursor = result.newOffset;
|
|
469
|
+
this._view().setBigUint64(newoffset_ptr, result.newOffset, true);
|
|
470
|
+
return ERRNO_SUCCESS;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get current file position.
|
|
474
|
+
*/
|
|
475
|
+
fd_tell(fd, offset_ptr) {
|
|
476
|
+
const entry = this.fdTable.get(fd);
|
|
477
|
+
if (!entry)
|
|
478
|
+
return ERRNO_EBADF;
|
|
479
|
+
if (entry.filetype !== FILETYPE_REGULAR_FILE)
|
|
480
|
+
return ERRNO_ESPIPE;
|
|
481
|
+
if (!(entry.rightsBase & RIGHT_FD_TELL))
|
|
482
|
+
return ERRNO_EBADF;
|
|
483
|
+
this._view().setBigUint64(offset_ptr, entry.cursor, true);
|
|
484
|
+
return ERRNO_SUCCESS;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Close a file descriptor. Delegates to kernel file I/O bridge.
|
|
488
|
+
*/
|
|
489
|
+
fd_close(fd) {
|
|
490
|
+
this._preopens.delete(fd);
|
|
491
|
+
return this._fileIO.fdClose(fd);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get file descriptor status.
|
|
495
|
+
* Writes fdstat struct (24 bytes) at buf_ptr:
|
|
496
|
+
* offset 0: fs_filetype (u8)
|
|
497
|
+
* offset 2: fs_flags (u16 LE)
|
|
498
|
+
* offset 8: fs_rights_base (u64 LE)
|
|
499
|
+
* offset 16: fs_rights_inheriting (u64 LE)
|
|
500
|
+
*/
|
|
501
|
+
fd_fdstat_get(fd, buf_ptr) {
|
|
502
|
+
const stat = this._processIO.fdFdstatGet(fd);
|
|
503
|
+
if (stat.errno !== ERRNO_SUCCESS)
|
|
504
|
+
return stat.errno;
|
|
505
|
+
const view = this._view();
|
|
506
|
+
view.setUint8(buf_ptr, stat.filetype);
|
|
507
|
+
view.setUint8(buf_ptr + 1, 0); // padding
|
|
508
|
+
view.setUint16(buf_ptr + 2, stat.fdflags, true);
|
|
509
|
+
view.setUint32(buf_ptr + 4, 0); // padding
|
|
510
|
+
view.setBigUint64(buf_ptr + 8, stat.rightsBase, true);
|
|
511
|
+
view.setBigUint64(buf_ptr + 16, stat.rightsInheriting, true);
|
|
512
|
+
return ERRNO_SUCCESS;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Set file descriptor flags.
|
|
516
|
+
*/
|
|
517
|
+
fd_fdstat_set_flags(fd, flags) {
|
|
518
|
+
const entry = this.fdTable.get(fd);
|
|
519
|
+
if (!entry)
|
|
520
|
+
return ERRNO_EBADF;
|
|
521
|
+
if (!(entry.rightsBase & RIGHT_FD_FDSTAT_SET_FLAGS))
|
|
522
|
+
return ERRNO_EBADF;
|
|
523
|
+
entry.fdflags = flags;
|
|
524
|
+
return ERRNO_SUCCESS;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get pre-opened directory info.
|
|
528
|
+
* Writes prestat struct (8 bytes) at buf_ptr:
|
|
529
|
+
* offset 0: pr_type (u8) = 0 for dir
|
|
530
|
+
* offset 4: u.dir.pr_name_len (u32 LE)
|
|
531
|
+
*/
|
|
532
|
+
fd_prestat_get(fd, buf_ptr) {
|
|
533
|
+
const path = this._preopens.get(fd);
|
|
534
|
+
if (path === undefined)
|
|
535
|
+
return ERRNO_EBADF;
|
|
536
|
+
const encoded = new TextEncoder().encode(path);
|
|
537
|
+
const view = this._view();
|
|
538
|
+
view.setUint8(buf_ptr, PREOPENTYPE_DIR);
|
|
539
|
+
view.setUint8(buf_ptr + 1, 0);
|
|
540
|
+
view.setUint16(buf_ptr + 2, 0);
|
|
541
|
+
view.setUint32(buf_ptr + 4, encoded.length, true);
|
|
542
|
+
return ERRNO_SUCCESS;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get the name of a pre-opened directory.
|
|
546
|
+
*/
|
|
547
|
+
fd_prestat_dir_name(fd, path_ptr, path_len) {
|
|
548
|
+
const path = this._preopens.get(fd);
|
|
549
|
+
if (path === undefined)
|
|
550
|
+
return ERRNO_EBADF;
|
|
551
|
+
const encoded = new TextEncoder().encode(path);
|
|
552
|
+
const len = Math.min(encoded.length, path_len);
|
|
553
|
+
this._bytes().set(encoded.subarray(0, len), path_ptr);
|
|
554
|
+
return ERRNO_SUCCESS;
|
|
555
|
+
}
|
|
556
|
+
// --- Helper methods for path/filestat operations (US-008) ---
|
|
557
|
+
/**
|
|
558
|
+
* Read a path string from WASM memory.
|
|
559
|
+
*/
|
|
560
|
+
_readPathString(pathPtr, pathLen) {
|
|
561
|
+
return new TextDecoder().decode(new Uint8Array(this.memory.buffer, pathPtr, pathLen));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Resolve a WASI path relative to a directory fd.
|
|
565
|
+
*/
|
|
566
|
+
_resolveWasiPath(dirfd, pathPtr, pathLen) {
|
|
567
|
+
const pathStr = this._readPathString(pathPtr, pathLen);
|
|
568
|
+
let basePath = this._preopens.get(dirfd);
|
|
569
|
+
if (basePath === undefined) {
|
|
570
|
+
const entry = this.fdTable.get(dirfd);
|
|
571
|
+
if (!entry)
|
|
572
|
+
return null;
|
|
573
|
+
basePath = entry.path || '/';
|
|
574
|
+
}
|
|
575
|
+
let fullPath;
|
|
576
|
+
if (pathStr.startsWith('/')) {
|
|
577
|
+
fullPath = pathStr;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
fullPath = basePath === '/' ? '/' + pathStr : basePath + '/' + pathStr;
|
|
581
|
+
}
|
|
582
|
+
// Normalize . and .. components (WASI paths may contain them)
|
|
583
|
+
return normalizePath(fullPath);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Convert VFS inode type to WASI filetype.
|
|
587
|
+
*/
|
|
588
|
+
_inodeTypeToFiletype(type) {
|
|
589
|
+
switch (type) {
|
|
590
|
+
case 'file': return FILETYPE_REGULAR_FILE;
|
|
591
|
+
case 'dir': return FILETYPE_DIRECTORY;
|
|
592
|
+
case 'symlink': return FILETYPE_SYMBOLIC_LINK;
|
|
593
|
+
case 'dev': return FILETYPE_CHARACTER_DEVICE;
|
|
594
|
+
default: return FILETYPE_UNKNOWN;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Write a WASI filestat struct (64 bytes) at the given pointer.
|
|
599
|
+
*/
|
|
600
|
+
_writeFilestat(ptr, ino, node) {
|
|
601
|
+
const view = this._view();
|
|
602
|
+
view.setBigUint64(ptr, 0n, true); // dev
|
|
603
|
+
view.setBigUint64(ptr + 8, BigInt(ino), true); // ino
|
|
604
|
+
view.setUint8(ptr + 16, this._inodeTypeToFiletype(node.type)); // filetype
|
|
605
|
+
for (let i = 17; i < 24; i++)
|
|
606
|
+
view.setUint8(ptr + i, 0); // padding
|
|
607
|
+
view.setBigUint64(ptr + 24, BigInt(node.nlink), true); // nlink
|
|
608
|
+
view.setBigUint64(ptr + 32, BigInt(node.size), true); // size
|
|
609
|
+
view.setBigUint64(ptr + 40, BigInt(node.atime) * 1000000n, true); // atim (ms->ns)
|
|
610
|
+
view.setBigUint64(ptr + 48, BigInt(node.mtime) * 1000000n, true); // mtim (ms->ns)
|
|
611
|
+
view.setBigUint64(ptr + 56, BigInt(node.ctime) * 1000000n, true); // ctim (ms->ns)
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Apply timestamp changes to a VFS inode based on fstflags.
|
|
615
|
+
*/
|
|
616
|
+
_applyTimestamps(node, atim, mtim, fst_flags) {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
if (fst_flags & FSTFLAG_ATIM_NOW) {
|
|
619
|
+
node.atime = now;
|
|
620
|
+
}
|
|
621
|
+
else if (fst_flags & FSTFLAG_ATIM) {
|
|
622
|
+
const atimBig = typeof atim === 'bigint' ? atim : BigInt(atim);
|
|
623
|
+
node.atime = Number(atimBig / 1000000n);
|
|
624
|
+
}
|
|
625
|
+
if (fst_flags & FSTFLAG_MTIM_NOW) {
|
|
626
|
+
node.mtime = now;
|
|
627
|
+
}
|
|
628
|
+
else if (fst_flags & FSTFLAG_MTIM) {
|
|
629
|
+
const mtimBig = typeof mtim === 'bigint' ? mtim : BigInt(mtim);
|
|
630
|
+
node.mtime = Number(mtimBig / 1000000n);
|
|
631
|
+
}
|
|
632
|
+
node.ctime = now;
|
|
633
|
+
}
|
|
634
|
+
// --- Path operations (US-008) ---
|
|
635
|
+
/**
|
|
636
|
+
* Open a file or directory at a path relative to a directory fd.
|
|
637
|
+
*/
|
|
638
|
+
path_open(dirfd, dirflags, path_ptr, path_len, oflags, fs_rights_base, fs_rights_inheriting, fdflags, opened_fd_ptr) {
|
|
639
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
640
|
+
if (!dirEntry)
|
|
641
|
+
return ERRNO_EBADF;
|
|
642
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_OPEN))
|
|
643
|
+
return ERRNO_EBADF;
|
|
644
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
645
|
+
if (!fullPath)
|
|
646
|
+
return ERRNO_EBADF;
|
|
647
|
+
// Intersect requested rights with directory's inheriting rights
|
|
648
|
+
const rightsBase = (typeof fs_rights_base === 'bigint' ? fs_rights_base : BigInt(fs_rights_base)) & dirEntry.rightsInheriting;
|
|
649
|
+
const rightsInheriting = (typeof fs_rights_inheriting === 'bigint' ? fs_rights_inheriting : BigInt(fs_rights_inheriting)) & dirEntry.rightsInheriting;
|
|
650
|
+
// Delegate to kernel file I/O bridge
|
|
651
|
+
const result = this._fileIO.fdOpen(fullPath, dirflags, oflags, fdflags, rightsBase, rightsInheriting);
|
|
652
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
653
|
+
return result.errno;
|
|
654
|
+
this._view().setUint32(opened_fd_ptr, result.fd, true);
|
|
655
|
+
return ERRNO_SUCCESS;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Create a directory at a path relative to a directory fd.
|
|
659
|
+
*/
|
|
660
|
+
path_create_directory(dirfd, path_ptr, path_len) {
|
|
661
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
662
|
+
if (!dirEntry)
|
|
663
|
+
return ERRNO_EBADF;
|
|
664
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_CREATE_DIRECTORY))
|
|
665
|
+
return ERRNO_EBADF;
|
|
666
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
667
|
+
if (!fullPath)
|
|
668
|
+
return ERRNO_EBADF;
|
|
669
|
+
try {
|
|
670
|
+
this.vfs.mkdir(fullPath);
|
|
671
|
+
}
|
|
672
|
+
catch (e) {
|
|
673
|
+
return vfsErrorToErrno(e);
|
|
674
|
+
}
|
|
675
|
+
return ERRNO_SUCCESS;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Unlink a file at a path relative to a directory fd.
|
|
679
|
+
*/
|
|
680
|
+
path_unlink_file(dirfd, path_ptr, path_len) {
|
|
681
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
682
|
+
if (!dirEntry)
|
|
683
|
+
return ERRNO_EBADF;
|
|
684
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_UNLINK_FILE))
|
|
685
|
+
return ERRNO_EBADF;
|
|
686
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
687
|
+
if (!fullPath)
|
|
688
|
+
return ERRNO_EBADF;
|
|
689
|
+
try {
|
|
690
|
+
this.vfs.unlink(fullPath);
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
return vfsErrorToErrno(e);
|
|
694
|
+
}
|
|
695
|
+
return ERRNO_SUCCESS;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Remove a directory at a path relative to a directory fd.
|
|
699
|
+
*/
|
|
700
|
+
path_remove_directory(dirfd, path_ptr, path_len) {
|
|
701
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
702
|
+
if (!dirEntry)
|
|
703
|
+
return ERRNO_EBADF;
|
|
704
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_REMOVE_DIRECTORY))
|
|
705
|
+
return ERRNO_EBADF;
|
|
706
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
707
|
+
if (!fullPath)
|
|
708
|
+
return ERRNO_EBADF;
|
|
709
|
+
try {
|
|
710
|
+
this.vfs.rmdir(fullPath);
|
|
711
|
+
}
|
|
712
|
+
catch (e) {
|
|
713
|
+
return vfsErrorToErrno(e);
|
|
714
|
+
}
|
|
715
|
+
return ERRNO_SUCCESS;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Rename a file or directory.
|
|
719
|
+
*/
|
|
720
|
+
path_rename(old_dirfd, old_path_ptr, old_path_len, new_dirfd, new_path_ptr, new_path_len) {
|
|
721
|
+
const oldDirEntry = this.fdTable.get(old_dirfd);
|
|
722
|
+
if (!oldDirEntry)
|
|
723
|
+
return ERRNO_EBADF;
|
|
724
|
+
if (!(oldDirEntry.rightsBase & RIGHT_PATH_RENAME_SOURCE))
|
|
725
|
+
return ERRNO_EBADF;
|
|
726
|
+
const newDirEntry = this.fdTable.get(new_dirfd);
|
|
727
|
+
if (!newDirEntry)
|
|
728
|
+
return ERRNO_EBADF;
|
|
729
|
+
if (!(newDirEntry.rightsBase & RIGHT_PATH_RENAME_TARGET))
|
|
730
|
+
return ERRNO_EBADF;
|
|
731
|
+
const oldPath = this._resolveWasiPath(old_dirfd, old_path_ptr, old_path_len);
|
|
732
|
+
const newPath = this._resolveWasiPath(new_dirfd, new_path_ptr, new_path_len);
|
|
733
|
+
if (!oldPath || !newPath)
|
|
734
|
+
return ERRNO_EBADF;
|
|
735
|
+
try {
|
|
736
|
+
this.vfs.rename(oldPath, newPath);
|
|
737
|
+
}
|
|
738
|
+
catch (e) {
|
|
739
|
+
return vfsErrorToErrno(e);
|
|
740
|
+
}
|
|
741
|
+
return ERRNO_SUCCESS;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Create a symbolic link.
|
|
745
|
+
*/
|
|
746
|
+
path_symlink(old_path_ptr, old_path_len, dirfd, new_path_ptr, new_path_len) {
|
|
747
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
748
|
+
if (!dirEntry)
|
|
749
|
+
return ERRNO_EBADF;
|
|
750
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_SYMLINK))
|
|
751
|
+
return ERRNO_EBADF;
|
|
752
|
+
const target = this._readPathString(old_path_ptr, old_path_len);
|
|
753
|
+
const linkPath = this._resolveWasiPath(dirfd, new_path_ptr, new_path_len);
|
|
754
|
+
if (!linkPath)
|
|
755
|
+
return ERRNO_EBADF;
|
|
756
|
+
try {
|
|
757
|
+
this.vfs.symlink(target, linkPath);
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
return vfsErrorToErrno(e);
|
|
761
|
+
}
|
|
762
|
+
return ERRNO_SUCCESS;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Read the target of a symbolic link.
|
|
766
|
+
*/
|
|
767
|
+
path_readlink(dirfd, path_ptr, path_len, buf_ptr, buf_len, bufused_ptr) {
|
|
768
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
769
|
+
if (!dirEntry)
|
|
770
|
+
return ERRNO_EBADF;
|
|
771
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_READLINK))
|
|
772
|
+
return ERRNO_EBADF;
|
|
773
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
774
|
+
if (!fullPath)
|
|
775
|
+
return ERRNO_EBADF;
|
|
776
|
+
let target;
|
|
777
|
+
try {
|
|
778
|
+
target = this.vfs.readlink(fullPath);
|
|
779
|
+
}
|
|
780
|
+
catch (e) {
|
|
781
|
+
return vfsErrorToErrno(e);
|
|
782
|
+
}
|
|
783
|
+
const encoded = new TextEncoder().encode(target);
|
|
784
|
+
const writeLen = Math.min(encoded.length, buf_len);
|
|
785
|
+
this._bytes().set(encoded.subarray(0, writeLen), buf_ptr);
|
|
786
|
+
this._view().setUint32(bufused_ptr, writeLen, true);
|
|
787
|
+
return ERRNO_SUCCESS;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Get file status by path.
|
|
791
|
+
*/
|
|
792
|
+
path_filestat_get(dirfd, flags, path_ptr, path_len, buf_ptr) {
|
|
793
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
794
|
+
if (!dirEntry)
|
|
795
|
+
return ERRNO_EBADF;
|
|
796
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_FILESTAT_GET))
|
|
797
|
+
return ERRNO_EBADF;
|
|
798
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
799
|
+
if (!fullPath)
|
|
800
|
+
return ERRNO_EBADF;
|
|
801
|
+
const followSymlinks = !!(flags & LOOKUP_SYMLINK_FOLLOW);
|
|
802
|
+
const ino = this.vfs.getIno(fullPath, followSymlinks);
|
|
803
|
+
if (ino === null)
|
|
804
|
+
return ERRNO_ENOENT;
|
|
805
|
+
const node = this.vfs.getInodeByIno(ino);
|
|
806
|
+
if (!node)
|
|
807
|
+
return ERRNO_ENOENT;
|
|
808
|
+
this._writeFilestat(buf_ptr, ino, node);
|
|
809
|
+
return ERRNO_SUCCESS;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Set file timestamps by path.
|
|
813
|
+
*/
|
|
814
|
+
path_filestat_set_times(dirfd, flags, path_ptr, path_len, atim, mtim, fst_flags) {
|
|
815
|
+
const dirEntry = this.fdTable.get(dirfd);
|
|
816
|
+
if (!dirEntry)
|
|
817
|
+
return ERRNO_EBADF;
|
|
818
|
+
if (!(dirEntry.rightsBase & RIGHT_PATH_FILESTAT_SET_TIMES))
|
|
819
|
+
return ERRNO_EBADF;
|
|
820
|
+
const fullPath = this._resolveWasiPath(dirfd, path_ptr, path_len);
|
|
821
|
+
if (!fullPath)
|
|
822
|
+
return ERRNO_EBADF;
|
|
823
|
+
const followSymlinks = !!(flags & LOOKUP_SYMLINK_FOLLOW);
|
|
824
|
+
const ino = this.vfs.getIno(fullPath, followSymlinks);
|
|
825
|
+
if (ino === null)
|
|
826
|
+
return ERRNO_ENOENT;
|
|
827
|
+
const node = this.vfs.getInodeByIno(ino);
|
|
828
|
+
if (!node)
|
|
829
|
+
return ERRNO_ENOENT;
|
|
830
|
+
this._applyTimestamps(node, atim, mtim, fst_flags);
|
|
831
|
+
return ERRNO_SUCCESS;
|
|
832
|
+
}
|
|
833
|
+
// --- FD filestat operations (US-008) ---
|
|
834
|
+
/**
|
|
835
|
+
* Get file status by fd.
|
|
836
|
+
*/
|
|
837
|
+
fd_filestat_get(fd, buf_ptr) {
|
|
838
|
+
const entry = this.fdTable.get(fd);
|
|
839
|
+
if (!entry)
|
|
840
|
+
return ERRNO_EBADF;
|
|
841
|
+
if (!(entry.rightsBase & RIGHT_FD_FILESTAT_GET))
|
|
842
|
+
return ERRNO_EBADF;
|
|
843
|
+
const resource = entry.resource;
|
|
844
|
+
if (resource.type === 'vfsFile' || resource.type === 'preopen') {
|
|
845
|
+
// Kernel-opened vfsFile resources have ino=0 (sentinel) — resolve by path
|
|
846
|
+
const ino = (resource.type === 'vfsFile' && resource.ino !== 0)
|
|
847
|
+
? resource.ino
|
|
848
|
+
: this.vfs.getIno(resource.path, true);
|
|
849
|
+
if (ino === null)
|
|
850
|
+
return ERRNO_EBADF;
|
|
851
|
+
const node = this.vfs.getInodeByIno(ino);
|
|
852
|
+
if (!node)
|
|
853
|
+
return ERRNO_EBADF;
|
|
854
|
+
this._writeFilestat(buf_ptr, ino, node);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// stdio, pipe, etc. -- return minimal stat
|
|
858
|
+
const view = this._view();
|
|
859
|
+
view.setBigUint64(buf_ptr, 0n, true); // dev
|
|
860
|
+
view.setBigUint64(buf_ptr + 8, 0n, true); // ino
|
|
861
|
+
view.setUint8(buf_ptr + 16, entry.filetype); // filetype
|
|
862
|
+
for (let i = 17; i < 24; i++)
|
|
863
|
+
view.setUint8(buf_ptr + i, 0);
|
|
864
|
+
view.setBigUint64(buf_ptr + 24, 1n, true); // nlink
|
|
865
|
+
view.setBigUint64(buf_ptr + 32, 0n, true); // size
|
|
866
|
+
view.setBigUint64(buf_ptr + 40, 0n, true); // atim
|
|
867
|
+
view.setBigUint64(buf_ptr + 48, 0n, true); // mtim
|
|
868
|
+
view.setBigUint64(buf_ptr + 56, 0n, true); // ctim
|
|
869
|
+
}
|
|
870
|
+
return ERRNO_SUCCESS;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Set file size by fd (truncate or extend).
|
|
874
|
+
*/
|
|
875
|
+
fd_filestat_set_size(fd, size) {
|
|
876
|
+
const entry = this.fdTable.get(fd);
|
|
877
|
+
if (!entry)
|
|
878
|
+
return ERRNO_EBADF;
|
|
879
|
+
if (!(entry.rightsBase & RIGHT_FD_FILESTAT_SET_SIZE))
|
|
880
|
+
return ERRNO_EBADF;
|
|
881
|
+
if (entry.resource.type !== 'vfsFile')
|
|
882
|
+
return ERRNO_EINVAL;
|
|
883
|
+
const node = this.vfs.getInodeByIno(entry.resource.ino);
|
|
884
|
+
if (!node || node.type !== 'file')
|
|
885
|
+
return ERRNO_EINVAL;
|
|
886
|
+
const newSize = Number(typeof size === 'bigint' ? size : BigInt(size));
|
|
887
|
+
if (newSize === node.data.length)
|
|
888
|
+
return ERRNO_SUCCESS;
|
|
889
|
+
const newData = new Uint8Array(newSize);
|
|
890
|
+
newData.set(node.data.subarray(0, Math.min(node.data.length, newSize)));
|
|
891
|
+
node.data = newData;
|
|
892
|
+
node.mtime = Date.now();
|
|
893
|
+
node.ctime = Date.now();
|
|
894
|
+
return ERRNO_SUCCESS;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Set file timestamps by fd.
|
|
898
|
+
*/
|
|
899
|
+
fd_filestat_set_times(fd, atim, mtim, fst_flags) {
|
|
900
|
+
const entry = this.fdTable.get(fd);
|
|
901
|
+
if (!entry)
|
|
902
|
+
return ERRNO_EBADF;
|
|
903
|
+
if (!(entry.rightsBase & RIGHT_FD_FILESTAT_SET_TIMES))
|
|
904
|
+
return ERRNO_EBADF;
|
|
905
|
+
if (entry.resource.type === 'vfsFile') {
|
|
906
|
+
const node = this.vfs.getInodeByIno(entry.resource.ino);
|
|
907
|
+
if (!node)
|
|
908
|
+
return ERRNO_EBADF;
|
|
909
|
+
this._applyTimestamps(node, atim, mtim, fst_flags);
|
|
910
|
+
}
|
|
911
|
+
else if (entry.resource.type === 'preopen') {
|
|
912
|
+
const ino = this.vfs.getIno(entry.resource.path, true);
|
|
913
|
+
if (ino === null)
|
|
914
|
+
return ERRNO_EBADF;
|
|
915
|
+
const node = this.vfs.getInodeByIno(ino);
|
|
916
|
+
if (!node)
|
|
917
|
+
return ERRNO_EBADF;
|
|
918
|
+
this._applyTimestamps(node, atim, mtim, fst_flags);
|
|
919
|
+
}
|
|
920
|
+
// For stdio/pipes, silently succeed
|
|
921
|
+
return ERRNO_SUCCESS;
|
|
922
|
+
}
|
|
923
|
+
// --- Directory operations (US-008) ---
|
|
924
|
+
/**
|
|
925
|
+
* Read directory entries from a directory fd.
|
|
926
|
+
* Writes dirent structs (24-byte header + name) into the buffer.
|
|
927
|
+
*/
|
|
928
|
+
fd_readdir(fd, buf_ptr, buf_len, cookie, bufused_ptr) {
|
|
929
|
+
const entry = this.fdTable.get(fd);
|
|
930
|
+
if (!entry)
|
|
931
|
+
return ERRNO_EBADF;
|
|
932
|
+
if (!(entry.rightsBase & RIGHT_FD_READDIR))
|
|
933
|
+
return ERRNO_EBADF;
|
|
934
|
+
let ino;
|
|
935
|
+
if (entry.resource.type === 'preopen') {
|
|
936
|
+
ino = this.vfs.getIno(entry.resource.path, true);
|
|
937
|
+
}
|
|
938
|
+
else if (entry.resource.type === 'vfsFile') {
|
|
939
|
+
ino = entry.resource.ino;
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
return ERRNO_EBADF;
|
|
943
|
+
}
|
|
944
|
+
const node = this.vfs.getInodeByIno(ino);
|
|
945
|
+
if (!node || node.type !== 'dir')
|
|
946
|
+
return ERRNO_ENOTDIR;
|
|
947
|
+
const entries = Array.from(node.entries.entries());
|
|
948
|
+
const cookieNum = Number(typeof cookie === 'bigint' ? cookie : BigInt(cookie));
|
|
949
|
+
const mem = this._bytes();
|
|
950
|
+
const view = this._view();
|
|
951
|
+
let offset = 0;
|
|
952
|
+
for (let i = cookieNum; i < entries.length; i++) {
|
|
953
|
+
const [name, childIno] = entries[i];
|
|
954
|
+
const childNode = this.vfs.getInodeByIno(childIno);
|
|
955
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
956
|
+
const headerSize = 24;
|
|
957
|
+
const entrySize = headerSize + nameBytes.length;
|
|
958
|
+
if (offset + entrySize > buf_len)
|
|
959
|
+
break;
|
|
960
|
+
// Write dirent header
|
|
961
|
+
view.setBigUint64(buf_ptr + offset, BigInt(i + 1), true); // d_next
|
|
962
|
+
view.setBigUint64(buf_ptr + offset + 8, BigInt(childIno), true); // d_ino
|
|
963
|
+
view.setUint32(buf_ptr + offset + 16, nameBytes.length, true); // d_namlen
|
|
964
|
+
view.setUint8(buf_ptr + offset + 20, childNode ? this._inodeTypeToFiletype(childNode.type) : FILETYPE_UNKNOWN); // d_type
|
|
965
|
+
view.setUint8(buf_ptr + offset + 21, 0); // padding
|
|
966
|
+
view.setUint8(buf_ptr + offset + 22, 0);
|
|
967
|
+
view.setUint8(buf_ptr + offset + 23, 0);
|
|
968
|
+
offset += headerSize;
|
|
969
|
+
// Write name (guaranteed to fit — checked entrySize above)
|
|
970
|
+
mem.set(nameBytes, buf_ptr + offset);
|
|
971
|
+
offset += nameBytes.length;
|
|
972
|
+
}
|
|
973
|
+
view.setUint32(bufused_ptr, offset, true);
|
|
974
|
+
return ERRNO_SUCCESS;
|
|
975
|
+
}
|
|
976
|
+
// --- Args and environ operations (US-009) ---
|
|
977
|
+
/**
|
|
978
|
+
* Write command-line arguments into WASM memory.
|
|
979
|
+
* argv_ptr: pointer to array of u32 pointers (one per arg)
|
|
980
|
+
* argv_buf_ptr: pointer to buffer where arg strings are written (null-terminated)
|
|
981
|
+
*/
|
|
982
|
+
args_get(argv_ptr, argv_buf_ptr) {
|
|
983
|
+
const args = this._processIO.getArgs();
|
|
984
|
+
const view = this._view();
|
|
985
|
+
const mem = this._bytes();
|
|
986
|
+
const encoder = new TextEncoder();
|
|
987
|
+
let bufOffset = argv_buf_ptr;
|
|
988
|
+
for (let i = 0; i < args.length; i++) {
|
|
989
|
+
view.setUint32(argv_ptr + i * 4, bufOffset, true);
|
|
990
|
+
const encoded = encoder.encode(args[i]);
|
|
991
|
+
mem.set(encoded, bufOffset);
|
|
992
|
+
mem[bufOffset + encoded.length] = 0; // null terminator
|
|
993
|
+
bufOffset += encoded.length + 1;
|
|
994
|
+
}
|
|
995
|
+
return ERRNO_SUCCESS;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Get the sizes needed for args_get.
|
|
999
|
+
* Writes argc (u32) at argc_ptr and total argv buffer size (u32) at argv_buf_size_ptr.
|
|
1000
|
+
*/
|
|
1001
|
+
args_sizes_get(argc_ptr, argv_buf_size_ptr) {
|
|
1002
|
+
const args = this._processIO.getArgs();
|
|
1003
|
+
const view = this._view();
|
|
1004
|
+
const encoder = new TextEncoder();
|
|
1005
|
+
let bufSize = 0;
|
|
1006
|
+
for (const arg of args) {
|
|
1007
|
+
bufSize += encoder.encode(arg).length + 1; // +1 for null terminator
|
|
1008
|
+
}
|
|
1009
|
+
view.setUint32(argc_ptr, args.length, true);
|
|
1010
|
+
view.setUint32(argv_buf_size_ptr, bufSize, true);
|
|
1011
|
+
return ERRNO_SUCCESS;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Write environment variables into WASM memory.
|
|
1015
|
+
* environ_ptr: pointer to array of u32 pointers (one per env entry)
|
|
1016
|
+
* environ_buf_ptr: pointer to buffer where "KEY=VALUE\0" strings are written
|
|
1017
|
+
*/
|
|
1018
|
+
environ_get(environ_ptr, environ_buf_ptr) {
|
|
1019
|
+
const env = this._processIO.getEnviron();
|
|
1020
|
+
const view = this._view();
|
|
1021
|
+
const mem = this._bytes();
|
|
1022
|
+
const encoder = new TextEncoder();
|
|
1023
|
+
const entries = Object.entries(env);
|
|
1024
|
+
let bufOffset = environ_buf_ptr;
|
|
1025
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1026
|
+
view.setUint32(environ_ptr + i * 4, bufOffset, true);
|
|
1027
|
+
const str = `${entries[i][0]}=${entries[i][1]}`;
|
|
1028
|
+
const encoded = encoder.encode(str);
|
|
1029
|
+
mem.set(encoded, bufOffset);
|
|
1030
|
+
mem[bufOffset + encoded.length] = 0; // null terminator
|
|
1031
|
+
bufOffset += encoded.length + 1;
|
|
1032
|
+
}
|
|
1033
|
+
return ERRNO_SUCCESS;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Get the sizes needed for environ_get.
|
|
1037
|
+
* Writes environ count (u32) at environc_ptr and total buffer size (u32) at environ_buf_size_ptr.
|
|
1038
|
+
*/
|
|
1039
|
+
environ_sizes_get(environc_ptr, environ_buf_size_ptr) {
|
|
1040
|
+
const env = this._processIO.getEnviron();
|
|
1041
|
+
const view = this._view();
|
|
1042
|
+
const encoder = new TextEncoder();
|
|
1043
|
+
const entries = Object.entries(env);
|
|
1044
|
+
let bufSize = 0;
|
|
1045
|
+
for (const [key, value] of entries) {
|
|
1046
|
+
bufSize += encoder.encode(`${key}=${value}`).length + 1;
|
|
1047
|
+
}
|
|
1048
|
+
view.setUint32(environc_ptr, entries.length, true);
|
|
1049
|
+
view.setUint32(environ_buf_size_ptr, bufSize, true);
|
|
1050
|
+
return ERRNO_SUCCESS;
|
|
1051
|
+
}
|
|
1052
|
+
// --- Clock, random, and process operations (US-009) ---
|
|
1053
|
+
/**
|
|
1054
|
+
* Get the resolution of a clock.
|
|
1055
|
+
* Writes resolution in nanoseconds as u64 at resolution_ptr.
|
|
1056
|
+
*/
|
|
1057
|
+
clock_res_get(id, resolution_ptr) {
|
|
1058
|
+
const view = this._view();
|
|
1059
|
+
switch (id) {
|
|
1060
|
+
case CLOCKID_REALTIME:
|
|
1061
|
+
// Date.now() has ~1ms resolution
|
|
1062
|
+
view.setBigUint64(resolution_ptr, 1000000n, true);
|
|
1063
|
+
return ERRNO_SUCCESS;
|
|
1064
|
+
case CLOCKID_MONOTONIC:
|
|
1065
|
+
// performance.now() has ~1us resolution (in practice, may be less)
|
|
1066
|
+
view.setBigUint64(resolution_ptr, 1000n, true);
|
|
1067
|
+
return ERRNO_SUCCESS;
|
|
1068
|
+
case CLOCKID_PROCESS_CPUTIME_ID:
|
|
1069
|
+
case CLOCKID_THREAD_CPUTIME_ID:
|
|
1070
|
+
// Approximate -- no real CPU time tracking
|
|
1071
|
+
view.setBigUint64(resolution_ptr, 1000000n, true);
|
|
1072
|
+
return ERRNO_SUCCESS;
|
|
1073
|
+
default:
|
|
1074
|
+
return ERRNO_EINVAL;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Get the current time of a clock.
|
|
1079
|
+
* Writes time in nanoseconds as u64 at time_ptr.
|
|
1080
|
+
*/
|
|
1081
|
+
clock_time_get(id, _precision, time_ptr) {
|
|
1082
|
+
const view = this._view();
|
|
1083
|
+
switch (id) {
|
|
1084
|
+
case CLOCKID_REALTIME: {
|
|
1085
|
+
const ms = Date.now();
|
|
1086
|
+
view.setBigUint64(time_ptr, BigInt(ms) * 1000000n, true);
|
|
1087
|
+
return ERRNO_SUCCESS;
|
|
1088
|
+
}
|
|
1089
|
+
case CLOCKID_MONOTONIC: {
|
|
1090
|
+
// Use performance.now() if available, fall back to Date.now()
|
|
1091
|
+
const nowMs = (typeof performance !== 'undefined' && performance.now)
|
|
1092
|
+
? performance.now()
|
|
1093
|
+
: Date.now();
|
|
1094
|
+
// Convert ms (float) to nanoseconds
|
|
1095
|
+
const ns = BigInt(Math.round(nowMs * 1_000_000));
|
|
1096
|
+
view.setBigUint64(time_ptr, ns, true);
|
|
1097
|
+
return ERRNO_SUCCESS;
|
|
1098
|
+
}
|
|
1099
|
+
case CLOCKID_PROCESS_CPUTIME_ID:
|
|
1100
|
+
case CLOCKID_THREAD_CPUTIME_ID: {
|
|
1101
|
+
// Approximate with monotonic clock
|
|
1102
|
+
const nowMs = (typeof performance !== 'undefined' && performance.now)
|
|
1103
|
+
? performance.now()
|
|
1104
|
+
: Date.now();
|
|
1105
|
+
const ns = BigInt(Math.round(nowMs * 1_000_000));
|
|
1106
|
+
view.setBigUint64(time_ptr, ns, true);
|
|
1107
|
+
return ERRNO_SUCCESS;
|
|
1108
|
+
}
|
|
1109
|
+
default:
|
|
1110
|
+
return ERRNO_EINVAL;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Fill a buffer with cryptographically secure random bytes.
|
|
1115
|
+
*/
|
|
1116
|
+
random_get(buf_ptr, buf_len) {
|
|
1117
|
+
const mem = this._bytes();
|
|
1118
|
+
const target = mem.subarray(buf_ptr, buf_ptr + buf_len);
|
|
1119
|
+
// Use crypto.getRandomValues -- works in both browser and Node.js
|
|
1120
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
1121
|
+
// getRandomValues has a 65536-byte limit per call
|
|
1122
|
+
for (let offset = 0; offset < buf_len; offset += 65536) {
|
|
1123
|
+
const len = Math.min(65536, buf_len - offset);
|
|
1124
|
+
crypto.getRandomValues(target.subarray(offset, offset + len));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// Fallback: Math.random (not cryptographically secure, but functional)
|
|
1129
|
+
for (let i = 0; i < buf_len; i++) {
|
|
1130
|
+
target[i] = Math.floor(Math.random() * 256);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return ERRNO_SUCCESS;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Terminate the process with an exit code.
|
|
1137
|
+
* Throws WasiProcExit to unwind the WASM call stack.
|
|
1138
|
+
*/
|
|
1139
|
+
proc_exit(exitCode) {
|
|
1140
|
+
this.exitCode = exitCode;
|
|
1141
|
+
this._processIO.procExit(exitCode);
|
|
1142
|
+
throw new WasiProcExit(exitCode);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Send a signal to the current process.
|
|
1146
|
+
* Not meaningful in WASM -- stub that returns ENOSYS.
|
|
1147
|
+
*/
|
|
1148
|
+
proc_raise(_sig) {
|
|
1149
|
+
return ERRNO_ENOSYS;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Yield the current thread's execution.
|
|
1153
|
+
* No-op in single-threaded WASM.
|
|
1154
|
+
*/
|
|
1155
|
+
sched_yield() {
|
|
1156
|
+
return ERRNO_SUCCESS;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Minimal poll_oneoff supporting clock subscriptions (for sleep).
|
|
1160
|
+
*
|
|
1161
|
+
* Subscription layout (48 bytes):
|
|
1162
|
+
* u64 userdata @ 0
|
|
1163
|
+
* u8 type @ 8 (0=clock, 1=fd_read, 2=fd_write)
|
|
1164
|
+
* -- padding to offset 16 --
|
|
1165
|
+
* For clock (type==0):
|
|
1166
|
+
* u32 clock_id @ 16
|
|
1167
|
+
* u64 timeout @ 24
|
|
1168
|
+
* u64 precision @ 32
|
|
1169
|
+
* u16 flags @ 40 (bit 0 = abstime)
|
|
1170
|
+
*
|
|
1171
|
+
* Event layout (32 bytes):
|
|
1172
|
+
* u64 userdata @ 0
|
|
1173
|
+
* u16 error @ 8
|
|
1174
|
+
* u8 type @ 10
|
|
1175
|
+
* -- padding --
|
|
1176
|
+
* u64 fd_readwrite.nbytes @ 16
|
|
1177
|
+
* u16 fd_readwrite.flags @ 24
|
|
1178
|
+
*/
|
|
1179
|
+
poll_oneoff(in_ptr, out_ptr, nsubscriptions, nevents_ptr) {
|
|
1180
|
+
const view = this._view();
|
|
1181
|
+
const nsubs = nsubscriptions;
|
|
1182
|
+
let nevents = 0;
|
|
1183
|
+
for (let i = 0; i < nsubs; i++) {
|
|
1184
|
+
const subBase = in_ptr + i * 48;
|
|
1185
|
+
const userdata = view.getBigUint64(subBase, true);
|
|
1186
|
+
const eventType = view.getUint8(subBase + 8);
|
|
1187
|
+
const evtBase = out_ptr + nevents * 32;
|
|
1188
|
+
view.setBigUint64(evtBase, userdata, true); // userdata
|
|
1189
|
+
view.setUint16(evtBase + 8, 0, true); // error = success
|
|
1190
|
+
view.setUint8(evtBase + 10, eventType); // type
|
|
1191
|
+
if (eventType === EVENTTYPE_CLOCK) {
|
|
1192
|
+
// Block for the requested duration (nanosleep/sleep via poll_oneoff)
|
|
1193
|
+
const timeoutNs = view.getBigUint64(subBase + 24, true);
|
|
1194
|
+
const flags = view.getUint16(subBase + 40, true);
|
|
1195
|
+
const isAbstime = (flags & 1) !== 0;
|
|
1196
|
+
let sleepMs;
|
|
1197
|
+
if (isAbstime) {
|
|
1198
|
+
// Absolute time: sleep until the specified wallclock time
|
|
1199
|
+
const targetMs = Number(timeoutNs / 1000000n);
|
|
1200
|
+
sleepMs = Math.max(0, targetMs - Date.now());
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
// Relative time: sleep for the specified duration
|
|
1204
|
+
sleepMs = Number(timeoutNs / 1000000n);
|
|
1205
|
+
}
|
|
1206
|
+
if (sleepMs > 0) {
|
|
1207
|
+
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
1208
|
+
let remainingMs = sleepMs;
|
|
1209
|
+
while (remainingMs > 0) {
|
|
1210
|
+
const sliceMs = Math.min(remainingMs, 10);
|
|
1211
|
+
Atomics.wait(buf, 0, 0, sliceMs);
|
|
1212
|
+
remainingMs -= sliceMs;
|
|
1213
|
+
this._sleepHook?.();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
else if (eventType === EVENTTYPE_FD_READ || eventType === EVENTTYPE_FD_WRITE) {
|
|
1218
|
+
// FD subscriptions -- report ready immediately
|
|
1219
|
+
view.setBigUint64(evtBase + 16, 0n, true); // nbytes
|
|
1220
|
+
view.setUint16(evtBase + 24, 0, true); // flags
|
|
1221
|
+
}
|
|
1222
|
+
nevents++;
|
|
1223
|
+
}
|
|
1224
|
+
view.setUint32(nevents_ptr, nevents, true);
|
|
1225
|
+
return ERRNO_SUCCESS;
|
|
1226
|
+
}
|
|
1227
|
+
// --- Stub/no-op fd operations (US-009) ---
|
|
1228
|
+
/**
|
|
1229
|
+
* Advise the system on intended file usage patterns.
|
|
1230
|
+
* No-op -- advisory only.
|
|
1231
|
+
*/
|
|
1232
|
+
fd_advise(fd, _offset, _len, _advice) {
|
|
1233
|
+
const entry = this.fdTable.get(fd);
|
|
1234
|
+
if (!entry)
|
|
1235
|
+
return ERRNO_EBADF;
|
|
1236
|
+
return ERRNO_SUCCESS;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Pre-allocate space for a file.
|
|
1240
|
+
* No-op in VFS (files grow dynamically).
|
|
1241
|
+
*/
|
|
1242
|
+
fd_allocate(fd, _offset, _len) {
|
|
1243
|
+
const entry = this.fdTable.get(fd);
|
|
1244
|
+
if (!entry)
|
|
1245
|
+
return ERRNO_EBADF;
|
|
1246
|
+
return ERRNO_SUCCESS;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Synchronize file data to storage.
|
|
1250
|
+
* No-op in in-memory VFS.
|
|
1251
|
+
*/
|
|
1252
|
+
fd_datasync(fd) {
|
|
1253
|
+
const entry = this.fdTable.get(fd);
|
|
1254
|
+
if (!entry)
|
|
1255
|
+
return ERRNO_EBADF;
|
|
1256
|
+
return ERRNO_SUCCESS;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Synchronize file data and metadata to storage.
|
|
1260
|
+
* No-op in in-memory VFS.
|
|
1261
|
+
*/
|
|
1262
|
+
fd_sync(fd) {
|
|
1263
|
+
const entry = this.fdTable.get(fd);
|
|
1264
|
+
if (!entry)
|
|
1265
|
+
return ERRNO_EBADF;
|
|
1266
|
+
return ERRNO_SUCCESS;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Set rights on a file descriptor (shrink only).
|
|
1270
|
+
* Minimal implementation -- just validates fd.
|
|
1271
|
+
*/
|
|
1272
|
+
fd_fdstat_set_rights(fd, fs_rights_base, fs_rights_inheriting) {
|
|
1273
|
+
const entry = this.fdTable.get(fd);
|
|
1274
|
+
if (!entry)
|
|
1275
|
+
return ERRNO_EBADF;
|
|
1276
|
+
// Rights can only be shrunk, not expanded
|
|
1277
|
+
const base = typeof fs_rights_base === 'bigint' ? fs_rights_base : BigInt(fs_rights_base);
|
|
1278
|
+
const inheriting = typeof fs_rights_inheriting === 'bigint' ? fs_rights_inheriting : BigInt(fs_rights_inheriting);
|
|
1279
|
+
entry.rightsBase = entry.rightsBase & base;
|
|
1280
|
+
entry.rightsInheriting = entry.rightsInheriting & inheriting;
|
|
1281
|
+
return ERRNO_SUCCESS;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Read from a file descriptor at a given offset without changing the cursor.
|
|
1285
|
+
* Delegates to kernel file I/O bridge.
|
|
1286
|
+
*/
|
|
1287
|
+
fd_pread(fd, iovs_ptr, iovs_len, offset, nread_ptr) {
|
|
1288
|
+
const entry = this.fdTable.get(fd);
|
|
1289
|
+
if (!entry)
|
|
1290
|
+
return ERRNO_EBADF;
|
|
1291
|
+
if (!(entry.rightsBase & RIGHT_FD_READ))
|
|
1292
|
+
return ERRNO_EBADF;
|
|
1293
|
+
if (entry.filetype !== FILETYPE_REGULAR_FILE)
|
|
1294
|
+
return ERRNO_ESPIPE;
|
|
1295
|
+
const iovecs = this._readIovecs(iovs_ptr, iovs_len);
|
|
1296
|
+
const mem = this._bytes();
|
|
1297
|
+
const offsetBig = typeof offset === 'bigint' ? offset : BigInt(offset);
|
|
1298
|
+
const totalRequested = iovecs.reduce((sum, iov) => sum + iov.buf_len, 0);
|
|
1299
|
+
const result = this._fileIO.fdPread(fd, totalRequested, offsetBig);
|
|
1300
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
1301
|
+
return result.errno;
|
|
1302
|
+
let dataOffset = 0;
|
|
1303
|
+
let totalRead = 0;
|
|
1304
|
+
for (const iov of iovecs) {
|
|
1305
|
+
const remaining = result.data.length - dataOffset;
|
|
1306
|
+
if (remaining <= 0)
|
|
1307
|
+
break;
|
|
1308
|
+
const n = Math.min(iov.buf_len, remaining);
|
|
1309
|
+
mem.set(result.data.subarray(dataOffset, dataOffset + n), iov.buf);
|
|
1310
|
+
dataOffset += n;
|
|
1311
|
+
totalRead += n;
|
|
1312
|
+
}
|
|
1313
|
+
this._view().setUint32(nread_ptr, totalRead, true);
|
|
1314
|
+
return ERRNO_SUCCESS;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Write to a file descriptor at a given offset without changing the cursor.
|
|
1318
|
+
* Delegates to kernel file I/O bridge.
|
|
1319
|
+
*/
|
|
1320
|
+
fd_pwrite(fd, iovs_ptr, iovs_len, offset, nwritten_ptr) {
|
|
1321
|
+
const entry = this.fdTable.get(fd);
|
|
1322
|
+
if (!entry)
|
|
1323
|
+
return ERRNO_EBADF;
|
|
1324
|
+
if (!(entry.rightsBase & RIGHT_FD_WRITE))
|
|
1325
|
+
return ERRNO_EBADF;
|
|
1326
|
+
if (entry.filetype !== FILETYPE_REGULAR_FILE)
|
|
1327
|
+
return ERRNO_ESPIPE;
|
|
1328
|
+
const iovecs = this._readIovecs(iovs_ptr, iovs_len);
|
|
1329
|
+
const mem = this._bytes();
|
|
1330
|
+
const offsetBig = typeof offset === 'bigint' ? offset : BigInt(offset);
|
|
1331
|
+
const chunks = [];
|
|
1332
|
+
let totalWritten = 0;
|
|
1333
|
+
for (const iov of iovecs) {
|
|
1334
|
+
if (iov.buf_len === 0)
|
|
1335
|
+
continue;
|
|
1336
|
+
chunks.push(mem.slice(iov.buf, iov.buf + iov.buf_len));
|
|
1337
|
+
totalWritten += iov.buf_len;
|
|
1338
|
+
}
|
|
1339
|
+
if (totalWritten > 0) {
|
|
1340
|
+
const writeData = concatBytes(chunks);
|
|
1341
|
+
const result = this._fileIO.fdPwrite(fd, writeData, offsetBig);
|
|
1342
|
+
if (result.errno !== ERRNO_SUCCESS)
|
|
1343
|
+
return result.errno;
|
|
1344
|
+
}
|
|
1345
|
+
this._view().setUint32(nwritten_ptr, totalWritten, true);
|
|
1346
|
+
return ERRNO_SUCCESS;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Renumber a file descriptor (atomically move oldFd to newFd).
|
|
1350
|
+
*/
|
|
1351
|
+
fd_renumber(from_fd, to_fd) {
|
|
1352
|
+
this._preopens.delete(from_fd);
|
|
1353
|
+
this._preopens.delete(to_fd);
|
|
1354
|
+
return this.fdTable.renumber(from_fd, to_fd);
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Create a hard link.
|
|
1358
|
+
* Not supported in our VFS -- return ENOSYS.
|
|
1359
|
+
*/
|
|
1360
|
+
path_link(_old_fd, _old_flags, _old_path_ptr, _old_path_len, _new_fd, _new_path_ptr, _new_path_len) {
|
|
1361
|
+
return ERRNO_ENOSYS;
|
|
1362
|
+
}
|
|
1363
|
+
// --- Socket stubs (US-009) -- all return ENOSYS ---
|
|
1364
|
+
sock_accept(_fd, _flags, _result_fd_ptr) {
|
|
1365
|
+
return ERRNO_ENOSYS;
|
|
1366
|
+
}
|
|
1367
|
+
sock_recv(_fd, _ri_data_ptr, _ri_data_len, _ri_flags, _ro_datalen_ptr, _ro_flags_ptr) {
|
|
1368
|
+
return ERRNO_ENOSYS;
|
|
1369
|
+
}
|
|
1370
|
+
sock_send(_fd, _si_data_ptr, _si_data_len, _si_flags, _so_datalen_ptr) {
|
|
1371
|
+
return ERRNO_ENOSYS;
|
|
1372
|
+
}
|
|
1373
|
+
sock_shutdown(_fd, _how) {
|
|
1374
|
+
return ERRNO_ENOSYS;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Get the wasi_snapshot_preview1 import object.
|
|
1378
|
+
* All 46 wasi_snapshot_preview1 functions.
|
|
1379
|
+
*/
|
|
1380
|
+
getImports() {
|
|
1381
|
+
return {
|
|
1382
|
+
// Core fd operations (US-007)
|
|
1383
|
+
fd_read: (fd, iovs_ptr, iovs_len, nread_ptr) => this.fd_read(fd, iovs_ptr, iovs_len, nread_ptr),
|
|
1384
|
+
fd_write: (fd, iovs_ptr, iovs_len, nwritten_ptr) => this.fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr),
|
|
1385
|
+
fd_seek: (fd, offset, whence, newoffset_ptr) => this.fd_seek(fd, offset, whence, newoffset_ptr),
|
|
1386
|
+
fd_tell: (fd, offset_ptr) => this.fd_tell(fd, offset_ptr),
|
|
1387
|
+
fd_close: (fd) => this.fd_close(fd),
|
|
1388
|
+
fd_fdstat_get: (fd, buf_ptr) => this.fd_fdstat_get(fd, buf_ptr),
|
|
1389
|
+
fd_fdstat_set_flags: (fd, flags) => this.fd_fdstat_set_flags(fd, flags),
|
|
1390
|
+
fd_prestat_get: (fd, buf_ptr) => this.fd_prestat_get(fd, buf_ptr),
|
|
1391
|
+
fd_prestat_dir_name: (fd, path_ptr, path_len) => this.fd_prestat_dir_name(fd, path_ptr, path_len),
|
|
1392
|
+
// Path operations (US-008)
|
|
1393
|
+
path_open: (dirfd, dirflags, path_ptr, path_len, oflags, fs_rights_base, fs_rights_inheriting, fdflags, opened_fd_ptr) => this.path_open(dirfd, dirflags, path_ptr, path_len, oflags, fs_rights_base, fs_rights_inheriting, fdflags, opened_fd_ptr),
|
|
1394
|
+
path_create_directory: (dirfd, path_ptr, path_len) => this.path_create_directory(dirfd, path_ptr, path_len),
|
|
1395
|
+
path_unlink_file: (dirfd, path_ptr, path_len) => this.path_unlink_file(dirfd, path_ptr, path_len),
|
|
1396
|
+
path_remove_directory: (dirfd, path_ptr, path_len) => this.path_remove_directory(dirfd, path_ptr, path_len),
|
|
1397
|
+
path_rename: (old_dirfd, old_path_ptr, old_path_len, new_dirfd, new_path_ptr, new_path_len) => this.path_rename(old_dirfd, old_path_ptr, old_path_len, new_dirfd, new_path_ptr, new_path_len),
|
|
1398
|
+
path_symlink: (old_path_ptr, old_path_len, dirfd, new_path_ptr, new_path_len) => this.path_symlink(old_path_ptr, old_path_len, dirfd, new_path_ptr, new_path_len),
|
|
1399
|
+
path_readlink: (dirfd, path_ptr, path_len, buf_ptr, buf_len, bufused_ptr) => this.path_readlink(dirfd, path_ptr, path_len, buf_ptr, buf_len, bufused_ptr),
|
|
1400
|
+
path_filestat_get: (dirfd, flags, path_ptr, path_len, buf_ptr) => this.path_filestat_get(dirfd, flags, path_ptr, path_len, buf_ptr),
|
|
1401
|
+
path_filestat_set_times: (dirfd, flags, path_ptr, path_len, atim, mtim, fst_flags) => this.path_filestat_set_times(dirfd, flags, path_ptr, path_len, atim, mtim, fst_flags),
|
|
1402
|
+
// FD filestat and directory operations (US-008)
|
|
1403
|
+
fd_filestat_get: (fd, buf_ptr) => this.fd_filestat_get(fd, buf_ptr),
|
|
1404
|
+
fd_filestat_set_size: (fd, size) => this.fd_filestat_set_size(fd, size),
|
|
1405
|
+
fd_filestat_set_times: (fd, atim, mtim, fst_flags) => this.fd_filestat_set_times(fd, atim, mtim, fst_flags),
|
|
1406
|
+
fd_readdir: (fd, buf_ptr, buf_len, cookie, bufused_ptr) => this.fd_readdir(fd, buf_ptr, buf_len, cookie, bufused_ptr),
|
|
1407
|
+
// Args, env, clock, random, process (US-009)
|
|
1408
|
+
args_get: (argv_ptr, argv_buf_ptr) => this.args_get(argv_ptr, argv_buf_ptr),
|
|
1409
|
+
args_sizes_get: (argc_ptr, argv_buf_size_ptr) => this.args_sizes_get(argc_ptr, argv_buf_size_ptr),
|
|
1410
|
+
environ_get: (environ_ptr, environ_buf_ptr) => this.environ_get(environ_ptr, environ_buf_ptr),
|
|
1411
|
+
environ_sizes_get: (environc_ptr, environ_buf_size_ptr) => this.environ_sizes_get(environc_ptr, environ_buf_size_ptr),
|
|
1412
|
+
clock_res_get: (id, resolution_ptr) => this.clock_res_get(id, resolution_ptr),
|
|
1413
|
+
clock_time_get: (id, precision, time_ptr) => this.clock_time_get(id, precision, time_ptr),
|
|
1414
|
+
random_get: (buf_ptr, buf_len) => this.random_get(buf_ptr, buf_len),
|
|
1415
|
+
proc_exit: (exitCode) => this.proc_exit(exitCode),
|
|
1416
|
+
proc_raise: (sig) => this.proc_raise(sig),
|
|
1417
|
+
sched_yield: () => this.sched_yield(),
|
|
1418
|
+
poll_oneoff: (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => this.poll_oneoff(in_ptr, out_ptr, nsubscriptions, nevents_ptr),
|
|
1419
|
+
// Stub fd operations (US-009)
|
|
1420
|
+
fd_advise: (fd, offset, len, advice) => this.fd_advise(fd, offset, len, advice),
|
|
1421
|
+
fd_allocate: (fd, offset, len) => this.fd_allocate(fd, offset, len),
|
|
1422
|
+
fd_datasync: (fd) => this.fd_datasync(fd),
|
|
1423
|
+
fd_sync: (fd) => this.fd_sync(fd),
|
|
1424
|
+
fd_fdstat_set_rights: (fd, fs_rights_base, fs_rights_inheriting) => this.fd_fdstat_set_rights(fd, fs_rights_base, fs_rights_inheriting),
|
|
1425
|
+
fd_pread: (fd, iovs_ptr, iovs_len, offset, nread_ptr) => this.fd_pread(fd, iovs_ptr, iovs_len, offset, nread_ptr),
|
|
1426
|
+
fd_pwrite: (fd, iovs_ptr, iovs_len, offset, nwritten_ptr) => this.fd_pwrite(fd, iovs_ptr, iovs_len, offset, nwritten_ptr),
|
|
1427
|
+
fd_renumber: (from_fd, to_fd) => this.fd_renumber(from_fd, to_fd),
|
|
1428
|
+
// Path stubs (US-009)
|
|
1429
|
+
path_link: (old_fd, old_flags, old_path_ptr, old_path_len, new_fd, new_path_ptr, new_path_len) => this.path_link(old_fd, old_flags, old_path_ptr, old_path_len, new_fd, new_path_ptr, new_path_len),
|
|
1430
|
+
// Socket stubs (US-009) -- all return ENOSYS
|
|
1431
|
+
sock_accept: (fd, flags, result_fd_ptr) => this.sock_accept(fd, flags, result_fd_ptr),
|
|
1432
|
+
sock_recv: (fd, ri_data_ptr, ri_data_len, ri_flags, ro_datalen_ptr, ro_flags_ptr) => this.sock_recv(fd, ri_data_ptr, ri_data_len, ri_flags, ro_datalen_ptr, ro_flags_ptr),
|
|
1433
|
+
sock_send: (fd, si_data_ptr, si_data_len, si_flags, so_datalen_ptr) => this.sock_send(fd, si_data_ptr, si_data_len, si_flags, so_datalen_ptr),
|
|
1434
|
+
sock_shutdown: (fd, how) => this.sock_shutdown(fd, how),
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
//# sourceMappingURL=wasi-polyfill.js.map
|