sylveos

Toy Operating System
Log | Files | Refs

commit 99ec7c869868c1b65f8795dc5fbc40282e7dbb6f
parent fb507cdb54f8724f34f2b061c15a2f04439e2823
Author: Sylvia Ivory <git@sivory.net>
Date:   Sun,  8 Mar 2026 21:05:40 -0700

Add FAT32 support

Diffstat:
Api/fs/fat.zig | 524+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Api/fs/fatvfs.zig | 347+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Api/fs/vfs.zig | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpi/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(&sector); + + 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;