commit 99ec7c869868c1b65f8795dc5fbc40282e7dbb6f
parent fb507cdb54f8724f34f2b061c15a2f04439e2823
Author: Sylvia Ivory <git@sivory.net>
Date: Sun, 8 Mar 2026 21:05:40 -0700
Add FAT32 support
Diffstat:
| A | pi/fs/fat.zig | | | 524 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pi/fs/fatvfs.zig | | | 347 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pi/fs/vfs.zig | | | 328 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | pi/root.zig | | | 6 | ++++++ |
4 files changed, 1205 insertions(+), 0 deletions(-)
diff --git a/pi/fs/fat.zig b/pi/fs/fat.zig
@@ -0,0 +1,524 @@
+const std = @import("std");
+
+pub const vfs = @import("./vfs.zig");
+
+pub const Error =
+ std.mem.Allocator.Error ||
+ std.unicode.Utf16LeToUtf8Error ||
+ vfs.Error;
+
+pub const Sector = vfs.Sector;
+pub const Disk = vfs.Disk;
+
+pub const MasterBootRecord = packed struct(Sector) {
+ // 446 bytes
+ // Zig doesn't allow arrays in packed
+ // u8[446]
+ boot_code: u3568,
+ pte1: PartitionTableEntry,
+ pte2: PartitionTableEntry,
+ pte3: PartitionTableEntry,
+ pte4: PartitionTableEntry,
+ // 0xAA55
+ signature: u16,
+
+ const Self = @This();
+
+ pub fn get_partition(self: *const Self, n: u2) *const PartitionTableEntry {
+ return switch (n) {
+ 0 => &self.pte1,
+ 1 => &self.pte2,
+ 2 => &self.pte3,
+ 3 => &self.pte4,
+ };
+ }
+
+ pub fn is_valid(self: *const Self) bool {
+ return self.signature == 0xAA55;
+ }
+
+ pub fn debug_print(self: *const Self, w: *std.io.Writer) !void {
+ var boot_code: [446]u8 = undefined;
+ std.mem.writeInt(u3568, &boot_code, self.boot_code, .little);
+ const crc32 = std.hash.Crc32.hash(&boot_code);
+
+ try w.print(
+ \\bootcode crc32 = 0x{X}
+ \\signature = 0x{X}
+ \\
+ , .{
+ crc32,
+ self.signature,
+ });
+
+ // Copy to proper alignment
+ var pte = self.pte1;
+ try w.print("Partition 1:\n", .{});
+ try pte.debug_print(w);
+
+ pte = self.pte2;
+ try w.print("Partition 2:\n", .{});
+ try pte.debug_print(w);
+
+ pte = self.pte3;
+ try w.print("Partition 3:\n", .{});
+ try pte.debug_print(w);
+
+ pte = self.pte4;
+ try w.print("Partition 4:\n", .{});
+ try pte.debug_print(w);
+ }
+};
+
+pub const PartitionTableEntry = packed struct(u128) {
+ pub const Status = packed struct(u8) {
+ _reserved: u7,
+ bootable: bool,
+ };
+
+ status: Status,
+ chs_start: u24,
+ partition_type: u8,
+ chs_end: u24,
+ first_lba: u32,
+ number_sectors: u32,
+
+ const Self = @This();
+
+ pub fn is_fat32(self: *const Self) bool {
+ return self.partition_type == 0x0B or self.partition_type == 0x0C;
+ }
+
+ pub fn is_empty(self: *const Self) bool {
+ return std.mem.allEqual(u8, @ptrCast(self), 0x00);
+ }
+
+ pub fn partition_type_str(self: *const Self) []const u8 {
+ return switch (self.partition_type) {
+ 0x00 => "Empty",
+ 0x01 => "FAT12 - CHS",
+ 0x04 => "FAT16 - 16-32 MB - CHS",
+ 0x05 => "Microsoft Extended - CHS",
+ 0x06 => "FAT16 - 32 MB-2 GB - CHS",
+ 0x07 => "NTFS",
+ 0x0B => "FAT32 - CHS",
+ 0x0C => "FAT32 - LBA",
+ 0x0E => "FAT16 - 32 MB-2 GB - LBA",
+ 0x0F => "Microsoft Extended - LBA",
+ 0x11 => "Hidden FAT12 - CHS",
+ 0x14 => "Hidden FAT16 - 16-32 MB - CHS",
+ 0x16 => "Hidden FAT16 - 32 MB-2 GB - CHS",
+ 0x1b => "Hidden FAT32 - CHS",
+ 0x1c => "Hidden FAT32 - LBA",
+ 0x1e => "Hidden FAT16 - 32 MB-2 GB - LBA",
+ 0x42 => "Microsoft MBR - Dynamic Disk",
+ 0x82 => "Solaris x86 OR Linux Swap",
+ 0x83 => "Linux",
+ 0x84 => "Hibernation",
+ 0x85 => "Linux Extended",
+ 0x86 => "NTFS Volume Set",
+ 0x87 => "NTFS Volume Set",
+ 0xa0 => "Hibernation",
+ 0xa1 => "Hibernation",
+ 0xa5 => "FreeBSD",
+ 0xa6 => "OpenBSD",
+ 0xa8 => "Mac OSX",
+ 0xa9 => "NetBSD",
+ 0xab => "Mac OSX Boot",
+ 0xb7 => "BSDI",
+ 0xb8 => "BSDI swap",
+ 0xee => "EFI GPT Disk",
+ 0xef => "EFI System Partition",
+ 0xfb => "Vmware File System",
+ 0xfc => "Vmware swap",
+ else => "Unknown",
+ };
+ }
+
+ pub fn debug_print(self: *const Self, w: *std.io.Writer) !void {
+ try w.print(
+ \\ bootable = {any}
+ \\ chs_start = 0x{X}
+ \\ partition_type = 0x{X} ({s})
+ \\ chs_end = 0X{X}
+ \\ lba_start = 0x{X}
+ \\ number sectors = {d} ({B})
+ \\
+ , .{
+ self.status.bootable,
+ self.chs_start,
+ self.partition_type,
+ self.partition_type_str(),
+ self.chs_end,
+ self.first_lba,
+ self.number_sectors,
+ @as(u64, self.number_sectors) * 512,
+ });
+ }
+};
+
+pub const Fat32BootSector = packed struct(Sector) {
+ // u8[3]
+ asm_code: u24,
+ // u8[8]
+ oem: u64,
+ bytes_per_sector: u16,
+ sectors_per_cluster: u8,
+ reserved_sectors: u16,
+ table_count: u8,
+ root_entry_count: u16,
+ total_sectors_16: u16,
+ media_type: u8,
+ table_size_16: u16,
+ sectors_per_track: u16,
+ head_side_count: u16,
+ hidden_sectors: u32,
+ total_sectors_32: u32,
+
+ // Fat32 Specific
+ table_size_32: u32,
+ extended_flags: u16,
+ fat_version: u16,
+ root_cluster: u32,
+ fat_info: u16,
+ backup_boot_location: u16,
+ // u8[12]
+ _reserved0: u96,
+ logical_drive_number: u8,
+ _reserved1: u8,
+ extended_signature: u8,
+ volume_id: u32,
+ // u8[11]
+ volume_label: u88,
+ // u8[8]
+ fs_type: u64,
+ // u8[420]
+ boot_code: u3360,
+ // 0xAA55
+ signature: u16,
+
+ const Self = @This();
+
+ pub fn oem_str(self: *const Self) [8]u8 {
+ var res: [8]u8 = undefined;
+ std.mem.writeInt(u64, &res, self.oem, .little);
+ return res;
+ }
+
+ pub fn volume_label_str(self: *const Self) [11]u8 {
+ var res: [11]u8 = undefined;
+ std.mem.writeInt(u88, &res, self.volume_label, .little);
+ return res;
+ }
+
+ pub fn fs_type_str(self: *const Self) [8]u8 {
+ var res: [8]u8 = undefined;
+ std.mem.writeInt(u64, &res, self.fs_type, .little);
+ return res;
+ }
+
+ pub inline fn first_fat_sector(self: *const Self) u32 {
+ return self.reserved_sectors;
+ }
+
+ pub inline fn first_data_sector(self: *const Self) u32 {
+ return self.first_fat_sector() + (self.table_count * self.table_size_32);
+ }
+
+ pub inline fn sector_from_cluster(self: *const Self, cluster: u32) u32 {
+ return ((cluster - 2) * self.sectors_per_cluster) + self.first_data_sector();
+ }
+
+ pub inline fn fat_entry_sector(self: *const Self, cluster: u32) u32 {
+ const fat_offset = cluster * 4;
+ return self.first_fat_sector() + (fat_offset / self.bytes_per_sector);
+ }
+
+ pub inline fn fat_entry(self: *const Self, cluster: u32, sector: Sector) u32 {
+ const fat_offset = cluster * 4;
+ const entry_offset = fat_offset % self.bytes_per_sector;
+
+ const sector_bytes: [*]const u32 = @ptrCast(§or);
+
+ return sector_bytes[entry_offset / 4] & @as(u32, 0x0FFFFFFF);
+ }
+
+ pub inline fn is_valid(self: *const Self) bool {
+ // TODO; check if FAT32 specifically
+ return self.signature == 0xAA55;
+ }
+
+ pub fn debug_print(self: *const Self, w: *std.io.Writer) !void {
+ try w.print(
+ \\ oem = {s}
+ \\ bytes_per_sector = {d}
+ \\ sectors_per_cluster = {d}
+ \\ reserved_sectors = {d}
+ \\ table_count = {d}
+ \\ root_entry_count = {d}
+ \\ total_sectors_16 = {d}
+ \\ media_type = 0x{X}
+ \\ table_size_16 = {d}
+ \\ sectors_per_track = {d}
+ \\ head_side_count = {d}
+ \\ hidden_sectors = {d}
+ \\ total_sectors_32 = {d}
+ \\ table_size_32 = {d}
+ \\ extended_flags = 0x{X}
+ \\ version = {d}
+ \\ root_cluster = 0x{X}
+ \\ fat_info = 0x{X}
+ \\ backup_boot_location = 0x{X}
+ \\ logical_drive_number = {d}
+ \\ extended_signature = 0x{X}
+ \\ volume_id = 0x{X}
+ \\ volume_label = {s}
+ \\ fs_type = {s}
+ \\ signature = 0x{X}
+ \\
+ , .{
+ self.oem_str(),
+ self.bytes_per_sector,
+ self.sectors_per_cluster,
+ self.reserved_sectors,
+ self.table_count,
+ self.root_entry_count,
+ self.total_sectors_16,
+ self.media_type,
+ self.table_size_16,
+ self.sectors_per_track,
+ self.head_side_count,
+ self.hidden_sectors,
+ self.total_sectors_32,
+ self.table_size_32,
+ self.extended_flags,
+ self.fat_version,
+ self.root_cluster,
+ self.fat_info,
+ self.backup_boot_location,
+ self.logical_drive_number,
+ self.extended_signature,
+ self.volume_id,
+ self.volume_label_str(),
+ self.fs_type_str(),
+ self.signature,
+ });
+ }
+};
+
+pub const FatInfo = packed struct(Sector) {
+ // 0x41615252
+ signature_1: u32,
+ // u8[480]
+ _reserved_0: u3840,
+ // 0x61417272
+ signature_2: u32,
+ free_cluster_count: u32,
+ next_free_cluster: u32,
+ // u8[12]
+ _reserved_1: u96,
+ // 0xAA550000
+ signature_3: u32,
+
+ const Self = @This();
+
+ pub inline fn is_valid(self: *const Self) bool {
+ return self.signature_1 == 0x41615252 and self.signature_2 == 0x61417272 and self.signature_3 == 0xAA550000;
+ }
+
+ pub fn debug_print(self: *const Self, w: *std.io.Writer) !void {
+ try w.print(
+ \\ signature_1 = 0x{X}
+ \\ signature_2 = 0x{X}
+ \\ free_cluster_count = {d}
+ \\ next_free_cluster = 0x{X}
+ \\ signature_3 = 0x{X}
+ \\
+ , .{
+ self.signature_1,
+ self.signature_2,
+ self.free_cluster_count,
+ self.next_free_cluster,
+ self.signature_3,
+ });
+ }
+};
+
+pub const DirectoryEntry = packed struct(u256) {
+ pub const Attributes = packed struct(u8) {
+ read_only: bool,
+ hidden: bool,
+ system_file: bool,
+ volume_label: bool,
+ subdirectory: bool,
+ archive: bool,
+ _unused: u2,
+ };
+
+ pub const Time = packed struct(u16) {
+ seconds: u5,
+ minutes: u6,
+ hour: u5,
+
+ pub fn into_seconds(self: Time) i32 {
+ return @as(i32, self.hour) * std.time.s_per_hour +
+ @as(i32, self.minutes) * std.time.s_per_min +
+ @as(i32, self.seconds);
+ }
+ };
+
+ pub const Date = packed struct(u16) {
+ day: u5,
+ month: u4,
+ year: u7,
+
+ pub fn into_seconds(self: Date) i32 {
+ var days: i32 = 0;
+
+ // Add 1970 - 1980
+ for (0..10) |y| {
+ days += std.time.epoch.getDaysInYear(1970 + @as(u16, @truncate(y)));
+ }
+
+ for (0..self.year) |y| {
+ days += std.time.epoch.getDaysInYear(1980 + @as(u16, @truncate(y)));
+ }
+
+ days += std.time.epoch.getDaysInMonth(self.year, @enumFromInt(self.month));
+ days += self.day;
+
+ return days * std.time.s_per_day;
+ }
+ };
+
+ // u8[11]
+ // 8.3 file name
+ // first 8 are name, last 3 are ext
+ filename: u88,
+ attributes: Attributes,
+ _reserved: u8,
+ creation_time_tenths: u8,
+ creation_time: Time,
+ create_date: Date,
+ access_date: Date,
+ high_start: u16,
+ modify_time: Time,
+ modify_date: Date,
+ low_start: u16,
+ number_bytes: u32,
+
+ const Self = @This();
+
+ pub fn filename_str(self: *const Self) [11]u8 {
+ var res: [11]u8 = undefined;
+ std.mem.writeInt(u88, &res, self.filename, .little);
+ return res;
+ }
+
+ pub fn is_long_filename(self: *const Self) bool {
+ return @as(u8, @bitCast(self.attributes)) == 0x0F;
+ }
+
+ pub fn as_lfn(self: *const Self) *const LFNDirectoryEntry {
+ return @ptrCast(self);
+ }
+
+ pub fn calculate_lfn_checksum(self: *const Self) u8 {
+ var buffer: [11]u8 = undefined;
+ std.mem.writeInt(u88, &buffer, self.filename, .little);
+
+ var checksum = 0;
+ for (0..11) |i| {
+ checksum = ((checksum & 1) << 7) + (checksum >> 1) + buffer[i];
+ }
+ return checksum;
+ }
+
+ pub inline fn is_free(self: *const Self) bool {
+ if (self.is_long_filename()) {
+ const lfn = self.as_lfn() catch unreachable;
+ return lfn.is_deleted();
+ } else {
+ return self.filename_str()[0] == 0 or self.filename_str()[0] == 0xE5;
+ }
+ }
+
+ pub fn first_cluster(self: *const Self) u32 {
+ return (@as(u32, self.high_start) << 16) | @as(u32, self.low_start);
+ }
+
+ pub fn debug_print(self: *const Self, w: *std.io.Writer) !void {
+ try w.print(
+ \\ filename = {s}
+ \\ attributes = 0b{b}
+ \\ creation_time_tenths = {}
+ \\ creation_time = {:0>2}:{:0>2}:{:0>2}
+ \\ create_date = {:0>4}/{:0>2}/{:0>2}
+ \\ access_date = {:0>4}/{:0>2}/{:0>2}
+ \\ modify_time = {:0>2}:{:0>2}:{:0>2}
+ \\ modify_date = {:0>4}/{:0>2}/{:0>2}
+ \\ first_cluster = {}
+ \\ number_bytes = {B}
+ \\
+ , .{
+ self.filename_str(),
+ @as(u8, @bitCast(self.attributes)),
+ self.creation_time_tenths,
+ self.creation_time.hour,
+ self.creation_time.minutes,
+ self.creation_time.seconds,
+ self.create_date.year,
+ self.create_date.month,
+ self.create_date.day,
+ self.access_date.year,
+ self.access_date.month,
+ self.access_date.day,
+ self.modify_time.hour,
+ self.modify_time.minutes,
+ self.modify_time.seconds,
+ self.modify_date.year,
+ self.modify_date.month,
+ self.modify_date.day,
+ self.first_cluster(),
+ self.number_bytes,
+ });
+ }
+};
+
+pub const LFNDirectoryEntry = packed struct(u256) {
+ sequence_number: u8,
+ // u8[10]
+ name1_5: u80,
+ attributes: DirectoryEntry.Attributes,
+ _reserved0: u8,
+ checksum: u8,
+ // u8[12]
+ name6_11: u96,
+ _reserved1: u16,
+ // u8[4]
+ name12_13: u32,
+
+ const Self = @This();
+
+ pub fn is_deleted(self: *const Self) bool {
+ return (self.sequence_number & 0xE5) == 0xE5;
+ }
+
+ pub fn is_last(self: *const Self) bool {
+ return (self.sequence_number & (1 << 6)) != 0;
+ }
+
+ pub fn is_first(self: *const Self) bool {
+ return (self.sequence_number & ~(1 << 6)) == 1;
+ }
+
+ pub fn name_ucs_2(self: *const Self) [13]u16 {
+ var res: [26]u8 = .{0} ** 26;
+
+ std.mem.writeInt(u80, res[0..10], self.name1_5, .little);
+ std.mem.writeInt(u96, res[10..22], self.name6_11, .little);
+ std.mem.writeInt(u32, res[22..26], self.name12_13, .little);
+
+ return @bitCast(res);
+ }
+};
diff --git a/pi/fs/fatvfs.zig b/pi/fs/fatvfs.zig
@@ -0,0 +1,347 @@
+const std = @import("std");
+
+const vfs = @import("./vfs.zig");
+const fat = @import("./fat.zig");
+
+// TODO;
+// - writing
+// - use arenas in place of FileSystem's allocator?
+
+const Error = vfs.Error;
+
+pub const DirectoryEntry = struct {
+ name: []const u8,
+ entry: fat.DirectoryEntry,
+
+ fs: *const Self,
+
+ pub fn close(self: *DirectoryEntry) void {
+ self.fs.alloc.free(self.name);
+ self.fs.alloc.destroy(self);
+ }
+
+ pub fn stat(self: *const DirectoryEntry) vfs.Stat {
+ const attr = self.entry.attributes;
+
+ return .{
+ .name = self.name,
+ .read_only = attr.read_only,
+ .hidden = attr.hidden,
+ .directory = attr.subdirectory,
+
+ .creation_time = self.entry.create_date.into_seconds() + self.entry.creation_time.into_seconds(),
+ .modify_time = self.entry.modify_date.into_seconds() + self.entry.modify_time.into_seconds(),
+ .access_time = self.entry.access_date.into_seconds(),
+
+ .size = self.entry.number_bytes,
+ };
+ }
+};
+
+pub const File = struct {
+ first_cluster: u32,
+ size: u32,
+
+ current_cluster: u32,
+ remaining: u32,
+
+ fs: *const Self,
+
+ pub fn close(self: *File) void {
+ self.fs.alloc.destroy(self);
+ }
+
+ // TODO; should we read by even smaller increments (by sector?)
+ pub fn read(self: *File) Error!?[]u8 {
+ if (self.remaining == 0) return null;
+
+ const to_alloc = @min(self.remaining, @sizeOf(vfs.Sector) * @as(usize, self.fs.bpb.sectors_per_cluster));
+ const file: []u8 = try self.fs.alloc.alloc(u8, to_alloc);
+
+ const contents = try self.fs.disk.read_sectors(
+ self.fs.lba + self.fs.bpb.sector_from_cluster(self.current_cluster),
+ self.fs.bpb.sectors_per_cluster,
+ );
+ defer self.fs.disk.free_sectors(contents);
+
+ const bytes: [*]const u8 = @ptrCast(contents);
+ @memcpy(file, bytes);
+
+ self.remaining -= to_alloc;
+
+ return file;
+ }
+
+ pub fn reset(self: *File) Error!void {
+ self.current_cluster = self.first_cluster;
+ self.remaining = self.size;
+ }
+
+ // TODO; seek
+};
+
+pub const DirectoryIterator = struct {
+ first_cluster: u32,
+ current_cluster: u32,
+ current_entry: u32,
+ fs: *const Self,
+
+ cluster: []vfs.Sector,
+
+ pub fn load(self: *DirectoryIterator) Error!void {
+ // Load cluster
+ self.current_entry = 0;
+ self.cluster = try self.fs.disk.read_sectors(
+ self.fs.lba + self.fs.bpb.sector_from_cluster(self.current_cluster),
+ self.fs.bpb.sectors_per_cluster,
+ );
+ }
+
+ pub fn close(self: *DirectoryIterator) void {
+ self.fs.disk.free_sectors(self.cluster);
+ self.fs.alloc.destroy(self);
+ }
+
+ pub fn next(self: *DirectoryIterator) Error!?vfs.DirectoryEntry {
+ // ensure we always increment
+ defer self.current_entry += 1;
+
+ const len = (@sizeOf(vfs.Sector) * @as(usize, self.fs.bpb.sectors_per_cluster)) / @sizeOf(fat.DirectoryEntry);
+
+ // Max file name length is 512
+ var long_name_buffer: [256]u16 = undefined;
+ var long_name_list: std.ArrayList(u16) = .initBuffer(&long_name_buffer);
+
+ // Find next actual entry (and collect together LFN)
+ const entries: [*]fat.DirectoryEntry = @ptrCast(self.cluster);
+ while (true) : (self.current_entry += 1) {
+ if (self.current_entry == len) {
+ // Cycle into new cluster
+ self.current_cluster = try self.fs.next_cluster(self.current_cluster) orelse return null;
+ self.fs.disk.free_sectors(self.cluster);
+
+ try self.load();
+ }
+
+ const entry = entries[self.current_entry];
+ const bytes: [*]const u8 = @ptrCast(&entry);
+
+ if (bytes[0] == 0x00) return null;
+ if (bytes[0] == 0xE5) continue;
+ if (entry.attributes.volume_label and !entry.is_long_filename()) continue;
+
+ if (entry.is_long_filename()) {
+ // We've manually checked it's LFN
+ const lfn = entry.as_lfn() catch unreachable;
+ const name = lfn.name_ucs_2();
+
+ for (0..name.len) |i| {
+ const rev = (name.len - 1 - i);
+
+ if (rev == 0xFFFF) continue;
+
+ if (long_name_list.items.len == long_name_buffer.len) return Error.InvalidFileName;
+ long_name_list.appendAssumeCapacity(name[rev]);
+ }
+
+ continue;
+ }
+
+ var vfs_entry = try self.fs.alloc.create(DirectoryEntry);
+ vfs_entry.entry = entry;
+ vfs_entry.fs = self.fs;
+
+ if (long_name_list.items.len > 0) {
+ // End of LFN chain
+ std.mem.reverse(u16, long_name_list.items);
+
+ const name = std.unicode.utf16LeToUtf8Alloc(
+ self.fs.alloc,
+ long_name_list.items,
+ ) catch return Error.InvalidFileName;
+
+ vfs_entry.name = for (name, 0..) |b, idx| {
+ if (b == 0) {
+ const new_name = try self.fs.alloc.dupe(u8, name[0..idx]);
+ self.fs.alloc.free(name);
+
+ break new_name;
+ }
+ } else name;
+
+ long_name_list.items.len = 0;
+
+ return vfs.DirectoryEntry.impl_by(vfs_entry);
+ }
+
+ const short_name = entry.filename_str();
+
+ if (short_name[0] == '.') {
+ // Exception to 8.3
+
+ vfs_entry.name = try self.fs.alloc.dupe(
+ u8,
+ if (short_name[1] == '.') ".." else ".",
+ );
+
+ return vfs.DirectoryEntry.impl_by(vfs_entry);
+ }
+
+ // 8.3 - max length is 12
+ var short_name_buffer: [12]u8 = undefined;
+ var short_name_list: std.ArrayList(u8) = .initBuffer(&short_name_buffer);
+
+ for (short_name) |b| {
+ if (b == ' ') break;
+ short_name_list.appendAssumeCapacity(b);
+ }
+
+ short_name_list.appendAssumeCapacity('.');
+ short_name_list.appendSliceAssumeCapacity(short_name[8..11]);
+
+ vfs_entry.name = try self.fs.alloc.dupe(u8, short_name_list.items);
+
+ return vfs.DirectoryEntry.impl_by(vfs_entry);
+ }
+ }
+
+ pub fn reset(self: *DirectoryIterator) Error!void {
+ self.current_cluster = self.first_cluster;
+ try self.load();
+ }
+};
+
+pub const Directory = struct {
+ first_cluster: u32,
+ fs: *const Self,
+
+ // FAT means we just need to iterate
+ pub fn open_entry(self: *Directory, name: []const u8) Error!?vfs.DirectoryEntry {
+ var it = try self.iterate();
+ defer it.close();
+
+ while (try it.next()) |entry| {
+ if (std.mem.eql(u8, entry.stat().name, name)) {
+ return entry;
+ } else {
+ entry.close();
+ }
+ }
+
+ return null;
+ }
+
+ pub fn close(self: *Directory) void {
+ self.fs.alloc.destroy(self);
+ }
+
+ pub fn iterate(self: *Directory) Error!vfs.DirectoryIterator {
+ var iter = try self.fs.alloc.create(DirectoryIterator);
+
+ iter.* = .{
+ .current_cluster = self.first_cluster,
+ .first_cluster = self.first_cluster,
+ .current_entry = 0,
+ .cluster = undefined,
+ .fs = self.fs,
+ };
+
+ try iter.load();
+
+ return vfs.DirectoryIterator.impl_by(iter);
+ }
+};
+
+alloc: std.mem.Allocator,
+bpb: fat.Fat32BootSector,
+info: fat.FatInfo,
+disk: *vfs.Disk,
+lba: u32,
+
+is_initialized: bool,
+
+const Self = @This();
+
+pub fn init() Self {
+ return .{
+ .is_initialized = false,
+ .alloc = undefined,
+ .bpb = undefined,
+ .info = undefined,
+ .disk = undefined,
+ .lba = undefined,
+ };
+}
+
+pub fn open(self: *Self, alloc: std.mem.Allocator, disk: *vfs.Disk, lba: u32) Error!void {
+ const bpb: fat.Fat32BootSector = @bitCast(try disk.read_sector(lba));
+ if (!bpb.is_valid()) return Error.InvalidFileSystem;
+
+ const info: fat.FatInfo = @bitCast(try disk.read_sector(lba + bpb.fat_info));
+ if (!info.is_valid()) return Error.InvalidFileSystem;
+
+ self.info = info;
+ self.disk = disk;
+ self.alloc = alloc;
+ self.bpb = bpb;
+ self.lba = lba;
+ self.is_initialized = true;
+}
+
+pub fn close(self: *Self) void {
+ self.is_initialized = false;
+}
+
+fn read_cluster_directory(self: *const Self, first_cluster: u32) Error!vfs.Directory {
+ const directory = try self.alloc.create(Directory);
+
+ directory.* = .{
+ .first_cluster = first_cluster,
+ .fs = self,
+ };
+
+ return vfs.Directory.impl_by(directory);
+}
+
+fn read_cluster_file(self: *const Self, first_cluster: u32, size: u32) Error!vfs.File {
+ const file = try self.alloc.create(File);
+
+ file.* = .{
+ .current_cluster = first_cluster,
+ .first_cluster = first_cluster,
+ .remaining = size,
+ .size = size,
+ .fs = self,
+ };
+
+ return vfs.File.impl_by(file);
+}
+
+fn next_cluster(self: *const Self, current_cluster: u32) Error!?u32 {
+ const sector_n = self.bpb.fat_entry_sector(current_cluster) + self.lba;
+ const sector = try self.disk.read_sector(sector_n);
+
+ const next = self.bpb.fat_entry(current_cluster, sector);
+
+ if (next >= 0x0FFFFFF8) return null;
+
+ return next;
+}
+
+pub fn open_root(self: *const Self) Error!vfs.Directory {
+ if (!self.is_initialized) return Error.NotInitialized;
+
+ return try self.read_cluster_directory(self.bpb.root_cluster);
+}
+
+pub fn open_directory(self: *const Self, entry: *fat.DirectoryEntry) Error!vfs.Directory {
+ if (!self.is_initialized) return Error.NotInitialized;
+
+ return try self.read_cluster_directory(entry.first_cluster());
+}
+
+pub fn open_file(self: *const Self, entry: *fat.DirectoryEntry) Error!vfs.File {
+ if (!self.is_initialized) return Error.NotInitialized;
+
+ return try self.read_cluster_file(entry.first_cluster(), entry.number_bytes);
+}
diff --git a/pi/fs/vfs.zig b/pi/fs/vfs.zig
@@ -0,0 +1,328 @@
+const std = @import("std");
+
+// TODO; future expansion
+// - File locking
+// - Writing
+
+pub const Sector = u4096;
+
+pub const Error = error{
+ InvalidFileSystem,
+ InvalidSector,
+
+ ExpectedFile,
+ ExpectedDirectory,
+
+ NotInitialized,
+
+ InvalidFileName,
+} || std.mem.Allocator.Error;
+
+fn TPtr(T: type, opaque_ptr: *anyopaque) T {
+ return @as(T, @ptrCast(@alignCast(opaque_ptr)));
+}
+
+pub const Disk = struct {
+ impl: *anyopaque,
+ v_read_sector: *const fn (*anyopaque, sector: u32) Error!Sector,
+ v_read_sectors: *const fn (*anyopaque, sector: u32, count: u32) Error![]Sector,
+ v_free_sectors: *const fn (*anyopaque, sectors: []Sector) void,
+
+ pub fn impl_by(impl_obj: anytype) Disk {
+ const delegate = DiskDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_read_sector = delegate.read_sector,
+ .v_read_sectors = delegate.read_sectors,
+ .v_free_sectors = delegate.free_sectors,
+ };
+ }
+
+ pub fn read_sector(self: Disk, sector: u32) Error!Sector {
+ return self.v_read_sector(self.impl, sector);
+ }
+
+ pub fn read_sectors(self: Disk, sector: u32, count: u32) Error![]Sector {
+ return self.v_read_sectors(self.impl, sector, count);
+ }
+
+ pub fn free_sectors(self: Disk, sectors: []Sector) void {
+ return self.v_free_sectors(self.impl, sectors);
+ }
+};
+
+inline fn DiskDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn read_sector(impl: *anyopaque, sector: u32) Error!Sector {
+ return try TPtr(ImplType, impl).read_sector(sector);
+ }
+
+ fn read_sectors(impl: *anyopaque, sector: u32, count: u32) Error![]Sector {
+ return try TPtr(ImplType, impl).read_sectors(sector, count);
+ }
+
+ fn free_sectors(impl: *anyopaque, sectors: []Sector) void {
+ return TPtr(ImplType, impl).free_sectors(sectors);
+ }
+ };
+}
+
+pub const Stat = struct {
+ name: []const u8,
+ read_only: bool,
+ hidden: bool,
+ directory: bool,
+
+ // 2k36 wcgw
+ creation_time: i32,
+ access_time: i32,
+ modify_time: i32,
+
+ size: u32,
+};
+
+pub const DirectoryEntry = struct {
+ impl: *anyopaque,
+
+ v_close: *const fn (*anyopaque) void,
+ v_stat: *const fn (*anyopaque) Stat,
+
+ pub fn impl_by(impl_obj: anytype) DirectoryEntry {
+ const delegate = DirectoryEntryDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_close = delegate.close,
+ .v_stat = delegate.stat,
+ };
+ }
+
+ pub fn close(self: DirectoryEntry) void {
+ return self.v_close(self.impl);
+ }
+
+ pub fn stat(self: DirectoryEntry) Stat {
+ return self.v_stat(self.impl);
+ }
+};
+
+inline fn DirectoryEntryDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn close(impl: *anyopaque) void {
+ return TPtr(ImplType, impl).close();
+ }
+
+ fn stat(impl: *anyopaque) Stat {
+ return TPtr(ImplType, impl).stat();
+ }
+ };
+}
+
+pub const DirectoryIterator = struct {
+ impl: *anyopaque,
+ v_next: *const fn (*anyopaque) Error!?DirectoryEntry,
+ v_reset: *const fn (*anyopaque) Error!void,
+ v_close: *const fn (*anyopaque) void,
+
+ pub fn impl_by(impl_obj: anytype) DirectoryIterator {
+ const delegate = DirectoryIteratorDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_next = delegate.next,
+ .v_reset = delegate.reset,
+ .v_close = delegate.close,
+ };
+ }
+
+ pub fn next(self: DirectoryIterator) Error!?DirectoryEntry {
+ return try self.v_next(self.impl);
+ }
+
+ pub fn reset(self: DirectoryIterator) Error!void {
+ return try self.v_reset(self.impl);
+ }
+
+ pub fn close(self: DirectoryIterator) void {
+ return self.v_close(self.impl);
+ }
+};
+
+inline fn DirectoryIteratorDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn next(impl: *anyopaque) Error!?DirectoryEntry {
+ return try TPtr(ImplType, impl).next();
+ }
+
+ fn reset(impl: *anyopaque) Error!void {
+ return try TPtr(ImplType, impl).reset();
+ }
+
+ fn close(impl: *anyopaque) void {
+ return TPtr(ImplType, impl).close();
+ }
+ };
+}
+
+pub const File = struct {
+ impl: *anyopaque,
+ v_close: *const fn (*anyopaque) void,
+ v_read: *const fn (*anyopaque) Error!?[]u8,
+ v_reset: *const fn (*anyopaque) Error!void,
+
+ pub fn impl_by(impl_obj: anytype) File {
+ const delegate = FileDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_close = delegate.close,
+ .v_read = delegate.read,
+ .v_reset = delegate.reset,
+ };
+ }
+
+ pub fn close(self: File) void {
+ return self.v_close(self.impl);
+ }
+
+ pub fn read(self: File) Error!?[]u8 {
+ return try self.v_read(self.impl);
+ }
+
+ pub fn reset(self: File) Error!void {
+ return try self.v_reset(self.impl);
+ }
+};
+
+inline fn FileDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn close(impl: *anyopaque) void {
+ return TPtr(ImplType, impl).close();
+ }
+
+ fn read(impl: *anyopaque) Error!?[]u8 {
+ return try TPtr(ImplType, impl).read();
+ }
+
+ fn reset(impl: *anyopaque) Error!void {
+ return try TPtr(ImplType, impl).reset();
+ }
+ };
+}
+
+pub const Directory = struct {
+ impl: *anyopaque,
+ v_open_entry: *const fn (*anyopaque, []const u8) Error!?DirectoryEntry,
+ v_close: *const fn (*anyopaque) void,
+ v_iterate: *const fn (*anyopaque) Error!DirectoryIterator,
+
+ pub fn impl_by(impl_obj: anytype) Directory {
+ const delegate = DirectoryDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_open_entry = delegate.open_entry,
+ .v_close = delegate.close,
+ .v_iterate = delegate.iterate,
+ };
+ }
+
+ pub fn open_entry(self: Directory, name: []const u8) Error!?DirectoryEntry {
+ return try self.v_open_entry(self.impl, name);
+ }
+
+ pub fn close(self: Directory) void {
+ return self.v_close(self.impl);
+ }
+
+ pub fn iterate(self: Directory) Error!DirectoryIterator {
+ return self.v_iterate(self.impl);
+ }
+};
+
+inline fn DirectoryDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn open_entry(impl: *anyopaque, name: []const u8) Error!?DirectoryEntry {
+ return TPtr(ImplType, impl).open_entry(name);
+ }
+
+ fn close(impl: *anyopaque) void {
+ return TPtr(ImplType, impl).close();
+ }
+
+ fn iterate(impl: *anyopaque) Error!DirectoryIterator {
+ return try TPtr(ImplType, impl).iterate();
+ }
+ };
+}
+
+pub const FileSystem = struct {
+ impl: *anyopaque,
+ v_open: *const fn (*anyopaque, alloc: std.mem.Allocator, disk: *Disk, lba: u32) Error!void,
+ v_close: *const fn (*anyopaque) void,
+ v_open_root: *const fn (*anyopaque) Error!Directory,
+ v_open_directory: *const fn (*anyopaque, *anyopaque) Error!Directory,
+ v_open_file: *const fn (*anyopaque, *anyopaque) Error!File,
+
+ pub fn impl_by(impl_obj: anytype) FileSystem {
+ const delegate = FileSystemDelegate(impl_obj);
+ return .{
+ .impl = impl_obj,
+ .v_open = delegate.open,
+ .v_close = delegate.close,
+ .v_open_root = delegate.open_root,
+ .v_open_directory = delegate.open_directory,
+ .v_open_file = delegate.open_file,
+ };
+ }
+
+ pub fn open(self: FileSystem, alloc: std.mem.Allocator, disk: *Disk, lba: u32) Error!void {
+ return self.v_open(self.impl, alloc, disk, lba);
+ }
+
+ pub fn close(self: FileSystem) void {
+ return self.v_close(self.impl);
+ }
+
+ pub fn open_root(self: FileSystem) Error!Directory {
+ return try self.v_open_root(self.impl);
+ }
+
+ pub fn open_directory(self: FileSystem, entry: *DirectoryEntry) Error!Directory {
+ if (!entry.stat().directory) return Error.ExpectedDirectory;
+
+ return try self.v_open_directory(self.impl, entry.impl);
+ }
+
+ pub fn open_file(self: FileSystem, entry: *DirectoryEntry) Error!File {
+ if (entry.stat().directory) return Error.ExpectedFile;
+
+ return try self.v_open_file(self.impl, entry.impl);
+ }
+};
+
+inline fn FileSystemDelegate(impl_obj: anytype) type {
+ const ImplType = @TypeOf(impl_obj);
+ return struct {
+ fn open(impl: *anyopaque, alloc: std.mem.Allocator, disk: *Disk, lba: u32) Error!void {
+ return try TPtr(ImplType, impl).open(alloc, disk, lba);
+ }
+
+ fn close(impl: *anyopaque) void {
+ return TPtr(ImplType, impl).close();
+ }
+
+ fn open_root(impl: *anyopaque) Error!Directory {
+ return try TPtr(ImplType, impl).open_root();
+ }
+
+ fn open_directory(impl: *anyopaque, entry_impl: *anyopaque) Error!Directory {
+ return try TPtr(ImplType, impl).open_directory(@ptrCast(@alignCast(entry_impl)));
+ }
+
+ fn open_file(impl: *anyopaque, entry_impl: *anyopaque) Error!File {
+ return try TPtr(ImplType, impl).open_file(@ptrCast(@alignCast(entry_impl)));
+ }
+ };
+}
diff --git a/pi/root.zig b/pi/root.zig
@@ -26,6 +26,12 @@ pub const devices = struct {
pub const sd = @import("./devices/sd.zig");
};
+pub const fs = struct {
+ pub const vfs = @import("./fs/vfs.zig");
+ pub const fat = @import("./fs/fat.zig");
+ pub const FatVFS = @import("./fs/fatvfs.zig");
+};
+
pub export const STACK_ADDRESS: usize = 0x8000000;
pub export const STACK_INTERRUPT_ADDRESS: usize = 0x9000000;