commit e90acc16640527926b45080708ac11997f88b049
parent 99ec7c869868c1b65f8795dc5fbc40282e7dbb6f
Author: Sylvia Ivory <git@sivory.net>
Date: Sun, 8 Mar 2026 21:13:12 -0700
Add explorer program
Diffstat:
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();
+}