summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--CONTEXT.md39
-rw-r--r--Cargo.toml16
-rw-r--r--rsdos/Cargo.toml3
-rw-r--r--rsdos/src/main.rs26
-rw-r--r--rsdosedit/Cargo.toml3
-rw-r--r--rsdosedit/src/lib.rs7
-rw-r--r--rsnix/Cargo.toml3
-rw-r--r--rsnix/src/ahci.rs8
-rw-r--r--rsnix/src/fat.rs130
-rw-r--r--rsnix/src/font.rs8
-rw-r--r--rsnix/src/gop.rs26
-rw-r--r--rsnix/src/interrupts.rs70
-rw-r--r--rsnix/src/memory.rs11
-rw-r--r--rsnix/src/ps2.rs10
-rw-r--r--rsnix/src/tty.rs1
-rw-r--r--rsnix/src/vt.rs8
-rw-r--r--shell/Cargo.toml3
18 files changed, 227 insertions, 146 deletions
diff --git a/.gitignore b/.gitignore
index 42b56cf..ae44880 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
3/esp/ 3/esp/
4*.img 4*.img
5NvVars 5NvVars
6/.pi-subagents/
diff --git a/CONTEXT.md b/CONTEXT.md
index 68d2368..1c73656 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -4,11 +4,12 @@
4 4
5Custom x86_64 kernel for the RNG ecosystem. Toy project — self-contained, 5Custom x86_64 kernel for the RNG ecosystem. Toy project — self-contained,
6no strict userspace separation, TempleOS-inspired design philosophy. 6no strict userspace separation, TempleOS-inspired design philosophy.
7Built in Rust, compiled for the `x86_64-unknown-uefi` target (nightly). 7Built in Rust, compiled for the `x86_64-unknown-uefi` target (stable).
8 8
9## Boot interface 9## Boot interface
10 10
11The kernel is a **PE32+ EFI application** loaded via UEFI `LoadImage`/`StartImage`. 11The kernel is a **PE32+ EFI application** loaded via UEFI
12`LoadImage`/`StartImage`.
12The existing bootloader (rsbl) loads us from `\bzImage` and passes the UEFI 13The existing bootloader (rsbl) loads us from `\bzImage` and passes the UEFI
13system table. No bootloader changes needed. 14system table. No bootloader changes needed.
14 15
@@ -56,9 +57,10 @@ same RAM. Contiguous runs with power-of-two alignment are supported
56 57
57## Input strategy 58## Input strategy
58 59
59**PS/2 keyboard polling.** Port I/O to `0x60`/`0x64`. Scancode set 1 60**PS/2 keyboard, interrupt-driven.** IRQ1 pushes scancodes into a ring
60(US QWERTY layout in the initial build). No interrupts — polling loop 61buffer (`interrupts::KEYBUF`); the shell polls it via `read_scancode()`.
61inside the shell. 62Port I/O to `0x60`/`0x64`. Scancode set 1 (US QWERTY layout in the
63initial build).
62 64
63## Display strategy 65## Display strategy
64 66
@@ -86,7 +88,14 @@ image as `\bzImage` (matching rsbl's expected path).
86 88
87## Project layout 89## Project layout
88 90
89Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: 91Workspace of four crates, all built for the `x86_64-unknown-uefi` target:
92
93- `rsnix` — the kernel library (module breakdown below)
94- `shell` — command loop and built-ins
95- `rsdos` — EFI entry point (`efi_main`) and boot orchestration
96- `rsdosedit` — kernel-native terminal editor
97
98`rsnix` module breakdown:
90 99
91- `main.rs` — entry point, panic handler, top-level init 100- `main.rs` — entry point, panic handler, top-level init
92- `uefi.rs` — UEFI protocol type definitions 101- `uefi.rs` — UEFI protocol type definitions
@@ -97,14 +106,17 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
97- `paging.rs` — identity page-table helpers (map/unmap/query, large-page split) 106- `paging.rs` — identity page-table helpers (map/unmap/query, large-page split)
98- `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback 107- `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback
99- `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization 108- `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization
100- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration; `config_write` 109- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full
110 enumeration; `config_write`
101 - 64-bit BAR reader for the AHCI driver 111 - 64-bit BAR reader for the AHCI driver
102- `ahci.rs` — SATA AHCI host driver (polling, DMA, no interrupts): scans PCI for 112- `ahci.rs` — SATA AHCI host driver (polling, DMA, no interrupts): scans PCI for
103 class 1/subclass 6/prog-if 1, maps ABAR, starts port 0 (FRE/ST/DMAE), runs 113 class 1/subclass 6/prog-if 1, maps ABAR, starts port 0 (FRE/ST/DMAE), runs
104 IDENTIFY + READ/WRITE DMA EXT through a 4 KiB DMA buffer 114 IDENTIFY + READ/WRITE DMA EXT through a 4 KiB DMA buffer
105- `fat.rs` — FAT32: BPB parse, whole-FAT cache in RAM with write-back to both FAT 115- `fat.rs` — FAT32: BPB parse, whole-FAT cache in RAM with
116 write-back to both FAT
106 copies, cluster chains, directory scan with checksum-validated LFN 117 copies, cluster chains, directory scan with checksum-validated LFN
107 reassembly, LFN + 8.3 writing, FSInfo free-count maintenance, mkdir/rm/read/write 118 reassembly, LFN + 8.3 writing, FSInfo free-count maintenance,
119 mkdir/rm/read/write
108- `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`, 120- `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`,
109 `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs; 121 `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs;
110 `/etc/motd`); `/disk` mount node + 24-slot ephemeral node pool so shell 122 `/etc/motd`); `/disk` mount node + 24-slot ephemeral node pool so shell
@@ -130,14 +142,17 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
130 142
131## Milestone status (v0.1) 143## Milestone status (v0.1)
132 144
133✅ **Booting** — kernel loads via rsbl, prints banner via UEFI console (verified on real hardware) 145✅ **Booting** — kernel loads via rsbl, prints banner via UEFI
146 console (verified on real hardware)
134✅ **ExitBootServices** — memory map captured via static `.bss` buffer 147✅ **ExitBootServices** — memory map captured via static `.bss` buffer
135 (see `docs/adr/0001-static-buffer-for-memory-map.md`) 148 (see `docs/adr/0001-static-buffer-for-memory-map.md`)
136✅ **Serial** — UART 16550 at COM1, 115200 8N1 149✅ **Serial** — UART 16550 at COM1, 115200 8N1
137✅ **Framebuffer** — GOP located, 8×16 bitmap font renders text 150✅ **Framebuffer** — GOP located, 8×16 bitmap font renders text
138✅ **Keyboard** — PS/2 polling, scancode set 1, US QWERTY 151✅ **Keyboard** — PS/2 polling, scancode set 1, US QWERTY
139✅ **Shell** — commands: `help`, `echo`, `clear`, `hello`, `info`, `mem`, `halt`, `poweroff` 152✅ **Shell** — commands: `help`, `echo`, `clear`, `hello`, `info`,
140✅ **Heap allocator** — first-fit free-list with splitting + coalescing, `alloc`/`free` 153 `mem`, `halt`, `poweroff`
154✅ **Heap allocator** — first-fit free-list with splitting +
155 coalescing, `alloc`/`free`
141 (boot-time self-test verifies alignment, reuse, and full coalescing) 156 (boot-time self-test verifies alignment, reuse, and full coalescing)
142✅ **Physical pages** — bitmap allocator over conventional regions, contiguous 157✅ **Physical pages** — bitmap allocator over conventional regions, contiguous
143 runs, heap reservation, page-0 trap guard (boot-time self-test included) 158 runs, heap reservation, page-0 trap guard (boot-time self-test included)
diff --git a/Cargo.toml b/Cargo.toml
index cf18339..0cc785d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,3 +5,19 @@ members = ["rsnix", "shell", "rsdos", "rsdosedit"]
5[workspace.package] 5[workspace.package]
6version = "0.2.0" 6version = "0.2.0"
7edition = "2021" 7edition = "2021"
8
9# no_std UEFI kernel: no unwinder available, so panic must abort.
10# (Also makes bare `cargo check`/`cargo build` without --target work.)
11[profile.dev]
12panic = "abort"
13
14[profile.release]
15panic = "abort"
16
17# Deliberate kernel style, keep clippy quiet without rewriting hot paths:
18# - `Result<_, ()>` is the internal error idiom everywhere (fs/fat/ahci)
19# - indexed loops over static buffers are clearer in kernel code (and
20# iterator rewrites would recreate `static_mut_refs` references)
21[workspace.lints.clippy]
22result_unit_err = "allow"
23needless_range_loop = "allow"
diff --git a/rsdos/Cargo.toml b/rsdos/Cargo.toml
index 5bf83fe..d09bd70 100644
--- a/rsdos/Cargo.toml
+++ b/rsdos/Cargo.toml
@@ -3,6 +3,9 @@ name = "rsdos"
3version.workspace = true 3version.workspace = true
4edition.workspace = true 4edition.workspace = true
5 5
6[lints]
7workspace = true
8
6[dependencies] 9[dependencies]
7rsnix = { path = "../rsnix" } 10rsnix = { path = "../rsnix" }
8shell = { path = "../shell" } 11shell = { path = "../shell" }
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index e6b7d2a..2ba5d32 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -1,6 +1,6 @@
1#![no_std] 1#![no_std]
2#![no_main] 2#![no_main]
3#![allow(static_mut_refs, unsafe_code)] 3#![allow(unsafe_code)]
4 4
5use core::panic::PanicInfo; 5use core::panic::PanicInfo;
6use rsnix::uefi::*; 6use rsnix::uefi::*;
@@ -31,8 +31,15 @@ fn panic(info: &PanicInfo) -> ! {
31// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
32// EFI entry point 32// EFI entry point
33// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
34/// EFI entry point.
35///
36/// # Safety
37///
38/// The firmware hands us raw pointers (image handle, system table) that
39/// must remain valid for the lifetime of the call — a contract guaranteed
40/// by the UEFI loader.
34#[no_mangle] 41#[no_mangle]
35pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemTable) -> ! { 42pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemTable) -> ! {
36 let st = unsafe { &mut *system_table }; 43 let st = unsafe { &mut *system_table };
37 let bs = unsafe { &mut *st.boot_services }; 44 let bs = unsafe { &mut *st.boot_services };
38 let con_out = unsafe { &mut *st.con_out }; 45 let con_out = unsafe { &mut *st.con_out };
@@ -83,22 +90,13 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT
83 rsnix::serial_println!("gop: not found (0x{:x})", gop_result); 90 rsnix::serial_println!("gop: not found (0x{:x})", gop_result);
84 } 91 }
85 rsnix::vt::init(); 92 rsnix::vt::init();
86 rsnix::serial_println!( 93 let (rows, cols) = rsnix::vt::size();
87 "vt: terminal ready ({}x{})", 94 rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols);
88 {
89 let (rows, cols) = rsnix::vt::size();
90 rows
91 },
92 {
93 let (rows, cols) = rsnix::vt::size();
94 cols
95 }
96 );
97 95
98 // ExitBootServices — we now own the machine 96 // ExitBootServices — we now own the machine
99 if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { 97 if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS {
100 rsnix::serial_println!("WARN: ExitBootServices failed, retrying..."); 98 rsnix::serial_println!("WARN: ExitBootServices failed, retrying...");
101 match rsnix::memory::refresh_and_exit(bs, image_handle) { 99 match unsafe { rsnix::memory::refresh_and_exit(bs, image_handle) } {
102 Ok(_) => {} 100 Ok(_) => {}
103 Err(e) => { 101 Err(e) => {
104 rsnix::serial_println!("FATAL: cannot ExitBootServices (0x{:x})", e); 102 rsnix::serial_println!("FATAL: cannot ExitBootServices (0x{:x})", e);
diff --git a/rsdosedit/Cargo.toml b/rsdosedit/Cargo.toml
index a15375c..b271eef 100644
--- a/rsdosedit/Cargo.toml
+++ b/rsdosedit/Cargo.toml
@@ -3,5 +3,8 @@ name = "rsdosedit"
3version = "0.1.0" 3version = "0.1.0"
4edition = "2021" 4edition = "2021"
5 5
6[lints]
7workspace = true
8
6[dependencies] 9[dependencies]
7rsnix = { path = "../rsnix" } 10rsnix = { path = "../rsnix" }
diff --git a/rsdosedit/src/lib.rs b/rsdosedit/src/lib.rs
index 2d04da2..cfb3b0e 100644
--- a/rsdosedit/src/lib.rs
+++ b/rsdosedit/src/lib.rs
@@ -321,12 +321,7 @@ impl Editor {
321 fn run(&mut self) { 321 fn run(&mut self) {
322 self.draw_full(); 322 self.draw_full();
323 323
324 loop { 324 while let Some(b) = self.read_byte() {
325 let b = match self.read_byte() {
326 Some(b) => b,
327 None => break,
328 };
329
330 self.key_count += 1; 325 self.key_count += 1;
331 self.status_msg.clear(); 326 self.status_msg.clear();
332 327
diff --git a/rsnix/Cargo.toml b/rsnix/Cargo.toml
index d8cd26b..5225b1a 100644
--- a/rsnix/Cargo.toml
+++ b/rsnix/Cargo.toml
@@ -2,3 +2,6 @@
2name = "rsnix" 2name = "rsnix"
3version.workspace = true 3version.workspace = true
4edition.workspace = true 4edition.workspace = true
5
6[lints]
7workspace = true
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs
index 427345d..9a49431 100644
--- a/rsnix/src/ahci.rs
+++ b/rsnix/src/ahci.rs
@@ -54,7 +54,6 @@ const ATA_WRITE_DMA_EXT: u8 = 0x35;
54struct Disk { 54struct Disk {
55 port: usize, // port register base = ABAR + 0x100 55 port: usize, // port register base = ABAR + 0x100
56 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned 56 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned
57 fis: usize, // FIS receive area, 256 B aligned
58 ct: usize, // command table, 256 B, 128 B aligned 57 ct: usize, // command table, 256 B, 128 B aligned
59 buf: usize, // 4 KiB DMA data buffer (page aligned) 58 buf: usize, // 4 KiB DMA data buffer (page aligned)
60 sectors: u64, // total sectors (LBA48 from IDENTIFY) 59 sectors: u64, // total sectors (LBA48 from IDENTIFY)
@@ -175,7 +174,6 @@ fn start_port(abar: usize, port_idx: u32) -> Option<Disk> {
175 let mut d = Disk { 174 let mut d = Disk {
176 port, 175 port,
177 clb, 176 clb,
178 fis,
179 ct, 177 ct,
180 buf, 178 buf,
181 sectors: 0, 179 sectors: 0,
@@ -389,8 +387,12 @@ fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()>
389 Ok(()) 387 Ok(())
390} 388}
391 389
390fn disks() -> &'static mut [Option<Disk>; MAX_DISKS] {
391 unsafe { &mut *core::ptr::addr_of_mut!(DISKS) }
392}
393
392fn active() -> Option<&'static Disk> { 394fn active() -> Option<&'static Disk> {
393 unsafe { DISKS.get(ACTIVE).and_then(|d| d.as_ref()) } 395 disks().get(unsafe { ACTIVE }).and_then(|d| d.as_ref())
394} 396}
395 397
396/// DMA `count` sectors starting at `lba` into the selected disk's buffer. 398/// DMA `count` sectors starting at `lba` into the selected disk's buffer.
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
index 52aa641..13d19df 100644
--- a/rsnix/src/fat.rs
+++ b/rsnix/src/fat.rs
@@ -38,7 +38,6 @@ struct Volume {
38 uid: String, // unique mount name: "LABEL-XXXXXXXX" 38 uid: String, // unique mount name: "LABEL-XXXXXXXX"
39 sectors_per_cluster: u8, 39 sectors_per_cluster: u8,
40 reserved: u16, 40 reserved: u16,
41 fat_count: u8,
42 fat_size: u32, // sectors per FAT 41 fat_size: u32, // sectors per FAT
43 root_cluster: u32, 42 root_cluster: u32,
44 first_data: u32, // LBA of cluster 2 43 first_data: u32, // LBA of cluster 2
@@ -50,28 +49,36 @@ struct Volume {
50static mut VOLS: Vec<Volume> = Vec::new(); 49static mut VOLS: Vec<Volume> = Vec::new();
51static mut ACTIVE_VOL: usize = 0; 50static mut ACTIVE_VOL: usize = 0;
52 51
52/// Raw-pointer accessors for the volume statics. The kernel is
53/// single-threaded, so `&'static` refs are sound; the `addr_of_mut!`
54/// round-trip keeps `static_mut_refs` (warn today, deny in edition
55/// 2024) quiet.
56fn vols() -> &'static mut Vec<Volume> {
57 unsafe { &mut *core::ptr::addr_of_mut!(VOLS) }
58}
59
60fn active_vol() -> &'static mut usize {
61 unsafe { &mut *core::ptr::addr_of_mut!(ACTIVE_VOL) }
62}
63
53pub fn volume_count() -> usize { 64pub fn volume_count() -> usize {
54 unsafe { VOLS.len() } 65 vols().len()
55} 66}
56 67
57/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F"). 68/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F").
58pub fn uid(vol: usize) -> &'static str { 69pub fn uid(vol: usize) -> &'static str {
59 unsafe { VOLS.get(vol) } 70 vols().get(vol).map(|v| v.uid.as_str()).unwrap_or("")
60 .map(|v| v.uid.as_str())
61 .unwrap_or("")
62} 71}
63 72
64pub fn root_cluster(vol: usize) -> u32 { 73pub fn root_cluster(vol: usize) -> u32 {
65 unsafe { VOLS.get(vol) } 74 vols().get(vol).map(|v| v.root_cluster).unwrap_or(0)
66 .map(|v| v.root_cluster)
67 .unwrap_or(0)
68} 75}
69 76
70/// Point all FAT operations at volume `vol`; also selects the backing 77/// Point all FAT operations at volume `vol`; also selects the backing
71/// AHCI disk. Called by every public entry point (single-threaded 78/// AHCI disk. Called by every public entry point (single-threaded
72/// kernel, so a process-wide "current volume" is fine). 79/// kernel, so a process-wide "current volume" is fine).
73fn select_vol(vol: usize) -> Result<(), ()> { 80fn select_vol(vol: usize) -> Result<(), ()> {
74 let disk = unsafe { VOLS.get(vol) }.map(|v| v.disk).ok_or(())?; 81 let disk = vols().get(vol).map(|v| v.disk).ok_or(())?;
75 unsafe { 82 unsafe {
76 ACTIVE_VOL = vol; 83 ACTIVE_VOL = vol;
77 } 84 }
@@ -97,7 +104,7 @@ fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> {
97// --- cluster math ---------------------------------------------------------- 104// --- cluster math ----------------------------------------------------------
98 105
99fn v() -> &'static mut Volume { 106fn v() -> &'static mut Volume {
100 unsafe { VOLS.get_mut(ACTIVE_VOL) }.unwrap() 107 vols().get_mut(*active_vol()).unwrap()
101} 108}
102 109
103fn spc() -> usize { 110fn spc() -> usize {
@@ -351,13 +358,13 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
351 return Err(()); // loop protection on a corrupt chain 358 return Err(()); // loop protection on a corrupt chain
352 } 359 }
353 read_cluster(c, &mut buf)?; 360 read_cluster(c, &mut buf)?;
354 let mut done = false;
355 for (i, e) in buf.chunks_exact(32).enumerate() { 361 for (i, e) in buf.chunks_exact(32).enumerate() {
356 match e[0] { 362 match e[0] {
357 0x00 => { 363 // 0x00 ends the entries in THIS cluster, not the whole
358 done = true; 364 // directory: a dir that spills into a second cluster keeps
359 break; 365 // its 0x00 marker in the first cluster, so we must keep
360 } 366 // walking the chain to see the rest.
367 0x00 => break,
361 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN 368 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN
362 _ => { 369 _ => {
363 let attr = e[11]; 370 let attr = e[11];
@@ -415,9 +422,6 @@ fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
415 } 422 }
416 } 423 }
417 } 424 }
418 if done {
419 break;
420 }
421 cur = next_cluster(c).ok(); 425 cur = next_cluster(c).ok();
422 } 426 }
423 Ok(items) 427 Ok(items)
@@ -729,10 +733,77 @@ fn to_8_3(name: &str) -> Option<([u8; 11], bool)> {
729 Some((sfn, needs)) 733 Some((sfn, needs))
730} 734}
731 735
736/// Collect the raw short names already present in a directory.
737fn dir_short_names(parent: u32) -> Result<Vec<[u8; 11]>, ()> {
738 let cs = cluster_size();
739 let mut buf = vec![0u8; cs];
740 let mut out = Vec::new();
741 let mut cur: Option<u32> = Some(parent);
742 let mut guard = 0;
743 while let Some(c) = cur {
744 guard += 1;
745 if guard > 1_000_000 {
746 return Err(()); // loop protection on a corrupt chain
747 }
748 read_cluster(c, &mut buf)?;
749 for e in buf.chunks_exact(32) {
750 match e[0] {
751 // Same rule as scan_dir: 0x00 ends this cluster's entries
752 // but the directory may continue in the next cluster.
753 0x00 => break,
754 0xE5 => {}
755 _ => {
756 if e[11] != 0x0F {
757 let mut sfn = [0u8; 11];
758 sfn.copy_from_slice(&e[0..11]);
759 out.push(sfn);
760 }
761 }
762 }
763 }
764 cur = next_cluster(c).ok();
765 }
766 Ok(out)
767}
768
769/// Make `base` unique within a directory: on collision, append a numeric
770/// ~N tail (DISK-TE~1, DISK-TE~2, …) — the standard FAT short-name
771/// disambiguation that fsck and other OSes expect. Without it, two files
772/// whose long names share an 8.3 prefix would get the same short name and
773/// fsck reports them as duplicate directory entries.
774fn unique_short_name(base: &[u8; 11], used: &[[u8; 11]]) -> [u8; 11] {
775 if !used.contains(base) {
776 return *base;
777 }
778 let mut name_len = 0;
779 while name_len < 8 && base[name_len] != b' ' {
780 name_len += 1;
781 }
782 for n in 1..=999u32 {
783 let tail = alloc::format!("~{}", n);
784 let tb = tail.as_bytes();
785 let keep = name_len.saturating_sub(tb.len());
786 let mut cand = [b' '; 11];
787 cand[..keep].copy_from_slice(&base[..keep]);
788 cand[keep..keep + tb.len()].copy_from_slice(tb);
789 cand[8..11].copy_from_slice(&base[8..11]);
790 if !used.contains(&cand) {
791 return cand;
792 }
793 }
794 *base // pathological; keep the colliding name
795}
796
732/// Write the LFN entries (if any) + SFN entry for `name` into the parent 797/// Write the LFN entries (if any) + SFN entry for `name` into the parent
733/// directory, extending its chain with a fresh zeroed cluster if needed. 798/// directory, extending its chain with a fresh zeroed cluster if needed.
734fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> { 799fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> {
735 let (sfn, needs_lfn) = to_8_3(name).ok_or(())?; 800 let (base_sfn, mut needs_lfn) = to_8_3(name).ok_or(())?;
801 // Short names must be unique within the directory; a plain truncation
802 // collides when two long names share an 8.3 prefix.
803 let sfn = unique_short_name(&base_sfn, &dir_short_names(parent)?);
804 if sfn != base_sfn {
805 needs_lfn = true; // keep the original name via an LFN entry
806 }
736 let lfn_chars: Vec<u16> = name.encode_utf16().collect(); 807 let lfn_chars: Vec<u16> = name.encode_utf16().collect();
737 let groups = if needs_lfn { 808 let groups = if needs_lfn {
738 lfn_chars.len().div_ceil(13) 809 lfn_chars.len().div_ceil(13)
@@ -810,6 +881,18 @@ fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<
810 write_cluster(c, &buf)?; 881 write_cluster(c, &buf)?;
811 return Ok(()); 882 return Ok(());
812 } 883 }
884 // Chain full: pack this cluster before extending. A mid-chain
885 // 0x00 would look like the end of the directory to other FAT
886 // readers (Windows, mtools stop at the first 0x00), hiding every
887 // entry written into the new cluster. Marking the free slots as
888 // 0xE5 (deleted) keeps them skippable while the chain walk finds
889 // the continuation.
890 for i in 0..slots {
891 if buf[i * 32] == 0x00 {
892 buf[i * 32] = 0xE5;
893 }
894 }
895 write_cluster(c, &buf)?;
813 // Chain full: extend with a fresh zeroed cluster and retry. 896 // Chain full: extend with a fresh zeroed cluster and retry.
814 match next_cluster(c) { 897 match next_cluster(c) {
815 Ok(n) => cur = Some(n), 898 Ok(n) => cur = Some(n),
@@ -910,10 +993,8 @@ pub fn init() -> bool {
910 match try_mount(d) { 993 match try_mount(d) {
911 Some(volume) => { 994 Some(volume) => {
912 let uid = volume.uid.clone(); 995 let uid = volume.uid.clone();
913 let vol = unsafe { 996 vols().push(volume);
914 VOLS.push(volume); 997 let vol = vols().len() - 1;
915 VOLS.len() - 1
916 };
917 // Re-sync the FSInfo free-cluster count (may be stale 998 // Re-sync the FSInfo free-cluster count (may be stale
918 // from earlier sessions). 999 // from earlier sessions).
919 if select_vol(vol).is_ok() { 1000 if select_vol(vol).is_ok() {
@@ -1026,7 +1107,7 @@ fn try_mount(d: usize) -> Option<Volume> {
1026 let mut uid = alloc::format!("{}-{:08X}", label, serial); 1107 let mut uid = alloc::format!("{}-{:08X}", label, serial);
1027 let mut n = 2u32; 1108 let mut n = 2u32;
1028 loop { 1109 loop {
1029 let taken = unsafe { VOLS.iter() }.any(|v| v.uid == uid); 1110 let taken = vols().iter().any(|v| v.uid == uid);
1030 if !taken { 1111 if !taken {
1031 break; 1112 break;
1032 } 1113 }
@@ -1038,7 +1119,6 @@ fn try_mount(d: usize) -> Option<Volume> {
1038 uid, 1119 uid,
1039 sectors_per_cluster: spc, 1120 sectors_per_cluster: spc,
1040 reserved, 1121 reserved,
1041 fat_count,
1042 fat_size, 1122 fat_size,
1043 root_cluster, 1123 root_cluster,
1044 first_data, 1124 first_data,
diff --git a/rsnix/src/font.rs b/rsnix/src/font.rs
index e188513..35327e7 100644
--- a/rsnix/src/font.rs
+++ b/rsnix/src/font.rs
@@ -1,5 +1,5 @@
1/// 8×16 bitmap font. 1// 8×16 bitmap font.
2/// 2//
3/// Glyphs are generated at build time by `build.rs` from the system PSF2 font 3// Glyphs are generated at build time by `build.rs` from the system PSF2 font
4/// (`/usr/share/kbd/consolefonts/lat1-16.psfu.gz`). 4// (`/usr/share/kbd/consolefonts/lat1-16.psfu.gz`).
5include!(concat!(env!("OUT_DIR"), "/font_data.rs")); 5include!(concat!(env!("OUT_DIR"), "/font_data.rs"));
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 77cc514..0d24d7d 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -105,23 +105,25 @@ pub fn clear_screen() {
105 crate::vt::clear_all(); 105 crate::vt::clear_all();
106} 106}
107 107
108/// Raw-pointer accessor for the framebuffer static (single-threaded
109/// kernel; keeps `static_mut_refs` quiet).
110fn fb() -> &'static mut Option<Framebuffer> {
111 unsafe { &mut *core::ptr::addr_of_mut!(FB) }
112}
113
108/// (cols, rows) of the text surface, from the current framebuffer mode. 114/// (cols, rows) of the text surface, from the current framebuffer mode.
109pub fn dims() -> (usize, usize) { 115pub fn dims() -> (usize, usize) {
110 unsafe { 116 match fb().as_ref() {
111 match FB.as_ref() { 117 Some(f) => (f.width / 8, f.height / 16),
112 Some(f) => (f.width / 8, f.height / 16), 118 None => (0, 0),
113 None => (0, 0),
114 }
115 } 119 }
116} 120}
117 121
118/// Pack an (r, g, b) colour for the framebuffer's pixel format. 122/// Pack an (r, g, b) colour for the framebuffer's pixel format.
119pub fn rgb(r: u8, g: u8, b: u8) -> u32 { 123pub fn rgb(r: u8, g: u8, b: u8) -> u32 {
120 unsafe { 124 match fb().as_ref() {
121 match FB.as_ref() { 125 Some(f) if f.bgr => ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
122 Some(f) if f.bgr => ((r as u32) << 16) | ((g as u32) << 8) | b as u32, 126 _ => ((b as u32) << 16) | ((g as u32) << 8) | r as u32,
123 _ => ((b as u32) << 16) | ((g as u32) << 8) | r as u32,
124 }
125 } 127 }
126} 128}
127 129
@@ -340,7 +342,7 @@ fn unicode_glyph_bits(ch: u16) -> Option<[u8; 16]> {
340} 342}
341 343
342pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { 344pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) {
343 if let Some(fb) = unsafe { FB.as_ref() } { 345 if let Some(fb) = fb().as_ref() {
344 let glyph = font::GLYPH_8X16; 346 let glyph = font::GLYPH_8X16;
345 let data = if ch < 256 { 347 let data = if ch < 256 {
346 match glyph.get(ch as usize) { 348 match glyph.get(ch as usize) {
@@ -381,7 +383,7 @@ pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg:
381/// Shift the framebuffer content up `n` text rows and clear the exposed 383/// Shift the framebuffer content up `n` text rows and clear the exposed
382/// bottom rows. `n` batches deferred scrolls into one push. 384/// bottom rows. `n` batches deferred scrolls into one push.
383pub(crate) fn shift_up_n(n: usize) { 385pub(crate) fn shift_up_n(n: usize) {
384 if let Some(fb) = unsafe { FB.as_ref() } { 386 if let Some(fb) = fb().as_ref() {
385 fb.shift_up_n(n); 387 fb.shift_up_n(n);
386 } 388 }
387} 389}
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs
index 66dc59f..cbd234e 100644
--- a/rsnix/src/interrupts.rs
+++ b/rsnix/src/interrupts.rs
@@ -98,7 +98,7 @@ fn pit_set_rate(hz: u64) {
98 let divisor = (PIT_FREQ / hz) as u16; 98 let divisor = (PIT_FREQ / hz) as u16;
99 unsafe { 99 unsafe {
100 // Mode 2, channel 0, lobyte/hibyte, binary 100 // Mode 2, channel 0, lobyte/hibyte, binary
101 asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b00_11_010_0u8, 101 asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b0011_0100u8,
102 options(nostack, preserves_flags)); 102 options(nostack, preserves_flags));
103 asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor & 0xFF) as u8, 103 asm!("out dx, al", in("dx") PIT_CH0, in("al") (divisor & 0xFF) as u8,
104 options(nostack, preserves_flags)); 104 options(nostack, preserves_flags));
@@ -122,47 +122,10 @@ unsafe impl Sync for KeyBuf {}
122 122
123impl KeyBuf { 123impl KeyBuf {
124 const fn new() -> Self { 124 const fn new() -> Self {
125 // UnsafeCell requires const initialisation in a const ctx. 125 // Inline-const array repeat (stable since 1.79): each element is a
126 // Build an array the long way — one cell at a time. 126 // fresh `UnsafeCell::new(0)` value, so no interior-mutable const.
127 const NEW_CELL: core::cell::UnsafeCell<u8> = core::cell::UnsafeCell::new(0);
128 // `[NEW_CELL; 256]` needs Clone, which UnsafeCell doesn't have.
129 // Use a union trick or just mark the whole buffer init with zeroes.
130 // SAFETY: we initialise via the const-fn path in init().
131 Self { 127 Self {
132 buf: [ 128 buf: [const { core::cell::UnsafeCell::new(0) }; 256],
133 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
134 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
135 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
136 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
137 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
138 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
139 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
140 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
141 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
142 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
143 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
144 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
145 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
146 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
147 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
148 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
149 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
150 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
151 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
152 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
153 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
154 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
155 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
156 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
157 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
158 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
159 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
160 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
161 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
162 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
163 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
164 NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL, NEW_CELL,
165 ],
166 head: AtomicU16::new(0), 129 head: AtomicU16::new(0),
167 tail: AtomicU16::new(0), 130 tail: AtomicU16::new(0),
168 } 131 }
@@ -297,7 +260,10 @@ struct IntFrame {
297unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { 260unsafe extern "C" fn isr_dispatch(frame: &IntFrame) {
298 unsafe { 261 unsafe {
299 match frame.vector { 262 match frame.vector {
300 32 => eoi(0), 263 32 => {
264 TICKS.fetch_add(1, Ordering::Relaxed);
265 eoi(0);
266 }
301 33 => { 267 33 => {
302 let byte: u8; 268 let byte: u8;
303 core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16, 269 core::arch::asm!("in al, dx", out("al") byte, in("dx") 0x60u16,
@@ -311,22 +277,6 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) {
311 } 277 }
312} 278}
313 279
314/// Timer IRQ (PIT channel 0 ~ 100 Hz).
315fn timer_irq() {
316 eoi(0);
317}
318
319/// Keyboard IRQ — read scancode from port 0x60 and push to ring buffer.
320fn keyboard_irq() {
321 let byte: u8;
322 unsafe {
323 asm!("in al, dx", out("al") byte, in("dx") 0x60,
324 options(nostack, preserves_flags));
325 }
326 KEYBUF.push(byte);
327 eoi(1);
328}
329
330// --------------------------------------------------------------------------- 280// ---------------------------------------------------------------------------
331// Public API 281// Public API
332// --------------------------------------------------------------------------- 282// ---------------------------------------------------------------------------
@@ -348,7 +298,7 @@ pub fn init() {
348 let cs = cs as u16; 298 let cs = cs as u16;
349 299
350 // Build the IDT. 300 // Build the IDT.
351 let idt_ptr = IDT.as_mut_ptr(); 301 let idt_ptr = core::ptr::addr_of_mut!(IDT[0]);
352 let spurious = isr_stub_spurious as *const () as u64; 302 let spurious = isr_stub_spurious as *const () as u64;
353 303
354 // Fill all 256 entries with the spurious handler. 304 // Fill all 256 entries with the spurious handler.
@@ -370,7 +320,7 @@ pub fn init() {
370 ]; 320 ];
371 321
372 // Load IDT. 322 // Load IDT.
373 asm!("lidt [{}]", in(reg) &IDTR, options(nostack, preserves_flags)); 323 asm!("lidt [{}]", in(reg) core::ptr::addr_of!(IDTR), options(nostack, preserves_flags));
374 324
375 // Remap PIC and mask everything. 325 // Remap PIC and mask everything.
376 remap_pic(); 326 remap_pic();
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs
index c80edab..54ff7be 100644
--- a/rsnix/src/memory.rs
+++ b/rsnix/src/memory.rs
@@ -289,7 +289,7 @@ pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> {
289 let buf_size = buf_size.div_ceil(desc_size) * desc_size; 289 let buf_size = buf_size.div_ceil(desc_size) * desc_size;
290 290
291 // If our static buffer is too small, fall back to pool (rare) 291 // If our static buffer is too small, fall back to pool (rare)
292 if buf_size > unsafe { MAP_WORK_BUF.len() } { 292 if buf_size > unsafe { (*core::ptr::addr_of!(MAP_WORK_BUF)).len() } {
293 return Err(EFI_BUFFER_TOO_SMALL); 293 return Err(EFI_BUFFER_TOO_SMALL);
294 } 294 }
295 295
@@ -337,7 +337,12 @@ pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> {
337} 337}
338 338
339/// Retry ExitBootServices with a fresh memory map. 339/// Retry ExitBootServices with a fresh memory map.
340pub fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize, Status> { 340///
341/// # Safety
342///
343/// `image_handle` must be the valid EFI image handle this kernel was
344/// started with (opaque firmware pointer; never dereferenced here).
345pub unsafe fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize, Status> {
341 let key = capture_memory_map(bs)?; 346 let key = capture_memory_map(bs)?;
342 let status = unsafe { (bs.exit_boot_services)(image_handle, key) }; 347 let status = unsafe { (bs.exit_boot_services)(image_handle, key) };
343 if status == EFI_SUCCESS { 348 if status == EFI_SUCCESS {
@@ -591,7 +596,7 @@ pub fn init_page_allocator() -> bool {
591 596
592 // The bitmap itself lives in the heap (one allocation, never freed). 597 // The bitmap itself lives in the heap (one allocation, never freed).
593 // `bytes` >= 1 and align 16 is a power of two, so this cannot fail. 598 // `bytes` >= 1 and align 16 is a power of two, so this cannot fail.
594 let layout = unsafe { Layout::from_size_align_unchecked(bytes, 16) }; 599 let layout = Layout::from_size_align_unchecked(bytes, 16);
595 let bm = ALLOCATOR.alloc(layout); 600 let bm = ALLOCATOR.alloc(layout);
596 if bm.is_null() { 601 if bm.is_null() {
597 return false; 602 return false;
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs
index 4747813..35848b6 100644
--- a/rsnix/src/ps2.rs
+++ b/rsnix/src/ps2.rs
@@ -263,8 +263,12 @@ pub fn try_poll_key() -> Option<KeyEvent> {
263 263
264/// Process a single scancode byte. Returns Some(KeyEvent) when a visible 264/// Process a single scancode byte. Returns Some(KeyEvent) when a visible
265/// event happens; returns None for modifier changes, releases, and prefixes. 265/// event happens; returns None for modifier changes, releases, and prefixes.
266fn kbd() -> &'static mut KbdState {
267 unsafe { &mut *core::ptr::addr_of_mut!(KBD) }
268}
269
266fn process_byte(byte: u8) -> Option<KeyEvent> { 270fn process_byte(byte: u8) -> Option<KeyEvent> {
267 let state = unsafe { &mut KBD }; 271 let state = kbd();
268 272
269 if byte == 0xE0 { 273 if byte == 0xE0 {
270 state.got_e0 = true; 274 state.got_e0 = true;
@@ -368,7 +372,7 @@ fn process_e0(byte: u8) -> Option<KeyEvent> {
368} 372}
369 373
370fn update_modifiers(scancode: u8, pressed: bool, e0: bool) { 374fn update_modifiers(scancode: u8, pressed: bool, e0: bool) {
371 let state = unsafe { &mut KBD }; 375 let state = kbd();
372 match (scancode, e0) { 376 match (scancode, e0) {
373 (0x2A, false) => set_mod(state, MOD_LSHIFT, pressed), 377 (0x2A, false) => set_mod(state, MOD_LSHIFT, pressed),
374 (0x36, false) => set_mod(state, MOD_RSHIFT, pressed), 378 (0x36, false) => set_mod(state, MOD_RSHIFT, pressed),
@@ -393,6 +397,6 @@ fn set_mod(state: &mut KbdState, bit: u8, pressed: bool) {
393 397
394/// Is either Ctrl key held? 398/// Is either Ctrl key held?
395fn ctrl_held() -> bool { 399fn ctrl_held() -> bool {
396 let state = unsafe { &mut KBD }; 400 let state = kbd();
397 state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0 401 state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0
398} 402}
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs
index b6b6cc8..81a7a24 100644
--- a/rsnix/src/tty.rs
+++ b/rsnix/src/tty.rs
@@ -20,6 +20,7 @@ use crate::{gop, serial};
20pub const LINE_BUF_SIZE: usize = 256; 20pub const LINE_BUF_SIZE: usize = 256;
21 21
22/// Result of one canonical line-input session. 22/// Result of one canonical line-input session.
23#[allow(clippy::large_enum_variant)] // stack buffer by design (no heap churn)
23pub enum LineInput { 24pub enum LineInput {
24 Line(strbuf::String<LINE_BUF_SIZE>), 25 Line(strbuf::String<LINE_BUF_SIZE>),
25 Interrupted, // ^C 26 Interrupted, // ^C
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 59378f3..2cfb0f3 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -146,8 +146,8 @@ fn cell(r: usize, c: usize) -> &'static mut Cell {
146 &mut cells()[idx(r, c)] 146 &mut cells()[idx(r, c)]
147} 147}
148 148
149fn rendered(r: usize, c: usize) -> &'static mut Cell { 149fn params() -> &'static mut [usize; 4] {
150 &mut rendered_cells()[idx(r, c)] 150 unsafe { &mut *core::ptr::addr_of_mut!(PARAMS) }
151} 151}
152 152
153fn put_cell(r: usize, c: usize, ch: u16) { 153fn put_cell(r: usize, c: usize, ch: u16) {
@@ -204,14 +204,14 @@ fn put_byte(b: u8) {
204 CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize); 204 CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize);
205 } 205 }
206 b';' => { 206 b';' => {
207 if NPARAMS < PARAMS.len() { 207 if NPARAMS < params().len() {
208 PARAMS[NPARAMS] = CUR; 208 PARAMS[NPARAMS] = CUR;
209 NPARAMS += 1; 209 NPARAMS += 1;
210 } 210 }
211 CUR = 0; 211 CUR = 0;
212 } 212 }
213 _ => { 213 _ => {
214 if NPARAMS < PARAMS.len() { 214 if NPARAMS < params().len() {
215 PARAMS[NPARAMS] = CUR; 215 PARAMS[NPARAMS] = CUR;
216 NPARAMS += 1; 216 NPARAMS += 1;
217 } 217 }
diff --git a/shell/Cargo.toml b/shell/Cargo.toml
index 89152e7..bc35524 100644
--- a/shell/Cargo.toml
+++ b/shell/Cargo.toml
@@ -3,6 +3,9 @@ name = "shell"
3version.workspace = true 3version.workspace = true
4edition.workspace = true 4edition.workspace = true
5 5
6[lints]
7workspace = true
8
6[dependencies] 9[dependencies]
7rsnix = { path = "../rsnix" } 10rsnix = { path = "../rsnix" }
8rsdosedit = { path = "../rsdosedit" } 11rsdosedit = { path = "../rsdosedit" }