hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/kernel
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 /kernel
parent09982d80c45805715984cc1cae96c37e4fc148c3 (diff)
feat: revelfs type shi
Diffstat (limited to 'kernel')
-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
5 files changed, 253 insertions, 15 deletions
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("% ")