hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-10-04 00:52:49 +0200
committerhachem <im@hachem.wtf>2026-10-04 00:52:49 +0200
commit627f05098785327744e666c9149375125499d2ff (patch)
treea72f3949e0729f1e73ad39ccb1809698a5ed8298
parent09982d80c45805715984cc1cae96c37e4fc148c3 (diff)
feat: revelfs type shi
-rw-r--r--.gitignore3
-rw-r--r--Makefile20
-rw-r--r--boot/ata.zig160
-rw-r--r--boot/boot.zig22
-rw-r--r--boot/port.zig15
-rw-r--r--bridge/bridge.zig37
-rw-r--r--build.zig1
-rw-r--r--kernel/console.rv10
-rw-r--r--kernel/fs.rv221
-rw-r--r--kernel/main.rv10
-rw-r--r--kernel/proc.rv2
-rw-r--r--kernel/shell.rv25
12 files changed, 507 insertions, 19 deletions
diff --git a/.gitignore b/.gitignore
index e26294a..c2d1211 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,9 @@
/iso_root/
/*.iso
+# persistent fs disk image
+/*.img
+
# local boot scratch (screendumps, qemu logs)
/*.ppm
/*.png
diff --git a/Makefile b/Makefile
index 4d95142..ea96df8 100644
--- a/Makefile
+++ b/Makefile
@@ -9,8 +9,10 @@ QEMU ?= qemu-system-x86_64
KERNEL := zig-out/bin/revel
ISO := revel.iso
+DISK := revel-disk.img
+DISK_MB := 16
-.PHONY: all kernel iso run clean distclean
+.PHONY: all kernel iso run clean distclean wipedisk
all: iso
@@ -42,8 +44,20 @@ iso: kernel limine/limine
iso_root -o $(ISO)
./limine/limine bios-install $(ISO)
-run: iso
- $(QEMU) -M q35 -m 256M -cdrom $(ISO) -boot d -no-reboot -no-shutdown
+# persistent scratch disk for the filesystem. survives across boots and across
+# `make clean`, blow it away by hand or with `make wipedisk` if you want a fresh one
+$(DISK):
+ qemu-img create -f raw $(DISK) $(DISK_MB)M
+
+wipedisk:
+ rm -f $(DISK)
+
+# -M pc (i440fx) not q35, we want the piix3 legacy ide controller at the classic
+# 0x1F0 ports so the ata pio driver can find the disk. q35 only gives you ahci
+run: iso $(DISK)
+ $(QEMU) -M pc -m 256M -cdrom $(ISO) -boot d \
+ -drive file=$(DISK),format=raw,if=ide \
+ -no-reboot -no-shutdown
clean:
rm -rf iso_root zig-out .zig-cache $(ISO)
diff --git a/boot/ata.zig b/boot/ata.zig
new file mode 100644
index 0000000..ec63c1c
--- /dev/null
+++ b/boot/ata.zig
@@ -0,0 +1,160 @@
+// ata pio driver, primary bus master drive, 28 bit lba
+//
+// this is the dumbest most simple most retartded possible block device driver
+// it busy polls the status port and shovels one sector at a time through the 16
+// bit data port. no dma no interrupts no second drive. its slow but the whole point
+// is that revo builds a real filesystem on top of these two calls (read/write a 512 byte sector) so
+// i need to rewrite this when im not drunk (never gonna happen)
+//
+// see: https://wiki.osdev.org/ATA_PIO_Mode
+
+const port = @import("port.zig");
+const serial = @import("serial.zig");
+
+pub const SECTOR = 512;
+
+// primary bus io ports
+const DATA = 0x1F0;
+const FEATURES = 0x1F1; // error on read
+const SECCOUNT = 0x1F2;
+const LBA_LO = 0x1F3;
+const LBA_MID = 0x1F4;
+const LBA_HI = 0x1F5;
+const DRIVE = 0x1F6;
+const CMD = 0x1F7; // status on read
+const CTRL = 0x3F6; // alt status / device control
+
+// status bits
+const ST_ERR = 0x01;
+const ST_DRQ = 0x08; // data request, drive wants a word moved
+const ST_DRDY = 0x40;
+const ST_BSY = 0x80;
+
+const CMD_READ = 0x20;
+const CMD_WRITE = 0x30;
+const CMD_FLUSH = 0xE7;
+const CMD_IDENTIFY = 0xEC;
+
+var g_present = false;
+var g_sectors: u64 = 0;
+
+pub fn present() bool {
+ return g_present;
+}
+
+pub fn sectorCount() u64 {
+ return g_sectors;
+}
+
+// reading the alt status 4 times burns ~400ns, the spec wants that gap after a
+// command before the status bits mean anything
+fn delay400() void {
+ _ = port.inb(CTRL);
+ _ = port.inb(CTRL);
+ _ = port.inb(CTRL);
+ _ = port.inb(CTRL);
+}
+
+// spin until bsy drops
+// returns false if it never does (dead bus)
+fn waitNotBusy() bool {
+ var spins: u32 = 0;
+ while (spins < 1_000_000) : (spins += 1) {
+ if (port.inb(CMD) & ST_BSY == 0) return true;
+ }
+ return false;
+}
+
+// spin until the drive is ready to move a word (drq set)
+// false on error/timeout
+fn waitDrq() bool {
+ var spins: u32 = 0;
+ while (spins < 1_000_000) : (spins += 1) {
+ const st = port.inb(CMD);
+ if (st & ST_ERR != 0) return false;
+ if (st & ST_BSY == 0 and st & ST_DRQ != 0) return true;
+ }
+ return false;
+}
+
+// select master and load the lba + a one sector count
+fn selectSector(lba: u28) void {
+ port.outb(DRIVE, 0xE0 | @as(u8, @truncate((lba >> 24) & 0x0F)));
+ port.outb(FEATURES, 0);
+ port.outb(SECCOUNT, 1);
+ port.outb(LBA_LO, @truncate(lba & 0xFF));
+ port.outb(LBA_MID, @truncate((lba >> 8) & 0xFF));
+ port.outb(LBA_HI, @truncate((lba >> 16) & 0xFF));
+}
+
+// probe the primary master with identify, the floating bus reads status
+// 0xFF, a present drive clears bsy and coughs up 256 words of identify data,
+// words 60/61 hold the 28 bit sector count
+pub fn init() void {
+ // a nonexistent bus floats high, bail before we wait a million spins on it
+ if (port.inb(CMD) == 0xFF) {
+ serial.write("ata: no drive on primary bus\r\n");
+ return;
+ }
+
+ port.outb(DRIVE, 0xA0); // master
+ delay400();
+ port.outb(SECCOUNT, 0);
+ port.outb(LBA_LO, 0);
+ port.outb(LBA_MID, 0);
+ port.outb(LBA_HI, 0);
+ port.outb(CMD, CMD_IDENTIFY);
+ delay400();
+
+ if (port.inb(CMD) == 0) {
+ serial.write("ata: primary master absent\r\n");
+ return;
+ }
+ if (!waitDrq()) {
+ serial.write("ata: identify failed\r\n");
+ return;
+ }
+
+ var id: [256]u16 = undefined;
+ for (&id) |*w| w.* = port.inw(DATA);
+
+ g_sectors = (@as(u64, id[61]) << 16) | id[60];
+ g_present = true;
+ serial.write("ata: primary master, sectors=");
+ serial.writeDec(g_sectors);
+ serial.write("\r\n");
+}
+
+// read one 512 byte sector at lba into buf
+pub fn read(lba: u28, buf: *[SECTOR]u8) bool {
+ if (!g_present) return false;
+ if (!waitNotBusy()) return false;
+ selectSector(lba);
+ port.outb(CMD, CMD_READ);
+ if (!waitDrq()) return false;
+ var i: usize = 0;
+ while (i < SECTOR) : (i += 2) {
+ const w = port.inw(DATA);
+ buf[i] = @truncate(w & 0xFF);
+ buf[i + 1] = @truncate(w >> 8);
+ }
+ return true;
+}
+
+// write one 512 byte sector from buf to lba, then flush the drive cache so it
+// actually hits the image and survives a reboot, yk, like an actual file system
+pub fn write(lba: u28, buf: *const [SECTOR]u8) bool {
+ if (!g_present) return false;
+ if (!waitNotBusy()) return false;
+ selectSector(lba);
+ port.outb(CMD, CMD_WRITE);
+ if (!waitDrq()) return false;
+ var i: usize = 0;
+ while (i < SECTOR) : (i += 2) {
+ const w = @as(u16, buf[i]) | (@as(u16, buf[i + 1]) << 8);
+ port.outw(DATA, w);
+ }
+ port.outb(CMD, CMD_FLUSH);
+ _ = waitNotBusy();
+ return true;
+}
diff --git a/boot/boot.zig b/boot/boot.zig
index 9f384fa..28fb8c1 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -12,6 +12,7 @@ const elf = @import("elf.zig");
const keyboard = @import("keyboard.zig");
const timer = @import("timer.zig");
const sched = @import("sched.zig");
+const ata = @import("ata.zig");
const bridge = @import("bridge");
// we need a panic handler otherwise zig wont be happy,
@@ -100,6 +101,22 @@ fn kProcCount() u64 {
return sched.procCount();
}
+// block device, revo builds the filesystem on top of these
+fn kDiskPresent() u64 {
+ return if (ata.present()) 1 else 0;
+}
+fn kDiskSectors() u64 {
+ return ata.sectorCount();
+}
+fn kDiskRead(lba: u64, phys: u64) u64 {
+ const buf: *[ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys));
+ return if (ata.read(@truncate(lba), buf)) 1 else 0;
+}
+fn kDiskWrite(lba: u64, phys: u64) u64 {
+ const buf: *const [ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys));
+ return if (ata.write(@truncate(lba), buf)) 1 else 0;
+}
+
// entry(_start)
export fn _start() callconv(.c) noreturn {
serial.init();
@@ -124,6 +141,7 @@ export fn _start() callconv(.c) noreturn {
pmmSelfTest();
vmmSelfTest();
+ ata.init();
const elf_pages = (elf.hello_elf.len + 4095) / 4096;
const elf_phys = pmm.allocContig(elf_pages) orelse @panic("no room for the embedded ELF");
@@ -150,6 +168,10 @@ export fn _start() callconv(.c) noreturn {
.proc_count = &kProcCount,
.elf_phys = elf_phys,
.elf_size = elf.hello_elf.len,
+ .disk_present = &kDiskPresent,
+ .disk_sectors = &kDiskSectors,
+ .disk_read = &kDiskRead,
+ .disk_write = &kDiskWrite,
};
// hand the kernel heap + framebuffer + low level ops to revo and bring up the vm
diff --git a/boot/port.zig b/boot/port.zig
index be760b1..d38f829 100644
--- a/boot/port.zig
+++ b/boot/port.zig
@@ -17,3 +17,18 @@ pub inline fn inb(port: u16) u8 {
: [port] "N{dx}" (port),
);
}
+
+pub inline fn outw(port: u16, val: u16) void {
+ asm volatile ("outw %[val], %[port]"
+ :
+ : [val] "{ax}" (val),
+ [port] "N{dx}" (port),
+ );
+}
+
+pub inline fn inw(port: u16) u16 {
+ return asm volatile ("inw %[port], %[result]"
+ : [result] "={ax}" (-> u16),
+ : [port] "N{dx}" (port),
+ );
+}
diff --git a/bridge/bridge.zig b/bridge/bridge.zig
index 2c4da6a..c4918b0 100644
--- a/bridge/bridge.zig
+++ b/bridge/bridge.zig
@@ -293,6 +293,11 @@ pub const KernelOps = struct {
proc_count: *const fn () u64,
elf_phys: u64,
elf_size: u64,
+ // block device, the filesystem in revo drives these (512 byte sectors)
+ disk_present: *const fn () u64,
+ disk_sectors: *const fn () u64,
+ disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
+ disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
};
var g_kops: ?KernelOps = null;
@@ -445,6 +450,30 @@ fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
return HostResult.data(Data.new.num(ops.proc_count()));
}
+fn hostDiskPresent(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const ops = g_kops orelse return HostResult.other("no kernel ops");
+ return HostResult.data(Data.new.num(ops.disk_present()));
+}
+
+fn hostDiskSectors(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const ops = g_kops orelse return HostResult.other("no kernel ops");
+ return HostResult.data(Data.new.num(ops.disk_sectors()));
+}
+
+fn hostDiskRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const ops = g_kops orelse return HostResult.other("no kernel ops");
+ const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_read: bad lba");
+ const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_read: bad phys");
+ return HostResult.data(Data.new.num(ops.disk_read(lba, phys)));
+}
+
+fn hostDiskWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const ops = g_kops orelse return HostResult.other("no kernel ops");
+ const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_write: bad lba");
+ const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_write: bad phys");
+ return HostResult.data(Data.new.num(ops.disk_write(lba, phys)));
+}
+
fn registerPrimitives(vm: *revo.VM) !void {
const define = revo.baselib.host.define;
const T = revo.baselib.host.ParamType;
@@ -455,7 +484,7 @@ fn registerPrimitives(vm: *revo.VM) !void {
try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight)));
try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect)));
try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll)));
- // low level: memory + address spaces + process launch (revos elf loader)
+
try vm.registerGlobal("mem_read", try vm.installHost("mem_read", define(&[_]T{ .number, .number }, hostMemRead)));
try vm.registerGlobal("mem_copy", try vm.installHost("mem_copy", define(&[_]T{ .number, .number, .number }, hostMemCopy)));
try vm.registerGlobal("mem_zero", try vm.installHost("mem_zero", define(&[_]T{ .number, .number }, hostMemZero)));
@@ -477,6 +506,11 @@ fn registerPrimitives(vm: *revo.VM) !void {
try vm.registerGlobal("mem_total", try vm.installHost("mem_total", define(&[_]T{}, hostMemTotal)));
try vm.registerGlobal("uptime", try vm.installHost("uptime", define(&[_]T{}, hostUptime)));
try vm.registerGlobal("proc_count", try vm.installHost("proc_count", define(&[_]T{}, hostProcCount)));
+
+ try vm.registerGlobal("disk_present", try vm.installHost("disk_present", define(&[_]T{}, hostDiskPresent)));
+ try vm.registerGlobal("disk_sectors", try vm.installHost("disk_sectors", define(&[_]T{}, hostDiskSectors)));
+ try vm.registerGlobal("disk_read", try vm.installHost("disk_read", define(&[_]T{ .number, .number }, hostDiskRead)));
+ try vm.registerGlobal("disk_write", try vm.installHost("disk_write", define(&[_]T{ .number, .number }, hostDiskWrite)));
}
// the vm outlives boot() now, so we juts make this hoe static
@@ -485,6 +519,7 @@ var g_vm: ?*revo.VM = null;
const init_program = @embedFile("k_font") ++ "\n" ++
@embedFile("k_console") ++ "\n" ++
@embedFile("k_vmm") ++ "\n" ++
+ @embedFile("k_fs") ++ "\n" ++
@embedFile("k_proc") ++ "\n" ++
@embedFile("k_shell") ++ "\n" ++
@embedFile("k_input") ++ "\n" ++
diff --git a/build.zig b/build.zig
index 6d33ab7..9877192 100644
--- a/build.zig
+++ b/build.zig
@@ -67,6 +67,7 @@ pub fn build(b: *std.Build) void {
.{ "k_font", "kernel/font.rv" },
.{ "k_console", "kernel/console.rv" },
.{ "k_vmm", "kernel/vmm.rv" },
+ .{ "k_fs", "kernel/fs.rv" },
.{ "k_proc", "kernel/proc.rv" },
.{ "k_shell", "kernel/shell.rv" },
.{ "k_input", "kernel/input.rv" },
diff --git a/kernel/console.rv b/kernel/console.rv
index d0ee43c..2083817 100644
--- a/kernel/console.rv
+++ b/kernel/console.rv
@@ -1,5 +1,5 @@
-const FW = 8 # glyph width in pixels
-const FH = 16 # glyph height in pixels (8x16 vga bitmap font)
+const FW = 8 # glyph width in pixels
+const FH = 16 # glyph height in pixels (8x16 vga bitmap font)
const FG = 0xd0d0d0
const CON_BG = 0x101018
@@ -39,6 +39,7 @@ end
# we dont do shift/cntrl
# thats a job for future me lmao
+# NOTE: future me kinda delt with it, i was mad drunk
fn con_putc(ch) do
const code = ch:ascii()
if code == 10 do con_newline() return :nil end # \n
@@ -58,7 +59,7 @@ fn puts(str) serial_puts(str)
const CUR = 0x50fa7b
# the cursor is an overlay block. its cleared before any drawing and shown after,
# so it never sits under a glyph, cur_x/cur_y remember where it was actually drawn
-# so we always clear the right cell even after the head moved (backspace etc)
+# so we always clear the right cell even after the head moved (backspace and shit)
fn cursor_clear() do
if con.shown do
fill_rect(con.cur_x * cell_w(), con.cur_y * cell_h(), cell_w(), cell_h(), CON_BG)
@@ -84,9 +85,6 @@ global flush_console = fn() do
cursor_show()
end
-# called each pit tick from the event loop (scheduler task 0). render any output a
-# running process left in the serial mirror (this is how its writes reach the
-# screen while time sliced), reprint the prompt when one exits, else blink
global on_tick = fn(ticks) do
let byte = serial_next()
if byte >= 0 do
diff --git a/kernel/fs.rv b/kernel/fs.rv
new file mode 100644
index 0000000..adb317c
--- /dev/null
+++ b/kernel/fs.rv
@@ -0,0 +1,221 @@
+# revelfs, a tiny fat style filesystem
+#
+# on disk layout for the 16 mib test disk (32768 sectors):
+# sector 0 superblock (magic + total sectors)
+# sectors 1..8 directory, 128 entries of 32 bytes (16 per sector)
+# sectors 9..136 fat, one 2 byte entry per sector on the whole disk
+# sectors 137.. file data, one block == one sector
+#
+# a fat entry is the next block in a files chain, 0 means free, END means last
+# block of a chain. a file just stores its first block + byte size in its dir
+# entry and we walk the chain from there
+
+const SB_LBA = 0
+const DIR_LBA = 1
+const DIR_SECTORS = 8
+const DIR_ENTRIES = 128
+const FAT_LBA = 9
+const FAT_SECTORS = 128
+const DATA_LBA = 137
+const BLOCK = 512
+const NAME_MAX = 24
+const MAGIC = 0x53465652 # "RVFS" little endian
+const END = 0xffff # end of chain marker in the fat
+
+# buf is a scratch sector for the superblock/directory/data, fat is a separate
+# scratch sector so fat lookups dont trample a directory sector were mid edit
+global fs = { buf = 0, fat = 0, ready = :false, total = 0 }
+
+fn fat_get(block) do
+ disk_read(FAT_LBA + (block shr 8), fs.fat)
+ mem_read(fs.fat + (block band 255) * 2, 2)
+end
+
+fn fat_set(block, val) do
+ const lba = FAT_LBA + (block shr 8)
+ disk_read(lba, fs.fat)
+ mem_write(fs.fat + (block band 255) * 2, val, 2)
+ disk_write(lba, fs.fat)
+end
+
+# first free data block, or 0 if the disk is full. scans a whole fat sector per
+# read instead of one entry at a time so this isnt snails pace
+fn fat_alloc() do
+ let block = DATA_LBA
+ while block < fs.total do
+ disk_read(FAT_LBA + (block shr 8), fs.fat)
+ let off = block band 255
+ while (off < 256 and block < fs.total) do
+ if mem_read(fs.fat + off * 2, 2) == 0 return block
+ off += 1
+ block += 1
+ end
+ end
+ 0
+end
+
+fn free_chain(first) do
+ let block = first
+ while (block != 0 and block != END) do
+ const nxt = fat_get(block)
+ fat_set(block, 0)
+ block = nxt
+ end
+end
+
+fn dir_lba(i) DIR_LBA + (i shr 4)
+fn dir_off(i) (i band 15) * 32
+
+# entry byte layout: base is fs.buf + dir_off(i) after loading the sector
+# +0 used (0/1)
+# +1 name length
+# +2 first block (2)
+# +4 size (4)
+# +8 name bytes (up to NAME_MAX)
+fn name_at(base, len) do
+ let out = ""
+ for k in 0..len do out = out ~ string.of_ascii(mem_read(base + 8 + k, 1)) end
+ out
+end
+
+fn name_matches(base, target) do
+ const len = mem_read(base + 1, 1)
+ if len != target:len() return :false
+ for k in 0..len do
+ if mem_read(base + 8 + k, 1) != target[k]:ascii() return :false
+ end
+ :true
+end
+
+fn fs_find(name) do
+ for i in 0..DIR_ENTRIES do
+ disk_read(dir_lba(i), fs.buf)
+ const base = fs.buf + dir_off(i)
+ if mem_read(base, 1) == 1 and name_matches(base, name) return i
+ end
+ return -1
+end
+
+fn dir_alloc() do
+ for i in 0..DIR_ENTRIES do
+ disk_read(dir_lba(i), fs.buf)
+ if mem_read(fs.buf + dir_off(i), 1) == 0 return i
+ end
+ return -1
+end
+
+fn fs_init() do
+ fs.buf = frame_alloc()
+ fs.fat = frame_alloc()
+ fs.total = disk_sectors()
+ if disk_present() == 0 do fs.ready = :false return :nil end
+ disk_read(SB_LBA, fs.buf)
+ fs.ready = if mem_read(fs.buf, 4) == MAGIC :true else :false
+end
+
+fn fs_format() do
+ if disk_present() == 0 do puts("mkfs: no disk\n") return :nil end
+ mem_zero(fs.buf, BLOCK)
+ mem_write(fs.buf, MAGIC, 4)
+ mem_write(fs.buf + 4, fs.total, 4)
+ disk_write(SB_LBA, fs.buf)
+ mem_zero(fs.buf, BLOCK)
+ for s in 0..DIR_SECTORS do disk_write(DIR_LBA + s, fs.buf) end
+ for s in 0..FAT_SECTORS do disk_write(FAT_LBA + s, fs.buf) end
+ fs.ready = :true
+ puts("formatted " ~ fs.total ~ " sectors, " ~ (fs.total shr 1) ~ " KiB\n")
+end
+
+# write a file, gee i wonder what a function called `fs_write` does
+fn fs_write(name, text) do
+ if fs.ready == :false do puts("no filesystem, run mkfs first\n") return :nil end
+ if name:len() > NAME_MAX do puts("name too long (max " ~ NAME_MAX ~ ")\n") return :nil end
+ let idx = fs_find(name)
+ if idx >= 0 do
+ disk_read(dir_lba(idx), fs.buf)
+ free_chain(mem_read(fs.buf + dir_off(idx) + 2, 2))
+ end
+ else do
+ idx = dir_alloc()
+ if idx < 0 do puts("directory full\n") return :nil end
+ end
+
+ const size = text:len()
+ let first = 0
+ let prev = 0
+ let written = 0
+ while written < size do
+ const block = fat_alloc()
+ if block == 0 do puts("disk full\n") return :nil end
+ fat_set(block, END) # reserve
+ if first == 0 do first = block end
+ else do fat_set(prev, block) end
+ mem_zero(fs.buf, BLOCK)
+ let k = 0
+ while (k < BLOCK and written < size) do
+ mem_write(fs.buf + k, text[written]:ascii(), 1)
+ k += 1
+ written += 1
+ end
+ disk_write(block, fs.buf)
+ prev = block
+ end
+
+ disk_read(dir_lba(idx), fs.buf)
+ const base = fs.buf + dir_off(idx)
+ mem_write(base, 1, 1)
+ mem_write(base + 1, name:len(), 1)
+ mem_write(base + 2, first, 2)
+ mem_write(base + 4, size, 4)
+ for k in 0..name:len() do mem_write(base + 8 + k, name[k]:ascii(), 1) end
+ disk_write(dir_lba(idx), fs.buf)
+ puts("wrote " ~ name ~ " (" ~ size ~ " bytes)\n")
+end
+
+fn fs_cat(name) do
+ if fs.ready == :false do puts("no filesystem, run mkfs first\n") return :nil end
+ const idx = fs_find(name)
+ if idx < 0 do puts("cat: " ~ name ~ ": no such file\n") return :nil end
+ disk_read(dir_lba(idx), fs.buf)
+ const base = fs.buf + dir_off(idx)
+ let block = mem_read(base + 2, 2)
+ let remaining = mem_read(base + 4, 4)
+ let out = ""
+ while (block != 0 and block != END and remaining > 0) do
+ const nxt = fat_get(block) # grab next before disk_read reuses fs.buf
+ disk_read(block, fs.buf)
+ const chunk = if remaining > BLOCK BLOCK else remaining
+ for k in 0..chunk do out = out ~ string.of_ascii(mem_read(fs.buf + k, 1)) end
+ remaining -= chunk
+ block = nxt
+ end
+ puts(out)
+ puts("\n")
+end
+
+fn fs_ls() do
+ if fs.ready == :false do puts("no filesystem, run mkfs first\n") return :nil end
+ let count = 0
+ for i in 0..DIR_ENTRIES do
+ disk_read(dir_lba(i), fs.buf)
+ const base = fs.buf + dir_off(i)
+ if mem_read(base, 1) == 1 do
+ puts(" " ~ name_at(base, mem_read(base + 1, 1)) ~ " " ~ mem_read(base + 4, 4) ~ " bytes\n")
+ count += 1
+ end
+ end
+ if count == 0 puts(" (empty)\n")
+end
+
+fn fs_rm(name) do
+ if fs.ready == :false do puts("no filesystem, run mkfs first\n") return :nil end
+ const idx = fs_find(name)
+ if idx < 0 do puts("rm: " ~ name ~ ": no such file\n") return :nil end
+ disk_read(dir_lba(idx), fs.buf)
+ const base = fs.buf + dir_off(idx)
+ free_chain(mem_read(base + 2, 2))
+ disk_read(dir_lba(idx), fs.buf)
+ mem_write(fs.buf + dir_off(idx), 0, 1)
+ disk_write(dir_lba(idx), fs.buf)
+ puts("removed " ~ name ~ "\n")
+end
diff --git a/kernel/main.rv b/kernel/main.rv
index 4a0db06..896bb54 100644
--- a/kernel/main.rv
+++ b/kernel/main.rv
@@ -2,10 +2,16 @@
pic_remap()
pit_init()
-pic_unmask(0) # pit (irq0) -> cursor blink / uptime
-pic_unmask(1) # keyboard (irq1)
+pic_unmask(0) # pit (irq0) -> cursor blink / uptime
+pic_unmask(1) # keyboard (irq1)
cmd_clear()
dump_memmap()
+
+fs_init()
+if disk_present() == 0 puts("no disk attached\n")
+else if fs.ready puts("mounted revelfs on disk\n")
+else puts("disk has no filesystem, run `mkfs`\n")
+
puts("revel shell, live in revo. type `help`\n% ")
flush_console()
diff --git a/kernel/proc.rv b/kernel/proc.rv
index 162d24b..e2f451b 100644
--- a/kernel/proc.rv
+++ b/kernel/proc.rv
@@ -1,7 +1,7 @@
# parse the embedded elf (physical address from elf_phys()) and run it in ring 3
const PT_LOAD = 1
-const PAGE_FLAGS = 6 # write | user
+const PAGE_FLAGS = 6 # write | user
fn cmd_run() do
const base = elf_phys()
diff --git a/kernel/shell.rv b/kernel/shell.rv
index 8d9019f..6e3c2e4 100644
--- a/kernel/shell.rv
+++ b/kernel/shell.rv
@@ -1,5 +1,16 @@
# this is just a simple prompt to test that shit is working
-fn cmd_help() puts("commands: help, clear, bars, echo <text>, time, mem, uptime, ps, run, zoom in|out\n")
+fn cmd_help() do
+ puts("commands: help, clear, bars, echo <text>, time, mem, uptime, ps, run, zoom in|out\n")
+ puts("fs: mkfs, ls, cat <name>, write <name> <text>, rm <name>\n")
+end
+
+fn cmd_write(line, words) do
+ if words:len() < 2 do puts("usage: write <name> <text>\n") return :nil end
+ const name = words[1]
+ const start = 6 + name:len() + 1
+ const text = if line:len() > start line:sub(start, line:len() - start) else ""
+ fs_write(name, text)
+end
fn zoom_by(delta) do
let scale = con.scale + delta
@@ -37,7 +48,7 @@ fn rtc(reg) do
end
fn cmd_time() puts("utc " ~ bcd(rtc(4)) ~ ":" ~ bcd(rtc(2)) ~ ":" ~ bcd(rtc(0)) ~ "\n")
-# 0x prefixed 16 digit hex, matching the kernels serial writehex
+# 0x prefixed 16 digit hex
fn hex(n) do
const digits = "0123456789abcdef"
let out = ""
@@ -57,7 +68,7 @@ fn mem_type_name(kind) do
"unknown"
end
-# the boot memory map dump, in revo now (called from kmain once the vm is up)
+# the boot memory map dump
fn dump_memmap() do
puts("memory map:\n")
for i in 0..memmap_count() do
@@ -74,9 +85,6 @@ fn run(line) do
if line:trim():len() == 0 do puts("% ") return :nil end
const words = line:split(" ")
const cmd = words[0]
- # `run` launches a process, its prompt is reprinted by on_tick when the process
- # exits (so it lands below the programs output, not before it). return early to
- # skip the trailing prompt here
if cmd == "run" do cmd_run() return :nil end
match cmd
| "help" => cmd_help()
@@ -87,6 +95,11 @@ fn run(line) do
| "uptime" => cmd_uptime()
| "ps" => cmd_ps()
| "zoom" => cmd_zoom(words)
+ | "mkfs" => fs_format()
+ | "ls" => fs_ls()
+ | "cat" => if words:len() > 1 fs_cat(words[1]) else puts("usage: cat <name>\n")
+ | "write" => cmd_write(line, words)
+ | "rm" => if words:len() > 1 fs_rm(words[1]) else puts("usage: rm <name>\n")
| "echo" => if line:len() > 5 puts(line:sub(5, line:len() - 5) ~ "\n") else puts("\n")
| _ => puts("unknown command: " ~ cmd ~ "\n")
puts("% ")