runmat_runtime/builtins/common/
fs.rs

1//! Filesystem helper utilities shared across REPL-facing builtins.
2//!
3//! These helpers centralize path normalization, wildcard detection,
4//! and platform-aware sorting so builtins such as `ls` and `dir`
5//! can focus on their user-facing semantics.
6
7use std::cmp::Ordering;
8use std::env;
9use std::path::{Path, PathBuf};
10
11/// Expand a user-relative path (e.g., `~` or `~/Documents`) into an absolute
12/// filesystem path string. Returns the original string when no expansion is
13/// required.
14pub fn expand_user_path(raw: &str, error_prefix: &str) -> Result<String, String> {
15    if raw == "~" {
16        return home_directory()
17            .map(|path| path_to_string(&path))
18            .ok_or_else(|| format!("{error_prefix}: unable to resolve home directory"));
19    }
20
21    if let Some(stripped) = raw.strip_prefix("~/").or_else(|| raw.strip_prefix("~\\")) {
22        let home = home_directory()
23            .ok_or_else(|| format!("{error_prefix}: unable to resolve home directory"))?;
24        let mut buf = home;
25        if !stripped.is_empty() {
26            buf.push(stripped);
27        }
28        return Ok(path_to_string(&buf));
29    }
30
31    Ok(raw.to_string())
32}
33
34/// Return the user's home directory if it can be determined.
35pub fn home_directory() -> Option<PathBuf> {
36    #[cfg(windows)]
37    {
38        if let Ok(user_profile) = env::var("USERPROFILE") {
39            return Some(PathBuf::from(user_profile));
40        }
41        if let (Ok(drive), Ok(path)) = (env::var("HOMEDRIVE"), env::var("HOMEPATH")) {
42            return Some(PathBuf::from(format!("{drive}{path}")));
43        }
44        None
45    }
46    #[cfg(not(windows))]
47    {
48        env::var("HOME").map(PathBuf::from).ok()
49    }
50}
51
52/// Convert a path into an owned string using lossless conversion semantics.
53pub fn path_to_string(path: &Path) -> String {
54    path.to_string_lossy().into_owned()
55}
56
57/// Return `true` when the text contains glob-style wildcards (`*` or `?`).
58pub fn contains_wildcards(text: &str) -> bool {
59    text.contains('*') || text.contains('?')
60}
61
62/// Sort entries in-place using MATLAB-compatible ordering.
63pub fn sort_entries(entries: &mut [String]) {
64    entries.sort_by(|a, b| compare_names(a, b));
65}
66
67/// Compare two file names using case-insensitive ordering on Windows and
68/// case-sensitive ordering elsewhere, matching MATLAB's behaviour.
69pub fn compare_names(a: &str, b: &str) -> Ordering {
70    #[cfg(windows)]
71    {
72        let lower_a = a.to_ascii_lowercase();
73        let lower_b = b.to_ascii_lowercase();
74        match lower_a.cmp(&lower_b) {
75            Ordering::Equal => a.cmp(b),
76            other => other,
77        }
78    }
79    #[cfg(not(windows))]
80    {
81        a.cmp(b)
82    }
83}