summaryrefslogtreecommitdiffstats
path: root/rsnix/src/fs.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rsnix/src/fs.rs')
-rw-r--r--rsnix/src/fs.rs501
1 files changed, 501 insertions, 0 deletions
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs
new file mode 100644
index 0000000..56820bb
--- /dev/null
+++ b/rsnix/src/fs.rs
@@ -0,0 +1,501 @@
1// ---------------------------------------------------------------------------
2// VFS — "everything is a file".
3//
4// A tiny hierarchical filesystem namespace: directories, in-memory regular
5// files (ramfs, writable via the shell), and character devices under /dev.
6// Device reads re-generate their content on every access, so
7// `cat /dev/rtc` shows the *current* time and `cat /dev/mem` hexdumps live
8// physical memory. Writes to /dev/console print to the screen, writes to
9// /dev/null disappear.
10//
11// Nodes live in a flat table; handles are stable indices (removal leaves a
12// tombstone). Single-threaded, interrupt-safe because no ISR touches the
13// table. A real disk filesystem can mount under this same namespace later.
14// ---------------------------------------------------------------------------
15
16extern crate alloc;
17
18use alloc::boxed::Box;
19use alloc::string::String;
20use alloc::vec::Vec;
21use crate::gop;
22use crate::memory;
23use crate::pci;
24use crate::rtc;
25use crate::serial;
26
27/// /dev/mem covers the low physical-memory window (identity-mapped RAM).
28pub const MEM_WINDOW: u64 = 64 * 1024;
29
30#[derive(Clone, Copy, PartialEq)]
31pub enum FileType {
32 Regular,
33 Char,
34 Dir,
35}
36
37#[derive(Clone, Copy)]
38enum CharDevice {
39 Mem,
40 Rtc,
41 Pci,
42 Version,
43 Meminfo,
44 Null,
45 Zero,
46 Console,
47 Serial,
48}
49
50enum NodeData {
51 Regular(Vec<u8>),
52 Dir(Vec<usize>),
53 Char(CharDevice),
54}
55
56struct Node {
57 name: String,
58 kind: FileType,
59 parent: Option<usize>,
60 data: NodeData,
61 removed: bool,
62}
63
64/// Node table; root is always handle 0. Boxed so the static stays a raw
65/// pointer (matches the rest of the crate's style).
66static mut NODES: *mut Vec<Node> = core::ptr::null_mut();
67
68fn nodes() -> &'static mut Vec<Node> {
69 unsafe { &mut *NODES }
70}
71
72fn add(name: &str, parent: Option<usize>, kind: FileType, data: NodeData) -> usize {
73 let h = nodes().len();
74 nodes().push(Node {
75 name: name.into(),
76 kind,
77 parent,
78 data,
79 removed: false,
80 });
81 if let Some(p) = parent {
82 if let NodeData::Dir(v) = &mut nodes()[p].data {
83 v.push(h);
84 }
85 }
86 h
87}
88
89/// Build the tree and report (dirs, files).
90pub fn init() -> (usize, usize) {
91 unsafe {
92 NODES = Box::into_raw(Box::new(Vec::new()));
93 }
94 let root = add("/", None, FileType::Dir, NodeData::Dir(Vec::new()));
95 let dev = add("dev", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
96 let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
97 let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
98
99 add("mem", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mem));
100 add("rtc", Some(dev), FileType::Char, NodeData::Char(CharDevice::Rtc));
101 add("pci", Some(dev), FileType::Char, NodeData::Char(CharDevice::Pci));
102 add(
103 "version",
104 Some(dev),
105 FileType::Char,
106 NodeData::Char(CharDevice::Version),
107 );
108 add(
109 "meminfo",
110 Some(dev),
111 FileType::Char,
112 NodeData::Char(CharDevice::Meminfo),
113 );
114 add(
115 "null",
116 Some(dev),
117 FileType::Char,
118 NodeData::Char(CharDevice::Null),
119 );
120 add(
121 "zero",
122 Some(dev),
123 FileType::Char,
124 NodeData::Char(CharDevice::Zero),
125 );
126 add(
127 "console",
128 Some(dev),
129 FileType::Char,
130 NodeData::Char(CharDevice::Console),
131 );
132 add(
133 "serial",
134 Some(dev),
135 FileType::Char,
136 NodeData::Char(CharDevice::Serial),
137 );
138
139 add(
140 "motd",
141 Some(etc),
142 FileType::Regular,
143 NodeData::Regular(
144 b"Welcome to rs-dos!\nEverything is a file. Try: cat /dev/rtc, cat /dev/pci, cat /dev/mem\n"
145 .to_vec(),
146 ),
147 );
148
149 let dirs = nodes()
150 .iter()
151 .filter(|n| n.kind == FileType::Dir)
152 .count();
153 let files = nodes().len() - dirs;
154 (dirs, files)
155}
156
157fn lookup(dir: usize, name: &str) -> Option<usize> {
158 let children = match &nodes()[dir].data {
159 NodeData::Dir(v) => v.clone(),
160 _ => return None,
161 };
162 for c in children {
163 let n = &nodes()[c];
164 if !n.removed && n.name == name {
165 return Some(c);
166 }
167 }
168 None
169}
170
171/// Resolve an absolute path (supports `.` and `..`).
172pub fn resolve(path: &str) -> Option<usize> {
173 if nodes().is_empty() {
174 return None;
175 }
176 let mut cur = 0usize; // root
177 for seg in path.split('/') {
178 match seg {
179 "" | "." => {}
180 ".." => {
181 if let Some(p) = nodes()[cur].parent {
182 cur = p;
183 }
184 }
185 name => {
186 cur = lookup(cur, name)?;
187 }
188 }
189 }
190 Some(cur)
191}
192
193pub fn kind(h: usize) -> FileType {
194 nodes()[h].kind
195}
196
197pub fn size(h: usize) -> u64 {
198 match &nodes()[h].data {
199 NodeData::Regular(v) => v.len() as u64,
200 _ => 0,
201 }
202}
203
204/// Entries of a directory as (name, kind, size) — tombstoned nodes skipped.
205pub fn children(h: usize) -> Vec<(String, FileType, u64)> {
206 let kids = match &nodes()[h].data {
207 NodeData::Dir(v) => v.clone(),
208 _ => return Vec::new(),
209 };
210 let mut out = Vec::new();
211 for c in kids {
212 let n = &nodes()[c];
213 if n.removed {
214 continue;
215 }
216 let sz = match &n.data {
217 NodeData::Regular(v) => v.len() as u64,
218 _ => 0,
219 };
220 out.push((n.name.clone(), n.kind, sz));
221 }
222 out
223}
224
225pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result<usize, ()> {
226 if buf.is_empty() {
227 return Ok(0);
228 }
229 match &mut nodes()[h].data {
230 NodeData::Regular(v) => {
231 let p = pos as usize;
232 if p >= v.len() {
233 return Ok(0);
234 }
235 let n = buf.len().min(v.len() - p);
236 buf[..n].copy_from_slice(&v[p..p + n]);
237 Ok(n)
238 }
239 NodeData::Char(CharDevice::Mem) => {
240 // /dev/mem is a raw byte window: file offset = physical address.
241 let p = pos as usize;
242 if p >= MEM_WINDOW as usize {
243 return Ok(0);
244 }
245 let n = buf.len().min(MEM_WINDOW as usize - p);
246 for i in 0..n {
247 buf[i] = unsafe { core::ptr::read_volatile((p + i) as *const u8) };
248 }
249 Ok(n)
250 }
251 NodeData::Char(CharDevice::Zero) => {
252 buf.fill(0);
253 Ok(buf.len())
254 }
255 NodeData::Char(dev) => {
256 let text = dev_text(*dev, pos);
257 let bytes = text.as_bytes();
258 let p = pos as usize;
259 if p >= bytes.len() {
260 return Ok(0);
261 }
262 let n = buf.len().min(bytes.len() - p);
263 buf[..n].copy_from_slice(&bytes[p..p + n]);
264 Ok(n)
265 }
266 NodeData::Dir(_) => Err(()),
267 }
268}
269
270pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> {
271 match &mut nodes()[h].data {
272 NodeData::Regular(v) => {
273 let p = pos as usize;
274 if p + buf.len() > v.len() {
275 v.resize(p + buf.len(), 0);
276 }
277 v[p..p + buf.len()].copy_from_slice(buf);
278 Ok(buf.len())
279 }
280 NodeData::Char(dev) => match dev {
281 CharDevice::Null | CharDevice::Zero => Ok(buf.len()),
282 CharDevice::Console => {
283 for &b in buf {
284 if b == b'\n' {
285 gop::put_char('\n');
286 serial::put_byte(b'\n');
287 } else if b.is_ascii() {
288 gop::put_char(b as char);
289 serial::put_byte(b);
290 }
291 }
292 Ok(buf.len())
293 }
294 CharDevice::Serial => {
295 for &b in buf {
296 serial::put_byte(b);
297 }
298 Ok(buf.len())
299 }
300 _ => Err(()), // read-only devices
301 },
302 NodeData::Dir(_) => Err(()),
303 }
304}
305
306/// Truncate/resize a regular file.
307pub fn set_len(h: usize, len: u64) -> Result<(), ()> {
308 match &mut nodes()[h].data {
309 NodeData::Regular(v) => {
310 v.resize(len as usize, 0);
311 Ok(())
312 }
313 _ => Err(()),
314 }
315}
316
317/// Create a Regular or Dir child. Fails if the name exists or is invalid.
318pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> {
319 if name.is_empty() || name.contains('/') || kind == FileType::Char {
320 return Err(());
321 }
322 if lookup(parent, name).is_some() {
323 return Err(());
324 }
325 let data = match kind {
326 FileType::Regular => NodeData::Regular(Vec::new()),
327 FileType::Dir => NodeData::Dir(Vec::new()),
328 FileType::Char => return Err(()),
329 };
330 Ok(add(name, Some(parent), kind, data))
331}
332
333/// Remove a regular file or an empty directory (tombstone).
334pub fn remove(h: usize) -> Result<(), ()> {
335 let parent = match nodes()[h].parent {
336 Some(p) => p,
337 None => return Err(()),
338 };
339 let empty = match &nodes()[h].data {
340 NodeData::Dir(v) => v.is_empty(),
341 _ => true,
342 };
343 if !empty {
344 return Err(());
345 }
346 if let NodeData::Dir(v) = &mut nodes()[parent].data {
347 v.retain(|&c| c != h);
348 }
349 let n = &mut nodes()[h];
350 n.removed = true;
351 n.parent = None;
352 Ok(())
353}
354
355// --- path helpers ---------------------------------------------------------
356
357/// Split `/a/b/c` into (handle of `/a/b`, "c").
358fn split_parent_name(path: &str) -> Result<(usize, String), ()> {
359 let path = path.trim_end_matches('/');
360 if path.is_empty() {
361 return Err(());
362 }
363 let (parent_path, name) = match path.rfind('/') {
364 Some(i) => (&path[..i], &path[i + 1..]),
365 None => ("/", path),
366 };
367 if name.is_empty() {
368 return Err(());
369 }
370 Some((resolve(parent_path).ok_or(())?, name.into())).ok_or(())
371}
372
373pub fn mkdir(path: &str) -> Result<(), ()> {
374 let (p, n) = split_parent_name(path)?;
375 create(p, &n, FileType::Dir).map(|_| ())
376}
377
378pub fn touch(path: &str) -> Result<(), ()> {
379 if resolve(path).is_some() {
380 return Ok(());
381 }
382 let (p, n) = split_parent_name(path)?;
383 create(p, &n, FileType::Regular).map(|_| ())
384}
385
386/// Resolve a path, creating a regular file if it doesn't exist yet.
387pub fn ensure_file(path: &str) -> Result<usize, ()> {
388 if let Some(h) = resolve(path) {
389 if kind(h) == FileType::Dir {
390 Err(())
391 } else {
392 Ok(h)
393 }
394 } else {
395 touch(path)?;
396 resolve(path).ok_or(())
397 }
398}
399
400pub fn remove_path(path: &str) -> Result<(), ()> {
401 let h = resolve(path).ok_or(())?;
402 remove(h)
403}
404
405// --- character devices ----------------------------------------------------
406
407fn version_text() -> String {
408 alloc::format!(
409 "rsnix v{} — TempleOS-inspired toy kernel\nUEFI x86_64, identity paging, free-list heap, bitmap pages\n",
410 env!("CARGO_PKG_VERSION")
411 )
412}
413
414fn meminfo_text() -> String {
415 let h = memory::heap_stats();
416 let p = memory::page_stats();
417 let (map, key) = memory::get_map();
418 let mut usable_kb = 0u64;
419 for d in map {
420 if d.kind == 7 {
421 usable_kb += d.number_of_pages * 4;
422 }
423 }
424 alloc::format!(
425 "heap: {} KB capacity, {} KB free in {} block(s)\npages: {} free / {} used, {} MB span\nmem: {} entries (key 0x{:x}), {} MB usable\n",
426 h.heap_size >> 10,
427 h.free_bytes >> 10,
428 h.free_blocks,
429 p.free_pages,
430 p.used_pages,
431 p.span_bytes >> 20,
432 map.len(),
433 key,
434 usable_kb >> 10,
435 )
436}
437
438fn pci_listing() -> String {
439 let mut out = String::new();
440 let devices = pci::probe();
441 out += &alloc::format!("{} device(s)\n", devices.len());
442 for d in &devices {
443 out += &alloc::format!(
444 "{:02x}:{:02x}.{} {:<16} {:04x}:{:04x}\n",
445 d.bus,
446 d.device,
447 d.func,
448 pci::class_name(d.class, d.subclass),
449 d.vendor,
450 d.device_id
451 );
452 }
453 out
454}
455
456fn dev_text(dev: CharDevice, _pos: u64) -> String {
457 match dev {
458 CharDevice::Mem => String::new(), // raw bytes, special-cased in read()
459 CharDevice::Rtc => {
460 let t = rtc::now();
461 alloc::format!(
462 "{}-{:02}-{:02} {:02}:{:02}:{:02}\n",
463 t.year,
464 t.month,
465 t.day,
466 t.hour,
467 t.minute,
468 t.second
469 )
470 }
471 CharDevice::Pci => pci_listing(),
472 CharDevice::Version => version_text(),
473 CharDevice::Meminfo => meminfo_text(),
474 CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(),
475 CharDevice::Zero => String::new(),
476 }
477}
478
479// --- self-test ------------------------------------------------------------
480
481pub fn self_test() -> bool {
482 let mut buf = [0u8; 8];
483 let h = match ensure_file("/tmp/selftest") {
484 Ok(h) => h,
485 Err(_) => return false,
486 };
487 if write(h, b"hello", 0) != Ok(5) {
488 return false;
489 }
490 if read(h, &mut buf, 0) != Ok(5) || &buf[..5] != b"hello" {
491 return false;
492 }
493 if remove(h).is_err() || resolve("/tmp/selftest").is_some() {
494 return false;
495 }
496 // device write goes nowhere: echo into /dev/null
497 if let Ok(nul) = ensure_file("/dev/null") {
498 let _ = write(nul, b"discard me", 0);
499 }
500 true
501}