xit-wasm 0.1.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/README.md +156 -0
- package/dist/archive.d.ts +118 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +363 -0
- package/dist/archive.js.map +1 -0
- package/dist/host-memory.d.ts +109 -0
- package/dist/host-memory.d.ts.map +1 -0
- package/dist/host-memory.js +432 -0
- package/dist/host-memory.js.map +1 -0
- package/dist/host-node.d.ts +102 -0
- package/dist/host-node.d.ts.map +1 -0
- package/dist/host-node.js +376 -0
- package/dist/host-node.js.map +1 -0
- package/dist/host.d.ts +160 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +164 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/archive.ts +515 -0
- package/src/host-memory.ts +458 -0
- package/src/host-node.ts +388 -0
- package/src/host.ts +262 -0
- package/src/index.ts +100 -0
- package/xit.wasm +0 -0
package/src/host-node.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-fs-backed Host implementation.
|
|
3
|
+
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* - Dir handles are i32s mapped to absolute filesystem paths. Handle 0 is
|
|
6
|
+
* reserved for the "root" directory provided to NodeHost's constructor;
|
|
7
|
+
* all sub-path operations resolve relative to it.
|
|
8
|
+
* - File handles are i32s mapped to Node file descriptors (fs.openSync), but
|
|
9
|
+
* wrapped with a small extra state object so we can track current position
|
|
10
|
+
* (for seek-by) and locked state without re-querying the fs.
|
|
11
|
+
* - All ops use Node sync APIs. Plenty fast for spike-grade testing; can
|
|
12
|
+
* swap for a Worker-driven async model later if needed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as crypto from "node:crypto";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
KIND_FILE,
|
|
21
|
+
KIND_DIR,
|
|
22
|
+
KIND_SYMLINK,
|
|
23
|
+
KIND_OTHER,
|
|
24
|
+
OK,
|
|
25
|
+
ERR_GENERIC,
|
|
26
|
+
ERR_NOT_FOUND,
|
|
27
|
+
} from "./host.ts";
|
|
28
|
+
import type { Host, StatResult } from "./host.ts";
|
|
29
|
+
|
|
30
|
+
interface FileEntry {
|
|
31
|
+
fd: number;
|
|
32
|
+
position: bigint;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class NodeHost implements Host {
|
|
36
|
+
private dirs = new Map<number, string>();
|
|
37
|
+
private files = new Map<number, FileEntry>();
|
|
38
|
+
private nextHandle = 100;
|
|
39
|
+
trace = false;
|
|
40
|
+
|
|
41
|
+
constructor(rootPath: string) {
|
|
42
|
+
this.dirs.set(0, rootPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private log(...args: unknown[]) {
|
|
46
|
+
if (this.trace) console.error("[host]", ...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private resolve(dirHandle: number, sub: string): string {
|
|
50
|
+
const base = this.dirs.get(dirHandle);
|
|
51
|
+
if (base === undefined) throw new Error(`unknown dir handle ${dirHandle}`);
|
|
52
|
+
return path.resolve(base, sub);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private allocHandle(): number {
|
|
56
|
+
return this.nextHandle++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private statToResult(s: fs.Stats): StatResult {
|
|
60
|
+
let kind = KIND_OTHER;
|
|
61
|
+
if (s.isFile()) kind = KIND_FILE;
|
|
62
|
+
else if (s.isDirectory()) kind = KIND_DIR;
|
|
63
|
+
else if (s.isSymbolicLink()) kind = KIND_SYMLINK;
|
|
64
|
+
// Node's plain Stats has *Ms (number); promote to ns BigInt by hand. The
|
|
65
|
+
// bigint variant of Stats has *Ns directly but it forces every access to
|
|
66
|
+
// BigInt and is hairier to thread through.
|
|
67
|
+
const msToNs = (ms: number) => BigInt(Math.floor(ms)) * 1_000_000n;
|
|
68
|
+
return {
|
|
69
|
+
size: BigInt(s.size),
|
|
70
|
+
mtimeNs: msToNs(s.mtimeMs),
|
|
71
|
+
atimeNs: msToNs(s.atimeMs),
|
|
72
|
+
ctimeNs: msToNs(s.ctimeMs),
|
|
73
|
+
inode: BigInt(s.ino),
|
|
74
|
+
kind,
|
|
75
|
+
modeBits: s.mode & 0o777,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ----- dir -----
|
|
80
|
+
|
|
81
|
+
dirCreateFile(dirHandle: number, sub: string) {
|
|
82
|
+
const abs = this.resolve(dirHandle, sub);
|
|
83
|
+
try {
|
|
84
|
+
const fd = fs.openSync(abs, "w+");
|
|
85
|
+
const handle = this.allocHandle();
|
|
86
|
+
this.files.set(handle, { fd, position: 0n });
|
|
87
|
+
this.log("dirCreateFile", abs, "->", handle);
|
|
88
|
+
return { code: OK, handle };
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
this.log("dirCreateFile FAIL", abs, err.code);
|
|
91
|
+
return { code: ERR_GENERIC, handle: -1 };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dirOpenFile(dirHandle: number, sub: string) {
|
|
96
|
+
const abs = this.resolve(dirHandle, sub);
|
|
97
|
+
try {
|
|
98
|
+
const fd = fs.openSync(abs, "r+");
|
|
99
|
+
const handle = this.allocHandle();
|
|
100
|
+
this.files.set(handle, { fd, position: 0n });
|
|
101
|
+
this.log("dirOpenFile", abs, "->", handle);
|
|
102
|
+
return { code: OK, handle };
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
this.log("dirOpenFile FAIL", abs, err.code);
|
|
105
|
+
return { code: ERR_NOT_FOUND, handle: -1 };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
dirCreateDir(dirHandle: number, sub: string) {
|
|
110
|
+
try {
|
|
111
|
+
fs.mkdirSync(this.resolve(dirHandle, sub));
|
|
112
|
+
return OK;
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
if (err.code === "EEXIST") return ERR_GENERIC; // xit treats this as already-exists
|
|
115
|
+
return ERR_GENERIC;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dirCreateDirPath(dirHandle: number, sub: string) {
|
|
120
|
+
const abs = this.resolve(dirHandle, sub);
|
|
121
|
+
let existed = false;
|
|
122
|
+
try {
|
|
123
|
+
fs.statSync(abs);
|
|
124
|
+
existed = true;
|
|
125
|
+
} catch {}
|
|
126
|
+
try {
|
|
127
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
128
|
+
return { code: OK, existed };
|
|
129
|
+
} catch {
|
|
130
|
+
return { code: ERR_GENERIC, existed: false };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
dirCreateDirPathOpen(dirHandle: number, sub: string) {
|
|
135
|
+
const abs = this.resolve(dirHandle, sub);
|
|
136
|
+
try {
|
|
137
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
138
|
+
const handle = this.allocHandle();
|
|
139
|
+
this.dirs.set(handle, abs);
|
|
140
|
+
this.log("dirCreateDirPathOpen", abs, "->", handle);
|
|
141
|
+
return { code: OK, handle };
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
this.log("dirCreateDirPathOpen FAIL", abs, err.code);
|
|
144
|
+
return { code: ERR_GENERIC, handle: -1 };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dirOpenDir(dirHandle: number, sub: string) {
|
|
149
|
+
const abs = this.resolve(dirHandle, sub);
|
|
150
|
+
try {
|
|
151
|
+
const s = fs.statSync(abs);
|
|
152
|
+
if (!s.isDirectory()) return { code: ERR_NOT_FOUND, handle: -1 };
|
|
153
|
+
const handle = this.allocHandle();
|
|
154
|
+
this.dirs.set(handle, abs);
|
|
155
|
+
return { code: OK, handle };
|
|
156
|
+
} catch {
|
|
157
|
+
return { code: ERR_NOT_FOUND, handle: -1 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dirClose(handle: number) {
|
|
162
|
+
if (handle === 0) return; // never close the root
|
|
163
|
+
this.dirs.delete(handle);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dirDeleteFile(dirHandle: number, sub: string) {
|
|
167
|
+
const abs = this.resolve(dirHandle, sub);
|
|
168
|
+
try {
|
|
169
|
+
fs.unlinkSync(abs);
|
|
170
|
+
this.log("dirDeleteFile", abs);
|
|
171
|
+
return OK;
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
this.log("dirDeleteFile FAIL", abs, err.code);
|
|
174
|
+
return ERR_NOT_FOUND;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dirRename(oldDir: number, oldSub: string, newDir: number, newSub: string) {
|
|
179
|
+
const oldAbs = this.resolve(oldDir, oldSub);
|
|
180
|
+
const newAbs = this.resolve(newDir, newSub);
|
|
181
|
+
try {
|
|
182
|
+
fs.renameSync(oldAbs, newAbs);
|
|
183
|
+
this.log("dirRename", oldAbs, "->", newAbs);
|
|
184
|
+
return OK;
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
this.log("dirRename FAIL", oldAbs, "->", newAbs, err.code);
|
|
187
|
+
return ERR_GENERIC;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
dirStatFile(dirHandle: number, sub: string) {
|
|
192
|
+
const abs = this.resolve(dirHandle, sub);
|
|
193
|
+
try {
|
|
194
|
+
const s = fs.lstatSync(abs);
|
|
195
|
+
this.log("dirStatFile", abs, "size", s.size);
|
|
196
|
+
return { code: OK, stat: this.statToResult(s) };
|
|
197
|
+
} catch (err: any) {
|
|
198
|
+
this.log("dirStatFile FAIL", abs, err.code);
|
|
199
|
+
return { code: ERR_NOT_FOUND };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
dirReadLink(dirHandle: number, sub: string) {
|
|
204
|
+
const abs = this.resolve(dirHandle, sub);
|
|
205
|
+
try {
|
|
206
|
+
const target = fs.readlinkSync(abs);
|
|
207
|
+
this.log("dirReadLink", abs, "->", target);
|
|
208
|
+
return { code: 1, target };
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
if (err.code === "EINVAL") {
|
|
211
|
+
// not a symlink — xit needs to know this distinction
|
|
212
|
+
this.log("dirReadLink", abs, "(not a symlink)");
|
|
213
|
+
return { code: 0 };
|
|
214
|
+
}
|
|
215
|
+
this.log("dirReadLink FAIL", abs, err.code);
|
|
216
|
+
return { code: -1 };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dirAccess(dirHandle: number, sub: string) {
|
|
221
|
+
const abs = this.resolve(dirHandle, sub);
|
|
222
|
+
try {
|
|
223
|
+
fs.accessSync(abs);
|
|
224
|
+
this.log("dirAccess OK", abs);
|
|
225
|
+
return OK;
|
|
226
|
+
} catch {
|
|
227
|
+
this.log("dirAccess MISSING", abs);
|
|
228
|
+
return ERR_NOT_FOUND;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ----- file -----
|
|
233
|
+
|
|
234
|
+
fileClose(handle: number) {
|
|
235
|
+
const entry = this.files.get(handle);
|
|
236
|
+
if (!entry) return;
|
|
237
|
+
try {
|
|
238
|
+
fs.closeSync(entry.fd);
|
|
239
|
+
} catch {}
|
|
240
|
+
this.files.delete(handle);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fileRead(handle: number, offset: bigint, len: number) {
|
|
244
|
+
const entry = this.files.get(handle);
|
|
245
|
+
if (!entry) return { code: ERR_GENERIC, data: new Uint8Array(0) };
|
|
246
|
+
const buf = Buffer.alloc(len);
|
|
247
|
+
try {
|
|
248
|
+
const n = fs.readSync(entry.fd, buf, 0, len, Number(offset));
|
|
249
|
+
this.log("fileRead", handle, "offset", offset, "len", len, "got", n);
|
|
250
|
+
return { code: OK, data: new Uint8Array(buf.buffer, buf.byteOffset, n) };
|
|
251
|
+
} catch (err: any) {
|
|
252
|
+
this.log("fileRead FAIL", handle, err.code);
|
|
253
|
+
return { code: ERR_GENERIC, data: new Uint8Array(0) };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fileWriteStream(handle: number, data: Uint8Array) {
|
|
258
|
+
const entry = this.files.get(handle);
|
|
259
|
+
if (!entry) return { code: ERR_GENERIC, written: 0 };
|
|
260
|
+
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
261
|
+
try {
|
|
262
|
+
const n = fs.writeSync(entry.fd, buf, 0, buf.length, Number(entry.position));
|
|
263
|
+
entry.position += BigInt(n);
|
|
264
|
+
this.log("fileWriteStream", handle, "wrote", n, "newPos", entry.position);
|
|
265
|
+
return { code: OK, written: n };
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
this.log("fileWriteStream FAIL", handle, err.code);
|
|
268
|
+
return { code: ERR_GENERIC, written: 0 };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fileReadStream(handle: number, len: number) {
|
|
273
|
+
const entry = this.files.get(handle);
|
|
274
|
+
if (!entry) return { code: ERR_GENERIC, data: new Uint8Array(0) };
|
|
275
|
+
const buf = Buffer.alloc(len);
|
|
276
|
+
try {
|
|
277
|
+
const n = fs.readSync(entry.fd, buf, 0, len, Number(entry.position));
|
|
278
|
+
entry.position += BigInt(n);
|
|
279
|
+
this.log("fileReadStream", handle, "got", n, "newPos", entry.position);
|
|
280
|
+
return { code: OK, data: new Uint8Array(buf.buffer, buf.byteOffset, n) };
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
this.log("fileReadStream FAIL", handle, err.code);
|
|
283
|
+
return { code: ERR_GENERIC, data: new Uint8Array(0) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fileWrite(handle: number, offset: bigint, data: Uint8Array) {
|
|
288
|
+
const entry = this.files.get(handle);
|
|
289
|
+
if (!entry) return { code: ERR_GENERIC, written: 0 };
|
|
290
|
+
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
291
|
+
try {
|
|
292
|
+
const n = fs.writeSync(entry.fd, buf, 0, buf.length, Number(offset));
|
|
293
|
+
this.log("fileWrite", handle, "offset", offset, "len", buf.length, "wrote", n);
|
|
294
|
+
return { code: OK, written: n };
|
|
295
|
+
} catch (err: any) {
|
|
296
|
+
this.log("fileWrite FAIL", handle, err.code);
|
|
297
|
+
return { code: ERR_GENERIC, written: 0 };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fileStat(handle: number) {
|
|
302
|
+
const entry = this.files.get(handle);
|
|
303
|
+
if (!entry) return { code: ERR_GENERIC };
|
|
304
|
+
try {
|
|
305
|
+
const s = fs.fstatSync(entry.fd);
|
|
306
|
+
this.log("fileStat", handle, "size", s.size, "kind", this.statToResult(s).kind);
|
|
307
|
+
return { code: OK, stat: this.statToResult(s) };
|
|
308
|
+
} catch (err: any) {
|
|
309
|
+
this.log("fileStat FAIL", handle, err.code);
|
|
310
|
+
return { code: ERR_GENERIC };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fileLength(handle: number) {
|
|
315
|
+
const entry = this.files.get(handle);
|
|
316
|
+
if (!entry) return { code: ERR_GENERIC, length: 0n };
|
|
317
|
+
try {
|
|
318
|
+
const s = fs.fstatSync(entry.fd);
|
|
319
|
+
this.log("fileLength", handle, "->", s.size);
|
|
320
|
+
return { code: OK, length: BigInt(s.size) };
|
|
321
|
+
} catch (err: any) {
|
|
322
|
+
this.log("fileLength FAIL", handle, err.code);
|
|
323
|
+
return { code: ERR_GENERIC, length: 0n };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fileSeekTo(handle: number, offset: bigint) {
|
|
328
|
+
const entry = this.files.get(handle);
|
|
329
|
+
if (!entry) return ERR_GENERIC;
|
|
330
|
+
entry.position = offset;
|
|
331
|
+
return OK;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fileSeekBy(handle: number, relative: bigint) {
|
|
335
|
+
const entry = this.files.get(handle);
|
|
336
|
+
if (!entry) return ERR_GENERIC;
|
|
337
|
+
entry.position += relative;
|
|
338
|
+
return OK;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fileLock(_handle: number, _kind: number) {
|
|
342
|
+
// Spike: no real flock. We're single-process, single-threaded — index/db
|
|
343
|
+
// mutexes inside one xit run are guaranteed serial via JS event loop.
|
|
344
|
+
return OK;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fileTryLock(_handle: number, _kind: number) {
|
|
348
|
+
return 1; // always acquired in spike mode
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fileUnlock(_handle: number) {
|
|
352
|
+
/* no-op */
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fileSync(handle: number) {
|
|
356
|
+
const entry = this.files.get(handle);
|
|
357
|
+
if (!entry) return ERR_GENERIC;
|
|
358
|
+
try {
|
|
359
|
+
fs.fsyncSync(entry.fd);
|
|
360
|
+
return OK;
|
|
361
|
+
} catch {
|
|
362
|
+
return ERR_GENERIC;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fileSetLength(handle: number, length: bigint) {
|
|
367
|
+
const entry = this.files.get(handle);
|
|
368
|
+
if (!entry) return ERR_GENERIC;
|
|
369
|
+
try {
|
|
370
|
+
fs.ftruncateSync(entry.fd, Number(length));
|
|
371
|
+
this.log("fileSetLength", handle, "->", length);
|
|
372
|
+
return OK;
|
|
373
|
+
} catch (err: any) {
|
|
374
|
+
this.log("fileSetLength FAIL", handle, err.code);
|
|
375
|
+
return ERR_GENERIC;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ----- misc -----
|
|
380
|
+
|
|
381
|
+
nowNanos() {
|
|
382
|
+
return BigInt(Date.now()) * 1_000_000n;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
random(buf: Uint8Array) {
|
|
386
|
+
crypto.randomFillSync(buf);
|
|
387
|
+
}
|
|
388
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host import surface. The wasm module declares 24 `host.*` extern functions;
|
|
3
|
+
* a Host implementation provides their JS-side bodies. This module defines:
|
|
4
|
+
*
|
|
5
|
+
* - The Host interface (signatures matching src/host_io.zig).
|
|
6
|
+
* - HostStat: byte-layout helper that mirrors the Zig HostStat extern struct.
|
|
7
|
+
* - hostImports(): converts a Host into the WebAssembly imports object the
|
|
8
|
+
* wasm module expects.
|
|
9
|
+
*
|
|
10
|
+
* The wasm passes pointers + lengths into linear memory; Host implementations
|
|
11
|
+
* take a `WebAssembly.Memory` reference at construction time so they can read
|
|
12
|
+
* and write into wasm memory directly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Wire layout matches `HostStat` in src/host_io.zig — 48 bytes, 8-aligned. */
|
|
16
|
+
export const HOST_STAT_BYTES = 48;
|
|
17
|
+
|
|
18
|
+
export type Kind = "file" | "directory" | "sym_link" | "other";
|
|
19
|
+
|
|
20
|
+
export const KIND_FILE = 0;
|
|
21
|
+
export const KIND_DIR = 1;
|
|
22
|
+
export const KIND_SYMLINK = 2;
|
|
23
|
+
export const KIND_OTHER = 3;
|
|
24
|
+
|
|
25
|
+
export const LOCK_NONE = 0;
|
|
26
|
+
export const LOCK_SHARED = 1;
|
|
27
|
+
export const LOCK_EXCLUSIVE = 2;
|
|
28
|
+
|
|
29
|
+
/** Status codes the Host returns. 0 = success; non-zero values are passed
|
|
30
|
+
* through to xit and surface as errors there. Keeping this enum loose for
|
|
31
|
+
* now — finer-grained error mapping is a follow-up. */
|
|
32
|
+
export const OK = 0;
|
|
33
|
+
export const ERR_GENERIC = 1;
|
|
34
|
+
export const ERR_NOT_FOUND = 2;
|
|
35
|
+
|
|
36
|
+
export interface StatResult {
|
|
37
|
+
size: bigint;
|
|
38
|
+
mtimeNs: bigint;
|
|
39
|
+
atimeNs: bigint;
|
|
40
|
+
ctimeNs: bigint;
|
|
41
|
+
inode: bigint;
|
|
42
|
+
kind: number;
|
|
43
|
+
modeBits: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The behavior surface a Host has to provide. Each method takes already-decoded
|
|
48
|
+
* arguments (paths as strings, handles as numbers) — the wasm-import shims in
|
|
49
|
+
* hostImports() handle pointer-decoding before calling into the host.
|
|
50
|
+
*
|
|
51
|
+
* Handles are opaque i32s; the host owns the mapping (e.g. dir handle 0 is the
|
|
52
|
+
* "root" the host chose to expose to xit). xit itself only ever passes them
|
|
53
|
+
* back through the same set of host calls.
|
|
54
|
+
*/
|
|
55
|
+
export interface Host {
|
|
56
|
+
// dir
|
|
57
|
+
dirCreateFile(dirHandle: number, path: string): { code: number; handle: number };
|
|
58
|
+
dirOpenFile(dirHandle: number, path: string): { code: number; handle: number };
|
|
59
|
+
dirCreateDir(dirHandle: number, path: string): number;
|
|
60
|
+
dirCreateDirPath(dirHandle: number, path: string): { code: number; existed: boolean };
|
|
61
|
+
dirCreateDirPathOpen(dirHandle: number, path: string): { code: number; handle: number };
|
|
62
|
+
dirOpenDir(dirHandle: number, path: string): { code: number; handle: number };
|
|
63
|
+
dirClose(handle: number): void;
|
|
64
|
+
dirDeleteFile(dirHandle: number, path: string): number;
|
|
65
|
+
dirRename(oldDir: number, oldPath: string, newDir: number, newPath: string): number;
|
|
66
|
+
dirStatFile(dirHandle: number, path: string): { code: number; stat?: StatResult };
|
|
67
|
+
dirAccess(dirHandle: number, path: string): number;
|
|
68
|
+
/** Returns:
|
|
69
|
+
* - { code: 1+, target } — symlink with target string
|
|
70
|
+
* - { code: 0 } — path exists but is not a symlink (xit needs the distinction)
|
|
71
|
+
* - { code: -1 } — path does not exist or unreadable
|
|
72
|
+
*/
|
|
73
|
+
dirReadLink(dirHandle: number, path: string): { code: number; target?: string };
|
|
74
|
+
// file
|
|
75
|
+
fileClose(handle: number): void;
|
|
76
|
+
fileRead(handle: number, offset: bigint, len: number): { code: number; data: Uint8Array };
|
|
77
|
+
fileWrite(handle: number, offset: bigint, data: Uint8Array): { code: number; written: number };
|
|
78
|
+
fileWriteStream(handle: number, data: Uint8Array): { code: number; written: number };
|
|
79
|
+
fileReadStream(handle: number, len: number): { code: number; data: Uint8Array };
|
|
80
|
+
fileStat(handle: number): { code: number; stat?: StatResult };
|
|
81
|
+
fileLength(handle: number): { code: number; length: bigint };
|
|
82
|
+
fileSeekTo(handle: number, offset: bigint): number;
|
|
83
|
+
fileSeekBy(handle: number, relative: bigint): number;
|
|
84
|
+
fileLock(handle: number, kind: number): number;
|
|
85
|
+
/** Returns 1 if acquired, 0 if would-block, negative on error. */
|
|
86
|
+
fileTryLock(handle: number, kind: number): number;
|
|
87
|
+
fileUnlock(handle: number): void;
|
|
88
|
+
fileSync(handle: number): number;
|
|
89
|
+
fileSetLength(handle: number, length: bigint): number;
|
|
90
|
+
// misc
|
|
91
|
+
nowNanos(): bigint;
|
|
92
|
+
random(buf: Uint8Array): void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the WebAssembly imports object expected by the wasm module. Pointer-
|
|
97
|
+
* decoding lives here (centralized) so individual Host implementations can stay
|
|
98
|
+
* focused on their own backing store.
|
|
99
|
+
*/
|
|
100
|
+
export function hostImports(host: Host, getMemory: () => WebAssembly.Memory) {
|
|
101
|
+
const dec = new TextDecoder();
|
|
102
|
+
const memU8 = () => new Uint8Array(getMemory().buffer);
|
|
103
|
+
const memDV = () => new DataView(getMemory().buffer);
|
|
104
|
+
|
|
105
|
+
const readStr = (ptr: number, len: number) => dec.decode(memU8().slice(ptr, ptr + len));
|
|
106
|
+
|
|
107
|
+
const writeStat = (ptr: number, s: StatResult) => {
|
|
108
|
+
const dv = memDV();
|
|
109
|
+
dv.setBigUint64(ptr + 0, s.size, true);
|
|
110
|
+
dv.setBigInt64(ptr + 8, s.mtimeNs, true);
|
|
111
|
+
dv.setBigInt64(ptr + 16, s.atimeNs, true);
|
|
112
|
+
dv.setBigInt64(ptr + 24, s.ctimeNs, true);
|
|
113
|
+
dv.setBigUint64(ptr + 32, s.inode, true);
|
|
114
|
+
dv.setUint32(ptr + 40, s.kind, true);
|
|
115
|
+
dv.setUint32(ptr + 44, s.modeBits, true);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
host: {
|
|
120
|
+
host_dir_create_file: (dirH: number, p: number, plen: number, outH: number) => {
|
|
121
|
+
const r = host.dirCreateFile(dirH, readStr(p, plen));
|
|
122
|
+
if (r.code === OK) memDV().setInt32(outH, r.handle, true);
|
|
123
|
+
return r.code;
|
|
124
|
+
},
|
|
125
|
+
host_dir_open_file: (dirH: number, p: number, plen: number, outH: number) => {
|
|
126
|
+
const r = host.dirOpenFile(dirH, readStr(p, plen));
|
|
127
|
+
if (r.code === OK) memDV().setInt32(outH, r.handle, true);
|
|
128
|
+
return r.code;
|
|
129
|
+
},
|
|
130
|
+
host_dir_create_dir: (dirH: number, p: number, plen: number) =>
|
|
131
|
+
host.dirCreateDir(dirH, readStr(p, plen)),
|
|
132
|
+
host_dir_create_dir_path: (dirH: number, p: number, plen: number, outExisted: number) => {
|
|
133
|
+
const r = host.dirCreateDirPath(dirH, readStr(p, plen));
|
|
134
|
+
if (r.code === OK) memU8()[outExisted] = r.existed ? 1 : 0;
|
|
135
|
+
return r.code;
|
|
136
|
+
},
|
|
137
|
+
host_dir_create_dir_path_open: (dirH: number, p: number, plen: number, outH: number) => {
|
|
138
|
+
const r = host.dirCreateDirPathOpen(dirH, readStr(p, plen));
|
|
139
|
+
if (r.code === OK) memDV().setInt32(outH, r.handle, true);
|
|
140
|
+
return r.code;
|
|
141
|
+
},
|
|
142
|
+
host_dir_open_dir: (dirH: number, p: number, plen: number, outH: number) => {
|
|
143
|
+
const r = host.dirOpenDir(dirH, readStr(p, plen));
|
|
144
|
+
if (r.code === OK) memDV().setInt32(outH, r.handle, true);
|
|
145
|
+
return r.code;
|
|
146
|
+
},
|
|
147
|
+
host_dir_close: (h: number) => host.dirClose(h),
|
|
148
|
+
host_dir_delete_file: (dirH: number, p: number, plen: number) =>
|
|
149
|
+
host.dirDeleteFile(dirH, readStr(p, plen)),
|
|
150
|
+
host_dir_rename: (
|
|
151
|
+
oldDir: number,
|
|
152
|
+
op: number,
|
|
153
|
+
olen: number,
|
|
154
|
+
newDir: number,
|
|
155
|
+
np: number,
|
|
156
|
+
nlen: number,
|
|
157
|
+
) => host.dirRename(oldDir, readStr(op, olen), newDir, readStr(np, nlen)),
|
|
158
|
+
host_dir_stat_file: (dirH: number, p: number, plen: number, outStat: number) => {
|
|
159
|
+
const r = host.dirStatFile(dirH, readStr(p, plen));
|
|
160
|
+
if (r.code === OK && r.stat) writeStat(outStat, r.stat);
|
|
161
|
+
return r.code;
|
|
162
|
+
},
|
|
163
|
+
host_dir_access: (dirH: number, p: number, plen: number) =>
|
|
164
|
+
host.dirAccess(dirH, readStr(p, plen)),
|
|
165
|
+
host_dir_read_link: (
|
|
166
|
+
dirH: number,
|
|
167
|
+
p: number,
|
|
168
|
+
plen: number,
|
|
169
|
+
bufPtr: number,
|
|
170
|
+
bufLen: number,
|
|
171
|
+
outSize: number,
|
|
172
|
+
) => {
|
|
173
|
+
const r = host.dirReadLink(dirH, readStr(p, plen));
|
|
174
|
+
if (r.code > 0 && r.target !== undefined) {
|
|
175
|
+
const bytes = new TextEncoder().encode(r.target);
|
|
176
|
+
if (bytes.length > bufLen) return -1;
|
|
177
|
+
memU8().set(bytes, bufPtr);
|
|
178
|
+
memDV().setUint32(outSize, bytes.length, true);
|
|
179
|
+
return bytes.length;
|
|
180
|
+
}
|
|
181
|
+
return r.code;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
host_file_close: (h: number) => host.fileClose(h),
|
|
185
|
+
host_file_read: (
|
|
186
|
+
h: number,
|
|
187
|
+
offset: bigint,
|
|
188
|
+
bufPtr: number,
|
|
189
|
+
bufLen: number,
|
|
190
|
+
outRead: number,
|
|
191
|
+
) => {
|
|
192
|
+
const r = host.fileRead(h, offset, bufLen);
|
|
193
|
+
if (r.code === OK) {
|
|
194
|
+
memU8().set(r.data, bufPtr);
|
|
195
|
+
// out_read is `*usize` which on wasm32 is 4 bytes
|
|
196
|
+
memDV().setUint32(outRead, r.data.length, true);
|
|
197
|
+
}
|
|
198
|
+
return r.code;
|
|
199
|
+
},
|
|
200
|
+
host_file_write: (
|
|
201
|
+
h: number,
|
|
202
|
+
offset: bigint,
|
|
203
|
+
bufPtr: number,
|
|
204
|
+
bufLen: number,
|
|
205
|
+
outWritten: number,
|
|
206
|
+
) => {
|
|
207
|
+
const data = memU8().slice(bufPtr, bufPtr + bufLen);
|
|
208
|
+
const r = host.fileWrite(h, offset, data);
|
|
209
|
+
if (r.code === OK) memDV().setUint32(outWritten, r.written, true);
|
|
210
|
+
return r.code;
|
|
211
|
+
},
|
|
212
|
+
host_file_stat: (h: number, outStat: number) => {
|
|
213
|
+
const r = host.fileStat(h);
|
|
214
|
+
if (r.code === OK && r.stat) writeStat(outStat, r.stat);
|
|
215
|
+
return r.code;
|
|
216
|
+
},
|
|
217
|
+
host_file_length: (h: number, outLen: number) => {
|
|
218
|
+
const r = host.fileLength(h);
|
|
219
|
+
if (r.code === OK) memDV().setBigUint64(outLen, r.length, true);
|
|
220
|
+
return r.code;
|
|
221
|
+
},
|
|
222
|
+
host_file_write_stream: (
|
|
223
|
+
h: number,
|
|
224
|
+
bufPtr: number,
|
|
225
|
+
bufLen: number,
|
|
226
|
+
outWritten: number,
|
|
227
|
+
) => {
|
|
228
|
+
const data = memU8().slice(bufPtr, bufPtr + bufLen);
|
|
229
|
+
const r = host.fileWriteStream(h, data);
|
|
230
|
+
if (r.code === OK) memDV().setUint32(outWritten, r.written, true);
|
|
231
|
+
return r.code;
|
|
232
|
+
},
|
|
233
|
+
host_file_read_stream: (
|
|
234
|
+
h: number,
|
|
235
|
+
bufPtr: number,
|
|
236
|
+
bufLen: number,
|
|
237
|
+
outRead: number,
|
|
238
|
+
) => {
|
|
239
|
+
const r = host.fileReadStream(h, bufLen);
|
|
240
|
+
if (r.code === OK) {
|
|
241
|
+
memU8().set(r.data, bufPtr);
|
|
242
|
+
memDV().setUint32(outRead, r.data.length, true);
|
|
243
|
+
}
|
|
244
|
+
return r.code;
|
|
245
|
+
},
|
|
246
|
+
host_file_seek_to: (h: number, offset: bigint) => host.fileSeekTo(h, offset),
|
|
247
|
+
host_file_seek_by: (h: number, relative: bigint) => host.fileSeekBy(h, relative),
|
|
248
|
+
host_file_lock: (h: number, kind: number) => host.fileLock(h, kind),
|
|
249
|
+
host_file_try_lock: (h: number, kind: number) => host.fileTryLock(h, kind),
|
|
250
|
+
host_file_unlock: (h: number) => host.fileUnlock(h),
|
|
251
|
+
host_file_sync: (h: number) => host.fileSync(h),
|
|
252
|
+
host_file_set_length: (h: number, len: bigint) => host.fileSetLength(h, len),
|
|
253
|
+
|
|
254
|
+
host_now_nanos: () => host.nowNanos(),
|
|
255
|
+
host_random: (bufPtr: number, bufLen: number) => {
|
|
256
|
+
const buf = new Uint8Array(bufLen);
|
|
257
|
+
host.random(buf);
|
|
258
|
+
memU8().set(buf, bufPtr);
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|