Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67702fa250 | ||
|
|
8a38540312 | ||
|
|
54635eecf5 | ||
|
|
184d90c2c6 | ||
|
|
a32eed877d | ||
|
|
347a041d85 | ||
|
|
53e42837e0 | ||
|
|
f52c591f5e | ||
|
|
77d2e22ed1 | ||
|
|
a64a01a6a9 | ||
|
|
35e8921de8 | ||
|
|
3fb9d5936a |
@@ -58,7 +58,6 @@ fn addUserBinary(
|
|||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
runtime_module: *std.Build.Module,
|
runtime_module: *std.Build.Module,
|
||||||
posix_module: *std.Build.Module,
|
|
||||||
mmio_module: *std.Build.Module,
|
mmio_module: *std.Build.Module,
|
||||||
xkeyboard_config_module: *std.Build.Module,
|
xkeyboard_config_module: *std.Build.Module,
|
||||||
acpi_ids_module: *std.Build.Module,
|
acpi_ids_module: *std.Build.Module,
|
||||||
@@ -78,9 +77,6 @@ fn addUserBinary(
|
|||||||
.stack_protector = false,
|
.stack_protector = false,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "runtime", .module = runtime_module },
|
.{ .name = "runtime", .module = runtime_module },
|
||||||
// POSIX/C compatibility layer, available to any program that wants it
|
|
||||||
// (danos-native code uses `runtime` directly). See library/posix/.
|
|
||||||
.{ .name = "posix", .module = posix_module },
|
|
||||||
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
||||||
.{ .name = "mmio", .module = mmio_module },
|
.{ .name = "mmio", .module = mmio_module },
|
||||||
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
||||||
@@ -142,6 +138,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 +243,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", .{
|
||||||
@@ -253,18 +276,6 @@ pub fn build(b: *std.Build) void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
|
||||||
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
|
||||||
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
|
||||||
// and library/posix/posix.zig.
|
|
||||||
const posix_module = b.addModule("posix", .{
|
|
||||||
.root_source_file = b.path("library/posix/posix.zig"),
|
|
||||||
.imports = &.{
|
|
||||||
.{ .name = "runtime", .module = runtime_module },
|
|
||||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
||||||
// build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
|
// build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
|
||||||
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
||||||
@@ -336,7 +347,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
||||||
// linked into the kernel's user region against the `runtime` runtime library, and
|
// linked into the kernel's user region against the `runtime` runtime library, and
|
||||||
// started in ring 3 by the kernel's user-ELF loader.
|
// started in ring 3 by the kernel's user-ELF loader.
|
||||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
const init_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
||||||
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
||||||
b.getInstallStep().dependOn(&init_install.step);
|
b.getInstallStep().dependOn(&init_install.step);
|
||||||
|
|
||||||
@@ -344,20 +355,41 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Each is built by the same user-binary recipe, then packed into one image by
|
// Each is built by the same user-binary recipe, then packed into one image by
|
||||||
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||||
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
||||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.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_keyboard_exe = addUserBinary(b, kernel_target, runtime_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, 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, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-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 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, 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, 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, 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, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
|
||||||
|
const fat_test_exe = addUserBinary(b, kernel_target, runtime_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, 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.
|
||||||
pci_bus_exe.root_module.addImport("pci-class", pci_class_module);
|
pci_bus_exe.root_module.addImport("pci-class", pci_class_module);
|
||||||
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||||
// what the driver-restart scenario drives the crash-loop cap with.
|
// what the driver-restart scenario drives the crash-loop cap with.
|
||||||
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
||||||
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
||||||
// The discovery service: one swappable process per firmware
|
// The discovery service: one swappable process per firmware
|
||||||
// (docs/discovery.md), bundled under the neutral ramdisk name
|
// (docs/discovery.md), bundled under the neutral ramdisk name
|
||||||
// "discovery" so the device manager never learns which firmware it is on.
|
// "discovery" so the device manager never learns which firmware it is on.
|
||||||
@@ -371,18 +403,22 @@ pub fn build(b: *std.Build) void {
|
|||||||
.acpi => "system/services/acpi/acpi.zig",
|
.acpi => "system/services/acpi/acpi.zig",
|
||||||
.fdt => "system/services/fdt/fdt.zig",
|
.fdt => "system/services/fdt/fdt.zig",
|
||||||
};
|
};
|
||||||
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||||
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
||||||
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, 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, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
||||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
|
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
|
||||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
||||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
||||||
|
const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig");
|
||||||
|
|
||||||
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
||||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||||
@@ -402,6 +438,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");
|
||||||
@@ -422,6 +468,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||||
mk_run.addArg("process-test");
|
mk_run.addArg("process-test");
|
||||||
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("log-flush");
|
||||||
|
mk_run.addFileArg(log_flush_exe.getEmittedBin());
|
||||||
|
|
||||||
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
||||||
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
||||||
@@ -433,6 +481,11 @@ 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" },
|
||||||
|
.{ log_flush_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 +519,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 +609,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 +667,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(.{
|
||||||
|
|||||||
+17
-11
@@ -82,6 +82,12 @@ Start with the north star:
|
|||||||
- **[resilience.md](resilience.md) — resilience.** A design note (not built yet) on
|
- **[resilience.md](resilience.md) — resilience.** A design note (not built yet) on
|
||||||
fault isolation + live restart — the reincarnation-server + capability model that
|
fault isolation + live restart — the reincarnation-server + capability model that
|
||||||
makes "if I break it, I can restart it" real. danos's core motivation.
|
makes "if I break it, I can restart it" real. danos's core motivation.
|
||||||
|
- **[zig-self-hosting.md](zig-self-hosting.md) — running Zig on danos.** A design note
|
||||||
|
(not built yet) on making danos a real Zig target (`-target x86_64-danos`) and
|
||||||
|
eventually running the compiler on it. The key realisation: Zig 0.16 reduces an OS
|
||||||
|
port to **one seam** (`std.os.danos`), so we build `runtime.os` (→ that seam) plus a
|
||||||
|
thin `runtime.fs`, retire the `posix` shim, and follow a phased path to
|
||||||
|
`zig build-exe hello.zig` running on danos — **not** Linux-ABI emulation.
|
||||||
|
|
||||||
Cutting across all of these:
|
Cutting across all of these:
|
||||||
|
|
||||||
@@ -191,21 +197,22 @@ system/ → /system danos's own internals (the self-representation)
|
|||||||
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
|
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
|
||||||
vfs.zig, vfs-test.zig, protocol.zig)
|
vfs.zig, vfs-test.zig, protocol.zig)
|
||||||
library/ → /lib libraries, one sub-directory each
|
library/ → /lib libraries, one sub-directory each
|
||||||
runtime/ the danos-native runtime — the stable application ABI
|
runtime/ the danos-native runtime + file API (fs) — the stable application ABI
|
||||||
posix/ POSIX/C compatibility, layered over runtime
|
|
||||||
boot/ → /boot the loaders
|
boot/ → /boot the loaders
|
||||||
tools/ test/ host-side build + QEMU test harness
|
tools/ test/ host-side build + QEMU test harness
|
||||||
```
|
```
|
||||||
|
|
||||||
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
||||||
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the POSIX
|
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's
|
||||||
layer imports by name. `usb`/`block` drivers will expose their protocols the same way.
|
file API (`runtime.fs`) imports by name. `usb`/`block` drivers expose their protocols the
|
||||||
|
same way.
|
||||||
|
|
||||||
`library/posix/` is special: it is the **one place** POSIX/C spellings are allowed
|
There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the
|
||||||
verbatim (`stat`, `O_CREAT`, `fopen`, `errno`). Everywhere else follows the danos
|
danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim
|
||||||
naming rule with no exception — see [coding-standards.md](coding-standards.md). The
|
(`library/posix/`) was retired as premature — the real POSIX/C surface will come later
|
||||||
POSIX layer calls the runtime, never the kernel's system calls directly, so it never
|
from the `std.os.danos` seam (and, eventually, musl) when danos becomes a Zig target (see
|
||||||
appears in the private-ABI path.
|
[zig-self-hosting.md](zig-self-hosting.md)). When it does, the foreign-ABI naming
|
||||||
|
exception in [coding-standards.md](coding-standards.md) applies to that seam.
|
||||||
|
|
||||||
## Source map
|
## Source map
|
||||||
|
|
||||||
@@ -229,8 +236,7 @@ appears in the private-ABI path.
|
|||||||
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
||||||
| In-kernel test cases | `system/kernel/tests.zig` |
|
| In-kernel test cases | `system/kernel/tests.zig` |
|
||||||
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
|
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
|
||||||
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access — the stable application ABI | `library/runtime/` |
|
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` |
|
||||||
| POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` |
|
|
||||||
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` |
|
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` |
|
||||||
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
||||||
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
||||||
|
|||||||
@@ -65,15 +65,18 @@ Three, and only three.
|
|||||||
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
||||||
`fwrite` any more.
|
`fwrite` any more.
|
||||||
|
|
||||||
**This exception is scoped to one place: `library/posix/`.** A file under
|
**This exception is scoped to a file that *is* a foreign ABI, and nothing else.**
|
||||||
`library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the
|
danos has no such file today: the old `library/posix/` compatibility shim was retired
|
||||||
whole rule for that directory. **Everywhere else, Zig/danos naming applies with no
|
once its callers moved to the danos-native `runtime.fs`, since a hand-rolled POSIX
|
||||||
POSIX exception**, so there is nothing to get wrong: if you're not in
|
layer is premature until danos actually needs it (see
|
||||||
`library/posix/`, expand it. A concept POSIX also has gets a danos name outside that
|
[zig-self-hosting.md](zig-self-hosting.md)). The exception will apply again to the
|
||||||
layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
`std.os.danos` seam when danos becomes a real Zig target — that module *is* the C-ABI
|
||||||
flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and
|
`system` interface, so it keeps `open`/`read`/`errno`/`O_CREAT`. **Everywhere else,
|
||||||
`O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an
|
Zig/danos naming applies with no exception**: a concept POSIX also has gets a danos
|
||||||
exception to this — they wrap the private danos ABI, so they use danos names.)
|
name — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
||||||
|
flag, not `O_CREAT`; the boundary is where `stat`→`status` and `O_CREAT`→`create` get
|
||||||
|
mapped. (The `syscall` *wrappers* elsewhere are not an exception — they wrap the
|
||||||
|
private danos ABI, so they use danos names.)
|
||||||
|
|
||||||
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
||||||
not ours, and are left alone:
|
not ours, and are left alone:
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ was a mistake) without inheriting the mechanism, the API, or the names. The nami
|
|||||||
rule is danos's own and it is strict: plain words that communicate intent
|
rule is danos's own and it is strict: plain words that communicate intent
|
||||||
(`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks
|
(`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks
|
||||||
(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for
|
(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for
|
||||||
a concept that already has one. Literal POSIX arrives later and lives elsewhere: a
|
a concept that already has one. Literal POSIX arrives later and lives elsewhere: the
|
||||||
**musl-based C layer** (growing out of library/posix) that wires C programs to the
|
`std.os.danos` seam that makes danos a Zig target, and eventually a **musl-based C
|
||||||
danos runtime — musl's syscall surface retargeted at danos system calls and IPC
|
layer** on the same native surface (see [zig-self-hosting.md](zig-self-hosting.md)) —
|
||||||
protocols (files onto the VFS protocol, `sigaction`/`wait` onto this lifecycle,
|
musl's syscall surface retargeted at danos system calls and IPC protocols (files onto
|
||||||
sockets onto whatever networking becomes). Ported programs see POSIX; the system
|
the VFS protocol, `sigaction`/`wait` onto this lifecycle, sockets onto whatever
|
||||||
underneath never does.
|
networking becomes). Ported programs see POSIX; the system underneath never does.
|
||||||
|
|
||||||
## Why a standard vocabulary
|
## Why a standard vocabulary
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# Running Zig on danos: the self-hosting roadmap
|
||||||
|
|
||||||
|
A design note (not built yet) on the path to making danos a **real Zig target** — a
|
||||||
|
target you can name (`-target x86_64-danos`) and, eventually, run the Zig compiler
|
||||||
|
itself on. It is forward-looking, like [vision.md](vision.md): it sets a direction
|
||||||
|
and the decisions that follow from it, so the code we write now bends toward it
|
||||||
|
instead of away.
|
||||||
|
|
||||||
|
This note deliberately does **not** cover a text editor or terminal. Those are
|
||||||
|
easier (single-process, I/O-bound) and fall out of the early phases here almost for
|
||||||
|
free; the hard, shaping problem is the standard-library surface, so that is what
|
||||||
|
this roadmap is about.
|
||||||
|
|
||||||
|
The analysis behind it was done against **Zig 0.16** (the pinned toolchain). Zig's
|
||||||
|
standard library moves between releases — especially the parts described here — so
|
||||||
|
treat upstream references as "the shape in 0.16.x," and expect to re-check them on a
|
||||||
|
toolchain bump.
|
||||||
|
|
||||||
|
## The win condition
|
||||||
|
|
||||||
|
danos runs the Zig compiler when a bare
|
||||||
|
|
||||||
|
```
|
||||||
|
zig build-exe hello.zig
|
||||||
|
```
|
||||||
|
|
||||||
|
completes **on danos** and produces a runnable danos binary. Note the milestone is
|
||||||
|
`build-exe`, not `zig build`: the `zig build` runner spawns child processes (the
|
||||||
|
build steps), which needs a whole process-control surface danos does not have yet.
|
||||||
|
A single `build-exe` needs none of that (see Phase 3). Reaching `build-exe` is
|
||||||
|
"self-hosting"; reaching `zig build` is a later, separate lift.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- **No Linux syscall/ABI emulation.** danos will not implement the Linux `syscall`
|
||||||
|
interface so that stock `x86_64-linux` binaries run. That is a permanent
|
||||||
|
compatibility treadmill and it inverts the microkernel design — explicitly out.
|
||||||
|
- **No musl port yet.** A musl libc port is a reasonable *later* effort (it unlocks
|
||||||
|
the C ecosystem), but it is not on the critical path to Zig-on-danos, and it is
|
||||||
|
deferred. The roadmap below is arranged so the work still pays off if musl ever
|
||||||
|
happens (see "The same surface, twice").
|
||||||
|
- **Editor/terminal are out of scope for this note** (they are downstream of Phase 1).
|
||||||
|
|
||||||
|
**On FFI.** Foreign-function interop splits the same way as the doors below. Zig-level
|
||||||
|
and C-ABI-*exposing* FFI (`extern`, `callconv(.c)`, C-ABI structs) work on a real target
|
||||||
|
immediately — and the `std.os.danos` seam is C-ABI-shaped by construction, so it is
|
||||||
|
FFI-friendly from the start. *Consuming* C libraries (`@cImport`, linking archives) is
|
||||||
|
the part that needs a libc + headers, i.e. the deferred musl door. So an eventual FFI
|
||||||
|
need reinforces keeping that door open; it does not change the plan.
|
||||||
|
|
||||||
|
## The realization that shapes everything: 0.16 gives us *one* seam
|
||||||
|
|
||||||
|
The instinct "to target Zig we'd have to reimplement all the `std` namespaces" was
|
||||||
|
how older Zig worked. Zig 0.16 (post-"writergate") is far kinder:
|
||||||
|
|
||||||
|
- **`std.fs` is essentially gone.** It is now path helpers plus deprecated aliases;
|
||||||
|
there is no `std.fs.File`, `std.fs.Dir`, or `std.fs.cwd()`. File and directory
|
||||||
|
work goes through **`std.Io`** — a single runtime **vtable** (`Io.zig`) of
|
||||||
|
function pointers handed to `main` as `std.process.Init.io`. `std.Io.File` and
|
||||||
|
`std.Io.Dir` are thin forwarders to that vtable. `Io.zig` and the `fs` shim carry
|
||||||
|
**zero** per-OS branches.
|
||||||
|
- **`std.posix` is one generic body** parameterised over a single `system` module.
|
||||||
|
With no libc, `system` resolves **per target OS**: `.linux => std.os.linux`,
|
||||||
|
`.plan9 => std.os.plan9`, and so on. The generic `std.posix.read`/`write`/`open`
|
||||||
|
bodies are just `system.read(...)` plus an errno switch — *identical for every
|
||||||
|
OS*. The only variable is what `system` binds to.
|
||||||
|
- **`std.os.<tag>`** (e.g. `std/os/linux.zig`) is therefore the real porting seam: a
|
||||||
|
low-level, C-ABI-shaped module of `read/write/open/close/lseek/mmap/clock/exit/…`
|
||||||
|
plus an `errno` enum and the constant tables (`O_*`, `CLOCK_*`, `S_*`).
|
||||||
|
|
||||||
|
Put together: **to port danos we write `std.os.danos` once** — the ~30-operation
|
||||||
|
seam — and the whole `std.posix` / `std.fs` / `std.Io` tower above it lights up
|
||||||
|
generically, because none of it branches on the OS. That is a dramatically smaller
|
||||||
|
and more contained target than "reimplement the namespaces."
|
||||||
|
|
||||||
|
## Three doors, and why we take the first
|
||||||
|
|
||||||
|
| Door | What it is | Verdict |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| **1. Implement the std seam** (`std.os.danos`) | Write the ~30-op `system` module over danos's native ABI + VFS; the generic std tower lights up. | **Take this.** The only door that touches neither C nor the Linux ABI. |
|
||||||
|
| **2. Port musl** | Port musl libc to danos, link Zig against it. | Defer. Good later for the *C* ecosystem; barely helps *Zig* (std only uses libc on the libc-linked path). |
|
||||||
|
| **3. Emulate the Linux ABI** | Implement Linux syscalls so stock linux binaries run. | Reject. Bottomless compatibility treadmill; against the design. |
|
||||||
|
|
||||||
|
### The same surface, twice
|
||||||
|
|
||||||
|
Doors 1 and 2 are the **same native surface at different layers**. `std.posix.read`
|
||||||
|
is `system.read(...)` + an errno switch *regardless of OS* — the only question is
|
||||||
|
whether `system` is **`std.os.danos` (Zig)** or **musl (C)**. Either way, the set of
|
||||||
|
danos-facing operations you must implement is the *same* ~30 ops, all bottoming out
|
||||||
|
in danos's native syscalls + the VFS/FAT server.
|
||||||
|
|
||||||
|
So the runtime work below is **not throwaway** if musl ever happens: you are building
|
||||||
|
the danos-native implementations of that surface either way. Door 1 just packages
|
||||||
|
them as Zig; a future musl re-uses the identical kernel/VFS operations underneath. The
|
||||||
|
two symmetries worth keeping in mind: doors 1 and 2 converge at the **top** (identical
|
||||||
|
POSIX surface); doors 2 and 3 converge at the **bottom** (unmodified musl needs the
|
||||||
|
Linux syscall ABI). Door 1 is the only one that avoids both C and Linux.
|
||||||
|
|
||||||
|
### A fork is table stakes — for any door
|
||||||
|
|
||||||
|
`std.Target.Os.Tag` is a **closed enum** baked into the compiler binary *and* into
|
||||||
|
the `std` linked with every program; `-target x86_64-danos` resolves through it. So
|
||||||
|
adding `danos` as a name requires patching and rebuilding the compiler — even the
|
||||||
|
musl door needs this. "Fork Zig" is therefore not an extra cost unique to door 1; it
|
||||||
|
is the price of admission for *any* real target. What door 1 adds on top is small and
|
||||||
|
localised (below).
|
||||||
|
|
||||||
|
## The architecture decision: `runtime.os` + `runtime.fs`, and retire `posix`
|
||||||
|
|
||||||
|
danos already has the right split ([the private-ABI boundary](../README.md)): the
|
||||||
|
kernel exposes a minimal syscall ABI ([syscall.md](syscall.md)); the **`runtime`**
|
||||||
|
library is the stable, danos-native application ABI. What this roadmap adds:
|
||||||
|
|
||||||
|
- **`runtime.os` — the seam.** A C-ABI-shaped module of the ~30 operations
|
||||||
|
(`read/write/open/close/lseek/mmap/munmap/clock/exit/…`) + an errno enum + the
|
||||||
|
constant tables, each backed by danos's native syscalls and the VFS. **Structure it
|
||||||
|
to mirror `std/os/linux.zig`.** This is the load-bearing, *non-throwaway* artifact:
|
||||||
|
when we fork Zig, `runtime.os` is copy-pasted (near-verbatim) into `std.os.danos`.
|
||||||
|
- **`runtime.fs` — the thin native file API** danos programs use *today*, layered
|
||||||
|
over `runtime.os`. It is also the concrete backing for the `std.Io` vtable's
|
||||||
|
file-write entry once we're a real target, which is why program stdout, diagnostics,
|
||||||
|
and file writes should all be *decided once at that seam* rather than as bespoke
|
||||||
|
per-call helpers (see "How this informs decisions now").
|
||||||
|
|
||||||
|
**Do not hand-mirror the high-level std namespaces.** `std.fs`/`std.Io`/`std.process`
|
||||||
|
are generic and OS-agnostic; once `std.os.danos` exists and we fork, upstream *gives*
|
||||||
|
them to danos for free. Hand-writing `runtime.std.fs` to imitate them would be
|
||||||
|
redundant the day the fork works, and it would chase a moving target (0.16's `std.Io`
|
||||||
|
is large and still shifting). Build the seam well; take the tower for free.
|
||||||
|
|
||||||
|
**Why not a library called `std`?** Because `@import("std")` resolves to the
|
||||||
|
compiler-provided standard library; a user module named `std` would *shadow* it for
|
||||||
|
anything that imports it that way. That is the real reason the seam lives *inside* a
|
||||||
|
forked std as `std/os/danos.zig`, not as a `runtime.std` library — and why danos's end
|
||||||
|
state (`@import("std")` just working, and knowing danos) is the most natively Zig it can
|
||||||
|
be. `runtime.os` is only the interim staging ground: developed against the stock
|
||||||
|
toolchain so Phase 1 need not wait on the fork, then promoted near-verbatim into the
|
||||||
|
fork's `std/os/danos.zig`.
|
||||||
|
|
||||||
|
### Retire `library/posix`
|
||||||
|
|
||||||
|
The `posix` compatibility layer (`unistd`, `stdio`) was the right instinct too early.
|
||||||
|
Its whole value is POSIX *spellings* for POSIX software — and danos has no POSIX
|
||||||
|
software; every current caller is danos-native code that could use `runtime.fs`
|
||||||
|
directly. The real POSIX story arrives later and from elsewhere (musl, or upstream
|
||||||
|
`std`'s own posix over `std.os.danos`), which supersedes a hand-rolled shim. So it is
|
||||||
|
premature abstraction that adds a "which layer do I use?" fork with no payoff yet.
|
||||||
|
|
||||||
|
Its footprint is tiny: **five** call sites, all `unistd` file operations —
|
||||||
|
`system/services/fat/fat.zig` (`mount`), the `vfs-test` and `fat-test` clients, and
|
||||||
|
(from the boot-log work) `init.zig` and `log-flush.zig`. `stdio.zig` is dead — nothing
|
||||||
|
imports it. The plan: build `runtime.fs`, migrate those five to it, delete
|
||||||
|
`library/posix/`, and drop the `posix` module from `build.zig`'s `addUserBinary`.
|
||||||
|
|
||||||
|
## Where danos stands: coverage vs. the gaps
|
||||||
|
|
||||||
|
What the seam needs, and what danos already provides:
|
||||||
|
|
||||||
|
| std need | danos today | Gap |
|
||||||
|
|----------|-------------|-----|
|
||||||
|
| open / read / write / close / lseek | VFS (via the current `unistd`, → `runtime.fs`) | none — repackage |
|
||||||
|
| directory read (`getdents`) | VFS `readdir` | none — repackage |
|
||||||
|
| mmap / munmap | native syscalls ([abi.zig](../system/abi.zig)) | none |
|
||||||
|
| page allocator | over `mmap`, via `root.os.heap.page_allocator` override | ~30-line hook |
|
||||||
|
| monotonic clock | `clock` syscall | none |
|
||||||
|
| args / argv | SysV entry stack ([sysv.md](sysv.md)), `runtime.process.Init` | none |
|
||||||
|
| stdout / stderr | `debug_write` today | wire fd 1/2 to a console **byte** stream |
|
||||||
|
| mkdir / unlink / rename / truncate | done — engine + VFS + `runtime.fs` (Phase 2) | — |
|
||||||
|
| stat fields | `{size, kind, mtime}` | **mode / inode** still missing (cache validity) |
|
||||||
|
| wall-clock / realtime | done — `wall_clock` syscall (CMOS RTC, Phase 2d) | — |
|
||||||
|
| **environment variables** | `Init` has no env field | missing (can start empty) |
|
||||||
|
| **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) |
|
||||||
|
| **entropy / random** | — | missing (needed behind `vtable.random`) |
|
||||||
|
| process spawn + exit status | `system_spawn` starts a *named ramdisk binary*; `ExitReason` is a *category* | no exec-of-path, no numeric `WEXITSTATUS` |
|
||||||
|
| threads | one thread per process | avoided via `-fsingle-threaded` (below) |
|
||||||
|
| symlinks | `NodeKind` has the tag; unimplemented | low priority |
|
||||||
|
|
||||||
|
The clustering is clear: reads and memory are basically done; the real work is
|
||||||
|
**filesystem mutation + richer stat + wall-clock**, and a few small seam pieces
|
||||||
|
(page-allocator hook, stdio bytes, entropy). Process spawning and threads are
|
||||||
|
side-stepped entirely for a single `build-exe`.
|
||||||
|
|
||||||
|
## The roadmap
|
||||||
|
|
||||||
|
### Phase 0 — Make `danos` a real target
|
||||||
|
|
||||||
|
**Host, target, self-host — keep the three roles straight.** The *host* is where the
|
||||||
|
compiler runs (your mac + linux dev machines); the *target* is what it emits (`danos`);
|
||||||
|
and eventually danos becomes a host too (self-hosting — the win condition). So the move
|
||||||
|
is: fork the compiler, build it **for** your dev hosts, and teach it to **cross-compile
|
||||||
|
to** danos. You already do this — danos is cross-compiled `freestanding` from your dev
|
||||||
|
host today; Phase 0 swaps that `freestanding` target for a real `x86_64-danos` one, which
|
||||||
|
is what unlocks the native `std`.
|
||||||
|
|
||||||
|
**Why a compiler fork, not just a `--zig-lib-dir` override.** `std.Target.Os.Tag` is a
|
||||||
|
*closed enum compiled into the compiler binary*, so `-target x86_64-danos` will not even
|
||||||
|
parse unless the compiler itself knows the tag. Overriding the std lib directory alone
|
||||||
|
cannot add a target — and there is no libc-only shortcut (a future musl needs the same
|
||||||
|
patch). The only alternative, staying on `freestanding` + hand-shims, is exactly the
|
||||||
|
non-native feel we are leaving: `@import("std")` there is stubbed, not real.
|
||||||
|
|
||||||
|
**The fork.** Clone `ziglang/zig` at the pinned 0.16 tag; build it with a stock
|
||||||
|
same-version `zig` (`zig build` in the tree — a standard, LLVM-pulling, roughly one-time
|
||||||
|
build); point danos's `build.zig`/CI at the resulting binary. Four localised patches:
|
||||||
|
|
||||||
|
- add `danos` to `std.Target.Os.Tag`, in the "no version range" group alongside
|
||||||
|
plan9/serenity;
|
||||||
|
- add `danos` to the freestanding/other **no-op `_start` list** in `std`'s `start.zig`,
|
||||||
|
so std does *not* emit its own System-V `_start` — danos keeps owning the entry shim
|
||||||
|
and `Init`/argv construction it already builds ([sysv.md](sysv.md));
|
||||||
|
- wire the `system` selector `.danos => std.os.danos` in `std.posix`;
|
||||||
|
- add `std/os/danos.zig` — **the seam itself**, promoted near-verbatim from the
|
||||||
|
`runtime.os` developed first in Phase 1 (against the stock toolchain, so the fork is
|
||||||
|
not a prerequisite for starting).
|
||||||
|
|
||||||
|
This is the fork treadmill we accept once. Keep the patch set tiny and `else`-friendly,
|
||||||
|
pin to one 0.16.x, and rebase on point releases.
|
||||||
|
|
||||||
|
### Phase 1 — `runtime.os` read-side + allocator + stdio + cwd; retire `posix`
|
||||||
|
|
||||||
|
Author `runtime.os` (→ `std.os.danos`): the `errno` enum, the constant tables, and
|
||||||
|
the C-convention `read / write / open / openat / close / lseek / mmap / munmap /
|
||||||
|
exit`, each returning result-or-`-errno`. Most backing already exists (VFS + native
|
||||||
|
mmap + clock).
|
||||||
|
|
||||||
|
- Provide `page_allocator` via `root.os.heap.page_allocator` (a thin override over
|
||||||
|
danos `mmap`). This sits **outside** the `std.Io` vtable, so it is wired separately.
|
||||||
|
- Wire fd 0/1/2 to a console **byte** stream (today output only reaches `debug_write`;
|
||||||
|
input is structured `InputEvent` IPC — a byte tty is a new, small thing in both
|
||||||
|
directions).
|
||||||
|
- Add a `getcwd`/`chdir` anchor so `std.fs.cwd()`-style resolution has something to
|
||||||
|
resolve against.
|
||||||
|
- Build `runtime.fs` over `runtime.os`; migrate the five `posix` callers to it; delete
|
||||||
|
`library/posix/` and drop its build module.
|
||||||
|
|
||||||
|
After Phase 1, the surface an editor or terminal needs (open/read/write/close/lseek/
|
||||||
|
readdir/isatty/args/exit) exists. Those are downstream and out of scope here.
|
||||||
|
|
||||||
|
### Phase 2 — Filesystem mutation + real stat (the compiler's cache tower)
|
||||||
|
|
||||||
|
danos's biggest genuine gap, and the correctness-critical one:
|
||||||
|
|
||||||
|
- Add **mkdir / unlink / rename / truncate** to *both* the VFS wire protocol
|
||||||
|
([protocol.zig](../system/services/vfs/protocol.zig)) and the FAT engine
|
||||||
|
([engine.zig](../system/services/fat/engine.zig)), then expose them via `runtime.os`.
|
||||||
|
- Extend `stat` beyond `{size, kind}` to carry **mtime + inode + mode** — `std`'s file
|
||||||
|
stat needs them for build-cache validity — which in turn needs **wall-clock** time
|
||||||
|
(danos is monotonic-only today; an RTC/time service is the dependency).
|
||||||
|
|
||||||
|
Because `std.fs`/`std.Io` have no per-OS branches, finishing this in `runtime.os`
|
||||||
|
lights up the whole file tower for the compiler at once. Environment can stay an empty
|
||||||
|
map until the kernel populates a non-empty `envp`.
|
||||||
|
|
||||||
|
**Status — Phase 2 complete.** `truncate` (O_TRUNC, closing the boot-log stale-tail
|
||||||
|
bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS
|
||||||
|
protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) —
|
||||||
|
host-tested and QEMU-tested (`fat-mutations` + `fat-rename` make a directory, write+read
|
||||||
|
a file in it, rename it, then remove it through the mount). `removeFile` and `rename`
|
||||||
|
are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and long-name-
|
||||||
|
preserving rename are noted limitations). Wall-clock is now a kernel syscall
|
||||||
|
(`wall_clock`, a CMOS-RTC read anchored to the monotonic clock), and the FAT engine
|
||||||
|
stamps and reports **mtime** — `stat` / `runtime.fs.Attributes` carry a real
|
||||||
|
modification time (the `fat-mtime` case reads it back within seconds of the host clock).
|
||||||
|
The remaining `stat` fields, `mode`/`inode`, are deferred (not needed until the
|
||||||
|
compiler's cache layer wants them). **Everything past here is gated on Phase 0 (the
|
||||||
|
fork):** the `runtime.os` seam, `cwd`, stdio-as-fds, and the compiler bring-up.
|
||||||
|
|
||||||
|
### Phase 3 — Single-threaded, self-linked compiler bring-up
|
||||||
|
|
||||||
|
Build the compiler with **two load-bearing flags**:
|
||||||
|
|
||||||
|
- **`-fsingle-threaded`** removes `std.Thread` entirely — `Thread.spawn` is a hard
|
||||||
|
compile error under it, and `std.Io`'s threaded backend runs inline. danos being
|
||||||
|
one-thread-per-process is therefore **not** a blocker. Parallel codegen is a
|
||||||
|
throughput optimisation, not a correctness requirement.
|
||||||
|
- **`-fno-llvm -fno-lld`** keeps codegen and linking **in-process** (the self-hosted
|
||||||
|
x86-64 backend + self-linker), so a single `build-exe` **never forks a child**. That
|
||||||
|
is what lets us defer the entire spawn/exec/wait surface.
|
||||||
|
|
||||||
|
Then supply the few remaining seam pieces: `now` (wrap the danos clock), an entropy
|
||||||
|
source behind `vtable.random` (`randomSecure` can alias it initially — low volume, for
|
||||||
|
temp-file names and hashmap seeds), and the Phase-2 mkdir/rename/unlink for cache dir
|
||||||
|
trees and atomic temp-then-rename output.
|
||||||
|
|
||||||
|
**Explicitly deferred** (not on the `build-exe` path): child-process spawn/exec (only
|
||||||
|
`zig build` and external tools need it), `std.Thread`, `fsync` (FAT is write-through
|
||||||
|
today), symlinks, and musl.
|
||||||
|
|
||||||
|
## Risks and gotchas
|
||||||
|
|
||||||
|
- **The std-fork rebase treadmill is the main ongoing cost.** A new OS tag touches the
|
||||||
|
same broad file set plan9/serenity touch (hundreds of `native_os` sites, plus
|
||||||
|
"unsupported OS" `@compileError` dead-ends a new tag must be routed around), and the
|
||||||
|
entire `std.Io` layer is new in 0.16 and still moving. Stay pinned to one 0.16.x,
|
||||||
|
keep additions localised and `else`-friendly. Watch the closed-enum gotcha: adding
|
||||||
|
`danos` to `Os.Tag` can break existing *exhaustive* switches that lack an `else`, so
|
||||||
|
expect to touch switch sites beyond the ones you implement.
|
||||||
|
- **Single-threaded is load-bearing.** The "no `std.Thread`" simplification rests
|
||||||
|
entirely on `-fsingle-threaded`. If a dependency or flag flips threading back on, you
|
||||||
|
inherit an unescapable compile error (no root-hook exists) — the only outs are a full
|
||||||
|
thread-impl fork or linking libc for pthreads. Keep `single_threaded` asserted end to
|
||||||
|
end.
|
||||||
|
- **In-process linking is load-bearing.** Reaching the compiler without fork/exec
|
||||||
|
depends on `-fno-llvm -fno-lld`. The moment you shell out to LLD/`ld`, you need the
|
||||||
|
full `spawn`/`wait` surface — the hardest microkernel piece — and danos's
|
||||||
|
`system_spawn` only starts a *named ramdisk binary*, not exec of an arbitrary path.
|
||||||
|
Verify the self-hosted backend covers the target output before assuming child
|
||||||
|
processes are optional.
|
||||||
|
- **The shim cannot host the compiler.** danos's current `runtime`/`posix` is fine for
|
||||||
|
danos's *own* native programs, but the compiler `import`s *upstream* `std`, which on
|
||||||
|
a non-target hits the void `system` stub. So the compiler forces the real target
|
||||||
|
(Phase 0's fork). Do not over-invest in extending the hand-shim for compiler
|
||||||
|
purposes; put that effort into `runtime.os` + the VFS/FAT operations, which both the
|
||||||
|
fork *and* a future musl consume.
|
||||||
|
- **`"w"`/`O_CREAT` does not truncate — a silent-corruption bug on this road.** The FAT
|
||||||
|
engine's `writeFile` only *grows* `node.size`, so overwriting a shorter file leaves
|
||||||
|
trailing garbage. Harmless for the boot log today, but for a compiler it means
|
||||||
|
**corrupt `.o`/cache files that look like nondeterministic compiler bugs.** Land
|
||||||
|
`truncate` (Phase 2) before the compiler ever writes cache.
|
||||||
|
- **Exit status is categorical, not numeric.** `process_exit_reason` returns an
|
||||||
|
`ExitReason` *category*, not a numeric code (`WEXITSTATUS`). Fine while spawn is
|
||||||
|
stubbed; the day `zig build` or external tools arrive, plan a kernel exit-record
|
||||||
|
extension — do not let it surprise you.
|
||||||
|
|
||||||
|
## How this informs decisions now
|
||||||
|
|
||||||
|
Two current decisions fall out of this roadmap:
|
||||||
|
|
||||||
|
1. **The `runtime.fs` / `std.Io` question resolves at the vtable seam.** Because 0.16
|
||||||
|
routes *all* output through the `std.Io` vtable's file-write entry, and stdout/stderr
|
||||||
|
are just `File`s with well-known handles, build `runtime.fs` (and the console stdout)
|
||||||
|
as the concrete backing for that entry — not as a bespoke `std.Io.Writer`-only shim.
|
||||||
|
Decide it once, at the seam, and program stdout, diagnostics, and file writes all
|
||||||
|
flow through the same danos VFS/console path.
|
||||||
|
2. **The boot-log `truncate` caveat is now fixed** (Phase 2a). It was the same
|
||||||
|
`writeFile`-only-grows gap that on the self-hosting road would corrupt build output;
|
||||||
|
`engine.truncate` + an O_TRUNC open flag now free the old chain so a shorter rewrite
|
||||||
|
leaves no stale tail, and the boot-log flush opens with it.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [vision.md](vision.md) — the north star this serves.
|
||||||
|
- [syscall.md](syscall.md) — the kernel↔runtime ABI `runtime.os` is built on.
|
||||||
|
- [sysv.md](sysv.md) — the entry stack (`argc/argv/envp/auxv`) danos already constructs.
|
||||||
|
- [ipc.md](ipc.md) — the IPC the VFS/FAT operations travel over.
|
||||||
|
- [danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md) — the
|
||||||
|
filesystem layout the file surface serves.
|
||||||
|
- [coding-standards.md](coding-standards.md) — danos naming (why the compat spellings
|
||||||
|
are confined, and now retired).
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//! DanOS's POSIX / C compatibility layer — `unistd`, `stdio`, and (later) the C
|
|
||||||
//! `errno` / `struct stat` / `extern "C"` surface. This is the *one* place POSIX and
|
|
||||||
//! C spellings are allowed to appear verbatim (see docs/coding-standards.md): a file
|
|
||||||
//! under library/posix/ *is* the foreign ABI, so it keeps the ABI's names. Everything
|
|
||||||
//! it touches on the danos side (the VFS protocol, the runtime) uses danos names,
|
|
||||||
//! which this layer translates to at the boundary.
|
|
||||||
//!
|
|
||||||
//! It is layered strictly *over* the runtime: it calls the runtime's IPC and heap,
|
|
||||||
//! never the kernel's system calls directly. danos-native applications use the
|
|
||||||
//! runtime; this exists so *POSIX* software can too.
|
|
||||||
|
|
||||||
pub const unistd = @import("unistd.zig");
|
|
||||||
pub const stdio = @import("stdio.zig");
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered
|
|
||||||
//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer
|
|
||||||
//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"`
|
|
||||||
//! symbols are provided, so Zig and future C programs share it.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const unistd = @import("unistd.zig");
|
|
||||||
const heap = @import("runtime").heap;
|
|
||||||
|
|
||||||
pub const SEEK_SET = unistd.SEEK_SET;
|
|
||||||
pub const SEEK_CURRENT = unistd.SEEK_CURRENT;
|
|
||||||
pub const SEEK_END = unistd.SEEK_END;
|
|
||||||
|
|
||||||
/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the
|
|
||||||
/// heap; `fclose` frees it.
|
|
||||||
pub const FILE = extern struct {
|
|
||||||
fd: i32,
|
|
||||||
eof: c_int = 0,
|
|
||||||
err: c_int = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn flagsFor(mode: []const u8) u32 {
|
|
||||||
if (mode.len == 0) return 0;
|
|
||||||
return switch (mode[0]) {
|
|
||||||
'w', 'a' => unistd.O_CREAT,
|
|
||||||
else => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error.
|
|
||||||
pub fn fopen(path: []const u8, mode: []const u8) ?*FILE {
|
|
||||||
const fd = unistd.open(path, flagsFor(mode));
|
|
||||||
if (fd < 0) return null;
|
|
||||||
const f = heap.allocator().create(FILE) catch {
|
|
||||||
unistd.close(fd);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
f.* = .{ .fd = fd };
|
|
||||||
if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END);
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fclose(f: *FILE) c_int {
|
|
||||||
unistd.close(f.fd);
|
|
||||||
heap.allocator().destroy(f);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read `size*nmemb` bytes; returns the number of whole items read.
|
|
||||||
pub fn fread(buffer: []u8, size: usize, nmemb: usize, f: *FILE) usize {
|
|
||||||
const total = size * nmemb;
|
|
||||||
if (total == 0) return 0;
|
|
||||||
const n = unistd.read(f.fd, buffer[0..@min(buffer.len, total)]);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.eof = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return @as(usize, @intCast(n)) / size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `size*nmemb` bytes; returns the number of whole items written.
|
|
||||||
pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize {
|
|
||||||
const total = @min(data.len, size * nmemb);
|
|
||||||
if (total == 0) return 0;
|
|
||||||
const n = unistd.write(f.fd, data[0..total]);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.err = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return @as(usize, @intCast(n)) / size;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fseek(f: *FILE, off: i64, whence: u32) c_int {
|
|
||||||
f.eof = 0;
|
|
||||||
return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ftell(f: *FILE) i64 {
|
|
||||||
return unistd.lseek(f.fd, 0, unistd.SEEK_CURRENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rewind(f: *FILE) void {
|
|
||||||
_ = fseek(f, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn feof(f: *FILE) c_int {
|
|
||||||
return f.eof;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ferror(f: *FILE) c_int {
|
|
||||||
return f.err;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fputs(s: []const u8, f: *FILE) c_int {
|
|
||||||
return if (unistd.write(f.fd, s) < 0) -1 else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fputc(c: u8, f: *FILE) c_int {
|
|
||||||
const b = [_]u8{c};
|
|
||||||
return if (unistd.write(f.fd, &b) == 1) c else -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fgetc(f: *FILE) c_int {
|
|
||||||
var b: [1]u8 = undefined;
|
|
||||||
const n = unistd.read(f.fd, &b);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.eof = 1;
|
|
||||||
return -1; // EOF
|
|
||||||
}
|
|
||||||
return b[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature
|
|
||||||
// distinct from the Zig slice API above — land with the first C program, wired
|
|
||||||
// via @export so they don't collide with these Zig names.
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
//! POSIX-style file API for user programs — the low level under C stdio. Files
|
|
||||||
//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each
|
|
||||||
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
|
|
||||||
//! kernel knows nothing of files or fds — the fd table lives here, per process.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const protocol = @import("vfs-protocol");
|
|
||||||
const ipc = @import("runtime").ipc;
|
|
||||||
|
|
||||||
pub const O_CREAT = protocol.create;
|
|
||||||
pub const SEEK_SET: u32 = 0;
|
|
||||||
pub const SEEK_CURRENT: u32 = 1;
|
|
||||||
pub const SEEK_END: u32 = 2;
|
|
||||||
|
|
||||||
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
|
|
||||||
var vfs_handle: usize = 0;
|
|
||||||
var vfs_resolved = false;
|
|
||||||
fn vfs() ?usize {
|
|
||||||
if (!vfs_resolved) {
|
|
||||||
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
|
||||||
vfs_resolved = true;
|
|
||||||
}
|
|
||||||
return vfs_handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maximum_fds = 32;
|
|
||||||
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
|
|
||||||
var fds = [_]Fd{.{}} ** maximum_fds;
|
|
||||||
|
|
||||||
fn allocFd() ?usize {
|
|
||||||
for (&fds, 0..) |*f, i| {
|
|
||||||
if (!f.used) {
|
|
||||||
f.* = .{ .used = true };
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
|
||||||
|
|
||||||
/// One request/reply round trip: [Request header][send payload] -> VFS ->
|
|
||||||
/// [Reply header][receive payload]. The receive payload is written into `out`.
|
|
||||||
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
|
||||||
const h = vfs() orelse return null;
|
|
||||||
var message: [protocol.message_maximum]u8 = undefined;
|
|
||||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
|
||||||
const slen = @min(send.len, protocol.maximum_payload);
|
|
||||||
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
|
||||||
|
|
||||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
|
||||||
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
|
||||||
if (n < protocol.reply_size) return null;
|
|
||||||
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
|
||||||
const rpl = @min(n - protocol.reply_size, out.len);
|
|
||||||
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
|
||||||
return .{ .reply = reply, .payload = out[0..rpl] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
|
|
||||||
pub fn open(path: []const u8, flags: u32) i32 {
|
|
||||||
const fd = allocFd() orelse return -1;
|
|
||||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
|
|
||||||
const r = transact(request, path, &.{}) orelse {
|
|
||||||
fds[fd].used = false;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
if (r.reply.status != 0) {
|
|
||||||
fds[fd].used = false;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
|
|
||||||
return @intCast(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fdPtr(fd: i32) ?*Fd {
|
|
||||||
if (fd < 0 or fd >= maximum_fds) return null;
|
|
||||||
const f = &fds[@intCast(fd)];
|
|
||||||
return if (f.used) f else null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read up to `buffer.len` bytes at the current offset; returns the count or -1.
|
|
||||||
pub fn read(fd: i32, buffer: []u8) isize {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
|
||||||
const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
||||||
const r = transact(request, &.{}, buffer) orelse return -1;
|
|
||||||
if (r.reply.status != 0) return -1;
|
|
||||||
f.offset += r.reply.len;
|
|
||||||
return @intCast(r.reply.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `data` at the current offset; returns the count or -1.
|
|
||||||
pub fn write(fd: i32, data: []const u8) isize {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
|
||||||
const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
||||||
const r = transact(request, data[0..want], &.{}) orelse return -1;
|
|
||||||
if (r.reply.status != 0) return -1;
|
|
||||||
f.offset += r.reply.len;
|
|
||||||
return @intCast(r.reply.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
|
|
||||||
/// file size, which `stat` provides; handled by fetching it here.)
|
|
||||||
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const base: i64 = switch (whence) {
|
|
||||||
SEEK_SET => 0,
|
|
||||||
SEEK_CURRENT => @intCast(f.offset),
|
|
||||||
SEEK_END => blk: {
|
|
||||||
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
||||||
const st = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
||||||
break :blk @intCast(st.size);
|
|
||||||
},
|
|
||||||
else => return -1,
|
|
||||||
};
|
|
||||||
const pos = base + off;
|
|
||||||
if (pos < 0) return -1;
|
|
||||||
f.offset = @intCast(pos);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stat `path`. Returns 0 or -1.
|
|
||||||
pub fn stat(path: []const u8, out: *protocol.FileStatus) i32 {
|
|
||||||
// Open, stat by node, close — simple and enough for now.
|
|
||||||
const fd = open(path, 0);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
defer close(fd);
|
|
||||||
const f = fdPtr(fd).?;
|
|
||||||
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
||||||
out.* = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close an fd (best effort — tells the VFS to release the open file).
|
|
||||||
pub fn close(fd: i32) void {
|
|
||||||
const f = fdPtr(fd) orelse return;
|
|
||||||
const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
_ = transact(request, &.{}, &.{});
|
|
||||||
f.used = false;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
|
||||||
|
//! lists files served by the user-space VFS (system/services/vfs), each call
|
||||||
|
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer
|
||||||
|
//! danos programs use directly; it is also where the file operations that later
|
||||||
|
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
|
||||||
|
//! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet.
|
||||||
|
//!
|
||||||
|
//! Handles are *values*, not entries in a global descriptor table: a `File` /
|
||||||
|
//! `Directory` owns its VFS node id and (for files) a byte offset. So there is no
|
||||||
|
//! per-process fd limit and no shared table to synchronise — the danos-native
|
||||||
|
//! shape, unlike the POSIX fd model the old shim emulated.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const protocol = @import("vfs-protocol");
|
||||||
|
|
||||||
|
/// The kind of a filesystem node — re-exported so a caller need not import the
|
||||||
|
/// wire protocol.
|
||||||
|
pub const Kind = protocol.NodeKind;
|
||||||
|
|
||||||
|
/// A node's metadata (the answer to a status request).
|
||||||
|
pub const Attributes = struct {
|
||||||
|
size: u64,
|
||||||
|
kind: Kind,
|
||||||
|
/// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none.
|
||||||
|
mtime: u64 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to
|
||||||
|
// `.regular` (the server is trusted, but a value outside the enum would be
|
||||||
|
// illegal to `@enumFromInt` directly).
|
||||||
|
fn kindFromWire(value: u32) Kind {
|
||||||
|
return switch (value) {
|
||||||
|
@intFromEnum(Kind.directory) => .directory,
|
||||||
|
@intFromEnum(Kind.character_device) => .character_device,
|
||||||
|
@intFromEnum(Kind.block_device) => .block_device,
|
||||||
|
@intFromEnum(Kind.symbolic_link) => .symbolic_link,
|
||||||
|
@intFromEnum(Kind.fifo) => .fifo,
|
||||||
|
@intFromEnum(Kind.socket) => .socket,
|
||||||
|
else => .regular,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to open a path.
|
||||||
|
pub const OpenOptions = struct {
|
||||||
|
/// Create the file if it does not exist.
|
||||||
|
create: bool = false,
|
||||||
|
/// Open a directory node (for listing) rather than a file.
|
||||||
|
directory: bool = false,
|
||||||
|
/// Truncate an existing file to zero length on open (O_TRUNC) — replace its
|
||||||
|
/// contents rather than overwriting in place.
|
||||||
|
truncate: bool = false,
|
||||||
|
|
||||||
|
fn wireFlags(self: OpenOptions) u32 {
|
||||||
|
var f: u32 = 0;
|
||||||
|
if (self.create) f |= protocol.create;
|
||||||
|
if (self.directory) f |= protocol.directory;
|
||||||
|
if (self.truncate) f |= protocol.truncate;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The VFS server endpoint, looked up once by well-known id and cached.
|
||||||
|
var vfs_handle: ipc.Handle = 0;
|
||||||
|
var vfs_resolved = false;
|
||||||
|
fn vfs() ?ipc.Handle {
|
||||||
|
if (!vfs_resolved) {
|
||||||
|
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
||||||
|
vfs_resolved = true;
|
||||||
|
}
|
||||||
|
return vfs_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
||||||
|
|
||||||
|
// One request/reply round trip: [Request header][send payload] -> VFS ->
|
||||||
|
// [Reply header][receive payload]. The receive payload lands in `out`.
|
||||||
|
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||||
|
const h = vfs() orelse return null;
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const slen = @min(send.len, protocol.maximum_payload);
|
||||||
|
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
||||||
|
|
||||||
|
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
||||||
|
if (n < protocol.reply_size) return null;
|
||||||
|
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
||||||
|
const rpl = @min(n - protocol.reply_size, out.len);
|
||||||
|
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
||||||
|
return .{ .reply = reply, .payload = out[0..rpl] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
|
||||||
|
pub const File = struct {
|
||||||
|
node: u64,
|
||||||
|
offset: u64 = 0,
|
||||||
|
|
||||||
|
/// Read up to `buffer.len` bytes at the current offset; returns the count, or
|
||||||
|
/// null on error.
|
||||||
|
pub fn read(self: *File, buffer: []u8) ?usize {
|
||||||
|
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
||||||
|
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||||
|
const r = transact(request, &.{}, buffer) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
self.offset += r.reply.len;
|
||||||
|
return r.reply.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `data` at the current offset; returns the count written. A single
|
||||||
|
/// call is capped at the VFS payload size, so the return may be short — use
|
||||||
|
/// `writeAll` to write the whole slice. Null on error.
|
||||||
|
pub fn write(self: *File, data: []const u8) ?usize {
|
||||||
|
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
||||||
|
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||||
|
const r = transact(request, data[0..want], &.{}) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
self.offset += r.reply.len;
|
||||||
|
return r.reply.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all of `data`, looping past the per-call payload cap. Returns the
|
||||||
|
/// total written, or null if a write failed before any progress.
|
||||||
|
pub fn writeAll(self: *File, data: []const u8) ?usize {
|
||||||
|
var written: usize = 0;
|
||||||
|
while (written < data.len) {
|
||||||
|
const n = self.write(data[written..]) orelse return if (written == 0) null else written;
|
||||||
|
if (n == 0) return written; // no forward progress; stop rather than spin
|
||||||
|
written += n;
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the read/write cursor to an absolute byte position.
|
||||||
|
pub fn seekTo(self: *File, position: u64) void {
|
||||||
|
self.offset = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This file's metadata.
|
||||||
|
pub fn attributes(self: *File) ?Attributes {
|
||||||
|
const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
|
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||||
|
const r = transact(request, &.{}, &buffer) orelse return null;
|
||||||
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
|
||||||
|
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
|
||||||
|
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the VFS's open handle for this file.
|
||||||
|
pub fn close(self: *File) void {
|
||||||
|
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
|
_ = transact(request, &.{}, &.{});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open (or create, with `.create`) `path`. Returns the open file, or null.
|
||||||
|
pub fn open(path: []const u8, options: OpenOptions) ?File {
|
||||||
|
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() };
|
||||||
|
const r = transact(request, path, &.{}) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
return .{ .node = r.reply.node };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A path's metadata without keeping it open (open -> status -> close).
|
||||||
|
pub fn attributes(path: []const u8) ?Attributes {
|
||||||
|
var file = open(path, .{}) orelse return null;
|
||||||
|
defer file.close();
|
||||||
|
return file.attributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount
|
||||||
|
/// to come up before writing to it).
|
||||||
|
pub fn exists(path: []const u8) bool {
|
||||||
|
return attributes(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry returned by `Directory.next`.
|
||||||
|
pub const Entry = struct {
|
||||||
|
kind: Kind = .regular,
|
||||||
|
size: u64 = 0,
|
||||||
|
name_buffer: [64]u8 = undefined,
|
||||||
|
name_len: usize = 0,
|
||||||
|
|
||||||
|
pub fn name(self: *const Entry) []const u8 {
|
||||||
|
return self.name_buffer[0..self.name_len];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An open directory being listed, cursor-advanced by `next`.
|
||||||
|
pub const Directory = struct {
|
||||||
|
node: u64,
|
||||||
|
cursor: u64 = 0,
|
||||||
|
|
||||||
|
/// Fill `entry` with the next directory entry; false at end of directory or
|
||||||
|
/// on error.
|
||||||
|
pub fn next(self: *Directory, entry: *Entry) bool {
|
||||||
|
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .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 = kindFromWire(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;
|
||||||
|
self.cursor += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the VFS's open handle for this directory.
|
||||||
|
pub fn close(self: *Directory) void {
|
||||||
|
var f = File{ .node = self.node };
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open `path` as a directory for listing. Returns null if it isn't one / on error.
|
||||||
|
pub fn openDirectory(path: []const u8) ?Directory {
|
||||||
|
const file = open(path, .{ .directory = true }) orelse return null;
|
||||||
|
return .{ .node = file.node };
|
||||||
|
}
|
||||||
|
|
||||||
|
// A path-based request that returns only a status (mkdir, unlink).
|
||||||
|
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
|
||||||
|
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 };
|
||||||
|
const r = transact(request, path, &.{}) orelse return false;
|
||||||
|
return r.reply.status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a directory at `path` (its parent must already exist). Returns true on
|
||||||
|
/// success. Only works under a mounted filesystem that supports directories.
|
||||||
|
pub fn makeDirectory(path: []const u8) bool {
|
||||||
|
return pathOperation(.mkdir, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the file at `path`. Returns true on success. Directories are refused
|
||||||
|
/// (a separate directory-removal would have to check emptiness).
|
||||||
|
pub fn remove(path: []const u8) bool {
|
||||||
|
return pathOperation(.unlink, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rename `old_path` to `new_path`. Both must be in the same directory (same-
|
||||||
|
/// directory, 8.3-name rename only for now). Returns true on success.
|
||||||
|
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||||
|
const total = old_path.len + 1 + new_path.len;
|
||||||
|
if (total > protocol.maximum_payload) return false;
|
||||||
|
var payload: [protocol.maximum_payload]u8 = undefined;
|
||||||
|
@memcpy(payload[0..old_path.len], old_path);
|
||||||
|
payload[old_path.len] = 0;
|
||||||
|
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
||||||
|
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||||
|
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
||||||
|
return r.reply.status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||||
|
/// the VFS then routes everything under `target` to that backend. This is the one
|
||||||
|
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
||||||
|
/// success.
|
||||||
|
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
|
||||||
|
const h = vfs() orelse return false;
|
||||||
|
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 false;
|
||||||
|
if (result.len < protocol.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
|
||||||
|
}
|
||||||
@@ -38,6 +38,19 @@ 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");
|
||||||
|
|
||||||
|
/// The danos-native file API (open/read/write/list over the user-space VFS) — the
|
||||||
|
/// layer danos programs use directly, and where the operations that later become
|
||||||
|
/// `std.os.danos` are staged. See docs/zig-self-hosting.md.
|
||||||
|
pub const fs = @import("fs.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;
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,24 @@ pub fn clock() u64 {
|
|||||||
return @intCast(sc.systemCall0(.clock));
|
return @intCast(sc.systemCall0(.clock));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wall-clock time in Unix epoch seconds (UTC) — the real date/time, from the RTC.
|
||||||
|
/// Unlike `clock` (monotonic since boot), this tracks calendar time, so it is what a
|
||||||
|
/// filesystem stamps as a file's modification time. Formatting it into a calendar
|
||||||
|
/// date/timezone is user-space policy layered on top.
|
||||||
|
pub fn wallClock() u64 {
|
||||||
|
return @intCast(sc.systemCall0(.wall_clock));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
|
||||||
|
/// stream of everything `write` (and the kernel itself) has emitted — starting at
|
||||||
|
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
||||||
|
/// A program reads the whole log by looping from offset 0, advancing by the return
|
||||||
|
/// value, until it gets 0. This is how the boot log is persisted to disk on a
|
||||||
|
/// headless/real machine where serial output is otherwise lost.
|
||||||
|
pub fn klogRead(offset: usize, out: []u8) usize {
|
||||||
|
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
||||||
|
}
|
||||||
|
|
||||||
/// End the process. Never returns.
|
/// End the process. Never returns.
|
||||||
pub fn exit(code: usize) noreturn {
|
pub fn exit(code: usize) noreturn {
|
||||||
_ = sc.systemCall1(.exit, code);
|
_ = sc.systemCall1(.exit, code);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ pub const SystemCall = enum(u64) {
|
|||||||
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||||
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||||
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||||
|
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
|
||||||
|
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,6 +180,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
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(¤t.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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -469,6 +469,95 @@ pub fn clockHz() u64 {
|
|||||||
return apic.tscHz();
|
return apic.tscHz();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- real-time clock (CMOS) --------------------------------------------------
|
||||||
|
//
|
||||||
|
// The battery-backed CMOS clock, read once at boot and thereafter anchored to the
|
||||||
|
// monotonic clock (see kernel/wall-clock.zig) — so this is never on a hot path and
|
||||||
|
// needs no lock. Wall-clock *seconds* are mechanism the kernel owns (the hardware's
|
||||||
|
// value), like the monotonic clock; calendars/timezones are policy layered on top.
|
||||||
|
|
||||||
|
fn cmosRead(register: u8) u8 {
|
||||||
|
io.outb(0x70, register);
|
||||||
|
return io.inb(0x71);
|
||||||
|
}
|
||||||
|
|
||||||
|
const RtcFields = struct { second: u8, minute: u8, hour: u8, day: u8, month: u8, year: u8 };
|
||||||
|
|
||||||
|
fn rtcRaw() RtcFields {
|
||||||
|
while (cmosRead(0x0A) & 0x80 != 0) {} // wait out any update in progress (status A bit 7)
|
||||||
|
return .{
|
||||||
|
.second = cmosRead(0x00),
|
||||||
|
.minute = cmosRead(0x02),
|
||||||
|
.hour = cmosRead(0x04),
|
||||||
|
.day = cmosRead(0x07),
|
||||||
|
.month = cmosRead(0x08),
|
||||||
|
.year = cmosRead(0x09),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bcdToBinary(v: u8) u8 {
|
||||||
|
return (v & 0x0F) + ((v >> 4) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLeapYear(y: u32) bool {
|
||||||
|
return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the CMOS real-time clock and convert it to Unix epoch seconds (UTC).
|
||||||
|
pub fn readRtcUnixSeconds() u64 {
|
||||||
|
// Read until two consecutive reads agree, so we never latch a half-updated time.
|
||||||
|
var a = rtcRaw();
|
||||||
|
while (true) {
|
||||||
|
const b = rtcRaw();
|
||||||
|
if (a.second == b.second and a.minute == b.minute and a.hour == b.hour and
|
||||||
|
a.day == b.day and a.month == b.month and a.year == b.year) break;
|
||||||
|
a = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status_b = cmosRead(0x0B);
|
||||||
|
const binary_mode = status_b & 0x04 != 0; // else BCD
|
||||||
|
const hour_24 = status_b & 0x02 != 0; // else 12-hour with a PM bit
|
||||||
|
|
||||||
|
var second = a.second;
|
||||||
|
var minute = a.minute;
|
||||||
|
var hour_field = a.hour;
|
||||||
|
var day = a.day;
|
||||||
|
var month = a.month;
|
||||||
|
var year = a.year;
|
||||||
|
if (!binary_mode) {
|
||||||
|
second = bcdToBinary(second);
|
||||||
|
minute = bcdToBinary(minute);
|
||||||
|
hour_field = bcdToBinary(hour_field & 0x7F) | (hour_field & 0x80); // preserve the PM bit
|
||||||
|
day = bcdToBinary(day);
|
||||||
|
month = bcdToBinary(month);
|
||||||
|
year = bcdToBinary(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hour: u32 = hour_field & 0x7F;
|
||||||
|
if (!hour_24) {
|
||||||
|
const pm = hour_field & 0x80 != 0;
|
||||||
|
hour %= 12; // 12 AM/PM -> 0
|
||||||
|
if (pm) hour += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CMOS year is 0..99; QEMU and modern hardware mean 20xx (there is no
|
||||||
|
// reliable century register on QEMU). Treat < 70 as 20xx, else 19xx.
|
||||||
|
const full_year: u32 = if (year < 70) 2000 + @as(u32, year) else 1900 + @as(u32, year);
|
||||||
|
|
||||||
|
var days: u64 = 0;
|
||||||
|
var y: u32 = 1970;
|
||||||
|
while (y < full_year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
|
||||||
|
const month_lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||||
|
var m: u8 = 1;
|
||||||
|
while (m < month) : (m += 1) {
|
||||||
|
days += month_lengths[m - 1];
|
||||||
|
if (m == 2 and isLeapYear(full_year)) days += 1;
|
||||||
|
}
|
||||||
|
days += @as(u64, day) - 1;
|
||||||
|
|
||||||
|
return ((days * 24 + hour) * 60 + minute) * 60 + second;
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
||||||
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
||||||
/// used as the clocksource.
|
/// used as the clocksource.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const parameters = @import("parameters");
|
|||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const console = @import("console.zig");
|
const console = @import("console.zig");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
const scheduler = @import("scheduler.zig");
|
const scheduler = @import("scheduler.zig");
|
||||||
@@ -62,6 +63,10 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
architecture.serialInit();
|
architecture.serialInit();
|
||||||
log.addSink(architecture.serialWrite);
|
log.addSink(architecture.serialWrite);
|
||||||
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
||||||
|
// Retain the whole stream in a RAM buffer too, so a user program can later
|
||||||
|
// read it back (klog_read) and persist the boot log to disk — the only way to
|
||||||
|
// see it on a headless/real machine with no host capturing serial.
|
||||||
|
log.addSink(log.ramSink);
|
||||||
|
|
||||||
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
|
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
|
||||||
// surface — a bootstrap text console today, a graphics device driver later — so
|
// surface — a bootstrap text console today, a graphics device driver later — so
|
||||||
@@ -281,6 +286,10 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
if (!architecture.clockSynchronized())
|
if (!architecture.clockSynchronized())
|
||||||
log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");
|
log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");
|
||||||
|
|
||||||
|
// Anchor wall-clock time: read the RTC once, now the monotonic clock is final.
|
||||||
|
wall_clock.init();
|
||||||
|
log.print("/system/kernel: wall clock {d} (Unix epoch seconds, UTC, from the RTC)\n", .{wall_clock.nowSeconds()});
|
||||||
|
|
||||||
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||||
// Normal builds fall through to the idle halt.
|
// Normal builds fall through to the idle halt.
|
||||||
if (build_options.test_case) |case| {
|
if (build_options.test_case) |case| {
|
||||||
|
|||||||
@@ -41,6 +41,39 @@ pub fn write(bytes: []const u8) void {
|
|||||||
for (sinks[0..sink_count]) |sink| sink(bytes);
|
for (sinks[0..sink_count]) |sink| sink(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the RAM sink: a retained copy of the whole diagnostic stream ------------
|
||||||
|
//
|
||||||
|
// A fixed in-image buffer that accumulates every logged byte, so a user program
|
||||||
|
// (`log-flush`, and init at shutdown) can read it back through `klog_read` and
|
||||||
|
// persist it to a file — the boot log survives on a headless/real machine that
|
||||||
|
// has no host capturing serial. It is a *sink like any other*: register it with
|
||||||
|
// `addSink(ramSink)` at boot. No allocation (works pre-heap and in a panic).
|
||||||
|
//
|
||||||
|
// It fills linearly and stops when full: the earliest output — the most valuable
|
||||||
|
// for diagnosing a boot — is kept, and the tail is still on the live serial sink.
|
||||||
|
// 256 KiB comfortably holds a full boot plus a long run (a boot is ~15 KiB).
|
||||||
|
|
||||||
|
const ram_capacity = 256 * 1024;
|
||||||
|
var ram_buffer: [ram_capacity]u8 = undefined;
|
||||||
|
var ram_len: usize = 0;
|
||||||
|
|
||||||
|
/// The RAM sink. Best-effort and self-guarding like every sink: appends what fits
|
||||||
|
/// and silently drops the rest once full. (Concurrency matches the other sinks —
|
||||||
|
/// the dominant writer, debug_write, already holds the kernel lock; a rare torn
|
||||||
|
/// append on a kernel-internal line is an accepted diagnostic imperfection.)
|
||||||
|
pub fn ramSink(bytes: []const u8) void {
|
||||||
|
const n = @min(ram_buffer.len - ram_len, bytes.len);
|
||||||
|
if (n != 0) {
|
||||||
|
@memcpy(ram_buffer[ram_len..][0..n], bytes[0..n]);
|
||||||
|
ram_len += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The accumulated log so far — what `klog_read` copies out.
|
||||||
|
pub fn ramSnapshot() []const u8 {
|
||||||
|
return ram_buffer[0..ram_len];
|
||||||
|
}
|
||||||
|
|
||||||
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
|
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
|
||||||
/// this is safe to call from interrupt context and from a panic.
|
/// this is safe to call from interrupt context and from a panic.
|
||||||
pub fn print(comptime fmt: []const u8, args: anytype) void {
|
pub fn print(comptime fmt: []const u8, args: anytype) void {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const devices_broker = @import("devices-broker.zig");
|
|||||||
const irq = @import("irq.zig");
|
const irq = @import("irq.zig");
|
||||||
const initial_ramdisk = @import("initial-ramdisk");
|
const initial_ramdisk = @import("initial-ramdisk");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
|
|
||||||
const page_size = abi.page_size;
|
const page_size = abi.page_size;
|
||||||
const SystemCall = abi.SystemCall;
|
const SystemCall = abi.SystemCall;
|
||||||
@@ -206,6 +207,8 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.signal_bind => systemSignalBind(state),
|
.signal_bind => systemSignalBind(state),
|
||||||
.process_signal => systemProcessSignal(state),
|
.process_signal => systemProcessSignal(state),
|
||||||
.timer_bind => systemTimerBind(state),
|
.timer_bind => systemTimerBind(state),
|
||||||
|
.klog_read => systemKlogRead(state),
|
||||||
|
.wall_clock => systemWallClock(state),
|
||||||
_ => fail(state),
|
_ => fail(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -974,6 +977,37 @@ fn systemDebugWrite(state: *architecture.CpuState) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// klog_read(offset, ptr, len) -> bytes copied: copy the kernel's in-memory
|
||||||
|
/// diagnostic log (the RAM sink in log.zig) out to the user buffer at `ptr`,
|
||||||
|
/// starting at `offset`. Returns the count copied — 0 once `offset` reaches the
|
||||||
|
/// end — so a program reads the whole log by looping from 0 until it gets 0.
|
||||||
|
///
|
||||||
|
/// The mirror of `debug_write`: the same overflow-safe user-half bounds check,
|
||||||
|
/// but the copy runs kernel -> user. Written under the kernel lock so the source
|
||||||
|
/// snapshot can't grow underneath the copy. A read-only diagnostic — it exposes
|
||||||
|
/// only the log the kernel already broadcasts to serial, nothing else.
|
||||||
|
fn systemKlogRead(state: *architecture.CpuState) void {
|
||||||
|
const offset = architecture.systemCallArg(state, 0);
|
||||||
|
const ptr = architecture.systemCallArg(state, 1);
|
||||||
|
const len = architecture.systemCallArg(state, 2);
|
||||||
|
// Confine the whole destination span to the user (low) half. `len <=
|
||||||
|
// user_half_end - ptr` bounds the length without an overflowing add.
|
||||||
|
if (ptr < user_half_end and len <= user_half_end - ptr) {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
const snapshot = log.ramSnapshot();
|
||||||
|
var n: usize = 0;
|
||||||
|
if (offset < snapshot.len) {
|
||||||
|
n = @min(len, snapshot.len - offset);
|
||||||
|
const dest: [*]u8 = @ptrFromInt(ptr);
|
||||||
|
@memcpy(dest[0..n], snapshot[offset..][0..n]);
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, n);
|
||||||
|
} else {
|
||||||
|
fail(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
||||||
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
||||||
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
||||||
@@ -1304,3 +1338,10 @@ pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []c
|
|||||||
fn systemClock(state: *architecture.CpuState) void {
|
fn systemClock(state: *architecture.CpuState) void {
|
||||||
architecture.setSystemCallResult(state, architecture.nanos());
|
architecture.setSystemCallResult(state, architecture.nanos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// wall_clock() -> Unix epoch seconds (UTC). The RTC value, read at boot and offset
|
||||||
|
/// by the monotonic clock (wall-clock.zig) — mechanism, not policy: calendars and
|
||||||
|
/// timezones layer on top in user space. Needed for filesystem timestamps (mtime).
|
||||||
|
fn systemWallClock(state: *architecture.CpuState) void {
|
||||||
|
architecture.setSystemCallResult(state, wall_clock.nowSeconds());
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const boot_handoff = @import("boot-handoff");
|
|||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
const device_abi = @import("device-abi");
|
const device_abi = @import("device-abi");
|
||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
const devices_broker = @import("devices-broker.zig");
|
const devices_broker = @import("devices-broker.zig");
|
||||||
const platform = @import("platform");
|
const platform = @import("platform");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
@@ -66,6 +67,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
timer();
|
timer();
|
||||||
} else if (eql(case, "clock")) {
|
} else if (eql(case, "clock")) {
|
||||||
clock();
|
clock();
|
||||||
|
} else if (eql(case, "wall-clock")) {
|
||||||
|
wallClock();
|
||||||
} else if (eql(case, "vmm")) {
|
} else if (eql(case, "vmm")) {
|
||||||
vmm();
|
vmm();
|
||||||
} else if (eql(case, "heap")) {
|
} else if (eql(case, "heap")) {
|
||||||
@@ -144,6 +147,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")) {
|
||||||
@@ -452,6 +461,17 @@ fn heapTest() void {
|
|||||||
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
|
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
|
||||||
/// advances with real ticks, and — the point of the TSC clock — nanosecond
|
/// advances with real ticks, and — the point of the TSC clock — nanosecond
|
||||||
/// resolution far finer than the 1 ms tick, with the unit functions consistent.
|
/// resolution far finer than the 1 ms tick, with the unit functions consistent.
|
||||||
|
fn wallClock() void {
|
||||||
|
log("DANOS-TEST-BEGIN: wall-clock\n", .{});
|
||||||
|
// The RTC was read and anchored at boot (kmain -> wall_clock.init()).
|
||||||
|
const seconds = wall_clock.nowSeconds();
|
||||||
|
log(" epoch: {d}\n", .{seconds});
|
||||||
|
// A plausible current wall-clock: after 2020-01-01 (1577836800) and before 2050
|
||||||
|
// (2524608000) — catches a broken CMOS read or a wrong epoch conversion.
|
||||||
|
check("wall clock reads a plausible current epoch", seconds > 1_577_836_800 and seconds < 2_524_608_000);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
fn clock() void {
|
fn clock() void {
|
||||||
log("DANOS-TEST-BEGIN: clock\n", .{});
|
log("DANOS-TEST-BEGIN: clock\n", .{});
|
||||||
|
|
||||||
@@ -1968,6 +1988,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) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! Wall-clock time: the CMOS real-time clock read once at boot and anchored to the
|
||||||
|
//! monotonic clock, so a query is a cheap arithmetic offset — no per-call CMOS poll,
|
||||||
|
//! no lock, no SMP hazard on the shared 0x70/0x71 ports.
|
||||||
|
//!
|
||||||
|
//! Wall-clock *seconds* are mechanism the kernel owns, exactly like the monotonic
|
||||||
|
//! clock ([[time-architecture]]): reading the hardware's value is not policy.
|
||||||
|
//! Calendars, timezones, and formatting layer on top in user space. It exists so the
|
||||||
|
//! filesystem can stamp real timestamps (mtime) — see docs/zig-self-hosting.md.
|
||||||
|
|
||||||
|
const architecture = @import("architecture");
|
||||||
|
|
||||||
|
var boot_unix_seconds: u64 = 0;
|
||||||
|
var boot_nanos: u64 = 0;
|
||||||
|
|
||||||
|
/// Read the RTC once and anchor it to the monotonic clock. Call at boot, after the
|
||||||
|
/// monotonic clock is calibrated.
|
||||||
|
pub fn init() void {
|
||||||
|
boot_unix_seconds = architecture.readRtcUnixSeconds();
|
||||||
|
boot_nanos = architecture.nanos();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current wall-clock time in Unix epoch seconds (UTC): the boot RTC value plus
|
||||||
|
/// the monotonic time elapsed since. Zero until `init` runs.
|
||||||
|
pub fn nowSeconds() u64 {
|
||||||
|
return boot_unix_seconds + (architecture.nanos() -% boot_nanos) / 1_000_000_000;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
//! 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");
|
||||||
|
const fs = runtime.fs;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 opened: ?fs.Directory = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (opened == null and tries < 1400) : (tries += 1) {
|
||||||
|
opened = fs.openDirectory("/mnt/usb");
|
||||||
|
if (opened == null) runtime.system.sleep(50);
|
||||||
|
}
|
||||||
|
var dir = opened orelse {
|
||||||
|
_ = runtime.system.write("fat-test: /mnt/usb never became available\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
var count: u32 = 0;
|
||||||
|
var entry: fs.Entry = .{};
|
||||||
|
while (dir.next(&entry)) {
|
||||||
|
writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), @intFromEnum(entry.kind), entry.size });
|
||||||
|
count += 1;
|
||||||
|
if (count > 32) break;
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
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.
|
||||||
|
if (fs.open("/mnt/usb/system/kernel", .{})) |opened_file| {
|
||||||
|
var file = opened_file;
|
||||||
|
var magic: [4]u8 = undefined;
|
||||||
|
const n = file.read(&magic) orelse 0;
|
||||||
|
file.close();
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise directory + file mutation through the mount: mkdir, create a file
|
||||||
|
// inside it, read it back, then remove it — proof mkdir/unlink reach the engine.
|
||||||
|
if (fs.makeDirectory("/mnt/usb/TESTDIR")) {
|
||||||
|
var wrote = false;
|
||||||
|
if (fs.open("/mnt/usb/TESTDIR/HELLO.TXT", .{ .create = true, .truncate = true })) |created| {
|
||||||
|
var f = created;
|
||||||
|
wrote = (f.writeAll("mutation-ok") orelse 0) == "mutation-ok".len;
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
// The created file carries a real modification time (stamped from the RTC).
|
||||||
|
var mtime_ok = false;
|
||||||
|
if (fs.attributes("/mnt/usb/TESTDIR/HELLO.TXT")) |attrs| {
|
||||||
|
writeLine("fat-test: mtime {d}\n", .{attrs.mtime});
|
||||||
|
mtime_ok = attrs.mtime > 1_577_836_800; // after 2020-01-01
|
||||||
|
}
|
||||||
|
if (mtime_ok) _ = runtime.system.write("fat-test: mtime ok\n");
|
||||||
|
|
||||||
|
// Rename it, then read from the new name and confirm the old name is gone.
|
||||||
|
const renamed = fs.rename("/mnt/usb/TESTDIR/HELLO.TXT", "/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||||
|
const old_gone = !fs.exists("/mnt/usb/TESTDIR/HELLO.TXT");
|
||||||
|
if (renamed and old_gone) _ = runtime.system.write("fat-test: rename ok\n");
|
||||||
|
var readback = false;
|
||||||
|
if (fs.open("/mnt/usb/TESTDIR/RENAMED.TXT", .{})) |reopened| {
|
||||||
|
var f = reopened;
|
||||||
|
var buf: [16]u8 = undefined;
|
||||||
|
const got = f.read(&buf) orelse 0;
|
||||||
|
f.close();
|
||||||
|
readback = std.mem.eql(u8, buf[0..got], "mutation-ok");
|
||||||
|
}
|
||||||
|
const removed = fs.remove("/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||||
|
const gone = !fs.exists("/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||||
|
if (wrote and mtime_ok and renamed and old_gone and readback and removed and gone) {
|
||||||
|
_ = runtime.system.write("fat-test: mutations ok\n");
|
||||||
|
} else {
|
||||||
|
writeLine("fat-test: mutations FAILED (wrote={} mtime={} renamed={} oldgone={} read={} removed={} gone={})\n", .{ wrote, mtime_ok, renamed, old_gone, readback, removed, gone });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = runtime.system.write("fat-test: mkdir /mnt/usb/TESTDIR failed\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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
//! 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 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 (runtime.fs.mount(mount_point, endpoint)) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParentLeaf = struct { parent: []const u8, leaf: []const u8 };
|
||||||
|
|
||||||
|
// Split a path into its parent directory and final component: "/a/b" -> ("/a",
|
||||||
|
// "b"); "/b" -> ("/", "b"); "b" -> ("/", "b").
|
||||||
|
fn splitParent(path: []const u8) ParentLeaf {
|
||||||
|
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
|
||||||
|
return .{
|
||||||
|
.parent = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/",
|
||||||
|
.leaf = if (slash) |s| path[s + 1 ..] else path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
|
||||||
|
var node = filesystem.resolve(path);
|
||||||
|
if (node == null and flags & protocol.create != 0) {
|
||||||
|
const split = splitParent(path);
|
||||||
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
||||||
|
node = filesystem.createFile(parent, split.leaf);
|
||||||
|
}
|
||||||
|
var resolved = node orelse return fail(out);
|
||||||
|
// O_TRUNC: replace an existing file's contents rather than overwriting in place
|
||||||
|
// (frees the old chain, so a shorter rewrite leaves no stale tail).
|
||||||
|
if (flags & protocol.truncate != 0 and !resolved.is_directory) {
|
||||||
|
filesystem.truncate(&resolved);
|
||||||
|
}
|
||||||
|
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..];
|
||||||
|
|
||||||
|
// Stamp create/write with the current wall-clock time (mtime). Cheap, and it
|
||||||
|
// keeps the engine pure (it takes the time as data, not a syscall).
|
||||||
|
filesystem.current_time_epoch = runtime.system.wallClock();
|
||||||
|
|
||||||
|
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), .mtime = o.node.mtime };
|
||||||
|
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 }, &.{});
|
||||||
|
},
|
||||||
|
.mkdir => {
|
||||||
|
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
||||||
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
||||||
|
if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
|
||||||
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
|
},
|
||||||
|
.unlink => {
|
||||||
|
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
||||||
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
||||||
|
if (!filesystem.removeFile(parent, split.leaf)) return fail(out);
|
||||||
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
|
},
|
||||||
|
.rename => {
|
||||||
|
const both = payload[0..@min(payload.len, request.len)];
|
||||||
|
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
||||||
|
const old_split = splitParent(both[0..sep]);
|
||||||
|
const new_split = splitParent(both[sep + 1 ..]);
|
||||||
|
// Same-directory rename only.
|
||||||
|
if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out);
|
||||||
|
const parent = filesystem.resolve(old_split.parent) orelse return fail(out);
|
||||||
|
if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out);
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
//! 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DOS date/time <-> Unix epoch --------------------------------------------
|
||||||
|
//
|
||||||
|
// FAT stamps a file's modification time as two 16-bit DOS fields. There is no
|
||||||
|
// timezone, so danos treats them as UTC. `date`: year-1980(7)|month(4)|day(5);
|
||||||
|
// `time`: hour(5)|minute(6)|(second/2)(5).
|
||||||
|
|
||||||
|
fn isLeapYear(year: u32) bool {
|
||||||
|
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const days_in_month = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||||
|
|
||||||
|
/// Convert a FAT date+time to Unix epoch seconds (UTC). Returns 0 for an unset
|
||||||
|
/// (zero) date.
|
||||||
|
pub fn fatToEpoch(date: u16, time: u16) u64 {
|
||||||
|
if (date == 0) return 0;
|
||||||
|
const day: u32 = date & 0x1F;
|
||||||
|
const month: u32 = (date >> 5) & 0x0F;
|
||||||
|
const year: u32 = 1980 + (date >> 9);
|
||||||
|
if (month < 1 or month > 12 or day < 1) return 0;
|
||||||
|
const second: u32 = @as(u32, time & 0x1F) * 2;
|
||||||
|
const minute: u32 = (time >> 5) & 0x3F;
|
||||||
|
const hour: u32 = (time >> 11) & 0x1F;
|
||||||
|
|
||||||
|
var days: u64 = 0;
|
||||||
|
var y: u32 = 1970;
|
||||||
|
while (y < year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
|
||||||
|
var m: u32 = 1;
|
||||||
|
while (m < month) : (m += 1) {
|
||||||
|
days += days_in_month[m - 1];
|
||||||
|
if (m == 2 and isLeapYear(year)) days += 1;
|
||||||
|
}
|
||||||
|
days += day - 1;
|
||||||
|
return ((days * 24 + hour) * 60 + minute) * 60 + second;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FatDateTime = struct { date: u16, time: u16 };
|
||||||
|
|
||||||
|
/// Convert Unix epoch seconds (UTC) to a FAT date+time. Returns {0,0} for epoch 0 or
|
||||||
|
/// any time before 1980 (which DOS cannot represent).
|
||||||
|
pub fn epochToFatDateTime(epoch: u64) FatDateTime {
|
||||||
|
if (epoch == 0) return .{ .date = 0, .time = 0 };
|
||||||
|
var remaining = epoch;
|
||||||
|
const second: u32 = @intCast(remaining % 60);
|
||||||
|
remaining /= 60;
|
||||||
|
const minute: u32 = @intCast(remaining % 60);
|
||||||
|
remaining /= 60;
|
||||||
|
const hour: u32 = @intCast(remaining % 24);
|
||||||
|
remaining /= 24;
|
||||||
|
var days: u32 = @intCast(remaining); // whole days since 1970-01-01
|
||||||
|
|
||||||
|
var year: u32 = 1970;
|
||||||
|
while (true) {
|
||||||
|
const y_days: u32 = if (isLeapYear(year)) 366 else 365;
|
||||||
|
if (days < y_days) break;
|
||||||
|
days -= y_days;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
if (year < 1980) return .{ .date = 0, .time = 0 };
|
||||||
|
var month: u32 = 1;
|
||||||
|
while (true) {
|
||||||
|
var m_days: u32 = days_in_month[month - 1];
|
||||||
|
if (month == 2 and isLeapYear(year)) m_days += 1;
|
||||||
|
if (days < m_days) break;
|
||||||
|
days -= m_days;
|
||||||
|
month += 1;
|
||||||
|
}
|
||||||
|
const day = days + 1;
|
||||||
|
return .{
|
||||||
|
.date = @intCast(((year - 1980) << 9) | (month << 5) | day),
|
||||||
|
.time = @intCast((hour << 11) | (minute << 5) | (second / 2)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "FAT date/time <-> Unix epoch round trip" {
|
||||||
|
// Even-second UTC times (FAT stores seconds/2, so even seconds round-trip exactly).
|
||||||
|
for ([_]u64{ 1_577_836_800, 1_700_000_000, 1_262_304_000, 1_783_971_244 }) |epoch| {
|
||||||
|
const fat = epochToFatDateTime(epoch);
|
||||||
|
try std.testing.expectEqual(epoch, fatToEpoch(fat.date, fat.time));
|
||||||
|
}
|
||||||
|
// Absolute check: 1577836800 is 2020-01-01 00:00:00 UTC.
|
||||||
|
const y2020 = epochToFatDateTime(1_577_836_800);
|
||||||
|
try std.testing.expectEqual(@as(u16, 2020), 1980 + (y2020.date >> 9));
|
||||||
|
try std.testing.expectEqual(@as(u16, 1), (y2020.date >> 5) & 0x0F); // month
|
||||||
|
try std.testing.expectEqual(@as(u16, 1), y2020.date & 0x1F); // day
|
||||||
|
// 0 is "unset" both ways.
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), fatToEpoch(0, 0));
|
||||||
|
try std.testing.expectEqual(@as(u16, 0), epochToFatDateTime(0).date);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
@@ -21,11 +21,16 @@ const std = @import("std");
|
|||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
const power = runtime.power_protocol;
|
const power = runtime.power_protocol;
|
||||||
|
|
||||||
|
/// Where the kernel boot log is persisted on the USB FAT volume — an 8.3 name at
|
||||||
|
/// the mount root (see system/services/log-flush). init writes it at shutdown;
|
||||||
|
/// the log-flush one-shot writes it once at boot.
|
||||||
|
const log_path = "/mnt/usb/DANOS.LOG";
|
||||||
|
|
||||||
/// The system services init brings up at boot, in order. This is init's policy — the
|
/// The system services init brings up at boot, in order. This is init's policy — the
|
||||||
/// 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;
|
||||||
@@ -65,6 +70,14 @@ pub fn main() void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Once the storage stack is up, a one-shot copies the boot log to the USB
|
||||||
|
// volume (/mnt/usb/DANOS.LOG) so it can be read on another machine — the only
|
||||||
|
// way to see it on a headless/real board with no host capturing serial. Fire
|
||||||
|
// and forget: it polls for the mount itself, and is deliberately NOT one of
|
||||||
|
// init's supervised children (a transient one-shot must not be stopped-and-
|
||||||
|
// waited-for during shutdown).
|
||||||
|
_ = runtime.system.spawn("log-flush");
|
||||||
|
|
||||||
// Subscribe to power events (retry: the power service registers well after
|
// Subscribe to power events (retry: the power service registers well after
|
||||||
// init starts). Best-effort — without it, a `terminate` signal still
|
// init starts). Best-effort — without it, a `terminate` signal still
|
||||||
// triggers the same shutdown path.
|
// triggers the same shutdown path.
|
||||||
@@ -115,11 +128,36 @@ fn subscribePower() void {
|
|||||||
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The stop sequence: terminate each child in reverse spawn order (vfs last —
|
/// Copy the whole kernel log to /mnt/usb/DANOS.LOG (the same file log-flush
|
||||||
/// other services may flush through it), waiting up to a deadline for each to
|
/// writes at boot), so a poweroff captures the fullest log. Best-effort: if the
|
||||||
/// exit before killing it, then ask the power service to enter S5.
|
/// USB volume is not mounted, the open fails and it does nothing. Must run while
|
||||||
|
/// the storage services are still alive (see shutDown).
|
||||||
|
fn flushKernelLog() void {
|
||||||
|
// Truncate on open so this fuller flush replaces the boot-time one cleanly.
|
||||||
|
var file = runtime.fs.open(log_path, .{ .create = true, .truncate = true }) orelse return; // no USB volume
|
||||||
|
defer file.close();
|
||||||
|
var chunk: [4096]u8 = undefined;
|
||||||
|
var offset: usize = 0;
|
||||||
|
while (true) {
|
||||||
|
const got = runtime.system.klogRead(offset, &chunk);
|
||||||
|
if (got == 0) break; // reached the end of the accumulated log
|
||||||
|
if (file.writeAll(chunk[0..got]) == null) break; // storage went away
|
||||||
|
offset += got;
|
||||||
|
}
|
||||||
|
var line: [96]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, "/system/services/init: flushed log to {s} ({d} bytes)\n", .{ log_path, offset }) catch "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stop sequence: persist the log while storage is still up, then terminate
|
||||||
|
/// each child in reverse spawn order (vfs last — other services may flush through
|
||||||
|
/// it), waiting up to a deadline for each to exit before killing it, then ask the
|
||||||
|
/// power service to enter S5.
|
||||||
fn shutDown() void {
|
fn shutDown() void {
|
||||||
_ = runtime.system.write("/system/services/init: shutting down\n");
|
_ = runtime.system.write("/system/services/init: shutting down\n");
|
||||||
|
// Persist the fullest log to the USB volume BEFORE tearing anything down: the
|
||||||
|
// reverse-order stop loop below kills the fat server (children[3]) first, so
|
||||||
|
// /mnt/usb must be written while it is still mounted.
|
||||||
|
flushKernelLog();
|
||||||
var i = child_count;
|
var i = child_count;
|
||||||
while (i > 0) {
|
while (i > 0) {
|
||||||
i -= 1;
|
i -= 1;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! system/services/log-flush — a one-shot that copies the kernel's in-memory
|
||||||
|
//! diagnostic log to a file on the mounted USB FAT volume, so the boot log
|
||||||
|
//! survives to be read on another machine. On a headless or real board there is
|
||||||
|
//! no host capturing serial, so without this the log is lost at power-off; this
|
||||||
|
//! is the on-disk equivalent of QEMU's `-serial file:`.
|
||||||
|
//!
|
||||||
|
//! It reads the whole kernel log back through `klog_read` (the RAM sink in
|
||||||
|
//! system/kernel/log.zig) and writes it to /mnt/usb/DANOS.LOG. The name is 8.3
|
||||||
|
//! (FAT short-name rule: base <= 8, extension <= 3) and lives at the mount root
|
||||||
|
//! (there is no mkdir on the FAT path yet). init spawns this once the boot
|
||||||
|
//! services are up; init itself repeats the flush at shutdown for a fuller log.
|
||||||
|
//!
|
||||||
|
//! If no USB volume is mounted — no stick, or the initial-ramdisk sweep that
|
||||||
|
//! spawns every bundled binary bare with no VFS — it waits briefly, then exits
|
||||||
|
//! silently, deranging no other test's output.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const fs = runtime.fs;
|
||||||
|
|
||||||
|
const log_path = "/mnt/usb/DANOS.LOG";
|
||||||
|
|
||||||
|
/// Copy the whole kernel log to the open file, looping klog_read -> write until
|
||||||
|
/// the log is exhausted. Returns the number of bytes written.
|
||||||
|
fn drainKernelLog(file: *fs.File) usize {
|
||||||
|
var chunk: [4096]u8 = undefined;
|
||||||
|
var offset: usize = 0;
|
||||||
|
while (true) {
|
||||||
|
const got = runtime.system.klogRead(offset, &chunk);
|
||||||
|
if (got == 0) break; // reached the end of the accumulated log
|
||||||
|
if (file.writeAll(chunk[0..got]) == null) break; // storage went away
|
||||||
|
offset += got;
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Wait for the fat server to mount /mnt/usb (it must bring up the whole USB
|
||||||
|
// storage chain first, so it races us at boot). Bounded: if the mount never
|
||||||
|
// appears — no volume, or the no-VFS ramdisk sweep — give up silently.
|
||||||
|
var ready = false;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (tries < 1400) : (tries += 1) {
|
||||||
|
if (fs.openDirectory("/mnt/usb")) |directory| {
|
||||||
|
var dir = directory;
|
||||||
|
dir.close();
|
||||||
|
ready = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
runtime.system.sleep(50);
|
||||||
|
}
|
||||||
|
if (!ready) return; // /mnt/usb never became available — nothing to persist to
|
||||||
|
|
||||||
|
// Truncate on open: each flush replaces the file, so a shorter log on a later
|
||||||
|
// boot of the same stick leaves no stale tail from a previous, longer one.
|
||||||
|
var file = fs.open(log_path, .{ .create = true, .truncate = true }) orelse return;
|
||||||
|
const written = drainKernelLog(&file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
var line: [96]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, "log-flush: wrote {d} bytes to {s}\n", .{ written, log_path }) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -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(""));
|
||||||
|
}
|
||||||
@@ -4,12 +4,11 @@
|
|||||||
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
|
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
|
||||||
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
|
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
|
||||||
//!
|
//!
|
||||||
//! This is a danos-native contract, so it uses danos names throughout — the POSIX
|
//! This is a danos-native contract, so it uses danos names throughout. The client
|
||||||
//! spellings (`stat`, `O_CREAT`, ...) live only in the POSIX layer
|
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
|
||||||
//! (library/posix/unistd.zig), which translates to these.
|
|
||||||
//!
|
//!
|
||||||
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
|
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
|
||||||
//! Shared by library/posix/unistd.zig (client) and system/services/vfs/vfs.zig (server).
|
//! Shared by library/runtime/fs.zig (client) and system/services/vfs/vfs.zig (server).
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
pub const Operation = enum(u32) {
|
||||||
open, // open(path) -> node id
|
open, // open(path) -> node id
|
||||||
@@ -17,8 +16,43 @@ 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)
|
||||||
|
// Appended for filesystem mutation (Phase 2). Path-based (the path is the
|
||||||
|
// payload); a mounted backend handles them, the flat ramfs refuses them.
|
||||||
|
mkdir, // mkdir(path payload) -> status
|
||||||
|
unlink, // unlink(path payload) -> status
|
||||||
|
// rename: the payload is the old path, a single 0x00 separator, then the new
|
||||||
|
// path. Same-directory rename only (the router requires both under one mount).
|
||||||
|
rename, // rename(old\0new payload) -> status
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
@@ -47,6 +81,9 @@ pub const FileStatus = extern struct {
|
|||||||
size: u64,
|
size: u64,
|
||||||
kind: u32,
|
kind: u32,
|
||||||
_padding: u32 = 0,
|
_padding: u32 = 0,
|
||||||
|
/// Modification time — Unix epoch seconds, UTC. 0 if the backend has none (the
|
||||||
|
/// flat ramfs). Filled from the FAT directory entry's write date/time.
|
||||||
|
mtime: u64 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const message_maximum: usize = 256;
|
pub const message_maximum: usize = 256;
|
||||||
@@ -55,5 +92,23 @@ pub const reply_size: usize = @sizeOf(Reply);
|
|||||||
/// Largest inline payload that still fits one IPC message alongside a header.
|
/// Largest inline payload that still fits one IPC message alongside a header.
|
||||||
pub const maximum_payload: usize = message_maximum - request_size;
|
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; `runtime.fs.OpenOptions` maps its booleans onto these).
|
||||||
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;
|
||||||
|
/// Truncate the file to zero length on open (O_TRUNC): replace its contents rather
|
||||||
|
/// than overwriting in place, so a shorter new file leaves no stale tail. A mounted
|
||||||
|
/// backend frees the old cluster chain; the flat ramfs ignores it.
|
||||||
|
pub const truncate: u32 = 4;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
//! /system/services/vfs/vfs-test — a client that proves the VFS round trip end to end: open a
|
//! /system/services/vfs/vfs-test — a client that proves the VFS round trip end to end: open a
|
||||||
//! file through the `runtime` file API, write to it, seek back, read it, and compare.
|
//! file through the `runtime.fs` file API, write to it, seek back, read it, and compare.
|
||||||
//! On success it heartbeats "vfstest: ok" so the kernel test can observe it;
|
//! On success it heartbeats "vfstest: ok" so the kernel test can observe it;
|
||||||
//! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs.
|
//! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const fs = runtime.fs;
|
||||||
|
|
||||||
pub fn main(init: runtime.process.Init) void {
|
pub fn main(init: runtime.process.Init) void {
|
||||||
const u = @import("posix").unistd;
|
|
||||||
const payload = "hello-vfs";
|
const payload = "hello-vfs";
|
||||||
|
|
||||||
// The "park" role (the vfs-client-death test): open a file, then hold the
|
// The "park" role (the vfs-client-death test): open a file, then hold the
|
||||||
// handle forever without closing — the kill and the VFS's release-on-death
|
// handle forever without closing — the kill and the VFS's release-on-death
|
||||||
// are the point.
|
// are the point.
|
||||||
if (init.arguments.count > 1) {
|
if (init.arguments.count > 1) {
|
||||||
var fd: i32 = -1;
|
var parked: ?fs.File = null;
|
||||||
var tries: u32 = 0;
|
var tries: u32 = 0;
|
||||||
while (fd < 0 and tries < 200) : (tries += 1) {
|
while (parked == null and tries < 200) : (tries += 1) {
|
||||||
fd = u.open("parked", u.O_CREAT);
|
parked = fs.open("parked", .{ .create = true });
|
||||||
if (fd < 0) runtime.system.sleep(20);
|
if (parked == null) runtime.system.sleep(20);
|
||||||
}
|
}
|
||||||
if (fd < 0) {
|
if (parked == null) {
|
||||||
_ = runtime.system.write("vfstest: park open failed\n");
|
_ = runtime.system.write("vfstest: park open failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -31,28 +31,28 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The VFS server may not have registered yet — retry open until it's up.
|
// The VFS server may not have registered yet — retry open until it's up.
|
||||||
var fd: i32 = -1;
|
var opened: ?fs.File = null;
|
||||||
var tries: u32 = 0;
|
var tries: u32 = 0;
|
||||||
while (fd < 0 and tries < 200) : (tries += 1) {
|
while (opened == null and tries < 200) : (tries += 1) {
|
||||||
fd = u.open("greeting", u.O_CREAT);
|
opened = fs.open("greeting", .{ .create = true });
|
||||||
if (fd < 0) runtime.system.sleep(20);
|
if (opened == null) runtime.system.sleep(20);
|
||||||
}
|
}
|
||||||
if (fd < 0) {
|
var greeting = opened orelse {
|
||||||
_ = runtime.system.write("vfstest: open failed\n");
|
_ = runtime.system.write("vfstest: open failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (u.write(fd, payload) != @as(isize, payload.len)) {
|
if ((greeting.write(payload) orelse 0) != payload.len) {
|
||||||
_ = runtime.system.write("vfstest: write failed\n");
|
_ = runtime.system.write("vfstest: write failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ = u.lseek(fd, 0, u.SEEK_SET);
|
greeting.seekTo(0);
|
||||||
|
|
||||||
var buffer: [32]u8 = undefined;
|
var buffer: [32]u8 = undefined;
|
||||||
const n = u.read(fd, &buffer);
|
const n = greeting.read(&buffer) orelse 0;
|
||||||
u.close(fd);
|
greeting.close();
|
||||||
|
|
||||||
if (n == @as(isize, payload.len) and std.mem.eql(u8, buffer[0..@intCast(n)], payload)) {
|
if (n == payload.len and std.mem.eql(u8, buffer[0..n], payload)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
_ = runtime.system.write("vfstest: ok\n");
|
_ = runtime.system.write("vfstest: ok\n");
|
||||||
runtime.system.sleep(1000);
|
runtime.system.sleep(1000);
|
||||||
|
|||||||
+242
-16
@@ -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,134 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forward a path-based operation (mkdir, unlink) under a mount to its backend and
|
||||||
|
/// relay the reply. No handle is created — these operate by path and return only a
|
||||||
|
/// status.
|
||||||
|
fn forwardPath(out: []u8, backend: ipc.Handle, operation: protocol.Operation, relative: []const u8) usize {
|
||||||
|
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
|
||||||
|
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);
|
||||||
|
const copy = @min(n, out.len);
|
||||||
|
@memcpy(out[0..copy], reply[0..copy]);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forward a rename to its backend: the payload is the mount-relative old path, a
|
||||||
|
/// 0x00 separator, then the mount-relative new path. Relays the backend's reply.
|
||||||
|
fn forwardRename(out: []u8, backend: ipc.Handle, old_relative: []const u8, new_relative: []const u8) usize {
|
||||||
|
const total = old_relative.len + 1 + new_relative.len;
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
if (protocol.request_size + total > message.len) return fail(out);
|
||||||
|
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
var p = protocol.request_size;
|
||||||
|
@memcpy(message[p..][0..old_relative.len], old_relative);
|
||||||
|
p += old_relative.len;
|
||||||
|
message[p] = 0;
|
||||||
|
p += 1;
|
||||||
|
@memcpy(message[p..][0..new_relative.len], new_relative);
|
||||||
|
p += new_relative.len;
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = ipc.call(backend, message[0..p], &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 +256,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 +289,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 +302,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,20 +317,58 @@ 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 }, &.{});
|
||||||
},
|
},
|
||||||
|
.mkdir, .unlink => {
|
||||||
|
const name = payload[0..@min(payload.len, request.len)];
|
||||||
|
if (longestMount(name)) |m| return forwardPath(out, m.backend, request.operation, m.relative);
|
||||||
|
// Only a mounted backend has real directories; the flat ramfs cannot
|
||||||
|
// create or remove them (and a bare-name path is not a mount target).
|
||||||
|
return fail(out);
|
||||||
|
},
|
||||||
|
.rename => {
|
||||||
|
const both = payload[0..@min(payload.len, request.len)];
|
||||||
|
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
||||||
|
const old_path = both[0..sep];
|
||||||
|
const new_path = both[sep + 1 ..];
|
||||||
|
const mo = longestMount(old_path) orelse return fail(out);
|
||||||
|
const mn = longestMount(new_path) orelse return fail(out);
|
||||||
|
// Both paths must live under the same mount — cross-filesystem rename is
|
||||||
|
// not supported.
|
||||||
|
if (mo.backend != mn.backend) return fail(out);
|
||||||
|
return forwardRename(out, mo.backend, mo.relative, mn.relative);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 +378,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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+104
-18
@@ -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",
|
||||||
@@ -100,6 +108,11 @@ CASES = [
|
|||||||
{"name": "clock",
|
{"name": "clock",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Wall-clock: the CMOS RTC read at boot gives a plausible current epoch (the
|
||||||
|
# foundation for filesystem mtime).
|
||||||
|
{"name": "wall-clock",
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
{"name": "vmm",
|
{"name": "vmm",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
@@ -274,9 +287,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 +296,69 @@ 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"},
|
||||||
|
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
||||||
|
# fat-test client, after listing, makes a directory, writes+reads a file inside
|
||||||
|
# it, then removes the file, exercising the whole VFS -> fat mutation path.
|
||||||
|
{"name": "fat-mutations",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"fat-test: mutations ok",
|
||||||
|
"fail": r"fat-test: mutations FAILED|fat-test: mkdir .* failed|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Phase 2c: rename through the mount — fat-test renames the file it created
|
||||||
|
# before removing it, and confirms the old name is gone.
|
||||||
|
{"name": "fat-rename",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"fat-test: rename ok",
|
||||||
|
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
|
||||||
|
# current wall-clock time (stamped from the RTC), read back through stat.
|
||||||
|
{"name": "fat-mtime",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"fat-test: mtime ok",
|
||||||
|
"fail": r"fat-test: mutations FAILED|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",
|
||||||
@@ -323,15 +398,26 @@ CASES = [
|
|||||||
r"init: shutting down[\s\S]*"
|
r"init: shutting down[\s\S]*"
|
||||||
r"power: entering S5",
|
r"power: entering S5",
|
||||||
"fail": r"power: S5 write did not take|DANOS-TEST-RESULT: FAIL"},
|
"fail": r"power: S5 write did not take|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M8: the boot log is persisted to the USB FAT volume. Reuses the orderly-
|
||||||
|
# shutdown build (full tree + power button): init spawns log-flush at boot,
|
||||||
|
# which copies the kernel log to /mnt/usb/DANOS.LOG once /mnt/usb is mounted
|
||||||
|
# (first marker); then the power button drives init's own pre-teardown flush
|
||||||
|
# (second marker), proving both triggers write the file while storage is up.
|
||||||
|
{"name": "log-flush",
|
||||||
|
"build_case": "orderly-shutdown",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qmp_after": {"delay": 8, "command": "system_powerdown"},
|
||||||
|
"expect": r"log-flush: wrote \d+ bytes to /mnt/usb/DANOS\.LOG[\s\S]*"
|
||||||
|
r"init: flushed log to /mnt/usb/DANOS\.LOG[\s\S]*"
|
||||||
|
r"power: entering S5",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers +
|
# M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers +
|
||||||
# reports its _HID devices — the two PS/2 nodes must appear with resources
|
# reports its _HID devices — the two PS/2 nodes must appear with resources
|
||||||
# (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/discovery.md).
|
# (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/discovery.md).
|
||||||
{"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 +434,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 +446,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 +551,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 +575,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"]
|
||||||
|
|||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user