Skip to main content

strat9_kernel/shell/commands/vfs/
mod.rs

1//! VFS management commands
2mod cat;
3mod cd;
4mod cp;
5mod df;
6mod ls;
7mod mkdir;
8mod mount;
9mod mv;
10mod rm;
11mod scheme;
12mod stat;
13mod touch;
14mod umount;
15mod write;
16
17use crate::{
18    shell::ShellError,
19    shell_println,
20    vfs::{self, scheme::DT_DIR, OpenFlags},
21};
22use alloc::{string::String, vec::Vec};
23use spin::Lazy;
24
25pub use cat::cmd_cat;
26pub use cd::cmd_cd;
27pub use cp::cmd_cp;
28pub use df::cmd_df;
29pub use ls::cmd_ls;
30pub use mkdir::cmd_mkdir;
31pub use mount::cmd_mount;
32pub use mv::cmd_mv;
33pub use rm::cmd_rm;
34pub use scheme::cmd_scheme;
35pub use stat::cmd_stat;
36pub use touch::cmd_touch;
37pub use umount::cmd_umount;
38pub use write::cmd_write;
39
40// ─── Shell CWD ───────────────────────────────────────────────────────────────
41
42/// Current working directory for the chevron shell.
43///
44/// Stored as a global because `CommandRegistry` uses plain function pointers
45/// (no `&mut self`) so state cannot be threaded through call arguments.
46static SHELL_CWD: Lazy<crate::sync::SpinLock<String>> =
47    Lazy::new(|| crate::sync::SpinLock::new(String::from("/")));
48
49/// Read the current working directory.
50pub fn get_cwd() -> String {
51    SHELL_CWD.lock().clone()
52}
53
54/// Sets cwd.
55fn set_cwd(path: String) {
56    *SHELL_CWD.lock() = path;
57}
58
59/// Collapse `..`, `.` and redundant `/` in an absolute path.
60fn normalize_path(path: &str) -> String {
61    let mut parts: Vec<&str> = Vec::new();
62    for component in path.split('/') {
63        match component {
64            "" | "." => {}
65            ".." => {
66                let _ = parts.pop();
67            }
68            other => parts.push(other),
69        }
70    }
71    if parts.is_empty() {
72        return String::from("/");
73    }
74    let mut result = String::from("/");
75    for (i, part) in parts.iter().enumerate() {
76        if i > 0 {
77            result.push('/');
78        }
79        result.push_str(part);
80    }
81    result
82}
83
84/// Resolve `path` relative to the shell CWD.
85///
86/// - Empty or `""` → current directory.
87/// - Starts with `/` → treated as absolute (normalized).
88/// - Otherwise → joined with CWD and normalized.
89pub fn resolve_shell_path(path: &str) -> String {
90    if path.is_empty() {
91        return get_cwd();
92    }
93    if path.starts_with('/') {
94        return normalize_path(path);
95    }
96    let cwd = get_cwd();
97    let combined = if cwd.ends_with('/') {
98        alloc::format!("{}{}", cwd, path)
99    } else {
100        alloc::format!("{}/{}", cwd, path)
101    };
102    normalize_path(&combined)
103}
104
105// ─── cd ──────────────────────────────────────────────────────────────────────
106
107/// Change the shell working directory.
108pub(super) fn cmd_cd_impl(args: &[String]) -> Result<(), ShellError> {
109    let target = if args.is_empty() {
110        String::from("/")
111    } else {
112        resolve_shell_path(&args[0])
113    };
114
115    // Verify the path exists and is a directory.
116    match vfs::open(&target, OpenFlags::READ | OpenFlags::DIRECTORY) {
117        Ok(fd) => {
118            let _ = vfs::close(fd);
119            set_cwd(target);
120        }
121        Err(e) => {
122            let arg = args.first().map(|s| s.as_str()).unwrap_or("/");
123            shell_println!("cd: {}: {:?}", arg, e);
124        }
125    }
126    Ok(())
127}
128
129// ─── ls ──────────────────────────────────────────────────────────────────────
130
131/// List directory contents or mount points.
132pub(super) fn cmd_ls_impl(args: &[String]) -> Result<(), ShellError> {
133    let path = if args.is_empty() {
134        resolve_shell_path("")
135    } else {
136        resolve_shell_path(&args[0])
137    };
138
139    // Root special-case: show mount points (until overlays are implemented).
140    if path == "/" {
141        shell_println!("Mount points:");
142        for m in vfs::list_mounts() {
143            shell_println!("  {}", m);
144        }
145        return Ok(());
146    }
147
148    match vfs::open(&path, OpenFlags::READ | OpenFlags::DIRECTORY) {
149        Ok(fd) => {
150            // Prefer getdents (scheme-neutral, works with ramfs, devfs, procfs…)
151            match vfs::getdents(fd) {
152                Ok(entries) => {
153                    if entries.is_empty() {
154                        shell_println!("(empty)");
155                    } else {
156                        for e in &entries {
157                            let type_char = if e.file_type == DT_DIR { 'd' } else { '-' };
158                            shell_println!("  {}{}", type_char, e.name);
159                        }
160                    }
161                }
162                Err(_) => {
163                    // Fallback for schemes that implement read-as-listing.
164                    let mut buf = [0u8; 4096];
165                    match vfs::read(fd, &mut buf) {
166                        Ok(n) if n > 0 => {
167                            let s = core::str::from_utf8(&buf[..n]).unwrap_or("(binary)");
168                            shell_println!("{}", s.trim_end());
169                        }
170                        _ => shell_println!("(empty)"),
171                    }
172                }
173            }
174            let _ = vfs::close(fd);
175        }
176        Err(e) => shell_println!("ls: {}: {:?}", path, e),
177    }
178
179    Ok(())
180}
181
182// ─── cat ─────────────────────────────────────────────────────────────────────
183
184/// Display file contents.
185/// Display file contents or piped input.
186///
187/// When invoked without arguments and pipe input is available,
188/// prints the piped data. Otherwise reads from the specified path.
189pub(super) fn cmd_cat_impl(args: &[String]) -> Result<(), ShellError> {
190    if let Some(piped) = crate::shell::output::take_pipe_input() {
191        if args.is_empty() {
192            let s = core::str::from_utf8(&piped).unwrap_or("(non-UTF8 data)");
193            crate::shell_print!("{}", s);
194            if !s.ends_with('\n') {
195                shell_println!();
196            }
197            return Ok(());
198        }
199    }
200
201    if args.is_empty() {
202        shell_println!("Usage: cat <path>");
203        return Ok(());
204    }
205
206    let path = resolve_shell_path(&args[0]);
207    match vfs::open(&path, OpenFlags::READ) {
208        Ok(fd) => {
209            let mut buf = [0u8; 1024];
210            loop {
211                match vfs::read(fd, &mut buf) {
212                    Ok(0) => break,
213                    Ok(n) => {
214                        let s = core::str::from_utf8(&buf[..n]).unwrap_or("(non-UTF8 data)");
215                        crate::shell_print!("{}", s);
216                    }
217                    Err(e) => {
218                        shell_println!("\nError reading file: {:?}", e);
219                        break;
220                    }
221                }
222            }
223            shell_println!("");
224            let _ = vfs::close(fd);
225        }
226        Err(e) => shell_println!("cat: {}: {:?}", path, e),
227    }
228    Ok(())
229}
230
231// ─── scheme ──────────────────────────────────────────────────────────────────
232
233/// List registered schemes.
234pub(super) fn cmd_scheme_impl(args: &[String]) -> Result<(), ShellError> {
235    if args.is_empty() || args[0] != "ls" {
236        shell_println!("Usage: scheme ls");
237        return Ok(());
238    }
239
240    shell_println!("Registered schemes:");
241    shell_println!("{:<14} {}", "Name", "Type");
242    shell_println!("────────────────────────────────────");
243    for scheme in vfs::list_schemes() {
244        shell_println!("  {:<12} Kernel/IPC", scheme);
245    }
246    shell_println!("");
247    Ok(())
248}
249
250/// Performs the cmd mount operation.
251pub(super) fn cmd_mount_impl(args: &[String]) -> Result<(), ShellError> {
252    if args.is_empty() || args[0] == "ls" {
253        shell_println!("Mount points:");
254        for m in vfs::list_mounts() {
255            shell_println!("  {}", m);
256        }
257        shell_println!("");
258        shell_println!("Usage: mount <source> <target>");
259        return Ok(());
260    }
261    if args.len() != 2 {
262        shell_println!("Usage: mount <source> <target>");
263        return Ok(());
264    }
265
266    let source = resolve_shell_path(&args[0]);
267    let target = resolve_shell_path(&args[1]);
268
269    let (scheme, rel) = match vfs::resolve(&source) {
270        Ok(v) => v,
271        Err(e) => {
272            shell_println!("mount: source {} unavailable: {:?}", source, e);
273            return Ok(());
274        }
275    };
276    if !rel.is_empty() {
277        shell_println!("mount: source must be a mount root: {}", source);
278        return Ok(());
279    }
280
281    match vfs::mount(&target, scheme) {
282        Ok(()) => shell_println!("mount: {} mounted on {}", source, target),
283        Err(e) => shell_println!("mount: {} -> {} failed: {:?}", source, target, e),
284    }
285    Ok(())
286}
287
288/// Performs the cmd umount operation.
289pub(super) fn cmd_umount_impl(args: &[String]) -> Result<(), ShellError> {
290    if args.len() != 1 {
291        shell_println!("Usage: umount <target>");
292        return Ok(());
293    }
294
295    let target = resolve_shell_path(&args[0]);
296    match vfs::unmount(&target) {
297        Ok(()) => shell_println!("umount: {}", target),
298        Err(e) => shell_println!("umount: {}: {:?}", target, e),
299    }
300    Ok(())
301}
302
303// ─── mkdir ───────────────────────────────────────────────────────────────────
304
305/// Create a new directory.
306pub(super) fn cmd_mkdir_impl(args: &[String]) -> Result<(), ShellError> {
307    if args.is_empty() {
308        shell_println!("Usage: mkdir <path>");
309        return Ok(());
310    }
311    let path = resolve_shell_path(&args[0]);
312    match vfs::mkdir(&path, 0o755) {
313        Ok(()) => shell_println!("mkdir: {}", path),
314        Err(e) => shell_println!("mkdir: {}: {:?}", path, e),
315    }
316    Ok(())
317}
318
319// ─── touch ───────────────────────────────────────────────────────────────────
320
321/// Create a new empty file.
322pub(super) fn cmd_touch_impl(args: &[String]) -> Result<(), ShellError> {
323    if args.is_empty() {
324        shell_println!("Usage: touch <path>");
325        return Ok(());
326    }
327    let path = resolve_shell_path(&args[0]);
328    match vfs::create_file(&path, 0o644) {
329        Ok(()) => shell_println!("touch: {}", path),
330        Err(e) => shell_println!("touch: {}: {:?}", path, e),
331    }
332    Ok(())
333}
334
335// ─── rm ──────────────────────────────────────────────────────────────────────
336
337/// Remove a file or directory.
338pub(super) fn cmd_rm_impl(args: &[String]) -> Result<(), ShellError> {
339    if args.is_empty() {
340        shell_println!("Usage: rm <path>");
341        return Ok(());
342    }
343    let path = resolve_shell_path(&args[0]);
344    match vfs::unlink(&path) {
345        Ok(()) => shell_println!("rm: {}", path),
346        Err(e) => shell_println!("rm: {}: {:?}", path, e),
347    }
348    Ok(())
349}
350
351// ─── write ───────────────────────────────────────────────────────────────────
352
353pub(super) fn cmd_write_impl(args: &[String]) -> Result<(), ShellError> {
354    if args.len() < 2 {
355        shell_println!("Usage: write <path> <text>");
356        return Ok(());
357    }
358    let path = resolve_shell_path(&args[0]);
359    let text = &args[1];
360
361    match vfs::open(&path, OpenFlags::WRITE | OpenFlags::CREATE) {
362        Ok(fd) => {
363            match vfs::write(fd, text.as_bytes()) {
364                Ok(n) => shell_println!("write: {} bytes -> {}", n, path),
365                Err(e) => shell_println!("write: {}: {:?}", path, e),
366            }
367            let _ = vfs::close(fd);
368        }
369        Err(e) => shell_println!("write: {}: {:?}", path, e),
370    }
371    Ok(())
372}
373
374// ─── stat ───────────────────────────────────────────────────────────────────
375
376pub(super) fn cmd_stat_impl(args: &[String]) -> Result<(), ShellError> {
377    if args.is_empty() {
378        shell_println!("Usage: stat <path>");
379        return Err(ShellError::InvalidArguments);
380    }
381    let path = resolve_shell_path(&args[0]);
382    match vfs::stat_path(&path) {
383        Ok(st) => {
384            let ftype = match st.st_mode & 0xF000 {
385                0x4000 => "directory",
386                0x8000 => "regular file",
387                0xA000 => "symbolic link",
388                0x1000 => "FIFO",
389                0x6000 => "block device",
390                0x2000 => "character device",
391                _ => "unknown",
392            };
393            shell_println!("  File: {}", path);
394            shell_println!("  Type: {}", ftype);
395            shell_println!("  Size: {} bytes", st.st_size);
396            shell_println!("  Mode: {:04o}", st.st_mode & 0o7777);
397            shell_println!("  Links: {}", st.st_nlink);
398            shell_println!("  Inode: {}", st.st_ino);
399        }
400        Err(e) => shell_println!("stat: {}: {:?}", path, e),
401    }
402    Ok(())
403}
404
405// ─── cp ─────────────────────────────────────────────────────────────────────
406
407pub(super) fn cmd_cp_impl(args: &[String]) -> Result<(), ShellError> {
408    if args.len() < 2 {
409        shell_println!("Usage: cp <src> <dst>");
410        return Err(ShellError::InvalidArguments);
411    }
412    let src = resolve_shell_path(&args[0]);
413    let dst = resolve_shell_path(&args[1]);
414
415    let fd_src = vfs::open(&src, OpenFlags::READ).map_err(|e| {
416        shell_println!("cp: cannot open '{}': {:?}", src, e);
417        ShellError::ExecutionFailed
418    })?;
419    let data = match vfs::read_all(fd_src) {
420        Ok(d) => d,
421        Err(e) => {
422            let _ = vfs::close(fd_src);
423            shell_println!("cp: cannot read '{}': {:?}", src, e);
424            return Err(ShellError::ExecutionFailed);
425        }
426    };
427    let _ = vfs::close(fd_src);
428
429    let fd_dst = vfs::open(
430        &dst,
431        OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
432    )
433    .map_err(|e| {
434        shell_println!("cp: cannot create '{}': {:?}", dst, e);
435        ShellError::ExecutionFailed
436    })?;
437    match vfs::write(fd_dst, &data) {
438        Ok(n) => shell_println!("cp: {} -> {} ({} bytes)", src, dst, n),
439        Err(e) => shell_println!("cp: write to '{}': {:?}", dst, e),
440    }
441    let _ = vfs::close(fd_dst);
442    Ok(())
443}
444
445// ─── mv ─────────────────────────────────────────────────────────────────────
446
447pub(super) fn cmd_mv_impl(args: &[String]) -> Result<(), ShellError> {
448    if args.len() < 2 {
449        shell_println!("Usage: mv <src> <dst>");
450        return Err(ShellError::InvalidArguments);
451    }
452    let src = resolve_shell_path(&args[0]);
453    let dst = resolve_shell_path(&args[1]);
454
455    match vfs::rename(&src, &dst) {
456        Ok(()) => {
457            shell_println!("mv: {} -> {}", src, dst);
458            return Ok(());
459        }
460        Err(crate::syscall::error::SyscallError::NotSupported) => {
461            // Cross-mount: fallback to cp + rm
462        }
463        Err(e) => {
464            shell_println!("mv: {:?}", e);
465            return Err(ShellError::ExecutionFailed);
466        }
467    }
468
469    cmd_cp(args)?;
470    match vfs::unlink(&src) {
471        Ok(()) => shell_println!("mv: removed {}", src),
472        Err(e) => shell_println!("mv: could not remove source '{}': {:?}", src, e),
473    }
474    Ok(())
475}
476
477// ─── df ─────────────────────────────────────────────────────────────────────
478
479pub(super) fn cmd_df_impl(_args: &[String]) -> Result<(), ShellError> {
480    let mounts = vfs::list_mounts();
481    shell_println!("{:<20} {}", "Mount", "Status");
482    shell_println!("────────────────────────────────────────");
483    for m in &mounts {
484        let status = if vfs::open(m, OpenFlags::READ | OpenFlags::DIRECTORY)
485            .map(|fd| {
486                let _ = vfs::close(fd);
487            })
488            .is_ok()
489        {
490            "accessible"
491        } else {
492            "unavailable"
493        };
494        shell_println!("{:<20} {}", m, status);
495    }
496    shell_println!("{} mount(s)", mounts.len());
497    Ok(())
498}