sylveos

Toy Operating System
Log | Files | Refs

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

Add explorer program

Diffstat:
Mpi/devices/sd.zig | 16+++++++++-------
Mpi/fs/fatvfs.zig | 3+--
Mpi/fs/vfs.zig | 1+
Aprograms/fs.zig | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 291 insertions(+), 9 deletions(-)

diff --git a/pi/devices/sd.zig b/pi/devices/sd.zig @@ -1,5 +1,7 @@ // Temporarily use external C code until I feel like writing my own const std = @import("std"); +const vfs = @import("../fs/vfs.zig"); + const pi_sd = @cImport({ @cInclude("pi-sd.h"); }); @@ -12,26 +14,26 @@ pub fn init(alloc: std.mem.Allocator) Self { return .{ .alloc = alloc }; } -pub fn read_sector(self: *const Self, sector: usize) !u4096 { +pub fn read_sector(self: *const Self, sector: usize) vfs.Error!vfs.Sector { _ = self; var buffer: [512]u8 = .{0} ** 512; if (pi_sd.pi_sd_read(&buffer, sector, 1) == -1) { - return error.InvalidRead; + return vfs.Error.ReadFailed; } - return std.mem.readInt(u4096, buffer[0..512], .little); + return std.mem.readInt(vfs.Sector, buffer[0..512], .little); } -pub fn read_sectors(self: *const Self, sector: usize, count: usize) ![]u4096 { +pub fn read_sectors(self: *const Self, sector: usize, count: usize) vfs.Error![]vfs.Sector { var buffer = try self.alloc.alloc(u8, count * 512); - if (pi_sd.pi_sd_read(&buffer, sector, 1) == -1) { - return error.InvalidRead; + if (pi_sd.pi_sd_read(@ptrCast(&buffer), sector, 1) == -1) { + return vfs.Error.ReadFailed; } return @ptrCast(@alignCast(buffer)); } -pub fn free_sectors(self: *const Self, sectors: []u4096) void { +pub fn free_sectors(self: *const Self, sectors: []vfs.Sector) void { self.alloc.free(sectors); } diff --git a/pi/fs/fatvfs.zig b/pi/fs/fatvfs.zig @@ -131,8 +131,7 @@ pub const DirectoryIterator = struct { 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 lfn = entry.as_lfn(); const name = lfn.name_ucs_2(); for (0..name.len) |i| { diff --git a/pi/fs/vfs.zig b/pi/fs/vfs.zig @@ -16,6 +16,7 @@ pub const Error = error{ NotInitialized, InvalidFileName, + ReadFailed, } || std.mem.Allocator.Error; fn TPtr(T: type, opaque_ptr: *anyopaque) T { diff --git a/programs/fs.zig b/programs/fs.zig @@ -0,0 +1,280 @@ +const std = @import("std"); +const pi = @import("pi"); + +const vfs = pi.fs.vfs; +const sd = pi.devices.sd; +const uart = pi.devices.mini_uart; + +const Explorer = struct { + alloc: std.mem.Allocator, + io_r: *std.Io.Reader, + io_w: *std.Io.Writer, + + fs: vfs.FileSystem, + dir: vfs.Directory, + path: std.ArrayList([]const u8), + + const Self = @This(); + + pub fn init(alloc: std.mem.Allocator, fs: vfs.FileSystem, io_r: *std.Io.Reader, io_w: *std.Io.Writer) !Self { + var path: std.ArrayList([]const u8) = try .initCapacity(alloc, 1); + try path.append(alloc, "/"); + + return .{ + .alloc = alloc, + .fs = fs, + .dir = try fs.open_root(), + .path = path, + .io_r = io_r, + .io_w = io_w, + }; + } + + pub fn start(self: *Self) !void { + { + const path = try std.mem.concat(self.alloc, u8, self.path.items); + defer self.alloc.free(path); + try self.io_w.print("[{s}] $ ", .{path}); + } + + while (try self.io_r.takeDelimiter('\n')) |line| { + try self.parse_line(line); + + const path = try std.mem.concat(self.alloc, u8, self.path.items); + defer self.alloc.free(path); + try self.io_w.print("[{s}] $ ", .{path}); + } + } + + // I would like to apologize for this truly bad parser + fn parse_line(self: *Self, line: []const u8) !void { + var it = std.mem.splitAny(u8, line, " "); + + const cmd = it.next() orelse return; + const entry = it.next() orelse "."; + + if (std.mem.eql(u8, cmd, "ls")) { + try self.ls(entry); + } else if (std.mem.eql(u8, cmd, "tree")) { + try self.tree(entry); + } else if (std.mem.eql(u8, cmd, "cd")) { + try self.cd(entry); + } else if (std.mem.eql(u8, cmd, "stat")) { + try self.stat(entry); + } else if (std.mem.eql(u8, cmd, "cat")) { + try self.cat(entry); + } else if (std.mem.eql(u8, cmd, "help")) { + try self.io_w.print("Commands: ls, tree, cd, stat, cat, help\n", .{}); + } else { + try self.io_w.print("\"{s}\": Command not found\n", .{cmd}); + } + } + + fn open_directory(self: *Self, directory: []const u8) !?vfs.Directory { + var entry = try self.dir.open_entry(directory) orelse { + try self.io_w.print("\"{s}\": No such file or directory\n", .{directory}); + return null; + }; + defer entry.close(); + + if (!entry.stat().directory) { + try self.io_w.print("\"{s}\": Not a directory\n", .{directory}); + return null; + } + + return try self.fs.open_directory(&entry); + } + + fn ls(self: *Self, directory: []const u8) !void { + const dir = if (std.mem.eql(u8, directory, ".")) self.dir else try self.open_directory(directory) orelse return; + + try self.io_w.print("Name:\n", .{}); + + var it = try dir.iterate(); + while (try it.next()) |entry| { + defer entry.close(); + + try self.io_w.print("- {s}\n", .{entry.stat().name}); + } + + if (!std.mem.eql(u8, directory, ".")) { + dir.close(); + } + } + fn tree(self: *Self, directory: []const u8) !void { + const dir = if (std.mem.eql(u8, directory, ".")) self.dir else try self.open_directory(directory) orelse return; + + try self.io_w.print("{s}\n", .{directory}); + + var visited: std.ArrayList(Visited) = try .initCapacity(self.alloc, 0); + defer visited.deinit(self.alloc); + + try walk( + self.alloc, + &self.fs, + dir, + &visited, + 1, + ); + + for (visited.items) |entry| { + const prefix = try make_prefix(self.alloc, entry.level); + defer self.alloc.free(prefix); + defer self.alloc.free(entry.name); + + try self.io_w.print("{s}{s}\n", .{ prefix, entry.name }); + } + + if (!std.mem.eql(u8, directory, ".")) { + dir.close(); + } + } + fn cd(self: *Self, directory: []const u8) !void { + if (std.mem.eql(u8, directory, ".")) return; + + const dir = try self.open_directory(directory) orelse return; + + self.dir.close(); + self.dir = dir; + + if (std.mem.eql(u8, directory, "..")) { + self.alloc.free(self.path.pop() orelse unreachable); + self.alloc.free(self.path.pop() orelse unreachable); + + if (self.path.items.len == 1) { + self.dir.close(); + self.dir = try self.fs.open_root(); + } + + return; + } + + try self.path.append(self.alloc, try self.alloc.dupe(u8, directory)); + try self.path.append(self.alloc, try self.alloc.dupe(u8, "/")); + } + fn stat(self: *Self, file: []const u8) !void { + const entry = (try self.dir.open_entry(file)) orelse { + try self.io_w.print("\"{s}\": No such file or directory\n", .{file}); + return; + }; + defer entry.close(); + + const info = entry.stat(); + + try self.io_w.print( + \\ File: {s} + \\ Size: {b} + \\ Kind: {s} + \\ Hidden: {any} + \\Read Only: {any} + \\ Creation: {d} + \\ Access: {d} + \\ Modify: {d} + \\ + , .{ + info.name, + info.size, + if (info.directory) "directory" else "file", + info.read_only, + info.hidden, + info.creation_time, + info.access_time, + info.modify_time, + }); + } + fn cat(self: *Self, file: []const u8) !void { + var file_entry = (try self.dir.open_entry(file)) orelse { + try self.io_w.print("\"{s}\": No such file or directory\n", .{file}); + return; + }; + defer file_entry.close(); + + const file_handle = try self.fs.open_file(&file_entry); + defer file_entry.close(); + + var contents: std.ArrayList(u8) = try .initCapacity(self.alloc, file_entry.stat().size); + defer contents.deinit(self.alloc); + + while (try file_handle.read()) |bytes| { + defer self.alloc.free(bytes); + try contents.appendSlice(self.alloc, bytes); + } + + try self.io_w.print("{s}\n", .{contents.items}); + } + + // Tree + const Visited = struct { + name: []const u8, + level: usize, + }; + + fn make_prefix(alloc: std.mem.Allocator, level: usize) ![]const u8 { + const prefix = "| "; + const base = "|- "; + + return switch (level) { + 0 => return try alloc.dupe(u8, ""), + 1 => return try alloc.dupe(u8, base), + else => { + var out = try alloc.alloc(u8, (level - 1) * prefix.len + base.len); + for (0..(level - 1)) |i| { + @memcpy(out[i * prefix.len .. (i + 1) * prefix.len], prefix); + } + @memcpy(out[(level - 1) * prefix.len .. level * prefix.len], base); + + return out; + }, + }; + } + + fn walk(alloc: std.mem.Allocator, fs: *vfs.FileSystem, directory: vfs.Directory, visited: *std.ArrayList(Visited), level: usize) !void { + defer directory.close(); + + var it = try directory.iterate(); + defer it.close(); + + while (try it.next()) |entry| { + defer entry.close(); + + const info = entry.stat(); + if (std.mem.eql(u8, info.name, ".") or std.mem.eql(u8, info.name, "..")) continue; + + const dupe_name = try alloc.dupe(u8, info.name); + try visited.append(alloc, .{ .level = level, .name = dupe_name }); + + if (info.directory) { + try walk( + alloc, + fs, + try fs.open_directory(@constCast(&entry)), + visited, + level + 1, + ); + } + } + } +}; + +pub fn main() !void { + var buffer: [1024 * 1024]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + const alloc = fba.allocator(); + + var sd_card = sd.init(alloc); + var fat_vfs: pi.fs.FatVFS = .init(); + + var vfs_disk = vfs.Disk.impl_by(&sd_card); + const vfs_fat = vfs.FileSystem.impl_by(&fat_vfs); + + const mbr: pi.fs.fat.MasterBootRecord = @bitCast(try vfs_disk.read_sector(0)); + + try mbr.debug_print(&uart.writer); + + try vfs_fat.open(alloc, &vfs_disk, mbr.pte1.first_lba); + defer vfs_fat.close(); + + var explorer = try Explorer.init(alloc, vfs_fat, &uart.reader, &uart.writer); + + try explorer.start(); +}