diff --git a/build.zig b/build.zig index 7f46488..82b9711 100644 --- a/build.zig +++ b/build.zig @@ -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); @@ -640,6 +651,8 @@ pub fn build(b: *std.Build) void { "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(.{ diff --git a/library/runtime/block.zig b/library/runtime/block.zig new file mode 100644 index 0000000..59968ec --- /dev/null +++ b/library/runtime/block.zig @@ -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; +} diff --git a/library/runtime/runtime.zig b/library/runtime/runtime.zig index 1dc5fb0..a85a799 100644 --- a/library/runtime/runtime.zig +++ b/library/runtime/runtime.zig @@ -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; diff --git a/system/abi.zig b/system/abi.zig index 9c770ab..39dc8bb 100644 --- a/system/abi.zig +++ b/system/abi.zig @@ -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 _, }; diff --git a/system/kernel/ipc-synchronous.zig b/system/kernel/ipc-synchronous.zig index 116f844..748fb21 100644 --- a/system/kernel/ipc-synchronous.zig +++ b/system/kernel/ipc-synchronous.zig @@ -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 diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index c984109..01ba467 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -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) { diff --git a/system/services/fat/engine.zig b/system/services/fat/engine.zig new file mode 100644 index 0000000..98241c4 --- /dev/null +++ b/system/services/fat/engine.zig @@ -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); +} diff --git a/system/services/fat/fat-test.zig b/system/services/fat/fat-test.zig new file mode 100644 index 0000000..136f719 --- /dev/null +++ b/system/services/fat/fat-test.zig @@ -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; +} diff --git a/system/services/fat/fat.zig b/system/services/fat/fat.zig new file mode 100644 index 0000000..a62c58a --- /dev/null +++ b/system/services/fat/fat.zig @@ -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; +} diff --git a/system/services/fat/on-disk.zig b/system/services/fat/on-disk.zig new file mode 100644 index 0000000..e58b73f --- /dev/null +++ b/system/services/fat/on-disk.zig @@ -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()); +} diff --git a/system/services/init/init.zig b/system/services/init/init.zig index 6cfaf38..48d14f0 100644 --- a/system/services/init/init.zig +++ b/system/services/init/init.zig @@ -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; diff --git a/system/services/vfs/vfs.zig b/system/services/vfs/vfs.zig index 0bea186..9a2475f 100644 --- a/system/services/vfs/vfs.zig +++ b/system/services/vfs/vfs.zig @@ -236,6 +236,10 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) .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) { diff --git a/test/qemu_test.py b/test/qemu_test.py index 92a92e3..a9c3a2a 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -310,6 +310,18 @@ CASES = [ "-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: attach a usb-storage device (a FAT volume via VVFAT), + # boot the full tree, and let the fat server mount it 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, + "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"fat: mounted /mnt/usb[\s\S]*fat-test: ok", + "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",