M6: FAT read/write filesystem server, mounted into the VFS
Add a FAT12/16/32 filesystem the VFS mounts at /mnt/usb, reading and writing a USB stick through the block device. Verified end to end under QEMU: the fat server mounts the volume, the VFS routes /mnt/usb to it, and a client lists the root and reads a file (the ELF magic of /mnt/usb/system/kernel). - engine.zig: the FAT engine over a BlockDevice interface — mount (a bare FAT or, as QEMU's VVFAT and most real sticks present it, an MBR-partitioned disk), FAT chain walk (12/16/32), cluster allocation, directory traversal with long-name read, and file read / write / create. Host-tested against a RAM-backed FAT16 image (create, cluster-spanning write, mid-file overwrite, read-back, list). - on-disk.zig: the align(1) boot-sector / directory / long-name / FSInfo structs and the cluster-count FAT-type detection. - fat.zig: the server — wraps the .block device (a DMA bounce buffer) in a BlockDevice, mounts the FAT, serves the vfs-protocol as a backend, and mounts itself into the VFS at /mnt/usb. Spawned by init as a boot service. - runtime.block: the block-device client (geometry / read / write by physical address, so whole sectors never cross IPC). - Raise the kernel service-name registry (maximum_services) 8 -> 16: it is indexed directly by ServiceId, and fat = 8 was being rejected, so the fat server exited before registering. - VFS: an absolute path with no matching mount is now not-found rather than silently created in the flat ramfs — so /mnt/usb fails cleanly until mounted. Tests: fat-mount (the full stack: block -> FAT -> VFS mount -> list + file read) passes; host units cover the engine and on-disk structs; the vfs, shutdown, and USB regression suite stays green (10/10).
This commit is contained in:
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user