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 long = args.iter().any(|a| a == "-l");
134    let path = args
135        .iter()
136        .find(|a| !a.starts_with('-'))
137        .map(|a| resolve_shell_path(a))
138        .unwrap_or_else(|| resolve_shell_path(""));
139
140    // Root special-case: show mount points (until overlays are implemented).
141    if path == "/" && !long {
142        shell_println!("Mount points:");
143        for m in vfs::list_mounts() {
144            shell_println!("  {}", m);
145        }
146        return Ok(());
147    }
148
149    match vfs::open(&path, OpenFlags::READ | OpenFlags::DIRECTORY) {
150        Ok(fd) => {
151            match vfs::getdents(fd) {
152                Ok(entries) => {
153                    if entries.is_empty() {
154                        shell_println!("(empty)");
155                    } else if long {
156                        for e in &entries {
157                            let full_path = if path.ends_with('/') {
158                                alloc::format!("{}{}", path, e.name)
159                            } else {
160                                alloc::format!("{}/{}", path, e.name)
161                            };
162                            let suffix = if e.file_type == DT_DIR { "/" } else { "" };
163                            match vfs::stat_path(&full_path) {
164                                Ok(st) => {
165                                    let mode_str = format_mode(st.st_mode);
166                                    shell_println!(
167                                        "{} {:>3} {:>4} {:>4} {:>8} {} {}{}",
168                                        mode_str,
169                                        st.st_nlink,
170                                        st.st_uid,
171                                        st.st_gid,
172                                        st.st_size,
173                                        format_mtime(st.st_mtime),
174                                        e.name,
175                                        suffix
176                                    );
177                                }
178                                Err(_) => {
179                                    let type_char = if e.file_type == DT_DIR { 'd' } else { '-' };
180                                    shell_println!("  {}{}", type_char, e.name);
181                                }
182                            }
183                        }
184                    } else {
185                        for e in &entries {
186                            let type_char = if e.file_type == DT_DIR { 'd' } else { '-' };
187                            shell_println!("  {}{}", type_char, e.name);
188                        }
189                    }
190                }
191                Err(_) => {
192                    let mut buf = [0u8; 4096];
193                    match vfs::read(fd, &mut buf) {
194                        Ok(n) if n > 0 => {
195                            let s = core::str::from_utf8(&buf[..n]).unwrap_or("(binary)");
196                            shell_println!("{}", s.trim_end());
197                        }
198                        _ => shell_println!("(empty)"),
199                    }
200                }
201            }
202            let _ = vfs::close(fd);
203        }
204        Err(e) => shell_println!("ls: {}: {:?}", path, e),
205    }
206
207    Ok(())
208}
209
210/// Format st_mode into a 10-char permission string like `drwxr-xr-x`.
211fn format_mode(mode: u32) -> alloc::string::String {
212    let ft = mode & 0o170000;
213    let type_char = match ft {
214        0o040000 => 'd',
215        0o120000 => 'l',
216        0o060000 => 'b',
217        0o020000 => 'c',
218        0o010000 => 'p',
219        0o140000 => 's',
220        _ => '-',
221    };
222    let perms = mode & 0o7777;
223    let mut buf = [0u8; 10];
224    buf[0] = type_char as u8;
225    // Owner
226    buf[1] = if perms & 0o400 != 0 { b'r' } else { b'-' };
227    buf[2] = if perms & 0o200 != 0 { b'w' } else { b'-' };
228    buf[3] = if perms & 0o100 != 0 {
229        if perms & 0o4000 != 0 {
230            b's'
231        } else {
232            b'x'
233        }
234    } else {
235        if perms & 0o4000 != 0 {
236            b'S'
237        } else {
238            b'-'
239        }
240    };
241    // Group
242    buf[4] = if perms & 0o40 != 0 { b'r' } else { b'-' };
243    buf[5] = if perms & 0o20 != 0 { b'w' } else { b'-' };
244    buf[6] = if perms & 0o10 != 0 {
245        if perms & 0o2000 != 0 {
246            b's'
247        } else {
248            b'x'
249        }
250    } else {
251        if perms & 0o2000 != 0 {
252            b'S'
253        } else {
254            b'-'
255        }
256    };
257    // Other
258    buf[7] = if perms & 0o4 != 0 { b'r' } else { b'-' };
259    buf[8] = if perms & 0o2 != 0 { b'w' } else { b'-' };
260    buf[9] = if perms & 0o1 != 0 {
261        if perms & 0o1000 != 0 {
262            b't'
263        } else {
264            b'x'
265        }
266    } else {
267        if perms & 0o1000 != 0 {
268            b'T'
269        } else {
270            b'-'
271        }
272    };
273    unsafe { alloc::string::String::from_utf8_unchecked(buf.to_vec()) }
274}
275
276/// Format mtime (seconds since epoch) into `Mon DD HH:MM`.
277fn format_mtime(mtime: strat9_abi::data::TimeSpec) -> alloc::string::String {
278    let secs = mtime.tv_sec as u64;
279    let days = secs / 86400;
280    let rem = secs % 86400;
281    let hours = rem / 3600;
282    let mins = (rem % 3600) / 60;
283
284    // Simple day-of-year calculation (no leap year handling : good enough)
285    let month_days: &[(u32, &str)] = &[
286        (31, "Jan"),
287        (28, "Feb"),
288        (31, "Mar"),
289        (30, "Apr"),
290        (31, "May"),
291        (30, "Jun"),
292        (31, "Jul"),
293        (31, "Aug"),
294        (30, "Sep"),
295        (31, "Oct"),
296        (30, "Nov"),
297        (31, "Dec"),
298    ];
299    let mut day_of_year = (days % 365) as u32;
300    let mut month_idx = 0;
301    for &(days_in_month, _) in month_days {
302        if day_of_year < days_in_month {
303            break;
304        }
305        day_of_year -= days_in_month;
306        month_idx += 1;
307    }
308    if month_idx >= 12 {
309        month_idx = 11;
310        day_of_year = 30;
311    }
312    let month_name = month_days[month_idx].1;
313
314    alloc::format!(
315        "{} {:>2} {:02}:{:02}",
316        month_name,
317        day_of_year + 1,
318        hours,
319        mins
320    )
321}
322
323// ========== cat ==================================================
324
325/// Display file contents.
326/// Display file contents or piped input.
327///
328/// When invoked without arguments and pipe input is available,
329/// prints the piped data. Otherwise reads from the specified path.
330pub(super) fn cmd_cat_impl(args: &[String]) -> Result<(), ShellError> {
331    if let Some(piped) = crate::shell::output::take_pipe_input() {
332        if args.is_empty() {
333            let s = core::str::from_utf8(&piped).unwrap_or("(non-UTF8 data)");
334            crate::shell_print!("{}", s);
335            if !s.ends_with('\n') {
336                shell_println!();
337            }
338            return Ok(());
339        }
340    }
341
342    if args.is_empty() {
343        shell_println!("Usage: cat <path>");
344        return Ok(());
345    }
346
347    let path = resolve_shell_path(&args[0]);
348    match vfs::open(&path, OpenFlags::READ) {
349        Ok(fd) => {
350            let mut buf = [0u8; 1024];
351            loop {
352                match vfs::read(fd, &mut buf) {
353                    Ok(0) => break,
354                    Ok(n) => {
355                        let s = core::str::from_utf8(&buf[..n]).unwrap_or("(non-UTF8 data)");
356                        crate::shell_print!("{}", s);
357                    }
358                    Err(e) => {
359                        shell_println!("\nError reading file: {:?}", e);
360                        break;
361                    }
362                }
363            }
364            shell_println!("");
365            let _ = vfs::close(fd);
366        }
367        Err(e) => shell_println!("cat: {}: {:?}", path, e),
368    }
369    Ok(())
370}
371
372// ========== scheme ========================================
373
374/// List registered schemes.
375pub(super) fn cmd_scheme_impl(args: &[String]) -> Result<(), ShellError> {
376    if args.is_empty() || args[0] != "ls" {
377        shell_println!("Usage: scheme ls");
378        return Ok(());
379    }
380
381    shell_println!("Registered schemes:");
382    shell_println!("{:<14} {}", "Name", "Type");
383    shell_println!("======================================================");
384    for scheme in vfs::list_schemes() {
385        shell_println!("  {:<12} Kernel/IPC", scheme);
386    }
387    shell_println!("");
388    Ok(())
389}
390
391/// Performs the cmd mount operation.
392pub(super) fn cmd_mount_impl(args: &[String]) -> Result<(), ShellError> {
393    if args.is_empty() || args[0] == "ls" {
394        shell_println!("Mount points:");
395        for m in vfs::list_mounts() {
396            shell_println!("  {}", m);
397        }
398        shell_println!("");
399        shell_println!("Usage: mount <source> <target>");
400        return Ok(());
401    }
402    if args.len() != 2 {
403        shell_println!("Usage: mount <source> <target>");
404        return Ok(());
405    }
406
407    let source = resolve_shell_path(&args[0]);
408    let target = resolve_shell_path(&args[1]);
409
410    let (scheme, rel) = match vfs::resolve(&source) {
411        Ok(v) => v,
412        Err(e) => {
413            shell_println!("mount: source {} unavailable: {:?}", source, e);
414            return Ok(());
415        }
416    };
417    if !rel.is_empty() {
418        shell_println!("mount: source must be a mount root: {}", source);
419        return Ok(());
420    }
421
422    match vfs::mount(&target, scheme) {
423        Ok(()) => shell_println!("mount: {} mounted on {}", source, target),
424        Err(e) => shell_println!("mount: {} -> {} failed: {:?}", source, target, e),
425    }
426    Ok(())
427}
428
429/// Performs the cmd umount operation.
430pub(super) fn cmd_umount_impl(args: &[String]) -> Result<(), ShellError> {
431    if args.len() != 1 {
432        shell_println!("Usage: umount <target>");
433        return Ok(());
434    }
435
436    let target = resolve_shell_path(&args[0]);
437    match vfs::unmount(&target) {
438        Ok(()) => shell_println!("umount: {}", target),
439        Err(e) => shell_println!("umount: {}: {:?}", target, e),
440    }
441    Ok(())
442}
443
444// ========== mkdir ==================================================
445
446/// Create a new directory.
447pub(super) fn cmd_mkdir_impl(args: &[String]) -> Result<(), ShellError> {
448    if args.is_empty() {
449        shell_println!("Usage: mkdir <path>");
450        return Ok(());
451    }
452    let path = resolve_shell_path(&args[0]);
453    match vfs::mkdir(&path, 0o755) {
454        Ok(()) => shell_println!("mkdir: {}", path),
455        Err(e) => shell_println!("mkdir: {}: {:?}", path, e),
456    }
457    Ok(())
458}
459
460// ========== touch ==================================================
461
462/// Create a new empty file.
463pub(super) fn cmd_touch_impl(args: &[String]) -> Result<(), ShellError> {
464    if args.is_empty() {
465        shell_println!("Usage: touch <path>");
466        return Ok(());
467    }
468    let path = resolve_shell_path(&args[0]);
469    match vfs::create_file(&path, 0o644) {
470        Ok(()) => shell_println!("touch: {}", path),
471        Err(e) => shell_println!("touch: {}: {:?}", path, e),
472    }
473    Ok(())
474}
475
476// ========== rm ============================================================
477
478/// Remove a file or directory.
479pub(super) fn cmd_rm_impl(args: &[String]) -> Result<(), ShellError> {
480    if args.is_empty() {
481        shell_println!("Usage: rm <path>");
482        return Ok(());
483    }
484    let path = resolve_shell_path(&args[0]);
485    match vfs::unlink(&path) {
486        Ok(()) => shell_println!("rm: {}", path),
487        Err(e) => shell_println!("rm: {}: {:?}", path, e),
488    }
489    Ok(())
490}
491
492// ========== write ==================================================
493
494pub(super) fn cmd_write_impl(args: &[String]) -> Result<(), ShellError> {
495    if args.len() < 2 {
496        shell_println!("Usage: write <path> <text>");
497        return Ok(());
498    }
499    let path = resolve_shell_path(&args[0]);
500    let text = &args[1];
501
502    match vfs::open(&path, OpenFlags::WRITE | OpenFlags::CREATE) {
503        Ok(fd) => {
504            match vfs::write(fd, text.as_bytes()) {
505                Ok(n) => shell_println!("write: {} bytes -> {}", n, path),
506                Err(e) => shell_println!("write: {}: {:?}", path, e),
507            }
508            let _ = vfs::close(fd);
509        }
510        Err(e) => shell_println!("write: {}: {:?}", path, e),
511    }
512    Ok(())
513}
514
515// ========== stat ==================================================
516
517pub(super) fn cmd_stat_impl(args: &[String]) -> Result<(), ShellError> {
518    if args.is_empty() {
519        shell_println!("Usage: stat <path>");
520        return Err(ShellError::InvalidArguments);
521    }
522    let path = resolve_shell_path(&args[0]);
523    match vfs::stat_path(&path) {
524        Ok(st) => {
525            let ftype = match st.st_mode & 0xF000 {
526                0x4000 => "directory",
527                0x8000 => "regular file",
528                0xA000 => "symbolic link",
529                0x1000 => "FIFO",
530                0x6000 => "block device",
531                0x2000 => "character device",
532                _ => "unknown",
533            };
534            shell_println!("  File: {}", path);
535            shell_println!("  Type: {}", ftype);
536            shell_println!("  Size: {} bytes", st.st_size);
537            shell_println!("  Mode: {:04o}", st.st_mode & 0o7777);
538            shell_println!("  Links: {}", st.st_nlink);
539            shell_println!("  Inode: {}", st.st_ino);
540        }
541        Err(e) => shell_println!("stat: {}: {:?}", path, e),
542    }
543    Ok(())
544}
545
546// ========== cp ==================================================
547
548pub(super) fn cmd_cp_impl(args: &[String]) -> Result<(), ShellError> {
549    if args.len() < 2 {
550        shell_println!("Usage: cp <src> <dst>");
551        return Err(ShellError::InvalidArguments);
552    }
553    let src = resolve_shell_path(&args[0]);
554    let dst = resolve_shell_path(&args[1]);
555
556    let fd_src = vfs::open(&src, OpenFlags::READ).map_err(|e| {
557        shell_println!("cp: cannot open '{}': {:?}", src, e);
558        ShellError::ExecutionFailed
559    })?;
560    let data = match vfs::read_all(fd_src) {
561        Ok(d) => d,
562        Err(e) => {
563            let _ = vfs::close(fd_src);
564            shell_println!("cp: cannot read '{}': {:?}", src, e);
565            return Err(ShellError::ExecutionFailed);
566        }
567    };
568    let _ = vfs::close(fd_src);
569
570    let fd_dst = vfs::open(
571        &dst,
572        OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
573    )
574    .map_err(|e| {
575        shell_println!("cp: cannot create '{}': {:?}", dst, e);
576        ShellError::ExecutionFailed
577    })?;
578    match vfs::write(fd_dst, &data) {
579        Ok(n) => shell_println!("cp: {} -> {} ({} bytes)", src, dst, n),
580        Err(e) => shell_println!("cp: write to '{}': {:?}", dst, e),
581    }
582    let _ = vfs::close(fd_dst);
583    Ok(())
584}
585
586// ========== mv ==================================================
587
588pub(super) fn cmd_mv_impl(args: &[String]) -> Result<(), ShellError> {
589    if args.len() < 2 {
590        shell_println!("Usage: mv <src> <dst>");
591        return Err(ShellError::InvalidArguments);
592    }
593    let src = resolve_shell_path(&args[0]);
594    let dst = resolve_shell_path(&args[1]);
595
596    match vfs::rename(&src, &dst) {
597        Ok(()) => {
598            shell_println!("mv: {} -> {}", src, dst);
599            return Ok(());
600        }
601        Err(crate::syscall::error::SyscallError::NotSupported) => {
602            // Cross-mount: fallback to cp + rm
603        }
604        Err(e) => {
605            shell_println!("mv: {:?}", e);
606            return Err(ShellError::ExecutionFailed);
607        }
608    }
609
610    cmd_cp(args)?;
611    match vfs::unlink(&src) {
612        Ok(()) => shell_println!("mv: removed {}", src),
613        Err(e) => shell_println!("mv: could not remove source '{}': {:?}", src, e),
614    }
615    Ok(())
616}
617
618// ========== df ==================================================
619
620pub(super) fn cmd_df_impl(_args: &[String]) -> Result<(), ShellError> {
621    let mounts = vfs::list_mounts();
622    shell_println!("{:<20} {}", "Mount", "Status");
623    shell_println!("=======================================================================================================");
624    for m in &mounts {
625        let status = if vfs::open(m, OpenFlags::READ | OpenFlags::DIRECTORY)
626            .map(|fd| {
627                let _ = vfs::close(fd);
628            })
629            .is_ok()
630        {
631            "accessible"
632        } else {
633            "unavailable"
634        };
635        shell_println!("{:<20} {}", m, status);
636    }
637    shell_println!("{} mount(s)", mounts.len());
638    Ok(())
639}