Author SHA1 Message Date
Daniel Samson 77d2e22ed1 M7: in-repo FAT32 image builder + boot the whole system off a USB stick
The system now boots off a real FAT32 filesystem on a USB mass-storage device
instead of QEMU's synthesized VVFAT drive. A new in-repo image builder formats
that filesystem from the FHS boot tree, and both QEMU call sites (the run step
and the test harness) attach it as a usb-storage device on the xHCI bus, so
every boot exercises the full USB path OVMF -> BOOTX64.efi -> kernel.

- tools/make-fat-image.py: a Python 3 stdlib-only FAT32 formatter (mirrors
  tools/make-initial-ramdisk.py — no external host dependencies). It lays down
  the boot sector + BPB/EBPB32, FSInfo, backup boot sector, two FATs, and the
  root/subdir/file cluster chains, emitting long-name entries where a name is
  not 8.3. Packs the four boot inputs (EFI/BOOT/BOOTX64.efi, system/kernel,
  system/services/init, boot/initial-ramdisk.img) into their boot paths. A
  --verify subcommand re-checks the 0xAA55 signature, recomputes the cluster
  count -> FAT32, and resolves EFI/BOOT/BOOTX64.efi, all with no dependencies.

- build.zig: a mk_fat step builds zig-out/danos-usb.img from the four boot
  artifacts (so changing -Dtest-case rebuilds the image with that kernel), a
  check-fat-image step runs --verify, and run-x86-64 boots the image on a
  usb-storage device (if=none,id=bootusb + usb-storage,bus=xhci.0,bootindex=0),
  keeping usb-kbd/usb-mouse on the same controller.

- test/qemu_test.py: the default boot config now boots off danos-usb.img on a
  usb-storage device (xHCI + usb-kbd + usb-mouse + the boot stick). The seven
  per-case qemu_extra blocks that added their own qemu-xhci/usb-kbd/usb-mouse
  (or a VVFAT stick) collided on id=xhci and are removed — the default provides
  the bus and the boot device. usb-storage and fat-mount now exercise the real
  FAT32 boot image (usb-storage reads its 0x55AA boot sector; fat mounts it at
  /mnt/usb). A build_case override lets a case reuse another's kernel, used by a
  new usb-boot case: an explicit, named boot-from-USB regression guard.

Verified: zig build, zig build test, and zig build check-fat-image are green
(FAT32, 128992 clusters, BOOTX64.efi present); a broad sequential QEMU sweep
passes — smoke, init, vfs, input, device-manager, usb-report, usb-hid,
usb-storage, fat-mount, device-list, driver-restart, acpi-report, iommu,
orderly-shutdown, usb-boot, dma, msi, initial-ramdisk, args, process — proving
the boot switch holds across kernel tests, the full init tree, the USB stack,
the FAT mount, and orderly shutdown.
2026-07-13 15:34:18 +01:00
Daniel Samson a64a01a6a9 M6: FAT read/write filesystem server, mounted into the VFS
Add a FAT12/16/32 filesystem the VFS mounts at /mnt/usb, reading and writing a
USB stick through the block device. Verified end to end under QEMU: the fat
server mounts the volume, the VFS routes /mnt/usb to it, and a client lists the
root and reads a file (the ELF magic of /mnt/usb/system/kernel).

