sandboxedjs 0.1.48 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.cts +2 -1
- package/dist/agent.d.ts +2 -1
- package/dist/{container-DyRF-bY0.d.cts → container-DBMPLbUu.d.ts} +103 -410
- package/dist/{container-DyRF-bY0.d.ts → container-Dg8NwBc7.d.cts} +103 -410
- package/dist/index.cjs +2335 -306
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -65
- package/dist/index.d.ts +20 -65
- package/dist/index.js +2331 -305
- package/dist/index.js.map +1 -1
- package/dist/memory-volume-CTOXNJ6n.d.ts +66 -0
- package/dist/memory-volume-DCzGZFJb.d.cts +66 -0
- package/dist/python/python.data +16026 -66
- package/dist/python/python.js +2 -0
- package/dist/python/python.wasm +0 -0
- package/dist/python/python.worker.js +87 -0
- package/dist/python/runtime.json +34 -0
- package/dist/python-abi.cjs +2892 -0
- package/dist/python-abi.cjs.map +1 -0
- package/dist/python-abi.d.cts +645 -0
- package/dist/python-abi.d.ts +645 -0
- package/dist/python-abi.js +2839 -0
- package/dist/python-abi.js.map +1 -0
- package/dist/python-worker.js +865 -0
- package/dist/python-worker.js.map +1 -0
- package/dist/rolldown-wasi-worker.js +5 -1
- package/dist/rolldown-wasi-worker.js.map +1 -1
- package/dist/vfs-DzEcPbMY.d.cts +427 -0
- package/dist/vfs-DzEcPbMY.d.ts +427 -0
- package/package.json +8 -3
|
@@ -0,0 +1,2892 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/runtime/python/host-abi.ts
|
|
4
|
+
var SBX_HOST_ABI_VERSION = 1;
|
|
5
|
+
var SBX_REQUEST_HEADER_BYTES = 16;
|
|
6
|
+
var SBX_RESPONSE_HEADER_BYTES = 20;
|
|
7
|
+
var Op = {
|
|
8
|
+
handshake: 0,
|
|
9
|
+
openat: 1,
|
|
10
|
+
close: 2,
|
|
11
|
+
read: 3,
|
|
12
|
+
write: 4,
|
|
13
|
+
pread: 5,
|
|
14
|
+
pwrite: 6,
|
|
15
|
+
lseek: 7,
|
|
16
|
+
fstat: 8,
|
|
17
|
+
statat: 9,
|
|
18
|
+
ftruncate: 10,
|
|
19
|
+
renameat: 11,
|
|
20
|
+
unlinkat: 12,
|
|
21
|
+
mkdirat: 13,
|
|
22
|
+
readlinkat: 14,
|
|
23
|
+
symlinkat: 15,
|
|
24
|
+
getdents: 16,
|
|
25
|
+
fsync: 17,
|
|
26
|
+
dup: 256,
|
|
27
|
+
dup2: 257,
|
|
28
|
+
get_flags: 258,
|
|
29
|
+
set_flags: 259,
|
|
30
|
+
pipe: 512,
|
|
31
|
+
poll: 768,
|
|
32
|
+
clock_gettime: 1024,
|
|
33
|
+
sleep: 1025,
|
|
34
|
+
getpid: 1280,
|
|
35
|
+
getcwd: 1281,
|
|
36
|
+
chdir: 1282,
|
|
37
|
+
environ: 1283,
|
|
38
|
+
getrandom: 1536
|
|
39
|
+
};
|
|
40
|
+
var Errno = {
|
|
41
|
+
EPERM: 1,
|
|
42
|
+
ENOENT: 2,
|
|
43
|
+
ESRCH: 3,
|
|
44
|
+
EINTR: 4,
|
|
45
|
+
EIO: 5,
|
|
46
|
+
ENXIO: 6,
|
|
47
|
+
E2BIG: 7,
|
|
48
|
+
EBADF: 9,
|
|
49
|
+
EAGAIN: 11,
|
|
50
|
+
ENOMEM: 12,
|
|
51
|
+
EACCES: 13,
|
|
52
|
+
EFAULT: 14,
|
|
53
|
+
EBUSY: 16,
|
|
54
|
+
EEXIST: 17,
|
|
55
|
+
EXDEV: 18,
|
|
56
|
+
ENODEV: 19,
|
|
57
|
+
ENOTDIR: 20,
|
|
58
|
+
EISDIR: 21,
|
|
59
|
+
EINVAL: 22,
|
|
60
|
+
ENFILE: 23,
|
|
61
|
+
EMFILE: 24,
|
|
62
|
+
ENOTTY: 25,
|
|
63
|
+
EFBIG: 27,
|
|
64
|
+
ENOSPC: 28,
|
|
65
|
+
ESPIPE: 29,
|
|
66
|
+
EROFS: 30,
|
|
67
|
+
EMLINK: 31,
|
|
68
|
+
EPIPE: 32,
|
|
69
|
+
ERANGE: 34,
|
|
70
|
+
ENAMETOOLONG: 36,
|
|
71
|
+
ENOSYS: 38,
|
|
72
|
+
ENOTEMPTY: 39,
|
|
73
|
+
ELOOP: 40,
|
|
74
|
+
ENODATA: 61,
|
|
75
|
+
EPROTO: 71,
|
|
76
|
+
EOVERFLOW: 75,
|
|
77
|
+
ETIMEDOUT: 110,
|
|
78
|
+
ECANCELED: 125,
|
|
79
|
+
EDQUOT: 122
|
|
80
|
+
};
|
|
81
|
+
var CAPABILITIES = ["files", "descriptors", "pipes", "readiness", "time", "identity", "entropy", "sockets", "processes", "signals", "storage", "services", "threads"];
|
|
82
|
+
var OP_NAMES = Object.fromEntries(
|
|
83
|
+
Object.entries(Op).map(([name, code]) => [code, name])
|
|
84
|
+
);
|
|
85
|
+
var ERRNO_NAMES = Object.fromEntries(
|
|
86
|
+
Object.entries(Errno).map(([name, code]) => [code, name])
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// src/runtime/python/protocol.ts
|
|
90
|
+
var ProtocolError = class extends Error {
|
|
91
|
+
code = "ERR_SBX_ABI_PROTOCOL";
|
|
92
|
+
};
|
|
93
|
+
function encodeRequest(header, payload) {
|
|
94
|
+
const frame = new Uint8Array(SBX_REQUEST_HEADER_BYTES + payload.length);
|
|
95
|
+
const view = new DataView(frame.buffer);
|
|
96
|
+
view.setUint16(0, header.version, true);
|
|
97
|
+
view.setUint16(2, header.op, true);
|
|
98
|
+
view.setUint32(4, header.requestId, true);
|
|
99
|
+
view.setUint32(8, header.generation, true);
|
|
100
|
+
view.setUint32(12, payload.length, true);
|
|
101
|
+
frame.set(payload, SBX_REQUEST_HEADER_BYTES);
|
|
102
|
+
return frame;
|
|
103
|
+
}
|
|
104
|
+
function decodeRequest(frame) {
|
|
105
|
+
if (frame.length < SBX_REQUEST_HEADER_BYTES) throw new ProtocolError("short request frame");
|
|
106
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
107
|
+
const header = {
|
|
108
|
+
version: view.getUint16(0, true),
|
|
109
|
+
op: view.getUint16(2, true),
|
|
110
|
+
requestId: view.getUint32(4, true),
|
|
111
|
+
generation: view.getUint32(8, true)
|
|
112
|
+
};
|
|
113
|
+
const length = view.getUint32(12, true);
|
|
114
|
+
if (frame.length - SBX_REQUEST_HEADER_BYTES < length) {
|
|
115
|
+
throw new ProtocolError(`truncated payload for ${OP_NAMES[header.op] ?? header.op}`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
header,
|
|
119
|
+
payload: frame.subarray(SBX_REQUEST_HEADER_BYTES, SBX_REQUEST_HEADER_BYTES + length)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function encodeResponse(header, status, payload) {
|
|
123
|
+
const frame = new Uint8Array(SBX_RESPONSE_HEADER_BYTES + payload.length);
|
|
124
|
+
const view = new DataView(frame.buffer);
|
|
125
|
+
view.setUint16(0, header.version, true);
|
|
126
|
+
view.setUint16(2, header.op, true);
|
|
127
|
+
view.setUint32(4, header.requestId, true);
|
|
128
|
+
view.setUint32(8, header.generation, true);
|
|
129
|
+
view.setInt32(12, status, true);
|
|
130
|
+
view.setUint32(16, payload.length, true);
|
|
131
|
+
frame.set(payload, SBX_RESPONSE_HEADER_BYTES);
|
|
132
|
+
return frame;
|
|
133
|
+
}
|
|
134
|
+
function decodeResponse(frame, expected) {
|
|
135
|
+
if (frame.length < SBX_RESPONSE_HEADER_BYTES) {
|
|
136
|
+
throw new ProtocolError("the host ABI transport returned an incomplete frame");
|
|
137
|
+
}
|
|
138
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
139
|
+
const header = {
|
|
140
|
+
version: view.getUint16(0, true),
|
|
141
|
+
op: view.getUint16(2, true),
|
|
142
|
+
requestId: view.getUint32(4, true),
|
|
143
|
+
generation: view.getUint32(8, true)
|
|
144
|
+
};
|
|
145
|
+
const status = view.getInt32(12, true);
|
|
146
|
+
const length = view.getUint32(16, true);
|
|
147
|
+
if (frame.length - SBX_RESPONSE_HEADER_BYTES < length) throw new ProtocolError("truncated response payload");
|
|
148
|
+
if (expected) {
|
|
149
|
+
if (header.requestId !== expected.requestId) throw new ProtocolError("stale response: request id mismatch");
|
|
150
|
+
if (header.generation !== expected.generation) throw new ProtocolError("stale response: process generation mismatch");
|
|
151
|
+
if (header.op !== expected.op) throw new ProtocolError("response answers a different operation");
|
|
152
|
+
}
|
|
153
|
+
if (header.version !== SBX_HOST_ABI_VERSION) {
|
|
154
|
+
throw new ProtocolError(`host ABI version ${header.version}, expected ${SBX_HOST_ABI_VERSION}`);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
header,
|
|
158
|
+
status,
|
|
159
|
+
payload: frame.subarray(SBX_RESPONSE_HEADER_BYTES, SBX_RESPONSE_HEADER_BYTES + length)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
var Reader = class {
|
|
163
|
+
at = 0;
|
|
164
|
+
view;
|
|
165
|
+
bytes;
|
|
166
|
+
constructor(bytes) {
|
|
167
|
+
this.bytes = bytes;
|
|
168
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
169
|
+
}
|
|
170
|
+
need(count) {
|
|
171
|
+
if (this.at + count > this.bytes.length) throw new ProtocolError("payload underrun");
|
|
172
|
+
const offset = this.at;
|
|
173
|
+
this.at += count;
|
|
174
|
+
return offset;
|
|
175
|
+
}
|
|
176
|
+
i32() {
|
|
177
|
+
return this.view.getInt32(this.need(4), true);
|
|
178
|
+
}
|
|
179
|
+
u32() {
|
|
180
|
+
return this.view.getUint32(this.need(4), true);
|
|
181
|
+
}
|
|
182
|
+
i64() {
|
|
183
|
+
return Number(this.view.getBigInt64(this.need(8), true));
|
|
184
|
+
}
|
|
185
|
+
u64() {
|
|
186
|
+
return Number(this.view.getBigUint64(this.need(8), true));
|
|
187
|
+
}
|
|
188
|
+
bytes32() {
|
|
189
|
+
const length = this.u32();
|
|
190
|
+
const offset = this.need(length);
|
|
191
|
+
return this.bytes.subarray(offset, offset + length);
|
|
192
|
+
}
|
|
193
|
+
string() {
|
|
194
|
+
return new TextDecoder().decode(this.bytes32());
|
|
195
|
+
}
|
|
196
|
+
get remaining() {
|
|
197
|
+
return this.bytes.length - this.at;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var Writer = class {
|
|
201
|
+
parts = [];
|
|
202
|
+
i32(value) {
|
|
203
|
+
const b = new Uint8Array(4);
|
|
204
|
+
new DataView(b.buffer).setInt32(0, value, true);
|
|
205
|
+
this.parts.push(b);
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
u32(value) {
|
|
209
|
+
const b = new Uint8Array(4);
|
|
210
|
+
new DataView(b.buffer).setUint32(0, value >>> 0, true);
|
|
211
|
+
this.parts.push(b);
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
i64(value) {
|
|
215
|
+
const b = new Uint8Array(8);
|
|
216
|
+
new DataView(b.buffer).setBigInt64(0, BigInt(Math.trunc(value)), true);
|
|
217
|
+
this.parts.push(b);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
u64(value) {
|
|
221
|
+
const b = new Uint8Array(8);
|
|
222
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(Math.trunc(value)), true);
|
|
223
|
+
this.parts.push(b);
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
bytes32(value) {
|
|
227
|
+
this.u32(value.length);
|
|
228
|
+
this.parts.push(value);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
string(value) {
|
|
232
|
+
return this.bytes32(new TextEncoder().encode(value));
|
|
233
|
+
}
|
|
234
|
+
finish() {
|
|
235
|
+
const total = this.parts.reduce((sum, part) => sum + part.length, 0);
|
|
236
|
+
const out = new Uint8Array(total);
|
|
237
|
+
let at = 0;
|
|
238
|
+
for (const part of this.parts) {
|
|
239
|
+
out.set(part, at);
|
|
240
|
+
at += part.length;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function failure(errno) {
|
|
246
|
+
return -Math.abs(errno);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/runtime/python/sync-transport.ts
|
|
250
|
+
var STATE = 0;
|
|
251
|
+
var LENGTH = 1;
|
|
252
|
+
var MORE = 2;
|
|
253
|
+
var CONTROL_WORDS = 4;
|
|
254
|
+
var STATE_IDLE = 0;
|
|
255
|
+
var STATE_REQUEST = 1;
|
|
256
|
+
var STATE_RESPONSE = 2;
|
|
257
|
+
var STATE_CONTINUE = 3;
|
|
258
|
+
var STATE_CLOSED = 4;
|
|
259
|
+
var WAIT_SLICE_MS = 200;
|
|
260
|
+
function createHostTransportBuffers(windowBytes = 1 << 20) {
|
|
261
|
+
return {
|
|
262
|
+
control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
|
|
263
|
+
data: new SharedArrayBuffer(windowBytes)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
var HostTransportError = class extends Error {
|
|
267
|
+
code = "ERR_SBX_HOST_TRANSPORT";
|
|
268
|
+
};
|
|
269
|
+
var HostCallClient = class {
|
|
270
|
+
control;
|
|
271
|
+
data;
|
|
272
|
+
capacity;
|
|
273
|
+
wake;
|
|
274
|
+
closed = false;
|
|
275
|
+
constructor(buffers, wake) {
|
|
276
|
+
this.control = new Int32Array(buffers.control);
|
|
277
|
+
this.data = new Uint8Array(buffers.data);
|
|
278
|
+
this.capacity = this.data.length;
|
|
279
|
+
this.wake = wake;
|
|
280
|
+
}
|
|
281
|
+
/** Send a frame and block until the whole answer is back. Never returns empty. */
|
|
282
|
+
call(request) {
|
|
283
|
+
if (this.closed || Atomics.load(this.control, STATE) === STATE_CLOSED) {
|
|
284
|
+
this.closed = true;
|
|
285
|
+
throw new HostTransportError("the host transport is closed");
|
|
286
|
+
}
|
|
287
|
+
this.send(request);
|
|
288
|
+
const response = this.receive();
|
|
289
|
+
if (response.length === 0) {
|
|
290
|
+
throw new HostTransportError("the host answered with no frame; the kernel side failed or went away");
|
|
291
|
+
}
|
|
292
|
+
return response;
|
|
293
|
+
}
|
|
294
|
+
send(request) {
|
|
295
|
+
let offset = 0;
|
|
296
|
+
for (; ; ) {
|
|
297
|
+
const size = Math.min(this.capacity, request.length - offset);
|
|
298
|
+
this.data.set(request.subarray(offset, offset + size), 0);
|
|
299
|
+
offset += size;
|
|
300
|
+
Atomics.store(this.control, LENGTH, size);
|
|
301
|
+
Atomics.store(this.control, MORE, offset < request.length ? 1 : 0);
|
|
302
|
+
this.publish(STATE_REQUEST);
|
|
303
|
+
const next = this.waitWhile(STATE_REQUEST);
|
|
304
|
+
if (next === STATE_RESPONSE) return;
|
|
305
|
+
if (next !== STATE_CONTINUE) {
|
|
306
|
+
this.closed = true;
|
|
307
|
+
throw new HostTransportError("the host went away mid-request");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
receive() {
|
|
312
|
+
const parts = [];
|
|
313
|
+
let total = 0;
|
|
314
|
+
for (; ; ) {
|
|
315
|
+
const size = Atomics.load(this.control, LENGTH);
|
|
316
|
+
parts.push(this.data.slice(0, size));
|
|
317
|
+
total += size;
|
|
318
|
+
if (Atomics.load(this.control, MORE) === 0) break;
|
|
319
|
+
this.publish(STATE_CONTINUE);
|
|
320
|
+
if (this.waitWhile(STATE_CONTINUE) !== STATE_RESPONSE) {
|
|
321
|
+
this.closed = true;
|
|
322
|
+
throw new HostTransportError("the host went away mid-response");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
Atomics.store(this.control, STATE, STATE_IDLE);
|
|
326
|
+
if (parts.length === 1) return parts[0];
|
|
327
|
+
const joined = new Uint8Array(total);
|
|
328
|
+
let at = 0;
|
|
329
|
+
for (const part of parts) {
|
|
330
|
+
joined.set(part, at);
|
|
331
|
+
at += part.length;
|
|
332
|
+
}
|
|
333
|
+
return joined;
|
|
334
|
+
}
|
|
335
|
+
publish(state) {
|
|
336
|
+
Atomics.store(this.control, STATE, state);
|
|
337
|
+
Atomics.notify(this.control, STATE);
|
|
338
|
+
this.wake();
|
|
339
|
+
}
|
|
340
|
+
waitWhile(state) {
|
|
341
|
+
for (; ; ) {
|
|
342
|
+
Atomics.wait(this.control, STATE, state, WAIT_SLICE_MS);
|
|
343
|
+
const current = Atomics.load(this.control, STATE);
|
|
344
|
+
if (current !== state) return current;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var HostCallServer = class {
|
|
349
|
+
control;
|
|
350
|
+
data;
|
|
351
|
+
capacity;
|
|
352
|
+
handle;
|
|
353
|
+
onFault;
|
|
354
|
+
incoming = [];
|
|
355
|
+
outgoing = null;
|
|
356
|
+
sent = 0;
|
|
357
|
+
closed = false;
|
|
358
|
+
constructor(buffers, handle, onFault = () => {
|
|
359
|
+
}) {
|
|
360
|
+
this.control = new Int32Array(buffers.control);
|
|
361
|
+
this.data = new Uint8Array(buffers.data);
|
|
362
|
+
this.capacity = this.data.length;
|
|
363
|
+
this.handle = handle;
|
|
364
|
+
this.onFault = onFault;
|
|
365
|
+
}
|
|
366
|
+
/** Call on each wake message from the client. */
|
|
367
|
+
async pump() {
|
|
368
|
+
if (this.closed) return;
|
|
369
|
+
const state = Atomics.load(this.control, STATE);
|
|
370
|
+
if (state === STATE_REQUEST) {
|
|
371
|
+
const size = Atomics.load(this.control, LENGTH);
|
|
372
|
+
this.incoming.push(this.data.slice(0, size));
|
|
373
|
+
if (Atomics.load(this.control, MORE) === 1) {
|
|
374
|
+
this.publish(STATE_CONTINUE);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const request = concat(this.incoming);
|
|
378
|
+
this.incoming = [];
|
|
379
|
+
let response;
|
|
380
|
+
try {
|
|
381
|
+
response = await this.handle(request);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.onFault(error);
|
|
384
|
+
this.close();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (this.closed) return;
|
|
388
|
+
this.outgoing = response;
|
|
389
|
+
this.sent = 0;
|
|
390
|
+
this.sendWindow();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (state === STATE_CONTINUE) this.sendWindow();
|
|
394
|
+
}
|
|
395
|
+
/** Release a blocked client — container teardown, or the worker being killed. */
|
|
396
|
+
close() {
|
|
397
|
+
if (this.closed) return;
|
|
398
|
+
this.closed = true;
|
|
399
|
+
Atomics.store(this.control, STATE, STATE_CLOSED);
|
|
400
|
+
Atomics.notify(this.control, STATE);
|
|
401
|
+
}
|
|
402
|
+
sendWindow() {
|
|
403
|
+
const payload = this.outgoing ?? new Uint8Array();
|
|
404
|
+
const size = Math.min(this.capacity, payload.length - this.sent);
|
|
405
|
+
this.data.set(payload.subarray(this.sent, this.sent + size), 0);
|
|
406
|
+
this.sent += size;
|
|
407
|
+
Atomics.store(this.control, LENGTH, size);
|
|
408
|
+
Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
|
|
409
|
+
this.publish(STATE_RESPONSE);
|
|
410
|
+
}
|
|
411
|
+
publish(state) {
|
|
412
|
+
Atomics.store(this.control, STATE, state);
|
|
413
|
+
Atomics.notify(this.control, STATE);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
function concat(parts) {
|
|
417
|
+
if (parts.length === 1) return parts[0];
|
|
418
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
419
|
+
const joined = new Uint8Array(total);
|
|
420
|
+
let at = 0;
|
|
421
|
+
for (const part of parts) {
|
|
422
|
+
joined.set(part, at);
|
|
423
|
+
at += part.length;
|
|
424
|
+
}
|
|
425
|
+
return joined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/kernel/open-file.ts
|
|
429
|
+
var PosixError = class extends Error {
|
|
430
|
+
constructor(errno, message) {
|
|
431
|
+
super(message ?? `errno ${errno}`);
|
|
432
|
+
this.errno = errno;
|
|
433
|
+
this.name = "PosixError";
|
|
434
|
+
}
|
|
435
|
+
errno;
|
|
436
|
+
};
|
|
437
|
+
var O_RDONLY = 0;
|
|
438
|
+
var O_WRONLY = 1;
|
|
439
|
+
var O_RDWR = 2;
|
|
440
|
+
var O_CREAT = 64;
|
|
441
|
+
var O_EXCL = 128;
|
|
442
|
+
var O_TRUNC = 512;
|
|
443
|
+
var O_APPEND = 1024;
|
|
444
|
+
var O_NONBLOCK = 2048;
|
|
445
|
+
var O_DIRECTORY = 65536;
|
|
446
|
+
var O_CLOEXEC = 524288;
|
|
447
|
+
function accessMode(flags) {
|
|
448
|
+
return flags & 3;
|
|
449
|
+
}
|
|
450
|
+
var SEEK_SET = 0;
|
|
451
|
+
var SEEK_CUR = 1;
|
|
452
|
+
var SEEK_END = 2;
|
|
453
|
+
var Inode = class {
|
|
454
|
+
constructor(ino, path, vfs, cred) {
|
|
455
|
+
this.ino = ino;
|
|
456
|
+
this.path = path;
|
|
457
|
+
this.vfs = vfs;
|
|
458
|
+
this.cred = cred;
|
|
459
|
+
}
|
|
460
|
+
ino;
|
|
461
|
+
path;
|
|
462
|
+
vfs;
|
|
463
|
+
cred;
|
|
464
|
+
detached = null;
|
|
465
|
+
refs = 0;
|
|
466
|
+
/** Read the whole file, wherever it currently lives. */
|
|
467
|
+
contents() {
|
|
468
|
+
if (this.detached) return this.detached;
|
|
469
|
+
try {
|
|
470
|
+
return this.vfs.readFile(this.path, this.cred);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw toPosixError(error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
replace(data) {
|
|
476
|
+
if (this.detached) {
|
|
477
|
+
this.detached = data;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
this.vfs.writeFile(this.path, data, { cred: this.cred });
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw toPosixError(error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Called when the last name is about to be removed. */
|
|
487
|
+
detach() {
|
|
488
|
+
if (this.detached) return;
|
|
489
|
+
try {
|
|
490
|
+
this.detached = this.vfs.readFile(this.path, this.cred);
|
|
491
|
+
} catch {
|
|
492
|
+
this.detached = new Uint8Array();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
size() {
|
|
496
|
+
return this.contents().length;
|
|
497
|
+
}
|
|
498
|
+
stat() {
|
|
499
|
+
if (this.detached) {
|
|
500
|
+
return {
|
|
501
|
+
ino: this.ino,
|
|
502
|
+
mode: 33188,
|
|
503
|
+
size: this.detached.length,
|
|
504
|
+
uid: this.cred.uid,
|
|
505
|
+
gid: this.cred.gid,
|
|
506
|
+
nlink: 0,
|
|
507
|
+
atimeMs: Date.now(),
|
|
508
|
+
mtimeMs: Date.now(),
|
|
509
|
+
ctimeMs: Date.now()
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const st = this.vfs.stat(this.path, { cred: this.cred });
|
|
514
|
+
return {
|
|
515
|
+
ino: st.ino,
|
|
516
|
+
mode: st.mode,
|
|
517
|
+
size: st.size,
|
|
518
|
+
uid: st.uid,
|
|
519
|
+
gid: st.gid,
|
|
520
|
+
nlink: st.nlink ?? 1,
|
|
521
|
+
atimeMs: st.atimeMs,
|
|
522
|
+
mtimeMs: st.mtimeMs,
|
|
523
|
+
ctimeMs: st.ctimeMs
|
|
524
|
+
};
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw toPosixError(error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
var FileDescription = class {
|
|
531
|
+
constructor(inode, flags) {
|
|
532
|
+
this.inode = inode;
|
|
533
|
+
this.flags = flags;
|
|
534
|
+
inode.refs += 1;
|
|
535
|
+
}
|
|
536
|
+
inode;
|
|
537
|
+
flags;
|
|
538
|
+
kind = "file";
|
|
539
|
+
offset = 0;
|
|
540
|
+
read(length) {
|
|
541
|
+
if (accessMode(this.flags) === O_WRONLY) throw new PosixError(Errno.EBADF);
|
|
542
|
+
const data = this.inode.contents();
|
|
543
|
+
const start = Math.min(this.offset, data.length);
|
|
544
|
+
const end = Math.min(start + length, data.length);
|
|
545
|
+
this.offset = end;
|
|
546
|
+
return data.slice(start, end);
|
|
547
|
+
}
|
|
548
|
+
pread(length, offset) {
|
|
549
|
+
if (accessMode(this.flags) === O_WRONLY) throw new PosixError(Errno.EBADF);
|
|
550
|
+
const data = this.inode.contents();
|
|
551
|
+
const start = Math.min(offset, data.length);
|
|
552
|
+
return data.slice(start, Math.min(start + length, data.length));
|
|
553
|
+
}
|
|
554
|
+
write(data) {
|
|
555
|
+
if (accessMode(this.flags) === O_RDONLY) throw new PosixError(Errno.EBADF);
|
|
556
|
+
const at = this.flags & O_APPEND ? this.inode.size() : this.offset;
|
|
557
|
+
const written = this.writeAt(data, at);
|
|
558
|
+
this.offset = at + written;
|
|
559
|
+
return written;
|
|
560
|
+
}
|
|
561
|
+
pwrite(data, offset) {
|
|
562
|
+
if (accessMode(this.flags) === O_RDONLY) throw new PosixError(Errno.EBADF);
|
|
563
|
+
return this.writeAt(data, offset);
|
|
564
|
+
}
|
|
565
|
+
writeAt(data, offset) {
|
|
566
|
+
const current = this.inode.contents();
|
|
567
|
+
const end = offset + data.length;
|
|
568
|
+
const next = new Uint8Array(Math.max(current.length, end));
|
|
569
|
+
next.set(current, 0);
|
|
570
|
+
next.set(data, offset);
|
|
571
|
+
this.inode.replace(next);
|
|
572
|
+
return data.length;
|
|
573
|
+
}
|
|
574
|
+
seek(offset, whence) {
|
|
575
|
+
const base = whence === SEEK_SET ? 0 : whence === SEEK_CUR ? this.offset : this.inode.size();
|
|
576
|
+
const next = base + offset;
|
|
577
|
+
if (next < 0) throw new PosixError(Errno.EINVAL);
|
|
578
|
+
this.offset = next;
|
|
579
|
+
return next;
|
|
580
|
+
}
|
|
581
|
+
truncate(length) {
|
|
582
|
+
const current = this.inode.contents();
|
|
583
|
+
const next = new Uint8Array(length);
|
|
584
|
+
next.set(current.subarray(0, Math.min(length, current.length)), 0);
|
|
585
|
+
this.inode.replace(next);
|
|
586
|
+
}
|
|
587
|
+
stat() {
|
|
588
|
+
return this.inode.stat();
|
|
589
|
+
}
|
|
590
|
+
readable() {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
writable() {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
whenReady() {
|
|
597
|
+
return Promise.resolve();
|
|
598
|
+
}
|
|
599
|
+
close() {
|
|
600
|
+
this.inode.refs -= 1;
|
|
601
|
+
if (this.inode.refs <= 0) this.inode.detached = null;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
var DirectoryDescription = class {
|
|
605
|
+
constructor(path, flags, vfs, cred) {
|
|
606
|
+
this.path = path;
|
|
607
|
+
this.flags = flags;
|
|
608
|
+
this.vfs = vfs;
|
|
609
|
+
this.cred = cred;
|
|
610
|
+
}
|
|
611
|
+
path;
|
|
612
|
+
flags;
|
|
613
|
+
vfs;
|
|
614
|
+
cred;
|
|
615
|
+
kind = "dir";
|
|
616
|
+
entries = null;
|
|
617
|
+
index = 0;
|
|
618
|
+
/** Entry names, NUL-terminated, as many as fit in `length`. */
|
|
619
|
+
read(length) {
|
|
620
|
+
if (!this.entries) {
|
|
621
|
+
try {
|
|
622
|
+
this.entries = [".", "..", ...this.vfs.readdir(this.path, this.cred)];
|
|
623
|
+
} catch (error) {
|
|
624
|
+
throw toPosixError(error);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const encoder3 = new TextEncoder();
|
|
628
|
+
const parts = [];
|
|
629
|
+
let total = 0;
|
|
630
|
+
while (this.index < this.entries.length) {
|
|
631
|
+
const encoded = encoder3.encode(this.entries[this.index] + "\0");
|
|
632
|
+
if (total + encoded.length > length) break;
|
|
633
|
+
parts.push(encoded);
|
|
634
|
+
total += encoded.length;
|
|
635
|
+
this.index += 1;
|
|
636
|
+
}
|
|
637
|
+
const joined = new Uint8Array(total);
|
|
638
|
+
let at = 0;
|
|
639
|
+
for (const part of parts) {
|
|
640
|
+
joined.set(part, at);
|
|
641
|
+
at += part.length;
|
|
642
|
+
}
|
|
643
|
+
return joined;
|
|
644
|
+
}
|
|
645
|
+
write() {
|
|
646
|
+
throw new PosixError(Errno.EISDIR);
|
|
647
|
+
}
|
|
648
|
+
seek(offset, whence) {
|
|
649
|
+
if (whence === SEEK_SET && offset === 0) {
|
|
650
|
+
this.index = 0;
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
throw new PosixError(Errno.EINVAL);
|
|
654
|
+
}
|
|
655
|
+
stat() {
|
|
656
|
+
try {
|
|
657
|
+
const st = this.vfs.stat(this.path, { cred: this.cred });
|
|
658
|
+
return {
|
|
659
|
+
ino: st.ino,
|
|
660
|
+
mode: st.mode,
|
|
661
|
+
size: st.size,
|
|
662
|
+
uid: st.uid,
|
|
663
|
+
gid: st.gid,
|
|
664
|
+
nlink: st.nlink ?? 2,
|
|
665
|
+
atimeMs: st.atimeMs,
|
|
666
|
+
mtimeMs: st.mtimeMs,
|
|
667
|
+
ctimeMs: st.ctimeMs
|
|
668
|
+
};
|
|
669
|
+
} catch (error) {
|
|
670
|
+
throw toPosixError(error);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
readable() {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
writable() {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
whenReady() {
|
|
680
|
+
return Promise.resolve();
|
|
681
|
+
}
|
|
682
|
+
close() {
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
var PIPE_CAPACITY = 64 * 1024;
|
|
686
|
+
var PipeBuffer = class {
|
|
687
|
+
chunks = [];
|
|
688
|
+
queued = 0;
|
|
689
|
+
readers = 0;
|
|
690
|
+
writers = 0;
|
|
691
|
+
waiters = [];
|
|
692
|
+
get available() {
|
|
693
|
+
return this.queued;
|
|
694
|
+
}
|
|
695
|
+
get space() {
|
|
696
|
+
return Math.max(0, PIPE_CAPACITY - this.queued);
|
|
697
|
+
}
|
|
698
|
+
push(data) {
|
|
699
|
+
this.chunks.push(data);
|
|
700
|
+
this.queued += data.length;
|
|
701
|
+
this.wake();
|
|
702
|
+
}
|
|
703
|
+
take(length) {
|
|
704
|
+
const out = new Uint8Array(Math.min(length, this.queued));
|
|
705
|
+
let at = 0;
|
|
706
|
+
while (at < out.length) {
|
|
707
|
+
const head = this.chunks[0];
|
|
708
|
+
const take = Math.min(head.length, out.length - at);
|
|
709
|
+
out.set(head.subarray(0, take), at);
|
|
710
|
+
at += take;
|
|
711
|
+
if (take === head.length) this.chunks.shift();
|
|
712
|
+
else this.chunks[0] = head.subarray(take);
|
|
713
|
+
}
|
|
714
|
+
this.queued -= out.length;
|
|
715
|
+
this.wake();
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
/** Anything that may have changed readiness for either end. */
|
|
719
|
+
wake() {
|
|
720
|
+
const waiting = this.waiters;
|
|
721
|
+
this.waiters = [];
|
|
722
|
+
for (const resolve2 of waiting) resolve2();
|
|
723
|
+
}
|
|
724
|
+
whenReady() {
|
|
725
|
+
return new Promise((resolve2) => this.waiters.push(resolve2));
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
var PipeReadEnd = class {
|
|
729
|
+
constructor(buffer, flags) {
|
|
730
|
+
this.buffer = buffer;
|
|
731
|
+
this.flags = flags;
|
|
732
|
+
buffer.readers += 1;
|
|
733
|
+
}
|
|
734
|
+
buffer;
|
|
735
|
+
flags;
|
|
736
|
+
kind = "pipe";
|
|
737
|
+
read(length) {
|
|
738
|
+
if (this.buffer.available === 0) {
|
|
739
|
+
if (this.buffer.writers === 0) return new Uint8Array();
|
|
740
|
+
throw new PosixError(Errno.EAGAIN);
|
|
741
|
+
}
|
|
742
|
+
return this.buffer.take(length);
|
|
743
|
+
}
|
|
744
|
+
write() {
|
|
745
|
+
throw new PosixError(Errno.EBADF);
|
|
746
|
+
}
|
|
747
|
+
stat() {
|
|
748
|
+
return pipeStat();
|
|
749
|
+
}
|
|
750
|
+
readable() {
|
|
751
|
+
return this.buffer.available > 0 || this.buffer.writers === 0;
|
|
752
|
+
}
|
|
753
|
+
writable() {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
whenReady() {
|
|
757
|
+
return this.buffer.whenReady();
|
|
758
|
+
}
|
|
759
|
+
close() {
|
|
760
|
+
this.buffer.readers -= 1;
|
|
761
|
+
this.buffer.wake();
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
var PipeWriteEnd = class {
|
|
765
|
+
constructor(buffer, flags) {
|
|
766
|
+
this.buffer = buffer;
|
|
767
|
+
this.flags = flags;
|
|
768
|
+
buffer.writers += 1;
|
|
769
|
+
}
|
|
770
|
+
buffer;
|
|
771
|
+
flags;
|
|
772
|
+
kind = "pipe";
|
|
773
|
+
read() {
|
|
774
|
+
throw new PosixError(Errno.EBADF);
|
|
775
|
+
}
|
|
776
|
+
write(data) {
|
|
777
|
+
if (this.buffer.readers === 0) throw new PosixError(Errno.EPIPE);
|
|
778
|
+
const space = this.buffer.space;
|
|
779
|
+
if (space === 0) throw new PosixError(Errno.EAGAIN);
|
|
780
|
+
const accepted = data.subarray(0, space);
|
|
781
|
+
this.buffer.push(accepted.slice());
|
|
782
|
+
return accepted.length;
|
|
783
|
+
}
|
|
784
|
+
stat() {
|
|
785
|
+
return pipeStat();
|
|
786
|
+
}
|
|
787
|
+
readable() {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
writable() {
|
|
791
|
+
return this.buffer.space > 0 || this.buffer.readers === 0;
|
|
792
|
+
}
|
|
793
|
+
whenReady() {
|
|
794
|
+
return this.buffer.whenReady();
|
|
795
|
+
}
|
|
796
|
+
close() {
|
|
797
|
+
this.buffer.writers -= 1;
|
|
798
|
+
this.buffer.wake();
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
function pipeStat() {
|
|
802
|
+
const now = Date.now();
|
|
803
|
+
return { ino: 0, mode: 4480, size: 0, uid: 0, gid: 0, nlink: 1, atimeMs: now, mtimeMs: now, ctimeMs: now };
|
|
804
|
+
}
|
|
805
|
+
function createPipe(flags = 0) {
|
|
806
|
+
const buffer = new PipeBuffer();
|
|
807
|
+
return [new PipeReadEnd(buffer, flags | O_RDONLY), new PipeWriteEnd(buffer, flags | O_WRONLY)];
|
|
808
|
+
}
|
|
809
|
+
var StreamDescription = class {
|
|
810
|
+
constructor(flags, sink, isTty = false) {
|
|
811
|
+
this.flags = flags;
|
|
812
|
+
this.sink = sink;
|
|
813
|
+
this.isTty = isTty;
|
|
814
|
+
}
|
|
815
|
+
flags;
|
|
816
|
+
sink;
|
|
817
|
+
isTty;
|
|
818
|
+
kind = "stream";
|
|
819
|
+
queue = [];
|
|
820
|
+
queued = 0;
|
|
821
|
+
ended = false;
|
|
822
|
+
waiters = [];
|
|
823
|
+
/** Host side: give the guest more input. */
|
|
824
|
+
push(data) {
|
|
825
|
+
if (data.length === 0) return;
|
|
826
|
+
this.queue.push(data);
|
|
827
|
+
this.queued += data.length;
|
|
828
|
+
this.wake();
|
|
829
|
+
}
|
|
830
|
+
/** Host side: no more input will arrive. */
|
|
831
|
+
end() {
|
|
832
|
+
this.ended = true;
|
|
833
|
+
this.wake();
|
|
834
|
+
}
|
|
835
|
+
read(length) {
|
|
836
|
+
if (this.queued === 0) {
|
|
837
|
+
if (this.ended) return new Uint8Array();
|
|
838
|
+
throw new PosixError(Errno.EAGAIN);
|
|
839
|
+
}
|
|
840
|
+
const out = new Uint8Array(Math.min(length, this.queued));
|
|
841
|
+
let at = 0;
|
|
842
|
+
while (at < out.length) {
|
|
843
|
+
const head = this.queue[0];
|
|
844
|
+
const take = Math.min(head.length, out.length - at);
|
|
845
|
+
out.set(head.subarray(0, take), at);
|
|
846
|
+
at += take;
|
|
847
|
+
if (take === head.length) this.queue.shift();
|
|
848
|
+
else this.queue[0] = head.subarray(take);
|
|
849
|
+
}
|
|
850
|
+
this.queued -= out.length;
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
write(data) {
|
|
854
|
+
if (!this.sink) throw new PosixError(Errno.EBADF);
|
|
855
|
+
this.sink(data);
|
|
856
|
+
return data.length;
|
|
857
|
+
}
|
|
858
|
+
stat() {
|
|
859
|
+
const now = Date.now();
|
|
860
|
+
return {
|
|
861
|
+
ino: 0,
|
|
862
|
+
mode: this.isTty ? 8592 : 4480,
|
|
863
|
+
size: 0,
|
|
864
|
+
uid: 0,
|
|
865
|
+
gid: 0,
|
|
866
|
+
nlink: 1,
|
|
867
|
+
atimeMs: now,
|
|
868
|
+
mtimeMs: now,
|
|
869
|
+
ctimeMs: now
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
readable() {
|
|
873
|
+
return this.queued > 0 || this.ended;
|
|
874
|
+
}
|
|
875
|
+
writable() {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
whenReady() {
|
|
879
|
+
return new Promise((resolve2) => this.waiters.push(resolve2));
|
|
880
|
+
}
|
|
881
|
+
close() {
|
|
882
|
+
this.wake();
|
|
883
|
+
}
|
|
884
|
+
wake() {
|
|
885
|
+
const waiting = this.waiters;
|
|
886
|
+
this.waiters = [];
|
|
887
|
+
for (const resolve2 of waiting) resolve2();
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
function toPosixError(error) {
|
|
891
|
+
if (error instanceof PosixError) return error;
|
|
892
|
+
const code = error?.code;
|
|
893
|
+
const message = error?.message;
|
|
894
|
+
const mapped = {
|
|
895
|
+
ENOENT: Errno.ENOENT,
|
|
896
|
+
EEXIST: Errno.EEXIST,
|
|
897
|
+
ENOTDIR: Errno.ENOTDIR,
|
|
898
|
+
EISDIR: Errno.EISDIR,
|
|
899
|
+
EACCES: Errno.EACCES,
|
|
900
|
+
EPERM: Errno.EPERM,
|
|
901
|
+
ENOTEMPTY: Errno.ENOTEMPTY,
|
|
902
|
+
EINVAL: Errno.EINVAL,
|
|
903
|
+
ELOOP: Errno.ELOOP,
|
|
904
|
+
EXDEV: Errno.EXDEV,
|
|
905
|
+
ENOSPC: Errno.ENOSPC,
|
|
906
|
+
EMFILE: Errno.EMFILE,
|
|
907
|
+
ENAMETOOLONG: Errno.ENAMETOOLONG,
|
|
908
|
+
EBADF: Errno.EBADF,
|
|
909
|
+
EPIPE: Errno.EPIPE
|
|
910
|
+
};
|
|
911
|
+
return new PosixError(code && mapped[code] ? mapped[code] : Errno.EIO, message);
|
|
912
|
+
}
|
|
913
|
+
var DescriptorTable = class _DescriptorTable {
|
|
914
|
+
slots = /* @__PURE__ */ new Map();
|
|
915
|
+
/** Descriptions this table shares with others, so close counts correctly. */
|
|
916
|
+
static refs = /* @__PURE__ */ new WeakMap();
|
|
917
|
+
/** Install at a chosen number, closing whatever was there. */
|
|
918
|
+
set(fd, description, cloexec = false) {
|
|
919
|
+
if (fd < 0) throw new PosixError(Errno.EBADF);
|
|
920
|
+
const existing = this.slots.get(fd);
|
|
921
|
+
if (existing) this.release(existing.description);
|
|
922
|
+
this.slots.set(fd, { description, cloexec });
|
|
923
|
+
_DescriptorTable.refs.set(description, (_DescriptorTable.refs.get(description) ?? 0) + 1);
|
|
924
|
+
return fd;
|
|
925
|
+
}
|
|
926
|
+
/** Install at the lowest free number at or above `from`. */
|
|
927
|
+
add(description, cloexec = false, from = 0) {
|
|
928
|
+
let fd = from;
|
|
929
|
+
while (this.slots.has(fd)) fd += 1;
|
|
930
|
+
return this.set(fd, description, cloexec);
|
|
931
|
+
}
|
|
932
|
+
get(fd) {
|
|
933
|
+
const slot = this.slots.get(fd);
|
|
934
|
+
if (!slot) throw new PosixError(Errno.EBADF);
|
|
935
|
+
return slot.description;
|
|
936
|
+
}
|
|
937
|
+
has(fd) {
|
|
938
|
+
return this.slots.has(fd);
|
|
939
|
+
}
|
|
940
|
+
cloexec(fd) {
|
|
941
|
+
const slot = this.slots.get(fd);
|
|
942
|
+
if (!slot) throw new PosixError(Errno.EBADF);
|
|
943
|
+
return slot.cloexec;
|
|
944
|
+
}
|
|
945
|
+
setCloexec(fd, value) {
|
|
946
|
+
const slot = this.slots.get(fd);
|
|
947
|
+
if (!slot) throw new PosixError(Errno.EBADF);
|
|
948
|
+
slot.cloexec = value;
|
|
949
|
+
}
|
|
950
|
+
/** `dup`: a second name for one description, so the offset is shared. */
|
|
951
|
+
dup(fd, from = 0) {
|
|
952
|
+
return this.add(this.get(fd), false, from);
|
|
953
|
+
}
|
|
954
|
+
dup2(fd, target) {
|
|
955
|
+
const description = this.get(fd);
|
|
956
|
+
if (fd === target) return target;
|
|
957
|
+
return this.set(target, description, false);
|
|
958
|
+
}
|
|
959
|
+
close(fd) {
|
|
960
|
+
const slot = this.slots.get(fd);
|
|
961
|
+
if (!slot) throw new PosixError(Errno.EBADF);
|
|
962
|
+
this.slots.delete(fd);
|
|
963
|
+
this.release(slot.description);
|
|
964
|
+
}
|
|
965
|
+
/** Everything a child inherits: a spawn keeps all but the close-on-exec ones. */
|
|
966
|
+
inherit() {
|
|
967
|
+
const child = new _DescriptorTable();
|
|
968
|
+
for (const [fd, slot] of this.slots) {
|
|
969
|
+
if (slot.cloexec) continue;
|
|
970
|
+
child.set(fd, slot.description, false);
|
|
971
|
+
}
|
|
972
|
+
return child;
|
|
973
|
+
}
|
|
974
|
+
/** Drop every descriptor. Every resource has an owner and a cleanup path. */
|
|
975
|
+
closeAll() {
|
|
976
|
+
for (const fd of [...this.slots.keys()]) {
|
|
977
|
+
try {
|
|
978
|
+
this.close(fd);
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
get openCount() {
|
|
984
|
+
return this.slots.size;
|
|
985
|
+
}
|
|
986
|
+
release(description) {
|
|
987
|
+
const refs = (_DescriptorTable.refs.get(description) ?? 1) - 1;
|
|
988
|
+
if (refs <= 0) {
|
|
989
|
+
_DescriptorTable.refs.delete(description);
|
|
990
|
+
description.close();
|
|
991
|
+
} else {
|
|
992
|
+
_DescriptorTable.refs.set(description, refs);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
var FileService = class {
|
|
997
|
+
constructor(vfs, cred) {
|
|
998
|
+
this.vfs = vfs;
|
|
999
|
+
this.cred = cred;
|
|
1000
|
+
}
|
|
1001
|
+
vfs;
|
|
1002
|
+
cred;
|
|
1003
|
+
openInodes = /* @__PURE__ */ new Map();
|
|
1004
|
+
open(path, flags, mode = 438) {
|
|
1005
|
+
const exists = this.vfs.lexists(path);
|
|
1006
|
+
if (flags & O_CREAT && flags & O_EXCL && exists) throw new PosixError(Errno.EEXIST);
|
|
1007
|
+
if (!exists) {
|
|
1008
|
+
if (!(flags & O_CREAT)) throw new PosixError(Errno.ENOENT);
|
|
1009
|
+
if (accessMode(flags) === O_RDONLY) throw new PosixError(Errno.EACCES);
|
|
1010
|
+
try {
|
|
1011
|
+
this.vfs.writeFile(path, new Uint8Array(), { cred: this.cred, mode });
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
throw toPosixError(error);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
let stat;
|
|
1017
|
+
try {
|
|
1018
|
+
stat = this.vfs.stat(path, { cred: this.cred });
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
throw toPosixError(error);
|
|
1021
|
+
}
|
|
1022
|
+
if (stat.isDirectory()) {
|
|
1023
|
+
if (accessMode(flags) !== O_RDONLY) throw new PosixError(Errno.EISDIR);
|
|
1024
|
+
return new DirectoryDescription(path, flags, this.vfs, this.cred);
|
|
1025
|
+
}
|
|
1026
|
+
if (flags & O_DIRECTORY) throw new PosixError(Errno.ENOTDIR);
|
|
1027
|
+
const inode = this.inodeFor(stat.ino, path);
|
|
1028
|
+
const description = new FileDescription(inode, flags);
|
|
1029
|
+
if (flags & O_TRUNC && accessMode(flags) !== O_RDONLY) description.truncate(0);
|
|
1030
|
+
return description;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Remove a name.
|
|
1034
|
+
*
|
|
1035
|
+
* If something still has the file open, its contents move into the open
|
|
1036
|
+
* description first: POSIX guarantees a reader keeps reading, and a volume
|
|
1037
|
+
* that only knows about paths cannot provide that on its own.
|
|
1038
|
+
*/
|
|
1039
|
+
unlink(path) {
|
|
1040
|
+
let ino = null;
|
|
1041
|
+
try {
|
|
1042
|
+
ino = this.vfs.lstat(path).ino;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
throw toPosixError(error);
|
|
1045
|
+
}
|
|
1046
|
+
const inode = ino === null ? void 0 : this.openInodes.get(ino);
|
|
1047
|
+
if (inode && inode.refs > 0 && inode.path === path) inode.detach();
|
|
1048
|
+
try {
|
|
1049
|
+
this.vfs.unlink(path, this.cred);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
throw toPosixError(error);
|
|
1052
|
+
}
|
|
1053
|
+
if (inode && inode.refs <= 0) this.openInodes.delete(ino);
|
|
1054
|
+
}
|
|
1055
|
+
/** Rename a name. Anything already open follows the file to its new name. */
|
|
1056
|
+
rename(from, to) {
|
|
1057
|
+
let ino = null;
|
|
1058
|
+
try {
|
|
1059
|
+
ino = this.vfs.lstat(from).ino;
|
|
1060
|
+
} catch {
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
this.vfs.rename(from, to, this.cred);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
throw toPosixError(error);
|
|
1066
|
+
}
|
|
1067
|
+
if (ino === null) return;
|
|
1068
|
+
const inode = this.openInodes.get(ino);
|
|
1069
|
+
if (inode) inode.path = to;
|
|
1070
|
+
}
|
|
1071
|
+
/** One {@link Inode} per file identity, shared by every open description. */
|
|
1072
|
+
inodeFor(ino, path) {
|
|
1073
|
+
const existing = this.openInodes.get(ino);
|
|
1074
|
+
if (existing) {
|
|
1075
|
+
existing.path = path;
|
|
1076
|
+
return existing;
|
|
1077
|
+
}
|
|
1078
|
+
const created = new Inode(ino, path, this.vfs, this.cred);
|
|
1079
|
+
this.openInodes.set(ino, created);
|
|
1080
|
+
return created;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// src/runtime/python/syscall-server.ts
|
|
1085
|
+
var MAX_TRANSFER = 8 * 1024 * 1024;
|
|
1086
|
+
function createHostAbiServer(proc) {
|
|
1087
|
+
const files = new FileService(proc.vfs, proc.cred);
|
|
1088
|
+
const started = Date.now();
|
|
1089
|
+
const monotonicBase = typeof performance === "object" ? performance.now() : 0;
|
|
1090
|
+
return async (frame) => {
|
|
1091
|
+
const { header, payload } = decodeRequest(frame);
|
|
1092
|
+
if (header.version !== SBX_HOST_ABI_VERSION) {
|
|
1093
|
+
return encodeResponse(header, failure(Errno.EPROTO), new Uint8Array());
|
|
1094
|
+
}
|
|
1095
|
+
if (header.generation !== proc.generation) {
|
|
1096
|
+
return encodeResponse(header, failure(Errno.ESRCH), new Uint8Array());
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const { status, payload: out } = await dispatch(header, new Reader(payload));
|
|
1100
|
+
return encodeResponse(header, status, out ?? new Uint8Array());
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
const posix = toPosixError(error);
|
|
1103
|
+
return encodeResponse(header, failure(posix.errno), new Uint8Array());
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
async function dispatch(header, r) {
|
|
1107
|
+
switch (header.op) {
|
|
1108
|
+
case Op.handshake: {
|
|
1109
|
+
const w = new Writer().u32(SBX_HOST_ABI_VERSION).u32(CAPABILITIES.length);
|
|
1110
|
+
for (const capability of CAPABILITIES) {
|
|
1111
|
+
w.string(capability).u32(implemented(capability) ? 1 : 0);
|
|
1112
|
+
}
|
|
1113
|
+
return { status: 0, payload: w.finish() };
|
|
1114
|
+
}
|
|
1115
|
+
/* ------------------------------------------------------------ files */
|
|
1116
|
+
case Op.openat: {
|
|
1117
|
+
const path = resolve2(r.string());
|
|
1118
|
+
const flags = r.u32();
|
|
1119
|
+
const mode = r.u32();
|
|
1120
|
+
const description = files.open(path, flags, mode);
|
|
1121
|
+
return { status: proc.table.add(description, (flags & O_CLOEXEC) !== 0) };
|
|
1122
|
+
}
|
|
1123
|
+
case Op.close: {
|
|
1124
|
+
proc.table.close(r.i32());
|
|
1125
|
+
return { status: 0 };
|
|
1126
|
+
}
|
|
1127
|
+
case Op.read: {
|
|
1128
|
+
const description = proc.table.get(r.i32());
|
|
1129
|
+
const length = clamp(r.u32());
|
|
1130
|
+
const data = await readBlocking(description, length);
|
|
1131
|
+
return { status: data.length, payload: data };
|
|
1132
|
+
}
|
|
1133
|
+
case Op.write: {
|
|
1134
|
+
const description = proc.table.get(r.i32());
|
|
1135
|
+
const data = r.bytes32();
|
|
1136
|
+
const written = await writeBlocking(description, data);
|
|
1137
|
+
return { status: written };
|
|
1138
|
+
}
|
|
1139
|
+
case Op.pread: {
|
|
1140
|
+
const description = proc.table.get(r.i32());
|
|
1141
|
+
const length = clamp(r.u32());
|
|
1142
|
+
const offset = r.u64();
|
|
1143
|
+
if (!description.pread) throw new PosixError(Errno.ESPIPE);
|
|
1144
|
+
const data = description.pread(length, offset);
|
|
1145
|
+
return { status: data.length, payload: data };
|
|
1146
|
+
}
|
|
1147
|
+
case Op.pwrite: {
|
|
1148
|
+
const description = proc.table.get(r.i32());
|
|
1149
|
+
const data = r.bytes32();
|
|
1150
|
+
const offset = r.u64();
|
|
1151
|
+
if (!description.pwrite) throw new PosixError(Errno.ESPIPE);
|
|
1152
|
+
return { status: description.pwrite(data, offset) };
|
|
1153
|
+
}
|
|
1154
|
+
case Op.lseek: {
|
|
1155
|
+
const description = proc.table.get(r.i32());
|
|
1156
|
+
const offset = r.i64();
|
|
1157
|
+
const whence = r.u32();
|
|
1158
|
+
if (!description.seek) throw new PosixError(Errno.ESPIPE);
|
|
1159
|
+
return { status: 0, payload: new Writer().i64(description.seek(offset, whence)).finish() };
|
|
1160
|
+
}
|
|
1161
|
+
case Op.fstat: {
|
|
1162
|
+
return { status: 0, payload: encodeStat(proc.table.get(r.i32()).stat()) };
|
|
1163
|
+
}
|
|
1164
|
+
case Op.statat: {
|
|
1165
|
+
const path = resolve2(r.string());
|
|
1166
|
+
const followLinks = r.u32() !== 0;
|
|
1167
|
+
try {
|
|
1168
|
+
const st = followLinks ? proc.vfs.stat(path, { cred: proc.cred }) : proc.vfs.lstat(path);
|
|
1169
|
+
return {
|
|
1170
|
+
status: 0,
|
|
1171
|
+
payload: encodeStat({
|
|
1172
|
+
ino: st.ino,
|
|
1173
|
+
mode: st.mode,
|
|
1174
|
+
size: st.size,
|
|
1175
|
+
uid: st.uid,
|
|
1176
|
+
gid: st.gid,
|
|
1177
|
+
nlink: st.nlink ?? 1,
|
|
1178
|
+
atimeMs: st.atimeMs,
|
|
1179
|
+
mtimeMs: st.mtimeMs,
|
|
1180
|
+
ctimeMs: st.ctimeMs
|
|
1181
|
+
})
|
|
1182
|
+
};
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
throw toPosixError(error);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
case Op.ftruncate: {
|
|
1188
|
+
const description = proc.table.get(r.i32());
|
|
1189
|
+
const length = r.u64();
|
|
1190
|
+
if (!description.truncate) throw new PosixError(Errno.EINVAL);
|
|
1191
|
+
description.truncate(length);
|
|
1192
|
+
return { status: 0 };
|
|
1193
|
+
}
|
|
1194
|
+
case Op.renameat: {
|
|
1195
|
+
files.rename(resolve2(r.string()), resolve2(r.string()));
|
|
1196
|
+
return { status: 0 };
|
|
1197
|
+
}
|
|
1198
|
+
case Op.unlinkat: {
|
|
1199
|
+
const path = resolve2(r.string());
|
|
1200
|
+
const removeDirectory = r.u32() !== 0;
|
|
1201
|
+
if (removeDirectory) {
|
|
1202
|
+
try {
|
|
1203
|
+
proc.vfs.rmdir(path, proc.cred);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
throw toPosixError(error);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
files.unlink(path);
|
|
1209
|
+
}
|
|
1210
|
+
return { status: 0 };
|
|
1211
|
+
}
|
|
1212
|
+
case Op.mkdirat: {
|
|
1213
|
+
const path = resolve2(r.string());
|
|
1214
|
+
const mode = r.u32();
|
|
1215
|
+
try {
|
|
1216
|
+
proc.vfs.mkdir(path, { mode, cred: proc.cred });
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
throw toPosixError(error);
|
|
1219
|
+
}
|
|
1220
|
+
return { status: 0 };
|
|
1221
|
+
}
|
|
1222
|
+
case Op.readlinkat: {
|
|
1223
|
+
try {
|
|
1224
|
+
const target = proc.vfs.readlink(resolve2(r.string()), proc.cred);
|
|
1225
|
+
return { status: 0, payload: new Writer().string(target).finish() };
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
throw toPosixError(error);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
case Op.symlinkat: {
|
|
1231
|
+
const target = r.string();
|
|
1232
|
+
try {
|
|
1233
|
+
proc.vfs.symlink(target, resolve2(r.string()), proc.cred);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
throw toPosixError(error);
|
|
1236
|
+
}
|
|
1237
|
+
return { status: 0 };
|
|
1238
|
+
}
|
|
1239
|
+
case Op.getdents: {
|
|
1240
|
+
const description = proc.table.get(r.i32());
|
|
1241
|
+
if (description.kind !== "dir") throw new PosixError(Errno.ENOTDIR);
|
|
1242
|
+
const data = description.read(clamp(r.u32()));
|
|
1243
|
+
return { status: data.length, payload: data };
|
|
1244
|
+
}
|
|
1245
|
+
case Op.fsync: {
|
|
1246
|
+
proc.table.get(r.i32());
|
|
1247
|
+
return { status: 0 };
|
|
1248
|
+
}
|
|
1249
|
+
/* ------------------------------------------------------ descriptors */
|
|
1250
|
+
case Op.dup:
|
|
1251
|
+
return { status: proc.table.dup(r.i32(), r.u32()) };
|
|
1252
|
+
case Op.dup2:
|
|
1253
|
+
return { status: proc.table.dup2(r.i32(), r.i32()) };
|
|
1254
|
+
case Op.get_flags: {
|
|
1255
|
+
const fd = r.i32();
|
|
1256
|
+
const description = proc.table.get(fd);
|
|
1257
|
+
return { status: 0, payload: new Writer().u32(description.flags).u32(proc.table.cloexec(fd) ? 1 : 0).finish() };
|
|
1258
|
+
}
|
|
1259
|
+
case Op.set_flags: {
|
|
1260
|
+
const fd = r.i32();
|
|
1261
|
+
const flags = r.u32();
|
|
1262
|
+
const cloexec = r.u32();
|
|
1263
|
+
const description = proc.table.get(fd);
|
|
1264
|
+
description.flags = accessMode(description.flags) | flags & -4;
|
|
1265
|
+
proc.table.setCloexec(fd, cloexec !== 0);
|
|
1266
|
+
return { status: 0 };
|
|
1267
|
+
}
|
|
1268
|
+
/* ------------------------------------------------------------ pipes */
|
|
1269
|
+
case Op.pipe: {
|
|
1270
|
+
const flags = r.u32();
|
|
1271
|
+
const [readEnd, writeEnd] = createPipe(flags & O_NONBLOCK);
|
|
1272
|
+
const cloexec = (flags & O_CLOEXEC) !== 0;
|
|
1273
|
+
const readFd = proc.table.add(readEnd, cloexec);
|
|
1274
|
+
const writeFd = proc.table.add(writeEnd, cloexec);
|
|
1275
|
+
return { status: 0, payload: new Writer().i32(readFd).i32(writeFd).finish() };
|
|
1276
|
+
}
|
|
1277
|
+
/* -------------------------------------------------------- readiness */
|
|
1278
|
+
case Op.poll: {
|
|
1279
|
+
const count = r.u32();
|
|
1280
|
+
const requests = [];
|
|
1281
|
+
for (let i = 0; i < count; i += 1) requests.push({ fd: r.i32(), events: r.u32() });
|
|
1282
|
+
const timeoutMs = r.i64();
|
|
1283
|
+
const ready = await poll(requests, timeoutMs);
|
|
1284
|
+
const w = new Writer().u32(ready.length);
|
|
1285
|
+
for (const entry of ready) w.i32(entry.fd).u32(entry.revents);
|
|
1286
|
+
return { status: ready.length, payload: w.finish() };
|
|
1287
|
+
}
|
|
1288
|
+
/* ------------------------------------------------------------- time */
|
|
1289
|
+
case Op.clock_gettime: {
|
|
1290
|
+
const monotonic = r.u32() !== 0;
|
|
1291
|
+
const now = monotonic ? (typeof performance === "object" ? performance.now() : Date.now() - started) - monotonicBase : Date.now();
|
|
1292
|
+
const nanos = Math.round(now * 1e6);
|
|
1293
|
+
return { status: 0, payload: new Writer().i64(Math.floor(nanos / 1e9)).u32(nanos % 1e9).finish() };
|
|
1294
|
+
}
|
|
1295
|
+
case Op.sleep: {
|
|
1296
|
+
const nanos = r.u64();
|
|
1297
|
+
await sleep(nanos / 1e6);
|
|
1298
|
+
return { status: proc.signal.aborted ? failure(Errno.EINTR) : 0 };
|
|
1299
|
+
}
|
|
1300
|
+
/* --------------------------------------------------------- identity */
|
|
1301
|
+
case Op.getpid: {
|
|
1302
|
+
const w = new Writer().i32(proc.pid).i32(proc.ppid).u32(proc.cred.uid).u32(proc.cred.gid).u32(proc.cred.umask ?? 18);
|
|
1303
|
+
return { status: 0, payload: w.finish() };
|
|
1304
|
+
}
|
|
1305
|
+
case Op.getcwd:
|
|
1306
|
+
return { status: 0, payload: new Writer().string(proc.cwd).finish() };
|
|
1307
|
+
case Op.chdir: {
|
|
1308
|
+
const path = resolve2(r.string());
|
|
1309
|
+
let st;
|
|
1310
|
+
try {
|
|
1311
|
+
st = proc.vfs.stat(path, { cred: proc.cred });
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
throw toPosixError(error);
|
|
1314
|
+
}
|
|
1315
|
+
if (!st.isDirectory()) throw new PosixError(Errno.ENOTDIR);
|
|
1316
|
+
proc.cwd = path;
|
|
1317
|
+
return { status: 0 };
|
|
1318
|
+
}
|
|
1319
|
+
case Op.environ: {
|
|
1320
|
+
const joined = Object.entries(proc.env).map(([key, value]) => `${key}=${value}`).join("\0");
|
|
1321
|
+
return { status: 0, payload: new Writer().string(joined).finish() };
|
|
1322
|
+
}
|
|
1323
|
+
/* ---------------------------------------------------------- entropy */
|
|
1324
|
+
case Op.getrandom: {
|
|
1325
|
+
const length = Math.min(r.u32(), 65536);
|
|
1326
|
+
const bytes = new Uint8Array(length);
|
|
1327
|
+
crypto.getRandomValues(bytes);
|
|
1328
|
+
return { status: length, payload: bytes };
|
|
1329
|
+
}
|
|
1330
|
+
default:
|
|
1331
|
+
return { status: failure(Errno.ENOSYS) };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function implemented(capability) {
|
|
1335
|
+
return ["files", "descriptors", "pipes", "readiness", "time", "identity", "entropy"].includes(capability);
|
|
1336
|
+
}
|
|
1337
|
+
function resolve2(path) {
|
|
1338
|
+
if (path.startsWith("/")) return proc.vfs.resolvePath(path);
|
|
1339
|
+
return proc.vfs.resolvePath(`${proc.cwd.replace(/\/$/, "")}/${path}`);
|
|
1340
|
+
}
|
|
1341
|
+
function clamp(length) {
|
|
1342
|
+
return Math.min(length, MAX_TRANSFER);
|
|
1343
|
+
}
|
|
1344
|
+
async function readBlocking(description, length) {
|
|
1345
|
+
for (; ; ) {
|
|
1346
|
+
try {
|
|
1347
|
+
return description.read(length);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
const posix = toPosixError(error);
|
|
1350
|
+
if (posix.errno !== Errno.EAGAIN || description.flags & O_NONBLOCK) throw posix;
|
|
1351
|
+
}
|
|
1352
|
+
await Promise.race([description.whenReady(), aborted()]);
|
|
1353
|
+
if (proc.signal.aborted) throw new PosixError(Errno.EINTR);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function writeBlocking(description, data) {
|
|
1357
|
+
for (; ; ) {
|
|
1358
|
+
try {
|
|
1359
|
+
return description.write(data);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
const posix = toPosixError(error);
|
|
1362
|
+
if (posix.errno !== Errno.EAGAIN || description.flags & O_NONBLOCK) throw posix;
|
|
1363
|
+
}
|
|
1364
|
+
await Promise.race([description.whenReady(), aborted()]);
|
|
1365
|
+
if (proc.signal.aborted) throw new PosixError(Errno.EINTR);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const POLLIN = 1;
|
|
1369
|
+
const POLLOUT = 4;
|
|
1370
|
+
const POLLHUP = 16;
|
|
1371
|
+
async function poll(requests, timeoutMs) {
|
|
1372
|
+
const deadline = timeoutMs < 0 ? Infinity : Date.now() + timeoutMs;
|
|
1373
|
+
for (; ; ) {
|
|
1374
|
+
const ready = [];
|
|
1375
|
+
const waits = [];
|
|
1376
|
+
for (const { fd, events } of requests) {
|
|
1377
|
+
let description;
|
|
1378
|
+
try {
|
|
1379
|
+
description = proc.table.get(fd);
|
|
1380
|
+
} catch {
|
|
1381
|
+
ready.push({ fd, revents: POLLHUP });
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
let revents = 0;
|
|
1385
|
+
if (events & POLLIN && description.readable()) revents |= POLLIN;
|
|
1386
|
+
if (events & POLLOUT && description.writable()) revents |= POLLOUT;
|
|
1387
|
+
if (revents) ready.push({ fd, revents });
|
|
1388
|
+
else waits.push(description.whenReady());
|
|
1389
|
+
}
|
|
1390
|
+
if (ready.length > 0) return ready;
|
|
1391
|
+
const remaining = deadline - Date.now();
|
|
1392
|
+
if (remaining <= 0) return [];
|
|
1393
|
+
await Promise.race([...waits, sleep(Math.min(remaining, 50)), aborted()]);
|
|
1394
|
+
if (proc.signal.aborted) throw new PosixError(Errno.EINTR);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function aborted() {
|
|
1398
|
+
if (proc.signal.aborted) return Promise.resolve();
|
|
1399
|
+
return new Promise((resolve3) => proc.signal.addEventListener("abort", () => resolve3(), { once: true }));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function sleep(ms) {
|
|
1403
|
+
return new Promise((resolve2) => setTimeout(resolve2, Math.max(0, ms)));
|
|
1404
|
+
}
|
|
1405
|
+
function encodeStat(st) {
|
|
1406
|
+
return new Writer().u64(st.ino).u32(st.mode).u64(st.size).u32(st.uid).u32(st.gid).u32(st.nlink).i64(Math.round(st.atimeMs * 1e6)).i64(Math.round(st.mtimeMs * 1e6)).i64(Math.round(st.ctimeMs * 1e6)).finish();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/runtime/python/syscall-client.ts
|
|
1410
|
+
var HostAbiError = class extends Error {
|
|
1411
|
+
errno;
|
|
1412
|
+
code;
|
|
1413
|
+
constructor(errno, op) {
|
|
1414
|
+
const name = ERRNO_NAMES[errno] ?? String(errno);
|
|
1415
|
+
super(`${OP_NAMES[op] ?? `op ${op}`} failed: ${name}`);
|
|
1416
|
+
this.errno = errno;
|
|
1417
|
+
this.code = name;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
var HostAbiClient = class {
|
|
1421
|
+
transport;
|
|
1422
|
+
generation;
|
|
1423
|
+
nextRequestId = 1;
|
|
1424
|
+
constructor(transport, generation) {
|
|
1425
|
+
this.transport = transport;
|
|
1426
|
+
this.generation = generation;
|
|
1427
|
+
}
|
|
1428
|
+
/** Send one frame, blocking. Returns the status and payload verbatim. */
|
|
1429
|
+
callRaw(op, payload) {
|
|
1430
|
+
const header = {
|
|
1431
|
+
version: SBX_HOST_ABI_VERSION,
|
|
1432
|
+
op,
|
|
1433
|
+
requestId: this.nextRequestId++,
|
|
1434
|
+
generation: this.generation
|
|
1435
|
+
};
|
|
1436
|
+
const frame = this.transport.call(encodeRequest(header, payload));
|
|
1437
|
+
const response = decodeResponse(frame, header);
|
|
1438
|
+
return { status: response.status, payload: response.payload };
|
|
1439
|
+
}
|
|
1440
|
+
/** As {@link callRaw}, but a negative status becomes an exception. */
|
|
1441
|
+
call(op, payload) {
|
|
1442
|
+
const result = this.callRaw(op, payload);
|
|
1443
|
+
if (result.status < 0) throw new HostAbiError(-result.status, op);
|
|
1444
|
+
return result;
|
|
1445
|
+
}
|
|
1446
|
+
handshake() {
|
|
1447
|
+
const { payload } = this.call(Op.handshake, new Uint8Array());
|
|
1448
|
+
const r = new Reader(payload);
|
|
1449
|
+
const version = r.u32();
|
|
1450
|
+
const count = r.u32();
|
|
1451
|
+
const capabilities = {};
|
|
1452
|
+
for (let i = 0; i < count; i += 1) capabilities[r.string()] = r.u32() === 1;
|
|
1453
|
+
return { version, capabilities };
|
|
1454
|
+
}
|
|
1455
|
+
open(path, flags, mode = 438) {
|
|
1456
|
+
return this.call(Op.openat, new Writer().string(path).u32(flags).u32(mode).finish()).status;
|
|
1457
|
+
}
|
|
1458
|
+
close(fd) {
|
|
1459
|
+
this.call(Op.close, new Writer().i32(fd).finish());
|
|
1460
|
+
}
|
|
1461
|
+
read(fd, length) {
|
|
1462
|
+
return this.call(Op.read, new Writer().i32(fd).u32(length).finish()).payload;
|
|
1463
|
+
}
|
|
1464
|
+
write(fd, data) {
|
|
1465
|
+
return this.call(Op.write, new Writer().i32(fd).bytes32(data).finish()).status;
|
|
1466
|
+
}
|
|
1467
|
+
/** Write every byte, looping over short writes the way a guest's libc must. */
|
|
1468
|
+
writeAll(fd, data) {
|
|
1469
|
+
let at = 0;
|
|
1470
|
+
while (at < data.length) at += this.write(fd, data.subarray(at));
|
|
1471
|
+
}
|
|
1472
|
+
pread(fd, length, offset) {
|
|
1473
|
+
return this.call(Op.pread, new Writer().i32(fd).u32(length).u64(offset).finish()).payload;
|
|
1474
|
+
}
|
|
1475
|
+
pwrite(fd, data, offset) {
|
|
1476
|
+
return this.call(Op.pwrite, new Writer().i32(fd).bytes32(data).u64(offset).finish()).status;
|
|
1477
|
+
}
|
|
1478
|
+
seek(fd, offset, whence) {
|
|
1479
|
+
const { payload } = this.call(Op.lseek, new Writer().i32(fd).i64(offset).u32(whence).finish());
|
|
1480
|
+
return new Reader(payload).i64();
|
|
1481
|
+
}
|
|
1482
|
+
fstat(fd) {
|
|
1483
|
+
return decodeStat(this.call(Op.fstat, new Writer().i32(fd).finish()).payload);
|
|
1484
|
+
}
|
|
1485
|
+
stat(path, followLinks = true) {
|
|
1486
|
+
return decodeStat(
|
|
1487
|
+
this.call(Op.statat, new Writer().string(path).u32(followLinks ? 1 : 0).finish()).payload
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
ftruncate(fd, length) {
|
|
1491
|
+
this.call(Op.ftruncate, new Writer().i32(fd).u64(length).finish());
|
|
1492
|
+
}
|
|
1493
|
+
rename(from, to) {
|
|
1494
|
+
this.call(Op.renameat, new Writer().string(from).string(to).finish());
|
|
1495
|
+
}
|
|
1496
|
+
unlink(path, removeDirectory = false) {
|
|
1497
|
+
this.call(Op.unlinkat, new Writer().string(path).u32(removeDirectory ? 1 : 0).finish());
|
|
1498
|
+
}
|
|
1499
|
+
mkdir(path, mode = 511) {
|
|
1500
|
+
this.call(Op.mkdirat, new Writer().string(path).u32(mode).finish());
|
|
1501
|
+
}
|
|
1502
|
+
readdir(fd, bufferSize = 4096) {
|
|
1503
|
+
const { payload } = this.call(Op.getdents, new Writer().i32(fd).u32(bufferSize).finish());
|
|
1504
|
+
const text = new TextDecoder().decode(payload);
|
|
1505
|
+
return text.split("\0").filter((name) => name.length > 0);
|
|
1506
|
+
}
|
|
1507
|
+
dup(fd, from = 0) {
|
|
1508
|
+
return this.call(Op.dup, new Writer().i32(fd).u32(from).finish()).status;
|
|
1509
|
+
}
|
|
1510
|
+
dup2(fd, target) {
|
|
1511
|
+
return this.call(Op.dup2, new Writer().i32(fd).i32(target).finish()).status;
|
|
1512
|
+
}
|
|
1513
|
+
getFlags(fd) {
|
|
1514
|
+
const r = new Reader(this.call(Op.get_flags, new Writer().i32(fd).finish()).payload);
|
|
1515
|
+
return { flags: r.u32(), cloexec: r.u32() === 1 };
|
|
1516
|
+
}
|
|
1517
|
+
setFlags(fd, flags, cloexec) {
|
|
1518
|
+
this.call(Op.set_flags, new Writer().i32(fd).u32(flags).u32(cloexec ? 1 : 0).finish());
|
|
1519
|
+
}
|
|
1520
|
+
pipe(flags = 0) {
|
|
1521
|
+
const r = new Reader(this.call(Op.pipe, new Writer().u32(flags).finish()).payload);
|
|
1522
|
+
return [r.i32(), r.i32()];
|
|
1523
|
+
}
|
|
1524
|
+
poll(entries, timeoutMs) {
|
|
1525
|
+
const w = new Writer().u32(entries.length);
|
|
1526
|
+
for (const entry of entries) w.i32(entry.fd).u32(entry.events);
|
|
1527
|
+
w.i64(timeoutMs);
|
|
1528
|
+
const r = new Reader(this.call(Op.poll, w.finish()).payload);
|
|
1529
|
+
const count = r.u32();
|
|
1530
|
+
const out = [];
|
|
1531
|
+
for (let i = 0; i < count; i += 1) out.push({ fd: r.i32(), revents: r.u32() });
|
|
1532
|
+
return out;
|
|
1533
|
+
}
|
|
1534
|
+
clockGettime(monotonic) {
|
|
1535
|
+
const r = new Reader(this.call(Op.clock_gettime, new Writer().u32(monotonic ? 1 : 0).finish()).payload);
|
|
1536
|
+
return { seconds: r.i64(), nanos: r.u32() };
|
|
1537
|
+
}
|
|
1538
|
+
sleep(nanos) {
|
|
1539
|
+
this.call(Op.sleep, new Writer().u64(nanos).finish());
|
|
1540
|
+
}
|
|
1541
|
+
identity() {
|
|
1542
|
+
const r = new Reader(this.call(Op.getpid, new Uint8Array()).payload);
|
|
1543
|
+
return { pid: r.i32(), ppid: r.i32(), uid: r.u32(), gid: r.u32(), umask: r.u32() };
|
|
1544
|
+
}
|
|
1545
|
+
getcwd() {
|
|
1546
|
+
return new Reader(this.call(Op.getcwd, new Uint8Array()).payload).string();
|
|
1547
|
+
}
|
|
1548
|
+
chdir(path) {
|
|
1549
|
+
this.call(Op.chdir, new Writer().string(path).finish());
|
|
1550
|
+
}
|
|
1551
|
+
environ() {
|
|
1552
|
+
const joined = new Reader(this.call(Op.environ, new Uint8Array()).payload).string();
|
|
1553
|
+
const out = {};
|
|
1554
|
+
for (const entry of joined.split("\0")) {
|
|
1555
|
+
if (!entry) continue;
|
|
1556
|
+
const split = entry.indexOf("=");
|
|
1557
|
+
out[entry.slice(0, split)] = entry.slice(split + 1);
|
|
1558
|
+
}
|
|
1559
|
+
return out;
|
|
1560
|
+
}
|
|
1561
|
+
getrandom(length) {
|
|
1562
|
+
return this.call(Op.getrandom, new Writer().u32(length).finish()).payload;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
function decodeStat(payload) {
|
|
1566
|
+
const r = new Reader(payload);
|
|
1567
|
+
return {
|
|
1568
|
+
ino: r.u64(),
|
|
1569
|
+
mode: r.u32(),
|
|
1570
|
+
size: r.u64(),
|
|
1571
|
+
uid: r.u32(),
|
|
1572
|
+
gid: r.u32(),
|
|
1573
|
+
nlink: r.u32(),
|
|
1574
|
+
atimeNs: r.i64(),
|
|
1575
|
+
mtimeNs: r.i64(),
|
|
1576
|
+
ctimeNs: r.i64()
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/fs/errno.ts
|
|
1581
|
+
var ERRNO = {
|
|
1582
|
+
EPERM: 1,
|
|
1583
|
+
ENOENT: 2,
|
|
1584
|
+
ESRCH: 3,
|
|
1585
|
+
EINTR: 4,
|
|
1586
|
+
EIO: 5,
|
|
1587
|
+
ENXIO: 6,
|
|
1588
|
+
E2BIG: 7,
|
|
1589
|
+
ENOEXEC: 8,
|
|
1590
|
+
EBADF: 9,
|
|
1591
|
+
ECHILD: 10,
|
|
1592
|
+
EAGAIN: 11,
|
|
1593
|
+
ENOMEM: 12,
|
|
1594
|
+
EACCES: 13,
|
|
1595
|
+
EFAULT: 14,
|
|
1596
|
+
ENOTBLK: 15,
|
|
1597
|
+
EBUSY: 16,
|
|
1598
|
+
EEXIST: 17,
|
|
1599
|
+
EXDEV: 18,
|
|
1600
|
+
ENODEV: 19,
|
|
1601
|
+
ENOTDIR: 20,
|
|
1602
|
+
EISDIR: 21,
|
|
1603
|
+
EINVAL: 22,
|
|
1604
|
+
ENFILE: 23,
|
|
1605
|
+
EMFILE: 24,
|
|
1606
|
+
ENOTTY: 25,
|
|
1607
|
+
ETXTBSY: 26,
|
|
1608
|
+
EFBIG: 27,
|
|
1609
|
+
ENOSPC: 28,
|
|
1610
|
+
ESPIPE: 29,
|
|
1611
|
+
EROFS: 30,
|
|
1612
|
+
EMLINK: 31,
|
|
1613
|
+
EPIPE: 32,
|
|
1614
|
+
EDOM: 33,
|
|
1615
|
+
ERANGE: 34,
|
|
1616
|
+
EDEADLK: 35,
|
|
1617
|
+
ENAMETOOLONG: 36,
|
|
1618
|
+
ENOLCK: 37,
|
|
1619
|
+
ENOSYS: 38,
|
|
1620
|
+
ENOTEMPTY: 39,
|
|
1621
|
+
ELOOP: 40,
|
|
1622
|
+
ENOMSG: 42,
|
|
1623
|
+
ENOTSOCK: 88,
|
|
1624
|
+
EADDRINUSE: 98,
|
|
1625
|
+
EADDRNOTAVAIL: 99,
|
|
1626
|
+
ENETDOWN: 100,
|
|
1627
|
+
ECONNRESET: 104,
|
|
1628
|
+
ENOTCONN: 107,
|
|
1629
|
+
ETIMEDOUT: 110,
|
|
1630
|
+
ECONNREFUSED: 111,
|
|
1631
|
+
EHOSTUNREACH: 113,
|
|
1632
|
+
ENOTSUP: 95
|
|
1633
|
+
};
|
|
1634
|
+
var STRERROR = {
|
|
1635
|
+
EPERM: "Operation not permitted",
|
|
1636
|
+
ENOENT: "No such file or directory",
|
|
1637
|
+
ESRCH: "No such process",
|
|
1638
|
+
EINTR: "Interrupted system call",
|
|
1639
|
+
EIO: "Input/output error",
|
|
1640
|
+
ENXIO: "No such device or address",
|
|
1641
|
+
E2BIG: "Argument list too long",
|
|
1642
|
+
ENOEXEC: "Exec format error",
|
|
1643
|
+
EBADF: "Bad file descriptor",
|
|
1644
|
+
ECHILD: "No child processes",
|
|
1645
|
+
EAGAIN: "Resource temporarily unavailable",
|
|
1646
|
+
ENOMEM: "Cannot allocate memory",
|
|
1647
|
+
EACCES: "Permission denied",
|
|
1648
|
+
EFAULT: "Bad address",
|
|
1649
|
+
ENOTBLK: "Block device required",
|
|
1650
|
+
EBUSY: "Device or resource busy",
|
|
1651
|
+
EEXIST: "File exists",
|
|
1652
|
+
EXDEV: "Invalid cross-device link",
|
|
1653
|
+
ENODEV: "No such device",
|
|
1654
|
+
ENOTDIR: "Not a directory",
|
|
1655
|
+
EISDIR: "Is a directory",
|
|
1656
|
+
EINVAL: "Invalid argument",
|
|
1657
|
+
ENFILE: "Too many open files in system",
|
|
1658
|
+
EMFILE: "Too many open files",
|
|
1659
|
+
ENOTTY: "Inappropriate ioctl for device",
|
|
1660
|
+
ETXTBSY: "Text file busy",
|
|
1661
|
+
EFBIG: "File too large",
|
|
1662
|
+
ENOSPC: "No space left on device",
|
|
1663
|
+
ESPIPE: "Illegal seek",
|
|
1664
|
+
EROFS: "Read-only file system",
|
|
1665
|
+
EMLINK: "Too many links",
|
|
1666
|
+
EPIPE: "Broken pipe",
|
|
1667
|
+
EDOM: "Numerical argument out of domain",
|
|
1668
|
+
ERANGE: "Numerical result out of range",
|
|
1669
|
+
EDEADLK: "Resource deadlock avoided",
|
|
1670
|
+
ENAMETOOLONG: "File name too long",
|
|
1671
|
+
ENOLCK: "No locks available",
|
|
1672
|
+
ENOSYS: "Function not implemented",
|
|
1673
|
+
ENOTEMPTY: "Directory not empty",
|
|
1674
|
+
ELOOP: "Too many levels of symbolic links",
|
|
1675
|
+
ENOMSG: "No message of desired type",
|
|
1676
|
+
ENOTSOCK: "Socket operation on non-socket",
|
|
1677
|
+
EADDRINUSE: "Address already in use",
|
|
1678
|
+
EADDRNOTAVAIL: "Cannot assign requested address",
|
|
1679
|
+
ENETDOWN: "Network is down",
|
|
1680
|
+
ECONNRESET: "Connection reset by peer",
|
|
1681
|
+
ENOTCONN: "Transport endpoint is not connected",
|
|
1682
|
+
ETIMEDOUT: "Connection timed out",
|
|
1683
|
+
ECONNREFUSED: "Connection refused",
|
|
1684
|
+
EHOSTUNREACH: "No route to host",
|
|
1685
|
+
ENOTSUP: "Operation not supported"
|
|
1686
|
+
};
|
|
1687
|
+
var SysError = class extends Error {
|
|
1688
|
+
code;
|
|
1689
|
+
errno;
|
|
1690
|
+
syscall;
|
|
1691
|
+
path;
|
|
1692
|
+
dest;
|
|
1693
|
+
constructor(code, syscall, path, dest) {
|
|
1694
|
+
const detail = path === void 0 ? "" : dest === void 0 ? ` '${path}'` : ` '${path}' -> '${dest}'`;
|
|
1695
|
+
super(`${code}: ${STRERROR[code]}, ${syscall}${detail}`);
|
|
1696
|
+
this.name = "SysError";
|
|
1697
|
+
this.code = code;
|
|
1698
|
+
this.errno = -ERRNO[code];
|
|
1699
|
+
this.syscall = syscall;
|
|
1700
|
+
if (path !== void 0) this.path = path;
|
|
1701
|
+
if (dest !== void 0) this.dest = dest;
|
|
1702
|
+
}
|
|
1703
|
+
/** The message a coreutil prints: `ls: /nope: No such file or directory`. */
|
|
1704
|
+
toUserMessage(program, subject = this.path) {
|
|
1705
|
+
return subject ? `${program}: ${subject}: ${STRERROR[this.code]}` : `${program}: ${STRERROR[this.code]}`;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
function toSysError(e, syscall, path, dest) {
|
|
1709
|
+
if (e instanceof SysError) return e;
|
|
1710
|
+
const code = e?.code;
|
|
1711
|
+
if (code && code in ERRNO) return new SysError(code, syscall, path, dest);
|
|
1712
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1713
|
+
if (/not a directory/i.test(msg)) return new SysError("ENOTDIR", syscall, path);
|
|
1714
|
+
if (/no such file|not found/i.test(msg)) return new SysError("ENOENT", syscall, path);
|
|
1715
|
+
if (/exists/i.test(msg)) return new SysError("EEXIST", syscall, path);
|
|
1716
|
+
if (/directory not empty/i.test(msg)) return new SysError("ENOTEMPTY", syscall, path);
|
|
1717
|
+
if (/is a directory/i.test(msg)) return new SysError("EISDIR", syscall, path);
|
|
1718
|
+
return new SysError("EIO", syscall, path);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/fs/path.ts
|
|
1722
|
+
function isAbsolute(p) {
|
|
1723
|
+
return p.startsWith("/");
|
|
1724
|
+
}
|
|
1725
|
+
function segments(p) {
|
|
1726
|
+
return p.split("/").filter((s) => s.length > 0);
|
|
1727
|
+
}
|
|
1728
|
+
function normalize(p) {
|
|
1729
|
+
if (p === "") return ".";
|
|
1730
|
+
const abs = isAbsolute(p);
|
|
1731
|
+
const trailing = p.length > 1 && p.endsWith("/");
|
|
1732
|
+
const out = [];
|
|
1733
|
+
for (const seg of p.split("/")) {
|
|
1734
|
+
if (seg === "" || seg === ".") continue;
|
|
1735
|
+
if (seg === "..") {
|
|
1736
|
+
if (out.length > 0 && out[out.length - 1] !== "..") out.pop();
|
|
1737
|
+
else if (!abs) out.push("..");
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
out.push(seg);
|
|
1741
|
+
}
|
|
1742
|
+
let result = out.join("/");
|
|
1743
|
+
if (abs) result = "/" + result;
|
|
1744
|
+
else if (result === "") result = ".";
|
|
1745
|
+
if (trailing && !result.endsWith("/")) result += "/";
|
|
1746
|
+
return result === "" ? "/" : result;
|
|
1747
|
+
}
|
|
1748
|
+
function join(...parts) {
|
|
1749
|
+
const joined = parts.filter((p) => p !== "").join("/");
|
|
1750
|
+
return joined === "" ? "." : normalize(joined);
|
|
1751
|
+
}
|
|
1752
|
+
function resolve(base, ...parts) {
|
|
1753
|
+
let resolved = "";
|
|
1754
|
+
let absolute = false;
|
|
1755
|
+
for (let i = parts.length - 1; i >= 0 && !absolute; i--) {
|
|
1756
|
+
const part = parts[i];
|
|
1757
|
+
if (part === "") continue;
|
|
1758
|
+
resolved = resolved === "" ? part : `${part}/${resolved}`;
|
|
1759
|
+
absolute = isAbsolute(part);
|
|
1760
|
+
}
|
|
1761
|
+
if (!absolute) {
|
|
1762
|
+
resolved = resolved === "" ? base : `${base}/${resolved}`;
|
|
1763
|
+
}
|
|
1764
|
+
const norm = normalize(resolved);
|
|
1765
|
+
return isAbsolute(norm) ? stripTrailing(norm) : stripTrailing(normalize(`/${norm}`));
|
|
1766
|
+
}
|
|
1767
|
+
function stripTrailing(p) {
|
|
1768
|
+
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
1769
|
+
}
|
|
1770
|
+
function dirname(p) {
|
|
1771
|
+
const norm = stripTrailing(normalize(p));
|
|
1772
|
+
if (norm === "/") return "/";
|
|
1773
|
+
const idx = norm.lastIndexOf("/");
|
|
1774
|
+
if (idx < 0) return ".";
|
|
1775
|
+
if (idx === 0) return "/";
|
|
1776
|
+
return norm.slice(0, idx);
|
|
1777
|
+
}
|
|
1778
|
+
function basename(p, ext) {
|
|
1779
|
+
const norm = stripTrailing(normalize(p));
|
|
1780
|
+
if (norm === "/") return "/";
|
|
1781
|
+
const idx = norm.lastIndexOf("/");
|
|
1782
|
+
let base = idx < 0 ? norm : norm.slice(idx + 1);
|
|
1783
|
+
return base;
|
|
1784
|
+
}
|
|
1785
|
+
function contains(parent, child) {
|
|
1786
|
+
const p = stripTrailing(normalize(parent));
|
|
1787
|
+
const c = stripTrailing(normalize(child));
|
|
1788
|
+
if (p === "/") return true;
|
|
1789
|
+
return c === p || c.startsWith(p + "/");
|
|
1790
|
+
}
|
|
1791
|
+
function clean(p) {
|
|
1792
|
+
return stripTrailing(normalize(p));
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// src/fs/mode.ts
|
|
1796
|
+
var S_IFMT = 61440;
|
|
1797
|
+
var S_IFSOCK = 49152;
|
|
1798
|
+
var S_IFLNK = 40960;
|
|
1799
|
+
var S_IFREG = 32768;
|
|
1800
|
+
var S_IFBLK = 24576;
|
|
1801
|
+
var S_IFDIR = 16384;
|
|
1802
|
+
var S_IFCHR = 8192;
|
|
1803
|
+
var S_IFIFO = 4096;
|
|
1804
|
+
var S_IRUSR = 256;
|
|
1805
|
+
var S_IWUSR = 128;
|
|
1806
|
+
var S_IXUSR = 64;
|
|
1807
|
+
var S_IRGRP = 32;
|
|
1808
|
+
var S_IWGRP = 16;
|
|
1809
|
+
var S_IXGRP = 8;
|
|
1810
|
+
var S_IROTH = 4;
|
|
1811
|
+
var S_IWOTH = 2;
|
|
1812
|
+
var S_IXOTH = 1;
|
|
1813
|
+
var PERM_MASK = 4095;
|
|
1814
|
+
var F_OK = 0;
|
|
1815
|
+
var X_OK = 1;
|
|
1816
|
+
var W_OK = 2;
|
|
1817
|
+
var R_OK = 4;
|
|
1818
|
+
function kindOf(mode) {
|
|
1819
|
+
switch (mode & S_IFMT) {
|
|
1820
|
+
case S_IFDIR:
|
|
1821
|
+
return "directory";
|
|
1822
|
+
case S_IFLNK:
|
|
1823
|
+
return "symlink";
|
|
1824
|
+
case S_IFCHR:
|
|
1825
|
+
return "chardev";
|
|
1826
|
+
case S_IFBLK:
|
|
1827
|
+
return "blockdev";
|
|
1828
|
+
case S_IFIFO:
|
|
1829
|
+
return "fifo";
|
|
1830
|
+
case S_IFSOCK:
|
|
1831
|
+
return "socket";
|
|
1832
|
+
default:
|
|
1833
|
+
return "file";
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
function ifmtFor(kind) {
|
|
1837
|
+
switch (kind) {
|
|
1838
|
+
case "directory":
|
|
1839
|
+
return S_IFDIR;
|
|
1840
|
+
case "symlink":
|
|
1841
|
+
return S_IFLNK;
|
|
1842
|
+
case "chardev":
|
|
1843
|
+
return S_IFCHR;
|
|
1844
|
+
case "blockdev":
|
|
1845
|
+
return S_IFBLK;
|
|
1846
|
+
case "fifo":
|
|
1847
|
+
return S_IFIFO;
|
|
1848
|
+
case "socket":
|
|
1849
|
+
return S_IFSOCK;
|
|
1850
|
+
default:
|
|
1851
|
+
return S_IFREG;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// src/fs/stat.ts
|
|
1856
|
+
var Stats = class {
|
|
1857
|
+
mode;
|
|
1858
|
+
size;
|
|
1859
|
+
uid;
|
|
1860
|
+
gid;
|
|
1861
|
+
ino;
|
|
1862
|
+
nlink;
|
|
1863
|
+
dev;
|
|
1864
|
+
rdev;
|
|
1865
|
+
blksize;
|
|
1866
|
+
atimeMs;
|
|
1867
|
+
mtimeMs;
|
|
1868
|
+
ctimeMs;
|
|
1869
|
+
birthtimeMs;
|
|
1870
|
+
constructor(init) {
|
|
1871
|
+
this.mode = init.mode;
|
|
1872
|
+
this.size = init.size;
|
|
1873
|
+
this.uid = init.uid;
|
|
1874
|
+
this.gid = init.gid;
|
|
1875
|
+
this.ino = init.ino;
|
|
1876
|
+
this.nlink = init.nlink;
|
|
1877
|
+
this.dev = init.dev ?? 2049;
|
|
1878
|
+
this.rdev = init.rdev ?? 0;
|
|
1879
|
+
this.blksize = init.blksize ?? 4096;
|
|
1880
|
+
this.atimeMs = init.atimeMs;
|
|
1881
|
+
this.mtimeMs = init.mtimeMs;
|
|
1882
|
+
this.ctimeMs = init.ctimeMs;
|
|
1883
|
+
this.birthtimeMs = init.birthtimeMs ?? init.ctimeMs;
|
|
1884
|
+
}
|
|
1885
|
+
get blocks() {
|
|
1886
|
+
return Math.ceil(this.size / 512);
|
|
1887
|
+
}
|
|
1888
|
+
get atime() {
|
|
1889
|
+
return new Date(this.atimeMs);
|
|
1890
|
+
}
|
|
1891
|
+
get mtime() {
|
|
1892
|
+
return new Date(this.mtimeMs);
|
|
1893
|
+
}
|
|
1894
|
+
get ctime() {
|
|
1895
|
+
return new Date(this.ctimeMs);
|
|
1896
|
+
}
|
|
1897
|
+
get birthtime() {
|
|
1898
|
+
return new Date(this.birthtimeMs);
|
|
1899
|
+
}
|
|
1900
|
+
get kind() {
|
|
1901
|
+
return kindOf(this.mode);
|
|
1902
|
+
}
|
|
1903
|
+
isFile() {
|
|
1904
|
+
return this.kind === "file";
|
|
1905
|
+
}
|
|
1906
|
+
isDirectory() {
|
|
1907
|
+
return this.kind === "directory";
|
|
1908
|
+
}
|
|
1909
|
+
isSymbolicLink() {
|
|
1910
|
+
return this.kind === "symlink";
|
|
1911
|
+
}
|
|
1912
|
+
isCharacterDevice() {
|
|
1913
|
+
return this.kind === "chardev";
|
|
1914
|
+
}
|
|
1915
|
+
isBlockDevice() {
|
|
1916
|
+
return this.kind === "blockdev";
|
|
1917
|
+
}
|
|
1918
|
+
isFIFO() {
|
|
1919
|
+
return this.kind === "fifo";
|
|
1920
|
+
}
|
|
1921
|
+
isSocket() {
|
|
1922
|
+
return this.kind === "socket";
|
|
1923
|
+
}
|
|
1924
|
+
/** Permission bits only, with the type bits masked off. */
|
|
1925
|
+
get perms() {
|
|
1926
|
+
return this.mode & ~S_IFMT & 4095;
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/fs/vfs.ts
|
|
1931
|
+
var ROOT_CRED = { uid: 0, gid: 0, groups: [0], umask: 18 };
|
|
1932
|
+
function makeCred(uid, gid, groups = [], umask = 18) {
|
|
1933
|
+
return { uid, gid, groups: groups.includes(gid) ? groups : [gid, ...groups], umask };
|
|
1934
|
+
}
|
|
1935
|
+
var MAX_SYMLINKS = 40;
|
|
1936
|
+
var encoder = new TextEncoder();
|
|
1937
|
+
var decoder = new TextDecoder();
|
|
1938
|
+
var Vfs = class {
|
|
1939
|
+
constructor(volume) {
|
|
1940
|
+
this.volume = volume;
|
|
1941
|
+
}
|
|
1942
|
+
volume;
|
|
1943
|
+
providers = [];
|
|
1944
|
+
nextVirtualIno = 983040;
|
|
1945
|
+
virtualInos = /* @__PURE__ */ new Map();
|
|
1946
|
+
// ── virtual providers ────────────────────────────────────────────────────
|
|
1947
|
+
addProvider(provider) {
|
|
1948
|
+
this.providers.push(provider);
|
|
1949
|
+
this.providers.sort((a, b) => b.root.length - a.root.length);
|
|
1950
|
+
}
|
|
1951
|
+
removeProvider(root) {
|
|
1952
|
+
const i = this.providers.findIndex((p) => p.root === root);
|
|
1953
|
+
if (i >= 0) this.providers.splice(i, 1);
|
|
1954
|
+
}
|
|
1955
|
+
/** The mount points currently served synthetically. */
|
|
1956
|
+
get virtualRoots() {
|
|
1957
|
+
return this.providers.map((p) => p.root);
|
|
1958
|
+
}
|
|
1959
|
+
lookupVirtual(abs) {
|
|
1960
|
+
for (const provider of this.providers) {
|
|
1961
|
+
if (abs === provider.root) return provider.resolve("");
|
|
1962
|
+
if (abs.startsWith(provider.root === "/" ? "/" : provider.root + "/")) {
|
|
1963
|
+
const rel = abs.slice(provider.root === "/" ? 1 : provider.root.length + 1);
|
|
1964
|
+
const node = provider.resolve(rel);
|
|
1965
|
+
if (node) return node;
|
|
1966
|
+
if (provider.exclusive !== false) return null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
/** True when a miss at `abs` must be ENOENT rather than a volume lookup. */
|
|
1972
|
+
isUnderProvider(abs) {
|
|
1973
|
+
return this.providers.some(
|
|
1974
|
+
(p) => p.exclusive !== false && (abs === p.root || abs.startsWith(p.root + "/"))
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
/** Synthetic children a non-exclusive provider contributes to a directory. */
|
|
1978
|
+
virtualChildren(abs) {
|
|
1979
|
+
const out = [];
|
|
1980
|
+
for (const p of this.providers) {
|
|
1981
|
+
if (p.exclusive !== false) continue;
|
|
1982
|
+
if (abs !== p.root && !abs.startsWith(p.root + "/")) continue;
|
|
1983
|
+
const rel = abs === p.root ? "" : abs.slice(p.root.length + 1);
|
|
1984
|
+
const node = p.resolve(rel);
|
|
1985
|
+
if (node?.kind === "directory") out.push(...node.list?.() ?? []);
|
|
1986
|
+
}
|
|
1987
|
+
return out;
|
|
1988
|
+
}
|
|
1989
|
+
virtualIno(abs) {
|
|
1990
|
+
let ino = this.virtualInos.get(abs);
|
|
1991
|
+
if (ino === void 0) {
|
|
1992
|
+
ino = this.nextVirtualIno++;
|
|
1993
|
+
this.virtualInos.set(abs, ino);
|
|
1994
|
+
}
|
|
1995
|
+
return ino;
|
|
1996
|
+
}
|
|
1997
|
+
// ── permission model ─────────────────────────────────────────────────────
|
|
1998
|
+
/** True when `cred` may perform `mode` (R_OK/W_OK/X_OK) on a stat result. */
|
|
1999
|
+
permitted(st, mode, cred) {
|
|
2000
|
+
if (mode === F_OK) return true;
|
|
2001
|
+
if (cred.uid === 0) {
|
|
2002
|
+
if ((mode & X_OK) !== 0 && !st.isDirectory()) {
|
|
2003
|
+
return (st.mode & (S_IXUSR | S_IXGRP | S_IXOTH)) !== 0;
|
|
2004
|
+
}
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
let bits;
|
|
2008
|
+
if (st.uid === cred.uid) bits = (st.mode & (S_IRUSR | S_IWUSR | S_IXUSR)) >> 6;
|
|
2009
|
+
else if (cred.groups.includes(st.gid)) bits = (st.mode & (S_IRGRP | S_IWGRP | S_IXGRP)) >> 3;
|
|
2010
|
+
else bits = st.mode & (S_IROTH | S_IWOTH | S_IXOTH);
|
|
2011
|
+
return (bits & mode) === mode;
|
|
2012
|
+
}
|
|
2013
|
+
require(st, mode, cred, syscall, path) {
|
|
2014
|
+
if (!cred) return;
|
|
2015
|
+
if (!this.permitted(st, mode, cred)) throw new SysError("EACCES", syscall, path);
|
|
2016
|
+
}
|
|
2017
|
+
// ── path resolution ──────────────────────────────────────────────────────
|
|
2018
|
+
/**
|
|
2019
|
+
* Walk `abs` component by component, following symlinks and checking search
|
|
2020
|
+
* (`+x`) permission on every directory along the way, exactly like `namei`.
|
|
2021
|
+
*
|
|
2022
|
+
* Returns the fully resolved absolute path. Does *not* require the final
|
|
2023
|
+
* component to exist — callers decide whether a miss is fatal.
|
|
2024
|
+
*/
|
|
2025
|
+
resolvePath(abs, opts = {}) {
|
|
2026
|
+
const { cred, followFinal = true } = opts;
|
|
2027
|
+
const parts = segments(normalize(abs));
|
|
2028
|
+
let current = "";
|
|
2029
|
+
let hops = 0;
|
|
2030
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2031
|
+
const isFinal = i === parts.length - 1;
|
|
2032
|
+
const next = `${current}/${parts[i]}`;
|
|
2033
|
+
if (cred && current !== "") {
|
|
2034
|
+
const parentStat = this.tryLstat(current);
|
|
2035
|
+
if (parentStat && parentStat.isDirectory() && !this.permitted(parentStat, X_OK, cred)) {
|
|
2036
|
+
throw new SysError("EACCES", "open", abs);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
const st = this.tryLstat(next);
|
|
2040
|
+
if (st?.isSymbolicLink() && (!isFinal || followFinal)) {
|
|
2041
|
+
if (++hops > MAX_SYMLINKS) throw new SysError("ELOOP", "open", abs);
|
|
2042
|
+
const target = this.readlinkRaw(next);
|
|
2043
|
+
const resolved = isAbsolute(target) ? target : resolve(current === "" ? "/" : current, target);
|
|
2044
|
+
const rest = parts.slice(i + 1);
|
|
2045
|
+
const combined = rest.length ? `${resolved}/${rest.join("/")}` : resolved;
|
|
2046
|
+
return this.resolvePath(combined, { ...opts, cred });
|
|
2047
|
+
}
|
|
2048
|
+
current = next;
|
|
2049
|
+
if (!isFinal && st && !st.isDirectory()) throw new SysError("ENOTDIR", "open", abs);
|
|
2050
|
+
}
|
|
2051
|
+
return current === "" ? "/" : current;
|
|
2052
|
+
}
|
|
2053
|
+
/** lstat that returns null instead of throwing, for internal probing. */
|
|
2054
|
+
tryLstat(abs) {
|
|
2055
|
+
try {
|
|
2056
|
+
return this.lstat(abs);
|
|
2057
|
+
} catch {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
readlinkRaw(abs) {
|
|
2062
|
+
const virt = this.lookupVirtual(abs);
|
|
2063
|
+
if (virt) {
|
|
2064
|
+
if (virt.kind !== "symlink" || virt.target === void 0) throw new SysError("EINVAL", "readlink", abs);
|
|
2065
|
+
return virt.target;
|
|
2066
|
+
}
|
|
2067
|
+
try {
|
|
2068
|
+
return this.volume.readlinkSync(abs);
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
throw toSysError(e, "readlink", abs);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
// ── metadata ─────────────────────────────────────────────────────────────
|
|
2074
|
+
/** stat(2) — follows symlinks. */
|
|
2075
|
+
stat(abs, opts = {}) {
|
|
2076
|
+
const real = this.resolvePath(abs, { cred: opts.cred, followFinal: true });
|
|
2077
|
+
return this.lstat(real);
|
|
2078
|
+
}
|
|
2079
|
+
/** lstat(2) — does not follow a symlink in the final position. */
|
|
2080
|
+
lstat(abs) {
|
|
2081
|
+
const path = clean(abs);
|
|
2082
|
+
const virt = this.lookupVirtual(path);
|
|
2083
|
+
if (virt) return this.statFromVirtual(path, virt);
|
|
2084
|
+
if (this.isUnderProvider(path)) throw new SysError("ENOENT", "lstat", path);
|
|
2085
|
+
let raw;
|
|
2086
|
+
try {
|
|
2087
|
+
raw = this.volume.lstatSync(path);
|
|
2088
|
+
} catch (e) {
|
|
2089
|
+
throw toSysError(e, "lstat", path);
|
|
2090
|
+
}
|
|
2091
|
+
const kind = raw.isSymbolicLink() ? "symlink" : raw.isDirectory() ? "directory" : "file";
|
|
2092
|
+
return new Stats({
|
|
2093
|
+
mode: ifmtFor(kind) | raw.mode & PERM_MASK,
|
|
2094
|
+
size: raw.size,
|
|
2095
|
+
uid: raw.uid ?? 0,
|
|
2096
|
+
gid: raw.gid ?? 0,
|
|
2097
|
+
ino: raw.ino,
|
|
2098
|
+
nlink: raw.nlink,
|
|
2099
|
+
atimeMs: raw.atimeMs,
|
|
2100
|
+
mtimeMs: raw.mtimeMs,
|
|
2101
|
+
ctimeMs: raw.ctimeMs,
|
|
2102
|
+
birthtimeMs: raw.birthtimeMs
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
statFromVirtual(abs, node) {
|
|
2106
|
+
let size = node.size ?? 0;
|
|
2107
|
+
if (size === 0 && node.read && node.kind === "file") {
|
|
2108
|
+
try {
|
|
2109
|
+
const data = node.read();
|
|
2110
|
+
size = typeof data === "string" ? encoder.encode(data).length : data.length;
|
|
2111
|
+
} catch {
|
|
2112
|
+
size = 0;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const now = node.mtimeMs ?? Date.now();
|
|
2116
|
+
return new Stats({
|
|
2117
|
+
mode: ifmtFor(node.kind) | node.mode & PERM_MASK,
|
|
2118
|
+
size,
|
|
2119
|
+
uid: node.uid ?? 0,
|
|
2120
|
+
gid: node.gid ?? 0,
|
|
2121
|
+
ino: this.virtualIno(abs),
|
|
2122
|
+
nlink: node.kind === "directory" ? 2 : 1,
|
|
2123
|
+
atimeMs: now,
|
|
2124
|
+
mtimeMs: now,
|
|
2125
|
+
ctimeMs: now
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
exists(abs, cred) {
|
|
2129
|
+
try {
|
|
2130
|
+
this.stat(abs, { cred });
|
|
2131
|
+
return true;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
lexists(abs) {
|
|
2137
|
+
try {
|
|
2138
|
+
this.lstat(abs);
|
|
2139
|
+
return true;
|
|
2140
|
+
} catch {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
access(abs, mode = F_OK, cred) {
|
|
2145
|
+
const st = this.stat(abs, { cred });
|
|
2146
|
+
if (cred && !this.permitted(st, mode, cred)) throw new SysError("EACCES", "access", abs);
|
|
2147
|
+
}
|
|
2148
|
+
realpath(abs, cred) {
|
|
2149
|
+
const real = this.resolvePath(abs, { cred, followFinal: true });
|
|
2150
|
+
this.lstat(real);
|
|
2151
|
+
return real;
|
|
2152
|
+
}
|
|
2153
|
+
// ── reading ──────────────────────────────────────────────────────────────
|
|
2154
|
+
readFile(abs, cred) {
|
|
2155
|
+
const real = this.resolvePath(abs, { cred });
|
|
2156
|
+
const virt = this.lookupVirtual(real);
|
|
2157
|
+
if (virt) {
|
|
2158
|
+
if (virt.kind === "directory") throw new SysError("EISDIR", "read", real);
|
|
2159
|
+
if (!virt.read) throw new SysError("EACCES", "read", real);
|
|
2160
|
+
this.require(this.statFromVirtual(real, virt), R_OK, cred, "open", real);
|
|
2161
|
+
const data = virt.read();
|
|
2162
|
+
return typeof data === "string" ? encoder.encode(data) : data;
|
|
2163
|
+
}
|
|
2164
|
+
if (this.isUnderProvider(real)) throw new SysError("ENOENT", "open", real);
|
|
2165
|
+
const st = this.lstat(real);
|
|
2166
|
+
if (st.isDirectory()) throw new SysError("EISDIR", "read", real);
|
|
2167
|
+
this.require(st, R_OK, cred, "open", real);
|
|
2168
|
+
try {
|
|
2169
|
+
return this.volume.readFileSync(real);
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
throw toSysError(e, "open", real);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
readText(abs, cred) {
|
|
2175
|
+
return decoder.decode(this.readFile(abs, cred));
|
|
2176
|
+
}
|
|
2177
|
+
readdir(abs, cred) {
|
|
2178
|
+
const real = this.resolvePath(abs, { cred });
|
|
2179
|
+
const names = /* @__PURE__ */ new Set();
|
|
2180
|
+
const virt = this.lookupVirtual(real);
|
|
2181
|
+
if (virt) {
|
|
2182
|
+
if (virt.kind !== "directory") throw new SysError("ENOTDIR", "scandir", real);
|
|
2183
|
+
this.require(this.statFromVirtual(real, virt), R_OK, cred, "scandir", real);
|
|
2184
|
+
for (const name of virt.list?.() ?? []) names.add(name);
|
|
2185
|
+
} else if (this.isUnderProvider(real)) {
|
|
2186
|
+
throw new SysError("ENOENT", "scandir", real);
|
|
2187
|
+
}
|
|
2188
|
+
const hasReal = this.volumeHasDir(real);
|
|
2189
|
+
if (hasReal) {
|
|
2190
|
+
const st = this.lstat(real);
|
|
2191
|
+
if (!virt) {
|
|
2192
|
+
if (!st.isDirectory()) throw new SysError("ENOTDIR", "scandir", real);
|
|
2193
|
+
this.require(st, R_OK, cred, "scandir", real);
|
|
2194
|
+
}
|
|
2195
|
+
try {
|
|
2196
|
+
for (const name of this.volume.readdirSync(real)) names.add(name);
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
if (!virt) throw toSysError(e, "scandir", real);
|
|
2199
|
+
}
|
|
2200
|
+
} else if (!virt) {
|
|
2201
|
+
const st = this.lstat(real);
|
|
2202
|
+
if (!st.isDirectory()) throw new SysError("ENOTDIR", "scandir", real);
|
|
2203
|
+
throw new SysError("ENOENT", "scandir", real);
|
|
2204
|
+
}
|
|
2205
|
+
for (const p of this.providers) {
|
|
2206
|
+
if (dirname(p.root) === real) names.add(basename(p.root));
|
|
2207
|
+
}
|
|
2208
|
+
for (const name of this.virtualChildren(real)) names.add(name);
|
|
2209
|
+
return [...names].sort();
|
|
2210
|
+
}
|
|
2211
|
+
/** True when the volume itself has a real directory at `abs`. */
|
|
2212
|
+
volumeHasDir(abs) {
|
|
2213
|
+
try {
|
|
2214
|
+
return this.volume.lstatSync(abs).isDirectory();
|
|
2215
|
+
} catch {
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
readdirWithTypes(abs, cred) {
|
|
2220
|
+
const real = this.resolvePath(abs, { cred });
|
|
2221
|
+
return this.readdir(real, cred).map((name) => {
|
|
2222
|
+
try {
|
|
2223
|
+
return { name, kind: this.lstat(join(real, name)).kind };
|
|
2224
|
+
} catch {
|
|
2225
|
+
return { name, kind: "file" };
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
readlink(abs, cred) {
|
|
2230
|
+
const real = this.resolvePath(abs, { cred, followFinal: false });
|
|
2231
|
+
const st = this.lstat(real);
|
|
2232
|
+
if (!st.isSymbolicLink()) throw new SysError("EINVAL", "readlink", real);
|
|
2233
|
+
return this.readlinkRaw(real);
|
|
2234
|
+
}
|
|
2235
|
+
// ── writing ──────────────────────────────────────────────────────────────
|
|
2236
|
+
writeFile(abs, data, opts = {}) {
|
|
2237
|
+
const cred = opts.privileged ? void 0 : opts.cred;
|
|
2238
|
+
const real = this.resolvePath(abs, { cred });
|
|
2239
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
2240
|
+
const virt = this.lookupVirtual(real);
|
|
2241
|
+
if (virt) {
|
|
2242
|
+
if (!virt.write) throw new SysError("EACCES", "write", real);
|
|
2243
|
+
this.require(this.statFromVirtual(real, virt), W_OK, cred, "open", real);
|
|
2244
|
+
virt.write(bytes, opts.append ?? false);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (this.isUnderProvider(real)) throw new SysError("ENOENT", "open", real);
|
|
2248
|
+
const existing = this.tryLstat(real);
|
|
2249
|
+
if (existing) {
|
|
2250
|
+
if (existing.isDirectory()) throw new SysError("EISDIR", "open", real);
|
|
2251
|
+
this.require(existing, W_OK, cred, "open", real);
|
|
2252
|
+
} else {
|
|
2253
|
+
const parent = dirname(real);
|
|
2254
|
+
const pst = this.lstat(parent);
|
|
2255
|
+
if (!pst.isDirectory()) throw new SysError("ENOTDIR", "open", real);
|
|
2256
|
+
this.require(pst, W_OK | X_OK, cred, "open", real);
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
if (opts.append && existing) this.volume.appendFileSync(real, bytes);
|
|
2260
|
+
else this.volume.writeFileSync(real, bytes);
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
throw toSysError(e, "open", real);
|
|
2263
|
+
}
|
|
2264
|
+
if (!existing) {
|
|
2265
|
+
const mode = (opts.mode ?? 438) & ~(opts.cred?.umask ?? 0);
|
|
2266
|
+
this.volume.chmodSync(real, mode & PERM_MASK);
|
|
2267
|
+
this.volume.chownSync(real, opts.cred?.uid ?? 0, opts.cred?.gid ?? 0);
|
|
2268
|
+
} else if (opts.mode !== void 0) {
|
|
2269
|
+
this.volume.chmodSync(real, opts.mode & PERM_MASK);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
appendFile(abs, data, opts = {}) {
|
|
2273
|
+
this.writeFile(abs, data, { ...opts, append: true });
|
|
2274
|
+
}
|
|
2275
|
+
truncate(abs, len = 0, cred) {
|
|
2276
|
+
const real = this.resolvePath(abs, { cred });
|
|
2277
|
+
const st = this.lstat(real);
|
|
2278
|
+
if (st.isDirectory()) throw new SysError("EISDIR", "truncate", real);
|
|
2279
|
+
this.require(st, W_OK, cred, "truncate", real);
|
|
2280
|
+
try {
|
|
2281
|
+
this.volume.truncateSync(real, len);
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
throw toSysError(e, "truncate", real);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
mkdir(abs, opts = {}) {
|
|
2287
|
+
const cred = opts.cred;
|
|
2288
|
+
const mode = (opts.mode ?? 511) & ~(cred?.umask ?? 0) & PERM_MASK;
|
|
2289
|
+
const real = this.resolvePath(abs, { cred, followFinal: false });
|
|
2290
|
+
if (opts.recursive) {
|
|
2291
|
+
const parts = segments(real);
|
|
2292
|
+
let cur = "";
|
|
2293
|
+
for (const part of parts) {
|
|
2294
|
+
cur += "/" + part;
|
|
2295
|
+
const st = this.tryLstat(cur);
|
|
2296
|
+
if (st) {
|
|
2297
|
+
if (!st.isDirectory()) throw new SysError("ENOTDIR", "mkdir", cur);
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
this.mkdirOne(cur, mode, cred);
|
|
2301
|
+
}
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
if (this.lexists(real)) throw new SysError("EEXIST", "mkdir", real);
|
|
2305
|
+
this.mkdirOne(real, mode, cred);
|
|
2306
|
+
}
|
|
2307
|
+
mkdirOne(abs, mode, cred) {
|
|
2308
|
+
const parent = dirname(abs);
|
|
2309
|
+
const pst = this.lstat(parent);
|
|
2310
|
+
if (!pst.isDirectory()) throw new SysError("ENOTDIR", "mkdir", abs);
|
|
2311
|
+
this.require(pst, W_OK | X_OK, cred, "mkdir", abs);
|
|
2312
|
+
try {
|
|
2313
|
+
this.volume.mkdirSync(abs, { mode });
|
|
2314
|
+
} catch (e) {
|
|
2315
|
+
throw toSysError(e, "mkdir", abs);
|
|
2316
|
+
}
|
|
2317
|
+
this.volume.chmodSync(abs, mode);
|
|
2318
|
+
this.volume.chownSync(abs, cred?.uid ?? 0, cred?.gid ?? 0);
|
|
2319
|
+
}
|
|
2320
|
+
rmdir(abs, cred) {
|
|
2321
|
+
const real = this.resolvePath(abs, { cred, followFinal: false });
|
|
2322
|
+
const st = this.lstat(real);
|
|
2323
|
+
if (!st.isDirectory()) throw new SysError("ENOTDIR", "rmdir", real);
|
|
2324
|
+
if (this.readdir(real).length > 0) throw new SysError("ENOTEMPTY", "rmdir", real);
|
|
2325
|
+
this.requireParentWrite(real, cred, "rmdir");
|
|
2326
|
+
try {
|
|
2327
|
+
this.volume.rmdirSync(real);
|
|
2328
|
+
} catch (e) {
|
|
2329
|
+
throw toSysError(e, "rmdir", real);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
unlink(abs, cred) {
|
|
2333
|
+
const real = this.resolvePath(abs, { cred, followFinal: false });
|
|
2334
|
+
const st = this.lstat(real);
|
|
2335
|
+
if (st.isDirectory()) throw new SysError("EISDIR", "unlink", real);
|
|
2336
|
+
this.requireParentWrite(real, cred, "unlink");
|
|
2337
|
+
if (this.isUnderProvider(real)) throw new SysError("EPERM", "unlink", real);
|
|
2338
|
+
try {
|
|
2339
|
+
this.volume.unlinkSync(real);
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
throw toSysError(e, "unlink", real);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
/** Recursive delete, the engine behind `rm -r`. */
|
|
2345
|
+
rmrf(abs, cred) {
|
|
2346
|
+
const real = this.resolvePath(abs, { cred, followFinal: false });
|
|
2347
|
+
const st = this.lstat(real);
|
|
2348
|
+
if (!st.isDirectory()) {
|
|
2349
|
+
this.unlink(real, cred);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
for (const name of this.readdir(real, cred)) {
|
|
2353
|
+
this.rmrf(join(real, name), cred);
|
|
2354
|
+
}
|
|
2355
|
+
this.rmdir(real, cred);
|
|
2356
|
+
}
|
|
2357
|
+
requireParentWrite(abs, cred, syscall) {
|
|
2358
|
+
if (!cred) return;
|
|
2359
|
+
const parent = dirname(abs);
|
|
2360
|
+
const pst = this.lstat(parent);
|
|
2361
|
+
this.require(pst, W_OK | X_OK, cred, syscall, abs);
|
|
2362
|
+
if ((pst.mode & 512) !== 0 && cred.uid !== 0) {
|
|
2363
|
+
const st = this.lstat(abs);
|
|
2364
|
+
if (st.uid !== cred.uid && pst.uid !== cred.uid) throw new SysError("EPERM", syscall, abs);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
rename(from, to, cred) {
|
|
2368
|
+
const src = this.resolvePath(from, { cred, followFinal: false });
|
|
2369
|
+
const dst = this.resolvePath(to, { cred, followFinal: false });
|
|
2370
|
+
if (src === dst) return;
|
|
2371
|
+
this.lstat(src);
|
|
2372
|
+
if (contains(src, dst) && src !== dst) throw new SysError("EINVAL", "rename", src, dst);
|
|
2373
|
+
this.requireParentWrite(src, cred, "rename");
|
|
2374
|
+
const dstParent = dirname(dst);
|
|
2375
|
+
const dpst = this.lstat(dstParent);
|
|
2376
|
+
this.require(dpst, W_OK | X_OK, cred, "rename", dst);
|
|
2377
|
+
try {
|
|
2378
|
+
this.volume.renameSync(src, dst);
|
|
2379
|
+
} catch (e) {
|
|
2380
|
+
throw toSysError(e, "rename", src);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
copyFile(from, to, cred) {
|
|
2384
|
+
const data = this.readFile(from, cred);
|
|
2385
|
+
const st = this.stat(from, { cred });
|
|
2386
|
+
this.writeFile(to, data, { cred, mode: st.perms });
|
|
2387
|
+
this.chmod(to, st.perms, cred);
|
|
2388
|
+
}
|
|
2389
|
+
symlink(target, linkPath, cred) {
|
|
2390
|
+
const real = this.resolvePath(linkPath, { cred, followFinal: false });
|
|
2391
|
+
if (this.lexists(real)) throw new SysError("EEXIST", "symlink", real);
|
|
2392
|
+
const parent = dirname(real);
|
|
2393
|
+
const pst = this.lstat(parent);
|
|
2394
|
+
this.require(pst, W_OK | X_OK, cred, "symlink", real);
|
|
2395
|
+
try {
|
|
2396
|
+
this.volume.symlinkSync(target, real);
|
|
2397
|
+
} catch (e) {
|
|
2398
|
+
throw toSysError(e, "symlink", real);
|
|
2399
|
+
}
|
|
2400
|
+
this.volume.lchownSync(real, cred?.uid ?? 0, cred?.gid ?? 0);
|
|
2401
|
+
}
|
|
2402
|
+
link(existing, newPath, cred) {
|
|
2403
|
+
const src = this.resolvePath(existing, { cred });
|
|
2404
|
+
const dst = this.resolvePath(newPath, { cred, followFinal: false });
|
|
2405
|
+
const st = this.lstat(src);
|
|
2406
|
+
if (st.isDirectory()) throw new SysError("EPERM", "link", src);
|
|
2407
|
+
if (this.lexists(dst)) throw new SysError("EEXIST", "link", dst);
|
|
2408
|
+
const pst = this.lstat(dirname(dst));
|
|
2409
|
+
this.require(pst, W_OK | X_OK, cred, "link", dst);
|
|
2410
|
+
try {
|
|
2411
|
+
this.volume.linkSync(src, dst);
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
throw toSysError(e, "link", src, dst);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
chmod(abs, mode, cred, follow = true) {
|
|
2417
|
+
const real = this.resolvePath(abs, { cred, followFinal: follow });
|
|
2418
|
+
const st = this.lstat(real);
|
|
2419
|
+
if (cred && cred.uid !== 0 && st.uid !== cred.uid) throw new SysError("EPERM", "chmod", real);
|
|
2420
|
+
try {
|
|
2421
|
+
if (st.isSymbolicLink()) this.volume.lchmodSync(real, mode & PERM_MASK);
|
|
2422
|
+
else this.volume.chmodSync(real, mode & PERM_MASK);
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
throw toSysError(e, "chmod", real);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
chown(abs, uid, gid, cred, follow = true) {
|
|
2428
|
+
const real = this.resolvePath(abs, { cred, followFinal: follow });
|
|
2429
|
+
const st = this.lstat(real);
|
|
2430
|
+
if (cred && cred.uid !== 0) {
|
|
2431
|
+
if (uid !== -1 && uid !== st.uid) throw new SysError("EPERM", "chown", real);
|
|
2432
|
+
if (gid !== -1 && !cred.groups.includes(gid)) throw new SysError("EPERM", "chown", real);
|
|
2433
|
+
if (st.uid !== cred.uid) throw new SysError("EPERM", "chown", real);
|
|
2434
|
+
}
|
|
2435
|
+
const nextUid = uid === -1 ? st.uid : uid;
|
|
2436
|
+
const nextGid = gid === -1 ? st.gid : gid;
|
|
2437
|
+
try {
|
|
2438
|
+
if (st.isSymbolicLink()) this.volume.lchownSync(real, nextUid, nextGid);
|
|
2439
|
+
else this.volume.chownSync(real, nextUid, nextGid);
|
|
2440
|
+
} catch (e) {
|
|
2441
|
+
throw toSysError(e, "chown", real);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
utimes(abs, atimeMs, mtimeMs, cred) {
|
|
2445
|
+
const real = this.resolvePath(abs, { cred });
|
|
2446
|
+
try {
|
|
2447
|
+
this.volume.utimesSync(real, new Date(atimeMs), new Date(mtimeMs));
|
|
2448
|
+
} catch (e) {
|
|
2449
|
+
throw toSysError(e, "utimes", real);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
/** `touch` semantics: create when missing, otherwise bump the timestamps. */
|
|
2453
|
+
touch(abs, cred, timeMs = Date.now()) {
|
|
2454
|
+
if (this.lexists(abs)) this.utimes(abs, timeMs, timeMs, cred);
|
|
2455
|
+
else this.writeFile(abs, new Uint8Array(0), { cred, mode: 438 });
|
|
2456
|
+
}
|
|
2457
|
+
// ── bulk helpers ─────────────────────────────────────────────────────────
|
|
2458
|
+
/** Depth-first walk yielding absolute paths. Symlinks are not followed. */
|
|
2459
|
+
*walk(abs, opts = {}) {
|
|
2460
|
+
const { includeSelf = true, cred, maxDepth = Infinity } = opts;
|
|
2461
|
+
const start = clean(abs);
|
|
2462
|
+
if (includeSelf) yield start;
|
|
2463
|
+
const recurse = (dir, depth) => {
|
|
2464
|
+
if (depth > maxDepth) return [];
|
|
2465
|
+
let names;
|
|
2466
|
+
try {
|
|
2467
|
+
names = this.readdir(dir, cred);
|
|
2468
|
+
} catch {
|
|
2469
|
+
return [];
|
|
2470
|
+
}
|
|
2471
|
+
const out = [];
|
|
2472
|
+
for (const name of names) {
|
|
2473
|
+
const child = join(dir, name);
|
|
2474
|
+
out.push(child);
|
|
2475
|
+
let st2 = null;
|
|
2476
|
+
try {
|
|
2477
|
+
st2 = this.lstat(child);
|
|
2478
|
+
} catch {
|
|
2479
|
+
}
|
|
2480
|
+
if (st2?.isDirectory()) out.push(...recurse(child, depth + 1));
|
|
2481
|
+
}
|
|
2482
|
+
return out;
|
|
2483
|
+
};
|
|
2484
|
+
let st = null;
|
|
2485
|
+
try {
|
|
2486
|
+
st = this.lstat(start);
|
|
2487
|
+
} catch {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
if (st.isDirectory()) yield* recurse(start, 1);
|
|
2491
|
+
}
|
|
2492
|
+
/** Recursive copy used by `cp -r` and the container's `copyIn` helper. */
|
|
2493
|
+
copyTree(from, to, cred) {
|
|
2494
|
+
const st = this.lstat(this.resolvePath(from, { cred, followFinal: false }));
|
|
2495
|
+
if (st.isSymbolicLink()) {
|
|
2496
|
+
this.symlink(this.readlink(from, cred), to, cred);
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
if (!st.isDirectory()) {
|
|
2500
|
+
this.copyFile(from, to, cred);
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
if (!this.lexists(to)) this.mkdir(to, { cred, mode: st.perms, recursive: true });
|
|
2504
|
+
for (const name of this.readdir(from, cred)) {
|
|
2505
|
+
this.copyTree(join(from, name), join(to, name), cred);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
/** Free/used byte accounting for `df` and `du`. */
|
|
2509
|
+
usage(abs = "/") {
|
|
2510
|
+
let files = 0;
|
|
2511
|
+
let dirs = 0;
|
|
2512
|
+
let bytes = 0;
|
|
2513
|
+
for (const p of this.walk(abs)) {
|
|
2514
|
+
let st = null;
|
|
2515
|
+
try {
|
|
2516
|
+
st = this.lstat(p);
|
|
2517
|
+
} catch {
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
if (st.isDirectory()) dirs++;
|
|
2521
|
+
else {
|
|
2522
|
+
files++;
|
|
2523
|
+
bytes += st.size;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return { files, dirs, bytes };
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
// src/runtime/memory-volume.ts
|
|
2531
|
+
var VolumeError = class extends Error {
|
|
2532
|
+
constructor(code, operation, path) {
|
|
2533
|
+
super(`${code}: ${operation}, '${path}'`);
|
|
2534
|
+
this.code = code;
|
|
2535
|
+
this.name = "VolumeError";
|
|
2536
|
+
}
|
|
2537
|
+
code;
|
|
2538
|
+
};
|
|
2539
|
+
var MemoryStat = class {
|
|
2540
|
+
constructor(inode) {
|
|
2541
|
+
this.inode = inode;
|
|
2542
|
+
}
|
|
2543
|
+
inode;
|
|
2544
|
+
get mode() {
|
|
2545
|
+
return this.inode.mode;
|
|
2546
|
+
}
|
|
2547
|
+
get size() {
|
|
2548
|
+
return this.inode.kind === "file" ? this.inode.data.length : this.inode.kind === "symlink" ? new TextEncoder().encode(this.inode.target).length : 0;
|
|
2549
|
+
}
|
|
2550
|
+
get uid() {
|
|
2551
|
+
return this.inode.uid;
|
|
2552
|
+
}
|
|
2553
|
+
get gid() {
|
|
2554
|
+
return this.inode.gid;
|
|
2555
|
+
}
|
|
2556
|
+
get ino() {
|
|
2557
|
+
return this.inode.ino;
|
|
2558
|
+
}
|
|
2559
|
+
get nlink() {
|
|
2560
|
+
return this.inode.nlink;
|
|
2561
|
+
}
|
|
2562
|
+
get atimeMs() {
|
|
2563
|
+
return this.inode.atimeMs;
|
|
2564
|
+
}
|
|
2565
|
+
get mtimeMs() {
|
|
2566
|
+
return this.inode.mtimeMs;
|
|
2567
|
+
}
|
|
2568
|
+
get ctimeMs() {
|
|
2569
|
+
return this.inode.ctimeMs;
|
|
2570
|
+
}
|
|
2571
|
+
get birthtimeMs() {
|
|
2572
|
+
return this.inode.birthtimeMs;
|
|
2573
|
+
}
|
|
2574
|
+
isFile() {
|
|
2575
|
+
return this.inode.kind === "file";
|
|
2576
|
+
}
|
|
2577
|
+
isDirectory() {
|
|
2578
|
+
return this.inode.kind === "directory";
|
|
2579
|
+
}
|
|
2580
|
+
isSymbolicLink() {
|
|
2581
|
+
return this.inode.kind === "symlink";
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
var encoder2 = new TextEncoder();
|
|
2585
|
+
var MemoryVolume = class {
|
|
2586
|
+
entries = /* @__PURE__ */ new Map();
|
|
2587
|
+
nextIno = 2;
|
|
2588
|
+
constructor() {
|
|
2589
|
+
this.entries.set("/", this.inode("directory", 493));
|
|
2590
|
+
}
|
|
2591
|
+
readFileSync(path) {
|
|
2592
|
+
const key = this.key(path);
|
|
2593
|
+
const node = this.required(key, "open");
|
|
2594
|
+
if (node.kind === "directory") throw new VolumeError("EISDIR", "read", key);
|
|
2595
|
+
if (node.kind !== "file") throw new VolumeError("EINVAL", "read", key);
|
|
2596
|
+
node.atimeMs = Date.now();
|
|
2597
|
+
return node.data.slice();
|
|
2598
|
+
}
|
|
2599
|
+
writeFileSync(path, data) {
|
|
2600
|
+
const key = this.key(path);
|
|
2601
|
+
this.requireParent(key, "open");
|
|
2602
|
+
const bytes = typeof data === "string" ? encoder2.encode(data) : data.slice();
|
|
2603
|
+
const current = this.entries.get(key);
|
|
2604
|
+
if (current?.kind === "directory") throw new VolumeError("EISDIR", "open", key);
|
|
2605
|
+
if (current?.kind === "symlink") throw new VolumeError("EINVAL", "open", key);
|
|
2606
|
+
if (current) {
|
|
2607
|
+
current.data = bytes;
|
|
2608
|
+
this.touchChanged(current, true);
|
|
2609
|
+
} else {
|
|
2610
|
+
this.entries.set(key, this.inode("file", 438, bytes));
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
appendFileSync(path, data) {
|
|
2614
|
+
const key = this.key(path);
|
|
2615
|
+
const bytes = typeof data === "string" ? encoder2.encode(data) : data;
|
|
2616
|
+
const current = this.entries.get(key);
|
|
2617
|
+
if (!current) return this.writeFileSync(key, bytes);
|
|
2618
|
+
if (current.kind !== "file") throw new VolumeError(current.kind === "directory" ? "EISDIR" : "EINVAL", "open", key);
|
|
2619
|
+
const next = new Uint8Array(current.data.length + bytes.length);
|
|
2620
|
+
next.set(current.data);
|
|
2621
|
+
next.set(bytes, current.data.length);
|
|
2622
|
+
current.data = next;
|
|
2623
|
+
this.touchChanged(current, true);
|
|
2624
|
+
}
|
|
2625
|
+
readdirSync(path) {
|
|
2626
|
+
const key = this.key(path);
|
|
2627
|
+
const node = this.required(key, "scandir");
|
|
2628
|
+
if (node.kind !== "directory") throw new VolumeError("ENOTDIR", "scandir", key);
|
|
2629
|
+
const prefix = key === "/" ? "/" : `${key}/`;
|
|
2630
|
+
const names = /* @__PURE__ */ new Set();
|
|
2631
|
+
for (const candidate of this.entries.keys()) {
|
|
2632
|
+
if (!candidate.startsWith(prefix) || candidate === key) continue;
|
|
2633
|
+
const rest = candidate.slice(prefix.length);
|
|
2634
|
+
if (rest && !rest.includes("/")) names.add(rest);
|
|
2635
|
+
}
|
|
2636
|
+
node.atimeMs = Date.now();
|
|
2637
|
+
return [...names].sort();
|
|
2638
|
+
}
|
|
2639
|
+
lstatSync(path) {
|
|
2640
|
+
return new MemoryStat(this.required(this.key(path), "lstat"));
|
|
2641
|
+
}
|
|
2642
|
+
readlinkSync(path) {
|
|
2643
|
+
const key = this.key(path);
|
|
2644
|
+
const node = this.required(key, "readlink");
|
|
2645
|
+
if (node.kind !== "symlink") throw new VolumeError("EINVAL", "readlink", key);
|
|
2646
|
+
return node.target;
|
|
2647
|
+
}
|
|
2648
|
+
mkdirSync(path, options = {}) {
|
|
2649
|
+
const key = this.key(path);
|
|
2650
|
+
if (this.entries.has(key)) throw new VolumeError("EEXIST", "mkdir", key);
|
|
2651
|
+
this.requireParent(key, "mkdir");
|
|
2652
|
+
this.entries.set(key, this.inode("directory", options.mode ?? 511));
|
|
2653
|
+
}
|
|
2654
|
+
rmdirSync(path) {
|
|
2655
|
+
const key = this.key(path);
|
|
2656
|
+
if (key === "/") throw new VolumeError("EBUSY", "rmdir", key);
|
|
2657
|
+
const node = this.required(key, "rmdir");
|
|
2658
|
+
if (node.kind !== "directory") throw new VolumeError("ENOTDIR", "rmdir", key);
|
|
2659
|
+
const prefix = `${key}/`;
|
|
2660
|
+
if ([...this.entries.keys()].some((candidate) => candidate.startsWith(prefix))) {
|
|
2661
|
+
throw new VolumeError("ENOTEMPTY", "rmdir", key);
|
|
2662
|
+
}
|
|
2663
|
+
this.entries.delete(key);
|
|
2664
|
+
}
|
|
2665
|
+
unlinkSync(path) {
|
|
2666
|
+
const key = this.key(path);
|
|
2667
|
+
const node = this.required(key, "unlink");
|
|
2668
|
+
if (node.kind === "directory") throw new VolumeError("EISDIR", "unlink", key);
|
|
2669
|
+
this.entries.delete(key);
|
|
2670
|
+
node.nlink--;
|
|
2671
|
+
node.ctimeMs = Date.now();
|
|
2672
|
+
}
|
|
2673
|
+
renameSync(from, to) {
|
|
2674
|
+
const source = this.key(from);
|
|
2675
|
+
const dest = this.key(to);
|
|
2676
|
+
const node = this.required(source, "rename");
|
|
2677
|
+
this.requireParent(dest, "rename");
|
|
2678
|
+
if (source === "/") throw new VolumeError("EBUSY", "rename", source);
|
|
2679
|
+
if (dest.startsWith(`${source}/`)) throw new VolumeError("EINVAL", "rename", source);
|
|
2680
|
+
const existing = this.entries.get(dest);
|
|
2681
|
+
if (existing?.kind === "directory" && this.readdirSync(dest).length > 0) {
|
|
2682
|
+
throw new VolumeError("ENOTEMPTY", "rename", dest);
|
|
2683
|
+
}
|
|
2684
|
+
if (existing) {
|
|
2685
|
+
this.entries.delete(dest);
|
|
2686
|
+
existing.nlink--;
|
|
2687
|
+
}
|
|
2688
|
+
const moved = [...this.entries.entries()].filter(([path]) => path === source || path.startsWith(`${source}/`)).sort(([a], [b]) => a.length - b.length);
|
|
2689
|
+
for (const [path] of moved) this.entries.delete(path);
|
|
2690
|
+
for (const [path, value] of moved) {
|
|
2691
|
+
this.entries.set(dest + path.slice(source.length), value);
|
|
2692
|
+
value.ctimeMs = Date.now();
|
|
2693
|
+
}
|
|
2694
|
+
node.ctimeMs = Date.now();
|
|
2695
|
+
}
|
|
2696
|
+
symlinkSync(target, path) {
|
|
2697
|
+
const key = this.key(path);
|
|
2698
|
+
if (this.entries.has(key)) throw new VolumeError("EEXIST", "symlink", key);
|
|
2699
|
+
this.requireParent(key, "symlink");
|
|
2700
|
+
const node = this.inode("symlink", 511);
|
|
2701
|
+
node.target = target;
|
|
2702
|
+
this.entries.set(key, node);
|
|
2703
|
+
}
|
|
2704
|
+
linkSync(existing, path) {
|
|
2705
|
+
const source = this.key(existing);
|
|
2706
|
+
const dest = this.key(path);
|
|
2707
|
+
const node = this.required(source, "link");
|
|
2708
|
+
if (node.kind === "directory") throw new VolumeError("EPERM", "link", source);
|
|
2709
|
+
if (this.entries.has(dest)) throw new VolumeError("EEXIST", "link", dest);
|
|
2710
|
+
this.requireParent(dest, "link");
|
|
2711
|
+
node.nlink++;
|
|
2712
|
+
node.ctimeMs = Date.now();
|
|
2713
|
+
this.entries.set(dest, node);
|
|
2714
|
+
}
|
|
2715
|
+
truncateSync(path, length = 0) {
|
|
2716
|
+
const key = this.key(path);
|
|
2717
|
+
const node = this.required(key, "truncate");
|
|
2718
|
+
if (node.kind !== "file") throw new VolumeError(node.kind === "directory" ? "EISDIR" : "EINVAL", "truncate", key);
|
|
2719
|
+
const data = new Uint8Array(length);
|
|
2720
|
+
data.set(node.data.subarray(0, length));
|
|
2721
|
+
node.data = data;
|
|
2722
|
+
this.touchChanged(node, true);
|
|
2723
|
+
}
|
|
2724
|
+
chmodSync(path, mode) {
|
|
2725
|
+
this.setMode(path, mode);
|
|
2726
|
+
}
|
|
2727
|
+
lchmodSync(path, mode) {
|
|
2728
|
+
this.setMode(path, mode);
|
|
2729
|
+
}
|
|
2730
|
+
chownSync(path, uid, gid) {
|
|
2731
|
+
this.setOwner(path, uid, gid);
|
|
2732
|
+
}
|
|
2733
|
+
lchownSync(path, uid, gid) {
|
|
2734
|
+
this.setOwner(path, uid, gid);
|
|
2735
|
+
}
|
|
2736
|
+
utimesSync(path, atime, mtime) {
|
|
2737
|
+
const node = this.required(this.key(path), "utimes");
|
|
2738
|
+
node.atimeMs = atime.getTime();
|
|
2739
|
+
node.mtimeMs = mtime.getTime();
|
|
2740
|
+
node.ctimeMs = Date.now();
|
|
2741
|
+
}
|
|
2742
|
+
getStats() {
|
|
2743
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2744
|
+
let totalBytes = 0;
|
|
2745
|
+
let fileCount = 0;
|
|
2746
|
+
let directoryCount = 0;
|
|
2747
|
+
for (const node of this.entries.values()) {
|
|
2748
|
+
if (seen.has(node.ino)) continue;
|
|
2749
|
+
seen.add(node.ino);
|
|
2750
|
+
if (node.kind === "directory") directoryCount++;
|
|
2751
|
+
else {
|
|
2752
|
+
fileCount++;
|
|
2753
|
+
totalBytes += node.kind === "file" ? node.data.length : encoder2.encode(node.target).length;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return { totalBytes, fileCount, directoryCount, dirCount: directoryCount };
|
|
2757
|
+
}
|
|
2758
|
+
snapshot() {
|
|
2759
|
+
return [...this.entries.entries()].map(([path, node]) => ({ path, ...node, data: [...node.data] }));
|
|
2760
|
+
}
|
|
2761
|
+
restore(snapshot) {
|
|
2762
|
+
const inodes = /* @__PURE__ */ new Map();
|
|
2763
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2764
|
+
for (const item of snapshot) {
|
|
2765
|
+
let inode = inodes.get(item.ino);
|
|
2766
|
+
if (!inode) {
|
|
2767
|
+
inode = { ...item, data: new Uint8Array(item.data) };
|
|
2768
|
+
inodes.set(item.ino, inode);
|
|
2769
|
+
}
|
|
2770
|
+
entries.set(this.key(item.path), inode);
|
|
2771
|
+
}
|
|
2772
|
+
if (!entries.has("/")) throw new VolumeError("EINVAL", "restore", "/");
|
|
2773
|
+
this.entries.clear();
|
|
2774
|
+
for (const [path, inode] of entries) this.entries.set(path, inode);
|
|
2775
|
+
this.nextIno = Math.max(1, ...inodes.keys()) + 1;
|
|
2776
|
+
}
|
|
2777
|
+
/** Serializable representation used by RuntimePod snapshots. */
|
|
2778
|
+
export() {
|
|
2779
|
+
return [...this.entries.entries()].map(([path, node]) => ({
|
|
2780
|
+
path,
|
|
2781
|
+
kind: node.kind,
|
|
2782
|
+
mode: node.mode,
|
|
2783
|
+
uid: node.uid,
|
|
2784
|
+
gid: node.gid,
|
|
2785
|
+
...node.kind === "file" ? { data: [...node.data] } : {},
|
|
2786
|
+
...node.kind === "symlink" ? { target: node.target } : {}
|
|
2787
|
+
}));
|
|
2788
|
+
}
|
|
2789
|
+
setMode(path, mode) {
|
|
2790
|
+
const node = this.required(this.key(path), "chmod");
|
|
2791
|
+
node.mode = mode & 4095;
|
|
2792
|
+
node.ctimeMs = Date.now();
|
|
2793
|
+
}
|
|
2794
|
+
setOwner(path, uid, gid) {
|
|
2795
|
+
const node = this.required(this.key(path), "chown");
|
|
2796
|
+
node.uid = uid;
|
|
2797
|
+
node.gid = gid;
|
|
2798
|
+
node.ctimeMs = Date.now();
|
|
2799
|
+
}
|
|
2800
|
+
key(path) {
|
|
2801
|
+
const clean2 = clean(path);
|
|
2802
|
+
if (!isAbsolute(clean2)) throw new VolumeError("EINVAL", "path", path);
|
|
2803
|
+
return clean2;
|
|
2804
|
+
}
|
|
2805
|
+
required(path, operation) {
|
|
2806
|
+
const node = this.entries.get(path);
|
|
2807
|
+
if (!node) throw new VolumeError("ENOENT", operation, path);
|
|
2808
|
+
return node;
|
|
2809
|
+
}
|
|
2810
|
+
requireParent(path, operation) {
|
|
2811
|
+
const parent = this.required(dirname(path), operation);
|
|
2812
|
+
if (parent.kind !== "directory") throw new VolumeError("ENOTDIR", operation, path);
|
|
2813
|
+
return parent;
|
|
2814
|
+
}
|
|
2815
|
+
inode(kind, mode, data = new Uint8Array(0)) {
|
|
2816
|
+
const now = Date.now();
|
|
2817
|
+
return {
|
|
2818
|
+
ino: kind === "directory" && this.entries.size === 0 ? 1 : this.nextIno++,
|
|
2819
|
+
kind,
|
|
2820
|
+
mode: mode & 4095,
|
|
2821
|
+
uid: 0,
|
|
2822
|
+
gid: 0,
|
|
2823
|
+
nlink: 1,
|
|
2824
|
+
data,
|
|
2825
|
+
target: "",
|
|
2826
|
+
atimeMs: now,
|
|
2827
|
+
mtimeMs: now,
|
|
2828
|
+
ctimeMs: now,
|
|
2829
|
+
birthtimeMs: now
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
touchChanged(node, modified) {
|
|
2833
|
+
const now = Date.now();
|
|
2834
|
+
node.ctimeMs = now;
|
|
2835
|
+
if (modified) node.mtimeMs = now;
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
exports.CAPABILITIES = CAPABILITIES;
|
|
2840
|
+
exports.DescriptorTable = DescriptorTable;
|
|
2841
|
+
exports.DirectoryDescription = DirectoryDescription;
|
|
2842
|
+
exports.ERRNO_NAMES = ERRNO_NAMES;
|
|
2843
|
+
exports.Errno = Errno;
|
|
2844
|
+
exports.FileDescription = FileDescription;
|
|
2845
|
+
exports.FileService = FileService;
|
|
2846
|
+
exports.HostAbiClient = HostAbiClient;
|
|
2847
|
+
exports.HostAbiError = HostAbiError;
|
|
2848
|
+
exports.HostCallClient = HostCallClient;
|
|
2849
|
+
exports.HostCallServer = HostCallServer;
|
|
2850
|
+
exports.HostTransportError = HostTransportError;
|
|
2851
|
+
exports.Inode = Inode;
|
|
2852
|
+
exports.MemoryVolume = MemoryVolume;
|
|
2853
|
+
exports.OP_NAMES = OP_NAMES;
|
|
2854
|
+
exports.O_APPEND = O_APPEND;
|
|
2855
|
+
exports.O_CLOEXEC = O_CLOEXEC;
|
|
2856
|
+
exports.O_CREAT = O_CREAT;
|
|
2857
|
+
exports.O_DIRECTORY = O_DIRECTORY;
|
|
2858
|
+
exports.O_EXCL = O_EXCL;
|
|
2859
|
+
exports.O_NONBLOCK = O_NONBLOCK;
|
|
2860
|
+
exports.O_RDONLY = O_RDONLY;
|
|
2861
|
+
exports.O_RDWR = O_RDWR;
|
|
2862
|
+
exports.O_TRUNC = O_TRUNC;
|
|
2863
|
+
exports.O_WRONLY = O_WRONLY;
|
|
2864
|
+
exports.Op = Op;
|
|
2865
|
+
exports.PipeReadEnd = PipeReadEnd;
|
|
2866
|
+
exports.PipeWriteEnd = PipeWriteEnd;
|
|
2867
|
+
exports.PosixError = PosixError;
|
|
2868
|
+
exports.ProtocolError = ProtocolError;
|
|
2869
|
+
exports.ROOT_CRED = ROOT_CRED;
|
|
2870
|
+
exports.Reader = Reader;
|
|
2871
|
+
exports.SBX_HOST_ABI_VERSION = SBX_HOST_ABI_VERSION;
|
|
2872
|
+
exports.SBX_REQUEST_HEADER_BYTES = SBX_REQUEST_HEADER_BYTES;
|
|
2873
|
+
exports.SBX_RESPONSE_HEADER_BYTES = SBX_RESPONSE_HEADER_BYTES;
|
|
2874
|
+
exports.SEEK_CUR = SEEK_CUR;
|
|
2875
|
+
exports.SEEK_END = SEEK_END;
|
|
2876
|
+
exports.SEEK_SET = SEEK_SET;
|
|
2877
|
+
exports.StreamDescription = StreamDescription;
|
|
2878
|
+
exports.Vfs = Vfs;
|
|
2879
|
+
exports.Writer = Writer;
|
|
2880
|
+
exports.accessMode = accessMode;
|
|
2881
|
+
exports.createHostAbiServer = createHostAbiServer;
|
|
2882
|
+
exports.createHostTransportBuffers = createHostTransportBuffers;
|
|
2883
|
+
exports.createPipe = createPipe;
|
|
2884
|
+
exports.decodeRequest = decodeRequest;
|
|
2885
|
+
exports.decodeResponse = decodeResponse;
|
|
2886
|
+
exports.encodeRequest = encodeRequest;
|
|
2887
|
+
exports.encodeResponse = encodeResponse;
|
|
2888
|
+
exports.failure = failure;
|
|
2889
|
+
exports.makeCred = makeCred;
|
|
2890
|
+
exports.toPosixError = toPosixError;
|
|
2891
|
+
//# sourceMappingURL=python-abi.cjs.map
|
|
2892
|
+
//# sourceMappingURL=python-abi.cjs.map
|