summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rsnix/src/ahci.rs355
-rw-r--r--rsnix/src/fat.rs934
-rw-r--r--rsnix/src/fs.rs164
-rw-r--r--rsnix/src/gop.rs6
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/pci.rs33
-rw-r--r--rsnix/src/vt.rs4
7 files changed, 1490 insertions, 8 deletions
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs
new file mode 100644
index 0000000..83d0d27
--- /dev/null
+++ b/rsnix/src/ahci.rs
@@ -0,0 +1,355 @@
1// ---------------------------------------------------------------------------
2// AHCI — SATA host-controller driver (polling, DMA, no interrupts).
3//
4// Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1,
5// e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts port 0
6// and exposes sector-level reads/writes for the FAT32 filesystem.
7//
8// Commands are issued through command-slot 0 with one 4 KiB DMA buffer;
9// completion is polled via PxCI (no ISR, no MSI). Everything is
10// identity-mapped, so "physical" DMA addresses are plain kernel pointers.
11// The display is never involved, so this is a straightforward polling
12// loop like the rest of the single-threaded kernel.
13// ---------------------------------------------------------------------------
14
15use crate::pci;
16
17const SECTOR: usize = 512;
18
19// ABAR (HBA memory space) offsets.
20const GHC: usize = 0x04;
21const PI: usize = 0x0C;
22const PORT_BASE: usize = 0x100;
23const PORT_STRIDE: usize = 0x80;
24
25// Port register offsets (relative to the port's base).
26const P_CLB: usize = 0x00;
27const P_CLBU: usize = 0x04;
28const P_FB: usize = 0x08;
29const P_FBU: usize = 0x0C;
30const P_IS: usize = 0x10;
31const P_CMD: usize = 0x18;
32const P_TFD: usize = 0x20;
33const P_SSTS: usize = 0x28;
34const P_SCTL: usize = 0x2C;
35const P_CI: usize = 0x38;
36
37const CMD_ST: u32 = 1 << 0; // start
38const CMD_SUD: u32 = 1 << 1; // spin-up device
39const CMD_FRE: u32 = 1 << 4; // FIS receive enable
40const CMD_DMAE: u32 = 1 << 23; // DMA enable (QEMU/ICH9) — required or DMA commands silently transfer nothing
41const CMD_FR: u32 = 1 << 14; // FIS receive running
42const CMD_CR: u32 = 1 << 15; // command list running
43
44const ATA_IDENTIFY: u8 = 0xEC;
45const ATA_READ_DMA_EXT: u8 = 0x25;
46const ATA_WRITE_DMA_EXT: u8 = 0x35;
47
48/// One attached disk: register map + DMA structures.
49struct Disk {
50 port: usize, // port register base = ABAR + 0x100
51 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned
52 fis: usize, // FIS receive area, 256 B aligned
53 ct: usize, // command table, 256 B, 128 B aligned
54 buf: usize, // 4 KiB DMA data buffer (page aligned)
55 sectors: u64, // total sectors (LBA48 from IDENTIFY)
56}
57
58static mut DISK: Option<Disk> = None;
59
60fn reg(a: usize) -> u32 {
61 unsafe { core::ptr::read_volatile(a as *const u32) }
62}
63
64fn wreg(a: usize, v: u32) {
65 unsafe { core::ptr::write_volatile(a as *mut u32, v) }
66}
67
68/// Scan bus 0 for the AHCI controller (SATA, AHCI prog-if).
69fn find_ahci() -> Option<(u8, u8)> {
70 for dev in 0..32u8 {
71 for func in 0..8u8 {
72 let id = pci::config_read(0, dev, func, 0);
73 if id == 0xFFFF_FFFF {
74 if func == 0 {
75 break;
76 }
77 continue;
78 }
79 let cc = pci::config_read(0, dev, func, 0x08);
80 let class = (cc >> 24) as u8;
81 let subclass = (cc >> 16) as u8;
82 let prog_if = (cc >> 8) as u8;
83 if class == 0x01 && subclass == 0x06 && prog_if == 0x01 {
84 return Some((dev, func));
85 }
86 }
87 }
88 None
89}
90
91/// Probe, map and start the first AHCI port. Returns true if a disk is
92/// ready for sector I/O. Called once at boot before the filesystem init.
93pub fn init() -> bool {
94 let Some((dev, func)) = find_ahci() else {
95 return false;
96 };
97 // Enable memory space + bus mastering so MMIO and DMA work.
98 let cmd_reg = pci::config_read(0, dev, func, 0x04);
99 pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7);
100
101 let Some(abar) = pci::bar64(0, dev, func, 5) else {
102 return false;
103 };
104 crate::serial_println!(
105 "ahci: controller at {:02x}:{:02x}.{} abar={:#x}",
106 0,
107 dev,
108 func,
109 abar
110 );
111
112 // AHCI enable (GHC.AE) before touching port registers.
113 wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31));
114
115 // Pick the first implemented port.
116 let pi = reg(abar as usize + PI);
117 let mut port_idx = 0u32;
118 while port_idx < 32 && (pi >> port_idx) & 1 == 0 {
119 port_idx += 1;
120 }
121 if port_idx >= 32 {
122 crate::serial_println!("ahci: no ports implemented (pi={:#x})", pi);
123 return false;
124 }
125 let port = abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE;
126
127 // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd
128 // table, page 1 = data buffer.
129 let Some(mem) = crate::memory::alloc_pages(2) else {
130 crate::serial_println!("ahci: cannot allocate DMA pages");
131 return false;
132 };
133 let clb = mem;
134 let fis = mem + 1024;
135 let ct = mem + 1280;
136 let buf = mem + 4096;
137
138 // Stop the port if firmware left it running.
139 let mut cmd = reg(port + P_CMD);
140 if cmd & CMD_ST != 0 {
141 wreg(port + P_CMD, cmd & !CMD_ST);
142 let mut i = 0;
143 while reg(port + P_CMD) & CMD_CR != 0 && i < 1_000_000 {
144 i += 1;
145 }
146 cmd = reg(port + P_CMD);
147 }
148 if cmd & CMD_FRE != 0 {
149 wreg(port + P_CMD, cmd & !CMD_FRE);
150 let mut i = 0;
151 while reg(port + P_CMD) & CMD_FR != 0 && i < 1_000_000 {
152 i += 1;
153 }
154 }
155 unsafe {
156 core::ptr::write_bytes(clb as *mut u8, 0, 1024);
157 core::ptr::write_bytes(fis as *mut u8, 0, 256);
158 core::ptr::write_bytes(ct as *mut u8, 0, 256);
159 }
160
161 wreg(port + P_CLB, clb as u32);
162 wreg(port + P_CLBU, 0);
163 wreg(port + P_FB, fis as u32);
164 wreg(port + P_FBU, 0);
165
166 // Allow device detection (clear PxSCTL.DET/IPM), then wait for the
167 // link to come up (PxSSTS.DET == 3, communication established).
168 wreg(port + P_SCTL, 0);
169 let mut link_ok = false;
170 for _ in 0..10_000_000 {
171 if reg(port + P_SSTS) & 0xF == 3 {
172 link_ok = true;
173 break;
174 }
175 }
176 if !link_ok {
177 crate::serial_println!(
178 "ahci: no device on port {} (ssts={:#x})",
179 port_idx,
180 reg(port + P_SSTS)
181 );
182 return false;
183 }
184
185 // Start the command engine (DMAE required for DMA commands).
186 wreg(port + P_CMD, CMD_FRE | CMD_SUD | CMD_DMAE);
187 wreg(port + P_CMD, CMD_ST | CMD_FRE | CMD_SUD | CMD_DMAE);
188
189 let mut d = Disk {
190 port,
191 clb,
192 fis,
193 ct,
194 buf,
195 sectors: 0,
196 };
197 // IDENTIFY DEVICE as a sanity check + to learn the capacity.
198 if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() {
199 crate::serial_println!("ahci: IDENTIFY failed");
200 return false;
201 }
202 let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) };
203 d.sectors = (id[100] as u64)
204 | ((id[101] as u64) << 16)
205 | ((id[102] as u64) << 32)
206 | ((id[103] as u64) << 48);
207 if d.sectors == 0 {
208 d.sectors = id[60] as u64; // LBA28 fallback
209 }
210 let mut model = [0u8; 40];
211 for i in 0..20 {
212 model[i * 2] = (id[27 + i] >> 8) as u8;
213 model[i * 2 + 1] = id[27 + i] as u8;
214 }
215 let model = core::str::from_utf8(&model).unwrap_or("?").trim();
216 crate::serial_println!(
217 "ahci: port {} up — {} sectors (~{} MB) \"{}\"",
218 port_idx,
219 d.sectors,
220 d.sectors / 2048,
221 model
222 );
223 unsafe {
224 DISK = Some(d);
225 }
226 true
227}
228
229/// Issue one command through slot 0 and poll for completion.
230/// `count` is sectors (1-8); data flows through the disk's DMA buffer.
231fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> {
232 if count == 0 || count > 8 {
233 return Err(());
234 }
235 // Wait for slot 0 to be free.
236 let mut i = 0;
237 while reg(d.port + P_CI) & 1 != 0 && i < 10_000_000 {
238 i += 1;
239 }
240 if reg(d.port + P_CI) & 1 != 0 {
241 return Err(());
242 }
243
244 unsafe {
245 core::ptr::write_bytes(d.ct as *mut u8, 0, 256);
246 }
247
248 // Host-to-device FIS (20 bytes) at the command table start.
249 let fis = d.ct as *mut u8;
250 unsafe {
251 *fis = 0x27; // FIS type H2D
252 *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4
253 *(fis.add(2)) = cmd;
254 // FIS field layout (SATA spec; verified against QEMU/Linux):
255 // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48],
256 // 11 features high, 12-13 sector count.
257 *(fis.add(4)) = lba as u8;
258 *(fis.add(5)) = (lba >> 8) as u8;
259 *(fis.add(6)) = (lba >> 16) as u8;
260 *(fis.add(7)) = 0x40 | (((lba >> 24) & 0x0F) as u8);
261 *(fis.add(8)) = (lba >> 24) as u8;
262 *(fis.add(9)) = (lba >> 32) as u8;
263 *(fis.add(10)) = (lba >> 40) as u8;
264 *(fis.add(11)) = 0; // features high
265 *(fis.add(12)) = count as u8;
266 *(fis.add(13)) = (count >> 8) as u8;
267 *(fis.add(14)) = 0; // ICC
268 *(fis.add(15)) = 0; // control
269 }
270
271 // PRD entry at the command table +0x80.
272 let prd = (d.ct + 0x80) as *mut u32;
273 unsafe {
274 *prd = d.buf as u32;
275 *prd.add(1) = 0;
276 *prd.add(2) = 0;
277 *prd.add(3) = (count * SECTOR as u32) - 1; // DBC, no I bit
278 }
279
280 // Command header 0. Layout per Linux libahci / QEMU AHCICmdHdr:
281 // DW0 bits 4:0 = CFL (FIS length in dwords), bit 6 = W,
282 // bits 31:16 = PRDTL; DW1 status; DW2/3 = command table address.
283 let hdr = d.clb as *mut u32;
284 unsafe {
285 *hdr = 5 | (1 << 16) | if write { 1 << 6 } else { 0 };
286 *hdr.add(1) = 0;
287 *hdr.add(2) = d.ct as u32;
288 *hdr.add(3) = 0;
289 }
290
291 // Issue and poll.
292 wreg(d.port + P_CI, 1);
293 let mut i = 0;
294 while reg(d.port + P_CI) & 1 != 0 && i < 100_000_000 {
295 i += 1;
296 }
297 if reg(d.port + P_CI) & 1 != 0 {
298 crate::serial_println!(
299 "ahci: command {:#x} lba={} cnt={} timed out (tfd={:#x} ci={:#x} cmd={:#x} prdbc={:#x})",
300 cmd,
301 lba,
302 count,
303 reg(d.port + P_TFD),
304 reg(d.port + P_CI),
305 reg(d.port + P_CMD),
306 unsafe { *(d.clb as *const u32).add(1) }
307 );
308 return Err(());
309 }
310 // Error? Clear any port error bits either way.
311 if reg(d.port + P_TFD) & 1 != 0 {
312 wreg(d.port + 0x30, 0xFFFF_FFFF); // PxSERR: clear
313 crate::serial_println!(
314 "ahci: command {:#x} error (tfd={:#x})",
315 cmd,
316 reg(d.port + P_TFD)
317 );
318 return Err(());
319 }
320 let is = reg(d.port + P_IS);
321 if is != 0 {
322 wreg(d.port + P_IS, is); // write-1-to-clear
323 }
324 Ok(())
325}
326
327/// DMA `count` sectors starting at `lba` into the internal buffer.
328pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> {
329 let d = unsafe { DISK.as_ref() }.ok_or(())?;
330 if lba + count as u64 > d.sectors {
331 return Err(());
332 }
333 issue(d, ATA_READ_DMA_EXT, lba, count, false)
334}
335
336/// DMA `count` sectors starting at `lba` out of the internal buffer.
337pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> {
338 let d = unsafe { DISK.as_ref() }.ok_or(())?;
339 if lba + count as u64 > d.sectors {
340 return Err(());
341 }
342 issue(d, ATA_WRITE_DMA_EXT, lba, count, true)
343}
344
345/// Pointer to the internal 4 KiB DMA buffer.
346pub fn buf() -> *mut u8 {
347 unsafe { DISK.as_ref() }
348 .map(|d| d.buf as *mut u8)
349 .unwrap_or(core::ptr::null_mut())
350}
351
352/// Total sector count of the attached disk.
353pub fn sector_count() -> u64 {
354 unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0)
355}
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
new file mode 100644
index 0000000..70fa688
--- /dev/null
+++ b/rsnix/src/fat.rs
@@ -0,0 +1,934 @@
1// ---------------------------------------------------------------------------
2// FAT32 — read/write a FAT32 volume over the AHCI sector interface.
3//
4// Mounts the volume's boot sector (BPB), caches the whole FAT in RAM,
5// follows cluster chains, and exposes file/directory operations used by
6// the VFS's Disk nodes: list, find, read, write, truncate, create (files
7// and dirs), remove. Long filenames are read back and written for names
8// that don't fit 8.3; short names are generated the standard way
9// (uppercased, 8+3, "~N" truncation). All timestamps are zero — this is
10// a toy kernel, not a backup tool.
11//
12// Single-threaded like the rest of the kernel; no caching beyond the FAT
13// itself, so every op re-reads directory sectors (cheap: the disk is
14// polled DMA at µs scale in QEMU).
15// ---------------------------------------------------------------------------
16
17extern crate alloc;
18
19use crate::ahci;
20use alloc::string::String;
21use alloc::vec;
22use alloc::vec::Vec;
23
24const SECTOR: usize = 512;
25const EOC_MASK: u32 = 0x0FFF_FFFF;
26const EOC: u32 = 0x0FFF_FFFF;
27
28/// One directory-listed entry (or a resolved file).
29pub struct Entry {
30 pub name: String,
31 pub is_dir: bool,
32 pub size: u64,
33 pub cluster: u32,
34}
35
36struct Volume {
37 sectors_per_cluster: u8,
38 reserved: u16,
39 fat_count: u8,
40 fat_size: u32, // sectors per FAT
41 root_cluster: u32,
42 first_data: u32, // LBA of cluster 2
43 cluster_count: u32,
44 fsinfo: Option<u32>, // sector number of the FSInfo structure
45 fat: Vec<u8>, // FAT 1 cached in RAM
46}
47
48static mut VOL: Option<Volume> = None;
49
50pub fn is_mounted() -> bool {
51 unsafe { VOL.is_some() }
52}
53
54pub fn root_cluster() -> u32 {
55 unsafe { VOL.as_ref() }.map(|v| v.root_cluster).unwrap_or(0)
56}
57
58// --- sector I/O -----------------------------------------------------------
59
60fn read_sector(lba: u64, out: &mut [u8; SECTOR]) -> Result<(), ()> {
61 ahci::read_sectors(lba, 1)?;
62 unsafe { core::ptr::copy(ahci::buf(), out.as_mut_ptr(), SECTOR) };
63 Ok(())
64}
65
66fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> {
67 unsafe { core::ptr::copy(data.as_ptr(), ahci::buf(), SECTOR) };
68 ahci::write_sectors(lba, 1)
69}
70
71// --- cluster math ----------------------------------------------------------
72
73fn v() -> &'static mut Volume {
74 unsafe { VOL.as_mut() }.unwrap()
75}
76
77fn spc() -> usize {
78 v().sectors_per_cluster as usize
79}
80
81fn cluster_size() -> usize {
82 spc() * SECTOR
83}
84
85fn cluster_lba(c: u32) -> u64 {
86 v().first_data as u64 + ((c as u64 - 2) * spc() as u64)
87}
88
89fn fat_off(c: u32) -> usize {
90 c as usize * 4
91}
92
93fn read_fat_entry(c: u32) -> u32 {
94 let off = fat_off(c);
95 u32::from_le_bytes([
96 v().fat[off],
97 v().fat[off + 1],
98 v().fat[off + 2],
99 v().fat[off + 3],
100 ]) & EOC_MASK
101}
102
103/// Update a FAT entry in the cache and write the affected sector back to
104/// both FAT copies.
105fn write_fat_entry(c: u32, val: u32) -> Result<(), ()> {
106 let off = fat_off(c);
107 v().fat[off..off + 4].copy_from_slice(&(val & EOC_MASK).to_le_bytes());
108 let sector_idx = off / SECTOR;
109 let mut buf = [0u8; SECTOR];
110 buf.copy_from_slice(&v().fat[sector_idx * SECTOR..(sector_idx + 1) * SECTOR]);
111 let lba1 = v().reserved as u64 + sector_idx as u64;
112 let lba2 = lba1 + v().fat_size as u64;
113 write_sector(lba1, &buf)?;
114 write_sector(lba2, &buf)
115}
116
117fn is_eoc(c: u32) -> bool {
118 c >= 0x0FFF_FFF8
119}
120
121fn next_cluster(c: u32) -> Result<u32, ()> {
122 let n = read_fat_entry(c);
123 if is_eoc(n) {
124 Err(())
125 } else {
126 Ok(n)
127 }
128}
129
130/// Allocate a fresh cluster: mark it EOC in the FAT and zero its data.
131fn alloc_cluster() -> Result<u32, ()> {
132 let total = v().cluster_count + 2;
133 for c in 2..total {
134 if read_fat_entry(c) == 0 {
135 write_fat_entry(c, EOC)?;
136 let zero = vec![0u8; cluster_size()];
137 write_cluster(c, &zero)?;
138 fsinfo_adjust(-1);
139 return Ok(c);
140 }
141 }
142 crate::serial_println!("fat: disk full");
143 Err(())
144}
145
146fn free_chain(mut c: u32) {
147 let mut freed = 0u64;
148 let mut guard = 0;
149 while !is_eoc(read_fat_entry(c)) && guard < 1_000_000 {
150 let nxt = read_fat_entry(c);
151 let _ = write_fat_entry(c, 0);
152 c = nxt;
153 freed += 1;
154 guard += 1;
155 }
156 let _ = write_fat_entry(c, 0);
157 freed += 1;
158 if freed > 0 {
159 fsinfo_adjust(freed as i64);
160 }
161}
162
163/// Keep FSInfo.FSFreeCount in sync with cluster allocations/frees so
164/// fsck and other tools see an accurate free count. Falls back to a full
165/// recount when the stored value is the "unknown" marker.
166fn fsinfo_adjust(delta: i64) {
167 let Some(s) = v().fsinfo else {
168 return;
169 };
170 let mut b = [0u8; SECTOR];
171 if read_sector(s as u64, &mut b).is_err() {
172 return;
173 }
174 if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" {
175 return; // not a valid FSInfo structure
176 }
177 let mut n = u32::from_le_bytes([b[488], b[489], b[490], b[491]]);
178 if n == 0xFFFF_FFFF {
179 n = free_clusters();
180 }
181 let n = (n as i64 + delta).max(0) as u32;
182 b[488..492].copy_from_slice(&n.to_le_bytes());
183 let _ = write_sector(s as u64, &b);
184}
185
186/// Rewrite FSInfo.FSFreeCount with the true free-cluster count.
187fn fsinfo_recount() {
188 let Some(s) = v().fsinfo else {
189 return;
190 };
191 let mut b = [0u8; SECTOR];
192 if read_sector(s as u64, &mut b).is_err() {
193 return;
194 }
195 if &b[0..4] != b"RRaA" || &b[484..488] != b"rrAa" {
196 return;
197 }
198 let n = free_clusters();
199 b[488..492].copy_from_slice(&n.to_le_bytes());
200 let _ = write_sector(s as u64, &b);
201}
202
203/// Count free clusters by scanning the cached FAT.
204fn free_clusters() -> u32 {
205 let mut n = 0u32;
206 for c in 2..v().cluster_count + 2 {
207 if read_fat_entry(c) == 0 {
208 n += 1;
209 }
210 }
211 n
212}
213
214fn read_cluster(c: u32, buf: &mut [u8]) -> Result<(), ()> {
215 if spc() <= 8 {
216 ahci::read_sectors(cluster_lba(c), spc() as u32)?;
217 unsafe { core::ptr::copy(ahci::buf(), buf.as_mut_ptr(), cluster_size()) };
218 } else {
219 for s in 0..spc() {
220 ahci::read_sectors(cluster_lba(c) + s as u64, 1)?;
221 unsafe {
222 core::ptr::copy(ahci::buf(), buf.as_mut_ptr().add(s * SECTOR), SECTOR);
223 }
224 }
225 }
226 Ok(())
227}
228
229fn write_cluster(c: u32, buf: &[u8]) -> Result<(), ()> {
230 if spc() <= 8 {
231 unsafe { core::ptr::copy(buf.as_ptr(), ahci::buf(), cluster_size()) };
232 ahci::write_sectors(cluster_lba(c), spc() as u32)
233 } else {
234 for s in 0..spc() {
235 unsafe {
236 core::ptr::copy(buf.as_ptr().add(s * SECTOR), ahci::buf(), SECTOR);
237 }
238 ahci::write_sectors(cluster_lba(c) + s as u64, 1)?;
239 }
240 Ok(())
241 }
242}
243
244// --- directory scanning ----------------------------------------------------
245
246/// Checksum of an SFN's 11 bytes; used to bind LFN entries to their SFN.
247fn lfn_checksum(sfn: &[u8; 11]) -> u8 {
248 let mut s: u8 = 0;
249 for &b in sfn {
250 s = s.rotate_right(1).wrapping_add(b);
251 }
252 s
253}
254
255fn sfn_decode(e: &[u8]) -> String {
256 let nts = e[12];
257 let mut s = String::new();
258 for &b in &e[0..8] {
259 if b == b' ' || b == 0 {
260 break;
261 }
262 let c = if nts & 0x08 != 0 {
263 b.to_ascii_lowercase()
264 } else {
265 b
266 };
267 s.push(c as char);
268 }
269 let mut ext = String::new();
270 for &b in &e[8..11] {
271 if b == b' ' || b == 0 {
272 break;
273 }
274 let c = if nts & 0x10 != 0 {
275 b.to_ascii_lowercase()
276 } else {
277 b
278 };
279 ext.push(c as char);
280 }
281 if !ext.is_empty() {
282 s.push('.');
283 s.push_str(&ext);
284 }
285 if s.is_empty() {
286 s.push('?');
287 }
288 s
289}
290
291fn utf16_to_string(v: &[u16]) -> String {
292 let mut s = String::new();
293 for &c in v {
294 if c == 0 {
295 break;
296 }
297 if let Some(ch) = char::from_u32(c as u32) {
298 s.push(ch);
299 }
300 }
301 s
302}
303
304struct DirItem {
305 name: String,
306 attr: u8,
307 cluster: u32,
308 size: u32,
309 sector: u64, // LBA of the entry's sector (for in-place updates)
310 offset: usize,
311}
312
313/// Scan a directory's cluster chain, reassembling long filenames.
314fn scan_dir(dir_cluster: u32) -> Result<Vec<DirItem>, ()> {
315 let cs = cluster_size();
316 let mut buf = vec![0u8; cs];
317 let mut items = Vec::new();
318 let mut lfn: Vec<u16> = Vec::new();
319 let mut lfn_sum = 0u8;
320 let mut cur: Option<u32> = Some(dir_cluster);
321 let mut guard = 0;
322 while let Some(c) = cur {
323 guard += 1;
324 if guard > 1_000_000 {
325 return Err(()); // loop protection on a corrupt chain
326 }
327 read_cluster(c, &mut buf)?;
328 let mut done = false;
329 for (i, e) in buf.chunks_exact(32).enumerate() {
330 match e[0] {
331 0x00 => {
332 done = true;
333 break;
334 }
335 0xE5 => lfn.clear(), // deleted entry; drop any partial LFN
336 _ => {
337 let attr = e[11];
338 if attr == 0x0F {
339 // LFN entry: 13 UTF-16 chars (bytes 1-10, 14-25, 28-31).
340 for k in 0..13 {
341 let pos = if k < 5 {
342 1 + k * 2
343 } else if k < 11 {
344 14 + (k - 5) * 2
345 } else {
346 28 + (k - 11) * 2
347 };
348 let ch = u16::from_le_bytes([e[pos], e[pos + 1]]);
349 if ch == 0x0000 {
350 break;
351 }
352 if ch != 0xFFFF {
353 lfn.push(ch);
354 }
355 }
356 lfn_sum = e[13];
357 } else {
358 // Short-name entry: bind any collected LFN.
359 let mut sfn = [0u8; 11];
360 sfn.copy_from_slice(&e[0..11]);
361 let name = if !lfn.is_empty() && lfn_checksum(&sfn) == lfn_sum {
362 utf16_to_string(&lfn)
363 } else {
364 sfn_decode(e)
365 };
366 let cluster = u16::from_le_bytes([e[26], e[27]]) as u32
367 | ((u16::from_le_bytes([e[20], e[21]]) as u32) << 16);
368 items.push(DirItem {
369 name,
370 attr,
371 cluster,
372 size: u32::from_le_bytes([e[28], e[29], e[30], e[31]]),
373 sector: cluster_lba(c) + (i * 32 / SECTOR) as u64,
374 offset: (i * 32) % SECTOR,
375 });
376 lfn.clear();
377 }
378 }
379 }
380 }
381 if done {
382 break;
383 }
384 cur = next_cluster(c).ok();
385 }
386 Ok(items)
387}
388
389/// Public listing: no `.` / `..`.
390pub fn list_dir(dir_cluster: u32) -> Result<Vec<Entry>, ()> {
391 Ok(scan_dir(dir_cluster)?
392 .into_iter()
393 .filter(|i| i.name != "." && i.name != "..")
394 .map(|i| Entry {
395 name: i.name,
396 is_dir: i.attr & 0x10 != 0,
397 size: i.size as u64,
398 cluster: i.cluster,
399 })
400 .collect())
401}
402
403/// Find an entry by name (case-insensitive), returning the scan record
404/// (which knows the entry's sector for in-place updates).
405fn find_item(parent_dir_cluster: u32, name: &str) -> Result<DirItem, ()> {
406 for i in scan_dir(parent_dir_cluster)? {
407 if i.name.eq_ignore_ascii_case(name) {
408 return Ok(i);
409 }
410 }
411 Err(())
412}
413
414/// Find an entry by name (case-insensitive).
415pub fn find(parent_dir_cluster: u32, name: &str) -> Result<Entry, ()> {
416 find_item(parent_dir_cluster, name).map(|i| Entry {
417 name: i.name,
418 is_dir: i.attr & 0x10 != 0,
419 size: i.size as u64,
420 cluster: i.cluster,
421 })
422}
423
424// --- file data --------------------------------------------------------------
425
426pub fn read_file(
427 parent_dir_cluster: u32,
428 name: &str,
429 buf: &mut [u8],
430 pos: u64,
431) -> Result<usize, ()> {
432 if buf.is_empty() {
433 return Ok(0);
434 }
435 let it = find(parent_dir_cluster, name)?;
436 if it.is_dir || it.cluster == 0 {
437 return Err(());
438 }
439 if pos >= it.size {
440 return Ok(0);
441 }
442 let cs = cluster_size() as u64;
443 let mut cluster = it.cluster;
444 let mut skip = pos;
445 while skip >= cs {
446 cluster = next_cluster(cluster)?;
447 skip -= cs;
448 }
449 let mut cbuf = vec![0u8; cluster_size()];
450 let mut remaining = buf.len() as u64;
451 if remaining > it.size - pos {
452 remaining = it.size - pos;
453 }
454 let mut out = 0usize;
455 while remaining > 0 {
456 read_cluster(cluster, &mut cbuf)?;
457 let from = skip as usize;
458 let n = ((cs as usize) - from).min(remaining as usize);
459 buf[out..out + n].copy_from_slice(&cbuf[from..from + n]);
460 out += n;
461 remaining -= n as u64;
462 if remaining > 0 {
463 cluster = next_cluster(cluster)?;
464 }
465 skip = 0;
466 }
467 Ok(out)
468}
469
470pub fn write_file(parent_dir_cluster: u32, name: &str, buf: &[u8], pos: u64) -> Result<usize, ()> {
471 if buf.is_empty() {
472 return Ok(0);
473 }
474 let it = find_item(parent_dir_cluster, name)?;
475 if it.attr & 0x10 != 0 {
476 return Err(());
477 }
478 let cs = cluster_size() as u64;
479 let end = pos + buf.len() as u64;
480
481 // Ensure a first cluster exists.
482 let mut cluster = it.cluster;
483 if cluster == 0 {
484 cluster = alloc_cluster()?;
485 update_entry(&it, Some(cluster), None)?;
486 }
487 // Walk to the cluster containing `pos`, extending the chain as needed.
488 let mut skip = pos;
489 let mut cur = cluster;
490 while skip >= cs {
491 match next_cluster(cur) {
492 Ok(n) => cur = n,
493 Err(_) => {
494 let n = alloc_cluster()?;
495 write_fat_entry(cur, n)?;
496 cur = n;
497 }
498 }
499 skip -= cs;
500 }
501 // Write the data.
502 let mut cbuf = vec![0u8; cluster_size()];
503 let mut remaining = buf.len() as u64;
504 let mut in_off = 0usize;
505 while remaining > 0 {
506 read_cluster(cur, &mut cbuf)?;
507 let from = skip as usize;
508 let n = ((cs as usize) - from).min(remaining as usize);
509 cbuf[from..from + n].copy_from_slice(&buf[in_off..in_off + n]);
510 write_cluster(cur, &cbuf)?;
511 in_off += n;
512 remaining -= n as u64;
513 if remaining > 0 {
514 // Make sure the chain continues past this cluster.
515 match next_cluster(cur) {
516 Ok(n) => cur = n,
517 Err(_) => {
518 let n = alloc_cluster()?;
519 write_fat_entry(cur, n)?;
520 cur = n;
521 }
522 }
523 }
524 skip = 0;
525 }
526 if end > it.size as u64 {
527 update_entry(&it, None, Some(end as u32))?;
528 }
529 Ok(buf.len())
530}
531
532pub fn set_len(parent_dir_cluster: u32, name: &str, len: u64) -> Result<(), ()> {
533 let it = find_item(parent_dir_cluster, name)?;
534 if it.attr & 0x10 != 0 {
535 return Err(());
536 }
537 let cs = cluster_size() as u64;
538 let needed = if len == 0 {
539 0
540 } else {
541 ((len - 1) / cs + 1) as usize
542 };
543 let have = chain_len(it.cluster);
544 if it.cluster == 0 {
545 if needed == 0 {
546 update_entry(&it, None, Some(0))?;
547 } else {
548 let first = alloc_cluster()?;
549 if needed > 1 {
550 extend_chain(first, needed - 1)?;
551 }
552 update_entry(&it, Some(first), Some(len as u32))?;
553 }
554 } else if needed < have {
555 truncate_chain(it.cluster, needed);
556 update_entry(
557 &it,
558 if needed == 0 { Some(0) } else { None },
559 Some(len as u32),
560 )?;
561 } else if needed > have {
562 extend_chain(it.cluster, needed - have)?;
563 update_entry(&it, None, Some(len as u32))?;
564 } else {
565 update_entry(&it, None, Some(len as u32))?;
566 }
567 Ok(())
568}
569
570fn chain_len(mut c: u32) -> usize {
571 let mut n = 0usize;
572 let mut guard = 0;
573 while c != 0 && !is_eoc(read_fat_entry(c)) && guard < 1_000_000 {
574 c = read_fat_entry(c);
575 n += 1;
576 guard += 1;
577 }
578 if c != 0 {
579 n += 1;
580 }
581 n
582}
583
584fn truncate_chain(mut c: u32, keep: usize) {
585 if keep == 0 {
586 free_chain(c);
587 return;
588 }
589 let mut idx = 0usize;
590 let mut guard = 0;
591 while idx + 1 < keep {
592 let n = read_fat_entry(c);
593 if is_eoc(n) {
594 return;
595 }
596 c = n;
597 idx += 1;
598 guard += 1;
599 if guard > 1_000_000 {
600 return;
601 }
602 }
603 let nxt = read_fat_entry(c);
604 if !is_eoc(nxt) {
605 free_chain(nxt);
606 let _ = write_fat_entry(c, EOC);
607 }
608}
609
610fn extend_chain(c: u32, add: usize) -> Result<(), ()> {
611 let mut last = c;
612 let mut guard = 0;
613 loop {
614 let n = read_fat_entry(last);
615 if is_eoc(n) {
616 break;
617 }
618 last = n;
619 guard += 1;
620 if guard > 1_000_000 {
621 return Err(());
622 }
623 }
624 for _ in 0..add {
625 let n = alloc_cluster()?;
626 write_fat_entry(last, n)?;
627 last = n;
628 }
629 Ok(())
630}
631
632/// Rewrite one field of a directory entry in place (its sector was cached
633/// at scan time; the parent dir chain never moves while we hold it).
634fn update_entry(it: &DirItem, cluster: Option<u32>, size: Option<u32>) -> Result<(), ()> {
635 let mut sec = [0u8; SECTOR];
636 read_sector(it.sector, &mut sec)?;
637 let e = &mut sec[it.offset..it.offset + 32];
638 if let Some(c) = cluster {
639 e[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes()));
640 e[26..28].copy_from_slice(&((c as u16).to_le_bytes()));
641 }
642 if let Some(s) = size {
643 e[28..32].copy_from_slice(&s.to_le_bytes());
644 }
645 write_sector(it.sector, &sec)
646}
647
648// --- creation / removal ------------------------------------------------------
649
650/// Uppercased 8.3 short name for `name`; `needs_lfn` if the original name
651/// can't round-trip through the short name (too long or lowercase).
652fn to_8_3(name: &str) -> Option<([u8; 11], bool)> {
653 if name.is_empty() || name.contains('/') || name == "." || name == ".." {
654 return None;
655 }
656 let upper = name.to_ascii_uppercase();
657 let (n, e) = match upper.rfind('.') {
658 Some(i) => (&upper[..i], Some(&upper[i + 1..])),
659 None => (upper.as_str(), None),
660 };
661 if n.is_empty() {
662 return None;
663 }
664 let mut sfn = [b' '; 11];
665 let nb = n.as_bytes();
666 for i in 0..8 {
667 if i < nb.len() {
668 sfn[i] = nb[i];
669 }
670 }
671 if let Some(ext) = e {
672 let eb = ext.as_bytes();
673 for i in 0..3 {
674 if i < eb.len() {
675 sfn[8 + i] = eb[i];
676 }
677 }
678 }
679 let needs = nb.len() > 8 || e.map(|x| x.len() > 3).unwrap_or(false) || name != upper;
680 Some((sfn, needs))
681}
682
683/// Write the LFN entries (if any) + SFN entry for `name` into the parent
684/// directory, extending its chain with a fresh zeroed cluster if needed.
685fn add_dir_entry(parent: u32, name: &str, is_dir: bool, cluster: u32) -> Result<(), ()> {
686 let (sfn, needs_lfn) = to_8_3(name).ok_or(())?;
687 let lfn_chars: Vec<u16> = name.encode_utf16().collect();
688 let groups = if needs_lfn {
689 lfn_chars.len().div_ceil(13)
690 } else {
691 0
692 };
693 let needed = groups + 1;
694 let cs = cluster_size();
695 let mut buf = vec![0u8; cs];
696 let mut cur: Option<u32> = Some(parent);
697 let mut guard = 0;
698 loop {
699 guard += 1;
700 if guard > 1_000_000 {
701 return Err(());
702 }
703 let c = cur.ok_or(())?;
704 read_cluster(c, &mut buf)?;
705 let slots = cs / 32;
706 let mut run = 0usize;
707 let mut start = 0usize;
708 for i in 0..slots {
709 if buf[i * 32] == 0x00 || buf[i * 32] == 0xE5 {
710 run += 1;
711 if run == 1 {
712 start = i;
713 }
714 if run >= needed {
715 break;
716 }
717 } else {
718 run = 0;
719 }
720 }
721 if run >= needed {
722 if groups > 0 {
723 for g in 0..groups {
724 let seq = (groups - g) as u8 | if g == groups - 1 { 0x40 } else { 0 };
725 let e = &mut buf[(start + g) * 32..(start + g) * 32 + 32];
726 e.fill(0);
727 e[0] = seq;
728 e[11] = 0x0F;
729 e[13] = lfn_checksum(&sfn);
730 for k in 0..13 {
731 let ci = g * 13 + k;
732 let ch: u16 = if ci < lfn_chars.len() {
733 lfn_chars[ci]
734 } else if ci == lfn_chars.len() {
735 0
736 } else {
737 0xFFFF
738 };
739 let pos = if k < 5 {
740 1 + k * 2
741 } else if k < 11 {
742 14 + (k - 5) * 2
743 } else {
744 28 + (k - 11) * 2
745 };
746 e[pos] = (ch & 0xFF) as u8;
747 e[pos + 1] = (ch >> 8) as u8;
748 }
749 }
750 }
751 let e = &mut buf[(start + groups) * 32..(start + groups) * 32 + 32];
752 e.fill(0);
753 e[0..11].copy_from_slice(&sfn);
754 e[11] = if is_dir { 0x10 } else { 0x20 };
755 e[20..22].copy_from_slice(&(((cluster >> 16) as u16).to_le_bytes()));
756 e[26..28].copy_from_slice(&((cluster as u16).to_le_bytes()));
757 write_cluster(c, &buf)?;
758 return Ok(());
759 }
760 // Chain full: extend with a fresh zeroed cluster and retry.
761 match next_cluster(c) {
762 Ok(n) => cur = Some(n),
763 Err(_) => {
764 let n = alloc_cluster()?;
765 write_fat_entry(c, n)?;
766 cur = Some(n);
767 }
768 }
769 }
770}
771
772pub fn create(parent_dir_cluster: u32, name: &str, is_dir: bool) -> Result<(), ()> {
773 if find(parent_dir_cluster, name).is_ok() {
774 return Err(());
775 }
776 let cluster = if is_dir {
777 let c = alloc_cluster()?;
778 // "." and ".." entries.
779 let cs = cluster_size();
780 let mut buf = vec![0u8; cs];
781 let dot = &mut buf[0..32];
782 dot.fill(0);
783 dot[0] = b'.';
784 dot[1..11].fill(b' '); // SFN is space-padded
785 dot[11] = 0x10;
786 dot[20..22].copy_from_slice(&(((c >> 16) as u16).to_le_bytes()));
787 dot[26..28].copy_from_slice(&((c as u16).to_le_bytes()));
788 let dotdot = &mut buf[32..64];
789 dotdot.fill(0);
790 dotdot[0] = b'.';
791 dotdot[1] = b'.';
792 dotdot[2..11].fill(b' '); // SFN is space-padded
793 dotdot[11] = 0x10;
794 // ".." points at the parent; the root has cluster number 0.
795 let parent_cluster = if parent_dir_cluster == v().root_cluster {
796 0
797 } else {
798 parent_dir_cluster
799 };
800 dotdot[20..22].copy_from_slice(&(((parent_cluster >> 16) as u16).to_le_bytes()));
801 dotdot[26..28].copy_from_slice(&((parent_cluster as u16).to_le_bytes()));
802 write_cluster(c, &buf)?;
803 c
804 } else {
805 0
806 };
807 add_dir_entry(parent_dir_cluster, name, is_dir, cluster)
808}
809
810pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> {
811 let it = find_item(parent_dir_cluster, name)?;
812 if it.attr & 0x10 != 0 {
813 // Only empty directories may be removed.
814 let kids = list_dir(it.cluster)?;
815 if !kids.is_empty() {
816 return Err(());
817 }
818 free_chain(it.cluster);
819 } else if it.cluster != 0 {
820 free_chain(it.cluster);
821 }
822 // Mark the SFN and its leading LFN entries deleted.
823 let mut sec = [0u8; SECTOR];
824 read_sector(it.sector, &mut sec)?;
825 let mut off = it.offset;
826 sec[off] = 0xE5;
827 while off >= 32 {
828 let prev = &sec[off - 32..off];
829 if prev[11] == 0x0F {
830 sec[off - 32] = 0xE5;
831 off -= 32;
832 } else {
833 break;
834 }
835 }
836 write_sector(it.sector, &sec)
837}
838
839// --- mount -------------------------------------------------------------------
840
841/// Parse the BPB, cache the FAT and validate the volume. Returns true if
842/// a usable FAT32 volume is mounted.
843pub fn init() -> bool {
844 let mut bpb = [0u8; SECTOR];
845 if read_sector(0, &mut bpb).is_err() {
846 crate::serial_println!("fat: no disk");
847 return false;
848 }
849 let bytes_per_sector = u16::from_le_bytes([bpb[11], bpb[12]]);
850 if bytes_per_sector != 512 {
851 crate::serial_println!("fat: unsupported sector size {}", bytes_per_sector);
852 return false;
853 }
854 let spc = bpb[13];
855 if spc == 0 || spc & (spc - 1) != 0 {
856 crate::serial_println!("fat: bad sectors/cluster {}", spc);
857 return false;
858 }
859 let reserved = u16::from_le_bytes([bpb[14], bpb[15]]);
860 let fat_count = bpb[16];
861 let total16 = u16::from_le_bytes([bpb[19], bpb[20]]);
862 let total32 = u32::from_le_bytes([bpb[32], bpb[33], bpb[34], bpb[35]]);
863 let total = if total16 != 0 {
864 total16 as u32
865 } else {
866 total32
867 };
868 let fat_size = u32::from_le_bytes([bpb[36], bpb[37], bpb[38], bpb[39]]);
869 let root_cluster = u32::from_le_bytes([bpb[44], bpb[45], bpb[46], bpb[47]]);
870 let fsinfo_sec = u16::from_le_bytes([bpb[48], bpb[49]]) as u32;
871 if fat_size == 0 || root_cluster < 2 || reserved == 0 || fat_count == 0 {
872 crate::serial_println!("fat: not FAT32 (root={} fat={})", root_cluster, fat_size);
873 return false;
874 }
875 let first_data = reserved as u32 + fat_count as u32 * fat_size;
876 if first_data >= total {
877 crate::serial_println!("fat: bad geometry");
878 return false;
879 }
880 let cluster_count = (total - first_data) / spc as u32;
881
882 // Load the whole FAT into RAM (chunked through the 4 KiB DMA buffer).
883 let fat_bytes = fat_size as usize * SECTOR;
884 let mut fat = alloc::vec![0u8; fat_bytes];
885 let mut lba = reserved as u64;
886 let mut done = 0usize;
887 while done < fat.len() {
888 let n = ((fat.len() - done) / SECTOR).min(8) as u32;
889 if ahci::read_sectors(lba, n).is_err() {
890 crate::serial_println!("fat: FAT read failed at lba {}", lba);
891 return false;
892 }
893 unsafe {
894 core::ptr::copy(ahci::buf(), fat.as_mut_ptr().add(done), n as usize * SECTOR);
895 }
896 lba += n as u64;
897 done += n as usize * SECTOR;
898 }
899 // Sanity: FAT[0] holds the media byte, FAT[1] should be an EOC.
900 let e0 = u32::from_le_bytes([fat[0], fat[1], fat[2], fat[3]]);
901 let e1 = u32::from_le_bytes([fat[4], fat[5], fat[6], fat[7]]);
902 if e0 & 0xFF != 0xF8 || e1 & EOC_MASK != EOC {
903 crate::serial_println!(
904 "fat: bad FAT markers ({:#x} {:#x})",
905 e0 & 0xFF,
906 e1 & EOC_MASK
907 );
908 return false;
909 }
910 unsafe {
911 VOL = Some(Volume {
912 sectors_per_cluster: spc,
913 reserved,
914 fat_count,
915 fat_size,
916 root_cluster,
917 first_data,
918 cluster_count,
919 fsinfo: (fsinfo_sec != 0 && fsinfo_sec < total).then_some(fsinfo_sec),
920 fat,
921 });
922 }
923 // Re-sync the FSInfo free-cluster count (may be stale from earlier
924 // sessions that predate FSInfo maintenance).
925 fsinfo_recount();
926 let count = list_dir(root_cluster).map(|v| v.len()).unwrap_or(0);
927 crate::serial_println!(
928 "fat: FAT32 mounted — {} clusters ({} bytes each), root: {} entries",
929 cluster_count,
930 cluster_size(),
931 count
932 );
933 true
934}
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs
index df9308b..6950171 100644
--- a/rsnix/src/fs.rs
+++ b/rsnix/src/fs.rs
@@ -51,6 +51,19 @@ enum NodeData {
51 Regular(Vec<u8>), 51 Regular(Vec<u8>),
52 Dir(Vec<usize>), 52 Dir(Vec<usize>),
53 Char(CharDevice), 53 Char(CharDevice),
54 Disk(DiskInfo),
55}
56
57/// A node backed by the FAT32 volume (`/disk` mount). `parent` is the
58/// directory cluster holding the entry (used to re-locate it on every
59/// op, so it stays correct as the volume changes), `cluster` the entry's
60/// own first cluster.
61struct DiskInfo {
62 parent: u32,
63 name: String,
64 is_dir: bool,
65 size: u64,
66 cluster: u32,
54} 67}
55 68
56struct Node { 69struct Node {
@@ -161,6 +174,23 @@ pub fn init() -> (usize, usize) {
161 ), 174 ),
162 ); 175 );
163 176
177 // Mount the FAT32 volume at /disk when one is present.
178 if crate::fat::is_mounted() {
179 let root_cl = crate::fat::root_cluster();
180 add(
181 "disk",
182 Some(root),
183 FileType::Dir,
184 NodeData::Disk(DiskInfo {
185 parent: root_cl,
186 name: String::new(),
187 is_dir: true,
188 size: 0,
189 cluster: root_cl,
190 }),
191 );
192 }
193
164 let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); 194 let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count();
165 let files = nodes().len() - dirs; 195 let files = nodes().len() - dirs;
166 (dirs, files) 196 (dirs, files)
@@ -180,22 +210,52 @@ fn lookup(dir: usize, name: &str) -> Option<usize> {
180 None 210 None
181} 211}
182 212
183/// Resolve an absolute path (supports `.` and `..`). 213/// Resolve an absolute path (supports `.` and `..`). Paths under
214/// `/disk` walk the mounted FAT32 volume through ephemeral Disk nodes.
184pub fn resolve(path: &str) -> Option<usize> { 215pub fn resolve(path: &str) -> Option<usize> {
185 if nodes().is_empty() { 216 if nodes().is_empty() {
186 return None; 217 return None;
187 } 218 }
188 let mut cur = 0usize; // root 219 let mut cur = 0usize; // root
220 // (cluster, parent, name) of directories descended into under /disk.
221 let mut dstack: Vec<(u32, u32, String)> = Vec::new();
189 for seg in path.split('/') { 222 for seg in path.split('/') {
190 match seg { 223 match seg {
191 "" | "." => {} 224 "" | "." => {}
192 ".." => { 225 ".." => {
193 if let Some(p) = nodes()[cur].parent { 226 if let Some((cl, p, n)) = dstack.pop() {
227 cur = materialize_disk(DiskInfo {
228 parent: p,
229 name: n,
230 is_dir: true,
231 size: 0,
232 cluster: cl,
233 });
234 } else if let Some(p) = nodes()[cur].parent {
194 cur = p; 235 cur = p;
195 } 236 }
196 } 237 }
197 name => { 238 name => {
198 cur = lookup(cur, name)?; 239 let disk_dir: Option<u32> = match &nodes()[cur].data {
240 NodeData::Disk(info) => Some(info.cluster),
241 _ => None,
242 };
243 if let Some(dir_cluster) = disk_dir {
244 let e = crate::fat::find(dir_cluster, name).ok()?;
245 let d = DiskInfo {
246 parent: dir_cluster,
247 name: e.name.clone(),
248 is_dir: e.is_dir,
249 size: e.size,
250 cluster: e.cluster,
251 };
252 if e.is_dir {
253 dstack.push((e.cluster, dir_cluster, e.name));
254 }
255 cur = materialize_disk(d);
256 } else {
257 cur = lookup(cur, name)?;
258 }
199 } 259 }
200 } 260 }
201 } 261 }
@@ -209,12 +269,34 @@ pub fn kind(h: usize) -> FileType {
209pub fn size(h: usize) -> u64 { 269pub fn size(h: usize) -> u64 {
210 match &nodes()[h].data { 270 match &nodes()[h].data {
211 NodeData::Regular(v) => v.len() as u64, 271 NodeData::Regular(v) => v.len() as u64,
272 NodeData::Disk(i) => i.size,
212 _ => 0, 273 _ => 0,
213 } 274 }
214} 275}
215 276
216/// Entries of a directory as (name, kind, size) — tombstoned nodes skipped. 277/// Entries of a directory as (name, kind, size) — tombstoned nodes
278/// skipped; disk directories are listed fresh from the FAT volume.
217pub fn children(h: usize) -> Vec<(String, FileType, u64)> { 279pub fn children(h: usize) -> Vec<(String, FileType, u64)> {
280 if let NodeData::Disk(i) = &nodes()[h].data {
281 let cl = i.cluster;
282 return crate::fat::list_dir(cl)
283 .map(|es| {
284 es.into_iter()
285 .map(|e| {
286 (
287 e.name,
288 if e.is_dir {
289 FileType::Dir
290 } else {
291 FileType::Regular
292 },
293 e.size,
294 )
295 })
296 .collect()
297 })
298 .unwrap_or_default();
299 }
218 let kids = match &nodes()[h].data { 300 let kids = match &nodes()[h].data {
219 NodeData::Dir(v) => v.clone(), 301 NodeData::Dir(v) => v.clone(),
220 _ => return Vec::new(), 302 _ => return Vec::new(),
@@ -275,6 +357,7 @@ pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result<usize, ()> {
275 buf[..n].copy_from_slice(&bytes[p..p + n]); 357 buf[..n].copy_from_slice(&bytes[p..p + n]);
276 Ok(n) 358 Ok(n)
277 } 359 }
360 NodeData::Disk(i) => crate::fat::read_file(i.parent, &i.name, buf, pos),
278 NodeData::Dir(_) => Err(()), 361 NodeData::Dir(_) => Err(()),
279 } 362 }
280} 363}
@@ -311,6 +394,7 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> {
311 } 394 }
312 _ => Err(()), // read-only devices 395 _ => Err(()), // read-only devices
313 }, 396 },
397 NodeData::Disk(i) => crate::fat::write_file(i.parent, &i.name, buf, pos),
314 NodeData::Dir(_) => Err(()), 398 NodeData::Dir(_) => Err(()),
315 } 399 }
316} 400}
@@ -322,6 +406,7 @@ pub fn set_len(h: usize, len: u64) -> Result<(), ()> {
322 v.resize(len as usize, 0); 406 v.resize(len as usize, 0);
323 Ok(()) 407 Ok(())
324 } 408 }
409 NodeData::Disk(i) => crate::fat::set_len(i.parent, &i.name, len),
325 _ => Err(()), 410 _ => Err(()),
326 } 411 }
327} 412}
@@ -331,6 +416,30 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> {
331 if name.is_empty() || name.contains('/') || kind == FileType::Char { 416 if name.is_empty() || name.contains('/') || kind == FileType::Char {
332 return Err(()); 417 return Err(());
333 } 418 }
419 // Disk directory: create through the FAT volume.
420 let disk_parent: Option<u32> = match &nodes()[parent].data {
421 NodeData::Disk(i) => {
422 if !i.is_dir {
423 return Err(());
424 }
425 Some(i.cluster)
426 }
427 _ => None,
428 };
429 if let Some(p) = disk_parent {
430 if crate::fat::find(p, name).is_ok() {
431 return Err(());
432 }
433 crate::fat::create(p, name, kind == FileType::Dir)?;
434 let e = crate::fat::find(p, name)?;
435 return Ok(materialize_disk(DiskInfo {
436 parent: p,
437 name: e.name,
438 is_dir: e.is_dir,
439 size: e.size,
440 cluster: e.cluster,
441 }));
442 }
334 if lookup(parent, name).is_some() { 443 if lookup(parent, name).is_some() {
335 return Err(()); 444 return Err(());
336 } 445 }
@@ -344,6 +453,15 @@ pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> {
344 453
345/// Remove a regular file or an empty directory (tombstone). 454/// Remove a regular file or an empty directory (tombstone).
346pub fn remove(h: usize) -> Result<(), ()> { 455pub fn remove(h: usize) -> Result<(), ()> {
456 // Disk nodes: delete through the FAT volume (dirs must be empty).
457 if let NodeData::Disk(i) = &nodes()[h].data {
458 if i.name.is_empty() {
459 return Err(()); // the /disk mount itself
460 }
461 let p = i.parent;
462 let n = i.name.clone();
463 return crate::fat::remove(p, &n);
464 }
347 let parent = match nodes()[h].parent { 465 let parent = match nodes()[h].parent {
348 Some(p) => p, 466 Some(p) => p,
349 None => return Err(()), 467 None => return Err(()),
@@ -414,6 +532,44 @@ pub fn remove_path(path: &str) -> Result<(), ()> {
414 remove(h) 532 remove(h)
415} 533}
416 534
535// --- disk mount ------------------------------------------------------------
536
537/// Round-robin pool of ephemeral disk nodes so a long-lived shell doesn't
538/// grow the node table forever.
539const DISK_SLOTS: usize = 24;
540static mut DISK_POOL: *mut Vec<usize> = core::ptr::null_mut();
541static mut DISK_ROBIN: usize = 0;
542
543fn materialize_disk(info: DiskInfo) -> usize {
544 unsafe {
545 if DISK_POOL.is_null() {
546 DISK_POOL = Box::into_raw(Box::new(Vec::new()));
547 }
548 let pool = &mut *DISK_POOL;
549 let idx = DISK_ROBIN % DISK_SLOTS;
550 DISK_ROBIN = DISK_ROBIN.wrapping_add(1);
551 let kind = if info.is_dir {
552 FileType::Dir
553 } else {
554 FileType::Regular
555 };
556 if idx < pool.len() {
557 let h = pool[idx];
558 let n = &mut nodes()[h];
559 n.name = info.name.clone();
560 n.kind = kind;
561 n.parent = None;
562 n.data = NodeData::Disk(info);
563 n.removed = false;
564 return h;
565 }
566 let name = info.name.clone();
567 let h = add(&name, None, kind, NodeData::Disk(info));
568 pool.push(h);
569 h
570 }
571}
572
417// --- character devices ---------------------------------------------------- 573// --- character devices ----------------------------------------------------
418 574
419fn version_text() -> String { 575fn version_text() -> String {
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index fa14eae..77cc514 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -167,7 +167,11 @@ impl Framebuffer {
167 if SHADOW.is_null() { 167 if SHADOW.is_null() {
168 // No shadow (allocation failed): fall back to shifting the 168 // No shadow (allocation failed): fall back to shifting the
169 // display directly. 169 // display directly.
170 ptr::copy(self.base.add(n * row_bytes), self.base, total - n * row_bytes); 170 ptr::copy(
171 self.base.add(n * row_bytes),
172 self.base,
173 total - n * row_bytes,
174 );
171 let last_row = self.base.add(total - n * row_bytes); 175 let last_row = self.base.add(total - n * row_bytes);
172 for i in 0..(n * row_words) { 176 for i in 0..(n * row_words) {
173 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); 177 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg);
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index a8e279a..fc823ea 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -2,6 +2,8 @@
2 2
3extern crate alloc; 3extern crate alloc;
4 4
5pub mod ahci;
6pub mod fat;
5pub mod font; 7pub mod font;
6pub mod fs; 8pub mod fs;
7pub mod gop; 9pub mod gop;
diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs
index 2d201f3..efdbc01 100644
--- a/rsnix/src/pci.rs
+++ b/rsnix/src/pci.rs
@@ -57,6 +57,39 @@ pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 {
57 inl(CONFIG_DATA) 57 inl(CONFIG_DATA)
58} 58}
59 59
60/// Write a dword to a device's config space (`reg` must be dword-aligned).
61pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) {
62 let addr = 0x8000_0000
63 | ((bus as u32) << 16)
64 | ((device as u32) << 11)
65 | ((func as u32) << 8)
66 | ((reg as u32) & 0xFC);
67 outl(CONFIG_ADDRESS, addr);
68 outl(CONFIG_DATA, val)
69}
70
71/// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit
72/// BARs. Returns None for I/O BARs or invalid values.
73pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> {
74 let reg_off = 0x10 + bar_index * 4;
75 let lo = config_read(bus, device, func, reg_off);
76 if lo & 1 == 1 {
77 return None; // I/O BAR
78 }
79 let kind = (lo >> 1) & 0x3;
80 let hi = if kind == 2 {
81 config_read(bus, device, func, reg_off + 4)
82 } else {
83 0
84 };
85 let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0);
86 if base == 0 {
87 None
88 } else {
89 Some(base)
90 }
91}
92
60/// Enumerate every device/function present on all buses. 93/// Enumerate every device/function present on all buses.
61pub fn probe() -> alloc::vec::Vec<DeviceInfo> { 94pub fn probe() -> alloc::vec::Vec<DeviceInfo> {
62 let mut out = alloc::vec::Vec::new(); 95 let mut out = alloc::vec::Vec::new();
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 88129f2..59378f3 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -432,9 +432,7 @@ fn scroll_up() {
432 // appears promptly. 432 // appears promptly.
433 const TICK_LIMIT: u16 = 4; 433 const TICK_LIMIT: u16 = 4;
434 let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); 434 let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);
435 if PENDING_SCROLLS >= SCROLL_BATCH 435 if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT {
436 || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT
437 {
438 sync_scroll(); 436 sync_scroll();
439 } 437 }
440 } 438 }