@aptre/v86 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,873 @@
1
+ // VirtioV86FS: custom virtio device for v86fs guest filesystem
2
+ // PCI device ID 0x107F (virtio type 63, last slot in modern range)
3
+ // Three virtqueues: hipriq (metadata), requestq (data), notifyq (push invalidation)
4
+
5
+ import { LOG_PCI } from './const.js'
6
+ import { dbg_log } from './log.js'
7
+ import { VirtIO, VIRTIO_F_VERSION_1 } from './virtio.js'
8
+ import { BusConnector } from './bus.js'
9
+
10
+ interface VirtioV86FSCPU {
11
+ io: {
12
+ register_read(
13
+ port: number,
14
+ device: object,
15
+ r8?: ((port: number) => number) | undefined,
16
+ r16?: ((port: number) => number) | undefined,
17
+ r32?: ((port: number) => number) | undefined,
18
+ ): void
19
+ register_write(
20
+ port: number,
21
+ device: object,
22
+ w8?: ((port: number) => void) | undefined,
23
+ w16?: ((port: number) => void) | undefined,
24
+ w32?: ((port: number) => void) | undefined,
25
+ ): void
26
+ }
27
+ devices: {
28
+ pci: {
29
+ register_device(device: VirtIO): void
30
+ raise_irq(pci_id: number): void
31
+ lower_irq(pci_id: number): void
32
+ }
33
+ }
34
+ read16(addr: number): number
35
+ read32s(addr: number): number
36
+ write16(addr: number, value: number): void
37
+ write32(addr: number, value: number): void
38
+ read_blob(addr: number, length: number): Uint8Array
39
+ write_blob(blob: Uint8Array, addr: number): void
40
+ zero_memory(addr: number, length: number): void
41
+ memory_size: Int32Array
42
+ }
43
+
44
+ // Protocol message types
45
+ const V86FS_MSG_MOUNT = 0x00
46
+ const V86FS_MSG_LOOKUP = 0x01
47
+ const V86FS_MSG_GETATTR = 0x02
48
+ const V86FS_MSG_READDIR = 0x03
49
+ const V86FS_MSG_OPEN = 0x04
50
+ const V86FS_MSG_CLOSE = 0x05
51
+ const V86FS_MSG_READ = 0x06
52
+ const V86FS_MSG_CREATE = 0x07
53
+ const V86FS_MSG_WRITE = 0x08
54
+ const V86FS_MSG_MKDIR = 0x09
55
+ const V86FS_MSG_SETATTR = 0x0a
56
+ const V86FS_MSG_FSYNC = 0x0b
57
+ const V86FS_MSG_UNLINK = 0x0c
58
+ const V86FS_MSG_RENAME = 0x0d
59
+ const V86FS_MSG_SYMLINK = 0x0e
60
+ const V86FS_MSG_READLINK = 0x0f
61
+ const V86FS_MSG_STATFS = 0x10
62
+ const V86FS_MSG_INVALIDATE = 0x20
63
+ const V86FS_MSG_INVALIDATE_DIR = 0x21
64
+
65
+ // Response types
66
+ const V86FS_MSG_MOUNT_R = 0x80
67
+ const V86FS_MSG_LOOKUP_R = 0x81
68
+ const V86FS_MSG_GETATTR_R = 0x82
69
+ const V86FS_MSG_READDIR_R = 0x83
70
+ const V86FS_MSG_OPEN_R = 0x84
71
+ const V86FS_MSG_CLOSE_R = 0x85
72
+ const V86FS_MSG_READ_R = 0x86
73
+ const V86FS_MSG_CREATE_R = 0x87
74
+ const V86FS_MSG_WRITE_R = 0x88
75
+ const V86FS_MSG_MKDIR_R = 0x89
76
+ const V86FS_MSG_SETATTR_R = 0x8a
77
+ const V86FS_MSG_FSYNC_R = 0x8b
78
+ const V86FS_MSG_UNLINK_R = 0x8c
79
+ const V86FS_MSG_RENAME_R = 0x8d
80
+ const V86FS_MSG_SYMLINK_R = 0x8e
81
+ const V86FS_MSG_READLINK_R = 0x8f
82
+ const V86FS_MSG_STATFS_R = 0x90
83
+ const _V86FS_MSG_ERROR_R = 0xff
84
+
85
+ // ATTR_* valid mask bits (matching Linux)
86
+ const ATTR_MODE = 1
87
+ const ATTR_SIZE = 8
88
+
89
+ // Status codes
90
+ const V86FS_STATUS_OK = 0
91
+ const V86FS_STATUS_ENOENT = 2
92
+
93
+ // DT_* types (matching Linux dirent.h)
94
+ const DT_DIR = 4
95
+ const DT_REG = 8
96
+ const DT_LNK = 10
97
+
98
+ // S_IF* mode bits
99
+ const S_IFDIR = 0o040000
100
+ const S_IFREG = 0o100000
101
+ const S_IFLNK = 0o120000
102
+
103
+ const textDecoder = new TextDecoder()
104
+ const textEncoder = new TextEncoder()
105
+
106
+ interface FsEntry {
107
+ inode_id: number
108
+ name: string
109
+ mode: number
110
+ size: number
111
+ dt_type: number
112
+ mtime_sec: number
113
+ mtime_nsec: number
114
+ content?: Uint8Array
115
+ symlink_target?: string
116
+ }
117
+
118
+ // Hardcoded test filesystem: root dir (inode 1) with two entries
119
+ export const FS_ENTRIES: Map<number, FsEntry[]> = new Map([
120
+ [
121
+ 1,
122
+ [
123
+ {
124
+ inode_id: 2,
125
+ name: 'hello.txt',
126
+ mode: S_IFREG | 0o644,
127
+ size: 12,
128
+ dt_type: DT_REG,
129
+ mtime_sec: 1711500000,
130
+ mtime_nsec: 0,
131
+ content: textEncoder.encode('hello world\n'),
132
+ },
133
+ {
134
+ inode_id: 3,
135
+ name: 'subdir',
136
+ mode: S_IFDIR | 0o755,
137
+ size: 0,
138
+ dt_type: DT_DIR,
139
+ mtime_sec: 1711500000,
140
+ mtime_nsec: 0,
141
+ },
142
+ ],
143
+ ],
144
+ ])
145
+
146
+ // Inode lookup map built from FS_ENTRIES (includes root dir)
147
+ export const INODE_MAP: Map<number, FsEntry> = new Map([
148
+ [
149
+ 1,
150
+ {
151
+ inode_id: 1,
152
+ name: '',
153
+ mode: S_IFDIR | 0o755,
154
+ size: 0,
155
+ dt_type: DT_DIR,
156
+ mtime_sec: 1711500000,
157
+ mtime_nsec: 0,
158
+ },
159
+ ],
160
+ ])
161
+ for (const entries of FS_ENTRIES.values()) {
162
+ for (const e of entries) {
163
+ INODE_MAP.set(e.inode_id, e)
164
+ }
165
+ }
166
+
167
+ // Header size: 4B length + 1B type + 2B tag
168
+ const V86FS_HDR_SIZE = 7
169
+
170
+ const _VIRTIO_V86FS_QUEUE_HIPRIQ = 0
171
+ const _VIRTIO_V86FS_QUEUE_REQUESTQ = 1
172
+ const _VIRTIO_V86FS_QUEUE_NOTIFYQ = 2
173
+
174
+ function packU32(buf: Uint8Array, offset: number, val: number): void {
175
+ buf[offset] = val & 0xff
176
+ buf[offset + 1] = (val >>> 8) & 0xff
177
+ buf[offset + 2] = (val >>> 16) & 0xff
178
+ buf[offset + 3] = (val >>> 24) & 0xff
179
+ }
180
+
181
+ function packU64(buf: Uint8Array, offset: number, val: number): void {
182
+ packU32(buf, offset, val)
183
+ packU32(buf, offset + 4, 0)
184
+ }
185
+
186
+ function packU16(buf: Uint8Array, offset: number, val: number): void {
187
+ buf[offset] = val & 0xff
188
+ buf[offset + 1] = (val >>> 8) & 0xff
189
+ }
190
+
191
+ function makeResp(size: number, type: number, tag: number): Uint8Array {
192
+ const resp = new Uint8Array(size)
193
+ packU32(resp, 0, size)
194
+ resp[4] = type
195
+ resp[5] = tag & 0xff
196
+ resp[6] = (tag >> 8) & 0xff
197
+ return resp
198
+ }
199
+
200
+ function readU32(buf: Uint8Array, offset: number): number {
201
+ return (
202
+ buf[offset] |
203
+ (buf[offset + 1] << 8) |
204
+ (buf[offset + 2] << 16) |
205
+ ((buf[offset + 3] << 24) >>> 0)
206
+ )
207
+ }
208
+
209
+ function readU16(buf: Uint8Array, offset: number): number {
210
+ return buf[offset] | (buf[offset + 1] << 8)
211
+ }
212
+
213
+ function readU64(buf: Uint8Array, offset: number): number {
214
+ return (
215
+ buf[offset] |
216
+ (buf[offset + 1] << 8) |
217
+ (buf[offset + 2] << 16) |
218
+ (((buf[offset + 3] << 24) >>> 0) +
219
+ (buf[offset + 4] |
220
+ (buf[offset + 5] << 8) |
221
+ (buf[offset + 6] << 16) |
222
+ ((buf[offset + 7] << 24) >>> 0)) *
223
+ 0x100000000)
224
+ )
225
+ }
226
+
227
+ export class VirtioV86FS {
228
+ bus: BusConnector
229
+ virtio: VirtIO
230
+ next_handle_id: number
231
+ next_inode_id: number
232
+ open_handles: Map<number, number> // handle_id -> inode_id
233
+ read_count: number
234
+
235
+ constructor(cpu: VirtioV86FSCPU, bus: BusConnector) {
236
+ this.bus = bus
237
+ this.next_handle_id = 1
238
+ this.next_inode_id = 100
239
+ this.open_handles = new Map()
240
+ this.read_count = 0
241
+
242
+ const queues = [
243
+ // Queue 0: hipriq - high-priority metadata (LOOKUP, GETATTR)
244
+ { size_supported: 128, notify_offset: 0 },
245
+ // Queue 1: requestq - data requests (READ, WRITE, READDIR)
246
+ { size_supported: 128, notify_offset: 1 },
247
+ // Queue 2: notifyq - host-to-guest push invalidation
248
+ { size_supported: 128, notify_offset: 2 },
249
+ ]
250
+
251
+ this.virtio = new VirtIO(cpu, {
252
+ name: 'virtio-v86fs',
253
+ pci_id: 0x0e << 3,
254
+ device_id: 0x107f,
255
+ subsystem_device_id: 63,
256
+ common: {
257
+ initial_port: 0xf800,
258
+ queues: queues,
259
+ features: [VIRTIO_F_VERSION_1],
260
+ on_driver_ok: () => {
261
+ console.log('v86fs: driver ok')
262
+ this.bus.send('virtio-v86fs-driver-ok')
263
+ },
264
+ },
265
+ notification: {
266
+ initial_port: 0xf900,
267
+ single_handler: false,
268
+ handlers: [
269
+ // Queue 0: hipriq
270
+ (queue_id: number) => {
271
+ this.handle_queue(queue_id)
272
+ },
273
+ // Queue 1: requestq
274
+ (queue_id: number) => {
275
+ this.handle_queue(queue_id)
276
+ },
277
+ // Queue 2: notifyq
278
+ (_queue_id: number) => {
279
+ dbg_log('v86fs: notifyq notification', LOG_PCI)
280
+ },
281
+ ],
282
+ },
283
+ isr_status: {
284
+ initial_port: 0xf700,
285
+ },
286
+ })
287
+ }
288
+
289
+ handle_queue(queue_id: number): void {
290
+ const queue = this.virtio.queues[queue_id]
291
+ while (queue.has_request()) {
292
+ const bufchain = queue.pop_request()
293
+ const req = new Uint8Array(bufchain.length_readable)
294
+ bufchain.get_next_blob(req)
295
+
296
+ const resp = this.handle_message(req)
297
+ if (resp && bufchain.length_writable > 0) {
298
+ bufchain.set_next_blob(resp)
299
+ }
300
+
301
+ queue.push_reply(bufchain)
302
+ }
303
+ queue.flush_replies()
304
+ }
305
+
306
+ handle_message(req: Uint8Array): Uint8Array | null {
307
+ if (req.length < V86FS_HDR_SIZE) {
308
+ console.warn('v86fs: message too short:', req.length)
309
+ return null
310
+ }
311
+
312
+ const type = req[4]
313
+ const tag = readU16(req, 5)
314
+
315
+ switch (type) {
316
+ case V86FS_MSG_MOUNT:
317
+ return this.handle_mount(req, tag)
318
+ case V86FS_MSG_LOOKUP:
319
+ return this.handle_lookup(req, tag)
320
+ case V86FS_MSG_GETATTR:
321
+ return this.handle_getattr(req, tag)
322
+ case V86FS_MSG_READDIR:
323
+ return this.handle_readdir(req, tag)
324
+ case V86FS_MSG_OPEN:
325
+ return this.handle_open(req, tag)
326
+ case V86FS_MSG_CLOSE:
327
+ return this.handle_close(req, tag)
328
+ case V86FS_MSG_READ:
329
+ return this.handle_read(req, tag)
330
+ case V86FS_MSG_CREATE:
331
+ return this.handle_create(req, tag)
332
+ case V86FS_MSG_WRITE:
333
+ return this.handle_write(req, tag)
334
+ case V86FS_MSG_MKDIR:
335
+ return this.handle_mkdir(req, tag)
336
+ case V86FS_MSG_SETATTR:
337
+ return this.handle_setattr(req, tag)
338
+ case V86FS_MSG_FSYNC:
339
+ return this.handle_fsync(req, tag)
340
+ case V86FS_MSG_UNLINK:
341
+ return this.handle_unlink(req, tag)
342
+ case V86FS_MSG_RENAME:
343
+ return this.handle_rename(req, tag)
344
+ case V86FS_MSG_SYMLINK:
345
+ return this.handle_symlink(req, tag)
346
+ case V86FS_MSG_READLINK:
347
+ return this.handle_readlink(req, tag)
348
+ case V86FS_MSG_STATFS:
349
+ return this.handle_statfs(tag)
350
+ default:
351
+ console.warn('v86fs: unknown message type:', type)
352
+ return null
353
+ }
354
+ }
355
+
356
+ handle_mount(req: Uint8Array, tag: number): Uint8Array {
357
+ // Parse MOUNT: [7B hdr] [2B name_len] [name...]
358
+ const name_len = readU16(req, 7)
359
+ const name =
360
+ name_len > 0
361
+ ? textDecoder.decode(req.subarray(9, 9 + name_len))
362
+ : ''
363
+
364
+ console.log('v86fs: mount:', name || '(default)')
365
+
366
+ // Emit bus event so host adapter can resolve the name
367
+ this.bus.send('virtio-v86fs-mount', name)
368
+
369
+ // Build MOUNT_R: [7B hdr] [4B status=0] [8B root_id=1] [4B mode=0x41ED]
370
+ // 0x41ED = S_IFDIR | 0755
371
+ const resp = new Uint8Array(23)
372
+ packU32(resp, 0, 23) // length
373
+ resp[4] = V86FS_MSG_MOUNT_R // type
374
+ resp[5] = tag & 0xff // tag low
375
+ resp[6] = (tag >> 8) & 0xff // tag high
376
+ packU32(resp, 7, 0) // status = 0 (success)
377
+ packU64(resp, 11, 1) // root_inode_id = 1
378
+ packU32(resp, 19, 0x41ed) // mode = S_IFDIR | 0755
379
+
380
+ return resp
381
+ }
382
+
383
+ handle_getattr(req: Uint8Array, tag: number): Uint8Array {
384
+ // Parse GETATTR: [7B hdr] [8B inode_id]
385
+ const inode_id = readU64(req, 7)
386
+ const entry = INODE_MAP.get(inode_id)
387
+
388
+ // GETATTR_R: [7B hdr] [4B status] [4B mode] [8B size] [8B mtime_sec] [4B mtime_nsec]
389
+ const resp = makeResp(35, V86FS_MSG_GETATTR_R, tag)
390
+
391
+ if (!entry) {
392
+ packU32(resp, 7, V86FS_STATUS_ENOENT)
393
+ return resp
394
+ }
395
+
396
+ packU32(resp, 7, V86FS_STATUS_OK)
397
+ packU32(resp, 11, entry.mode)
398
+ packU64(resp, 15, entry.size)
399
+ packU64(resp, 23, entry.mtime_sec)
400
+ packU32(resp, 31, entry.mtime_nsec)
401
+ return resp
402
+ }
403
+
404
+ handle_lookup(req: Uint8Array, tag: number): Uint8Array {
405
+ // Parse LOOKUP: [7B hdr] [8B parent_id] [2B name_len] [name...]
406
+ const parent_id = readU64(req, 7)
407
+ const name_len = readU16(req, 15)
408
+ const name = textDecoder.decode(req.subarray(17, 17 + name_len))
409
+
410
+ const entries = FS_ENTRIES.get(parent_id)
411
+ const entry = entries?.find((e) => e.name === name)
412
+
413
+ // LOOKUP_R: [7B hdr] [4B status] [8B inode_id] [4B mode] [8B size]
414
+ const resp = new Uint8Array(31)
415
+ packU32(resp, 0, 31) // length
416
+ resp[4] = V86FS_MSG_LOOKUP_R
417
+ resp[5] = tag & 0xff
418
+ resp[6] = (tag >> 8) & 0xff
419
+
420
+ if (!entry) {
421
+ packU32(resp, 7, V86FS_STATUS_ENOENT)
422
+ return resp
423
+ }
424
+
425
+ packU32(resp, 7, V86FS_STATUS_OK)
426
+ packU64(resp, 11, entry.inode_id)
427
+ packU32(resp, 19, entry.mode)
428
+ packU64(resp, 23, entry.size)
429
+ return resp
430
+ }
431
+
432
+ handle_readdir(req: Uint8Array, tag: number): Uint8Array {
433
+ // Parse READDIR: [7B hdr] [8B dir_id]
434
+ const dir_id = readU64(req, 7)
435
+ const entries = FS_ENTRIES.get(dir_id) || []
436
+
437
+ // Pre-encode names to avoid double encoding
438
+ const encodedNames = entries.map((e) => textEncoder.encode(e.name))
439
+
440
+ // READDIR_R: [7B hdr] [4B status] [4B count] [entries...]
441
+ // Each entry: [8B inode_id] [1B type] [2B name_len] [name...]
442
+ let size = 7 + 4 + 4
443
+ for (const nameBytes of encodedNames) {
444
+ size += 8 + 1 + 2 + nameBytes.length
445
+ }
446
+
447
+ const resp = makeResp(size, V86FS_MSG_READDIR_R, tag)
448
+ packU32(resp, 7, V86FS_STATUS_OK)
449
+ packU32(resp, 11, entries.length)
450
+
451
+ let off = 15
452
+ for (let i = 0; i < entries.length; i++) {
453
+ const e = entries[i]
454
+ const nameBytes = encodedNames[i]
455
+ packU64(resp, off, e.inode_id)
456
+ resp[off + 8] = e.dt_type
457
+ packU16(resp, off + 9, nameBytes.length)
458
+ resp.set(nameBytes, off + 11)
459
+ off += 11 + nameBytes.length
460
+ }
461
+
462
+ return resp
463
+ }
464
+
465
+ handle_open(req: Uint8Array, tag: number): Uint8Array {
466
+ // Parse OPEN: [7B hdr] [8B inode_id] [4B flags]
467
+ const inode_id = readU64(req, 7)
468
+ const handle_id = this.next_handle_id++
469
+ this.open_handles.set(handle_id, inode_id)
470
+
471
+ this.bus.send('virtio-v86fs-open', inode_id)
472
+
473
+ // OPEN_R: [7B hdr] [4B status] [8B handle_id]
474
+ const resp = makeResp(19, V86FS_MSG_OPEN_R, tag)
475
+ packU32(resp, 7, V86FS_STATUS_OK)
476
+ packU64(resp, 11, handle_id)
477
+ return resp
478
+ }
479
+
480
+ handle_close(req: Uint8Array, tag: number): Uint8Array {
481
+ // Parse CLOSE: [7B hdr] [8B handle_id]
482
+ const handle_id = readU64(req, 7)
483
+ this.open_handles.delete(handle_id)
484
+
485
+ this.bus.send('virtio-v86fs-close', handle_id)
486
+
487
+ // CLOSE_R: [7B hdr] [4B status]
488
+ const resp = makeResp(11, V86FS_MSG_CLOSE_R, tag)
489
+ packU32(resp, 7, V86FS_STATUS_OK)
490
+ return resp
491
+ }
492
+
493
+ handle_read(req: Uint8Array, tag: number): Uint8Array {
494
+ // Parse READ: [7B hdr] [8B handle_id] [8B offset] [4B size]
495
+ const handle_id = readU64(req, 7)
496
+ const offset = readU64(req, 15)
497
+ const size = readU32(req, 23)
498
+
499
+ const inode_id = this.open_handles.get(handle_id) ?? handle_id
500
+ const entry = INODE_MAP.get(inode_id)
501
+ const content = entry?.content
502
+
503
+ this.read_count++
504
+ this.bus.send('virtio-v86fs-read', {
505
+ handle_id,
506
+ inode_id,
507
+ offset,
508
+ size,
509
+ })
510
+
511
+ if (!content || offset >= content.length) {
512
+ // EOF or no content
513
+ const resp = makeResp(15, V86FS_MSG_READ_R, tag)
514
+ packU32(resp, 7, V86FS_STATUS_OK)
515
+ packU32(resp, 11, 0) // 0 bytes read
516
+ return resp
517
+ }
518
+
519
+ const start = Math.min(offset, content.length)
520
+ const end = Math.min(start + size, content.length)
521
+ const data = content.subarray(start, end)
522
+
523
+ // READ_R: [7B hdr] [4B status] [4B bytes_read] [data...]
524
+ const resp = new Uint8Array(15 + data.length)
525
+ packU32(resp, 0, 15 + data.length)
526
+ resp[4] = V86FS_MSG_READ_R
527
+ resp[5] = tag & 0xff
528
+ resp[6] = (tag >> 8) & 0xff
529
+ packU32(resp, 7, V86FS_STATUS_OK)
530
+ packU32(resp, 11, data.length)
531
+ resp.set(data, 15)
532
+ return resp
533
+ }
534
+
535
+ handle_create(req: Uint8Array, tag: number): Uint8Array {
536
+ // Parse CREATE: [7B hdr] [8B parent_id] [2B name_len] [name...] [4B mode]
537
+ const parent_id = readU64(req, 7)
538
+ const name_len = readU16(req, 15)
539
+ const name = textDecoder.decode(req.subarray(17, 17 + name_len))
540
+ const mode = readU32(req, 17 + name_len)
541
+
542
+ const inode_id = this.next_inode_id++
543
+ const entry: FsEntry = {
544
+ inode_id,
545
+ name,
546
+ mode: mode | S_IFREG,
547
+ size: 0,
548
+ dt_type: DT_REG,
549
+ mtime_sec: Math.floor(Date.now() / 1000),
550
+ mtime_nsec: 0,
551
+ content: new Uint8Array(0),
552
+ }
553
+
554
+ // Add to parent's entry list
555
+ let children = FS_ENTRIES.get(parent_id)
556
+ if (!children) {
557
+ children = []
558
+ FS_ENTRIES.set(parent_id, children)
559
+ }
560
+ children.push(entry)
561
+ INODE_MAP.set(inode_id, entry)
562
+
563
+ // CREATE_R: [7B hdr] [4B status] [8B inode_id] [4B mode]
564
+ const resp = makeResp(23, V86FS_MSG_CREATE_R, tag)
565
+ packU32(resp, 7, V86FS_STATUS_OK)
566
+ packU64(resp, 11, inode_id)
567
+ packU32(resp, 19, entry.mode)
568
+ return resp
569
+ }
570
+
571
+ handle_write(req: Uint8Array, tag: number): Uint8Array {
572
+ // Parse WRITE: [7B hdr] [8B inode_id] [8B offset] [4B size] [data...]
573
+ const inode_id = readU64(req, 7)
574
+ const offset = readU64(req, 15)
575
+ const size = readU32(req, 23)
576
+ const data = req.subarray(27, 27 + size)
577
+
578
+ const entry = INODE_MAP.get(inode_id)
579
+ if (entry) {
580
+ // Grow content buffer if needed
581
+ const needed = offset + size
582
+ if (!entry.content || entry.content.length < needed) {
583
+ const newContent = new Uint8Array(needed)
584
+ if (entry.content) {
585
+ newContent.set(entry.content)
586
+ }
587
+ entry.content = newContent
588
+ }
589
+ entry.content.set(data, offset)
590
+ if (needed > entry.size) {
591
+ entry.size = needed
592
+ }
593
+ }
594
+
595
+ // WRITE_R: [7B hdr] [4B status] [4B bytes_written]
596
+ const resp = makeResp(15, V86FS_MSG_WRITE_R, tag)
597
+ packU32(resp, 7, V86FS_STATUS_OK)
598
+ packU32(resp, 11, size)
599
+ return resp
600
+ }
601
+
602
+ handle_mkdir(req: Uint8Array, tag: number): Uint8Array {
603
+ // Parse MKDIR: [7B hdr] [8B parent_id] [2B name_len] [name...] [4B mode]
604
+ const parent_id = readU64(req, 7)
605
+ const name_len = readU16(req, 15)
606
+ const name = textDecoder.decode(req.subarray(17, 17 + name_len))
607
+ const mode = readU32(req, 17 + name_len)
608
+
609
+ const inode_id = this.next_inode_id++
610
+ const entry: FsEntry = {
611
+ inode_id,
612
+ name,
613
+ mode: mode | S_IFDIR,
614
+ size: 0,
615
+ dt_type: DT_DIR,
616
+ mtime_sec: Math.floor(Date.now() / 1000),
617
+ mtime_nsec: 0,
618
+ }
619
+
620
+ let children = FS_ENTRIES.get(parent_id)
621
+ if (!children) {
622
+ children = []
623
+ FS_ENTRIES.set(parent_id, children)
624
+ }
625
+ children.push(entry)
626
+ INODE_MAP.set(inode_id, entry)
627
+ FS_ENTRIES.set(inode_id, []) // empty dir
628
+
629
+ // MKDIR_R: [7B hdr] [4B status] [8B inode_id] [4B mode]
630
+ const resp = makeResp(23, V86FS_MSG_MKDIR_R, tag)
631
+ packU32(resp, 7, V86FS_STATUS_OK)
632
+ packU64(resp, 11, inode_id)
633
+ packU32(resp, 19, entry.mode)
634
+ return resp
635
+ }
636
+
637
+ handle_setattr(req: Uint8Array, tag: number): Uint8Array {
638
+ // Parse SETATTR: [7B hdr] [8B inode_id] [4B valid] [4B mode] [8B size]
639
+ const inode_id = readU64(req, 7)
640
+ const valid = readU32(req, 15)
641
+ const mode = readU32(req, 19)
642
+ const size = readU64(req, 23)
643
+
644
+ const entry = INODE_MAP.get(inode_id)
645
+ if (entry) {
646
+ if (valid & ATTR_MODE) {
647
+ entry.mode = (entry.mode & 0o170000) | (mode & 0o7777)
648
+ }
649
+ if (valid & ATTR_SIZE) {
650
+ entry.size = size
651
+ if (entry.content) {
652
+ if (size === 0) {
653
+ entry.content = new Uint8Array(0)
654
+ } else if (size < entry.content.length) {
655
+ entry.content = entry.content.subarray(0, size)
656
+ }
657
+ }
658
+ }
659
+ }
660
+
661
+ // SETATTR_R: [7B hdr] [4B status]
662
+ const resp = makeResp(11, V86FS_MSG_SETATTR_R, tag)
663
+ packU32(resp, 7, V86FS_STATUS_OK)
664
+ return resp
665
+ }
666
+
667
+ handle_fsync(req: Uint8Array, tag: number): Uint8Array {
668
+ // FSYNC is a no-op for the in-memory test FS
669
+ const resp = makeResp(11, V86FS_MSG_FSYNC_R, tag)
670
+ packU32(resp, 7, V86FS_STATUS_OK)
671
+ return resp
672
+ }
673
+
674
+ handle_unlink(req: Uint8Array, tag: number): Uint8Array {
675
+ // Parse UNLINK: [7B hdr] [8B parent_id] [2B name_len] [name...]
676
+ const parent_id = readU64(req, 7)
677
+ const name_len = readU16(req, 15)
678
+ const name = textDecoder.decode(req.subarray(17, 17 + name_len))
679
+
680
+ const children = FS_ENTRIES.get(parent_id)
681
+ let status = V86FS_STATUS_ENOENT
682
+ if (children) {
683
+ const idx = children.findIndex((e) => e.name === name)
684
+ if (idx >= 0) {
685
+ const entry = children[idx]
686
+ INODE_MAP.delete(entry.inode_id)
687
+ FS_ENTRIES.delete(entry.inode_id)
688
+ children.splice(idx, 1)
689
+ status = V86FS_STATUS_OK
690
+ }
691
+ }
692
+
693
+ const resp = makeResp(11, V86FS_MSG_UNLINK_R, tag)
694
+ packU32(resp, 7, status)
695
+ return resp
696
+ }
697
+
698
+ handle_rename(req: Uint8Array, tag: number): Uint8Array {
699
+ // Parse RENAME: [7B hdr] [8B old_parent_id] [2B old_name_len] [old_name...]
700
+ // [8B new_parent_id] [2B new_name_len] [new_name...]
701
+ let off = 7
702
+ const old_parent_id = readU64(req, off)
703
+ off += 8
704
+ const old_name_len = readU16(req, off)
705
+ off += 2
706
+ const old_name = textDecoder.decode(
707
+ req.subarray(off, off + old_name_len),
708
+ )
709
+ off += old_name_len
710
+ const new_parent_id = readU64(req, off)
711
+ off += 8
712
+ const new_name_len = readU16(req, off)
713
+ off += 2
714
+ const new_name = textDecoder.decode(
715
+ req.subarray(off, off + new_name_len),
716
+ )
717
+
718
+ let status = V86FS_STATUS_ENOENT
719
+ const old_children = FS_ENTRIES.get(old_parent_id)
720
+ if (old_children) {
721
+ const idx = old_children.findIndex((e) => e.name === old_name)
722
+ if (idx >= 0) {
723
+ const entry = old_children[idx]
724
+ old_children.splice(idx, 1)
725
+ entry.name = new_name
726
+ let new_children = FS_ENTRIES.get(new_parent_id)
727
+ if (!new_children) {
728
+ new_children = []
729
+ FS_ENTRIES.set(new_parent_id, new_children)
730
+ }
731
+ // Remove any existing entry with new_name in target dir
732
+ const existing = new_children.findIndex(
733
+ (e) => e.name === new_name,
734
+ )
735
+ if (existing >= 0) {
736
+ const old_entry = new_children[existing]
737
+ INODE_MAP.delete(old_entry.inode_id)
738
+ new_children.splice(existing, 1)
739
+ }
740
+ new_children.push(entry)
741
+ status = V86FS_STATUS_OK
742
+ }
743
+ }
744
+
745
+ const resp = makeResp(11, V86FS_MSG_RENAME_R, tag)
746
+ packU32(resp, 7, status)
747
+ return resp
748
+ }
749
+
750
+ handle_symlink(req: Uint8Array, tag: number): Uint8Array {
751
+ // Parse SYMLINK: [7B hdr] [8B parent_id] [2B name_len] [name...]
752
+ // [2B target_len] [target...]
753
+ let off = 7
754
+ const parent_id = readU64(req, off)
755
+ off += 8
756
+ const name_len = readU16(req, off)
757
+ off += 2
758
+ const name = textDecoder.decode(req.subarray(off, off + name_len))
759
+ off += name_len
760
+ const target_len = readU16(req, off)
761
+ off += 2
762
+ const target = textDecoder.decode(req.subarray(off, off + target_len))
763
+
764
+ const inode_id = this.next_inode_id++
765
+ const entry: FsEntry = {
766
+ inode_id,
767
+ name,
768
+ mode: S_IFLNK | 0o777,
769
+ size: target.length,
770
+ dt_type: DT_LNK,
771
+ mtime_sec: Math.floor(Date.now() / 1000),
772
+ mtime_nsec: 0,
773
+ symlink_target: target,
774
+ }
775
+
776
+ let children = FS_ENTRIES.get(parent_id)
777
+ if (!children) {
778
+ children = []
779
+ FS_ENTRIES.set(parent_id, children)
780
+ }
781
+ children.push(entry)
782
+ INODE_MAP.set(inode_id, entry)
783
+
784
+ // SYMLINK_R: [7B hdr] [4B status] [8B inode_id] [4B mode]
785
+ const resp = makeResp(23, V86FS_MSG_SYMLINK_R, tag)
786
+ packU32(resp, 7, V86FS_STATUS_OK)
787
+ packU64(resp, 11, inode_id)
788
+ packU32(resp, 19, entry.mode)
789
+ return resp
790
+ }
791
+
792
+ handle_readlink(req: Uint8Array, tag: number): Uint8Array {
793
+ // Parse READLINK: [7B hdr] [8B inode_id]
794
+ const inode_id = readU64(req, 7)
795
+ const entry = INODE_MAP.get(inode_id)
796
+
797
+ if (!entry || !entry.symlink_target) {
798
+ const resp = makeResp(11, V86FS_MSG_READLINK_R, tag)
799
+ packU32(resp, 7, V86FS_STATUS_ENOENT)
800
+ return resp
801
+ }
802
+
803
+ const target_bytes = textEncoder.encode(entry.symlink_target)
804
+ const resp_len = 11 + 2 + target_bytes.length
805
+ const resp = makeResp(resp_len, V86FS_MSG_READLINK_R, tag)
806
+ packU32(resp, 7, V86FS_STATUS_OK)
807
+ packU16(resp, 11, target_bytes.length)
808
+ resp.set(target_bytes, 13)
809
+ return resp
810
+ }
811
+
812
+ handle_statfs(tag: number): Uint8Array {
813
+ // STATFS_R: [7B hdr] [4B status] [8B blocks] [8B bfree] [8B bavail]
814
+ // [8B files] [8B ffree] [4B bsize] [4B namelen]
815
+ const resp = makeResp(55, V86FS_MSG_STATFS_R, tag)
816
+ packU32(resp, 7, V86FS_STATUS_OK)
817
+ packU64(resp, 11, 1024 * 1024) // blocks
818
+ packU64(resp, 19, 512 * 1024) // bfree
819
+ packU64(resp, 27, 512 * 1024) // bavail
820
+ packU64(resp, 35, 1024 * 1024) // files
821
+ packU64(resp, 43, 512 * 1024) // ffree
822
+ packU32(resp, 51, 4096) // bsize
823
+ return resp
824
+ }
825
+
826
+ /** Push an INVALIDATE notification to the guest via notifyq.
827
+ * Guest kernel will invalidate page cache for the given inode. */
828
+ invalidate_inode(inode_id: number): boolean {
829
+ const queue = this.virtio.queues[2] // notifyq
830
+ if (!queue.has_request()) return false
831
+
832
+ const bufchain = queue.pop_request()
833
+ // INVALIDATE: [7B hdr] [8B inode_id]
834
+ const msg = new Uint8Array(15)
835
+ packU32(msg, 0, 15)
836
+ msg[4] = V86FS_MSG_INVALIDATE
837
+ packU16(msg, 5, 0)
838
+ packU64(msg, 7, inode_id)
839
+ bufchain.set_next_blob(msg)
840
+ queue.push_reply(bufchain)
841
+ queue.flush_replies()
842
+ return true
843
+ }
844
+
845
+ /** Push an INVALIDATE_DIR notification to the guest via notifyq.
846
+ * Guest kernel will invalidate dcache for the given directory inode. */
847
+ invalidate_dir(inode_id: number): boolean {
848
+ const queue = this.virtio.queues[2] // notifyq
849
+ if (!queue.has_request()) return false
850
+
851
+ const bufchain = queue.pop_request()
852
+ // INVALIDATE_DIR: [7B hdr] [8B inode_id]
853
+ const msg = new Uint8Array(15)
854
+ packU32(msg, 0, 15)
855
+ msg[4] = V86FS_MSG_INVALIDATE_DIR
856
+ packU16(msg, 5, 0)
857
+ packU64(msg, 7, inode_id)
858
+ bufchain.set_next_blob(msg)
859
+ queue.push_reply(bufchain)
860
+ queue.flush_replies()
861
+ return true
862
+ }
863
+
864
+ get_state(): any[] {
865
+ const state: any[] = []
866
+ state[0] = this.virtio
867
+ return state
868
+ }
869
+
870
+ set_state(state: any[]): void {
871
+ this.virtio.set_state(state[0])
872
+ }
873
+ }