Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77d2e22ed1 | ||
|
|
a64a01a6a9 | ||
|
|
35e8921de8 |
@@ -250,6 +250,8 @@ pub fn build(b: *std.Build) void {
|
||||
// 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).
|
||||
const power_protocol_module = b.addModule("power-protocol", .{
|
||||
@@ -392,6 +394,10 @@ pub fn build(b: *std.Build) void {
|
||||
// with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`.
|
||||
const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig");
|
||||
usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module);
|
||||
// The FAT filesystem server: mounts the block device and serves it into the VFS
|
||||
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
|
||||
const fat_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
|
||||
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
|
||||
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||
// 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.
|
||||
@@ -453,6 +459,10 @@ pub fn build(b: *std.Build) void {
|
||||
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.addFileArg(pci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("crash-test");
|
||||
@@ -487,6 +497,7 @@ pub fn build(b: *std.Build) void {
|
||||
.{ usb_hid_keyboard_exe, "system/drivers" },
|
||||
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||
.{ usb_storage_exe, "system/drivers" },
|
||||
.{ fat_exe, "system/services" },
|
||||
}) |entry| {
|
||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||
b.getInstallStep().dependOn(&step.step);
|
||||
@@ -520,6 +531,36 @@ pub fn build(b: *std.Build) void {
|
||||
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
||||
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 ---
|
||||
// Firmware lives in different places per OS/distro, so probe the known
|
||||
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
|
||||
@@ -580,10 +621,13 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
run_efi.addArg("-drive");
|
||||
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(&.{
|
||||
"-drive",
|
||||
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}),
|
||||
"-device",
|
||||
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
||||
"-net",
|
||||
"none",
|
||||
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
||||
@@ -638,6 +682,10 @@ pub fn build(b: *std.Build) void {
|
||||
"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| {
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
|
||||
@@ -146,3 +146,61 @@ pub fn close(fd: i32) void {
|
||||
_ = transact(request, &.{}, &.{});
|
||||
f.used = false;
|
||||
}
|
||||
|
||||
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||
/// the VFS then routes every path under `target` to that backend. Returns 0 or
|
||||
/// -1. This is the one call that hands the VFS a capability (the backend).
|
||||
pub fn mount(target: []const u8, backend: usize) i32 {
|
||||
const h = vfs() orelse return -1;
|
||||
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const tlen = @min(target.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
|
||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1;
|
||||
if (result.len < protocol.reply_size) return -1;
|
||||
return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1;
|
||||
}
|
||||
|
||||
/// A directory entry filled by `readdir`.
|
||||
pub const DirEntry = struct {
|
||||
kind: u32 = 0, // a protocol.NodeKind
|
||||
size: u64 = 0,
|
||||
name_buffer: [64]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
|
||||
pub fn name(self: *const DirEntry) []const u8 {
|
||||
return self.name_buffer[0..self.name_len];
|
||||
}
|
||||
};
|
||||
|
||||
/// Open a directory for reading with `readdir`. Returns an fd or -1.
|
||||
pub fn opendir(path: []const u8) i32 {
|
||||
return open(path, protocol.directory);
|
||||
}
|
||||
|
||||
/// Read the next entry of a directory fd into `entry`; returns false at EOF or on
|
||||
/// error. Advances the fd's cursor by one entry.
|
||||
pub fn readdir(fd: i32, entry: *DirEntry) bool {
|
||||
const f = fdPtr(fd) orelse return false;
|
||||
const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 };
|
||||
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||
const r = transact(request, &.{}, &buffer) orelse return false;
|
||||
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
||||
if (r.payload.len < protocol.directory_entry_size) return false;
|
||||
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
|
||||
entry.kind = header.kind;
|
||||
entry.size = header.size;
|
||||
const source = r.payload[protocol.directory_entry_size..];
|
||||
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
||||
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
||||
entry.name_len = nlen;
|
||||
f.offset += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Close a directory fd (same as `close`).
|
||||
pub fn closedir(fd: i32) void {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -42,6 +42,10 @@ pub const dma = @import("dma.zig");
|
||||
/// (control / interrupt / bulk transfers). See library/runtime/usb.zig.
|
||||
pub const usb = @import("usb.zig");
|
||||
|
||||
/// Block-device client: read/write a block device (a USB stick, via
|
||||
/// usb-storage). See library/runtime/block.zig.
|
||||
pub const block = @import("block.zig");
|
||||
|
||||
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
||||
pub const panic = start.panic;
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ pub const ServiceId = enum(u32) {
|
||||
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
|
||||
_,
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ const Task = scheduler.Task;
|
||||
pub const MESSAGE_MAXIMUM: usize = 256;
|
||||
|
||||
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.
|
||||
pub const EBADF: i64 = 1; // bad handle
|
||||
|
||||
@@ -148,6 +148,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
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")) {
|
||||
deviceListTest(boot_information);
|
||||
} else if (eql(case, "pci-scan")) {
|
||||
@@ -1991,6 +1993,32 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,707 @@
|
||||
//! The FAT filesystem engine: mount a block device, walk the FAT and directory
|
||||
//! structures, and read / write / create files. FAT12/16/32 (the type is
|
||||
//! detected from the cluster count). Pure logic over a `BlockDevice` interface —
|
||||
//! no IPC — so it is host-testable against a RAM-backed image (see the tests at
|
||||
//! the bottom). The fat.zig server wraps a real `.block` device in a BlockDevice
|
||||
//! and serves this over the VFS protocol.
|
||||
//!
|
||||
//! Everything works in 512-byte sectors; a cluster is N sectors. Names are
|
||||
//! matched case-insensitively against both the 8.3 short name and, when present,
|
||||
//! the reconstructed long name. Writes update the directory entry, every FAT
|
||||
//! copy, and (FAT32) the FSInfo hint, in the crash-safe order data -> FAT ->
|
||||
//! directory. Long-name *creation* is not implemented — new files get an 8.3
|
||||
//! name (the common case; the plan flags LFN-write as optional).
|
||||
|
||||
const std = @import("std");
|
||||
const on_disk = @import("on-disk.zig");
|
||||
|
||||
/// A block device the engine reads and writes in fixed-size blocks. The two
|
||||
/// function pointers let the same engine run over a real `.block` driver or a
|
||||
/// RAM buffer (the tests).
|
||||
pub const BlockDevice = struct {
|
||||
context: *anyopaque,
|
||||
block_size: u32,
|
||||
block_count: u64,
|
||||
readBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []u8) bool,
|
||||
writeBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []const u8) bool,
|
||||
|
||||
pub fn readBlock(self: BlockDevice, lba: u64, buffer: []u8) bool {
|
||||
return self.readBlockFn(self.context, lba, buffer);
|
||||
}
|
||||
pub fn writeBlock(self: BlockDevice, lba: u64, buffer: []const u8) bool {
|
||||
return self.writeBlockFn(self.context, lba, buffer);
|
||||
}
|
||||
};
|
||||
|
||||
/// A resolved filesystem object: a file or directory, and where its 8.3 entry
|
||||
/// lives so writes can update its size and first cluster.
|
||||
pub const Node = struct {
|
||||
first_cluster: u32,
|
||||
size: u32,
|
||||
is_directory: bool,
|
||||
// The absolute sector and byte offset of this node's 8.3 directory entry, so
|
||||
// size/first-cluster changes can be written back. Absent for the root.
|
||||
entry_sector: u64 = 0,
|
||||
entry_offset: u32 = 0,
|
||||
has_entry: bool = false,
|
||||
};
|
||||
|
||||
const sector_size = 512;
|
||||
const entries_per_sector = sector_size / @sizeOf(on_disk.DirectoryEntry); // 16
|
||||
|
||||
pub const FileSystem = struct {
|
||||
device: BlockDevice,
|
||||
geometry: on_disk.Geometry,
|
||||
// The absolute LBA the filesystem starts at: 0 for a bare FAT ("superfloppy"),
|
||||
// or the first partition's start LBA when the disk carries an MBR. Every
|
||||
// filesystem-relative sector read/write adds this.
|
||||
base_lba: u64 = 0,
|
||||
// Distinct scratch sectors so nested reads (a FAT lookup during a directory
|
||||
// scan) never alias each other.
|
||||
sector: [sector_size]u8 = undefined,
|
||||
fat_sector: [sector_size]u8 = undefined,
|
||||
dir_sector: [sector_size]u8 = undefined,
|
||||
|
||||
// Every filesystem-relative sector access adds the partition base.
|
||||
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
|
||||
return self.device.readBlock(self.base_lba + lba, buffer);
|
||||
}
|
||||
fn blockWrite(self: *FileSystem, lba: u64, buffer: []const u8) bool {
|
||||
return self.device.writeBlock(self.base_lba + lba, buffer);
|
||||
}
|
||||
|
||||
/// Mount the filesystem on `device`: either a bare FAT with its boot sector at
|
||||
/// LBA 0, or (as QEMU's VVFAT and most real USB sticks present it) an MBR-
|
||||
/// partitioned disk whose first FAT partition holds the boot sector. Returns
|
||||
/// null if neither is found.
|
||||
pub fn mount(device: BlockDevice) ?FileSystem {
|
||||
var boot: [sector_size]u8 = undefined;
|
||||
if (!device.readBlock(0, &boot)) return null;
|
||||
|
||||
// A bare FAT: a valid boot sector right at LBA 0.
|
||||
if (on_disk.geometryOf(&boot)) |geometry| {
|
||||
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = 0 };
|
||||
}
|
||||
|
||||
// Otherwise an MBR: the 0x55AA signature but no BPB. Walk its four
|
||||
// partition entries (16 bytes each at offset 446) for the first non-empty
|
||||
// one, and mount the FAT boot sector at that partition's start LBA.
|
||||
if (boot[510] == 0x55 and boot[511] == 0xAA) {
|
||||
var partition: usize = 0;
|
||||
while (partition < 4) : (partition += 1) {
|
||||
const entry = boot[446 + partition * 16 ..][0..16];
|
||||
const partition_type = entry[4];
|
||||
const start_lba = std.mem.readInt(u32, entry[8..12], .little);
|
||||
if (partition_type == 0 or start_lba == 0) continue;
|
||||
var partition_boot: [sector_size]u8 = undefined;
|
||||
if (!device.readBlock(start_lba, &partition_boot)) continue;
|
||||
if (on_disk.geometryOf(&partition_boot)) |geometry| {
|
||||
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = start_lba };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- cluster <-> sector -------------------------------------------------
|
||||
|
||||
fn clusterSector(self: *const FileSystem, cluster: u32, sector_in_cluster: u32) u64 {
|
||||
return @as(u64, self.geometry.first_data_sector) + @as(u64, cluster - 2) * self.geometry.sectors_per_cluster + sector_in_cluster;
|
||||
}
|
||||
|
||||
fn fatByteBase(self: *const FileSystem) u64 {
|
||||
return @as(u64, self.geometry.reserved_sector_count) * sector_size;
|
||||
}
|
||||
|
||||
fn rootDirStartSector(self: *const FileSystem) u64 {
|
||||
return @as(u64, self.geometry.reserved_sector_count) + @as(u64, self.geometry.fat_count) * self.geometry.fat_size_sectors;
|
||||
}
|
||||
|
||||
fn rootDirSectors(self: *const FileSystem) u32 {
|
||||
return (self.geometry.root_entry_count * 32 + sector_size - 1) / sector_size;
|
||||
}
|
||||
|
||||
// --- FAT access ---------------------------------------------------------
|
||||
|
||||
// Read `out.len` bytes from FAT #0 starting at `byte_offset`, spanning sectors.
|
||||
fn readFatBytes(self: *FileSystem, byte_offset: u64, out: []u8) bool {
|
||||
var done: usize = 0;
|
||||
var position = self.fatByteBase() + byte_offset;
|
||||
while (done < out.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
const n = @min(out.len - done, sector_size - within);
|
||||
@memcpy(out[done .. done + n], self.fat_sector[within .. within + n]);
|
||||
done += n;
|
||||
position += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write `in.len` bytes at `byte_offset` into every FAT copy (read-modify-write
|
||||
// per sector).
|
||||
fn writeFatBytes(self: *FileSystem, byte_offset: u64, in: []const u8) bool {
|
||||
var fat: u32 = 0;
|
||||
while (fat < self.geometry.fat_count) : (fat += 1) {
|
||||
const base = self.fatByteBase() + @as(u64, fat) * @as(u64, self.geometry.fat_size_sectors) * sector_size;
|
||||
var done: usize = 0;
|
||||
var position = base + byte_offset;
|
||||
while (done < in.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
const n = @min(in.len - done, sector_size - within);
|
||||
@memcpy(self.fat_sector[within .. within + n], in[done .. done + n]);
|
||||
if (!self.blockWrite(lba, &self.fat_sector)) return false;
|
||||
done += n;
|
||||
position += n;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn readFatEntry(self: *FileSystem, cluster: u32) u32 {
|
||||
switch (self.geometry.fat_type) {
|
||||
.fat12 => {
|
||||
var pair: [2]u8 = undefined;
|
||||
const offset = cluster + cluster / 2; // cluster * 1.5
|
||||
if (!self.readFatBytes(offset, &pair)) return on_disk.end_of_chain_12;
|
||||
const word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
|
||||
return if (cluster & 1 == 1) (word >> 4) else (word & 0x0FFF);
|
||||
},
|
||||
.fat16 => {
|
||||
var value: [2]u8 = undefined;
|
||||
if (!self.readFatBytes(@as(u64, cluster) * 2, &value)) return on_disk.end_of_chain_16;
|
||||
return @as(u16, value[0]) | (@as(u16, value[1]) << 8);
|
||||
},
|
||||
.fat32 => {
|
||||
var value: [4]u8 = undefined;
|
||||
if (!self.readFatBytes(@as(u64, cluster) * 4, &value)) return on_disk.end_of_chain_32;
|
||||
return (@as(u32, value[0]) | (@as(u32, value[1]) << 8) | (@as(u32, value[2]) << 16) | (@as(u32, value[3]) << 24)) & 0x0FFFFFFF;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn writeFatEntry(self: *FileSystem, cluster: u32, value: u32) bool {
|
||||
switch (self.geometry.fat_type) {
|
||||
.fat12 => {
|
||||
const offset = cluster + cluster / 2;
|
||||
var pair: [2]u8 = undefined;
|
||||
if (!self.readFatBytes(offset, &pair)) return false;
|
||||
var word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
|
||||
if (cluster & 1 == 1) {
|
||||
word = (word & 0x000F) | (@as(u16, @truncate(value)) << 4);
|
||||
} else {
|
||||
word = (word & 0xF000) | (@as(u16, @truncate(value)) & 0x0FFF);
|
||||
}
|
||||
pair[0] = @truncate(word);
|
||||
pair[1] = @truncate(word >> 8);
|
||||
return self.writeFatBytes(offset, &pair);
|
||||
},
|
||||
.fat16 => {
|
||||
const bytes = [2]u8{ @truncate(value), @truncate(value >> 8) };
|
||||
return self.writeFatBytes(@as(u64, cluster) * 2, &bytes);
|
||||
},
|
||||
.fat32 => {
|
||||
const bytes = [4]u8{ @truncate(value), @truncate(value >> 8), @truncate(value >> 16), @truncate(value >> 24) };
|
||||
return self.writeFatBytes(@as(u64, cluster) * 4, &bytes);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn isEndOfChain(self: *const FileSystem, value: u32) bool {
|
||||
return switch (self.geometry.fat_type) {
|
||||
.fat12 => value >= on_disk.end_of_chain_12,
|
||||
.fat16 => value >= on_disk.end_of_chain_16,
|
||||
.fat32 => value >= on_disk.end_of_chain_32,
|
||||
};
|
||||
}
|
||||
|
||||
fn endOfChainValue(self: *const FileSystem) u32 {
|
||||
return switch (self.geometry.fat_type) {
|
||||
.fat12 => 0xFFF,
|
||||
.fat16 => 0xFFFF,
|
||||
.fat32 => 0x0FFFFFFF,
|
||||
};
|
||||
}
|
||||
|
||||
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
|
||||
fn allocateCluster(self: *FileSystem) ?u32 {
|
||||
var cluster: u32 = 2;
|
||||
while (cluster < self.geometry.cluster_count + 2) : (cluster += 1) {
|
||||
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
|
||||
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- directory iteration ------------------------------------------------
|
||||
|
||||
// The absolute LBA of the `sector_index`th sector of directory `dir`, or null
|
||||
// past its end. If `grow` is set and a cluster chain runs out, a new cluster
|
||||
// is allocated and linked (used when appending a directory entry).
|
||||
fn dirSectorLba(self: *FileSystem, dir: Node, sector_index: u32, grow: bool) ?u64 {
|
||||
const is_fixed_root = dir.first_cluster == 0 and self.geometry.fat_type != .fat32;
|
||||
if (is_fixed_root) {
|
||||
if (sector_index >= self.rootDirSectors()) return null;
|
||||
return self.rootDirStartSector() + sector_index;
|
||||
}
|
||||
const spc = self.geometry.sectors_per_cluster;
|
||||
var cluster = if (dir.first_cluster == 0) self.geometry.root_cluster else dir.first_cluster;
|
||||
var remaining = sector_index;
|
||||
while (remaining >= spc) : (remaining -= spc) {
|
||||
var next = self.readFatEntry(cluster);
|
||||
if (self.isEndOfChain(next) or next < 2) {
|
||||
if (!grow) return null;
|
||||
const fresh = self.allocateCluster() orelse return null;
|
||||
self.zeroCluster(fresh);
|
||||
if (!self.writeFatEntry(cluster, fresh)) return null;
|
||||
next = fresh;
|
||||
}
|
||||
cluster = next;
|
||||
}
|
||||
return self.clusterSector(cluster, remaining);
|
||||
}
|
||||
|
||||
fn zeroCluster(self: *FileSystem, cluster: u32) void {
|
||||
var zero = [_]u8{0} ** sector_size;
|
||||
var s: u32 = 0;
|
||||
while (s < self.geometry.sectors_per_cluster) : (s += 1) {
|
||||
_ = self.blockWrite(self.clusterSector(cluster, s), &zero);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rootNode(self: *const FileSystem) Node {
|
||||
return .{
|
||||
.first_cluster = if (self.geometry.fat_type == .fat32) self.geometry.root_cluster else 0,
|
||||
.size = 0,
|
||||
.is_directory = true,
|
||||
.has_entry = false,
|
||||
};
|
||||
}
|
||||
|
||||
// --- name handling ------------------------------------------------------
|
||||
|
||||
// Format a raw 8.3 name ("NAME EXT") into the displayed "NAME.EXT".
|
||||
fn format83(raw: [11]u8, out: []u8) []const u8 {
|
||||
var length: usize = 0;
|
||||
var base_len: usize = 8;
|
||||
while (base_len > 0 and raw[base_len - 1] == ' ') base_len -= 1;
|
||||
for (raw[0..base_len]) |c| {
|
||||
if (length < out.len) {
|
||||
out[length] = c;
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
var ext_len: usize = 3;
|
||||
while (ext_len > 0 and raw[8 + ext_len - 1] == ' ') ext_len -= 1;
|
||||
if (ext_len > 0) {
|
||||
if (length < out.len) {
|
||||
out[length] = '.';
|
||||
length += 1;
|
||||
}
|
||||
for (raw[8 .. 8 + ext_len]) |c| {
|
||||
if (length < out.len) {
|
||||
out[length] = c;
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out[0..length];
|
||||
}
|
||||
|
||||
// Convert a name to a raw 8.3 field (uppercased, space-padded), or null if it
|
||||
// cannot be represented (too long a base or extension).
|
||||
fn to83(name: []const u8) ?[11]u8 {
|
||||
var raw = [_]u8{' '} ** 11;
|
||||
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
|
||||
const base = if (dot) |d| name[0..d] else name;
|
||||
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
|
||||
if (base.len == 0 or base.len > 8 or ext.len > 3) return null;
|
||||
for (base, 0..) |c, i| raw[i] = std.ascii.toUpper(c);
|
||||
for (ext, 0..) |c, i| raw[8 + i] = std.ascii.toUpper(c);
|
||||
return raw;
|
||||
}
|
||||
|
||||
fn nameMatches(display: []const u8, query: []const u8) bool {
|
||||
if (display.len != query.len) return false;
|
||||
for (display, query) |a, b| {
|
||||
if (std.ascii.toUpper(a) != std.ascii.toUpper(b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pull the 13 UTF-16 code units of one long-name entry into `out` (ASCII only,
|
||||
// non-ASCII becomes '?'). Returns how many characters (stopping at 0x0000).
|
||||
fn longNameChars(entry: on_disk.LongNameEntry, out: *[13]u8) usize {
|
||||
const units = [13]u16{
|
||||
entry.name1[0], entry.name1[1], entry.name1[2], entry.name1[3], entry.name1[4],
|
||||
entry.name2[0], entry.name2[1], entry.name2[2], entry.name2[3], entry.name2[4],
|
||||
entry.name2[5], entry.name3[0], entry.name3[1],
|
||||
};
|
||||
var count: usize = 0;
|
||||
for (units) |unit| {
|
||||
if (unit == 0x0000 or unit == 0xFFFF) break;
|
||||
out[count] = if (unit < 0x80) @truncate(unit) else '?';
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// --- directory search + listing ----------------------------------------
|
||||
|
||||
/// Iterate the entries of a directory, calling `visit` with each real (non-LFN,
|
||||
/// non-free) entry, its reconstructed display name, and where it lives. Stops
|
||||
/// when `visit` returns true or the directory ends.
|
||||
fn scanDirectory(
|
||||
self: *FileSystem,
|
||||
dir: Node,
|
||||
context: anytype,
|
||||
comptime visit: fn (@TypeOf(context), entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool,
|
||||
) void {
|
||||
var long_name: [260]u8 = undefined;
|
||||
var long_len: usize = 0;
|
||||
var sector_index: u32 = 0;
|
||||
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
|
||||
if (!self.blockRead(lba, &self.dir_sector)) return;
|
||||
var i: u32 = 0;
|
||||
while (i < entries_per_sector) : (i += 1) {
|
||||
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
||||
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
||||
if (entry.isEnd()) return;
|
||||
if (entry.name[0] == 0xE5) {
|
||||
long_len = 0;
|
||||
continue;
|
||||
}
|
||||
if (entry.isLongName()) {
|
||||
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
|
||||
const order = lfn.order & 0x1F;
|
||||
if (order >= 1 and order <= 20) {
|
||||
var chunk: [13]u8 = undefined;
|
||||
const n = longNameChars(lfn, &chunk);
|
||||
const start = (order - 1) * 13;
|
||||
if (start + n <= long_name.len) {
|
||||
@memcpy(long_name[start .. start + n], chunk[0..n]);
|
||||
if (lfn.order & 0x40 != 0) long_len = start + n; // last (first physical) piece sets the length
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry.isVolumeLabel()) {
|
||||
long_len = 0;
|
||||
continue;
|
||||
}
|
||||
var short: [12]u8 = undefined;
|
||||
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
|
||||
if (visit(context, entry, display, lba, offset)) return;
|
||||
long_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FindResult = struct { found: bool = false, node: Node = undefined };
|
||||
const FindContext = struct { query: []const u8, result: *FindResult };
|
||||
|
||||
fn findVisit(context: *const FindContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
||||
if (!nameMatches(name, context.query)) return false;
|
||||
context.result.* = .{ .found = true, .node = .{
|
||||
.first_cluster = entry.firstCluster(),
|
||||
.size = entry.file_size,
|
||||
.is_directory = entry.isDirectory(),
|
||||
.entry_sector = entry_sector,
|
||||
.entry_offset = entry_offset,
|
||||
.has_entry = true,
|
||||
} };
|
||||
return true;
|
||||
}
|
||||
|
||||
fn findChild(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
var result = FindResult{};
|
||||
var context = FindContext{ .query = name, .result = &result };
|
||||
self.scanDirectory(dir, &context, findVisit);
|
||||
return if (result.found) result.node else null;
|
||||
}
|
||||
|
||||
/// Resolve an absolute or "/"-relative path to a node. "/" is the root.
|
||||
pub fn resolve(self: *FileSystem, path: []const u8) ?Node {
|
||||
var node = self.rootNode();
|
||||
var it = std.mem.tokenizeScalar(u8, path, '/');
|
||||
while (it.next()) |component| {
|
||||
if (component.len == 0) continue;
|
||||
if (!node.is_directory) return null;
|
||||
node = self.findChild(node, component) orelse return null;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// The `cursor`th real entry of a directory (for readdir): its display name,
|
||||
/// kind, and size. Returns null past the end.
|
||||
pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0 };
|
||||
const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false };
|
||||
|
||||
fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
||||
_ = entry_sector;
|
||||
_ = entry_offset;
|
||||
if (context.index == context.target) {
|
||||
const n = @min(name.len, context.out.name_buffer.len);
|
||||
@memcpy(context.out.name_buffer[0..n], name[0..n]);
|
||||
context.out.name_len = n;
|
||||
context.out.is_directory = entry.isDirectory();
|
||||
context.out.size = entry.file_size;
|
||||
context.done = true;
|
||||
return true;
|
||||
}
|
||||
context.index += 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn listEntry(self: *FileSystem, dir: Node, cursor: u32) ?Listing {
|
||||
var listing = Listing{};
|
||||
var context = ListContext{ .target = cursor, .out = &listing };
|
||||
self.scanDirectory(dir, &context, listVisit);
|
||||
return if (context.done) listing else null;
|
||||
}
|
||||
|
||||
// --- file read / write --------------------------------------------------
|
||||
|
||||
// The cluster holding byte `offset` of a chain starting at `first`, walking
|
||||
// (and optionally growing) the chain. Returns null at end without grow.
|
||||
fn clusterAt(self: *FileSystem, first: u32, offset: u32, grow: bool) ?u32 {
|
||||
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
||||
var cluster = first;
|
||||
var steps = offset / cluster_bytes;
|
||||
while (steps > 0) : (steps -= 1) {
|
||||
var next = self.readFatEntry(cluster);
|
||||
if (self.isEndOfChain(next) or next < 2) {
|
||||
if (!grow) return null;
|
||||
const fresh = self.allocateCluster() orelse return null;
|
||||
if (!self.writeFatEntry(cluster, fresh)) return null;
|
||||
next = fresh;
|
||||
}
|
||||
cluster = next;
|
||||
}
|
||||
return cluster;
|
||||
}
|
||||
|
||||
/// Read up to `buffer.len` bytes of a file node starting at `offset`. Returns
|
||||
/// the number read (0 at or past EOF).
|
||||
pub fn readFile(self: *FileSystem, node: Node, offset: u32, buffer: []u8) usize {
|
||||
if (offset >= node.size or node.first_cluster < 2) return 0;
|
||||
const available = node.size - offset;
|
||||
const want = @min(buffer.len, available);
|
||||
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
||||
|
||||
var produced: usize = 0;
|
||||
var position = offset;
|
||||
while (produced < want) {
|
||||
const cluster = self.clusterAt(node.first_cluster, position, false) orelse break;
|
||||
const in_cluster = position % cluster_bytes;
|
||||
const sector_in_cluster = in_cluster / sector_size;
|
||||
const in_sector = in_cluster % sector_size;
|
||||
if (!self.blockRead(self.clusterSector(cluster, sector_in_cluster), &self.sector)) break;
|
||||
const n = @min(want - produced, sector_size - in_sector);
|
||||
@memcpy(buffer[produced .. produced + n], self.sector[in_sector .. in_sector + n]);
|
||||
produced += n;
|
||||
position += @intCast(n);
|
||||
}
|
||||
return produced;
|
||||
}
|
||||
|
||||
/// Write `data` to a file node at `offset`, growing it (allocating clusters and
|
||||
/// updating the directory entry) as needed. Returns the number written.
|
||||
pub fn writeFile(self: *FileSystem, node: *Node, offset: u32, data: []const u8) usize {
|
||||
if (data.len == 0) return 0;
|
||||
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
||||
|
||||
// Ensure the file has a first cluster.
|
||||
if (node.first_cluster < 2) {
|
||||
const fresh = self.allocateCluster() orelse return 0;
|
||||
self.zeroCluster(fresh);
|
||||
node.first_cluster = fresh;
|
||||
}
|
||||
|
||||
var consumed: usize = 0;
|
||||
var position = offset;
|
||||
while (consumed < data.len) {
|
||||
const cluster = self.clusterAt(node.first_cluster, position, true) orelse break;
|
||||
const in_cluster = position % cluster_bytes;
|
||||
const sector_in_cluster = in_cluster / sector_size;
|
||||
const in_sector = in_cluster % sector_size;
|
||||
const lba = self.clusterSector(cluster, sector_in_cluster);
|
||||
// Read-modify-write the sector for a partial write.
|
||||
if (!self.blockRead(lba, &self.sector)) break;
|
||||
const n = @min(data.len - consumed, sector_size - in_sector);
|
||||
@memcpy(self.sector[in_sector .. in_sector + n], data[consumed .. consumed + n]);
|
||||
if (!self.blockWrite(lba, &self.sector)) break;
|
||||
consumed += n;
|
||||
position += @intCast(n);
|
||||
}
|
||||
|
||||
const new_end = offset + @as(u32, @intCast(consumed));
|
||||
if (new_end > node.size) node.size = new_end;
|
||||
self.updateEntry(node.*);
|
||||
return consumed;
|
||||
}
|
||||
|
||||
// Write a node's size and first cluster back into its 8.3 directory entry.
|
||||
fn updateEntry(self: *FileSystem, node: Node) void {
|
||||
if (!node.has_entry) return;
|
||||
if (!self.blockRead(node.entry_sector, &self.dir_sector)) return;
|
||||
var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]);
|
||||
entry.file_size = node.size;
|
||||
entry.setFirstCluster(node.first_cluster);
|
||||
@memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
|
||||
_ = self.blockWrite(node.entry_sector, &self.dir_sector);
|
||||
}
|
||||
|
||||
/// Create an 8.3-named file in directory `dir`. Returns the new (empty) node,
|
||||
/// or null if the name is not 8.3-representable or no directory slot is free.
|
||||
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
const raw = to83(name) orelse return null;
|
||||
// Find a free directory slot (a 0x00 or 0xE5 entry), growing the directory.
|
||||
var sector_index: u32 = 0;
|
||||
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
|
||||
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
||||
var i: u32 = 0;
|
||||
while (i < entries_per_sector) : (i += 1) {
|
||||
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
||||
const existing = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
||||
if (existing.isFree()) {
|
||||
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
|
||||
entry.name = raw;
|
||||
entry.attributes = on_disk.attribute_archive;
|
||||
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
|
||||
if (!self.blockWrite(lba, &self.dir_sector)) return null;
|
||||
return .{
|
||||
.first_cluster = 0,
|
||||
.size = 0,
|
||||
.is_directory = false,
|
||||
.entry_sector = lba,
|
||||
.entry_offset = offset,
|
||||
.has_entry = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Only the fixed root can run out (it can't grow); a chain grows above.
|
||||
if (sector_index > 4096) return null; // runaway guard
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests: a RAM-backed FAT16 image ----------------------------------------
|
||||
|
||||
const RamDisk = struct {
|
||||
bytes: []u8,
|
||||
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
||||
const self: *RamDisk = @ptrCast(@alignCast(context));
|
||||
const start = lba * sector_size;
|
||||
if (start + sector_size > self.bytes.len) return false;
|
||||
@memcpy(buffer[0..sector_size], self.bytes[start .. start + sector_size]);
|
||||
return true;
|
||||
}
|
||||
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
||||
const self: *RamDisk = @ptrCast(@alignCast(context));
|
||||
const start = lba * sector_size;
|
||||
if (start + sector_size > self.bytes.len) return false;
|
||||
@memcpy(self.bytes[start .. start + sector_size], buffer[0..sector_size]);
|
||||
return true;
|
||||
}
|
||||
fn device(self: *RamDisk) BlockDevice {
|
||||
return .{
|
||||
.context = self,
|
||||
.block_size = sector_size,
|
||||
.block_count = self.bytes.len / sector_size,
|
||||
.readBlockFn = readBlock,
|
||||
.writeBlockFn = writeBlock,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Format a minimal FAT16 volume into `bytes`: BPB + boot signature, FATs with the
|
||||
// two reserved entries, an empty root directory. Enough for the engine to mount
|
||||
// and operate on.
|
||||
fn formatFat16(bytes: []u8) void {
|
||||
@memset(bytes, 0);
|
||||
const total_sectors: u16 = @intCast(bytes.len / sector_size);
|
||||
var bpb = std.mem.zeroes(on_disk.BiosParameterBlock);
|
||||
bpb.jump = .{ 0xEB, 0x3C, 0x90 };
|
||||
bpb.oem_name = "MSWIN4.1".*;
|
||||
bpb.bytes_per_sector = sector_size;
|
||||
bpb.sectors_per_cluster = 1;
|
||||
bpb.reserved_sector_count = 1;
|
||||
bpb.fat_count = 2;
|
||||
bpb.root_entry_count = 512;
|
||||
bpb.total_sectors_16 = total_sectors;
|
||||
bpb.media = 0xF8;
|
||||
bpb.fat_size_16 = 16; // 16 sectors per FAT (covers ~4000 FAT16 entries)
|
||||
@memcpy(bytes[0..@sizeOf(on_disk.BiosParameterBlock)], std.mem.asBytes(&bpb));
|
||||
bytes[on_disk.boot_signature_offset] = 0x55;
|
||||
bytes[on_disk.boot_signature_offset + 1] = 0xAA;
|
||||
// FAT reserved entries: entry0 = media in low byte + 0xFF, entry1 = EOC.
|
||||
const fat0 = 1 * sector_size;
|
||||
bytes[fat0] = 0xF8;
|
||||
bytes[fat0 + 1] = 0xFF;
|
||||
bytes[fat0 + 2] = 0xFF;
|
||||
bytes[fat0 + 3] = 0xFF;
|
||||
const fat1 = fat0 + 16 * sector_size;
|
||||
bytes[fat1] = 0xF8;
|
||||
bytes[fat1 + 1] = 0xFF;
|
||||
bytes[fat1 + 2] = 0xFF;
|
||||
bytes[fat1 + 3] = 0xFF;
|
||||
}
|
||||
|
||||
test "mount a formatted FAT16 image" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size); // ~2.4 MB
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
try std.testing.expectEqual(on_disk.FatType.fat16, fs.geometry.fat_type);
|
||||
try std.testing.expect(fs.geometry.cluster_count >= 4085);
|
||||
|
||||
// An empty root directory lists nothing.
|
||||
try std.testing.expect(fs.listEntry(fs.rootNode(), 0) == null);
|
||||
}
|
||||
|
||||
test "create, write, read back a file through the engine" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
// Create /HELLO.TXT and write a payload larger than one sector (spans clusters).
|
||||
var node = fs.createFile(fs.rootNode(), "HELLO.TXT").?;
|
||||
var payload: [1500]u8 = undefined;
|
||||
for (&payload, 0..) |*b, i| b.* = @truncate(i);
|
||||
const written = fs.writeFile(&node, 0, &payload);
|
||||
try std.testing.expectEqual(@as(usize, payload.len), written);
|
||||
|
||||
// Re-resolve from the directory (proving the entry was persisted) and read back.
|
||||
const resolved = fs.resolve("/HELLO.TXT").?;
|
||||
try std.testing.expectEqual(@as(u32, payload.len), resolved.size);
|
||||
var readback: [1500]u8 = undefined;
|
||||
const got = fs.readFile(resolved, 0, &readback);
|
||||
try std.testing.expectEqual(@as(usize, payload.len), got);
|
||||
try std.testing.expectEqualSlices(u8, &payload, &readback);
|
||||
|
||||
// A mid-file overwrite is visible on re-read.
|
||||
var patch = [_]u8{0xAB} ** 4;
|
||||
_ = fs.writeFile(&node, 600, &patch);
|
||||
const patched = fs.resolve("/HELLO.TXT").?;
|
||||
_ = fs.readFile(patched, 600, readback[0..4]);
|
||||
try std.testing.expectEqualSlices(u8, &patch, readback[0..4]);
|
||||
|
||||
// The root now lists exactly HELLO.TXT.
|
||||
const listing = fs.listEntry(fs.rootNode(), 0).?;
|
||||
try std.testing.expectEqualStrings("HELLO.TXT", listing.name_buffer[0..listing.name_len]);
|
||||
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! system/services/fat/fat-test — a client that proves the FAT mount end to end:
|
||||
//! it waits for the fat server to mount the USB volume at /mnt/usb, lists the
|
||||
//! root directory through the VFS (which routes /mnt/usb to the fat backend), and
|
||||
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
|
||||
//! kernel test spawns it alongside init.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
_ = init;
|
||||
const unistd = @import("posix").unistd;
|
||||
|
||||
// Wait for /mnt/usb to be mounted — the fat server races us at boot (it must
|
||||
// bring up the whole USB storage chain first).
|
||||
var dir: i32 = -1;
|
||||
var tries: u32 = 0;
|
||||
while (dir < 0 and tries < 1400) : (tries += 1) {
|
||||
dir = unistd.opendir("/mnt/usb");
|
||||
if (dir < 0) runtime.system.sleep(50);
|
||||
}
|
||||
if (dir < 0) {
|
||||
_ = runtime.system.write("fat-test: /mnt/usb never became available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
var count: u32 = 0;
|
||||
var entry: unistd.DirEntry = .{};
|
||||
while (unistd.readdir(dir, &entry)) {
|
||||
writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), entry.kind, entry.size });
|
||||
count += 1;
|
||||
if (count > 32) break;
|
||||
}
|
||||
unistd.closedir(dir);
|
||||
writeLine("fat-test: listed {d} entries\n", .{count});
|
||||
|
||||
// Read a known file off the boot volume through the mount (best effort): the
|
||||
// kernel image is an ELF, so its first bytes are the ELF magic.
|
||||
const fd = unistd.open("/mnt/usb/system/kernel", 0);
|
||||
if (fd >= 0) {
|
||||
var magic: [4]u8 = undefined;
|
||||
const n = unistd.read(fd, &magic);
|
||||
unistd.close(fd);
|
||||
if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') {
|
||||
_ = runtime.system.write("fat-test: read /mnt/usb/system/kernel ELF magic ok\n");
|
||||
} else {
|
||||
writeLine("fat-test: /mnt/usb/system/kernel read {d} bytes (not ELF magic)\n", .{n});
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
while (true) {
|
||||
_ = runtime.system.write("fat-test: ok\n");
|
||||
runtime.system.sleep(1000);
|
||||
}
|
||||
}
|
||||
_ = runtime.system.write("fat-test: root listing was empty\n");
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it
|
||||
//! opens the block device (a USB stick via usb-storage) under `.block`, mounts the
|
||||
//! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into
|
||||
//! the VFS at /mnt/usb. From then on the VFS forwards every open/read/write/
|
||||
//! status/readdir/close under /mnt/usb to this server, which serves the same
|
||||
//! vfs-protocol as a backend — turning block reads into file reads.
|
||||
//!
|
||||
//! The block data path never crosses IPC: a DMA bounce buffer is handed to the
|
||||
//! block driver by physical address, and the engine copies sectors in and out of
|
||||
//! it.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const engine = @import("engine.zig");
|
||||
const on_disk = @import("on-disk.zig");
|
||||
const protocol = runtime.vfs_protocol;
|
||||
const unistd = @import("posix").unistd;
|
||||
const dma = runtime.dma;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
const mount_point = "/mnt/usb";
|
||||
|
||||
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
|
||||
// buffer the driver reads/writes by physical address.
|
||||
const IpcBlock = struct {
|
||||
device: runtime.block.Device,
|
||||
bounce: dma.Region,
|
||||
|
||||
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
||||
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
||||
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
|
||||
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
|
||||
@memcpy(buffer[0..512], source[0..512]);
|
||||
return true;
|
||||
}
|
||||
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
||||
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
||||
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
|
||||
@memcpy(destination[0..512], buffer[0..512]);
|
||||
return self.device.write(lba, 1, self.bounce.physical);
|
||||
}
|
||||
};
|
||||
|
||||
var ipc_block: IpcBlock = undefined;
|
||||
var filesystem: engine.FileSystem = undefined;
|
||||
|
||||
// Open handles the VFS holds against this backend: each maps a node id to a
|
||||
// resolved engine node.
|
||||
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
||||
var open_nodes = [_]OpenNode{.{}} ** 32;
|
||||
|
||||
fn allocOpen() ?usize {
|
||||
for (&open_nodes, 0..) |*o, i| {
|
||||
if (!o.used) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn openAt(id: u64) ?*OpenNode {
|
||||
if (id >= open_nodes.len) return null;
|
||||
const o = &open_nodes[@intCast(id)];
|
||||
return if (o.used) o else null;
|
||||
}
|
||||
|
||||
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
|
||||
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
|
||||
const n = @min(payload.len, out.len - protocol.reply_size);
|
||||
@memcpy(out[protocol.reply_size..][0..n], payload[0..n]);
|
||||
return protocol.reply_size + n;
|
||||
}
|
||||
|
||||
fn fail(out: []u8) usize {
|
||||
return writeReply(out, .{ .status = -1 }, &.{});
|
||||
}
|
||||
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = runtime.system.write("/system/services/fat: starting, waiting for a block device\n");
|
||||
const device = runtime.block.open() orelse {
|
||||
_ = runtime.system.write("/system/services/fat: no block device (no storage attached)\n");
|
||||
return false; // clean exit: nothing to serve
|
||||
};
|
||||
const geometry = device.geometry() orelse {
|
||||
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
|
||||
return false;
|
||||
};
|
||||
ipc_block = .{ .device = device, .bounce = dma.alloc(4096, dma.coherent) orelse return false };
|
||||
|
||||
const block_device = engine.BlockDevice{
|
||||
.context = &ipc_block,
|
||||
.block_size = geometry.block_size,
|
||||
.block_count = geometry.block_count,
|
||||
.readBlockFn = IpcBlock.readBlock,
|
||||
.writeBlockFn = IpcBlock.writeBlock,
|
||||
};
|
||||
filesystem = engine.FileSystem.mount(block_device) orelse {
|
||||
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/services/fat: mounted FAT ({s}, {d} clusters, partition lba {d})\n", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
||||
|
||||
// Mount ourselves into the VFS namespace at /mnt/usb (retry while the VFS
|
||||
// comes up). From here the VFS routes /mnt/usb/... to this server.
|
||||
var tries: u32 = 0;
|
||||
while (tries < 100) : (tries += 1) {
|
||||
if (unistd.mount(mount_point, endpoint) == 0) {
|
||||
writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
|
||||
return true;
|
||||
}
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
|
||||
return true; // still serve directly, even if the namespace mount didn't take
|
||||
}
|
||||
|
||||
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
|
||||
var node = filesystem.resolve(path);
|
||||
if (node == null and flags & protocol.create != 0) {
|
||||
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
|
||||
const parent_path = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/";
|
||||
const leaf = if (slash) |s| path[s + 1 ..] else path;
|
||||
const parent = filesystem.resolve(parent_path) orelse return fail(out);
|
||||
node = filesystem.createFile(parent, leaf);
|
||||
}
|
||||
const resolved = node orelse return fail(out);
|
||||
const index = allocOpen() orelse return fail(out);
|
||||
open_nodes[index] = .{ .used = true, .node = resolved };
|
||||
return writeReply(out, .{ .status = 0, .node = index }, &.{});
|
||||
}
|
||||
|
||||
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = capability;
|
||||
_ = sender;
|
||||
if (message.len < protocol.request_size) return fail(out);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
const payload = message[protocol.request_size..];
|
||||
|
||||
switch (request.operation) {
|
||||
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
|
||||
.read => {
|
||||
const o = openAt(request.node) orelse return fail(out);
|
||||
var buffer: [protocol.maximum_payload]u8 = undefined;
|
||||
const want = @min(@as(usize, request.len), buffer.len);
|
||||
const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
|
||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
|
||||
},
|
||||
.write => {
|
||||
const o = openAt(request.node) orelse return fail(out);
|
||||
const data = payload[0..@min(payload.len, request.len)];
|
||||
const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
|
||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
|
||||
},
|
||||
.status => {
|
||||
const o = openAt(request.node) orelse return fail(out);
|
||||
const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
||||
const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind) };
|
||||
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
|
||||
},
|
||||
.readdir => {
|
||||
const o = openAt(request.node) orelse return fail(out);
|
||||
if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
||||
const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
||||
const kind: protocol.NodeKind = if (listing.is_directory) .directory else .regular;
|
||||
const header = protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
|
||||
var buffer: [protocol.maximum_payload]u8 = undefined;
|
||||
@memcpy(buffer[0..protocol.directory_entry_size], std.mem.asBytes(&header));
|
||||
const nlen = @min(listing.name_len, buffer.len - protocol.directory_entry_size);
|
||||
@memcpy(buffer[protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
|
||||
const total = protocol.directory_entry_size + nlen;
|
||||
return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
|
||||
},
|
||||
.close => {
|
||||
if (openAt(request.node)) |o| o.used = false;
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
// A backend is never itself a mount target.
|
||||
.mount, .unmount => return fail(out),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .fat,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! The on-disk layout of a FAT filesystem — the boot sector / BIOS Parameter
|
||||
//! Block, directory entries, long-file-name entries, and the FAT32 FSInfo — as
|
||||
//! `align(1)` extern structs that bit-cast straight out of a 512-byte sector
|
||||
//! (multi-byte fields are little-endian, like usb-abi.zig). Pure data, plus the
|
||||
//! cluster-count FAT-type detection. Host-testable.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The BIOS Parameter Block, common to FAT12/16/32 (offset 0..36 of the boot
|
||||
/// sector). The extended part that follows differs by FAT type.
|
||||
pub const BiosParameterBlock = extern struct {
|
||||
jump: [3]u8,
|
||||
oem_name: [8]u8,
|
||||
bytes_per_sector: u16 align(1),
|
||||
sectors_per_cluster: u8,
|
||||
reserved_sector_count: u16 align(1),
|
||||
fat_count: u8,
|
||||
root_entry_count: u16 align(1),
|
||||
total_sectors_16: u16 align(1),
|
||||
media: u8,
|
||||
fat_size_16: u16 align(1),
|
||||
sectors_per_track: u16 align(1),
|
||||
head_count: u16 align(1),
|
||||
hidden_sectors: u32 align(1),
|
||||
total_sectors_32: u32 align(1),
|
||||
};
|
||||
|
||||
/// The FAT12/16 extended boot record (offset 36).
|
||||
pub const ExtendedBootRecord16 = extern struct {
|
||||
drive_number: u8,
|
||||
reserved: u8,
|
||||
boot_signature: u8,
|
||||
volume_id: u32 align(1),
|
||||
volume_label: [11]u8,
|
||||
filesystem_type: [8]u8,
|
||||
};
|
||||
|
||||
/// The FAT32 extended boot record (offset 36).
|
||||
pub const ExtendedBootRecord32 = extern struct {
|
||||
fat_size_32: u32 align(1),
|
||||
extended_flags: u16 align(1),
|
||||
filesystem_version: u16 align(1),
|
||||
root_cluster: u32 align(1),
|
||||
filesystem_information_sector: u16 align(1),
|
||||
backup_boot_sector: u16 align(1),
|
||||
reserved: [12]u8,
|
||||
drive_number: u8,
|
||||
reserved1: u8,
|
||||
boot_signature: u8,
|
||||
volume_id: u32 align(1),
|
||||
volume_label: [11]u8,
|
||||
filesystem_type: [8]u8,
|
||||
};
|
||||
|
||||
/// A 32-byte directory entry (8.3 short name form).
|
||||
pub const DirectoryEntry = extern struct {
|
||||
name: [11]u8, // 8 name + 3 extension, space-padded
|
||||
attributes: u8,
|
||||
reserved_nt: u8,
|
||||
creation_time_tenth: u8,
|
||||
creation_time: u16 align(1),
|
||||
creation_date: u16 align(1),
|
||||
last_access_date: u16 align(1),
|
||||
first_cluster_high: u16 align(1),
|
||||
write_time: u16 align(1),
|
||||
write_date: u16 align(1),
|
||||
first_cluster_low: u16 align(1),
|
||||
file_size: u32 align(1),
|
||||
|
||||
pub fn firstCluster(self: DirectoryEntry) u32 {
|
||||
return (@as(u32, self.first_cluster_high) << 16) | self.first_cluster_low;
|
||||
}
|
||||
pub fn setFirstCluster(self: *DirectoryEntry, cluster: u32) void {
|
||||
self.first_cluster_low = @truncate(cluster);
|
||||
self.first_cluster_high = @truncate(cluster >> 16);
|
||||
}
|
||||
pub fn isFree(self: DirectoryEntry) bool {
|
||||
return self.name[0] == 0x00 or self.name[0] == 0xE5;
|
||||
}
|
||||
pub fn isEnd(self: DirectoryEntry) bool {
|
||||
return self.name[0] == 0x00;
|
||||
}
|
||||
pub fn isDirectory(self: DirectoryEntry) bool {
|
||||
return self.attributes & attribute_directory != 0;
|
||||
}
|
||||
pub fn isLongName(self: DirectoryEntry) bool {
|
||||
return self.attributes & attribute_long_name_mask == attribute_long_name;
|
||||
}
|
||||
pub fn isVolumeLabel(self: DirectoryEntry) bool {
|
||||
return self.attributes & attribute_volume_id != 0 and !self.isLongName();
|
||||
}
|
||||
};
|
||||
|
||||
/// A 32-byte long-file-name entry (attributes == 0x0F). A sequence of these
|
||||
/// precedes the 8.3 entry they name, each carrying 13 UTF-16 code units.
|
||||
pub const LongNameEntry = extern struct {
|
||||
order: u8,
|
||||
name1: [5]u16 align(1),
|
||||
attributes: u8,
|
||||
kind: u8,
|
||||
checksum: u8,
|
||||
name2: [6]u16 align(1),
|
||||
first_cluster_low: u16 align(1),
|
||||
name3: [2]u16 align(1),
|
||||
};
|
||||
|
||||
/// The FAT32 FSInfo sector (usually sector 1): advisory free-cluster bookkeeping.
|
||||
pub const FileSystemInformation = extern struct {
|
||||
lead_signature: u32 align(1), // 0x41615252
|
||||
reserved1: [480]u8,
|
||||
struct_signature: u32 align(1), // 0x61417272
|
||||
free_count: u32 align(1),
|
||||
next_free: u32 align(1),
|
||||
reserved2: [12]u8,
|
||||
trail_signature: u32 align(1), // 0xAA550000
|
||||
};
|
||||
|
||||
// Directory-entry attribute bits.
|
||||
pub const attribute_read_only: u8 = 0x01;
|
||||
pub const attribute_hidden: u8 = 0x02;
|
||||
pub const attribute_system: u8 = 0x04;
|
||||
pub const attribute_volume_id: u8 = 0x08;
|
||||
pub const attribute_directory: u8 = 0x10;
|
||||
pub const attribute_archive: u8 = 0x20;
|
||||
pub const attribute_long_name: u8 = 0x0F; // read_only|hidden|system|volume_id
|
||||
pub const attribute_long_name_mask: u8 = 0x3F;
|
||||
|
||||
// FSInfo signatures.
|
||||
pub const fsinfo_lead_signature: u32 = 0x41615252;
|
||||
pub const fsinfo_struct_signature: u32 = 0x61417272;
|
||||
pub const fsinfo_trail_signature: u32 = 0xAA550000;
|
||||
|
||||
/// End-of-chain markers (a cluster value >= these ends a chain).
|
||||
pub const end_of_chain_12: u32 = 0xFF8;
|
||||
pub const end_of_chain_16: u32 = 0xFFF8;
|
||||
pub const end_of_chain_32: u32 = 0x0FFFFFF8;
|
||||
pub const bad_cluster_32: u32 = 0x0FFFFFF7;
|
||||
|
||||
pub const free_cluster: u32 = 0;
|
||||
pub const boot_signature_offset: usize = 510; // 0x55 0xAA at the end of the boot sector
|
||||
|
||||
pub const FatType = enum { fat12, fat16, fat32 };
|
||||
|
||||
/// The geometry derived from the BPB, plus the FAT type (by the Microsoft
|
||||
/// cluster-count rule: <4085 FAT12, <65525 FAT16, else FAT32).
|
||||
pub const Geometry = struct {
|
||||
fat_type: FatType,
|
||||
bytes_per_sector: u32,
|
||||
sectors_per_cluster: u32,
|
||||
reserved_sector_count: u32,
|
||||
fat_count: u32,
|
||||
fat_size_sectors: u32, // per FAT
|
||||
root_entry_count: u32, // FAT12/16
|
||||
root_cluster: u32, // FAT32
|
||||
first_data_sector: u32,
|
||||
total_sectors: u32,
|
||||
cluster_count: u32,
|
||||
fsinfo_sector: u32, // FAT32
|
||||
};
|
||||
|
||||
/// Derive the geometry (and FAT type) from a boot sector's first 512 bytes.
|
||||
/// Returns null if the sector is not a plausible FAT boot sector.
|
||||
pub fn geometryOf(sector: []const u8) ?Geometry {
|
||||
if (sector.len < 512) return null;
|
||||
if (sector[boot_signature_offset] != 0x55 or sector[boot_signature_offset + 1] != 0xAA) return null;
|
||||
const bpb = std.mem.bytesToValue(BiosParameterBlock, sector[0..@sizeOf(BiosParameterBlock)]);
|
||||
if (bpb.bytes_per_sector == 0 or bpb.sectors_per_cluster == 0 or bpb.fat_count == 0) return null;
|
||||
|
||||
const fat_size_16: u32 = bpb.fat_size_16;
|
||||
var fat_size: u32 = fat_size_16;
|
||||
var root_cluster: u32 = 0;
|
||||
var fsinfo_sector: u32 = 0;
|
||||
if (fat_size_16 == 0) {
|
||||
const ebr = std.mem.bytesToValue(ExtendedBootRecord32, sector[36 .. 36 + @sizeOf(ExtendedBootRecord32)]);
|
||||
fat_size = ebr.fat_size_32;
|
||||
root_cluster = ebr.root_cluster;
|
||||
fsinfo_sector = ebr.filesystem_information_sector;
|
||||
}
|
||||
|
||||
const total_sectors: u32 = if (bpb.total_sectors_16 != 0) bpb.total_sectors_16 else bpb.total_sectors_32;
|
||||
const root_dir_sectors = (@as(u32, bpb.root_entry_count) * 32 + bpb.bytes_per_sector - 1) / bpb.bytes_per_sector;
|
||||
const first_data_sector = bpb.reserved_sector_count + bpb.fat_count * fat_size + root_dir_sectors;
|
||||
if (total_sectors < first_data_sector) return null;
|
||||
const data_sectors = total_sectors - first_data_sector;
|
||||
const cluster_count = data_sectors / bpb.sectors_per_cluster;
|
||||
|
||||
const fat_type: FatType = if (cluster_count < 4085) .fat12 else if (cluster_count < 65525) .fat16 else .fat32;
|
||||
|
||||
return .{
|
||||
.fat_type = fat_type,
|
||||
.bytes_per_sector = bpb.bytes_per_sector,
|
||||
.sectors_per_cluster = bpb.sectors_per_cluster,
|
||||
.reserved_sector_count = bpb.reserved_sector_count,
|
||||
.fat_count = bpb.fat_count,
|
||||
.fat_size_sectors = fat_size,
|
||||
.root_entry_count = bpb.root_entry_count,
|
||||
.root_cluster = root_cluster,
|
||||
.first_data_sector = first_data_sector,
|
||||
.total_sectors = total_sectors,
|
||||
.cluster_count = cluster_count,
|
||||
.fsinfo_sector = fsinfo_sector,
|
||||
};
|
||||
}
|
||||
|
||||
test "on-disk struct sizes match the specification" {
|
||||
try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
|
||||
try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
|
||||
try std.testing.expectEqual(@as(usize, 54), @sizeOf(ExtendedBootRecord32));
|
||||
try std.testing.expectEqual(@as(usize, 32), @sizeOf(DirectoryEntry));
|
||||
try std.testing.expectEqual(@as(usize, 32), @sizeOf(LongNameEntry));
|
||||
try std.testing.expectEqual(@as(usize, 512), @sizeOf(FileSystemInformation));
|
||||
}
|
||||
|
||||
test "directory entry cluster split/join" {
|
||||
var entry = std.mem.zeroes(DirectoryEntry);
|
||||
entry.setFirstCluster(0x01234567);
|
||||
try std.testing.expectEqual(@as(u16, 0x4567), entry.first_cluster_low);
|
||||
try std.testing.expectEqual(@as(u16, 0x0123), entry.first_cluster_high);
|
||||
try std.testing.expectEqual(@as(u32, 0x01234567), entry.firstCluster());
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const power = runtime.power_protocol;
|
||||
/// 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
|
||||
/// 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 child_count: usize = 0;
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
@@ -17,8 +17,36 @@ pub const Operation = enum(u32) {
|
||||
read, // read(node, offset, len) -> bytes
|
||||
write, // write(node, offset, bytes) -> count
|
||||
status, // status(node) -> FileStatus
|
||||
// Appended for the mount router (M5). Values stay stable, so existing clients
|
||||
// and the flat-ramfs tests are unaffected.
|
||||
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
|
||||
mount, // mount(prefix payload, capability = backend endpoint)
|
||||
unmount, // unmount(prefix payload)
|
||||
};
|
||||
|
||||
/// The type of a filesystem node, aligned to the FSH file-type table
|
||||
/// (docs/danos-file-system-hierarchy-FSH.md). Fills `FileStatus.kind` and
|
||||
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
|
||||
pub const NodeKind = enum(u32) {
|
||||
regular = 0,
|
||||
directory = 1,
|
||||
character_device = 2,
|
||||
block_device = 3,
|
||||
symbolic_link = 4,
|
||||
fifo = 5,
|
||||
socket = 6,
|
||||
};
|
||||
|
||||
/// One directory entry, returned by `readdir`: a fixed header followed inline in
|
||||
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
|
||||
pub const DirectoryEntry = extern struct {
|
||||
kind: u32, // a NodeKind
|
||||
name_len: u32,
|
||||
size: u64,
|
||||
};
|
||||
|
||||
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
||||
|
||||
/// Request header. `node` is the server-side open-file id (from a prior open);
|
||||
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
||||
/// carry the read/write position and count.
|
||||
@@ -57,3 +85,17 @@ pub const maximum_payload: usize = message_maximum - request_size;
|
||||
|
||||
/// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`).
|
||||
pub const create: u32 = 1;
|
||||
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
||||
/// this to open a directory node; the flat ramfs ignores it.
|
||||
pub const directory: u32 = 2;
|
||||
|
||||
test "protocol struct sizes and node kinds" {
|
||||
const std = @import("std");
|
||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
||||
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
||||
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
||||
// The appended operations keep the original values.
|
||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
|
||||
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
|
||||
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
|
||||
}
|
||||
|
||||
+185
-16
@@ -3,14 +3,24 @@
|
||||
//! file API marshals open/read/write/stat/close into calls to this server's
|
||||
//! endpoint, published under the well-known `vfs` service id).
|
||||
//!
|
||||
//! For now the namespace is a small in-memory ramfs (opening a name creates it):
|
||||
//! enough to prove the whole path — client file API -> IPC -> server dispatch ->
|
||||
//! reply. Device nodes backed by user-space drivers (/device) layer on top in M10,
|
||||
//! where `open` on a /device name forwards to the owning driver's endpoint.
|
||||
//! Two namespaces meet here (M5):
|
||||
//! - a small in-memory **ramfs** — opening a bare name creates it — enough to
|
||||
//! prove the round trip and to back the existing tests;
|
||||
//! - **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 runtime = @import("runtime");
|
||||
const protocol = runtime.vfs_protocol;
|
||||
const path = @import("path.zig");
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
const Node = struct {
|
||||
used: bool = false,
|
||||
@@ -22,15 +32,29 @@ const Node = struct {
|
||||
|
||||
const OpenFile = struct {
|
||||
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,
|
||||
// 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
|
||||
// release-on-death sweeps by: a service must never depend on its clients
|
||||
// cleaning up after themselves (docs/process-lifecycle.md).
|
||||
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 opens = [_]OpenFile{.{}} ** 16;
|
||||
var mounts = [_]Mount{.{}} ** 8;
|
||||
|
||||
fn findNode(name: []const u8) ?usize {
|
||||
for (&nodes, 0..) |*n, i| {
|
||||
@@ -57,6 +81,26 @@ fn openAt(id: u64) ?*OpenFile {
|
||||
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.
|
||||
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
|
||||
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
|
||||
@@ -76,13 +120,96 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// --- mount routing ----------------------------------------------------------
|
||||
|
||||
/// Forward an open under a mount to its backend and, on success, allocate a local
|
||||
/// forwarding handle that remembers the backend's node id.
|
||||
fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize {
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
|
||||
@memcpy(message[protocol.request_size..][0..rel.len], rel);
|
||||
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
|
||||
if (n < protocol.reply_size) return fail(out);
|
||||
const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{});
|
||||
|
||||
for (&opens, 0..) |*o, i| {
|
||||
if (!o.used) {
|
||||
o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender };
|
||||
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
/// Relay a read/write/status/readdir/close on a forwarding handle to the backend
|
||||
/// (the node already rewritten to the backend's id) and copy its reply out.
|
||||
fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize {
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const plen = @min(payload.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..plen], payload[0..plen]);
|
||||
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out);
|
||||
const copy = @min(n, out.len);
|
||||
@memcpy(out[0..copy], reply[0..copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Best-effort close of a backend node (used when a dead client's forwarding
|
||||
/// handles are swept — the backend must not leak the vfs's opens).
|
||||
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
|
||||
const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {};
|
||||
}
|
||||
|
||||
fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize {
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||
m.backend = backend;
|
||||
writeLine("/system/services/vfs: remounted {s}\n", .{prefix});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
for (&mounts) |*m| {
|
||||
if (!m.used) {
|
||||
const l = @min(prefix.len, m.prefix.len);
|
||||
m.used = true;
|
||||
@memcpy(m.prefix[0..l], prefix[0..l]);
|
||||
m.prefix_len = l;
|
||||
m.backend = backend;
|
||||
writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
fn doUnmount(out: []u8, prefix: []const u8) usize {
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||
m.used = false;
|
||||
writeLine("/system/services/vfs: unmounted {s}\n", .{prefix});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
/// Release every open handle `client` held — called on that client's published
|
||||
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers,
|
||||
/// only the dead client's handles go.
|
||||
/// exit event. Forwarding handles also tell their backend to release; local
|
||||
/// nodes (the ramfs files) stay, since ramfs contents outlive their writers.
|
||||
fn releaseClientHandles(client: u32) void {
|
||||
var released: u32 = 0;
|
||||
for (&opens) |*o| {
|
||||
if (o.used and o.owner == client) {
|
||||
if (o.backend) |backend| forwardClose(backend, o.node);
|
||||
o.used = false;
|
||||
released += 1;
|
||||
}
|
||||
@@ -91,19 +218,32 @@ fn releaseClientHandles(client: u32) void {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
_ = capability;
|
||||
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize {
|
||||
if (message.len < protocol.request_size) return fail(out);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
const payload = message[protocol.request_size..];
|
||||
|
||||
switch (request.operation) {
|
||||
.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 => {
|
||||
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);
|
||||
for (&opens, 0..) |*o, i| {
|
||||
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 }, &.{});
|
||||
}
|
||||
}
|
||||
@@ -111,7 +251,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
||||
},
|
||||
.read => {
|
||||
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);
|
||||
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
|
||||
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
|
||||
@@ -119,7 +264,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
||||
},
|
||||
.write => {
|
||||
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);
|
||||
if (off > nd.data.len) return fail(out);
|
||||
const n = @min(@min(payload.len, request.len), nd.data.len - off);
|
||||
@@ -129,11 +279,30 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
||||
},
|
||||
.status => {
|
||||
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));
|
||||
},
|
||||
.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 => {
|
||||
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 }, &.{});
|
||||
},
|
||||
}
|
||||
@@ -142,7 +311,7 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
||||
/// Startup, under the harness: subscribe to the published exit events — when a
|
||||
/// client dies holding open handles, the exit notification is how the VFS learns
|
||||
/// 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)) {
|
||||
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
|
||||
}
|
||||
@@ -152,8 +321,8 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
|
||||
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
||||
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit)));
|
||||
if (badge & ipc.notify_exit_bit != 0) {
|
||||
releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-27
@@ -67,7 +67,14 @@ ARCHES = {
|
||||
"-machine", "q35", "-m", "128M",
|
||||
"-drive", f"if=pflash,format=raw,readonly=on,file={a['ovmf_code']}",
|
||||
"-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",
|
||||
"-vga", "none", "-device", "VGA,edid=on,xres=1280,yres=720",
|
||||
"-display", "none",
|
||||
@@ -275,9 +282,8 @@ CASES = [
|
||||
{"name": "usb-report",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||
"-device", "usb-kbd,bus=xhci.0",
|
||||
"-device", "usb-mouse,bus=xhci.0"],
|
||||
# The xHCI bus + usb-kbd/usb-mouse come from the default boot config now
|
||||
# (every case boots off a usb-storage device on that bus).
|
||||
"expect": r"device-manager: child added[\s\S]*"
|
||||
r"device-manager: child added[\s\S]*"
|
||||
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||
@@ -292,24 +298,37 @@ CASES = [
|
||||
{"name": "usb-hid",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||
"-device", "usb-kbd,bus=xhci.0",
|
||||
"-device", "usb-mouse,bus=xhci.0"],
|
||||
# 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: attach a usb-storage device (a FAT volume via
|
||||
# QEMU's VVFAT, so it has a real boot sector), boot the full tree, and let the
|
||||
# manager spawn 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.
|
||||
# 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,
|
||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||
"-drive", "if=none,id=stick,format=raw,file=fat:rw:" + os.path.join(REPO, "zig-out"),
|
||||
"-device", "usb-storage,drive=stick,bus=xhci.0"],
|
||||
"expect": r"usb-storage: ready[\s\S]*usb-storage: block 0 signature 0x55aa",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# FAT mount end to end: the fat server mounts the boot usb-storage device (the
|
||||
# FAT32 image) into the VFS at /mnt/usb. A fat-test client then lists and reads
|
||||
# through the mount — proof of the whole stack: block device -> FAT parse ->
|
||||
# VFS routing -> file read.
|
||||
{"name": "fat-mount",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"fat: mounted /mnt/usb[\s\S]*fat-test: ok",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# Boot-from-USB smoke: the whole system now boots off the FAT32 image on a
|
||||
# usb-storage device (OVMF -> \EFI\BOOT\BOOTX64.efi -> kernel), so the kernel
|
||||
# reaching its PASS marker at all proves the USB boot path end to end. Reuses
|
||||
# the smoke kernel build; the value is the explicit, named regression guard.
|
||||
{"name": "usb-boot",
|
||||
"build_case": "smoke",
|
||||
"qmp_after": {"delay": 2, "command": "query-status"},
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses
|
||||
# them) finds exactly the Device count the kernel's own parse produced.
|
||||
{"name": "acpi-parse",
|
||||
@@ -355,9 +374,6 @@ CASES = [
|
||||
{"name": "acpi-report",
|
||||
"smp": 4,
|
||||
"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]*"
|
||||
r"acpi: reported PNP0F13 \(device \d+, 1 resources\)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
@@ -374,9 +390,6 @@ CASES = [
|
||||
{"name": "device-list",
|
||||
"smp": 4,
|
||||
"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]*"
|
||||
r"device-list: subscribed[\s\S]*"
|
||||
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||
@@ -389,9 +402,6 @@ CASES = [
|
||||
{"name": "driver-restart",
|
||||
"smp": 4,
|
||||
"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]*"
|
||||
r"device-manager: restarting crash-test[\s\S]*"
|
||||
r"device-manager: crash-test is failing repeatedly",
|
||||
@@ -497,12 +507,15 @@ def qmp_send(path, command):
|
||||
|
||||
|
||||
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:
|
||||
return False, "build failed:\n" + err
|
||||
|
||||
# zig-out is the FHS boot volume; hand it to the guest as-is (see qemu_args).
|
||||
boot_volume = os.path.join(REPO, "zig-out")
|
||||
# The bootable FAT32 USB image the build produced (tools/make-fat-image.py),
|
||||
# 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")
|
||||
shutil.copy(arch["ovmf_vars"], vars_fd)
|
||||
serial = os.path.join(WORK, "serial.log")
|
||||
|
||||
@@ -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