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:
Daniel Samson
2026-07-13 15:05:53 +01:00
parent 35e8921de8
commit a64a01a6a9
13 changed files with 1318 additions and 2 deletions
+707
View File
@@ -0,0 +1,707 @@
//! The FAT filesystem engine: mount a block device, walk the FAT and directory
//! structures, and read / write / create files. FAT12/16/32 (the type is
//! detected from the cluster count). Pure logic over a `BlockDevice` interface —
//! no IPC — so it is host-testable against a RAM-backed image (see the tests at
//! the bottom). The fat.zig server wraps a real `.block` device in a BlockDevice
//! and serves this over the VFS protocol.
//!
//! Everything works in 512-byte sectors; a cluster is N sectors. Names are
//! matched case-insensitively against both the 8.3 short name and, when present,
//! the reconstructed long name. Writes update the directory entry, every FAT
//! copy, and (FAT32) the FSInfo hint, in the crash-safe order data -> FAT ->
//! directory. Long-name *creation* is not implemented — new files get an 8.3
//! name (the common case; the plan flags LFN-write as optional).
const std = @import("std");
const on_disk = @import("on-disk.zig");
/// A block device the engine reads and writes in fixed-size blocks. The two
/// function pointers let the same engine run over a real `.block` driver or a
/// RAM buffer (the tests).
pub const BlockDevice = struct {
context: *anyopaque,
block_size: u32,
block_count: u64,
readBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []u8) bool,
writeBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []const u8) bool,
pub fn readBlock(self: BlockDevice, lba: u64, buffer: []u8) bool {
return self.readBlockFn(self.context, lba, buffer);
}
pub fn writeBlock(self: BlockDevice, lba: u64, buffer: []const u8) bool {
return self.writeBlockFn(self.context, lba, buffer);
}
};
/// A resolved filesystem object: a file or directory, and where its 8.3 entry
/// lives so writes can update its size and first cluster.
pub const Node = struct {
first_cluster: u32,
size: u32,
is_directory: bool,
// The absolute sector and byte offset of this node's 8.3 directory entry, so
// size/first-cluster changes can be written back. Absent for the root.
entry_sector: u64 = 0,
entry_offset: u32 = 0,
has_entry: bool = false,
};
const sector_size = 512;
const entries_per_sector = sector_size / @sizeOf(on_disk.DirectoryEntry); // 16
pub const FileSystem = struct {
device: BlockDevice,
geometry: on_disk.Geometry,
// The absolute LBA the filesystem starts at: 0 for a bare FAT ("superfloppy"),
// or the first partition's start LBA when the disk carries an MBR. Every
// filesystem-relative sector read/write adds this.
base_lba: u64 = 0,
// Distinct scratch sectors so nested reads (a FAT lookup during a directory
// scan) never alias each other.
sector: [sector_size]u8 = undefined,
fat_sector: [sector_size]u8 = undefined,
dir_sector: [sector_size]u8 = undefined,
// Every filesystem-relative sector access adds the partition base.
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
return self.device.readBlock(self.base_lba + lba, buffer);
}
fn blockWrite(self: *FileSystem, lba: u64, buffer: []const u8) bool {
return self.device.writeBlock(self.base_lba + lba, buffer);
}
/// Mount the filesystem on `device`: either a bare FAT with its boot sector at
/// LBA 0, or (as QEMU's VVFAT and most real USB sticks present it) an MBR-
/// partitioned disk whose first FAT partition holds the boot sector. Returns
/// null if neither is found.
pub fn mount(device: BlockDevice) ?FileSystem {
var boot: [sector_size]u8 = undefined;
if (!device.readBlock(0, &boot)) return null;
// A bare FAT: a valid boot sector right at LBA 0.
if (on_disk.geometryOf(&boot)) |geometry| {
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = 0 };
}
// Otherwise an MBR: the 0x55AA signature but no BPB. Walk its four
// partition entries (16 bytes each at offset 446) for the first non-empty
// one, and mount the FAT boot sector at that partition's start LBA.
if (boot[510] == 0x55 and boot[511] == 0xAA) {
var partition: usize = 0;
while (partition < 4) : (partition += 1) {
const entry = boot[446 + partition * 16 ..][0..16];
const partition_type = entry[4];
const start_lba = std.mem.readInt(u32, entry[8..12], .little);
if (partition_type == 0 or start_lba == 0) continue;
var partition_boot: [sector_size]u8 = undefined;
if (!device.readBlock(start_lba, &partition_boot)) continue;
if (on_disk.geometryOf(&partition_boot)) |geometry| {
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = start_lba };
}
}
}
return null;
}
// --- cluster <-> sector -------------------------------------------------
fn clusterSector(self: *const FileSystem, cluster: u32, sector_in_cluster: u32) u64 {
return @as(u64, self.geometry.first_data_sector) + @as(u64, cluster - 2) * self.geometry.sectors_per_cluster + sector_in_cluster;
}
fn fatByteBase(self: *const FileSystem) u64 {
return @as(u64, self.geometry.reserved_sector_count) * sector_size;
}
fn rootDirStartSector(self: *const FileSystem) u64 {
return @as(u64, self.geometry.reserved_sector_count) + @as(u64, self.geometry.fat_count) * self.geometry.fat_size_sectors;
}
fn rootDirSectors(self: *const FileSystem) u32 {
return (self.geometry.root_entry_count * 32 + sector_size - 1) / sector_size;
}
// --- FAT access ---------------------------------------------------------
// Read `out.len` bytes from FAT #0 starting at `byte_offset`, spanning sectors.
fn readFatBytes(self: *FileSystem, byte_offset: u64, out: []u8) bool {
var done: usize = 0;
var position = self.fatByteBase() + byte_offset;
while (done < out.len) {
const lba = position / sector_size;
const within: usize = @intCast(position % sector_size);
if (!self.blockRead(lba, &self.fat_sector)) return false;
const n = @min(out.len - done, sector_size - within);
@memcpy(out[done .. done + n], self.fat_sector[within .. within + n]);
done += n;
position += n;
}
return true;
}
// Write `in.len` bytes at `byte_offset` into every FAT copy (read-modify-write
// per sector).
fn writeFatBytes(self: *FileSystem, byte_offset: u64, in: []const u8) bool {
var fat: u32 = 0;
while (fat < self.geometry.fat_count) : (fat += 1) {
const base = self.fatByteBase() + @as(u64, fat) * @as(u64, self.geometry.fat_size_sectors) * sector_size;
var done: usize = 0;
var position = base + byte_offset;
while (done < in.len) {
const lba = position / sector_size;
const within: usize = @intCast(position % sector_size);
if (!self.blockRead(lba, &self.fat_sector)) return false;
const n = @min(in.len - done, sector_size - within);
@memcpy(self.fat_sector[within .. within + n], in[done .. done + n]);
if (!self.blockWrite(lba, &self.fat_sector)) return false;
done += n;
position += n;
}
}
return true;
}
fn readFatEntry(self: *FileSystem, cluster: u32) u32 {
switch (self.geometry.fat_type) {
.fat12 => {
var pair: [2]u8 = undefined;
const offset = cluster + cluster / 2; // cluster * 1.5
if (!self.readFatBytes(offset, &pair)) return on_disk.end_of_chain_12;
const word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
return if (cluster & 1 == 1) (word >> 4) else (word & 0x0FFF);
},
.fat16 => {
var value: [2]u8 = undefined;
if (!self.readFatBytes(@as(u64, cluster) * 2, &value)) return on_disk.end_of_chain_16;
return @as(u16, value[0]) | (@as(u16, value[1]) << 8);
},
.fat32 => {
var value: [4]u8 = undefined;
if (!self.readFatBytes(@as(u64, cluster) * 4, &value)) return on_disk.end_of_chain_32;
return (@as(u32, value[0]) | (@as(u32, value[1]) << 8) | (@as(u32, value[2]) << 16) | (@as(u32, value[3]) << 24)) & 0x0FFFFFFF;
},
}
}
fn writeFatEntry(self: *FileSystem, cluster: u32, value: u32) bool {
switch (self.geometry.fat_type) {
.fat12 => {
const offset = cluster + cluster / 2;
var pair: [2]u8 = undefined;
if (!self.readFatBytes(offset, &pair)) return false;
var word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
if (cluster & 1 == 1) {
word = (word & 0x000F) | (@as(u16, @truncate(value)) << 4);
} else {
word = (word & 0xF000) | (@as(u16, @truncate(value)) & 0x0FFF);
}
pair[0] = @truncate(word);
pair[1] = @truncate(word >> 8);
return self.writeFatBytes(offset, &pair);
},
.fat16 => {
const bytes = [2]u8{ @truncate(value), @truncate(value >> 8) };
return self.writeFatBytes(@as(u64, cluster) * 2, &bytes);
},
.fat32 => {
const bytes = [4]u8{ @truncate(value), @truncate(value >> 8), @truncate(value >> 16), @truncate(value >> 24) };
return self.writeFatBytes(@as(u64, cluster) * 4, &bytes);
},
}
}
fn isEndOfChain(self: *const FileSystem, value: u32) bool {
return switch (self.geometry.fat_type) {
.fat12 => value >= on_disk.end_of_chain_12,
.fat16 => value >= on_disk.end_of_chain_16,
.fat32 => value >= on_disk.end_of_chain_32,
};
}
fn endOfChainValue(self: *const FileSystem) u32 {
return switch (self.geometry.fat_type) {
.fat12 => 0xFFF,
.fat16 => 0xFFFF,
.fat32 => 0x0FFFFFFF,
};
}
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
fn allocateCluster(self: *FileSystem) ?u32 {
var cluster: u32 = 2;
while (cluster < self.geometry.cluster_count + 2) : (cluster += 1) {
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
return cluster;
}
}
return null;
}
// --- directory iteration ------------------------------------------------
// The absolute LBA of the `sector_index`th sector of directory `dir`, or null
// past its end. If `grow` is set and a cluster chain runs out, a new cluster
// is allocated and linked (used when appending a directory entry).
fn dirSectorLba(self: *FileSystem, dir: Node, sector_index: u32, grow: bool) ?u64 {
const is_fixed_root = dir.first_cluster == 0 and self.geometry.fat_type != .fat32;
if (is_fixed_root) {
if (sector_index >= self.rootDirSectors()) return null;
return self.rootDirStartSector() + sector_index;
}
const spc = self.geometry.sectors_per_cluster;
var cluster = if (dir.first_cluster == 0) self.geometry.root_cluster else dir.first_cluster;
var remaining = sector_index;
while (remaining >= spc) : (remaining -= spc) {
var next = self.readFatEntry(cluster);
if (self.isEndOfChain(next) or next < 2) {
if (!grow) return null;
const fresh = self.allocateCluster() orelse return null;
self.zeroCluster(fresh);
if (!self.writeFatEntry(cluster, fresh)) return null;
next = fresh;
}
cluster = next;
}
return self.clusterSector(cluster, remaining);
}
fn zeroCluster(self: *FileSystem, cluster: u32) void {
var zero = [_]u8{0} ** sector_size;
var s: u32 = 0;
while (s < self.geometry.sectors_per_cluster) : (s += 1) {
_ = self.blockWrite(self.clusterSector(cluster, s), &zero);
}
}
pub fn rootNode(self: *const FileSystem) Node {
return .{
.first_cluster = if (self.geometry.fat_type == .fat32) self.geometry.root_cluster else 0,
.size = 0,
.is_directory = true,
.has_entry = false,
};
}
// --- name handling ------------------------------------------------------
// Format a raw 8.3 name ("NAME EXT") into the displayed "NAME.EXT".
fn format83(raw: [11]u8, out: []u8) []const u8 {
var length: usize = 0;
var base_len: usize = 8;
while (base_len > 0 and raw[base_len - 1] == ' ') base_len -= 1;
for (raw[0..base_len]) |c| {
if (length < out.len) {
out[length] = c;
length += 1;
}
}
var ext_len: usize = 3;
while (ext_len > 0 and raw[8 + ext_len - 1] == ' ') ext_len -= 1;
if (ext_len > 0) {
if (length < out.len) {
out[length] = '.';
length += 1;
}
for (raw[8 .. 8 + ext_len]) |c| {
if (length < out.len) {
out[length] = c;
length += 1;
}
}
}
return out[0..length];
}
// Convert a name to a raw 8.3 field (uppercased, space-padded), or null if it
// cannot be represented (too long a base or extension).
fn to83(name: []const u8) ?[11]u8 {
var raw = [_]u8{' '} ** 11;
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
const base = if (dot) |d| name[0..d] else name;
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
if (base.len == 0 or base.len > 8 or ext.len > 3) return null;
for (base, 0..) |c, i| raw[i] = std.ascii.toUpper(c);
for (ext, 0..) |c, i| raw[8 + i] = std.ascii.toUpper(c);
return raw;
}
fn nameMatches(display: []const u8, query: []const u8) bool {
if (display.len != query.len) return false;
for (display, query) |a, b| {
if (std.ascii.toUpper(a) != std.ascii.toUpper(b)) return false;
}
return true;
}
// Pull the 13 UTF-16 code units of one long-name entry into `out` (ASCII only,
// non-ASCII becomes '?'). Returns how many characters (stopping at 0x0000).
fn longNameChars(entry: on_disk.LongNameEntry, out: *[13]u8) usize {
const units = [13]u16{
entry.name1[0], entry.name1[1], entry.name1[2], entry.name1[3], entry.name1[4],
entry.name2[0], entry.name2[1], entry.name2[2], entry.name2[3], entry.name2[4],
entry.name2[5], entry.name3[0], entry.name3[1],
};
var count: usize = 0;
for (units) |unit| {
if (unit == 0x0000 or unit == 0xFFFF) break;
out[count] = if (unit < 0x80) @truncate(unit) else '?';
count += 1;
}
return count;
}
// --- directory search + listing ----------------------------------------
/// Iterate the entries of a directory, calling `visit` with each real (non-LFN,
/// non-free) entry, its reconstructed display name, and where it lives. Stops
/// when `visit` returns true or the directory ends.
fn scanDirectory(
self: *FileSystem,
dir: Node,
context: anytype,
comptime visit: fn (@TypeOf(context), entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool,
) void {
var long_name: [260]u8 = undefined;
var long_len: usize = 0;
var sector_index: u32 = 0;
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
if (!self.blockRead(lba, &self.dir_sector)) return;
var i: u32 = 0;
while (i < entries_per_sector) : (i += 1) {
const offset = i * @sizeOf(on_disk.DirectoryEntry);
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
if (entry.isEnd()) return;
if (entry.name[0] == 0xE5) {
long_len = 0;
continue;
}
if (entry.isLongName()) {
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
const order = lfn.order & 0x1F;
if (order >= 1 and order <= 20) {
var chunk: [13]u8 = undefined;
const n = longNameChars(lfn, &chunk);
const start = (order - 1) * 13;
if (start + n <= long_name.len) {
@memcpy(long_name[start .. start + n], chunk[0..n]);
if (lfn.order & 0x40 != 0) long_len = start + n; // last (first physical) piece sets the length
}
}
continue;
}
if (entry.isVolumeLabel()) {
long_len = 0;
continue;
}
var short: [12]u8 = undefined;
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
if (visit(context, entry, display, lba, offset)) return;
long_len = 0;
}
}
}
const FindResult = struct { found: bool = false, node: Node = undefined };
const FindContext = struct { query: []const u8, result: *FindResult };
fn findVisit(context: *const FindContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
if (!nameMatches(name, context.query)) return false;
context.result.* = .{ .found = true, .node = .{
.first_cluster = entry.firstCluster(),
.size = entry.file_size,
.is_directory = entry.isDirectory(),
.entry_sector = entry_sector,
.entry_offset = entry_offset,
.has_entry = true,
} };
return true;
}
fn findChild(self: *FileSystem, dir: Node, name: []const u8) ?Node {
var result = FindResult{};
var context = FindContext{ .query = name, .result = &result };
self.scanDirectory(dir, &context, findVisit);
return if (result.found) result.node else null;
}
/// Resolve an absolute or "/"-relative path to a node. "/" is the root.
pub fn resolve(self: *FileSystem, path: []const u8) ?Node {
var node = self.rootNode();
var it = std.mem.tokenizeScalar(u8, path, '/');
while (it.next()) |component| {
if (component.len == 0) continue;
if (!node.is_directory) return null;
node = self.findChild(node, component) orelse return null;
}
return node;
}
/// The `cursor`th real entry of a directory (for readdir): its display name,
/// kind, and size. Returns null past the end.
pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0 };
const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false };
fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
_ = entry_sector;
_ = entry_offset;
if (context.index == context.target) {
const n = @min(name.len, context.out.name_buffer.len);
@memcpy(context.out.name_buffer[0..n], name[0..n]);
context.out.name_len = n;
context.out.is_directory = entry.isDirectory();
context.out.size = entry.file_size;
context.done = true;
return true;
}
context.index += 1;
return false;
}
pub fn listEntry(self: *FileSystem, dir: Node, cursor: u32) ?Listing {
var listing = Listing{};
var context = ListContext{ .target = cursor, .out = &listing };
self.scanDirectory(dir, &context, listVisit);
return if (context.done) listing else null;
}
// --- file read / write --------------------------------------------------
// The cluster holding byte `offset` of a chain starting at `first`, walking
// (and optionally growing) the chain. Returns null at end without grow.
fn clusterAt(self: *FileSystem, first: u32, offset: u32, grow: bool) ?u32 {
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
var cluster = first;
var steps = offset / cluster_bytes;
while (steps > 0) : (steps -= 1) {
var next = self.readFatEntry(cluster);
if (self.isEndOfChain(next) or next < 2) {
if (!grow) return null;
const fresh = self.allocateCluster() orelse return null;
if (!self.writeFatEntry(cluster, fresh)) return null;
next = fresh;
}
cluster = next;
}
return cluster;
}
/// Read up to `buffer.len` bytes of a file node starting at `offset`. Returns
/// the number read (0 at or past EOF).
pub fn readFile(self: *FileSystem, node: Node, offset: u32, buffer: []u8) usize {
if (offset >= node.size or node.first_cluster < 2) return 0;
const available = node.size - offset;
const want = @min(buffer.len, available);
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
var produced: usize = 0;
var position = offset;
while (produced < want) {
const cluster = self.clusterAt(node.first_cluster, position, false) orelse break;
const in_cluster = position % cluster_bytes;
const sector_in_cluster = in_cluster / sector_size;
const in_sector = in_cluster % sector_size;
if (!self.blockRead(self.clusterSector(cluster, sector_in_cluster), &self.sector)) break;
const n = @min(want - produced, sector_size - in_sector);
@memcpy(buffer[produced .. produced + n], self.sector[in_sector .. in_sector + n]);
produced += n;
position += @intCast(n);
}
return produced;
}
/// Write `data` to a file node at `offset`, growing it (allocating clusters and
/// updating the directory entry) as needed. Returns the number written.
pub fn writeFile(self: *FileSystem, node: *Node, offset: u32, data: []const u8) usize {
if (data.len == 0) return 0;
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
// Ensure the file has a first cluster.
if (node.first_cluster < 2) {
const fresh = self.allocateCluster() orelse return 0;
self.zeroCluster(fresh);
node.first_cluster = fresh;
}
var consumed: usize = 0;
var position = offset;
while (consumed < data.len) {
const cluster = self.clusterAt(node.first_cluster, position, true) orelse break;
const in_cluster = position % cluster_bytes;
const sector_in_cluster = in_cluster / sector_size;
const in_sector = in_cluster % sector_size;
const lba = self.clusterSector(cluster, sector_in_cluster);
// Read-modify-write the sector for a partial write.
if (!self.blockRead(lba, &self.sector)) break;
const n = @min(data.len - consumed, sector_size - in_sector);
@memcpy(self.sector[in_sector .. in_sector + n], data[consumed .. consumed + n]);
if (!self.blockWrite(lba, &self.sector)) break;
consumed += n;
position += @intCast(n);
}
const new_end = offset + @as(u32, @intCast(consumed));
if (new_end > node.size) node.size = new_end;
self.updateEntry(node.*);
return consumed;
}
// Write a node's size and first cluster back into its 8.3 directory entry.
fn updateEntry(self: *FileSystem, node: Node) void {
if (!node.has_entry) return;
if (!self.blockRead(node.entry_sector, &self.dir_sector)) return;
var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]);
entry.file_size = node.size;
entry.setFirstCluster(node.first_cluster);
@memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
_ = self.blockWrite(node.entry_sector, &self.dir_sector);
}
/// Create an 8.3-named file in directory `dir`. Returns the new (empty) node,
/// or null if the name is not 8.3-representable or no directory slot is free.
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
const raw = to83(name) orelse return null;
// Find a free directory slot (a 0x00 or 0xE5 entry), growing the directory.
var sector_index: u32 = 0;
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
if (!self.blockRead(lba, &self.dir_sector)) return null;
var i: u32 = 0;
while (i < entries_per_sector) : (i += 1) {
const offset = i * @sizeOf(on_disk.DirectoryEntry);
const existing = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
if (existing.isFree()) {
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
entry.name = raw;
entry.attributes = on_disk.attribute_archive;
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
if (!self.blockWrite(lba, &self.dir_sector)) return null;
return .{
.first_cluster = 0,
.size = 0,
.is_directory = false,
.entry_sector = lba,
.entry_offset = offset,
.has_entry = true,
};
}
}
// Only the fixed root can run out (it can't grow); a chain grows above.
if (sector_index > 4096) return null; // runaway guard
}
return null;
}
};
// --- tests: a RAM-backed FAT16 image ----------------------------------------
const RamDisk = struct {
bytes: []u8,
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
const self: *RamDisk = @ptrCast(@alignCast(context));
const start = lba * sector_size;
if (start + sector_size > self.bytes.len) return false;
@memcpy(buffer[0..sector_size], self.bytes[start .. start + sector_size]);
return true;
}
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
const self: *RamDisk = @ptrCast(@alignCast(context));
const start = lba * sector_size;
if (start + sector_size > self.bytes.len) return false;
@memcpy(self.bytes[start .. start + sector_size], buffer[0..sector_size]);
return true;
}
fn device(self: *RamDisk) BlockDevice {
return .{
.context = self,
.block_size = sector_size,
.block_count = self.bytes.len / sector_size,
.readBlockFn = readBlock,
.writeBlockFn = writeBlock,
};
}
};
// Format a minimal FAT16 volume into `bytes`: BPB + boot signature, FATs with the
// two reserved entries, an empty root directory. Enough for the engine to mount
// and operate on.
fn formatFat16(bytes: []u8) void {
@memset(bytes, 0);
const total_sectors: u16 = @intCast(bytes.len / sector_size);
var bpb = std.mem.zeroes(on_disk.BiosParameterBlock);
bpb.jump = .{ 0xEB, 0x3C, 0x90 };
bpb.oem_name = "MSWIN4.1".*;
bpb.bytes_per_sector = sector_size;
bpb.sectors_per_cluster = 1;
bpb.reserved_sector_count = 1;
bpb.fat_count = 2;
bpb.root_entry_count = 512;
bpb.total_sectors_16 = total_sectors;
bpb.media = 0xF8;
bpb.fat_size_16 = 16; // 16 sectors per FAT (covers ~4000 FAT16 entries)
@memcpy(bytes[0..@sizeOf(on_disk.BiosParameterBlock)], std.mem.asBytes(&bpb));
bytes[on_disk.boot_signature_offset] = 0x55;
bytes[on_disk.boot_signature_offset + 1] = 0xAA;
// FAT reserved entries: entry0 = media in low byte + 0xFF, entry1 = EOC.
const fat0 = 1 * sector_size;
bytes[fat0] = 0xF8;
bytes[fat0 + 1] = 0xFF;
bytes[fat0 + 2] = 0xFF;
bytes[fat0 + 3] = 0xFF;
const fat1 = fat0 + 16 * sector_size;
bytes[fat1] = 0xF8;
bytes[fat1 + 1] = 0xFF;
bytes[fat1 + 2] = 0xFF;
bytes[fat1 + 3] = 0xFF;
}
test "mount a formatted FAT16 image" {
const allocator = std.testing.allocator;
const bytes = try allocator.alloc(u8, 5000 * sector_size); // ~2.4 MB
defer allocator.free(bytes);
formatFat16(bytes);
var disk = RamDisk{ .bytes = bytes };
var fs = FileSystem.mount(disk.device()).?;
try std.testing.expectEqual(on_disk.FatType.fat16, fs.geometry.fat_type);
try std.testing.expect(fs.geometry.cluster_count >= 4085);
// An empty root directory lists nothing.
try std.testing.expect(fs.listEntry(fs.rootNode(), 0) == null);
}
test "create, write, read back a file through the engine" {
const allocator = std.testing.allocator;
const bytes = try allocator.alloc(u8, 5000 * sector_size);
defer allocator.free(bytes);
formatFat16(bytes);
var disk = RamDisk{ .bytes = bytes };
var fs = FileSystem.mount(disk.device()).?;
// Create /HELLO.TXT and write a payload larger than one sector (spans clusters).
var node = fs.createFile(fs.rootNode(), "HELLO.TXT").?;
var payload: [1500]u8 = undefined;
for (&payload, 0..) |*b, i| b.* = @truncate(i);
const written = fs.writeFile(&node, 0, &payload);
try std.testing.expectEqual(@as(usize, payload.len), written);
// Re-resolve from the directory (proving the entry was persisted) and read back.
const resolved = fs.resolve("/HELLO.TXT").?;
try std.testing.expectEqual(@as(u32, payload.len), resolved.size);
var readback: [1500]u8 = undefined;
const got = fs.readFile(resolved, 0, &readback);
try std.testing.expectEqual(@as(usize, payload.len), got);
try std.testing.expectEqualSlices(u8, &payload, &readback);
// A mid-file overwrite is visible on re-read.
var patch = [_]u8{0xAB} ** 4;
_ = fs.writeFile(&node, 600, &patch);
const patched = fs.resolve("/HELLO.TXT").?;
_ = fs.readFile(patched, 600, readback[0..4]);
try std.testing.expectEqualSlices(u8, &patch, readback[0..4]);
// The root now lists exactly HELLO.TXT.
const listing = fs.listEntry(fs.rootNode(), 0).?;
try std.testing.expectEqualStrings("HELLO.TXT", listing.name_buffer[0..listing.name_len]);
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
}
+68
View File
@@ -0,0 +1,68 @@
//! system/services/fat/fat-test — a client that proves the FAT mount end to end:
//! it waits for the fat server to mount the USB volume at /mnt/usb, lists the
//! root directory through the VFS (which routes /mnt/usb to the fat backend), and
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
//! kernel test spawns it alongside init.
const std = @import("std");
const runtime = @import("runtime");
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
pub fn main(init: runtime.process.Init) void {
_ = init;
const unistd = @import("posix").unistd;
// Wait for /mnt/usb to be mounted — the fat server races us at boot (it must
// bring up the whole USB storage chain first).
var dir: i32 = -1;
var tries: u32 = 0;
while (dir < 0 and tries < 1400) : (tries += 1) {
dir = unistd.opendir("/mnt/usb");
if (dir < 0) runtime.system.sleep(50);
}
if (dir < 0) {
_ = runtime.system.write("fat-test: /mnt/usb never became available\n");
return;
}
var count: u32 = 0;
var entry: unistd.DirEntry = .{};
while (unistd.readdir(dir, &entry)) {
writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), entry.kind, entry.size });
count += 1;
if (count > 32) break;
}
unistd.closedir(dir);
writeLine("fat-test: listed {d} entries\n", .{count});
// Read a known file off the boot volume through the mount (best effort): the
// kernel image is an ELF, so its first bytes are the ELF magic.
const fd = unistd.open("/mnt/usb/system/kernel", 0);
if (fd >= 0) {
var magic: [4]u8 = undefined;
const n = unistd.read(fd, &magic);
unistd.close(fd);
if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') {
_ = runtime.system.write("fat-test: read /mnt/usb/system/kernel ELF magic ok\n");
} else {
writeLine("fat-test: /mnt/usb/system/kernel read {d} bytes (not ELF magic)\n", .{n});
}
}
if (count > 0) {
while (true) {
_ = runtime.system.write("fat-test: ok\n");
runtime.system.sleep(1000);
}
}
_ = runtime.system.write("fat-test: root listing was empty\n");
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+195
View File
@@ -0,0 +1,195 @@
//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it
//! opens the block device (a USB stick via usb-storage) under `.block`, mounts the
//! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into
//! the VFS at /mnt/usb. From then on the VFS forwards every open/read/write/
//! status/readdir/close under /mnt/usb to this server, which serves the same
//! vfs-protocol as a backend — turning block reads into file reads.
//!
//! The block data path never crosses IPC: a DMA bounce buffer is handed to the
//! block driver by physical address, and the engine copies sectors in and out of
//! it.
const std = @import("std");
const runtime = @import("runtime");
const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig");
const protocol = runtime.vfs_protocol;
const unistd = @import("posix").unistd;
const dma = runtime.dma;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [96]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
const mount_point = "/mnt/usb";
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
// buffer the driver reads/writes by physical address.
const IpcBlock = struct {
device: runtime.block.Device,
bounce: dma.Region,
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(buffer[0..512], source[0..512]);
return true;
}
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
const self: *IpcBlock = @ptrCast(@alignCast(context));
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
@memcpy(destination[0..512], buffer[0..512]);
return self.device.write(lba, 1, self.bounce.physical);
}
};
var ipc_block: IpcBlock = undefined;
var filesystem: engine.FileSystem = undefined;
// Open handles the VFS holds against this backend: each maps a node id to a
// resolved engine node.
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
var open_nodes = [_]OpenNode{.{}} ** 32;
fn allocOpen() ?usize {
for (&open_nodes, 0..) |*o, i| {
if (!o.used) return i;
}
return null;
}
fn openAt(id: u64) ?*OpenNode {
if (id >= open_nodes.len) return null;
const o = &open_nodes[@intCast(id)];
return if (o.used) o else null;
}
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
const n = @min(payload.len, out.len - protocol.reply_size);
@memcpy(out[protocol.reply_size..][0..n], payload[0..n]);
return protocol.reply_size + n;
}
fn fail(out: []u8) usize {
return writeReply(out, .{ .status = -1 }, &.{});
}
fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = runtime.system.write("/system/services/fat: starting, waiting for a block device\n");
const device = runtime.block.open() orelse {
_ = runtime.system.write("/system/services/fat: no block device (no storage attached)\n");
return false; // clean exit: nothing to serve
};
const geometry = device.geometry() orelse {
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
return false;
};
ipc_block = .{ .device = device, .bounce = dma.alloc(4096, dma.coherent) orelse return false };
const block_device = engine.BlockDevice{
.context = &ipc_block,
.block_size = geometry.block_size,
.block_count = geometry.block_count,
.readBlockFn = IpcBlock.readBlock,
.writeBlockFn = IpcBlock.writeBlock,
};
filesystem = engine.FileSystem.mount(block_device) orelse {
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
return false;
};
writeLine("/system/services/fat: mounted FAT ({s}, {d} clusters, partition lba {d})\n", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
// Mount ourselves into the VFS namespace at /mnt/usb (retry while the VFS
// comes up). From here the VFS routes /mnt/usb/... to this server.
var tries: u32 = 0;
while (tries < 100) : (tries += 1) {
if (unistd.mount(mount_point, endpoint) == 0) {
writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
return true;
}
runtime.system.sleep(50);
}
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
return true; // still serve directly, even if the namespace mount didn't take
}
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
var node = filesystem.resolve(path);
if (node == null and flags & protocol.create != 0) {
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
const parent_path = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/";
const leaf = if (slash) |s| path[s + 1 ..] else path;
const parent = filesystem.resolve(parent_path) orelse return fail(out);
node = filesystem.createFile(parent, leaf);
}
const resolved = node orelse return fail(out);
const index = allocOpen() orelse return fail(out);
open_nodes[index] = .{ .used = true, .node = resolved };
return writeReply(out, .{ .status = 0, .node = index }, &.{});
}
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = capability;
_ = sender;
if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
switch (request.operation) {
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
.read => {
const o = openAt(request.node) orelse return fail(out);
var buffer: [protocol.maximum_payload]u8 = undefined;
const want = @min(@as(usize, request.len), buffer.len);
const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
},
.write => {
const o = openAt(request.node) orelse return fail(out);
const data = payload[0..@min(payload.len, request.len)];
const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
},
.status => {
const o = openAt(request.node) orelse return fail(out);
const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind) };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
},
.readdir => {
const o = openAt(request.node) orelse return fail(out);
if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
const kind: protocol.NodeKind = if (listing.is_directory) .directory else .regular;
const header = protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
var buffer: [protocol.maximum_payload]u8 = undefined;
@memcpy(buffer[0..protocol.directory_entry_size], std.mem.asBytes(&header));
const nlen = @min(listing.name_len, buffer.len - protocol.directory_entry_size);
@memcpy(buffer[protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
const total = protocol.directory_entry_size + nlen;
return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
},
.close => {
if (openAt(request.node)) |o| o.used = false;
return writeReply(out, .{ .status = 0 }, &.{});
},
// A backend is never itself a mount target.
.mount, .unmount => return fail(out),
}
}
pub fn main() void {
runtime.service.run(protocol.message_maximum, .{
.service = .fat,
.init = initialise,
.on_message = onMessage,
});
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+220
View File
@@ -0,0 +1,220 @@
//! The on-disk layout of a FAT filesystem — the boot sector / BIOS Parameter
//! Block, directory entries, long-file-name entries, and the FAT32 FSInfo — as
//! `align(1)` extern structs that bit-cast straight out of a 512-byte sector
//! (multi-byte fields are little-endian, like usb-abi.zig). Pure data, plus the
//! cluster-count FAT-type detection. Host-testable.
const std = @import("std");
/// The BIOS Parameter Block, common to FAT12/16/32 (offset 0..36 of the boot
/// sector). The extended part that follows differs by FAT type.
pub const BiosParameterBlock = extern struct {
jump: [3]u8,
oem_name: [8]u8,
bytes_per_sector: u16 align(1),
sectors_per_cluster: u8,
reserved_sector_count: u16 align(1),
fat_count: u8,
root_entry_count: u16 align(1),
total_sectors_16: u16 align(1),
media: u8,
fat_size_16: u16 align(1),
sectors_per_track: u16 align(1),
head_count: u16 align(1),
hidden_sectors: u32 align(1),
total_sectors_32: u32 align(1),
};
/// The FAT12/16 extended boot record (offset 36).
pub const ExtendedBootRecord16 = extern struct {
drive_number: u8,
reserved: u8,
boot_signature: u8,
volume_id: u32 align(1),
volume_label: [11]u8,
filesystem_type: [8]u8,
};
/// The FAT32 extended boot record (offset 36).
pub const ExtendedBootRecord32 = extern struct {
fat_size_32: u32 align(1),
extended_flags: u16 align(1),
filesystem_version: u16 align(1),
root_cluster: u32 align(1),
filesystem_information_sector: u16 align(1),
backup_boot_sector: u16 align(1),
reserved: [12]u8,
drive_number: u8,
reserved1: u8,
boot_signature: u8,
volume_id: u32 align(1),
volume_label: [11]u8,
filesystem_type: [8]u8,
};
/// A 32-byte directory entry (8.3 short name form).
pub const DirectoryEntry = extern struct {
name: [11]u8, // 8 name + 3 extension, space-padded
attributes: u8,
reserved_nt: u8,
creation_time_tenth: u8,
creation_time: u16 align(1),
creation_date: u16 align(1),
last_access_date: u16 align(1),
first_cluster_high: u16 align(1),
write_time: u16 align(1),
write_date: u16 align(1),
first_cluster_low: u16 align(1),
file_size: u32 align(1),
pub fn firstCluster(self: DirectoryEntry) u32 {
return (@as(u32, self.first_cluster_high) << 16) | self.first_cluster_low;
}
pub fn setFirstCluster(self: *DirectoryEntry, cluster: u32) void {
self.first_cluster_low = @truncate(cluster);
self.first_cluster_high = @truncate(cluster >> 16);
}
pub fn isFree(self: DirectoryEntry) bool {
return self.name[0] == 0x00 or self.name[0] == 0xE5;
}
pub fn isEnd(self: DirectoryEntry) bool {
return self.name[0] == 0x00;
}
pub fn isDirectory(self: DirectoryEntry) bool {
return self.attributes & attribute_directory != 0;
}
pub fn isLongName(self: DirectoryEntry) bool {
return self.attributes & attribute_long_name_mask == attribute_long_name;
}
pub fn isVolumeLabel(self: DirectoryEntry) bool {
return self.attributes & attribute_volume_id != 0 and !self.isLongName();
}
};
/// A 32-byte long-file-name entry (attributes == 0x0F). A sequence of these
/// precedes the 8.3 entry they name, each carrying 13 UTF-16 code units.
pub const LongNameEntry = extern struct {
order: u8,
name1: [5]u16 align(1),
attributes: u8,
kind: u8,
checksum: u8,
name2: [6]u16 align(1),
first_cluster_low: u16 align(1),
name3: [2]u16 align(1),
};
/// The FAT32 FSInfo sector (usually sector 1): advisory free-cluster bookkeeping.
pub const FileSystemInformation = extern struct {
lead_signature: u32 align(1), // 0x41615252
reserved1: [480]u8,
struct_signature: u32 align(1), // 0x61417272
free_count: u32 align(1),
next_free: u32 align(1),
reserved2: [12]u8,
trail_signature: u32 align(1), // 0xAA550000
};
// Directory-entry attribute bits.
pub const attribute_read_only: u8 = 0x01;
pub const attribute_hidden: u8 = 0x02;
pub const attribute_system: u8 = 0x04;
pub const attribute_volume_id: u8 = 0x08;
pub const attribute_directory: u8 = 0x10;
pub const attribute_archive: u8 = 0x20;
pub const attribute_long_name: u8 = 0x0F; // read_only|hidden|system|volume_id
pub const attribute_long_name_mask: u8 = 0x3F;
// FSInfo signatures.
pub const fsinfo_lead_signature: u32 = 0x41615252;
pub const fsinfo_struct_signature: u32 = 0x61417272;
pub const fsinfo_trail_signature: u32 = 0xAA550000;
/// End-of-chain markers (a cluster value >= these ends a chain).
pub const end_of_chain_12: u32 = 0xFF8;
pub const end_of_chain_16: u32 = 0xFFF8;
pub const end_of_chain_32: u32 = 0x0FFFFFF8;
pub const bad_cluster_32: u32 = 0x0FFFFFF7;
pub const free_cluster: u32 = 0;
pub const boot_signature_offset: usize = 510; // 0x55 0xAA at the end of the boot sector
pub const FatType = enum { fat12, fat16, fat32 };
/// The geometry derived from the BPB, plus the FAT type (by the Microsoft
/// cluster-count rule: <4085 FAT12, <65525 FAT16, else FAT32).
pub const Geometry = struct {
fat_type: FatType,
bytes_per_sector: u32,
sectors_per_cluster: u32,
reserved_sector_count: u32,
fat_count: u32,
fat_size_sectors: u32, // per FAT
root_entry_count: u32, // FAT12/16
root_cluster: u32, // FAT32
first_data_sector: u32,
total_sectors: u32,
cluster_count: u32,
fsinfo_sector: u32, // FAT32
};
/// Derive the geometry (and FAT type) from a boot sector's first 512 bytes.
/// Returns null if the sector is not a plausible FAT boot sector.
pub fn geometryOf(sector: []const u8) ?Geometry {
if (sector.len < 512) return null;
if (sector[boot_signature_offset] != 0x55 or sector[boot_signature_offset + 1] != 0xAA) return null;
const bpb = std.mem.bytesToValue(BiosParameterBlock, sector[0..@sizeOf(BiosParameterBlock)]);
if (bpb.bytes_per_sector == 0 or bpb.sectors_per_cluster == 0 or bpb.fat_count == 0) return null;
const fat_size_16: u32 = bpb.fat_size_16;
var fat_size: u32 = fat_size_16;
var root_cluster: u32 = 0;
var fsinfo_sector: u32 = 0;
if (fat_size_16 == 0) {
const ebr = std.mem.bytesToValue(ExtendedBootRecord32, sector[36 .. 36 + @sizeOf(ExtendedBootRecord32)]);
fat_size = ebr.fat_size_32;
root_cluster = ebr.root_cluster;
fsinfo_sector = ebr.filesystem_information_sector;
}
const total_sectors: u32 = if (bpb.total_sectors_16 != 0) bpb.total_sectors_16 else bpb.total_sectors_32;
const root_dir_sectors = (@as(u32, bpb.root_entry_count) * 32 + bpb.bytes_per_sector - 1) / bpb.bytes_per_sector;
const first_data_sector = bpb.reserved_sector_count + bpb.fat_count * fat_size + root_dir_sectors;
if (total_sectors < first_data_sector) return null;
const data_sectors = total_sectors - first_data_sector;
const cluster_count = data_sectors / bpb.sectors_per_cluster;
const fat_type: FatType = if (cluster_count < 4085) .fat12 else if (cluster_count < 65525) .fat16 else .fat32;
return .{
.fat_type = fat_type,
.bytes_per_sector = bpb.bytes_per_sector,
.sectors_per_cluster = bpb.sectors_per_cluster,
.reserved_sector_count = bpb.reserved_sector_count,
.fat_count = bpb.fat_count,
.fat_size_sectors = fat_size,
.root_entry_count = bpb.root_entry_count,
.root_cluster = root_cluster,
.first_data_sector = first_data_sector,
.total_sectors = total_sectors,
.cluster_count = cluster_count,
.fsinfo_sector = fsinfo_sector,
};
}
test "on-disk struct sizes match the specification" {
try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
try std.testing.expectEqual(@as(usize, 54), @sizeOf(ExtendedBootRecord32));
try std.testing.expectEqual(@as(usize, 32), @sizeOf(DirectoryEntry));
try std.testing.expectEqual(@as(usize, 32), @sizeOf(LongNameEntry));
try std.testing.expectEqual(@as(usize, 512), @sizeOf(FileSystemInformation));
}
test "directory entry cluster split/join" {
var entry = std.mem.zeroes(DirectoryEntry);
entry.setFirstCluster(0x01234567);
try std.testing.expectEqual(@as(u16, 0x4567), entry.first_cluster_low);
try std.testing.expectEqual(@as(u16, 0x0123), entry.first_cluster_high);
try std.testing.expectEqual(@as(u32, 0x01234567), entry.firstCluster());
}