- engine.zig: the FAT engine over a BlockDevice interface — mount (a bare FAT or,
  as QEMU's VVFAT and most real sticks present it, an MBR-partitioned disk), FAT
  chain walk (12/16/32), cluster allocation, directory traversal with long-name
  read, and file read / write / create. Host-tested against a RAM-backed FAT16
  image (create, cluster-spanning write, mid-file overwrite, read-back, list).
- on-disk.zig: the align(1) boot-sector / directory / long-name / FSInfo structs
  and the cluster-count FAT-type detection.
- fat.zig: the server — wraps the .block device (a DMA bounce buffer) in a
  BlockDevice, mounts the FAT, serves the vfs-protocol as a backend, and mounts
  itself into the VFS at /mnt/usb. Spawned by init as a boot service.
- runtime.block: the block-device client (geometry / read / write by physical
  address, so whole sectors never cross IPC).
- Raise the kernel service-name registry (maximum_services) 8 -> 16: it is
  indexed directly by ServiceId, and fat = 8 was being rejected, so the fat
  server exited before registering.
- VFS: an absolute path with no matching mount is now not-found rather than
  silently created in the flat ramfs — so /mnt/usb fails cleanly until mounted.

Tests: fat-mount (the full stack: block -> FAT -> VFS mount -> list + file read)
passes; host units cover the engine and on-disk structs; the vfs, shutdown, and
USB regression suite stays green (10/10).
2026-07-13 15:05:53 +01:00
Daniel Samson 35e8921de8 M5: VFS mount support — mount table + forwarding router
Turn the flat-ramfs VFS into a router: a mount table maps an absolute path prefix
(e.g. /mnt/usb) to a backend server's endpoint, and open/read/write/status/
readdir/close on a path under a mount are forwarded to that backend, which speaks
the same vfs-protocol. This is what a FAT filesystem mounts into.

- protocol: append readdir / mount / unmount operations, a NodeKind enum (the FSH
  file types) that now fills FileStatus.kind, a DirectoryEntry record, and a
  directory open flag. Appended values keep existing clients and tests unchanged.
- vfs.zig: a mount table, longest-prefix routing, forwarding of every op on a
  backend handle, mount/unmount handlers (the backend arrives as the call's
  capability), and release-on-death that also closes the backend's handles.
- path.zig: pure, host-tested mount-prefix matching that never captures a
  non-boundary like /mnt/usbextra.
- unistd: mount(), opendir / readdir / closedir clients.

Bare names still resolve in the flat ramfs — the backward-compat contract; the
vfs and vfs-client-death tests pass unchanged. End-to-end mount+read is exercised
by the FAT server (M6). Host units cover path matching and protocol sizes.
2026-07-13 14:21:30 +01:00
Daniel Samson 3fb9d5936a USB driver stack: xHCI transfers, HID keyboard/mouse, mass storage
Flesh out the xHCI host-controller driver into a full transfer engine and build
the three USB class drivers on top, all verified end to end under QEMU.

- xHCI engine (usb-xhci-library.zig): controller reset, command/event rings with
  cycle-bit bookkeeping (gated on a No-Op-command proof), device slots, Address
  Device, control transfers, full chapter-9 enumeration, Configure Endpoint, and
  interrupt/bulk transfers. Each interface is device_registered with its
  (class,subclass,protocol) identity, unique per (port,interface).
- Bus<->class transfer protocol (usb-transfer-protocol.zig + runtime.usb): open /
  control / interrupt-subscribe (async report pump on a poll timer) / bulk-by-
  physical-address, so sector data never crosses the 256-byte IPC limit.
- USB HID keyboard + mouse (usb-hid/): decode boot-protocol reports and publish
  to the input service. A USB usage is already the input protocol's keycode.
- USB mass storage (usb-storage/): Bulk-Only Transport + transparent SCSI,
  serving a block device under the new .block service id (block-protocol).
- device-manager matches USB interfaces to class drivers (usbDriverForIdentity).
- usb-abi / usb-ids made importable modules; add HID and mass-storage class
  requests, packTriple, and a usb_device DeviceClass.
- Fix test/qemu_test.py on macOS: the QMP unix-socket path was built from the
  deep worktree path and exceeded the 104-byte sun_path limit, so QEMU exited
  before booting. It now lives under a short temp path.

Tests: usb-report, usb-hid, usb-storage pass under python3 test/qemu_test.py;
host units (usb-abi, usb-ids, hid-report, bulk-only-transport, scsi) green.
2026-07-13 14:11:00 +01:00
33 changed files with 4911 additions and 147 deletions
+108 -3
View File
@@ -142,6 +142,28 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("system/devices/acpi-ids.zig"), .root_source_file = b.path("system/devices/acpi-ids.zig"),
}); });
// The USB device-framework wire ABI (chapter-9 set-up packets, standard +
// class requests, descriptors) and the USB class-code taxonomy — the flat
// reference the xHCI bus driver, the USB class drivers, and the device
// manager's identity matcher all share. Pure data, like pci-class/acpi-ids.
const usb_abi_module = b.addModule("usb-abi", .{
.root_source_file = b.path("system/devices/usb-abi.zig"),
});
const usb_ids_module = b.addModule("usb-ids", .{
.root_source_file = b.path("system/devices/usb-ids.zig"),
});
// The USB transfer protocol: what a USB class driver says to the xHCI bus
// driver to drive its device (open / control / interrupt / bulk). A protocol
// module like vfs-protocol, shared by the bus driver and every class driver.
const usb_transfer_protocol_module = b.addModule("usb-transfer-protocol", .{
.root_source_file = b.path("system/drivers/usb-xhci-bus/usb-transfer-protocol.zig"),
});
// The block-device protocol: read/write of fixed-size blocks, spoken between a
// filesystem and a block driver (usb-storage). A protocol module like the rest.
const block_protocol_module = b.addModule("block-protocol", .{
.root_source_file = b.path("system/services/block/protocol.zig"),
});
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of // Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
// compile-time constants, imported wherever a knob is read; keeps the trade-offs // compile-time constants, imported wherever a knob is read; keeps the trade-offs
// in one place instead of scattered across the tree. See system/parameters.zig. // in one place instead of scattered across the tree. See system/parameters.zig.
@@ -225,6 +247,11 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"), .root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"),
}); });
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module); runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
// The USB transfer protocol, so runtime.usb (the class-driver client) can speak
// it, the way runtime.input speaks the input protocol.
runtime_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
// The block protocol, so runtime.block (the block-device client) can speak it.
runtime_module.addImport("block-protocol", block_protocol_module);
// The power protocol: system power's domain-named surface (docs/power.md). // The power protocol: system power's domain-named surface (docs/power.md).
const power_protocol_module = b.addModule("power-protocol", .{ const power_protocol_module = b.addModule("power-protocol", .{
@@ -350,6 +377,27 @@ pub fn build(b: *std.Build) void {
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
// The xHCI bus driver builds chapter-9 requests and decodes descriptors from
// usb-abi, and reports each interface's (class,subclass,protocol) identity via
// usb-ids.packTriple.
usb_xhci_bus_exe.root_module.addImport("usb-abi", usb_abi_module);
usb_xhci_bus_exe.root_module.addImport("usb-ids", usb_ids_module);
usb_xhci_bus_exe.root_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
// The USB HID class drivers: keyboard and mouse. They own no hardware — each
// opens its device through runtime.usb (the transfer protocol) and publishes to
// the input service. They build chapter-9 class requests from usb-abi.
const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig");
usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module);
const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig");
usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module);
// The USB mass-storage class driver: opens its device via runtime.usb, drives it
// with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`.
const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig");
usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module);
// The FAT filesystem server: mounts the block device and serves it into the VFS
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
const fat_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
// The PCI bus driver decodes each function's class triple to human names in its // The PCI bus driver decodes each function's class triple to human names in its
// boot log (class/subclass/prog-IF), so pull in the shared pci-class reference. // boot log (class/subclass/prog-IF), so pull in the shared pci-class reference.
@@ -376,6 +424,9 @@ pub fn build(b: *std.Build) void {
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
// Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330. // Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330.
device_manager_exe.root_module.addImport("pci-class", pci_class_module); device_manager_exe.root_module.addImport("pci-class", pci_class_module);
// The manager matches reported USB interfaces by their (class,subclass,protocol)
// triple (usbDriverForIdentity), built from the named usb-ids codes.
device_manager_exe.root_module.addImport("usb-ids", usb_ids_module);
// The input service and its exercisers: the fan-out server, a hardware-free synthetic // The input service and its exercisers: the fan-out server, a hardware-free synthetic
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig"); const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
@@ -402,6 +453,16 @@ pub fn build(b: *std.Build) void {
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
mk_run.addArg("usb-xhci-bus"); mk_run.addArg("usb-xhci-bus");
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
mk_run.addArg("usb-hid-keyboard");
mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin());
mk_run.addArg("usb-hid-mouse");
mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin());
mk_run.addArg("usb-storage");
mk_run.addFileArg(usb_storage_exe.getEmittedBin());
mk_run.addArg("fat");
mk_run.addFileArg(fat_exe.getEmittedBin());
mk_run.addArg("fat-test");
mk_run.addFileArg(fat_test_exe.getEmittedBin());
mk_run.addArg("pci-bus"); mk_run.addArg("pci-bus");
mk_run.addFileArg(pci_bus_exe.getEmittedBin()); mk_run.addFileArg(pci_bus_exe.getEmittedBin());
mk_run.addArg("crash-test"); mk_run.addArg("crash-test");
@@ -433,6 +494,10 @@ pub fn build(b: *std.Build) void {
.{ ps2_keyboard_exe, "system/drivers" }, .{ ps2_keyboard_exe, "system/drivers" },
.{ ps2_mouse_exe, "system/drivers" }, .{ ps2_mouse_exe, "system/drivers" },
.{ usb_xhci_bus_exe, "system/drivers" }, .{ usb_xhci_bus_exe, "system/drivers" },
.{ usb_hid_keyboard_exe, "system/drivers" },
.{ usb_hid_mouse_exe, "system/drivers" },
.{ usb_storage_exe, "system/drivers" },
.{ fat_exe, "system/services" },
}) |entry| { }) |entry| {
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
b.getInstallStep().dependOn(&step.step); b.getInstallStep().dependOn(&step.step);
@@ -466,6 +531,36 @@ pub fn build(b: *std.Build) void {
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } }); const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
b.getInstallStep().dependOn(&efi_install.step); b.getInstallStep().dependOn(&efi_install.step);
// --- danos-usb.img: the bootable FAT32 USB image ---
// Format a real FAT32 image (the in-repo Python builder, no external tools)
// holding exactly what the firmware and bootloader need off the ESP: the EFI
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
// harness), and the danos fat driver mounts the same image at /mnt/usb.
const mk_fat = b.addSystemCommand(&.{"python3"});
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
mk_fat.addArg("64"); // MiB
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
mk_fat.addFileArg(efiexe.getEmittedBin());
mk_fat.addArg("system/kernel");
mk_fat.addFileArg(exe.getEmittedBin());
mk_fat.addArg("system/services/init");
mk_fat.addFileArg(init_exe.getEmittedBin());
mk_fat.addArg("boot/initial-ramdisk.img");
mk_fat.addFileArg(initial_ramdisk_img);
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
b.getInstallStep().dependOn(&fat_image_install.step);
// `zig build check-fat-image` — validate the produced image is a real FAT32
// with the EFI stub present (the builder's own --verify, no external tools).
const check_fat = b.addSystemCommand(&.{"python3"});
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
check_fat.addArg("--verify");
check_fat.addFileArg(fat_image);
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
check_fat_step.dependOn(&check_fat.step);
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF --- // --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
// Firmware lives in different places per OS/distro, so probe the known // Firmware lives in different places per OS/distro, so probe the known
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first // layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
@@ -526,10 +621,13 @@ pub fn build(b: *std.Build) void {
}); });
run_efi.addArg("-drive"); run_efi.addArg("-drive");
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out); run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
// Present the FHS zig-out to the guest as a FAT drive — it is the boot volume. // Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
run_efi.addArg("-drive");
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image);
run_efi.addArgs(&.{ run_efi.addArgs(&.{
"-drive", "-device",
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}), "usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
"-net", "-net",
"none", "none",
// Emulated display advertising 1280x720 as its native (EDID preferred) // Emulated display advertising 1280x720 as its native (EDID preferred)
@@ -581,6 +679,13 @@ pub fn build(b: *std.Build) void {
"library/mmio/mmio.zig", // barriers assemble + registers round-trip "library/mmio/mmio.zig", // barriers assemble + registers round-trip
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
"system/services/vfs/path.zig", // mount-prefix path matching
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
}) |root| { }) |root| {
const mod_tests = b.addTest(.{ const mod_tests = b.addTest(.{
.root_module = b.createModule(.{ .root_module = b.createModule(.{
+58
View File
@@ -146,3 +146,61 @@ pub fn close(fd: i32) void {
_ = transact(request, &.{}, &.{}); _ = transact(request, &.{}, &.{});
f.used = false; f.used = false;
} }
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
/// the VFS then routes every path under `target` to that backend. Returns 0 or
/// -1. This is the one call that hands the VFS a capability (the backend).
pub fn mount(target: []const u8, backend: usize) i32 {
const h = vfs() orelse return -1;
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const tlen = @min(target.len, protocol.maximum_payload);
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
var rbuf: [protocol.message_maximum]u8 = undefined;
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1;
if (result.len < protocol.reply_size) return -1;
return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1;
}
/// A directory entry filled by `readdir`.
pub const DirEntry = struct {
kind: u32 = 0, // a protocol.NodeKind
size: u64 = 0,
name_buffer: [64]u8 = undefined,
name_len: usize = 0,
pub fn name(self: *const DirEntry) []const u8 {
return self.name_buffer[0..self.name_len];
}
};
/// Open a directory for reading with `readdir`. Returns an fd or -1.
pub fn opendir(path: []const u8) i32 {
return open(path, protocol.directory);
}
/// Read the next entry of a directory fd into `entry`; returns false at EOF or on
/// error. Advances the fd's cursor by one entry.
pub fn readdir(fd: i32, entry: *DirEntry) bool {
const f = fdPtr(fd) orelse return false;
const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 };
var buffer: [protocol.message_maximum]u8 = undefined;
const r = transact(request, &.{}, &buffer) orelse return false;
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
if (r.payload.len < protocol.directory_entry_size) return false;
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
entry.kind = header.kind;
entry.size = header.size;
const source = r.payload[protocol.directory_entry_size..];
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
entry.name_len = nlen;
f.offset += 1;
return true;
}
/// Close a directory fd (same as `close`).
pub fn closedir(fd: i32) void {
close(fd);
}
+62
View File
@@ -0,0 +1,62 @@
//! Block-device client: the helper a filesystem uses to read and write a block
//! device (a USB stick, via usb-storage) without hand-rolling the block-protocol
//! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like
//! `runtime.usb` over the transfer protocol.
//!
//! Transfers name a caller-owned DMA buffer by physical address (from
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
//! limit — the same handoff usb-storage uses toward the controller.
const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("block-protocol");
pub const Geometry = struct { block_size: u32, block_count: u64 };
pub const Device = struct {
endpoint: ipc.Handle,
/// The device's block size and total block count.
pub fn geometry(self: Device) ?Geometry {
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 };
var reply: [protocol.reply_size]u8 = undefined;
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null;
if (n < protocol.reply_size) return null;
const result = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
if (result.status != 0) return null;
return .{ .block_size = result.block_size, .block_count = result.block_count };
}
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
return self.transfer(.read, lba, count, physical);
}
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
return self.transfer(.write, lba, count, physical);
}
fn transfer(self: Device, operation: protocol.Operation, lba: u64, count: u32, physical: u64) bool {
var request = protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical };
var reply: [protocol.reply_size]u8 = undefined;
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false;
if (n < protocol.reply_size) return false;
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
}
};
/// Look up the block device, retrying generously while the USB storage chain
/// (controller reset, enumeration, mass-storage bring-up) comes up.
pub fn open() ?Device {
// Patient: the whole USB storage chain (firmware discovery, xHCI reset and
// enumeration, mass-storage bring-up) must complete first, which can take
// tens of seconds under emulation.
var attempts: usize = 0;
while (attempts < 1200) : (attempts += 1) {
if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle };
system.sleep(50);
}
return null;
}
+8
View File
@@ -38,6 +38,14 @@ pub const device = @import("device.zig");
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers. /// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
pub const dma = @import("dma.zig"); pub const dma = @import("dma.zig");
/// USB class-driver client: open a device on the xHCI bus and drive it
/// (control / interrupt / bulk transfers). See library/runtime/usb.zig.
pub const usb = @import("usb.zig");
/// Block-device client: read/write a block device (a USB stick, via
/// usb-storage). See library/runtime/block.zig.
pub const block = @import("block.zig");
/// Re-exported so a user binary can `pub const panic = runtime.panic;`. /// Re-exported so a user binary can `pub const panic = runtime.panic;`.
pub const panic = start.panic; pub const panic = start.panic;
+162
View File
@@ -0,0 +1,162 @@
//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver
//! uses to reach its device through the xHCI bus driver, so it never hand-rolls
//! the transfer-protocol IPC. Layered over `ipc` and the shared
//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input
//! service and `device.zig` over the raw device calls.
//!
//! A class driver, spawned with its interface's assigned device id as argv[1]:
//! if (!usb.helloManager(id)) return; // meet the spawn deadline
//! var device = usb.open(id) orelse return; // open + get its endpoints
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
//!
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport`
//! messages (the class driver runs a bare `replyWait` loop to read them, because
//! the service harness drops buffered-message payloads — see service.zig).
const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("usb-transfer-protocol");
const device_manager = @import("device-manager-protocol");
pub const Endpoint = protocol.Endpoint;
pub const InterruptReport = protocol.InterruptReport;
pub const max_report_data = protocol.max_report_data;
// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`.
pub const transfer_type_bulk: u8 = 2;
pub const transfer_type_interrupt: u8 = 3;
/// An opened USB device: the bus endpoint to send requests to, this driver's own
/// endpoint that reports arrive on, the device token, and the interface's
/// endpoints (so a driver need not re-read the configuration descriptor).
pub const Device = struct {
bus: ipc.Handle,
endpoint: ipc.Handle,
token: u64,
class: u8,
subclass: u8,
protocol_code: u8,
interface_number: u8,
endpoint_count: usize = 0,
endpoints: [protocol.max_reported_endpoints]Endpoint = undefined,
/// The interface's first endpoint of the given transfer type and direction
/// (`transfer_type_bulk` / `transfer_type_interrupt`), or null.
pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint {
for (self.endpoints[0..self.endpoint_count]) |endpoint| {
if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint;
}
return null;
}
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
var request = protocol.ControlRequest{
.device_token = self.token,
.setup = setup,
.direction_in = @intFromBool(direction_in),
.data_length = @intCast(data.len),
};
if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
var reply: [@sizeOf(protocol.ControlReply)]u8 = undefined;
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
if (length < @sizeOf(protocol.ControlReply)) return null;
const control_reply = std.mem.bytesToValue(protocol.ControlReply, reply[0..@sizeOf(protocol.ControlReply)]);
if (control_reply.status != 0) return null;
const actual = @min(control_reply.actual_length, data.len);
if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]);
return actual;
}
/// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The
/// `setup` is a bit-cast `usb_abi.Request`.
pub fn controlOut(self: *Device, setup: [8]u8) bool {
return self.controlTransfer(setup, false, &.{}) != null;
}
/// A device-to-host control transfer, returning the bytes read into `out`.
pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize {
return self.controlTransfer(setup, true, out);
}
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
/// `self.endpoint` as asynchronous `InterruptReport` messages.
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
var request = protocol.InterruptSubscribeRequest{
.device_token = self.token,
.endpoint_address = endpoint_address,
.max_length = max_length,
};
var reply: [@sizeOf(protocol.InterruptSubscribeReply)]u8 = undefined;
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false;
if (length < @sizeOf(protocol.InterruptSubscribeReply)) return false;
return std.mem.bytesToValue(protocol.InterruptSubscribeReply, reply[0..@sizeOf(protocol.InterruptSubscribeReply)]).status == 0;
}
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
var request = protocol.BulkRequest{
.device_token = self.token,
.physical_address = physical,
.length = length,
.endpoint_address = endpoint_address,
};
var reply: [@sizeOf(protocol.BulkReply)]u8 = undefined;
const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
if (replied < @sizeOf(protocol.BulkReply)) return null;
const bulk_reply = std.mem.bytesToValue(protocol.BulkReply, reply[0..@sizeOf(protocol.BulkReply)]);
if (bulk_reply.status != 0) return null;
return bulk_reply.actual_length;
}
};
/// Look up the USB bus and open the device with the assigned id, handing over a
/// freshly created endpoint for asynchronous interrupt reports. Retries while the
/// bus is still coming up (a class driver races the bus driver at boot).
pub fn open(device_id: u64) ?Device {
var attempts: usize = 0;
const bus = while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.usb_bus)) |handle| break handle;
system.sleep(20);
} else return null;
const endpoint = ipc.createIpcEndpoint() orelse return null;
var request = protocol.OpenRequest{ .device_id = device_id };
var reply: [@sizeOf(protocol.OpenReply)]u8 = undefined;
const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null;
if (result.len < @sizeOf(protocol.OpenReply)) return null;
const open_reply = std.mem.bytesToValue(protocol.OpenReply, reply[0..@sizeOf(protocol.OpenReply)]);
if (open_reply.status != 0) return null;
var device = Device{
.bus = bus,
.endpoint = endpoint,
.token = open_reply.device_token,
.class = open_reply.interface_class,
.subclass = open_reply.interface_subclass,
.protocol_code = open_reply.interface_protocol,
.interface_number = open_reply.interface_number,
.endpoint_count = @min(open_reply.endpoint_count, protocol.max_reported_endpoints),
};
for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
return device;
}
/// Hello the device manager as a class driver (Role.device) so a supervised
/// spawn meets its hello deadline. Retries while the manager comes up.
pub fn helloManager(device_id: u64) bool {
var attempts: usize = 0;
const manager = while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.device_manager)) |handle| break handle;
system.sleep(20);
} else return false;
const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id };
var reply: [device_manager.message_maximum]u8 = undefined;
const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false;
if (length < device_manager.reply_size) return false;
return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0;
}
+3
View File
@@ -178,6 +178,9 @@ pub const ServiceId = enum(u32) {
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md) device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM) power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM)
usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md)
block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
_, _,
}; };
+5
View File
@@ -33,6 +33,11 @@ pub const DeviceClass = enum(u32) {
/// a broad io_port grant for OperationRegion access, and the SCI interrupt. /// a broad io_port grant for OperationRegion access, and the SCI interrupt.
/// The one node whose claimant is trusted to run firmware bytecode. /// The one node whose claimant is trusted to run firmware bytecode.
acpi_tables, acpi_tables,
/// One interface of a USB device, registered by the xHCI bus driver. It owns
/// no MMIO — it is reached through its controller — so it carries no
/// resources; the (class, subclass, protocol) triple that says what it is
/// travels in the bus report's identity, not here.
usb_device,
unknown, unknown,
}; };
+139 -51
View File
@@ -8,7 +8,7 @@
//! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic //! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic
//! mask. Class, subclass, and protocol code tables live in usb-ids.zig. //! mask. Class, subclass, and protocol code tables live in usb-ids.zig.
const DeviceState = enum(u8) { pub const DeviceState = enum(u8) {
// Immediately after the USB device is attached to the USB system, it is in this state. // Immediately after the USB device is attached to the USB system, it is in this state.
// The USB specifications do not define the state of a USB device that is detached from // The USB specifications do not define the state of a USB device that is detached from
// a USB system. // a USB system.
@@ -47,7 +47,7 @@ const DeviceState = enum(u8) {
suspended, suspended,
}; };
const RequestCode = enum(u8) { pub const RequestCode = enum(u8) {
get_status = 0, get_status = 0,
clear_feature = 1, clear_feature = 1,
set_feature = 3, set_feature = 3,
@@ -59,10 +59,15 @@ const RequestCode = enum(u8) {
get_interface = 10, get_interface = 10,
set_interface = 11, set_interface = 11,
sync_frame = 12, sync_frame = 12,
// Non-exhaustive: class-specific requests (HID, mass storage) reuse this byte
// field with codes from their own class's namespace — see the class-request
// constructors below. Some class codes numerically coincide with a standard
// one; the wire byte is what matters, and the constructors set it explicitly.
_,
}; };
// Direction of an endpoint, from the host's point of view // Direction of an endpoint, from the host's point of view
const EndpointDirection = enum(u1) { pub const EndpointDirection = enum(u1) {
out = 0, out = 0,
in = 1, in = 1,
}; };
@@ -74,7 +79,7 @@ const EndpointDirection = enum(u1) {
// The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits // The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits
// wide. // wide.
const DeviceAddress = enum(u7) { pub const DeviceAddress = enum(u7) {
// The default address every device answers at after a reset, until SET_ADDRESS // The default address every device answers at after a reset, until SET_ADDRESS
// completes // completes
default = 0, default = 0,
@@ -82,7 +87,7 @@ const DeviceAddress = enum(u7) {
}; };
// Identifies a configuration; from ConfigurationDescriptor.configuration_value. // Identifies a configuration; from ConfigurationDescriptor.configuration_value.
const ConfigurationValue = enum(u8) { pub const ConfigurationValue = enum(u8) {
// Not configured: returned by GET_CONFIGURATION while the device is in the address // Not configured: returned by GET_CONFIGURATION while the device is in the address
// state, and passed to SET_CONFIGURATION to return a configured device to the address // state, and passed to SET_CONFIGURATION to return a configured device to the address
// state // state
@@ -92,11 +97,11 @@ const ConfigurationValue = enum(u8) {
// Identifies an interface within a configuration; from // Identifies an interface within a configuration; from
// InterfaceDescriptor.interface_number. // InterfaceDescriptor.interface_number.
const InterfaceNumber = enum(u8) { _ }; pub const InterfaceNumber = enum(u8) { _ };
// Selects between the alternate settings of one interface; from // Selects between the alternate settings of one interface; from
// InterfaceDescriptor.alternate_setting. // InterfaceDescriptor.alternate_setting.
const AlternateSetting = enum(u8) { pub const AlternateSetting = enum(u8) {
// The default setting of an interface // The default setting of an interface
default = 0, default = 0,
_, _,
@@ -104,7 +109,7 @@ const AlternateSetting = enum(u8) {
// The number of an endpoint within a device, 4 bits wide. The direction bit carried // The number of an endpoint within a device, 4 bits wide. The direction bit carried
// alongside it tells the two endpoints sharing a number apart. // alongside it tells the two endpoints sharing a number apart.
const EndpointNumber = enum(u4) { pub const EndpointNumber = enum(u4) {
// Endpoint zero: the default control pipe every device provides // Endpoint zero: the default control pipe every device provides
default_control = 0, default_control = 0,
_, _,
@@ -112,7 +117,7 @@ const EndpointNumber = enum(u4) {
// Index of a STRING descriptor, stored in descriptors that reference a string and passed to // Index of a STRING descriptor, stored in descriptors that reference a string and passed to
// GET_DESCRIPTOR to read it. // GET_DESCRIPTOR to read it.
const StringIndex = enum(u8) { pub const StringIndex = enum(u8) {
// The device has no string descriptor for this field // The device has no string descriptor for this field
none = 0, none = 0,
_, _,
@@ -121,7 +126,7 @@ const StringIndex = enum(u8) {
// Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are // Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are
// declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and // declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and
// direction bit 7. // direction bit 7.
const RequestType = packed struct(u8) { pub const RequestType = packed struct(u8) {
// The recipient of the request (values 4...31 are reserved) // The recipient of the request (values 4...31 are reserved)
recipient: Recipient, recipient: Recipient,
// The type of the request // The type of the request
@@ -129,27 +134,27 @@ const RequestType = packed struct(u8) {
// Data transfer direction. The value of this bit is ignored when length is zero. // Data transfer direction. The value of this bit is ignored when length is zero.
direction: Direction, direction: Direction,
const Recipient = enum(u5) { pub const Recipient = enum(u5) {
device = 0, device = 0,
interface = 1, interface = 1,
endpoint = 2, endpoint = 2,
other = 3, other = 3,
}; };
const Kind = enum(u2) { pub const Kind = enum(u2) {
standard = 0, standard = 0,
class = 1, class = 1,
vendor = 2, vendor = 2,
reserved = 3, reserved = 3,
}; };
const Direction = enum(u1) { pub const Direction = enum(u1) {
host_to_device = 0, host_to_device = 0,
device_to_host = 1, device_to_host = 1,
}; };
}; };
const Request = extern struct { pub const Request = extern struct {
// Characteristics of the request // Characteristics of the request
request_type: RequestType, request_type: RequestType,
// Specific request // Specific request
@@ -175,7 +180,7 @@ const Request = extern struct {
// The format of the index field when request_type specifies an endpoint as the // The format of the index field when request_type specifies an endpoint as the
// recipient. The host should always set the direction bit to zero (but the device // recipient. The host should always set the direction bit to zero (but the device
// should accept either value) when the endpoint is part of a control pipe. // should accept either value) when the endpoint is part of a control pipe.
const EndpointIndex = packed struct(u16) { pub const EndpointIndex = packed struct(u16) {
// Endpoint number // Endpoint number
number: EndpointNumber, number: EndpointNumber,
// Reserved (reset to zero) // Reserved (reset to zero)
@@ -188,7 +193,7 @@ const Request = extern struct {
// The format of the index field when request_type specifies an interface as the // The format of the index field when request_type specifies an interface as the
// recipient. // recipient.
const InterfaceIndex = packed struct(u16) { pub const InterfaceIndex = packed struct(u16) {
// Interface number // Interface number
number: u8, number: u8,
// Reserved (reset to zero) // Reserved (reset to zero)
@@ -199,7 +204,7 @@ const Request = extern struct {
// descriptor type in the high byte, and the descriptor index in the low byte. The index // descriptor type in the high byte, and the descriptor index in the low byte. The index
// is used to select a specific descriptor (only for CONFIGURATION and STRING // is used to select a specific descriptor (only for CONFIGURATION and STRING
// descriptors) when several descriptors of that type are implemented by a device. // descriptors) when several descriptors of that type are implemented by a device.
const DescriptorValue = packed struct(u16) { pub const DescriptorValue = packed struct(u16) {
// Descriptor index // Descriptor index
index: u8 = 0, index: u8 = 0,
// Descriptor type // Descriptor type
@@ -209,7 +214,7 @@ const Request = extern struct {
// Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The // Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The
// comment on each value notes the recipient the selector applies to. // comment on each value notes the recipient the selector applies to.
const FeatureSelector = enum(u16) { pub const FeatureSelector = enum(u16) {
// Halts an endpoint (recipient: endpoint) // Halts an endpoint (recipient: endpoint)
endpoint_halt = 0, endpoint_halt = 0,
// Enables or disables the device's remote wakeup capability (recipient: device) // Enables or disables the device's remote wakeup capability (recipient: device)
@@ -223,7 +228,7 @@ const FeatureSelector = enum(u16) {
// with the test_mode feature selector. Values 06h...3Fh are reserved for standard test // with the test_mode feature selector. Values 06h...3Fh are reserved for standard test
// selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are // selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are
// reserved. // reserved.
const TestMode = enum(u8) { pub const TestMode = enum(u8) {
test_j = 0x01, test_j = 0x01,
test_k = 0x02, test_k = 0x02,
test_se0_nak = 0x03, test_se0_nak = 0x03,
@@ -234,7 +239,7 @@ const TestMode = enum(u8) {
// The two bytes returned by a GET_STATUS request directed at a device. Fields are declared // The two bytes returned by a GET_STATUS request directed at a device. Fields are declared
// least-significant first. // least-significant first.
const DeviceStatus = packed struct(u16) { pub const DeviceStatus = packed struct(u16) {
// Whether the device is currently self-powered (as opposed to bus-powered). This bit // Whether the device is currently self-powered (as opposed to bus-powered). This bit
// cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests. // cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests.
self_powered: bool, self_powered: bool,
@@ -248,7 +253,7 @@ const DeviceStatus = packed struct(u16) {
// The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS // The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS
// request directed at an interface returns two bytes that are entirely reserved.) // request directed at an interface returns two bytes that are entirely reserved.)
const EndpointStatus = packed struct(u16) { pub const EndpointStatus = packed struct(u16) {
// Whether the endpoint is currently halted. Set with the SET_FEATURE request using the // Whether the endpoint is currently halted. Set with the SET_FEATURE request using the
// endpoint_halt feature selector, and cleared with CLEAR_FEATURE. // endpoint_halt feature selector, and cleared with CLEAR_FEATURE.
halted: bool, halted: bool,
@@ -258,7 +263,7 @@ const EndpointStatus = packed struct(u16) {
// A target for the standard requests that may be directed at the device, an interface, or // A target for the standard requests that may be directed at the device, an interface, or
// an endpoint. // an endpoint.
const Target = union(enum) { pub const Target = union(enum) {
device, device,
interface: InterfaceNumber, interface: InterfaceNumber,
endpoint: Request.EndpointIndex, endpoint: Request.EndpointIndex,
@@ -287,7 +292,7 @@ const Target = union(enum) {
// Reads the status of the given target: bit-cast the two bytes the device returns into a // Reads the status of the given target: bit-cast the two bytes the device returns into a
// DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely // DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely
// reserved.) // reserved.)
fn getStatus(target: Target) Request { pub fn getStatus(target: Target) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = target.recipient(), .recipient = target.recipient(),
@@ -303,7 +308,7 @@ fn getStatus(target: Target) Request {
// Clears or disables the given feature. A device cannot be taken out of a test mode with // Clears or disables the given feature. A device cannot be taken out of a test mode with
// this request; test_mode is only cleared by cycling power. // this request; test_mode is only cleared by cycling power.
fn clearFeature(feature: FeatureSelector, target: Target) Request { pub fn clearFeature(feature: FeatureSelector, target: Target) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = target.recipient(), .recipient = target.recipient(),
@@ -319,7 +324,7 @@ fn clearFeature(feature: FeatureSelector, target: Target) Request {
// Sets or enables the given feature. For the test_mode feature selector, use setTestMode // Sets or enables the given feature. For the test_mode feature selector, use setTestMode
// instead: the test selector rides in the high byte of the index field. // instead: the test selector rides in the high byte of the index field.
fn setFeature(feature: FeatureSelector, target: Target) Request { pub fn setFeature(feature: FeatureSelector, target: Target) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = target.recipient(), .recipient = target.recipient(),
@@ -335,7 +340,7 @@ fn setFeature(feature: FeatureSelector, target: Target) Request {
// Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode // Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode
// feature selector and the test selector in the high byte of the index field. // feature selector and the test selector in the high byte of the index field.
fn setTestMode(mode: TestMode) Request { pub fn setTestMode(mode: TestMode) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -352,7 +357,7 @@ fn setTestMode(mode: TestMode) Request {
// Assigns the device its bus address, moving it from the default state to the address // Assigns the device its bus address, moving it from the default state to the address
// state. The device does not answer at the new address until the status stage of this // state. The device does not answer at the new address until the status stage of this
// request completes. // request completes.
fn setAddress(address: DeviceAddress) Request { pub fn setAddress(address: DeviceAddress) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -372,7 +377,7 @@ fn setAddress(address: DeviceAddress) Request {
// - language_id selects the language of a string descriptor, and is zero otherwise. // - language_id selects the language of a string descriptor, and is zero otherwise.
// - length is the number of bytes to read; a device never returns more than length bytes, // - length is the number of bytes to read; a device never returns more than length bytes,
// but may return less if the descriptor is shorter. // but may return less if the descriptor is shorter.
fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { pub fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -389,7 +394,7 @@ fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, l
// Updates an existing descriptor or adds a new one (optional; many devices do not support // Updates an existing descriptor or adds a new one (optional; many devices do not support
// this request). The parameters mirror getDescriptor; the descriptor itself is sent in the // this request). The parameters mirror getDescriptor; the descriptor itself is sent in the
// DATA stage. // DATA stage.
fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { pub fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -405,7 +410,7 @@ fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, l
// Reads the currently active configuration: @enumFromInt the byte the device returns into a // Reads the currently active configuration: @enumFromInt the byte the device returns into a
// ConfigurationValue, which is none while the device is not configured. // ConfigurationValue, which is none while the device is not configured.
fn getConfiguration() Request { pub fn getConfiguration() Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -422,7 +427,7 @@ fn getConfiguration() Request {
// Selects the configuration with the given configuration_value (from // Selects the configuration with the given configuration_value (from
// ConfigurationDescriptor.configuration_value), moving the device from the address state to // ConfigurationDescriptor.configuration_value), moving the device from the address state to
// the configured state. Selecting none returns the device to the address state. // the configured state. Selecting none returns the device to the address state.
fn setConfiguration(configuration_value: ConfigurationValue) Request { pub fn setConfiguration(configuration_value: ConfigurationValue) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .device, .recipient = .device,
@@ -438,7 +443,7 @@ fn setConfiguration(configuration_value: ConfigurationValue) Request {
// Reads the alternate setting currently selected for the given interface: @enumFromInt the // Reads the alternate setting currently selected for the given interface: @enumFromInt the
// byte the device returns into an AlternateSetting. // byte the device returns into an AlternateSetting.
fn getInterface(interface: InterfaceNumber) Request { pub fn getInterface(interface: InterfaceNumber) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .interface, .recipient = .interface,
@@ -454,7 +459,7 @@ fn getInterface(interface: InterfaceNumber) Request {
// Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given // Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given
// interface. // interface.
fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request { pub fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .interface, .recipient = .interface,
@@ -470,7 +475,7 @@ fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting)
// Reads the two-byte number of the frame in which the given isochronous endpoint's // Reads the two-byte number of the frame in which the given isochronous endpoint's
// repeating pattern of transfers begins. // repeating pattern of transfers begins.
fn syncFrame(endpoint: Request.EndpointIndex) Request { pub fn syncFrame(endpoint: Request.EndpointIndex) Request {
return .{ return .{
.request_type = .{ .request_type = .{
.recipient = .endpoint, .recipient = .endpoint,
@@ -484,7 +489,79 @@ fn syncFrame(endpoint: Request.EndpointIndex) Request {
}; };
} }
const DescriptorType = enum(u8) { // Class-specific requests. These carry a `kind = .class` request_type and a
// request_code from the interface's class namespace (not the standard
// RequestCode set above); the code is written into the same byte field, which
// is why RequestCode is non-exhaustive. Each is directed at an interface, whose
// number rides in the index field.
// The HID class request codes (USB HID 1.11 §7.2). Only the ones danos issues
// are named; the field on the wire is the raw byte.
pub const HidRequestCode = enum(u8) {
get_report = 0x01,
get_idle = 0x02,
get_protocol = 0x03,
set_report = 0x09,
set_idle = 0x0A,
set_protocol = 0x0B,
};
// The two protocols a boot-capable HID device can run (USB HID 1.11 §7.2.5).
// A driver selects `boot` for the simplified fixed-format boot report, usable
// before a full report-descriptor parser exists.
pub const HidProtocol = enum(u8) {
boot = 0,
report = 1,
};
// SET_PROTOCOL: choose the boot or report protocol on a HID interface.
pub fn setProtocol(interface: InterfaceNumber, protocol: HidProtocol) Request {
return .{
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_protocol)),
.value = @intFromEnum(protocol),
.index = @intFromEnum(interface),
.length = 0,
};
}
// SET_IDLE: bound a HID interface's report rate. `duration` is in 4 ms units
// (0 means report only on change); `report_id` selects a report (0 = all).
pub fn setIdle(interface: InterfaceNumber, duration: u8, report_id: u8) Request {
return .{
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_idle)),
.value = (@as(u16, duration) << 8) | report_id,
.index = @intFromEnum(interface),
.length = 0,
};
}
// Bulk-Only Mass Storage Reset (USB MSC BOT §3.1): ready a mass-storage
// interface for the next Command Block Wrapper after a protocol error.
pub fn bulkOnlyMassStorageReset(interface: InterfaceNumber) Request {
return .{
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
.request_code = @enumFromInt(0xFF),
.value = 0,
.index = @intFromEnum(interface),
.length = 0,
};
}
// Get Max LUN (USB MSC BOT §3.2): read the highest logical unit number the
// device supports (0 for a single-LUN flash drive). One byte is returned.
pub fn getMaxLun(interface: InterfaceNumber) Request {
return .{
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .device_to_host },
.request_code = @enumFromInt(0xFE),
.value = 0,
.index = @intFromEnum(interface),
.length = 1,
};
}
pub const DescriptorType = enum(u8) {
device = 1, device = 1,
configuration = 2, configuration = 2,
string = 3, string = 3,
@@ -496,7 +573,7 @@ const DescriptorType = enum(u8) {
_, _,
}; };
const DeviceDescriptor = extern struct { pub const DeviceDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// DEVICE Descriptor Type // DEVICE Descriptor Type
@@ -541,7 +618,7 @@ const DeviceDescriptor = extern struct {
configuration_count: u8, configuration_count: u8,
}; };
const DeviceQualifierDescriptor = extern struct { pub const DeviceQualifierDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// DEVICE_QUALIFIER Descriptor Type // DEVICE_QUALIFIER Descriptor Type
@@ -564,7 +641,7 @@ const DeviceQualifierDescriptor = extern struct {
reserved: u8, reserved: u8,
}; };
const ConfigurationDescriptor = extern struct { pub const ConfigurationDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// CONFIGURATION Descriptor Type // CONFIGURATION Descriptor Type
@@ -593,7 +670,7 @@ const ConfigurationDescriptor = extern struct {
max_power: u8, max_power: u8,
// Configuration characteristics. Fields are declared least-significant first. // Configuration characteristics. Fields are declared least-significant first.
const Attributes = packed struct(u8) { pub const Attributes = packed struct(u8) {
// Reserved, reset to zero (D4...0) // Reserved, reset to zero (D4...0)
reserved: u5, reserved: u5,
// Whether Remote Wakeup is supported by this configuration (D5) // Whether Remote Wakeup is supported by this configuration (D5)
@@ -612,9 +689,9 @@ const ConfigurationDescriptor = extern struct {
// its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that // its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that
// of the CONFIGURATION descriptor; the only difference is that the descriptor_type field // of the CONFIGURATION descriptor; the only difference is that the descriptor_type field
// reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor. // reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor.
const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor; pub const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor;
const InterfaceDescriptor = extern struct { pub const InterfaceDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// INTERFACE Descriptor Type // INTERFACE Descriptor Type
@@ -654,7 +731,7 @@ const InterfaceDescriptor = extern struct {
interface_index: StringIndex, interface_index: StringIndex,
}; };
const EndpointDescriptor = extern struct { pub const EndpointDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// ENDPOINT Descriptor Type // ENDPOINT Descriptor Type
@@ -683,7 +760,7 @@ const EndpointDescriptor = extern struct {
interval: u8, interval: u8,
// The address of an endpoint. Fields are declared least-significant first. // The address of an endpoint. Fields are declared least-significant first.
const Address = packed struct(u8) { pub const Address = packed struct(u8) {
// Endpoint Number (D3...0) // Endpoint Number (D3...0)
number: EndpointNumber, number: EndpointNumber,
// Reserved, reset to zero (D6...4) // Reserved, reset to zero (D6...4)
@@ -693,7 +770,7 @@ const EndpointDescriptor = extern struct {
}; };
// An endpoint's attributes. Fields are declared least-significant first. // An endpoint's attributes. Fields are declared least-significant first.
const Attributes = packed struct(u8) { pub const Attributes = packed struct(u8) {
// Transfer Type (D1...0) // Transfer Type (D1...0)
transfer_type: TransferType, transfer_type: TransferType,
// Synchronization Type; isochronous endpoints only, reserved and reset to zero for // Synchronization Type; isochronous endpoints only, reserved and reset to zero for
@@ -706,21 +783,21 @@ const EndpointDescriptor = extern struct {
reserved: u2, reserved: u2,
}; };
const TransferType = enum(u2) { pub const TransferType = enum(u2) {
control = 0, control = 0,
isochronous = 1, isochronous = 1,
bulk = 2, bulk = 2,
interrupt = 3, interrupt = 3,
}; };
const Synchronization = enum(u2) { pub const Synchronization = enum(u2) {
none = 0, none = 0,
asynchronous = 1, asynchronous = 1,
adaptive = 2, adaptive = 2,
synchronous = 3, synchronous = 3,
}; };
const Usage = enum(u2) { pub const Usage = enum(u2) {
data = 0, data = 0,
feedback = 1, feedback = 1,
implicit_feedback_data = 2, implicit_feedback_data = 2,
@@ -728,7 +805,7 @@ const EndpointDescriptor = extern struct {
}; };
// The maximum packet size of an endpoint. Fields are declared least-significant first. // The maximum packet size of an endpoint. Fields are declared least-significant first.
const MaxPacketSize = packed struct(u16) { pub const MaxPacketSize = packed struct(u16) {
// Maximum packet size in bytes (bits 10...0) // Maximum packet size in bytes (bits 10...0)
size: u11, size: u11,
// Number of additional transaction opportunities per microframe, for high-speed // Number of additional transaction opportunities per microframe, for high-speed
@@ -739,7 +816,7 @@ const EndpointDescriptor = extern struct {
reserved: u3, reserved: u3,
}; };
const AdditionalTransactions = enum(u2) { pub const AdditionalTransactions = enum(u2) {
// None (1 transaction per microframe) // None (1 transaction per microframe)
none = 0, none = 0,
// 1 additional (2 transactions per microframe) // 1 additional (2 transactions per microframe)
@@ -755,7 +832,7 @@ const EndpointDescriptor = extern struct {
// header, followed by the variable-length payload: // header, followed by the variable-length payload:
// - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x]) // - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x])
// - other indices: a Unicode string of N bytes // - other indices: a Unicode string of N bytes
const StringDescriptor = extern struct { pub const StringDescriptor = extern struct {
// Size of this descriptor in bytes // Size of this descriptor in bytes
length: u8, length: u8,
// STRING Descriptor Type // STRING Descriptor Type
@@ -834,7 +911,7 @@ test "bitmap packings match the specification" {
try expect(hid_type != .device); try expect(hid_type != .device);
} }
fn expectRequestBytes(request: Request, expected: [8]u8) !void { pub fn expectRequestBytes(request: Request, expected: [8]u8) !void {
try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request)); try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request));
} }
@@ -855,3 +932,14 @@ test "standard request constructors encode the specification's set-up packets" {
try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 }); try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 });
try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 }); try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 });
} }
test "class request constructors encode the specification's set-up packets" {
// bmRequestType for a host-to-device class request to an interface = 0x21;
// device-to-host = 0xA1. The request_code byte is the class code, not a
// standard one — SET_PROTOCOL 0x0B, SET_IDLE 0x0A, BOT reset 0xFF, Max LUN 0xFE.
try expectRequestBytes(setProtocol(@enumFromInt(0), .boot), .{ 0x21, 0x0B, 0, 0, 0, 0, 0, 0 });
try expectRequestBytes(setProtocol(@enumFromInt(1), .report), .{ 0x21, 0x0B, 1, 0, 1, 0, 0, 0 });
try expectRequestBytes(setIdle(@enumFromInt(1), 0, 0), .{ 0x21, 0x0A, 0, 0, 1, 0, 0, 0 });
try expectRequestBytes(bulkOnlyMassStorageReset(@enumFromInt(0)), .{ 0x21, 0xFF, 0, 0, 0, 0, 0, 0 });
try expectRequestBytes(getMaxLun(@enumFromInt(0)), .{ 0xA1, 0xFE, 0, 0, 0, 0, 1, 0 });
}
+62 -19
View File
@@ -11,7 +11,7 @@
// Base class codes (assigned by the USB-IF). The comment on each value notes where the code // Base class codes (assigned by the USB-IF). The comment on each value notes where the code
// may legally appear: in the device descriptor, in interface descriptors, or both. // may legally appear: in the device descriptor, in interface descriptors, or both.
const Class = enum(u8) { pub const Class = enum(u8) {
// Use class information in the interface descriptors (device descriptor only). Each // Use class information in the interface descriptors (device descriptor only). Each
// interface within a configuration specifies its own class information and the various // interface within a configuration specifies its own class information and the various
// interfaces operate independently. // interfaces operate independently.
@@ -72,8 +72,8 @@ const Class = enum(u8) {
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the // Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
// protocol distinguishes the hub's transaction-translator arrangement. // protocol distinguishes the hub's transaction-translator arrangement.
const hub = struct { pub const hub = struct {
const Protocol = enum(u8) { pub const Protocol = enum(u8) {
// Full-speed hub // Full-speed hub
full_speed = 0x00, full_speed = 0x00,
// Hi-speed hub with a single transaction translator // Hi-speed hub with a single transaction translator
@@ -87,8 +87,8 @@ const hub = struct {
}; };
// Subclass and protocol codes qualified by Class.hid. // Subclass and protocol codes qualified by Class.hid.
const hid = struct { pub const hid = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// No subclass // No subclass
none = 0x00, none = 0x00,
// Boot interface: the device also supports the simplified boot protocol, usable by // Boot interface: the device also supports the simplified boot protocol, usable by
@@ -98,7 +98,7 @@ const hid = struct {
}; };
// Only meaningful when the subclass is boot // Only meaningful when the subclass is boot
const Protocol = enum(u8) { pub const Protocol = enum(u8) {
none = 0x00, none = 0x00,
keyboard = 0x01, keyboard = 0x01,
mouse = 0x02, mouse = 0x02,
@@ -109,8 +109,8 @@ const hid = struct {
// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the // Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
// command set the device understands; the protocol identifies the transport used to carry // command set the device understands; the protocol identifies the transport used to carry
// commands, data, and status over the bus. // commands, data, and status over the bus.
const mass_storage = struct { pub const mass_storage = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// SCSI command set not reported; de facto, treat as scsi // SCSI command set not reported; de facto, treat as scsi
not_reported = 0x00, not_reported = 0x00,
// Reduced Block Commands: typically flash devices // Reduced Block Commands: typically flash devices
@@ -134,7 +134,7 @@ const mass_storage = struct {
_, _,
}; };
const Protocol = enum(u8) { pub const Protocol = enum(u8) {
// Control/Bulk/Interrupt with command completion interrupt // Control/Bulk/Interrupt with command completion interrupt
cbi_completion_interrupt = 0x00, cbi_completion_interrupt = 0x00,
// Control/Bulk/Interrupt without command completion interrupt // Control/Bulk/Interrupt without command completion interrupt
@@ -152,8 +152,8 @@ const mass_storage = struct {
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes // Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
// are model-specific; the useful invariant is the subclass, which selects the control model // are model-specific; the useful invariant is the subclass, which selects the control model
// the interface implements. // the interface implements.
const communications = struct { pub const communications = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// Direct line control model // Direct line control model
direct_line = 0x01, direct_line = 0x01,
// Abstract control model: USB modems and serial adapters // Abstract control model: USB modems and serial adapters
@@ -185,15 +185,15 @@ const communications = struct {
}; };
// Subclass and protocol codes qualified by Class.wireless_controller. // Subclass and protocol codes qualified by Class.wireless_controller.
const wireless_controller = struct { pub const wireless_controller = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// Radio frequency controllers // Radio frequency controllers
radio_frequency = 0x01, radio_frequency = 0x01,
_, _,
}; };
// Only meaningful when the subclass is radio_frequency // Only meaningful when the subclass is radio_frequency
const Protocol = enum(u8) { pub const Protocol = enum(u8) {
// Bluetooth programming interface // Bluetooth programming interface
bluetooth = 0x01, bluetooth = 0x01,
// Ultra-wideband radio control // Ultra-wideband radio control
@@ -207,15 +207,15 @@ const wireless_controller = struct {
}; };
// Subclass and protocol codes qualified by Class.miscellaneous. // Subclass and protocol codes qualified by Class.miscellaneous.
const miscellaneous = struct { pub const miscellaneous = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// Common class // Common class
common = 0x02, common = 0x02,
_, _,
}; };
// Only meaningful when the subclass is common // Only meaningful when the subclass is common
const Protocol = enum(u8) { pub const Protocol = enum(u8) {
// Interface association descriptor: at the device level, announces that the // Interface association descriptor: at the device level, announces that the
// configuration groups interfaces into functions with IADs // configuration groups interfaces into functions with IADs
interface_association = 0x01, interface_association = 0x01,
@@ -224,8 +224,8 @@ const miscellaneous = struct {
}; };
// Subclass and protocol codes qualified by Class.application_specific. // Subclass and protocol codes qualified by Class.application_specific.
const application_specific = struct { pub const application_specific = struct {
const SubClass = enum(u8) { pub const SubClass = enum(u8) {
// Device firmware upgrade // Device firmware upgrade
firmware_upgrade = 0x01, firmware_upgrade = 0x01,
// IrDA bridge // IrDA bridge
@@ -236,6 +236,23 @@ const application_specific = struct {
}; };
}; };
/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
/// bus-native identity a USB bus driver reports in `ChildAdded.identity` and the
/// device manager matches on (the USB analog of a packed PCI class code). Mirrors
/// `pci_class.ClassCode.pack`, so both sides build/decode the identical u64.
pub fn packTriple(class: u8, subclass: u8, protocol: u8) u64 {
return (@as(u64, class) << 16) | (@as(u64, subclass) << 8) | protocol;
}
/// The inverse of `packTriple`.
pub fn unpackTriple(triple: u64) struct { class: u8, subclass: u8, protocol: u8 } {
return .{
.class = @truncate(triple >> 16),
.subclass = @truncate(triple >> 8),
.protocol = @truncate(triple),
};
}
test "class codes match the USB-IF assignments" { test "class codes match the USB-IF assignments" {
const std = @import("std"); const std = @import("std");
const expectEqual = std.testing.expectEqual; const expectEqual = std.testing.expectEqual;
@@ -262,3 +279,29 @@ test "class codes match the USB-IF assignments" {
_ = miscellaneous.Protocol.interface_association; _ = miscellaneous.Protocol.interface_association;
_ = application_specific.SubClass.firmware_upgrade; _ = application_specific.SubClass.firmware_upgrade;
} }
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
const std = @import("std");
const expectEqual = std.testing.expectEqual;
// A boot keyboard interface: HID / boot / keyboard.
const keyboard = packTriple(
@intFromEnum(Class.hid),
@intFromEnum(hid.SubClass.boot),
@intFromEnum(hid.Protocol.keyboard),
);
try expectEqual(@as(u64, 0x03_01_01), keyboard);
// A flash drive interface: mass storage / SCSI / bulk-only.
const storage = packTriple(
@intFromEnum(Class.mass_storage),
@intFromEnum(mass_storage.SubClass.scsi),
@intFromEnum(mass_storage.Protocol.bulk_only),
);
try expectEqual(@as(u64, 0x08_06_50), storage);
const parts = unpackTriple(storage);
try expectEqual(@as(u8, 0x08), parts.class);
try expectEqual(@as(u8, 0x06), parts.subclass);
try expectEqual(@as(u8, 0x50), parts.protocol);
}
+191
View File
@@ -0,0 +1,191 @@
//! Pure decoders for USB HID **boot-protocol** reports — the simplified,
//! fixed-format reports a boot keyboard and boot mouse send, the USB analog of
//! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes
//! into make/break transitions and motion, which the usb-hid drivers publish to
//! the input service. Host-testable in isolation (like mouse-packet.zig).
//!
//! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in
//! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system
//! boot.
const std = @import("std");
// --- keyboard ---------------------------------------------------------------
/// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up
/// to six concurrently-pressed key usages.
pub const KeyboardReport = extern struct {
modifiers: u8 = 0,
reserved: u8 = 0,
keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 },
};
// The modifier byte's bits (HID keyboard boot report).
pub const modifier_left_control: u8 = 1 << 0;
pub const modifier_left_shift: u8 = 1 << 1;
pub const modifier_left_alt: u8 = 1 << 2;
pub const modifier_left_gui: u8 = 1 << 3;
pub const modifier_right_control: u8 = 1 << 4;
pub const modifier_right_shift: u8 = 1 << 5;
pub const modifier_right_alt: u8 = 1 << 6;
pub const modifier_right_gui: u8 = 1 << 7;
pub const TransitionKind = enum { pressed, released };
/// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys
/// map to usages 224..231 — which is exactly the input protocol's `Keycode`.
pub const Transition = struct { kind: TransitionKind, usage: u8 };
// A report can change at most all 8 modifiers and all 6 keys at once.
pub const max_transitions = 8 + 6;
pub const Transitions = struct {
items: [max_transitions]Transition = undefined,
count: usize = 0,
fn add(self: *Transitions, transition: Transition) void {
if (self.count < self.items.len) {
self.items[self.count] = transition;
self.count += 1;
}
}
pub fn slice(self: *const Transitions) []const Transition {
return self.items[0..self.count];
}
};
/// Turns a stream of boot keyboard reports into make/break transitions by diffing
/// each report against the last.
pub const KeyboardDecoder = struct {
previous: KeyboardReport = .{},
pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions {
var out = Transitions{};
// Rollover: 0x01 (ErrorRollOver) means more keys are held than the report
// can carry, so the key array is invalid. Emit nothing and keep the prior
// state (so the eventual releases still resolve against real keys).
for (current.keys) |key| {
if (key == 0x01) return out;
}
// Modifiers: one make/break per changed bit; modifier usages are 224..231.
const changed = current.modifiers ^ self.previous.modifiers;
var bit: u3 = 0;
while (true) : (bit += 1) {
const mask = @as(u8, 1) << bit;
if (changed & mask != 0) {
out.add(.{
.kind = if (current.modifiers & mask != 0) .pressed else .released,
.usage = 224 + @as(u8, bit),
});
}
if (bit == 7) break;
}
// Keys made: present now, absent before.
for (current.keys) |key| {
if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key });
}
// Keys broken: present before, absent now.
for (self.previous.keys) |key| {
if (key != 0 and !contains(&current.keys, key)) out.add(.{ .kind = .released, .usage = key });
}
self.previous = current;
return out;
}
};
fn contains(keys: *const [6]u8, value: u8) bool {
for (keys) |key| {
if (key == value) return true;
}
return false;
}
// --- mouse ------------------------------------------------------------------
/// A decoded boot mouse report: the button bitmap and relative motion. The wheel
/// byte is present only on 4-byte reports (QEMU's usb-mouse sends one).
pub const MouseReport = struct {
buttons: u8 = 0,
dx: i8 = 0,
dy: i8 = 0,
wheel: i8 = 0,
has_wheel: bool = false,
};
pub const mouse_button_left: u8 = 1 << 0;
pub const mouse_button_right: u8 = 1 << 1;
pub const mouse_button_middle: u8 = 1 << 2;
/// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen
/// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated.
pub fn parseMouse(bytes: []const u8) ?MouseReport {
if (bytes.len < 3) return null;
return .{
.buttons = bytes[0],
.dx = @bitCast(bytes[1]),
.dy = @bitCast(bytes[2]),
.wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0,
.has_wheel = bytes.len >= 4,
};
}
// --- tests ------------------------------------------------------------------
test "keyboard diff produces make and break transitions" {
var decoder = KeyboardDecoder{};
// Press 'a' (usage 4).
var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } });
try std.testing.expectEqual(@as(usize, 1), t.count);
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
try std.testing.expectEqual(@as(u8, 4), t.items[0].usage);
// Hold 'a', press 'b' (usage 5): only 'b' is new.
t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } });
try std.testing.expectEqual(@as(usize, 1), t.count);
try std.testing.expectEqual(@as(u8, 5), t.items[0].usage);
// Release everything: 'a' and 'b' both break.
t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
try std.testing.expectEqual(@as(usize, 2), t.count);
try std.testing.expectEqual(TransitionKind.released, t.items[0].kind);
// Press Left Shift (modifier bit 1 -> usage 225).
t = decoder.feed(.{ .modifiers = modifier_left_shift });
try std.testing.expectEqual(@as(usize, 1), t.count);
try std.testing.expectEqual(@as(u8, 225), t.items[0].usage);
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
}
test "rollover report is ignored but state is preserved" {
var decoder = KeyboardDecoder{};
_ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a'
const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } });
try std.testing.expectEqual(@as(usize, 0), rollover.count);
// 'a' is still considered down, so releasing all keys now breaks it.
const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
try std.testing.expectEqual(@as(usize, 1), release.count);
try std.testing.expectEqual(@as(u8, 4), release.items[0].usage);
try std.testing.expectEqual(TransitionKind.released, release.items[0].kind);
}
test "mouse report parses motion without inverting Y" {
const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5
try std.testing.expectEqual(mouse_button_left, three.buttons);
try std.testing.expectEqual(@as(i8, 5), three.dx);
try std.testing.expectEqual(@as(i8, -5), three.dy);
try std.testing.expect(!three.has_wheel);
const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1
try std.testing.expect(four.has_wheel);
try std.testing.expectEqual(@as(i8, -1), four.wheel);
try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short
}
+174
View File
@@ -0,0 +1,174 @@
//! USB HID boot keyboard driver.
//!
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id
//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2].
//! It owns no hardware: it opens its device through the USB transfer protocol
//! (`runtime.usb`), asks the device for the boot protocol, subscribes to its
//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol
//! events, published to the input service — the USB analogue of ps2-bus/keyboard.
//!
//! interrupt report -> hid-report diff -> key_down / key_up
//! -> xkeyboard-config -> character -> key_press
//!
//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are
//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation.
const std = @import("std");
const runtime = @import("runtime");
const usb_abi = @import("usb-abi");
const xkb = @import("xkeyboard-config");
const hid = @import("hid-report.zig");
const ipc = runtime.ipc;
const process = runtime.process;
const input_protocol = runtime.input_protocol;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
// The modifier state a character lookup needs — derived from the report's
// modifier byte, plus the driver-tracked caps-lock toggle.
const ModifierSnapshot = struct {
shift: bool,
control: bool,
right_alt: bool,
caps_lock: bool,
};
/// The character a key produces under `modifiers`, or 0 for none — the layout
/// lookup for printable keys, with ASCII control characters for the keys every
/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard.
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 {
const mapping = xkb.map(layout, usage, .{
.shift = modifiers.shift,
.caps_lock = modifiers.caps_lock,
.level3 = modifiers.right_alt,
.control = modifiers.control,
});
if (mapping.character) |character| return character;
return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) {
.enter, .keypad_enter => '\n',
.tab => '\t',
.backspace => 0x08,
.escape => 0x1B,
else => 0,
};
}
fn modifierWord(modifiers: u8) u32 {
var word: u32 = 0;
if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift;
if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control;
if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt;
return word;
}
pub fn main(init: runtime.process.Init) void {
const argument = init.arguments.get(1) orelse {
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n");
return;
};
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
writeLine("/system/drivers/usb-hid/keyboard: malformed device id '{s}'\n", .{argument});
return;
};
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
// Hello the manager first (meet the spawn deadline), then open the device.
if (!runtime.usb.helloManager(device_id)) {
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n");
return;
}
var device = runtime.usb.open(device_id) orelse {
writeLine("/system/drivers/usb-hid/keyboard: could not open device {d}\n", .{device_id});
return;
};
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n");
return;
};
// Ask for the boot protocol and an indefinite idle (report only on change).
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
_ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0)));
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n");
return;
}
var source = runtime.input.connectSource() orelse {
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: input service unavailable\n");
return;
};
_ = process.bindSignals(device.endpoint);
writeLine("/system/drivers/usb-hid/keyboard: ok (device {d}, interface {d}, layout {s})\n", .{ device_id, device.interface_number, layout.name });
var decoder = hid.KeyboardDecoder{};
var caps_lock = false;
var receive: [64]u8 = undefined;
while (true) {
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
if (!got.isNotification()) continue;
if (process.signalsFrom(got.badge)) |signals| {
if (signals.has(.terminate)) return;
continue;
}
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
const transitions = decoder.feed(report);
// Caps Lock toggles on its own key-down (a stateful lock, not a modifier).
for (transitions.slice()) |transition| {
if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock;
}
const modifiers = ModifierSnapshot{
.shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0,
.control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0,
.right_alt = report.modifiers & hid.modifier_right_alt != 0,
.caps_lock = caps_lock,
};
const modifier_word = modifierWord(report.modifiers);
for (transitions.slice()) |transition| {
switch (transition.kind) {
.pressed => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_down),
.keycode = transition.usage,
.character = 0,
.modifiers = modifier_word,
});
const character = characterFor(layout, transition.usage, modifiers);
if (character != 0) {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_press),
.keycode = transition.usage,
.character = character,
.modifiers = modifier_word,
});
}
},
.released => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_up),
.keycode = transition.usage,
.character = 0,
.modifiers = modifier_word,
});
},
}
}
}
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+140
View File
@@ -0,0 +1,140 @@
//! USB HID boot mouse driver.
//!
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
//! mouse interface (class 3, subclass 1, protocol 2); its assigned device id
//! arrives as argv[1]. Like the keyboard driver it owns no hardware: it opens its
//! device through the USB transfer protocol (`runtime.usb`), asks for the boot
//! protocol, subscribes to its interrupt-IN endpoint, and turns each 3- or 4-byte
//! boot report into input-protocol mouse events published to the input service.
//!
//! Unlike PS/2, HID reports Y in screen convention (positive = down), so motion
//! is passed straight through (the decode in hid-report.zig does not negate it).
const std = @import("std");
const runtime = @import("runtime");
const usb_abi = @import("usb-abi");
const hid = @import("hid-report.zig");
const ipc = runtime.ipc;
const process = runtime.process;
const input_protocol = runtime.input_protocol;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
// The current pressed-button bitmask in input-protocol terms.
fn buttonMask(buttons: u8) u32 {
var mask: u32 = 0;
if (buttons & hid.mouse_button_left != 0) mask |= input_protocol.mouse_button_left;
if (buttons & hid.mouse_button_right != 0) mask |= input_protocol.mouse_button_right;
if (buttons & hid.mouse_button_middle != 0) mask |= input_protocol.mouse_button_middle;
return mask;
}
pub fn main(init: runtime.process.Init) void {
const argument = init.arguments.get(1) orelse {
_ = runtime.system.write("/system/drivers/usb-hid/mouse: missing device id (argv[1])\n");
return;
};
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
writeLine("/system/drivers/usb-hid/mouse: malformed device id '{s}'\n", .{argument});
return;
};
if (!runtime.usb.helloManager(device_id)) {
_ = runtime.system.write("/system/drivers/usb-hid/mouse: hello to device manager failed\n");
return;
}
var device = runtime.usb.open(device_id) orelse {
writeLine("/system/drivers/usb-hid/mouse: could not open device {d}\n", .{device_id});
return;
};
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
_ = runtime.system.write("/system/drivers/usb-hid/mouse: no interrupt-IN endpoint\n");
return;
};
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
_ = runtime.system.write("/system/drivers/usb-hid/mouse: interrupt subscribe failed\n");
return;
}
var source = runtime.input.connectSource() orelse {
_ = runtime.system.write("/system/drivers/usb-hid/mouse: input service unavailable\n");
return;
};
_ = process.bindSignals(device.endpoint);
writeLine("/system/drivers/usb-hid/mouse: ok (device {d}, interface {d})\n", .{ device_id, device.interface_number });
var previous_buttons: u8 = 0;
var receive: [64]u8 = undefined;
while (true) {
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
if (!got.isNotification()) continue;
if (process.signalsFrom(got.badge)) |signals| {
if (signals.has(.terminate)) return;
continue;
}
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
const length = @min(message.length, message.data.len);
const report = hid.parseMouse(message.data[0..length]) orelse continue;
const mask = buttonMask(report.buttons);
// Button transitions: one event per changed button bit.
const changed = report.buttons ^ previous_buttons;
inline for (.{
.{ hid.mouse_button_left, input_protocol.mouse_button_left },
.{ hid.mouse_button_right, input_protocol.mouse_button_right },
.{ hid.mouse_button_middle, input_protocol.mouse_button_middle },
}) |pair| {
if (changed & pair[0] != 0) {
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(if (report.buttons & pair[0] != 0) input_protocol.MouseEventKind.button_down else input_protocol.MouseEventKind.button_up),
.button = pair[1],
.dx = 0,
.dy = 0,
.scroll_x = 0,
.scroll_y = 0,
.buttons = mask,
});
}
}
previous_buttons = report.buttons;
// Relative motion (dy straight through — HID Y is already screen convention).
if (report.dx != 0 or report.dy != 0) {
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(input_protocol.MouseEventKind.motion),
.button = 0,
.dx = report.dx,
.dy = report.dy,
.scroll_x = 0,
.scroll_y = 0,
.buttons = mask,
});
}
// Wheel (4-byte reports only): positive = scroll up.
if (report.has_wheel and report.wheel != 0) {
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(input_protocol.MouseEventKind.scroll),
.button = 0,
.dx = 0,
.dy = 0,
.scroll_x = 0,
.scroll_y = report.wheel,
.buttons = mask,
});
}
}
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
@@ -0,0 +1,73 @@
//! USB Mass Storage Bulk-Only Transport (BOT) wire structures — the Command and
//! Command Status Wrappers that bracket every command (USB MSC BOT §5). Pure data
//! definitions, host-testable in isolation. The command inside the CBW is a SCSI
//! CDB (see scsi.zig); the transport here just carries it and reports status.
//!
//! One command is three bulk transfers: CBW out, an optional data stage, CSW in.
const std = @import("std");
/// "USBC" — the signature at the head of every Command Block Wrapper.
pub const cbw_signature: u32 = 0x43425355;
/// "USBS" — the signature at the head of every Command Status Wrapper.
pub const csw_signature: u32 = 0x53425355;
/// CBW `flags`: set for a device-to-host (IN) data stage, clear for OUT.
pub const flag_data_in: u8 = 0x80;
/// The 31-byte Command Block Wrapper, sent on the bulk-OUT endpoint.
pub const CommandBlockWrapper = extern struct {
signature: u32 align(1) = cbw_signature,
tag: u32 align(1),
data_transfer_length: u32 align(1),
flags: u8,
lun: u8,
cdb_length: u8,
cdb: [16]u8 = [_]u8{0} ** 16,
};
/// A device's answer to a command (the CSW `status` byte).
pub const CommandStatus = enum(u8) {
passed = 0,
failed = 1,
phase_error = 2,
_,
};
/// The 13-byte Command Status Wrapper, read from the bulk-IN endpoint.
pub const CommandStatusWrapper = extern struct {
signature: u32 align(1) = csw_signature,
tag: u32 align(1),
data_residue: u32 align(1),
status: u8,
};
comptime {
std.debug.assert(@sizeOf(CommandBlockWrapper) == 31);
std.debug.assert(@sizeOf(CommandStatusWrapper) == 13);
}
test "wrapper sizes and signatures match the specification" {
const cbw = CommandBlockWrapper{
.tag = 0x11223344,
.data_transfer_length = 512,
.flags = flag_data_in,
.lun = 0,
.cdb_length = 10,
};
const bytes = std.mem.asBytes(&cbw);
try std.testing.expectEqual(@as(usize, 31), bytes.len);
// "USBC" little-endian.
try std.testing.expectEqualSlices(u8, "USBC", bytes[0..4]);
try std.testing.expectEqual(flag_data_in, bytes[12]);
const csw = std.mem.bytesToValue(CommandStatusWrapper, &[_]u8{
0x55, 0x53, 0x42, 0x53, // "USBS"
0x44, 0x33, 0x22, 0x11, // tag
0x00, 0x00, 0x00, 0x00, // residue
0x00, // passed
});
try std.testing.expectEqual(csw_signature, csw.signature);
try std.testing.expectEqual(@as(u32, 0x11223344), csw.tag);
try std.testing.expectEqual(@as(u8, @intFromEnum(CommandStatus.passed)), csw.status);
}
+86
View File
@@ -0,0 +1,86 @@
//! The SCSI command descriptor blocks a transparent-SCSI (subclass 0x06) mass
//! storage device understands, and the parsers for what they return. Pure data —
//! host-testable. These CDBs go inside a Bulk-Only-Transport CBW (see
//! bulk-only-transport.zig).
//!
//! Every multi-byte SCSI field is **big-endian** — the opposite of the USB wire
//! ABI — so the LBA and transfer-length encodings are the load-bearing detail.
const std = @import("std");
// SCSI operation codes.
const op_test_unit_ready: u8 = 0x00;
const op_request_sense: u8 = 0x03;
const op_inquiry: u8 = 0x12;
const op_read_capacity_10: u8 = 0x25;
const op_read_10: u8 = 0x28;
const op_write_10: u8 = 0x2A;
/// INQUIRY: standard device data (36 bytes: peripheral type, removable, vendor
/// and product strings).
pub fn inquiry(allocation_length: u8) [6]u8 {
return .{ op_inquiry, 0, 0, 0, allocation_length, 0 };
}
/// TEST UNIT READY: no data; success (CSW passed) means the unit is ready.
pub fn testUnitReady() [6]u8 {
return .{ op_test_unit_ready, 0, 0, 0, 0, 0 };
}
/// REQUEST SENSE: 18 bytes of sense data (sense key + ASC/ASCQ) explaining the
/// previous failure.
pub fn requestSense(allocation_length: u8) [6]u8 {
return .{ op_request_sense, 0, 0, 0, allocation_length, 0 };
}
/// READ CAPACITY(10): 8 bytes back — the last LBA and the block size, both u32
/// big-endian. Block count is last_lba + 1.
pub fn readCapacity10() [10]u8 {
return .{ op_read_capacity_10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
}
/// READ(10): read `blocks` logical blocks starting at `lba` into the data stage.
pub fn read10(lba: u32, blocks: u16) [10]u8 {
var cdb = [_]u8{0} ** 10;
cdb[0] = op_read_10;
std.mem.writeInt(u32, cdb[2..6], lba, .big);
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
return cdb;
}
/// WRITE(10): write `blocks` logical blocks starting at `lba` from the data stage.
pub fn write10(lba: u32, blocks: u16) [10]u8 {
var cdb = [_]u8{0} ** 10;
cdb[0] = op_write_10;
std.mem.writeInt(u32, cdb[2..6], lba, .big);
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
return cdb;
}
/// Decode an 8-byte READ CAPACITY(10) reply.
pub fn parseCapacity(bytes: [8]u8) struct { last_lba: u32, block_size: u32 } {
return .{
.last_lba = std.mem.readInt(u32, bytes[0..4], .big),
.block_size = std.mem.readInt(u32, bytes[4..8], .big),
};
}
test "read/write CDBs encode the LBA and length big-endian" {
const read = read10(0x01020304, 8);
try std.testing.expectEqualSlices(u8, &.{ 0x28, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00 }, &read);
const write = write10(0xAABBCCDD, 1);
try std.testing.expectEqualSlices(u8, &.{ 0x2A, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x00, 0x01, 0x00 }, &write);
try std.testing.expectEqual(@as(u8, 0x25), readCapacity10()[0]);
try std.testing.expectEqual(@as(u8, 0x12), inquiry(36)[0]);
try std.testing.expectEqual(@as(u8, 36), inquiry(36)[4]);
try std.testing.expectEqual(@as(u8, 0x00), testUnitReady()[0]);
}
test "read capacity parses last LBA and block size" {
// last_lba = 0x0003FFFF (262144 blocks), block_size = 512.
const capacity = parseCapacity(.{ 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0x00 });
try std.testing.expectEqual(@as(u32, 0x0003FFFF), capacity.last_lba);
try std.testing.expectEqual(@as(u32, 512), capacity.block_size);
}
+179
View File
@@ -0,0 +1,179 @@
//! USB mass-storage class driver (Bulk-Only Transport + transparent SCSI).
//!
//! Spawned by the device manager when the xHCI bus driver reports a mass-storage
//! / SCSI / bulk-only interface (class 8, subclass 6, protocol 0x50); its device
//! id arrives as argv[1]. It owns no hardware: it opens its device through the
//! USB transfer protocol (`runtime.usb`), then drives it with the BOT command
//! cycle — CBW out, an optional data stage, CSW in — carrying SCSI commands
//! (READ CAPACITY, READ(10), WRITE(10)). Upward it is a block device: it serves
//! the block protocol under `.block`, the storage a FAT filesystem sits on.
//!
//! Block data never crosses IPC: read/write name a caller-owned DMA buffer by
//! physical address, which the data stage DMAs straight to/from.
const std = @import("std");
const runtime = @import("runtime");
const scsi = @import("scsi.zig");
const bot = @import("bulk-only-transport.zig");
const block_protocol = @import("block-protocol");
const dma = runtime.dma;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
var device_id: u64 = 0;
var device: runtime.usb.Device = undefined;
var bulk_in: runtime.usb.Endpoint = undefined;
var bulk_out: runtime.usb.Endpoint = undefined;
// DMA buffers for the transport: the 31-byte CBW, the 13-byte CSW, and a page
// for the small command data (INQUIRY / READ CAPACITY / the self-check sector).
var command_wrapper: dma.Region = undefined;
var status_wrapper: dma.Region = undefined;
var command_data: dma.Region = undefined;
var next_tag: u32 = 1;
var block_size: u32 = 512;
var block_count: u64 = 0;
/// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from
/// `data_physical`), read and validate the CSW. Returns true on a passed status.
fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool {
const tag = next_tag;
next_tag +%= 1;
const wrapper: *bot.CommandBlockWrapper = @ptrFromInt(command_wrapper.virtual);
wrapper.* = .{
.tag = tag,
.data_transfer_length = data_length,
.flags = if (direction_in) bot.flag_data_in else 0,
.lun = 0,
.cdb_length = @intCast(cdb.len),
};
@memcpy(wrapper.cdb[0..cdb.len], cdb);
if (device.bulk(bulk_out.address, command_wrapper.physical, @sizeOf(bot.CommandBlockWrapper)) == null) return false;
if (data_length > 0) {
const endpoint = if (direction_in) bulk_in.address else bulk_out.address;
if (device.bulk(endpoint, data_physical, data_length) == null) return false;
}
if (device.bulk(bulk_in.address, status_wrapper.physical, @sizeOf(bot.CommandStatusWrapper)) == null) return false;
const status: *const bot.CommandStatusWrapper = @ptrFromInt(status_wrapper.virtual);
if (status.signature != bot.csw_signature or status.tag != tag) return false;
return status.status == @intFromEnum(bot.CommandStatus.passed);
}
fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = endpoint;
if (!runtime.usb.helloManager(device_id)) {
_ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n");
return false;
}
device = runtime.usb.open(device_id) orelse {
writeLine("/system/drivers/usb-storage: could not open device {d}\n", .{device_id});
return false;
};
bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse {
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-IN endpoint\n");
return false;
};
bulk_out = device.findEndpoint(runtime.usb.transfer_type_bulk, false) orelse {
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-OUT endpoint\n");
return false;
};
command_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
status_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
command_data = dma.alloc(4096, dma.coherent) orelse return false;
// Bring the LUN up: wait for it to be ready (clearing the initial unit-attention
// with REQUEST SENSE), identify it, and read its capacity.
var tries: u32 = 0;
while (tries < 10) : (tries += 1) {
const ready = scsi.testUnitReady();
if (transact(&ready, false, 0, 0)) break;
const sense = scsi.requestSense(18);
_ = transact(&sense, true, command_data.physical, 18);
runtime.system.sleep(50);
}
const inquiry = scsi.inquiry(36);
_ = transact(&inquiry, true, command_data.physical, 36);
const capacity_command = scsi.readCapacity10();
if (!transact(&capacity_command, true, command_data.physical, 8)) {
_ = runtime.system.write("/system/drivers/usb-storage: READ CAPACITY failed\n");
return false;
}
var capacity_bytes: [8]u8 = undefined;
const capacity_source: [*]const u8 = @ptrFromInt(command_data.virtual);
@memcpy(&capacity_bytes, capacity_source[0..8]);
const capacity = scsi.parseCapacity(capacity_bytes);
block_size = capacity.block_size;
block_count = @as(u64, capacity.last_lba) + 1;
writeLine("/system/drivers/usb-storage: ready ({d} blocks x {d} bytes)\n", .{ block_count, block_size });
// Self-check: read block 0 and log its trailing signature (0x55AA for a boot
// sector) — proof READ(10) works end to end over the bulk path.
const read0 = scsi.read10(0, 1);
if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) {
const sector: [*]const u8 = @ptrFromInt(command_data.virtual);
writeLine("/system/drivers/usb-storage: block 0 signature 0x{x:0>2}{x:0>2}\n", .{ sector[510], sector[511] });
}
return true;
}
/// Serve the block protocol: geometry, and whole-block read/write to/from the
/// caller's DMA buffer (named by physical address).
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = sender;
_ = capability;
if (message.len < block_protocol.request_size) return 0;
const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
switch (request.operation) {
@intFromEnum(block_protocol.Operation.geometry) => {
return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
},
@intFromEnum(block_protocol.Operation.read) => {
const count: u16 = @intCast(request.count);
const cdb = scsi.read10(@intCast(request.lba), count);
const ok = transact(&cdb, true, request.physical, request.count * block_size);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
},
@intFromEnum(block_protocol.Operation.write) => {
const count: u16 = @intCast(request.count);
const cdb = scsi.write10(@intCast(request.lba), count);
const ok = transact(&cdb, false, request.physical, request.count * block_size);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
},
else => return 0,
}
}
fn writeReply(reply: []u8, value: block_protocol.Reply) usize {
const bytes = std.mem.asBytes(&value);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
}
pub fn main(init: runtime.process.Init) void {
const argument = init.arguments.get(1) orelse {
_ = runtime.system.write("/system/drivers/usb-storage: missing device id (argv[1])\n");
return;
};
device_id = std.fmt.parseInt(u64, argument, 10) catch {
writeLine("/system/drivers/usb-storage: malformed device id '{s}'\n", .{argument});
return;
};
runtime.service.run(block_protocol.message_maximum, .{
.service = .block,
.init = initialise,
.on_message = onMessage,
});
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
@@ -0,0 +1,159 @@
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
//! mass-storage driver) says to the xHCI bus driver over its well-known
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
//!
//! The shape:
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
//! its own endpoint (for asynchronous interrupt reports) and its assigned
//! device id, and receives a `device_token` plus its interface's endpoints.
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
//! it completes. Control data travels inline (descriptors, HID/MSC class
//! requests are all small); bulk data travels by **physical address** — the
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
//! to cross the 256-byte IPC boundary.
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
//! interrupt endpoint; each report the device produces is then pushed to the
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
//! exactly how the input service delivers events.
//!
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
//! A multi-controller machine would need a per-controller endpoint (the device
//! manager handing each class driver the right one); noted, not built.
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
pub const message_maximum: usize = 256;
/// The largest inline control-transfer payload. Sized so a whole message
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
/// requests are all far smaller.
pub const max_inline_data: usize = 200;
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
/// 8 bytes, boot mouse reports 3–4.
pub const max_report_data: usize = 48;
/// Endpoints per interface reported back in an open reply (a boot HID interface
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
pub const max_reported_endpoints: usize = 4;
pub const Operation = enum(u32) {
open = 0,
control = 1,
interrupt_subscribe = 2,
bulk = 3,
};
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
/// the bus driver already parsed during enumeration.
pub const Endpoint = extern struct {
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
address: u8,
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
transfer_type: u8,
max_packet_size: u16,
interval: u8,
reserved: [3]u8 = .{ 0, 0, 0 },
};
/// open: the class driver's receive endpoint rides as the call's capability, and
/// `device_id` is the interface's assigned id (its argv[1]).
pub const OpenRequest = extern struct {
operation: u32 = @intFromEnum(Operation.open),
reserved: u32 = 0,
device_id: u64,
};
/// The answer to open: a token scoping every later request to this device, the
/// interface's class triple (a sanity check), and its endpoints.
pub const OpenReply = extern struct {
status: i32,
endpoint_count: u32,
device_token: u64,
interface_class: u8,
interface_subclass: u8,
interface_protocol: u8,
interface_number: u8,
reserved2: u32 = 0,
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
};
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
/// reply carries up to `data_length` bytes back.
pub const ControlRequest = extern struct {
operation: u32 = @intFromEnum(Operation.control),
reserved: u32 = 0,
device_token: u64,
setup: [8]u8,
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
reserved2: u8 = 0,
data_length: u16,
reserved3: u32 = 0,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
pub const ControlReply = extern struct {
status: i32, // 0 success, negative on failure/stall
actual_length: u32,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
/// report the device returns is pushed to the caller's endpoint (handed over at
/// open) as an asynchronous `InterruptReport`.
pub const InterruptSubscribeRequest = extern struct {
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
reserved: u32 = 0,
device_token: u64,
endpoint_address: u8,
reserved2: u8 = 0,
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
};
pub const InterruptSubscribeReply = extern struct {
status: i32,
reserved: u32 = 0,
};
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
pub const BulkRequest = extern struct {
operation: u32 = @intFromEnum(Operation.bulk),
reserved: u32 = 0,
device_token: u64,
physical_address: u64,
length: u32,
endpoint_address: u8,
reserved2: u8 = 0,
reserved3: u16 = 0,
};
pub const BulkReply = extern struct {
status: i32,
actual_length: u32,
};
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
pub const InterruptReport = extern struct {
device_token: u64,
endpoint_address: u8,
length: u8,
reserved: u16 = 0,
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
};
comptime {
const std = @import("std");
// Every synchronous message must fit one IPC message; the async report must
// fit an ipc_send payload slot.
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
std.debug.assert(@sizeOf(InterruptReport) <= 64);
}
+268 -35
View File
@@ -17,6 +17,52 @@ const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol; const protocol = runtime.device_manager_protocol;
const device = runtime.device; const device = runtime.device;
const usb_ids = @import("usb-ids");
const usb_abi = @import("usb-abi");
const transfer = @import("usb-transfer-protocol");
const library = @import("usb-xhci-library.zig");
/// The controller engine (reset, rings, transfers), stood up in `initialise`.
var controller: ?library.Controller = null;
/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
/// requests, signals, and the interrupt-poll timer all arrive.
var service_endpoint: runtime.ipc.Handle = 0;
/// How often the driver drains the event ring for interrupt reports (~125 Hz),
/// re-armed each tick. Frequent enough for responsive input.
const poll_interval_ms: u64 = 8;
/// The class driver endpoints that opened each device, so interrupt reports can
/// be pushed back to them. Keyed by the device token (the interface's device id).
const Open = struct {
used: bool = false,
device_token: u64 = 0,
report_endpoint: usize = 0,
};
var opens = [_]Open{.{}} ** 16;
fn recordOpen(device_token: u64, report_endpoint: usize) void {
for (&opens) |*open| {
if (open.used and open.device_token == device_token) {
open.report_endpoint = report_endpoint;
return;
}
}
for (&opens) |*open| {
if (!open.used) {
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
return;
}
}
}
fn reportEndpointFor(device_token: u64) ?usize {
for (&opens) |*open| {
if (open.used and open.device_token == device_token) return open.report_endpoint;
}
return null;
}
/// Format one whole log line and emit it in a single `debug_write`, so /// Format one whole log line and emit it in a single `debug_write`, so
/// concurrent instances (one per controller) can never interleave mid-line. /// concurrent instances (one per controller) can never interleave mid-line.
@@ -31,7 +77,7 @@ var controller_id: u64 = protocol.no_device;
/// manager. Any failure returns false: the process exits cleanly, which the /// manager. Any failure returns false: the process exits cleanly, which the
/// manager reads as "meant to stop" — a missing assignment is not a crash loop. /// manager reads as "meant to stop" — a missing assignment is not a crash loop.
fn initialise(endpoint: runtime.ipc.Handle) bool { fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = endpoint; service_endpoint = endpoint;
if (!device.claim(controller_id)) { if (!device.claim(controller_id)) {
writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id}); writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id});
return false; return false;
@@ -72,6 +118,26 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
return false; return false;
}; };
// Bring the controller up: reset it, stand up the command and event rings,
// and start it running (the hardware half lives in usb-xhci-library.zig).
controller = library.Controller.init(register_base) orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
return false;
};
writeLine("/system/drivers/usb-xhci-bus: controller running ({d} slots, {d}-byte contexts)\n", .{
controller.?.max_slots,
controller.?.context_size,
});
// The proof of life: a No-Op command round-trips the command ring, the event
// ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the
// engine is sound; transfers build on exactly this machinery.
if (controller.?.noOpCommand()) {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
} else {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n");
return false;
}
// The handshake: role, protocol version, assignment — inside the manager's // The handshake: role, protocol version, assignment — inside the manager's
// deadline (the lookup retries cover the manager still registering). // deadline (the lookup retries cover the manager still registering).
var manager: ?runtime.ipc.Handle = null; var manager: ?runtime.ipc.Handle = null;
@@ -97,17 +163,15 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n"); _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");
scanPorts(h); scanPorts(h);
// Arm the poll timer that drains interrupt reports from the event ring. It is
// re-armed on each tick in onNotification; class drivers subscribe later.
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
return true; return true;
} }
var register_base: usize = 0; var register_base: usize = 0;
/// One 32-bit volatile register read at `offset` from the mapped window.
fn readRegister(offset: usize) u32 {
const register: *volatile u32 = @ptrFromInt(register_base + offset);
return register.*;
}
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10) /// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on /// decoded to human names — the boot-log breadcrumb for what actually enumerated on
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine /// a port, the USB analog of the pci-bus class-code line. A controller may redefine
@@ -124,50 +188,217 @@ fn speedName(speed: u32) []const u8 {
}; };
} }
/// The root-hub port scan: read the capability registers for the port count /// The root-hub scan and enumeration: for each connected port, bring the device
/// and the operational-register offset, then one PORTSC per port. The connect /// up (reset → enable slot → address), read its descriptors, and register +
/// bit (CCS) and the speed field reflect hardware state directly — no /// report one child per interface — carrying the interface's (class, subclass,
/// controller reset or run needed to *see* the devices; driving them needs the /// protocol) triple as identity, which is what the device manager matches a
/// rings (the USB track). /// class driver against.
fn scanPorts(manager: runtime.ipc.Handle) void { fn scanPorts(manager: runtime.ipc.Handle) void {
// Capability registers: CAPLENGTH is byte 0 of the first dword; HCSPARAMS1 const engine = if (controller) |*c| c else {
// carries MaxPorts in bits 31:24. _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
const capability_length = readRegister(0) & 0xFF; return;
const structural = readRegister(0x04); };
const maximum_ports: u32 = structural >> 24; writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{engine.max_ports});
writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{maximum_ports});
// PORTSC registers: operational base + 0x400 + 0x10 per port (1-based).
var port: u32 = 1; var port: u32 = 1;
var connected: u32 = 0; var connected: u32 = 0;
while (port <= maximum_ports) : (port += 1) { while (port <= engine.max_ports) : (port += 1) {
const port_status = readRegister(capability_length + 0x400 + 0x10 * (port - 1)); const port_status = engine.portStatus(port);
if (port_status & 1 == 0) continue; // CCS: nothing connected if (port_status & 1 == 0) continue; // CCS: nothing connected
connected += 1; connected += 1;
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed }); writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed });
const report = protocol.ChildAdded{ const usb_device = engine.setupDevice(port, speed) orelse {
.parent = controller_id, writeLine("/system/drivers/usb-xhci-bus: port {d} device setup failed\n", .{port});
.bus_address = port,
.identity = speed,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
writeLine("/system/drivers/usb-xhci-bus: child report for port {d} failed\n", .{port});
continue; continue;
}; };
if (!engine.enumerate(usb_device)) {
writeLine("/system/drivers/usb-xhci-bus: port {d} enumeration failed\n", .{port});
continue;
}
writeLine("/system/drivers/usb-xhci-bus: port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)\n", .{
port,
usb_device.device_descriptor.vendor_id,
usb_device.device_descriptor.product_id,
usb_device.interface_count,
});
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
// Record the id each interface was registered as, so a class driver
// opening the interface (by that id) resolves to it.
if (reportInterface(manager, port, interface.*)) |registered| {
interface.registered_device_id = registered;
}
}
} }
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n"); if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
} }
/// No bus protocol to serve yet — transfer requests arrive with the USB track. /// Register one interface as a resource-less child of the controller and report
/// it to the device manager. The identity is the packed USB class triple, so the
/// manager can match a class driver (HID keyboard, mouse, mass storage); the
/// registered device id becomes that driver's argv[1] assignment. Returns the
/// registered device id, or null if registration or the report failed.
fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 {
const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol);
// A USB device is reached through its controller, not by MMIO, so the child
// carries no resources; register() allows that. Its bus-local identity — the
// (port, interface) address, written as a short "P<port>I<interface>" tag in
// the hid field — makes each interface a distinct kernel node (the register
// dedup keys on class/pci_class/hid/resources, all otherwise identical here)
// and keeps re-registration idempotent across a bus restart: the same port
// and interface always map back to the same device id.
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
descriptor.class = @intFromEnum(device.DeviceClass.usb_device);
descriptor.pci_class = device.no_pci_class;
descriptor.resource_count = 0;
var hid_buffer: [8]u8 = undefined;
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
descriptor.hid_len = hid_text.len;
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
const registered = device.register(controller_id, &descriptor) orelse {
writeLine("/system/drivers/usb-xhci-bus: register refused for port {d} interface {d}\n", .{ port, interface.number });
return null;
};
const report = protocol.ChildAdded{
.parent = controller_id,
.bus_address = (@as(u64, port) << 8) | interface.number,
.identity = identity,
.device_id = registered,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
writeLine("/system/drivers/usb-xhci-bus: child report for port {d} interface {d} failed\n", .{ port, interface.number });
return null;
};
writeLine("/system/drivers/usb-xhci-bus: port {d} interface {d} class {d}/{d}/{d} registered as device {d}\n", .{
port,
interface.number,
interface.class,
interface.subclass,
interface.protocol,
registered,
});
return registered;
}
/// Serve the USB transfer protocol: a class driver opens its device, then issues
/// control / interrupt-subscribe / bulk requests against it.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = message;
_ = reply;
_ = sender; _ = sender;
_ = capability; if (message.len < 4) return 0;
return 0; const operation = std.mem.readInt(u32, message[0..4], .little);
return switch (operation) {
@intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability),
@intFromEnum(transfer.Operation.control) => handleControl(message, reply),
@intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
@intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply),
else => 0,
};
}
fn writeReply(reply: []u8, value: anytype) usize {
const bytes = std.mem.asBytes(&value);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
}
/// open: resolve the assigned device id to an interface, remember the caller's
/// endpoint (for interrupt reports), and answer with a device token + the
/// interface's endpoints so the class driver need not re-read the config.
fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize {
if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
if (capability) |endpoint| recordOpen(request.device_id, endpoint);
var open_reply = transfer.OpenReply{
.status = 0,
.endpoint_count = found.interface.endpoint_count,
.device_token = request.device_id,
.interface_class = found.interface.class,
.interface_subclass = found.interface.subclass,
.interface_protocol = found.interface.protocol,
.interface_number = found.interface.number,
};
const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints);
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
open_reply.endpoints[index] = .{
.address = endpoint.address,
.transfer_type = endpoint.transfer_type,
.max_packet_size = endpoint.max_packet_size,
.interval = endpoint.interval,
};
}
return writeReply(reply, open_reply);
}
/// control: one EP0 control transfer, small data inline both ways.
fn handleControl(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup);
const direction_in = request.direction_in != 0;
const data_length = @min(request.data_length, transfer.max_inline_data);
var data: [transfer.max_inline_data]u8 = undefined;
if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]);
const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in);
var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
return writeReply(reply, control_reply);
}
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
fn handleSubscribe(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
}
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
/// address), so sector-sized data never crosses IPC.
fn handleBulk(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
}
/// The poll timer landed: drain any interrupt reports off the event ring and push
/// each to the class driver that subscribed, then re-arm the timer.
fn onNotification(badge: u64) void {
if (badge & runtime.ipc.notify_timer_bit == 0) return;
if (controller) |*engine| {
engine.pump();
while (engine.takeReport()) |report| {
var message = transfer.InterruptReport{
.device_token = report.device_token,
.endpoint_address = report.endpoint_address,
.length = @intCast(@min(report.length, transfer.max_report_data)),
};
const n = @min(report.length, transfer.max_report_data);
@memcpy(message.data[0..n], report.data[0..n]);
_ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message));
}
}
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
} }
pub fn main(init: runtime.process.Init) void { pub fn main(init: runtime.process.Init) void {
@@ -179,9 +410,11 @@ pub fn main(init: runtime.process.Init) void {
writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument}); writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument});
return; return;
}; };
runtime.service.run(protocol.message_maximum, .{ runtime.service.run(transfer.message_maximum, .{
.service = .usb_bus,
.init = initialise, .init = initialise,
.on_message = onMessage, .on_message = onMessage,
.on_notification = onNotification,
}); });
} }
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -37,7 +37,9 @@ const Task = scheduler.Task;
pub const MESSAGE_MAXIMUM: usize = 256; pub const MESSAGE_MAXIMUM: usize = 256;
pub const maximum_handles = scheduler.ipc_maximum_handles; pub const maximum_handles = scheduler.ipc_maximum_handles;
pub const maximum_services = 8; // The name registry is indexed directly by ServiceId, so this must exceed the
// largest id (currently fat = 8). Sized with headroom for new services.
pub const maximum_services = 16;
/// Errno-style failures, returned as `-value` in the system_call result register. /// Errno-style failures, returned as `-value` in the system_call result register.
pub const EBADF: i64 = 1; // bad handle pub const EBADF: i64 = 1; // bad handle
+66
View File
@@ -144,6 +144,12 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
driverRestartTest(boot_information); driverRestartTest(boot_information);
} else if (eql(case, "usb-report")) { } else if (eql(case, "usb-report")) {
usbReportTest(boot_information); usbReportTest(boot_information);
} else if (eql(case, "usb-hid")) {
usbHidTest(boot_information);
} else if (eql(case, "usb-storage")) {
usbStorageTest(boot_information);
} else if (eql(case, "fat-mount")) {
fatMountTest(boot_information);
} else if (eql(case, "device-list")) { } else if (eql(case, "device-list")) {
deviceListTest(boot_information); deviceListTest(boot_information);
} else if (eql(case, "pci-scan")) { } else if (eql(case, "pci-scan")) {
@@ -1968,6 +1974,66 @@ fn pciScanTest(boot_information: *const BootInformation) void {
/// the acpi service publishes it; init runs the stop sequence over its children /// the acpi service publishes it; init runs the stop sequence over its children
/// and asks the power service for S5; the machine powers off (QEMU exits). The /// and asks the power service for S5; the machine powers off (QEMU exits). The
/// kernel test only spawns init — the ordered chain is the harness assertion. /// kernel test only spawns init — the ordered chain is the harness assertion.
/// The USB HID chain, end to end: boot the full service tree (init spawns vfs,
/// input, device-manager), and let discovery run — the manager matches the PCI
/// host bridge to pci-bus, pci-bus reports the xHCI controller, usb-xhci-bus
/// enumerates the HID interfaces, and the manager spawns the class drivers. The
/// harness's expect regex requires usb-xhci-bus to register the boot-keyboard
/// interface, the manager to spawn usb-hid-keyboard, and that driver to come up
/// (open its device, ask for boot protocol, subscribe) — proof the transfer
/// protocol works class-driver to controller.
fn usbHidTest(boot_information: *const BootInformation) void {
bootServiceTreeTest(boot_information, "usb-hid");
}
/// The USB storage chain: same full-tree boot, but the harness attaches a
/// usb-storage device and the expect regex requires usb-storage to come up
/// (open its device, run the BOT bring-up, read its capacity, and read block 0).
fn usbStorageTest(boot_information: *const BootInformation) void {
bootServiceTreeTest(boot_information, "usb-storage");
}
/// The FAT mount chain: boot the full tree (init spawns the fat server, which
/// brings up the USB storage chain, mounts the FAT volume, and mounts itself into
/// the VFS at /mnt/usb), then spawn a fat-test client that lists and reads through
/// the mount. The harness attaches a usb-storage device; the expect regex requires
/// the fat mount and the client's success.
fn fatMountTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: fat-mount\n", .{});
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over init and the initial_ramdisk", false);
result();
return;
}
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(ramdisk);
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const init_ok = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
check("init spawned (boots the tree, incl. the fat server)", init_ok);
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
result();
}
fn bootServiceTreeTest(boot_information: *const BootInformation, comptime label: []const u8) void {
log("DANOS-TEST-BEGIN: " ++ label ++ "\n", .{});
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over init and the initial_ramdisk", false);
result();
return;
}
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
process.setInitialRamdisk(ramdisk);
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
check("init spawned (boots vfs, input, device-manager, and the USB chain)", spawned);
result();
}
fn orderlyShutdownTest(boot_information: *const BootInformation) void { fn orderlyShutdownTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{}); log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{});
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
+6 -3
View File
@@ -17,10 +17,13 @@ pub const maximum_cpus = 128;
/// Maximum tasks (kernel threads) alive at once — the static task-table size. Each /// Maximum tasks (kernel threads) alive at once — the static task-table size. Each
/// online core consumes one slot for its idle task, plus task 0 on the BSP. Sized /// online core consumes one slot for its idle task, plus task 0 on the BSP. Sized
/// for the initial-ramdisk sweep (15 bundled binaries spawned at once) plus the /// for the initial-ramdisk sweep (the bundled binaries spawned at once) plus the
/// device manager's supervised children with room to grow — at 16 the sweep /// device manager's supervised children with room to grow — at 16 the sweep
/// started failing spawns once the bundle passed a dozen binaries. /// started failing spawns once the bundle passed a dozen binaries. Raised to 48
pub const maximum_tasks = 32; /// for the USB stack: the xHCI bus driver spawns a supervised class-driver instance
/// per matched interface (keyboard, mouse, mass storage), on top of the FAT and
/// block servers and the growing ramdisk bundle.
pub const maximum_tasks = 48;
/// Each task's kernel stack (also each AP's bring-up stack), in bytes. /// Each task's kernel stack (also each AP's bring-up stack), in bytes.
pub const kernel_stack_size = 16 * 1024; pub const kernel_stack_size = 16 * 1024;
+40
View File
@@ -0,0 +1,40 @@
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol
//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an
//! `Operation` tag, everything in one IPC message.
//!
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
//! buffer**, named by its physical address — the same physical-address handoff
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
//! sector never has to cross the 256-byte IPC boundary; only the small request /
//! reply headers do. (Safe while the IOMMU is unenforced; see docs/driver-model.md.)
pub const Operation = enum(u32) {
/// geometry() -> { block_size, block_count }
geometry = 0,
/// read(lba, count, physical): read `count` blocks from `lba` into the buffer
read = 1,
/// write(lba, count, physical): write `count` blocks at `lba` from the buffer
write = 2,
};
pub const Request = extern struct {
operation: u32,
reserved: u32 = 0,
lba: u64,
count: u32, // number of blocks (read/write)
reserved2: u32 = 0,
physical: u64, // caller's DMA buffer physical address (read/write)
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
block_size: u32, // geometry: bytes per block (512)
reserved2: u32 = 0,
block_count: u64, // geometry: total blocks; read/write: blocks moved
};
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
@@ -19,6 +19,7 @@ const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const acpi_ids = @import("acpi-ids"); const acpi_ids = @import("acpi-ids");
const pci_class = @import("pci-class"); const pci_class = @import("pci-class");
const usb_ids = @import("usb-ids");
const protocol = runtime.device_manager_protocol; const protocol = runtime.device_manager_protocol;
const device = runtime.device; const device = runtime.device;
const system = runtime.system; const system = runtime.system;
@@ -61,6 +62,36 @@ fn hidDriverFor(hid: []const u8) ?[]const u8 {
return null; return null;
} }
/// The driver that serves a *reported* USB interface by its (class, subclass,
/// protocol) triple — the third bus after PCI and ACPI (docs/device-manager.md:
/// matching stays code until the third bus). The xHCI bus driver reports each
/// interface with this packed triple as its identity; the matched class driver is
/// spawned with the interface's registered id as argv[1], which it presents to the
/// bus driver to open the device.
fn usbDriverForIdentity(identity: u64) ?[]const u8 {
const keyboard = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.hid),
@intFromEnum(usb_ids.hid.SubClass.boot),
@intFromEnum(usb_ids.hid.Protocol.keyboard),
);
const mouse = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.hid),
@intFromEnum(usb_ids.hid.SubClass.boot),
@intFromEnum(usb_ids.hid.Protocol.mouse),
);
const storage = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.mass_storage),
@intFromEnum(usb_ids.mass_storage.SubClass.scsi),
@intFromEnum(usb_ids.mass_storage.Protocol.bulk_only),
);
return switch (identity) {
keyboard => "usb-hid-keyboard",
mouse => "usb-hid-mouse",
storage => "usb-storage",
else => null,
};
}
/// Whether some driver entry already serves registered device `device_id` — /// Whether some driver entry already serves registered device `device_id` —
/// a re-report after a bus restart must not spawn a second instance. /// a re-report after a bus restart must not spawn a second instance.
fn driverForDevice(device_id: u64) bool { fn driverForDevice(device_id: u64) bool {
@@ -411,6 +442,11 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
if (pciDriverForIdentity(report.identity)) |child_driver| { if (pciDriverForIdentity(report.identity)) |child_driver| {
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true); if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true);
} }
// USB interface match: the reported identity is the packed class triple,
// and the class driver is spawned with the interface's registered id.
if (usbDriverForIdentity(report.identity)) |usb_driver| {
if (!driverForDevice(report.device_id)) addDriver(usb_driver, report.device_id, true);
}
// ACPI _HID match (M20.3): ps2-bus is a singleton that finds its own // ACPI _HID match (M20.3): ps2-bus is a singleton that finds its own
// devices by hid, so spawn it once, without a device assignment. // devices by hid, so spawn it once, without a device assignment.
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len; const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
+707
View File
@@ -0,0 +1,707 @@
//! The FAT filesystem engine: mount a block device, walk the FAT and directory
//! structures, and read / write / create files. FAT12/16/32 (the type is
//! detected from the cluster count). Pure logic over a `BlockDevice` interface —
//! no IPC — so it is host-testable against a RAM-backed image (see the tests at
//! the bottom). The fat.zig server wraps a real `.block` device in a BlockDevice
//! and serves this over the VFS protocol.
//!
//! Everything works in 512-byte sectors; a cluster is N sectors. Names are
//! matched case-insensitively against both the 8.3 short name and, when present,
//! the reconstructed long name. Writes update the directory entry, every FAT
//! copy, and (FAT32) the FSInfo hint, in the crash-safe order data -> FAT ->
//! directory. Long-name *creation* is not implemented — new files get an 8.3
//! name (the common case; the plan flags LFN-write as optional).
const std = @import("std");
const on_disk = @import("on-disk.zig");
/// A block device the engine reads and writes in fixed-size blocks. The two
/// function pointers let the same engine run over a real `.block` driver or a
/// RAM buffer (the tests).
pub const BlockDevice = struct {
context: *anyopaque,
block_size: u32,
block_count: u64,
readBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []u8) bool,
writeBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []const u8) bool,
pub fn readBlock(self: BlockDevice, lba: u64, buffer: []u8) bool {
return self.readBlockFn(self.context, lba, buffer);
}
pub fn writeBlock(self: BlockDevice, lba: u64, buffer: []const u8) bool {
return self.writeBlockFn(self.context, lba, buffer);
}
};
/// A resolved filesystem object: a file or directory, and where its 8.3 entry
/// lives so writes can update its size and first cluster.
pub const Node = struct {
first_cluster: u32,
size: u32,
is_directory: bool,
// The absolute sector and byte offset of this node's 8.3 directory entry, so
// size/first-cluster changes can be written back. Absent for the root.
entry_sector: u64 = 0,
entry_offset: u32 = 0,
has_entry: bool = false,
};
const sector_size = 512;
const entries_per_sector = sector_size / @sizeOf(on_disk.DirectoryEntry); // 16
pub const FileSystem = struct {
device: BlockDevice,
geometry: on_disk.Geometry,
// The absolute LBA the filesystem starts at: 0 for a bare FAT ("superfloppy"),
// or the first partition's start LBA when the disk carries an MBR. Every
// filesystem-relative sector read/write adds this.
base_lba: u64 = 0,
// Distinct scratch sectors so nested reads (a FAT lookup during a directory
// scan) never alias each other.
sector: [sector_size]u8 = undefined,
fat_sector: [sector_size]u8 = undefined,
dir_sector: [sector_size]u8 = undefined,
// Every filesystem-relative sector access adds the partition base.
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
return self.device.readBlock(self.base_lba + lba, buffer);
}
fn blockWrite(self: *FileSystem, lba: u64, buffer: []const u8) bool {
return self.device.writeBlock(self.base_lba + lba, buffer);
}
/// Mount the filesystem on `device`: either a bare FAT with its boot sector at
/// LBA 0, or (as QEMU's VVFAT and most real USB sticks present it) an MBR-
/// partitioned disk whose first FAT partition holds the boot sector. Returns
/// null if neither is found.
pub fn mount(device: BlockDevice) ?FileSystem {
var boot: [sector_size]u8 = undefined;
if (!device.readBlock(0, &boot)) return null;
// A bare FAT: a valid boot sector right at LBA 0.
if (on_disk.geometryOf(&boot)) |geometry| {
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = 0 };
}
// Otherwise an MBR: the 0x55AA signature but no BPB. Walk its four
// partition entries (16 bytes each at offset 446) for the first non-empty
// one, and mount the FAT boot sector at that partition's start LBA.
if (boot[510] == 0x55 and boot[511] == 0xAA) {
var partition: usize = 0;
while (partition < 4) : (partition += 1) {
const entry = boot[446 + partition * 16 ..][0..16];
const partition_type = entry[4];
const start_lba = std.mem.readInt(u32, entry[8..12], .little);
if (partition_type == 0 or start_lba == 0) continue;
var partition_boot: [sector_size]u8 = undefined;
if (!device.readBlock(start_lba, &partition_boot)) continue;
if (on_disk.geometryOf(&partition_boot)) |geometry| {
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = start_lba };
}
}
}
return null;
}
// --- cluster <-> sector -------------------------------------------------
fn clusterSector(self: *const FileSystem, cluster: u32, sector_in_cluster: u32) u64 {
return @as(u64, self.geometry.first_data_sector) + @as(u64, cluster - 2) * self.geometry.sectors_per_cluster + sector_in_cluster;
}
fn fatByteBase(self: *const FileSystem) u64 {
return @as(u64, self.geometry.reserved_sector_count) * sector_size;
}
fn rootDirStartSector(self: *const FileSystem) u64 {
return @as(u64, self.geometry.reserved_sector_count) + @as(u64, self.geometry.fat_count) * self.geometry.fat_size_sectors;
}
fn rootDirSectors(self: *const FileSystem) u32 {
return (self.geometry.root_entry_count * 32 + sector_size - 1) / sector_size;
}
// --- FAT access ---------------------------------------------------------
// Read `out.len` bytes from FAT #0 starting at `byte_offset`, spanning sectors.
fn readFatBytes(self: *FileSystem, byte_offset: u64, out: []u8) bool {
var done: usize = 0;
var position = self.fatByteBase() + byte_offset;
while (done < out.len) {
const lba = position / sector_size;
const within: usize = @intCast(position % sector_size);
if (!self.blockRead(lba, &self.fat_sector)) return false;
const n = @min(out.len - done, sector_size - within);
@memcpy(out[done .. done + n], self.fat_sector[within .. within + n]);
done += n;
position += n;
}
return true;
}
// Write `in.len` bytes at `byte_offset` into every FAT copy (read-modify-write
// per sector).
fn writeFatBytes(self: *FileSystem, byte_offset: u64, in: []const u8) bool {
var fat: u32 = 0;
while (fat < self.geometry.fat_count) : (fat += 1) {
const base = self.fatByteBase() + @as(u64, fat) * @as(u64, self.geometry.fat_size_sectors) * sector_size;
var done: usize = 0;
var position = base + byte_offset;
while (done < in.len) {
const lba = position / sector_size;
const within: usize = @intCast(position % sector_size);
if (!self.blockRead(lba, &self.fat_sector)) return false;
const n = @min(in.len - done, sector_size - within);
@memcpy(self.fat_sector[within .. within + n], in[done .. done + n]);
if (!self.blockWrite(lba, &self.fat_sector)) return false;
done += n;
position += n;
}
}
return true;
}
fn readFatEntry(self: *FileSystem, cluster: u32) u32 {
switch (self.geometry.fat_type) {
.fat12 => {
var pair: [2]u8 = undefined;
const offset = cluster + cluster / 2; // cluster * 1.5
if (!self.readFatBytes(offset, &pair)) return on_disk.end_of_chain_12;
const word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
return if (cluster & 1 == 1) (word >> 4) else (word & 0x0FFF);
},
.fat16 => {
var value: [2]u8 = undefined;
if (!self.readFatBytes(@as(u64, cluster) * 2, &value)) return on_disk.end_of_chain_16;
return @as(u16, value[0]) | (@as(u16, value[1]) << 8);
},
.fat32 => {
var value: [4]u8 = undefined;
if (!self.readFatBytes(@as(u64, cluster) * 4, &value)) return on_disk.end_of_chain_32;
return (@as(u32, value[0]) | (@as(u32, value[1]) << 8) | (@as(u32, value[2]) << 16) | (@as(u32, value[3]) << 24)) & 0x0FFFFFFF;
},
}
}
fn writeFatEntry(self: *FileSystem, cluster: u32, value: u32) bool {
switch (self.geometry.fat_type) {
.fat12 => {
const offset = cluster + cluster / 2;
var pair: [2]u8 = undefined;
if (!self.readFatBytes(offset, &pair)) return false;
var word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
if (cluster & 1 == 1) {
word = (word & 0x000F) | (@as(u16, @truncate(value)) << 4);
} else {
word = (word & 0xF000) | (@as(u16, @truncate(value)) & 0x0FFF);
}
pair[0] = @truncate(word);
pair[1] = @truncate(word >> 8);
return self.writeFatBytes(offset, &pair);
},
.fat16 => {
const bytes = [2]u8{ @truncate(value), @truncate(value >> 8) };
return self.writeFatBytes(@as(u64, cluster) * 2, &bytes);
},
.fat32 => {
const bytes = [4]u8{ @truncate(value), @truncate(value >> 8), @truncate(value >> 16), @truncate(value >> 24) };
return self.writeFatBytes(@as(u64, cluster) * 4, &bytes);
},
}
}
fn isEndOfChain(self: *const FileSystem, value: u32) bool {
return switch (self.geometry.fat_type) {
.fat12 => value >= on_disk.end_of_chain_12,
.fat16 => value >= on_disk.end_of_chain_16,
.fat32 => value >= on_disk.end_of_chain_32,
};
}
fn endOfChainValue(self: *const FileSystem) u32 {
return switch (self.geometry.fat_type) {
.fat12 => 0xFFF,
.fat16 => 0xFFFF,
.fat32 => 0x0FFFFFFF,
};
}
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
fn allocateCluster(self: *FileSystem) ?u32 {
var cluster: u32 = 2;
while (cluster < self.geometry.cluster_count + 2) : (cluster += 1) {
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
return cluster;
}
}
return null;
}
// --- directory iteration ------------------------------------------------
// The absolute LBA of the `sector_index`th sector of directory `dir`, or null
// past its end. If `grow` is set and a cluster chain runs out, a new cluster
// is allocated and linked (used when appending a directory entry).
fn dirSectorLba(self: *FileSystem, dir: Node, sector_index: u32, grow: bool) ?u64 {
const is_fixed_root = dir.first_cluster == 0 and self.geometry.fat_type != .fat32;
if (is_fixed_root) {
if (sector_index >= self.rootDirSectors()) return null;
return self.rootDirStartSector() + sector_index;
}
const spc = self.geometry.sectors_per_cluster;
var cluster = if (dir.first_cluster == 0) self.geometry.root_cluster else dir.first_cluster;
var remaining = sector_index;
while (remaining >= spc) : (remaining -= spc) {
var next = self.readFatEntry(cluster);
if (self.isEndOfChain(next) or next < 2) {
if (!grow) return null;
const fresh = self.allocateCluster() orelse return null;
self.zeroCluster(fresh);
if (!self.writeFatEntry(cluster, fresh)) return null;
next = fresh;
}
cluster = next;
}
return self.clusterSector(cluster, remaining);
}
fn zeroCluster(self: *FileSystem, cluster: u32) void {
var zero = [_]u8{0} ** sector_size;
var s: u32 = 0;
while (s < self.geometry.sectors_per_cluster) : (s += 1) {
_ = self.blockWrite(self.clusterSector(cluster, s), &zero);
}
}
pub fn rootNode(self: *const FileSystem) Node {
return .{
.first_cluster = if (self.geometry.fat_type == .fat32) self.geometry.root_cluster else 0,
.size = 0,
.is_directory = true,
.has_entry = false,
};
}
// --- name handling ------------------------------------------------------
// Format a raw 8.3 name ("NAME EXT") into the displayed "NAME.EXT".
fn format83(raw: [11]u8, out: []u8) []const u8 {
var length: usize = 0;
var base_len: usize = 8;
while (base_len > 0 and raw[base_len - 1] == ' ') base_len -= 1;
for (raw[0..base_len]) |c| {
if (length < out.len) {
out[length] = c;
length += 1;
}
}
var ext_len: usize = 3;
while (ext_len > 0 and raw[8 + ext_len - 1] == ' ') ext_len -= 1;
if (ext_len > 0) {
if (length < out.len) {
out[length] = '.';
length += 1;
}
for (raw[8 .. 8 + ext_len]) |c| {
if (length < out.len) {
out[length] = c;
length += 1;
}
}
}
return out[0..length];
}
// Convert a name to a raw 8.3 field (uppercased, space-padded), or null if it
// cannot be represented (too long a base or extension).
fn to83(name: []const u8) ?[11]u8 {
var raw = [_]u8{' '} ** 11;
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
const base = if (dot) |d| name[0..d] else name;
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
if (base.len == 0 or base.len > 8 or ext.len > 3) return null;
for (base, 0..) |c, i| raw[i] = std.ascii.toUpper(c);
for (ext, 0..) |c, i| raw[8 + i] = std.ascii.toUpper(c);
return raw;
}
fn nameMatches(display: []const u8, query: []const u8) bool {
if (display.len != query.len) return false;
for (display, query) |a, b| {
if (std.ascii.toUpper(a) != std.ascii.toUpper(b)) return false;
}
return true;
}
// Pull the 13 UTF-16 code units of one long-name entry into `out` (ASCII only,
// non-ASCII becomes '?'). Returns how many characters (stopping at 0x0000).
fn longNameChars(entry: on_disk.LongNameEntry, out: *[13]u8) usize {
const units = [13]u16{
entry.name1[0], entry.name1[1], entry.name1[2], entry.name1[3], entry.name1[4],
entry.name2[0], entry.name2[1], entry.name2[2], entry.name2[3], entry.name2[4],
entry.name2[5], entry.name3[0], entry.name3[1],
};
var count: usize = 0;
for (units) |unit| {
if (unit == 0x0000 or unit == 0xFFFF) break;
out[count] = if (unit < 0x80) @truncate(unit) else '?';
count += 1;
}
return count;
}
// --- directory search + listing ----------------------------------------
/// Iterate the entries of a directory, calling `visit` with each real (non-LFN,
/// non-free) entry, its reconstructed display name, and where it lives. Stops
/// when `visit` returns true or the directory ends.
fn scanDirectory(
self: *FileSystem,
dir: Node,
context: anytype,
comptime visit: fn (@TypeOf(context), entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool,
) void {
var long_name: [260]u8 = undefined;
var long_len: usize = 0;
var sector_index: u32 = 0;
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
if (!self.blockRead(lba, &self.dir_sector)) return;
var i: u32 = 0;
while (i < entries_per_sector) : (i += 1) {
const offset = i * @sizeOf(on_disk.DirectoryEntry);
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
if (entry.isEnd()) return;
if (entry.name[0] == 0xE5) {
long_len = 0;
continue;
}
if (entry.isLongName()) {
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
const order = lfn.order & 0x1F;
if (order >= 1 and order <= 20) {
var chunk: [13]u8 = undefined;
const n = longNameChars(lfn, &chunk);
const start = (order - 1) * 13;
if (start + n <= long_name.len) {
@memcpy(long_name[start .. start + n], chunk[0..n]);
if (lfn.order & 0x40 != 0) long_len = start + n; // last (first physical) piece sets the length
}
}
continue;
}
if (entry.isVolumeLabel()) {
long_len = 0;
continue;
}
var short: [12]u8 = undefined;
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
if (visit(context, entry, display, lba, offset)) return;
long_len = 0;
}
}
}
const FindResult = struct { found: bool = false, node: Node = undefined };
const FindContext = struct { query: []const u8, result: *FindResult };
fn findVisit(context: *const FindContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
if (!nameMatches(name, context.query)) return false;
context.result.* = .{ .found = true, .node = .{
.first_cluster = entry.firstCluster(),
.size = entry.file_size,
.is_directory = entry.isDirectory(),
.entry_sector = entry_sector,
.entry_offset = entry_offset,
.has_entry = true,
} };
return true;
}
fn findChild(self: *FileSystem, dir: Node, name: []const u8) ?Node {
var result = FindResult{};
var context = FindContext{ .query = name, .result = &result };
self.scanDirectory(dir, &context, findVisit);
return if (result.found) result.node else null;
}
/// Resolve an absolute or "/"-relative path to a node. "/" is the root.
pub fn resolve(self: *FileSystem, path: []const u8) ?Node {
var node = self.rootNode();
var it = std.mem.tokenizeScalar(u8, path, '/');
while (it.next()) |component| {
if (component.len == 0) continue;
if (!node.is_directory) return null;
node = self.findChild(node, component) orelse return null;
}
return node;
}
/// The `cursor`th real entry of a directory (for readdir): its display name,
/// kind, and size. Returns null past the end.
pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0 };
const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false };
fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
_ = entry_sector;
_ = entry_offset;
if (context.index == context.target) {
const n = @min(name.len, context.out.name_buffer.len);
@memcpy(context.out.name_buffer[0..n], name[0..n]);
context.out.name_len = n;
context.out.is_directory = entry.isDirectory();
context.out.size = entry.file_size;
context.done = true;
return true;
}
context.index += 1;
return false;
}
pub fn listEntry(self: *FileSystem, dir: Node, cursor: u32) ?Listing {
var listing = Listing{};
var context = ListContext{ .target = cursor, .out = &listing };
self.scanDirectory(dir, &context, listVisit);
return if (context.done) listing else null;
}
// --- file read / write --------------------------------------------------
// The cluster holding byte `offset` of a chain starting at `first`, walking
// (and optionally growing) the chain. Returns null at end without grow.
fn clusterAt(self: *FileSystem, first: u32, offset: u32, grow: bool) ?u32 {
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
var cluster = first;
var steps = offset / cluster_bytes;
while (steps > 0) : (steps -= 1) {
var next = self.readFatEntry(cluster);
if (self.isEndOfChain(next) or next < 2) {
if (!grow) return null;
const fresh = self.allocateCluster() orelse return null;
if (!self.writeFatEntry(cluster, fresh)) return null;
next = fresh;
}
cluster = next;
}
return cluster;
}
/// Read up to `buffer.len` bytes of a file node starting at `offset`. Returns
/// the number read (0 at or past EOF).
pub fn readFile(self: *FileSystem, node: Node, offset: u32, buffer: []u8) usize {
if (offset >= node.size or node.first_cluster < 2) return 0;
const available = node.size - offset;
const want = @min(buffer.len, available);
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
var produced: usize = 0;
var position = offset;
while (produced < want) {
const cluster = self.clusterAt(node.first_cluster, position, false) orelse break;
const in_cluster = position % cluster_bytes;
const sector_in_cluster = in_cluster / sector_size;
const in_sector = in_cluster % sector_size;
if (!self.blockRead(self.clusterSector(cluster, sector_in_cluster), &self.sector)) break;
const n = @min(want - produced, sector_size - in_sector);
@memcpy(buffer[produced .. produced + n], self.sector[in_sector .. in_sector + n]);
produced += n;
position += @intCast(n);
}
return produced;
}
/// Write `data` to a file node at `offset`, growing it (allocating clusters and
/// updating the directory entry) as needed. Returns the number written.
pub fn writeFile(self: *FileSystem, node: *Node, offset: u32, data: []const u8) usize {
if (data.len == 0) return 0;
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
// Ensure the file has a first cluster.
if (node.first_cluster < 2) {
const fresh = self.allocateCluster() orelse return 0;
self.zeroCluster(fresh);
node.first_cluster = fresh;
}
var consumed: usize = 0;
var position = offset;
while (consumed < data.len) {
const cluster = self.clusterAt(node.first_cluster, position, true) orelse break;
const in_cluster = position % cluster_bytes;
const sector_in_cluster = in_cluster / sector_size;
const in_sector = in_cluster % sector_size;
const lba = self.clusterSector(cluster, sector_in_cluster);
// Read-modify-write the sector for a partial write.
if (!self.blockRead(lba, &self.sector)) break;
const n = @min(data.len - consumed, sector_size - in_sector);
@memcpy(self.sector[in_sector .. in_sector + n], data[consumed .. consumed + n]);
if (!self.blockWrite(lba, &self.sector)) break;
consumed += n;
position += @intCast(n);
}
const new_end = offset + @as(u32, @intCast(consumed));
if (new_end > node.size) node.size = new_end;
self.updateEntry(node.*);
return consumed;
}
// Write a node's size and first cluster back into its 8.3 directory entry.
fn updateEntry(self: *FileSystem, node: Node) void {
if (!node.has_entry) return;
if (!self.blockRead(node.entry_sector, &self.dir_sector)) return;
var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]);
entry.file_size = node.size;
entry.setFirstCluster(node.first_cluster);
@memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
_ = self.blockWrite(node.entry_sector, &self.dir_sector);
}
/// Create an 8.3-named file in directory `dir`. Returns the new (empty) node,
/// or null if the name is not 8.3-representable or no directory slot is free.
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
const raw = to83(name) orelse return null;
// Find a free directory slot (a 0x00 or 0xE5 entry), growing the directory.
var sector_index: u32 = 0;
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
if (!self.blockRead(lba, &self.dir_sector)) return null;
var i: u32 = 0;
while (i < entries_per_sector) : (i += 1) {
const offset = i * @sizeOf(on_disk.DirectoryEntry);
const existing = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
if (existing.isFree()) {
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
entry.name = raw;
entry.attributes = on_disk.attribute_archive;
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
if (!self.blockWrite(lba, &self.dir_sector)) return null;
return .{
.first_cluster = 0,
.size = 0,
.is_directory = false,
.entry_sector = lba,
.entry_offset = offset,
.has_entry = true,
};
}
}
// Only the fixed root can run out (it can't grow); a chain grows above.
if (sector_index > 4096) return null; // runaway guard
}
return null;
}
};
// --- tests: a RAM-backed FAT16 image ----------------------------------------
const RamDisk = struct {
bytes: []u8,
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
const self: *RamDisk = @ptrCast(@alignCast(context));
const start = lba * sector_size;
if (start + sector_size > self.bytes.len) return false;
@memcpy(buffer[0..sector_size], self.bytes[start .. start + sector_size]);
return true;
}
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
const self: *RamDisk = @ptrCast(@alignCast(context));
const start = lba * sector_size;
if (start + sector_size > self.bytes.len) return false;
@memcpy(self.bytes[start .. start + sector_size], buffer[0..sector_size]);
return true;
}
fn device(self: *RamDisk) BlockDevice {
return .{
.context = self,
.block_size = sector_size,
.block_count = self.bytes.len / sector_size,
.readBlockFn = readBlock,
.writeBlockFn = writeBlock,
};
}
};
// Format a minimal FAT16 volume into `bytes`: BPB + boot signature, FATs with the
// two reserved entries, an empty root directory. Enough for the engine to mount
// and operate on.
fn formatFat16(bytes: []u8) void {
@memset(bytes, 0);
const total_sectors: u16 = @intCast(bytes.len / sector_size);
var bpb = std.mem.zeroes(on_disk.BiosParameterBlock);
bpb.jump = .{ 0xEB, 0x3C, 0x90 };
bpb.oem_name = "MSWIN4.1".*;
bpb.bytes_per_sector = sector_size;
bpb.sectors_per_cluster = 1;
bpb.reserved_sector_count = 1;
bpb.fat_count = 2;
bpb.root_entry_count = 512;
bpb.total_sectors_16 = total_sectors;
bpb.media = 0xF8;
bpb.fat_size_16 = 16; // 16 sectors per FAT (covers ~4000 FAT16 entries)
@memcpy(bytes[0..@sizeOf(on_disk.BiosParameterBlock)], std.mem.asBytes(&bpb));
bytes[on_disk.boot_signature_offset] = 0x55;
bytes[on_disk.boot_signature_offset + 1] = 0xAA;
// FAT reserved entries: entry0 = media in low byte + 0xFF, entry1 = EOC.
const fat0 = 1 * sector_size;
bytes[fat0] = 0xF8;
bytes[fat0 + 1] = 0xFF;
bytes[fat0 + 2] = 0xFF;
bytes[fat0 + 3] = 0xFF;
const fat1 = fat0 + 16 * sector_size;
bytes[fat1] = 0xF8;
bytes[fat1 + 1] = 0xFF;
bytes[fat1 + 2] = 0xFF;
bytes[fat1 + 3] = 0xFF;
}
test "mount a formatted FAT16 image" {
const allocator = std.testing.allocator;
const bytes = try allocator.alloc(u8, 5000 * sector_size); // ~2.4 MB
defer allocator.free(bytes);
formatFat16(bytes);
var disk = RamDisk{ .bytes = bytes };
var fs = FileSystem.mount(disk.device()).?;
try std.testing.expectEqual(on_disk.FatType.fat16, fs.geometry.fat_type);
try std.testing.expect(fs.geometry.cluster_count >= 4085);
// An empty root directory lists nothing.
try std.testing.expect(fs.listEntry(fs.rootNode(), 0) == null);
}
test "create, write, read back a file through the engine" {
const allocator = std.testing.allocator;
const bytes = try allocator.alloc(u8, 5000 * sector_size);
defer allocator.free(bytes);
formatFat16(bytes);
var disk = RamDisk{ .bytes = bytes };
var fs = FileSystem.mount(disk.device()).?;
// Create /HELLO.TXT and write a payload larger than one sector (spans clusters).
var node = fs.createFile(fs.rootNode(), "HELLO.TXT").?;
var payload: [1500]u8 = undefined;
for (&payload, 0..) |*b, i| b.* = @truncate(i);
const written = fs.writeFile(&node, 0, &payload);
try std.testing.expectEqual(@as(usize, payload.len), written);
// Re-resolve from the directory (proving the entry was persisted) and read back.
const resolved = fs.resolve("/HELLO.TXT").?;
try std.testing.expectEqual(@as(u32, payload.len), resolved.size);
var readback: [1500]u8 = undefined;
const got = fs.readFile(resolved, 0, &readback);
try std.testing.expectEqual(@as(usize, payload.len), got);
try std.testing.expectEqualSlices(u8, &payload, &readback);
// A mid-file overwrite is visible on re-read.
var patch = [_]u8{0xAB} ** 4;
_ = fs.writeFile(&node, 600, &patch);
const patched = fs.resolve("/HELLO.TXT").?;
_ = fs.readFile(patched, 600, readback[0..4]);
try std.testing.expectEqualSlices(u8, &patch, readback[0..4]);
// The root now lists exactly HELLO.TXT.
const listing = fs.listEntry(fs.rootNode(), 0).?;
try std.testing.expectEqualStrings("HELLO.TXT", listing.name_buffer[0..listing.name_len]);
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
}
+68
View File
@@ -0,0 +1,68 @@
//! system/services/fat/fat-test — a client that proves the FAT mount end to end:
//! it waits for the fat server to mount the USB volume at /mnt/usb, lists the
//! root directory through the VFS (which routes /mnt/usb to the fat backend), and
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
//! kernel test spawns it alongside init.
const std = @import("std");
const runtime = @import("runtime");
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
pub fn main(init: runtime.process.Init) void {
_ = init;
const unistd = @import("posix").unistd;
// Wait for /mnt/usb to be mounted — the fat server races us at boot (it must
// bring up the whole USB storage chain first).
var dir: i32 = -1;
var tries: u32 = 0;
while (dir < 0 and tries < 1400) : (tries += 1) {
dir = unistd.opendir("/mnt/usb");
if (dir < 0) runtime.system.sleep(50);
}
if (dir < 0) {
_ = runtime.system.write("fat-test: /mnt/usb never became available\n");
return;
}
var count: u32 = 0;
var entry: unistd.DirEntry = .{};
while (unistd.readdir(dir, &entry)) {
writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), entry.kind, entry.size });
count += 1;
if (count > 32) break;
}
unistd.closedir(dir);
writeLine("fat-test: listed {d} entries\n", .{count});
// Read a known file off the boot volume through the mount (best effort): the
// kernel image is an ELF, so its first bytes are the ELF magic.
const fd = unistd.open("/mnt/usb/system/kernel", 0);
if (fd >= 0) {
var magic: [4]u8 = undefined;
const n = unistd.read(fd, &magic);
unistd.close(fd);
if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') {
_ = runtime.system.write("fat-test: read /mnt/usb/system/kernel ELF magic ok\n");
} else {
writeLine("fat-test: /mnt/usb/system/kernel read {d} bytes (not ELF magic)\n", .{n});
}
}
if (count > 0) {
while (true) {
_ = runtime.system.write("fat-test: ok\n");
runtime.system.sleep(1000);
}
}
_ = runtime.system.write("fat-test: root listing was empty\n");
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+195
View File
@@ -0,0 +1,195 @@
//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it
//! opens the block device (a USB stick via usb-storage) under `.block`, mounts the
//! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into
//! the VFS at /mnt/usb. From then on the VFS forwards every open/read/write/
//! status/readdir/close under /mnt/usb to this server, which serves the same
//! vfs-protocol as a backend — turning block reads into file reads.
//!
//! The block data path never crosses IPC: a DMA bounce buffer is handed to the
//! block driver by physical address, and the engine copies sectors in and out of
//! it.
const std = @import("std");
const runtime = @import("runtime");
const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig");
const protocol = runtime.vfs_protocol;
const unistd = @import("posix").unistd;
const dma = runtime.dma;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [96]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
const mount_point = "/mnt/usb";
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
// buffer the driver reads/writes by physical address.
const IpcBlock = struct {
device: runtime.block.Device,
bounce: dma.Region,
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(buffer[0..512], source[0..512]);
return true;
}
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(destination[0..512], buffer[0..512]);
return self.device.write(lba, 1, self.bounce.physical);
}
};
var ipc_block: IpcBlock = undefined;
var filesystem: engine.FileSystem = undefined;
// Open handles the VFS holds against this backend: each maps a node id to a
// resolved engine node.
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
var open_nodes = [_]OpenNode{.{}} ** 32;
fn allocOpen() ?usize {
for (&open_nodes, 0..) |*o, i| {
if (!o.used) return i;
}
return null;
}
fn openAt(id: u64) ?*OpenNode {
if (id >= open_nodes.len) return null;
const o = &open_nodes[@intCast(id)];
return if (o.used) o else null;
}
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
const n = @min(payload.len, out.len - protocol.reply_size);
@memcpy(out[protocol.reply_size..][0..n], payload[0..n]);
return protocol.reply_size + n;
}
fn fail(out: []u8) usize {
return writeReply(out, .{ .status = -1 }, &.{});
}
fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = runtime.system.write("/system/services/fat: starting, waiting for a block device\n");
const device = runtime.block.open() orelse {
_ = runtime.system.write("/system/services/fat: no block device (no storage attached)\n");
return false; // clean exit: nothing to serve
};
const geometry = device.geometry() orelse {
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
return false;
};
ipc_block = .{ .device = device, .bounce = dma.alloc(4096, dma.coherent) orelse return false };
const block_device = engine.BlockDevice{
.context = &ipc_block,
.block_size = geometry.block_size,
.block_count = geometry.block_count,
.readBlockFn = IpcBlock.readBlock,
.writeBlockFn = IpcBlock.writeBlock,
};
filesystem = engine.FileSystem.mount(block_device) orelse {
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
return false;
};
writeLine("/system/services/fat: mounted FAT ({s}, {d} clusters, partition lba {d})\n", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
// Mount ourselves into the VFS namespace at /mnt/usb (retry while the VFS
// comes up). From here the VFS routes /mnt/usb/... to this server.
var tries: u32 = 0;
while (tries < 100) : (tries += 1) {
if (unistd.mount(mount_point, endpoint) == 0) {
writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
return true;
}
runtime.system.sleep(50);
}
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
return true; // still serve directly, even if the namespace mount didn't take
}
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
var node = filesystem.resolve(path);
if (node == null and flags & protocol.create != 0) {
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
const parent_path = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/";
const leaf = if (slash) |s| path[s + 1 ..] else path;
const parent = filesystem.resolve(parent_path) orelse return fail(out);
node = filesystem.createFile(parent, leaf);
}
const resolved = node orelse return fail(out);
const index = allocOpen() orelse return fail(out);
open_nodes[index] = .{ .used = true, .node = resolved };
return writeReply(out, .{ .status = 0, .node = index }, &.{});
}
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = capability;
_ = sender;
if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
switch (request.operation) {
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
.read => {
const o = openAt(request.node) orelse return fail(out);
var buffer: [protocol.maximum_payload]u8 = undefined;
const want = @min(@as(usize, request.len), buffer.len);
const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
},
.write => {
const o = openAt(request.node) orelse return fail(out);
const data = payload[0..@min(payload.len, request.len)];
const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
},
.status => {
const o = openAt(request.node) orelse return fail(out);
const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind) };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
},
.readdir => {
const o = openAt(request.node) orelse return fail(out);
if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
const kind: protocol.NodeKind = if (listing.is_directory) .directory else .regular;
const header = protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
var buffer: [protocol.maximum_payload]u8 = undefined;
@memcpy(buffer[0..protocol.directory_entry_size], std.mem.asBytes(&header));
const nlen = @min(listing.name_len, buffer.len - protocol.directory_entry_size);
@memcpy(buffer[protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
const total = protocol.directory_entry_size + nlen;
return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
},
.close => {
if (openAt(request.node)) |o| o.used = false;
return writeReply(out, .{ .status = 0 }, &.{});
},
// A backend is never itself a mount target.
.mount, .unmount => return fail(out),
}
}
pub fn main() void {
runtime.service.run(protocol.message_maximum, .{
.service = .fat,
.init = initialise,
.on_message = onMessage,
});
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+220
View File
@@ -0,0 +1,220 @@
//! The on-disk layout of a FAT filesystem — the boot sector / BIOS Parameter
//! Block, directory entries, long-file-name entries, and the FAT32 FSInfo — as
//! `align(1)` extern structs that bit-cast straight out of a 512-byte sector
//! (multi-byte fields are little-endian, like usb-abi.zig). Pure data, plus the
//! cluster-count FAT-type detection. Host-testable.
const std = @import("std");
/// The BIOS Parameter Block, common to FAT12/16/32 (offset 0..36 of the boot
/// sector). The extended part that follows differs by FAT type.
pub const BiosParameterBlock = extern struct {
jump: [3]u8,
oem_name: [8]u8,
bytes_per_sector: u16 align(1),
sectors_per_cluster: u8,
reserved_sector_count: u16 align(1),
fat_count: u8,
root_entry_count: u16 align(1),
total_sectors_16: u16 align(1),
media: u8,
fat_size_16: u16 align(1),
sectors_per_track: u16 align(1),
head_count: u16 align(1),
hidden_sectors: u32 align(1),
total_sectors_32: u32 align(1),
};
/// The FAT12/16 extended boot record (offset 36).
pub const ExtendedBootRecord16 = extern struct {
drive_number: u8,
reserved: u8,
boot_signature: u8,
volume_id: u32 align(1),
volume_label: [11]u8,
filesystem_type: [8]u8,
};
/// The FAT32 extended boot record (offset 36).
pub const ExtendedBootRecord32 = extern struct {
fat_size_32: u32 align(1),
extended_flags: u16 align(1),
filesystem_version: u16 align(1),
root_cluster: u32 align(1),
filesystem_information_sector: u16 align(1),
backup_boot_sector: u16 align(1),
reserved: [12]u8,
drive_number: u8,
reserved1: u8,
boot_signature: u8,
volume_id: u32 align(1),
volume_label: [11]u8,
filesystem_type: [8]u8,
};
/// A 32-byte directory entry (8.3 short name form).
pub const DirectoryEntry = extern struct {
name: [11]u8, // 8 name + 3 extension, space-padded
attributes: u8,
reserved_nt: u8,
creation_time_tenth: u8,
creation_time: u16 align(1),
creation_date: u16 align(1),
last_access_date: u16 align(1),
first_cluster_high: u16 align(1),
write_time: u16 align(1),
write_date: u16 align(1),
first_cluster_low: u16 align(1),
file_size: u32 align(1),
pub fn firstCluster(self: DirectoryEntry) u32 {
return (@as(u32, self.first_cluster_high) << 16) | self.first_cluster_low;
}
pub fn setFirstCluster(self: *DirectoryEntry, cluster: u32) void {
self.first_cluster_low = @truncate(cluster);
self.first_cluster_high = @truncate(cluster >> 16);
}
pub fn isFree(self: DirectoryEntry) bool {
return self.name[0] == 0x00 or self.name[0] == 0xE5;
}
pub fn isEnd(self: DirectoryEntry) bool {
return self.name[0] == 0x00;
}
pub fn isDirectory(self: DirectoryEntry) bool {
return self.attributes & attribute_directory != 0;
}
pub fn isLongName(self: DirectoryEntry) bool {
return self.attributes & attribute_long_name_mask == attribute_long_name;
}
pub fn isVolumeLabel(self: DirectoryEntry) bool {
return self.attributes & attribute_volume_id != 0 and !self.isLongName();
}
};
/// A 32-byte long-file-name entry (attributes == 0x0F). A sequence of these
/// precedes the 8.3 entry they name, each carrying 13 UTF-16 code units.
pub const LongNameEntry = extern struct {
order: u8,
name1: [5]u16 align(1),
attributes: u8,
kind: u8,
checksum: u8,
name2: [6]u16 align(1),
first_cluster_low: u16 align(1),
name3: [2]u16 align(1),
};
/// The FAT32 FSInfo sector (usually sector 1): advisory free-cluster bookkeeping.
pub const FileSystemInformation = extern struct {
lead_signature: u32 align(1), // 0x41615252
reserved1: [480]u8,
struct_signature: u32 align(1), // 0x61417272
free_count: u32 align(1),
next_free: u32 align(1),
reserved2: [12]u8,
trail_signature: u32 align(1), // 0xAA550000
};
// Directory-entry attribute bits.
pub const attribute_read_only: u8 = 0x01;
pub const attribute_hidden: u8 = 0x02;
pub const attribute_system: u8 = 0x04;
pub const attribute_volume_id: u8 = 0x08;
pub const attribute_directory: u8 = 0x10;
pub const attribute_archive: u8 = 0x20;
pub const attribute_long_name: u8 = 0x0F; // read_only|hidden|system|volume_id
pub const attribute_long_name_mask: u8 = 0x3F;
// FSInfo signatures.
pub const fsinfo_lead_signature: u32 = 0x41615252;
pub const fsinfo_struct_signature: u32 = 0x61417272;
pub const fsinfo_trail_signature: u32 = 0xAA550000;
/// End-of-chain markers (a cluster value >= these ends a chain).
pub const end_of_chain_12: u32 = 0xFF8;
pub const end_of_chain_16: u32 = 0xFFF8;
pub const end_of_chain_32: u32 = 0x0FFFFFF8;
pub const bad_cluster_32: u32 = 0x0FFFFFF7;
pub const free_cluster: u32 = 0;
pub const boot_signature_offset: usize = 510; // 0x55 0xAA at the end of the boot sector
pub const FatType = enum { fat12, fat16, fat32 };
/// The geometry derived from the BPB, plus the FAT type (by the Microsoft
/// cluster-count rule: <4085 FAT12, <65525 FAT16, else FAT32).
pub const Geometry = struct {
fat_type: FatType,
bytes_per_sector: u32,
sectors_per_cluster: u32,
reserved_sector_count: u32,
fat_count: u32,
fat_size_sectors: u32, // per FAT
root_entry_count: u32, // FAT12/16
root_cluster: u32, // FAT32
first_data_sector: u32,
total_sectors: u32,
cluster_count: u32,
fsinfo_sector: u32, // FAT32
};
/// Derive the geometry (and FAT type) from a boot sector's first 512 bytes.
/// Returns null if the sector is not a plausible FAT boot sector.
pub fn geometryOf(sector: []const u8) ?Geometry {
if (sector.len < 512) return null;
if (sector[boot_signature_offset] != 0x55 or sector[boot_signature_offset + 1] != 0xAA) return null;
const bpb = std.mem.bytesToValue(BiosParameterBlock, sector[0..@sizeOf(BiosParameterBlock)]);
if (bpb.bytes_per_sector == 0 or bpb.sectors_per_cluster == 0 or bpb.fat_count == 0) return null;
const fat_size_16: u32 = bpb.fat_size_16;
var fat_size: u32 = fat_size_16;
var root_cluster: u32 = 0;
var fsinfo_sector: u32 = 0;
if (fat_size_16 == 0) {
const ebr = std.mem.bytesToValue(ExtendedBootRecord32, sector[36 .. 36 + @sizeOf(ExtendedBootRecord32)]);
fat_size = ebr.fat_size_32;
root_cluster = ebr.root_cluster;
fsinfo_sector = ebr.filesystem_information_sector;
}
const total_sectors: u32 = if (bpb.total_sectors_16 != 0) bpb.total_sectors_16 else bpb.total_sectors_32;
const root_dir_sectors = (@as(u32, bpb.root_entry_count) * 32 + bpb.bytes_per_sector - 1) / bpb.bytes_per_sector;
const first_data_sector = bpb.reserved_sector_count + bpb.fat_count * fat_size + root_dir_sectors;
if (total_sectors < first_data_sector) return null;
const data_sectors = total_sectors - first_data_sector;
const cluster_count = data_sectors / bpb.sectors_per_cluster;
const fat_type: FatType = if (cluster_count < 4085) .fat12 else if (cluster_count < 65525) .fat16 else .fat32;
return .{
.fat_type = fat_type,
.bytes_per_sector = bpb.bytes_per_sector,
.sectors_per_cluster = bpb.sectors_per_cluster,
.reserved_sector_count = bpb.reserved_sector_count,
.fat_count = bpb.fat_count,
.fat_size_sectors = fat_size,
.root_entry_count = bpb.root_entry_count,
.root_cluster = root_cluster,
.first_data_sector = first_data_sector,
.total_sectors = total_sectors,
.cluster_count = cluster_count,
.fsinfo_sector = fsinfo_sector,
};
}
test "on-disk struct sizes match the specification" {
try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
try std.testing.expectEqual(@as(usize, 54), @sizeOf(ExtendedBootRecord32));
try std.testing.expectEqual(@as(usize, 32), @sizeOf(DirectoryEntry));
try std.testing.expectEqual(@as(usize, 32), @sizeOf(LongNameEntry));
try std.testing.expectEqual(@as(usize, 512), @sizeOf(FileSystemInformation));
}
test "directory entry cluster split/join" {
var entry = std.mem.zeroes(DirectoryEntry);
entry.setFirstCluster(0x01234567);
try std.testing.expectEqual(@as(u16, 0x4567), entry.first_cluster_low);
try std.testing.expectEqual(@as(u16, 0x0123), entry.first_cluster_high);
try std.testing.expectEqual(@as(u32, 0x01234567), entry.firstCluster());
}
+1 -1
View File
@@ -25,7 +25,7 @@ const power = runtime.power_protocol;
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent /// microkernel keeps such choices in user space, not the kernel. Drivers are absent
/// on purpose: the device manager owns those. (A future init reads this from a /// on purpose: the device manager owns those. (A future init reads this from a
/// manifest under /system/services instead of a hardcoded list.) /// manifest under /system/services instead of a hardcoded list.)
const boot_services = [_][]const u8{ "vfs", "input", "device-manager" }; const boot_services = [_][]const u8{ "vfs", "input", "device-manager", "fat" };
var children: [boot_services.len]u32 = .{0} ** boot_services.len; var children: [boot_services.len]u32 = .{0} ** boot_services.len;
var child_count: usize = 0; var child_count: usize = 0;
+39
View File
@@ -0,0 +1,39 @@
//! Pure path utilities for the VFS mount router — no IPC, no state, so they are
//! host-testable in isolation. The router uses these to decide whether an opened
//! path lies under a mount point and, if so, what it looks like relative to that
//! mount.
const std = @import("std");
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by a
/// path separator — return the path relative to the mount ("/" for an exact
/// match, otherwise the tail beginning with '/'). Returns null when `path` is not
/// under the mount, so a prefix like "/mnt/usb" never captures "/mnt/usbextra".
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
if (path.len < mount_prefix.len) return null;
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
if (path.len == mount_prefix.len) return "/";
if (path[mount_prefix.len] != '/') return null;
return path[mount_prefix.len..];
}
/// Whether `path` is absolute (rooted at '/'). Bare names — what the flat ramfs
/// uses — are relative and never route through a mount.
pub fn isAbsolute(path: []const u8) bool {
return path.len > 0 and path[0] == '/';
}
test "underMount matches only at path boundaries" {
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null); // not a boundary
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null); // shorter than the prefix
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
try std.testing.expect(underMount("greeting", "/mnt/usb") == null); // a bare name
}
test "isAbsolute distinguishes paths from bare names" {
try std.testing.expect(isAbsolute("/mnt/usb"));
try std.testing.expect(!isAbsolute("greeting"));
try std.testing.expect(!isAbsolute(""));
}
+42
View File
@@ -17,8 +17,36 @@ pub const Operation = enum(u32) {
read, // read(node, offset, len) -> bytes read, // read(node, offset, len) -> bytes
write, // write(node, offset, bytes) -> count write, // write(node, offset, bytes) -> count
status, // status(node) -> FileStatus status, // status(node) -> FileStatus
// Appended for the mount router (M5). Values stay stable, so existing clients
// and the flat-ramfs tests are unaffected.
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
mount, // mount(prefix payload, capability = backend endpoint)
unmount, // unmount(prefix payload)
}; };
/// The type of a filesystem node, aligned to the FSH file-type table
/// (docs/danos-file-system-hierarchy-FSH.md). Fills `FileStatus.kind` and
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
pub const NodeKind = enum(u32) {
regular = 0,
directory = 1,
character_device = 2,
block_device = 3,
symbolic_link = 4,
fifo = 5,
socket = 6,
};
/// One directory entry, returned by `readdir`: a fixed header followed inline in
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
pub const DirectoryEntry = extern struct {
kind: u32, // a NodeKind
name_len: u32,
size: u64,
};
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
/// Request header. `node` is the server-side open-file id (from a prior open); /// Request header. `node` is the server-side open-file id (from a prior open);
/// for `open` the path is the payload and `len` is its length. `offset`/`len` /// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count. /// carry the read/write position and count.
@@ -57,3 +85,17 @@ pub const maximum_payload: usize = message_maximum - request_size;
/// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`). /// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`).
pub const create: u32 = 1; pub const create: u32 = 1;
/// Open a directory (for readdir) rather than a file. A mounted backend uses
/// this to open a directory node; the flat ramfs ignores it.
pub const directory: u32 = 2;
test "protocol struct sizes and node kinds" {
const std = @import("std");
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
// The appended operations keep the original values.
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
}
+185 -16
View File
@@ -3,14 +3,24 @@
//! file API marshals open/read/write/stat/close into calls to this server's //! file API marshals open/read/write/stat/close into calls to this server's
//! endpoint, published under the well-known `vfs` service id). //! endpoint, published under the well-known `vfs` service id).
//! //!
//! For now the namespace is a small in-memory ramfs (opening a name creates it): //! Two namespaces meet here (M5):
//! enough to prove the whole path — client file API -> IPC -> server dispatch -> //! - a small in-memory **ramfs** — opening a bare name creates it — enough to
//! reply. Device nodes backed by user-space drivers (/device) layer on top in M10, //! prove the round trip and to back the existing tests;
//! where `open` on a /device name forwards to the owning driver's endpoint. //! - **mounted filesystems**: a mount table maps an absolute path prefix (e.g.
//! `/mnt/usb`) to a backend server's endpoint. An open of a path under a mount
//! is *forwarded* to that backend (which speaks this same protocol), and every
//! later read/write/status/readdir/close on the resulting handle is relayed to
//! it. The VFS is the router; a filesystem (FAT) is the backend.
//!
//! A path routes through a mount only when it is absolute and lies under a mount
//! prefix; bare names always resolve in the flat ramfs — the backward-compat
//! contract the `vfs` / `vfs-client-death` tests rely on.
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const protocol = runtime.vfs_protocol; const protocol = runtime.vfs_protocol;
const path = @import("path.zig");
const ipc = runtime.ipc;
const Node = struct { const Node = struct {
used: bool = false, used: bool = false,
@@ -22,15 +32,29 @@ const Node = struct {
const OpenFile = struct { const OpenFile = struct {
used: bool = false, used: bool = false,
// For a local handle: an index into `nodes`. For a forwarding handle: the
// node id the backend returned. (usize == u64 here, so it holds either.)
node: usize = 0, node: usize = 0,
// Non-null for a handle that forwards to a mounted backend.
backend: ?ipc.Handle = null,
// The client (task id — an IPC badge is one) that opened this handle. What // The client (task id — an IPC badge is one) that opened this handle. What
// release-on-death sweeps by: a service must never depend on its clients // release-on-death sweeps by: a service must never depend on its clients
// cleaning up after themselves (docs/process-lifecycle.md). // cleaning up after themselves (docs/process-lifecycle.md).
owner: u32 = 0, owner: u32 = 0,
}; };
// One mounted filesystem: an absolute path prefix and the backend endpoint that
// serves everything under it.
const Mount = struct {
used: bool = false,
prefix: [64]u8 = undefined,
prefix_len: usize = 0,
backend: ipc.Handle = 0,
};
var nodes = [_]Node{.{}} ** 8; var nodes = [_]Node{.{}} ** 8;
var opens = [_]OpenFile{.{}} ** 16; var opens = [_]OpenFile{.{}} ** 16;
var mounts = [_]Mount{.{}} ** 8;
fn findNode(name: []const u8) ?usize { fn findNode(name: []const u8) ?usize {
for (&nodes, 0..) |*n, i| { for (&nodes, 0..) |*n, i| {
@@ -57,6 +81,26 @@ fn openAt(id: u64) ?*OpenFile {
return if (o.used) o else null; return if (o.used) o else null;
} }
/// The mount whose prefix most specifically contains `name`, and the path
/// relative to it. Only absolute paths route; bare names never match.
const MountMatch = struct { backend: ipc.Handle, relative: []const u8 };
fn longestMount(name: []const u8) ?MountMatch {
if (!path.isAbsolute(name)) return null;
var best: ?MountMatch = null;
var best_len: usize = 0;
for (&mounts) |*m| {
if (!m.used) continue;
const prefix = m.prefix[0..m.prefix_len];
if (path.underMount(name, prefix)) |relative| {
if (best == null or prefix.len >= best_len) {
best_len = prefix.len;
best = .{ .backend = m.backend, .relative = relative };
}
}
}
return best;
}
/// Serialise a reply header + payload into `out`; returns the total length. /// Serialise a reply header + payload into `out`; returns the total length.
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize { fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply)); @memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
@@ -76,13 +120,96 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
} }
// --- mount routing ----------------------------------------------------------
/// Forward an open under a mount to its backend and, on success, allocate a local
/// forwarding handle that remembers the backend's node id.
fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize {
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags };
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
@memcpy(message[protocol.request_size..][0..rel.len], rel);
var reply: [protocol.message_maximum]u8 = undefined;
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
if (n < protocol.reply_size) return fail(out);
const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{});
for (&opens, 0..) |*o, i| {
if (!o.used) {
o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender };
return writeReply(out, .{ .status = 0, .node = i }, &.{});
}
}
return fail(out);
}
/// Relay a read/write/status/readdir/close on a forwarding handle to the backend
/// (the node already rewritten to the backend's id) and copy its reply out.
fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize {
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const plen = @min(payload.len, protocol.maximum_payload);
@memcpy(message[protocol.request_size..][0..plen], payload[0..plen]);
var reply: [protocol.message_maximum]u8 = undefined;
const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out);
const copy = @min(n, out.len);
@memcpy(out[0..copy], reply[0..copy]);
return copy;
}
/// Best-effort close of a backend node (used when a dead client's forwarding
/// handles are swept — the backend must not leak the vfs's opens).
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 };
var reply: [protocol.message_maximum]u8 = undefined;
_ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {};
}
fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize {
for (&mounts) |*m| {
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
m.backend = backend;
writeLine("/system/services/vfs: remounted {s}\n", .{prefix});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
for (&mounts) |*m| {
if (!m.used) {
const l = @min(prefix.len, m.prefix.len);
m.used = true;
@memcpy(m.prefix[0..l], prefix[0..l]);
m.prefix_len = l;
m.backend = backend;
writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
return fail(out);
}
fn doUnmount(out: []u8, prefix: []const u8) usize {
for (&mounts) |*m| {
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
m.used = false;
writeLine("/system/services/vfs: unmounted {s}\n", .{prefix});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
return fail(out);
}
/// Release every open handle `client` held — called on that client's published /// Release every open handle `client` held — called on that client's published
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers, /// exit event. Forwarding handles also tell their backend to release; local
/// only the dead client's handles go. /// nodes (the ramfs files) stay, since ramfs contents outlive their writers.
fn releaseClientHandles(client: u32) void { fn releaseClientHandles(client: u32) void {
var released: u32 = 0; var released: u32 = 0;
for (&opens) |*o| { for (&opens) |*o| {
if (o.used and o.owner == client) { if (o.used and o.owner == client) {
if (o.backend) |backend| forwardClose(backend, o.node);
o.used = false; o.used = false;
released += 1; released += 1;
} }
@@ -91,19 +218,32 @@ fn releaseClientHandles(client: u32) void {
} }
/// Handle one request from `sender`; write the reply into `out`, return its length. /// Handle one request from `sender`; write the reply into `out`, return its length.
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = capability;
if (message.len < protocol.request_size) return fail(out); if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..]; const payload = message[protocol.request_size..];
switch (request.operation) { switch (request.operation) {
.mount => {
const prefix = payload[0..@min(payload.len, request.len)];
const backend = capability orelse return fail(out);
return doMount(out, prefix, backend);
},
.unmount => {
const prefix = payload[0..@min(payload.len, request.len)];
return doUnmount(out, prefix);
},
.open => { .open => {
const name = payload[0..@min(payload.len, request.len)]; const name = payload[0..@min(payload.len, request.len)];
if (longestMount(name)) |m| return forwardOpen(out, m.backend, m.relative, request.flags, sender);
// An absolute path with no matching mount is simply not found — only
// bare names live in the flat ramfs. (Else /mnt/usb would be silently
// created as a flat file when its filesystem is not yet mounted.)
if (path.isAbsolute(name)) return fail(out);
const ni = findNode(name) orelse createNode(name) orelse return fail(out); const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| { for (&opens, 0..) |*o, i| {
if (!o.used) { if (!o.used) {
o.* = .{ .used = true, .node = ni, .owner = sender }; o.* = .{ .used = true, .node = ni, .backend = null, .owner = sender };
return writeReply(out, .{ .status = 0, .node = i }, &.{}); return writeReply(out, .{ .status = 0, .node = i }, &.{});
} }
} }
@@ -111,7 +251,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.read => { .read => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node]; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const nd = &nodes[@intCast(of.node)];
const off: usize = @intCast(request.offset); const off: usize = @intCast(request.offset);
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload); const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
@@ -119,7 +264,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.write => { .write => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node]; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const nd = &nodes[@intCast(of.node)];
const off: usize = @intCast(request.offset); const off: usize = @intCast(request.offset);
if (off > nd.data.len) return fail(out); if (off > nd.data.len) return fail(out);
const n = @min(@min(payload.len, request.len), nd.data.len - off); const n = @min(@min(payload.len, request.len), nd.data.len - off);
@@ -129,11 +279,30 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.status => { .status => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const st = protocol.FileStatus{ .size = nodes[of.node].size, .kind = 0 }; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const st = protocol.FileStatus{ .size = nodes[@intCast(of.node)].size, .kind = @intFromEnum(protocol.NodeKind.regular) };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st)); return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st));
}, },
.readdir => {
const of = openAt(request.node) orelse return fail(out);
if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
// The flat ramfs has no directories: report EOF.
return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
},
.close => { .close => {
if (request.node < opens.len) opens[@intCast(request.node)].used = false; const of = openAt(request.node);
if (of) |o| {
if (o.backend) |backend| forwardClose(backend, o.node);
o.used = false;
}
return writeReply(out, .{ .status = 0 }, &.{}); return writeReply(out, .{ .status = 0 }, &.{});
}, },
} }
@@ -142,7 +311,7 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
/// Startup, under the harness: subscribe to the published exit events — when a /// Startup, under the harness: subscribe to the published exit events — when a
/// client dies holding open handles, the exit notification is how the VFS learns /// client dies holding open handles, the exit notification is how the VFS learns
/// to release them (docs/process-lifecycle.md). /// to release them (docs/process-lifecycle.md).
fn initialise(endpoint: runtime.ipc.Handle) bool { fn initialise(endpoint: ipc.Handle) bool {
if (!runtime.process.subscribeExits(endpoint)) { if (!runtime.process.subscribeExits(endpoint)) {
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n"); _ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
} }
@@ -152,8 +321,8 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
/// A non-signal notification: the only kind the VFS subscribes to is exit events. /// A non-signal notification: the only kind the VFS subscribes to is exit events.
fn onNotification(badge: u64) void { fn onNotification(badge: u64) void {
if (badge & runtime.ipc.notify_exit_bit != 0) { if (badge & ipc.notify_exit_bit != 0) {
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit))); releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
} }
} }
+60 -18
View File
@@ -25,6 +25,7 @@ import shutil
import socket import socket
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -66,7 +67,14 @@ ARCHES = {
"-machine", "q35", "-m", "128M", "-machine", "q35", "-m", "128M",
"-drive", f"if=pflash,format=raw,readonly=on,file={a['ovmf_code']}", "-drive", f"if=pflash,format=raw,readonly=on,file={a['ovmf_code']}",
"-drive", f"if=pflash,format=raw,file={vars_fd}", "-drive", f"if=pflash,format=raw,file={vars_fd}",
"-drive", f"format=raw,file=fat:rw:{boot_volume}", # Boot off a FAT USB device: the boot volume is a mass-storage device on
# the xHCI bus (usb-kbd/usb-mouse ride the same controller). `boot_volume`
# is the FAT image the build produces. bootindex=0 steers OVMF to it.
"-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0",
"-drive", f"if=none,id=bootusb,format=raw,file={boot_volume}",
"-device", "usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
"-net", "none", "-net", "none",
"-vga", "none", "-device", "VGA,edid=on,xres=1280,yres=720", "-vga", "none", "-device", "VGA,edid=on,xres=1280,yres=720",
"-display", "none", "-display", "none",
@@ -274,9 +282,8 @@ CASES = [
{"name": "usb-report", {"name": "usb-report",
"smp": 4, "smp": 4,
"timeout": 150, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci", # The xHCI bus + usb-kbd/usb-mouse come from the default boot config now
"-device", "usb-kbd,bus=xhci.0", # (every case boots off a usb-storage device on that bus).
"-device", "usb-mouse,bus=xhci.0"],
"expect": r"device-manager: child added[\s\S]*" "expect": r"device-manager: child added[\s\S]*"
r"device-manager: child added[\s\S]*" r"device-manager: child added[\s\S]*"
r"device-manager: test mode: killing the reporter[\s\S]*" r"device-manager: test mode: killing the reporter[\s\S]*"
@@ -284,6 +291,44 @@ CASES = [
r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: restarting usb-xhci-bus[\s\S]*"
r"device-manager: child added", r"device-manager: child added",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# USB HID end to end: boot the full tree, enumerate the xHCI, and let the
# manager spawn the USB keyboard driver, which opens its device over the
# transfer protocol, asks for boot protocol, subscribes to its interrupt
# endpoint, and comes up — proof the class-driver <-> controller path works.
{"name": "usb-hid",
"smp": 4,
"timeout": 150,
# usb-kbd/usb-mouse ride the default boot xHCI bus (see qemu_args).
"expect": r"(?=[\s\S]*usb-hid/keyboard: ok)(?=[\s\S]*usb-hid/mouse: ok)",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# USB mass storage end to end: the boot usb-storage device (the FAT32 image,
# which has a real 0x55AA boot sector) is enough — the manager spawns
# usb-storage, which opens the device, runs the Bulk-Only / SCSI bring-up,
# reads its capacity, and reads block 0 (the 0x55AA boot sig). Proof of the
# bulk transfer path + BOT + SCSI end to end.
{"name": "usb-storage",
"smp": 4,
"timeout": 150,
"expect": r"usb-storage: ready[\s\S]*usb-storage: block 0 signature 0x55aa",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# FAT mount end to end: the fat server mounts the boot usb-storage device (the
# FAT32 image) into the VFS at /mnt/usb. A fat-test client then lists and reads
# through the mount — proof of the whole stack: block device -> FAT parse ->
# VFS routing -> file read.
{"name": "fat-mount",
"smp": 4,
"timeout": 150,
"expect": r"fat: mounted /mnt/usb[\s\S]*fat-test: ok",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Boot-from-USB smoke: the whole system now boots off the FAT32 image on a
# usb-storage device (OVMF -> \EFI\BOOT\BOOTX64.efi -> kernel), so the kernel
# reaching its PASS marker at all proves the USB boot path end to end. Reuses
# the smoke kernel build; the value is the explicit, named regression guard.
{"name": "usb-boot",
"build_case": "smoke",
"qmp_after": {"delay": 2, "command": "query-status"},
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses # M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses
# them) finds exactly the Device count the kernel's own parse produced. # them) finds exactly the Device count the kernel's own parse produced.
{"name": "acpi-parse", {"name": "acpi-parse",
@@ -329,9 +374,6 @@ CASES = [
{"name": "acpi-report", {"name": "acpi-report",
"smp": 4, "smp": 4,
"timeout": 150, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"],
"expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*" "expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*"
r"acpi: reported PNP0F13 \(device \d+, 1 resources\)", r"acpi: reported PNP0F13 \(device \d+, 1 resources\)",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
@@ -348,9 +390,6 @@ CASES = [
{"name": "device-list", {"name": "device-list",
"smp": 4, "smp": 4,
"timeout": 150, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"],
"expect": r"device-list: \d+ devices[\s\S]*" "expect": r"device-list: \d+ devices[\s\S]*"
r"device-list: subscribed[\s\S]*" r"device-list: subscribed[\s\S]*"
r"device-manager: test mode: killing the reporter[\s\S]*" r"device-manager: test mode: killing the reporter[\s\S]*"
@@ -363,9 +402,6 @@ CASES = [
{"name": "driver-restart", {"name": "driver-restart",
"smp": 4, "smp": 4,
"timeout": 150, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"],
"expect": r"usb-xhci-bus: hello acknowledged[\s\S]*" "expect": r"usb-xhci-bus: hello acknowledged[\s\S]*"
r"device-manager: restarting crash-test[\s\S]*" r"device-manager: restarting crash-test[\s\S]*"
r"device-manager: crash-test is failing repeatedly", r"device-manager: crash-test is failing repeatedly",
@@ -471,12 +507,15 @@ def qmp_send(path, command):
def run_case(arch, case): def run_case(arch, case):
err = build(arch, case["name"]) # A case's kernel build defaults to its name; `build_case` decouples the two
# so a case can reuse another's kernel (e.g. usb-boot reuses smoke's).
err = build(arch, case.get("build_case", case["name"]))
if err: if err:
return False, "build failed:\n" + err return False, "build failed:\n" + err
# zig-out is the FHS boot volume; hand it to the guest as-is (see qemu_args). # The bootable FAT32 USB image the build produced (tools/make-fat-image.py),
boot_volume = os.path.join(REPO, "zig-out") # presented to the guest as a usb-storage device (see qemu_args).
boot_volume = os.path.join(REPO, "zig-out", "danos-usb.img")
vars_fd = os.path.join(WORK, "vars.fd") vars_fd = os.path.join(WORK, "vars.fd")
shutil.copy(arch["ovmf_vars"], vars_fd) shutil.copy(arch["ovmf_vars"], vars_fd)
serial = os.path.join(WORK, "serial.log") serial = os.path.join(WORK, "serial.log")
@@ -492,8 +531,11 @@ def run_case(arch, case):
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
cmd += case["qemu_extra"] cmd += case["qemu_extra"]
# A QMP control socket, always present (additive): how a case's `qmp_after` # A QMP control socket, always present (additive): how a case's `qmp_after`
# hook injects host-side events into the guest mid-run. # hook injects host-side events into the guest mid-run. Kept under a short temp
qmp_path = os.path.join(WORK, "qmp.sock") # dir, not WORK: a unix socket path is capped at ~104 bytes (sun_path), and a
# deep worktree path (e.g. .claude/worktrees/<name>/zig-out/qemu-test/qmp.sock)
# blows that limit on macOS, so QEMU fails to bind and exits before booting.
qmp_path = os.path.join(tempfile.gettempdir(), f"danos-qmp-{os.getpid()}.sock")
if os.path.exists(qmp_path): if os.path.exists(qmp_path):
os.remove(qmp_path) os.remove(qmp_path)
cmd += ["-qmp", f"unix:{qmp_path},server,nowait"] cmd += ["-qmp", f"unix:{qmp_path},server,nowait"]
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env python3
"""Format a real FAT32 image from a set of host files — the danos boot volume.
Mirrors tools/make-initial-ramdisk.py in spirit: pure Python 3 standard library,
no external tools (no mkfs.fat / mtools). It writes a valid FAT32 filesystem — a
boot sector + BPB, an FSInfo sector, a backup boot sector, two FATs, and a
directory tree of clusters — so UEFI/OVMF boots \\EFI\\BOOT\\BOOTX64.efi off it
and the danos FAT driver mounts the same image.
make-fat-image.py <out.img> <size-MiB> [<dest-path> <host-file>]...
make-fat-image.py --verify <out.img>
Each <dest-path> is a forward-slash path inside the image (e.g.
"EFI/BOOT/BOOTX64.efi"); intermediate directories are created. Names that do not
fit 8.3 get a mangled short name plus long-file-name (LFN) entries.
"""
import struct
import sys
SECTOR = 512
SECTORS_PER_CLUSTER = 1 # 512-byte clusters keep the cluster count high for FAT32
RESERVED_SECTORS = 32
NUM_FATS = 2
CLUSTER_BYTES = SECTOR * SECTORS_PER_CLUSTER
END_OF_CHAIN = 0x0FFFFFFF
BAD_CLUSTER = 0x0FFFFFF7
ATTR_ARCHIVE = 0x20
ATTR_DIRECTORY = 0x10
ATTR_LONG_NAME = 0x0F
VALID_83 = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$%'-_@~!(){}^#& ")
def fat32_geometry(total_sectors):
"""Solve for the FAT size (sectors per FAT) and cluster count that fit."""
fat_size = 1
while True:
data_sectors = total_sectors - RESERVED_SECTORS - NUM_FATS * fat_size
cluster_count = data_sectors // SECTORS_PER_CLUSTER
needed = ((cluster_count + 2) * 4 + SECTOR - 1) // SECTOR
if needed <= fat_size:
return fat_size, cluster_count
fat_size = needed
class Fat32Image:
def __init__(self, total_sectors):
self.total_sectors = total_sectors
self.fat_size, self.cluster_count = fat32_geometry(total_sectors)
if self.cluster_count < 65525:
sys.exit(f"error: image too small for FAT32 ({self.cluster_count} clusters "
f"< 65525); use a larger size")
self.first_data_sector = RESERVED_SECTORS + NUM_FATS * self.fat_size
# The FAT, in memory: entry 0 media, entry 1 EOC, entry 2 the root dir.
self.fat = [0] * (self.cluster_count + 2)
self.fat[0] = 0x0FFFFFF8
self.fat[1] = END_OF_CHAIN
self.fat[2] = END_OF_CHAIN
self.next_free = 3
self.cluster_data = {} # cluster number -> bytes (one cluster's worth)
def alloc(self):
cluster = self.next_free
if cluster >= self.cluster_count + 2:
sys.exit("error: image out of clusters")
self.next_free += 1
self.fat[cluster] = END_OF_CHAIN
return cluster
def store_chain(self, content):
"""Allocate a cluster chain holding `content` and return its first cluster."""
length = max(1, (len(content) + CLUSTER_BYTES - 1) // CLUSTER_BYTES)
clusters = [self.alloc() for _ in range(length)]
for i in range(length - 1):
self.fat[clusters[i]] = clusters[i + 1]
for i, cluster in enumerate(clusters):
chunk = content[i * CLUSTER_BYTES:(i + 1) * CLUSTER_BYTES]
self.cluster_data[cluster] = chunk + b"\x00" * (CLUSTER_BYTES - len(chunk))
return clusters[0]
def store_directory(self, first_cluster, entries):
"""Write directory `entries` (bytes) into `first_cluster`, extending the chain."""
length = max(1, (len(entries) + CLUSTER_BYTES - 1) // CLUSTER_BYTES)
clusters = [first_cluster]
for _ in range(length - 1):
clusters.append(self.alloc())
for i in range(len(clusters) - 1):
self.fat[clusters[i]] = clusters[i + 1]
for i, cluster in enumerate(clusters):
chunk = entries[i * CLUSTER_BYTES:(i + 1) * CLUSTER_BYTES]
self.cluster_data[cluster] = chunk + b"\x00" * (CLUSTER_BYTES - len(chunk))
def cluster_sector(self, cluster):
return self.first_data_sector + (cluster - 2) * SECTORS_PER_CLUSTER
def serialize(self):
image = bytearray(self.total_sectors * SECTOR)
image[0:SECTOR] = self.boot_sector()
image[SECTOR:2 * SECTOR] = self.fsinfo_sector()
image[6 * SECTOR:7 * SECTOR] = self.boot_sector() # backup boot sector
# Both FATs.
fat_bytes = b"".join(struct.pack("<I", entry & 0x0FFFFFFF) for entry in self.fat)
fat_bytes += b"\x00" * (self.fat_size * SECTOR - len(fat_bytes))
for copy in range(NUM_FATS):
base = (RESERVED_SECTORS + copy * self.fat_size) * SECTOR
image[base:base + len(fat_bytes)] = fat_bytes
# The data region (clusters).
for cluster, data in self.cluster_data.items():
base = self.cluster_sector(cluster) * SECTOR
image[base:base + len(data)] = data
return bytes(image)
def boot_sector(self):
sector = bytearray(SECTOR)
# BPB.
struct.pack_into(
"<3s8sHBHBHHBHHHII", sector, 0,
b"\xEB\x58\x90", # jump
b"MSWIN4.1", # OEM name (widest firmware compatibility)
SECTOR, # bytes per sector
SECTORS_PER_CLUSTER, # sectors per cluster
RESERVED_SECTORS, # reserved sector count
NUM_FATS, # number of FATs
0, # root entry count (0 for FAT32)
0, # total sectors 16 (0 -> use 32)
0xF8, # media descriptor
0, # FAT size 16 (0 for FAT32)
32, # sectors per track
2, # heads
0, # hidden sectors
self.total_sectors, # total sectors 32
)
# FAT32 extended BPB (offset 36).
struct.pack_into(
"<IHHIHH12sBBBI11s8s", sector, 36,
self.fat_size, # FAT size 32
0, # extended flags
0, # filesystem version
2, # root cluster
1, # FSInfo sector
6, # backup boot sector
b"\x00" * 12, # reserved
0x80, # drive number
0, # reserved
0x29, # extended boot signature
0x12345678, # volume id
b"DANOS ", # volume label
b"FAT32 ", # filesystem type
)
sector[510] = 0x55
sector[511] = 0xAA
return bytes(sector)
def fsinfo_sector(self):
sector = bytearray(SECTOR)
struct.pack_into("<I", sector, 0, 0x41615252) # lead signature
struct.pack_into("<I", sector, 484, 0x61417272) # struct signature
free = self.cluster_count - (self.next_free - 2)
struct.pack_into("<I", sector, 488, free) # free count
struct.pack_into("<I", sector, 492, self.next_free) # next free hint
struct.pack_into("<I", sector, 508, 0xAA550000) # trail signature
return bytes(sector)
def lfn_checksum(short_name):
checksum = 0
for byte in short_name:
checksum = (((checksum & 1) << 7) + (checksum >> 1) + byte) & 0xFF
return checksum
def short_name_for(name, used):
"""Return (raw 11-byte 8.3 name, needs_lfn)."""
if "." in name and not name.startswith("."):
base, ext = name.rsplit(".", 1)
else:
base, ext = name, ""
upper_base, upper_ext = base.upper(), ext.upper()
# A name fits 8.3 if it is short enough and uses valid characters; a lowercase
# name is simply stored uppercased (FAT is case-insensitive, so the bootloader
# and the danos driver still find it). Only genuinely non-8.3 names (too long,
# e.g. initial-ramdisk.img) get a mangled short name plus LFN entries.
fits = (1 <= len(base) <= 8 and len(ext) <= 3
and all(c in VALID_83 for c in upper_base + upper_ext))
if fits:
return (upper_base.ljust(8) + upper_ext.ljust(3)).encode("ascii"), False
# Mangle to STEM~N.EXT.
stem = "".join(c for c in upper_base if c in VALID_83 and c != " ")[:6] or "FILE"
index = 1
while True:
candidate = f"{stem}~{index}".ljust(8)[:8] + upper_ext.ljust(3)[:3]
raw = candidate.encode("ascii")
if raw not in used:
used.add(raw)
return raw, True
index += 1
def lfn_entries(name, short_raw):
checksum = lfn_checksum(short_raw)
units = list(name.encode("utf-16-le"))
pairs = [bytes(units[i:i + 2]) for i in range(0, len(units), 2)]
pairs.append(b"\x00\x00") # null terminator
while len(pairs) % 13 != 0:
pairs.append(b"\xff\xff")
count = len(pairs) // 13
out = bytearray()
for sequence in range(count, 0, -1): # stored last-logical-first
piece = pairs[(sequence - 1) * 13:sequence * 13]
entry = bytearray(32)
entry[0] = sequence | (0x40 if sequence == count else 0)
for i in range(5):
entry[1 + i * 2:1 + i * 2 + 2] = piece[i]
entry[11] = ATTR_LONG_NAME
entry[12] = 0
entry[13] = checksum
for i in range(6):
entry[14 + i * 2:14 + i * 2 + 2] = piece[5 + i]
entry[26:28] = b"\x00\x00"
for i in range(2):
entry[28 + i * 2:28 + i * 2 + 2] = piece[11 + i]
out += entry
return bytes(out)
def short_entry(raw11, attributes, cluster, size):
return struct.pack(
"<11sBBBHHHHHHHI",
raw11, attributes, 0, 0, 0, 0, 0,
(cluster >> 16) & 0xFFFF, 0, 0, cluster & 0xFFFF, size,
)
def write_directory(image, cluster, children, parent_cluster, is_root):
"""Recursively lay out a directory: allocate child clusters, build entries."""
entries = bytearray()
if not is_root:
entries += short_entry(b". ", ATTR_DIRECTORY, cluster, 0)
parent = 0 if parent_cluster == 2 else parent_cluster
entries += short_entry(b".. ", ATTR_DIRECTORY, parent, 0)
used_short_names = set()
for name, child in children.items():
raw, needs_lfn = short_name_for(name, used_short_names)
used_short_names.add(raw)
if child["type"] == "dir":
child_cluster = image.alloc()
if needs_lfn:
entries += lfn_entries(name, raw)
entries += short_entry(raw, ATTR_DIRECTORY, child_cluster, 0)
write_directory(image, child_cluster, child["children"], cluster, False)
else:
data = child["data"]
first = image.store_chain(data) if data else 0
if needs_lfn:
entries += lfn_entries(name, raw)
entries += short_entry(raw, ATTR_ARCHIVE, first, len(data))
image.store_directory(cluster, bytes(entries))
def build_tree(pairs):
root = {}
for dest, host in pairs:
with open(host, "rb") as handle:
data = handle.read()
parts = [p for p in dest.replace("\\", "/").split("/") if p]
node = root
for part in parts[:-1]:
node = node.setdefault(part, {"type": "dir", "children": {}})["children"]
node[parts[-1]] = {"type": "file", "data": data}
return root
def build(out_path, size_mib, pairs):
total_sectors = size_mib * 1024 * 1024 // SECTOR
image = Fat32Image(total_sectors)
tree = build_tree(pairs)
write_directory(image, 2, tree, 0, True)
with open(out_path, "wb") as handle:
handle.write(image.serialize())
print(f"make-fat-image: wrote {out_path} "
f"({size_mib} MiB FAT32, {image.cluster_count} clusters)")
def verify(path):
with open(path, "rb") as handle:
data = handle.read()
if len(data) < SECTOR or data[510] != 0x55 or data[511] != 0xAA:
sys.exit("verify: missing 0x55AA boot signature")
bytes_per_sector, sectors_per_cluster = struct.unpack_from("<HB", data, 11)
reserved, num_fats = struct.unpack_from("<H", data, 14)[0], data[16]
fat_size_32, root_cluster = struct.unpack_from("<I", data, 36)[0], struct.unpack_from("<I", data, 44)[0]
total_sectors = struct.unpack_from("<I", data, 32)[0]
if bytes_per_sector != SECTOR or sectors_per_cluster == 0 or num_fats == 0 or fat_size_32 == 0:
sys.exit("verify: implausible BPB")
first_data = reserved + num_fats * fat_size_32
cluster_count = (total_sectors - first_data) // sectors_per_cluster
if cluster_count < 65525:
sys.exit(f"verify: not FAT32 ({cluster_count} clusters)")
# Resolve EFI/BOOT/BOOTX64.efi through the directory tree to prove it is present.
if not _resolve(data, ["EFI", "BOOT", "BOOTX64.EFI"], root_cluster,
reserved, num_fats, fat_size_32, first_data, sectors_per_cluster):
sys.exit("verify: EFI/BOOT/BOOTX64.efi not found")
print(f"verify: {path} is FAT32 ({cluster_count} clusters); EFI/BOOT/BOOTX64.efi present")
def _read_fat(data, cluster, reserved):
offset = reserved * SECTOR + cluster * 4
return struct.unpack_from("<I", data, offset)[0] & 0x0FFFFFFF
def _resolve(data, parts, cluster, reserved, num_fats, fat_size, first_data, spc):
for part in parts:
cluster = _find(data, cluster, part, reserved, first_data, spc)
if cluster is None:
return False
return True
def _find(data, dir_cluster, name, reserved, first_data, spc):
target = name.upper()
cluster = dir_cluster
guard = 0
while cluster >= 2 and cluster < BAD_CLUSTER and guard < 100000:
sector = first_data + (cluster - 2) * spc
for s in range(spc):
base = (sector + s) * SECTOR
for i in range(SECTOR // 32):
entry = data[base + i * 32:base + i * 32 + 32]
if entry[0] == 0x00:
return None
if entry[0] == 0xE5 or (entry[11] & ATTR_LONG_NAME) == ATTR_LONG_NAME:
continue
raw = entry[0:11]
short = (raw[0:8].rstrip().decode("latin1") +
("." + raw[8:11].rstrip().decode("latin1") if raw[8:11].strip() else "")).upper()
if short == target:
return ((entry[20] | (entry[21] << 8)) << 16) | (entry[26] | (entry[27] << 8))
cluster = _read_fat(data, cluster, reserved)
guard += 1
return None
def main(argv):
if len(argv) == 3 and argv[1] == "--verify":
verify(argv[2])
return 0
if len(argv) < 3 or (len(argv) - 3) % 2 != 0:
sys.exit("usage: make-fat-image.py <out.img> <size-MiB> [<dest> <host>]...\n"
" make-fat-image.py --verify <out.img>")
out_path = argv[1]
size_mib = int(argv[2])
rest = argv[3:]
pairs = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)]
build(out_path, size_mib, pairs)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))