summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 16:31:34 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 16:31:34 +0200
commit6767d96e8ed5049af7b551b00d6320fbfb5dcd05 (patch)
tree118e6b52d75684e52f96b923e773a1100c02c5cb
parentrtc + pci: CMOS clock with date command, PCI bus enumeration (diff)
downloadrs-dos-6767d96e8ed5049af7b551b00d6320fbfb5dcd05.tar.gz
rs-dos-6767d96e8ed5049af7b551b00d6320fbfb5dcd05.zip
vfs: everything is a file — dirs, ramfs, character devices
- fs.rs: node-table VFS (root /dev /tmp /etc). Regular files are in-memory ramfs; char devices regenerate content on every read so 'cat /dev/rtc' shows the live time. Nodes are stable handles with tombstone removal; . and .. path resolution. - /dev devices: mem (raw physical RAM, offset = address), rtc, pci, version, meminfo, null, zero, console (write prints to screen+serial), serial. /etc/motd static file. - shell: ls, cat, hexdump (hexdump -C style), mkdir, touch, rm; echo gains '>' overwrite and '>>' append redirection (append bug fixed: first '>' in '>>' was parsed as overwrite) - boot: fs init + self-test (create/read/write/remove) before the shell - QEMU verified: file lifecycle, redirection, console/null writes, mkdir/touch/rm, hexdump /dev/mem and /tmp/x byte-exact
-rw-r--r--CONTEXT.md6
-rw-r--r--rsdos/src/main.rs8
-rw-r--r--rsnix/src/fs.rs501
-rw-r--r--rsnix/src/lib.rs1
-rw-r--r--shell/src/lib.rs222
5 files changed, 736 insertions, 2 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 8ee7f6c..5b76eaa 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -98,6 +98,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
98- `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback 98- `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback
99- `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization 99- `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization
100- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration 100- `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration
101- `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`,
102 `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs;
103 `/etc/motd`)
101- `gop.rs` — framebuffer access and text rendering 104- `gop.rs` — framebuffer access and text rendering
102- `font.rs` — 8×16 bitmap font data 105- `font.rs` — 8×16 bitmap font data
103- `ps2.rs` — PS/2 keyboard driver (polling) 106- `ps2.rs` — PS/2 keyboard driver (polling)
@@ -124,6 +127,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown:
124 (ACPI S5; `halt` keeps the plain hlt loop) 127 (ACPI S5; `halt` keeps the plain hlt loop)
125✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled) 128✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled)
126✅ **PCI** — port-based config access, full bus enumeration via `pci` 129✅ **PCI** — port-based config access, full bus enumeration via `pci`
130✅ **VFS** — “everything is a file”: dirs, ramfs, char devices
131 (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices
132 regenerate on read, `/dev/mem` is raw physical RAM)
127 133
128## Target audience 134## Target audience
129 135
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index c35d88f..8e49937 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -154,6 +154,14 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT
154 let devices = rsnix::pci::probe(); 154 let devices = rsnix::pci::probe();
155 rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); 155 rsnix::serial_println!("pci: {} device(s) on all buses", devices.len());
156 156
157 let (dirs, files) = rsnix::fs::init();
158 rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files);
159 if rsnix::fs::self_test() {
160 rsnix::serial_println!("fs: self-test OK (create/read/write/remove)");
161 } else {
162 rsnix::serial_println!("fs: self-test FAILED");
163 }
164
157 rsnix::serial_println!("boot: entering shell"); 165 rsnix::serial_println!("boot: entering shell");
158 shell::run(); 166 shell::run();
159} 167}
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}
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index f84fa73..bb0a3eb 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -3,6 +3,7 @@
3extern crate alloc; 3extern crate alloc;
4 4
5pub mod font; 5pub mod font;
6pub mod fs;
6pub mod gop; 7pub mod gop;
7pub mod interrupts; 8pub mod interrupts;
8pub mod memory; 9pub mod memory;
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index 500eaae..7e1439e 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -2,6 +2,8 @@
2 2
3extern crate alloc; 3extern crate alloc;
4 4
5use alloc::string::ToString;
6
5/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. 7/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial.
6use rsnix::ps2::{self, KeyEvent}; 8use rsnix::ps2::{self, KeyEvent};
7use rsnix::{gop_print, gop_println, serial_print, serial_println}; 9use rsnix::{gop_print, gop_println, serial_print, serial_println};
@@ -52,6 +54,12 @@ pub fn run() -> ! {
52 "stress" => cmd_stress(args), 54 "stress" => cmd_stress(args),
53 "date" => cmd_date(), 55 "date" => cmd_date(),
54 "pci" => cmd_pci(), 56 "pci" => cmd_pci(),
57 "ls" => cmd_ls(args),
58 "cat" => cmd_cat(args),
59 "hexdump" => cmd_hexdump(args),
60 "mkdir" => cmd_mkdir(args),
61 "touch" => cmd_touch(args),
62 "rm" => cmd_rm(args),
55 _ => { 63 _ => {
56 gop_println!("unknown command: {}", cmd); 64 gop_println!("unknown command: {}", cmd);
57 serial_println!("unknown command: {}", cmd); 65 serial_println!("unknown command: {}", cmd);
@@ -128,15 +136,220 @@ fn cmd_help() {
128 gop_println!(" stress — stress-test heap/page allocators"); 136 gop_println!(" stress — stress-test heap/page allocators");
129 gop_println!(" date — show current date/time (RTC)"); 137 gop_println!(" date — show current date/time (RTC)");
130 gop_println!(" pci — list PCI devices"); 138 gop_println!(" pci — list PCI devices");
139 gop_println!(" ls — list a directory");
140 gop_println!(" cat — print files (e.g. cat /dev/rtc)");
141 gop_println!(" hexdump — hex-dump a file (e.g. hexdump /dev/mem)");
142 gop_println!(" mkdir — create a directory");
143 gop_println!(" touch — create an empty file");
144 gop_println!(" rm — remove a file or empty dir");
145 gop_println!(" echo > /dev/null, > file, >> file");
131 gop_println!(" halt — halt the system (hlt loop)"); 146 gop_println!(" halt — halt the system (hlt loop)");
132 gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); 147 gop_println!(" poweroff — shut down via ACPI (ResetSystem)");
133 gop_println!(" exit — alias for poweroff"); 148 gop_println!(" exit — alias for poweroff");
134 serial_println!("available commands:"); 149 serial_println!("available commands:");
135 serial_println!(" help, echo, clear, hello, info, mem, mmap, stress, date, pci, halt, poweroff, exit"); 150 serial_println!(
151 " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, halt, poweroff, exit"
152 );
136} 153}
137 154
138fn cmd_echo(args: &str) { 155fn cmd_echo(args: &str) {
156 // Redirection: `text > path` (overwrite) or `text >> path` (append).
157 if let Some(idx) = args.find('>') {
158 let append = args[idx..].starts_with(">>");
159 let text = &args[..idx];
160 let path = args[idx + 1 + append as usize..].trim();
161 let path = path.split_whitespace().next().unwrap_or("");
162 if !path.is_empty() {
163 let content = alloc::format!("{}\n", text.trim_end());
164 let result = rsnix::fs::ensure_file(path).and_then(|h| {
165 if append {
166 rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h))
167 .map(|_| ())
168 } else {
169 let _ = rsnix::fs::set_len(h, 0);
170 rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ())
171 }
172 });
173 match result {
174 Ok(()) => {}
175 Err(()) => {
176 gop_println!("echo: cannot write '{}'", path);
177 serial_println!("echo: cannot write '{}'", path);
178 }
179 }
180 return;
181 }
182 }
139 gop_println!("{}", args); 183 gop_println!("{}", args);
184 serial_println!("{}", args);
185}
186
187fn cmd_ls(args: &str) {
188 let path = args.split_whitespace().next().unwrap_or("/");
189 let mut out = alloc::string::String::new();
190 match rsnix::fs::resolve(path) {
191 None => {
192 out.push_str(&alloc::format!("ls: {}: no such file or directory\n", path));
193 }
194 Some(h) => {
195 let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir {
196 rsnix::fs::children(h)
197 } else {
198 alloc::vec![(
199 path.rsplit('/').next().unwrap_or(path).to_string(),
200 rsnix::fs::kind(h),
201 rsnix::fs::size(h)
202 )]
203 };
204 for (name, kind, size) in list {
205 match kind {
206 rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)),
207 rsnix::fs::FileType::Char => {
208 out.push_str(&alloc::format!("{}@\n", name))
209 }
210 rsnix::fs::FileType::Regular => {
211 out.push_str(&alloc::format!("{} {} B\n", name, size))
212 }
213 }
214 }
215 }
216 }
217 gop_print!("{}", out);
218 serial_print!("{}", out);
219}
220
221fn cmd_cat(args: &str) {
222 let paths: alloc::vec::Vec<&str> = args.split_whitespace().collect();
223 if paths.is_empty() {
224 gop_println!("usage: cat <path> [path...]");
225 serial_println!("usage: cat <path> [path...]");
226 return;
227 }
228 let mut out = alloc::string::String::new();
229 let mut buf = [0u8; 512];
230 for p in paths {
231 match rsnix::fs::resolve(p) {
232 None => out.push_str(&alloc::format!("cat: {}: no such file\n", p)),
233 Some(h) => {
234 let mut pos = 0u64;
235 loop {
236 match rsnix::fs::read(h, &mut buf, pos) {
237 Ok(0) | Err(_) => break,
238 Ok(n) => {
239 out.push_str(core::str::from_utf8(&buf[..n]).unwrap_or("?"));
240 pos += n as u64;
241 }
242 }
243 }
244 }
245 }
246 }
247 gop_print!("{}", out);
248 serial_print!("{}", out);
249}
250
251fn cmd_hexdump(args: &str) {
252 let path = args.split_whitespace().next().unwrap_or("");
253 if path.is_empty() {
254 gop_println!("usage: hexdump <path>");
255 serial_println!("usage: hexdump <path>");
256 return;
257 }
258 let h = match rsnix::fs::resolve(path) {
259 Some(h) => h,
260 None => {
261 gop_println!("hexdump: {}: no such file", path);
262 serial_println!("hexdump: {}: no such file", path);
263 return;
264 }
265 };
266 let mut out = alloc::string::String::new();
267 let mut buf = [0u8; 512];
268 let mut pos = 0u64;
269 use core::fmt::Write as _;
270 'outer: loop {
271 let n = match rsnix::fs::read(h, &mut buf, pos) {
272 Ok(0) | Err(_) => break,
273 Ok(n) => n,
274 };
275 for i in (0..n).step_by(16) {
276 let _ = core::write!(out, "{:08x} ", pos + i as u64);
277 for j in 0..16 {
278 if i + j < n {
279 let _ = core::write!(out, "{:02x} ", buf[i + j]);
280 } else {
281 out.push_str(" ");
282 }
283 }
284 out.push_str(" |");
285 for j in 0..16 {
286 if i + j >= n {
287 break;
288 }
289 let b = buf[i + j];
290 out.push(if b.is_ascii_graphic() || b == b' ' {
291 b as char
292 } else {
293 '.'
294 });
295 }
296 out.push_str("|\n");
297 if i + 16 >= n {
298 pos += n as u64;
299 continue 'outer;
300 }
301 }
302 }
303 gop_print!("{}", out);
304 serial_print!("{}", out);
305}
306
307fn cmd_mkdir(args: &str) {
308 let path = args.split_whitespace().next().unwrap_or("");
309 if path.is_empty() {
310 gop_println!("usage: mkdir <path>");
311 serial_println!("usage: mkdir <path>");
312 return;
313 }
314 match rsnix::fs::mkdir(path) {
315 Ok(()) => {}
316 Err(()) => {
317 gop_println!("mkdir: cannot create directory '{}'", path);
318 serial_println!("mkdir: cannot create directory '{}'", path);
319 }
320 }
321}
322
323fn cmd_touch(args: &str) {
324 let path = args.split_whitespace().next().unwrap_or("");
325 if path.is_empty() {
326 gop_println!("usage: touch <path>");
327 serial_println!("usage: touch <path>");
328 return;
329 }
330 match rsnix::fs::touch(path) {
331 Ok(()) => {}
332 Err(()) => {
333 gop_println!("touch: cannot create '{}'", path);
334 serial_println!("touch: cannot create '{}'", path);
335 }
336 }
337}
338
339fn cmd_rm(args: &str) {
340 let path = args.split_whitespace().next().unwrap_or("");
341 if path.is_empty() {
342 gop_println!("usage: rm <path>");
343 serial_println!("usage: rm <path>");
344 return;
345 }
346 match rsnix::fs::remove_path(path) {
347 Ok(()) => {}
348 Err(()) => {
349 gop_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path);
350 serial_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path);
351 }
352 }
140} 353}
141 354
142fn cmd_clear() { 355fn cmd_clear() {
@@ -372,7 +585,12 @@ fn cmd_date() {
372 let t = rsnix::rtc::now(); 585 let t = rsnix::rtc::now();
373 let line = alloc::format!( 586 let line = alloc::format!(
374 "{}-{:02}-{:02} {:02}:{:02}:{:02}", 587 "{}-{:02}-{:02} {:02}:{:02}:{:02}",
375 t.year, t.month, t.day, t.hour, t.minute, t.second 588 t.year,
589 t.month,
590 t.day,
591 t.hour,
592 t.minute,
593 t.second
376 ); 594 );
377 gop_println!("{}", line); 595 gop_println!("{}", line);
378 serial_println!("{}", line); 596 serial_println!("{}", line);