Skip to main content

kernel/
fs.rs

1//! Small filesystem-path helpers shared across the workspace.
2
3use std::path::{Path, PathBuf};
4
5/// Expand a leading `~` or `~/` in `path` against `home`. A path that does not
6/// begin with a tilde segment is returned unchanged.
7pub fn expand_tilde(path: &str, home: &Path) -> PathBuf {
8    if path == "~" {
9        home.to_path_buf()
10    } else if let Some(rest) = path.strip_prefix("~/") {
11        home.join(rest)
12    } else {
13        PathBuf::from(path)
14    }
15}
16
17/// Expand a leading `~`/`~/` in `path` against `$HOME`, read from the process
18/// environment. A path without a tilde segment — or when `$HOME` is unset —
19/// passes through unchanged.
20pub fn expand_tilde_env(path: &str) -> PathBuf {
21    match std::env::var("HOME") {
22        Ok(home) => expand_tilde(path, Path::new(&home)),
23        Err(_) => PathBuf::from(path),
24    }
25}
26
27/// Whether `path` is an existing regular file the process may execute (on
28/// non-Unix, merely an existing file — there is no mode bit to check).
29pub fn is_executable(path: &Path) -> bool {
30    #[cfg(unix)]
31    {
32        use std::os::unix::fs::PermissionsExt;
33        std::fs::metadata(path)
34            .map(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
35            .unwrap_or(false)
36    }
37    #[cfg(not(unix))]
38    {
39        path.is_file()
40    }
41}
42
43/// Find `binary` on the process `PATH`, returning the first executable match.
44pub fn find_on_path(binary: &str) -> Option<PathBuf> {
45    let path = std::env::var_os("PATH")?;
46    std::env::split_paths(&path)
47        .map(|dir| dir.join(binary))
48        .find(|candidate| is_executable(candidate))
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn a_tilde_expands_against_home() {
57        let home = Path::new("/home/koala");
58        assert_eq!(expand_tilde("~", home), PathBuf::from("/home/koala"));
59        assert_eq!(
60            expand_tilde("~/models/a.gguf", home),
61            PathBuf::from("/home/koala/models/a.gguf")
62        );
63    }
64
65    #[test]
66    fn a_trailing_slash_on_home_is_normalized() {
67        assert_eq!(
68            expand_tilde("~/x", Path::new("/home/koala/")),
69            PathBuf::from("/home/koala/x")
70        );
71    }
72
73    #[test]
74    fn a_non_tilde_path_is_unchanged() {
75        let home = Path::new("/home/koala");
76        assert_eq!(expand_tilde("/abs/path", home), PathBuf::from("/abs/path"));
77        assert_eq!(expand_tilde("relative", home), PathBuf::from("relative"));
78        // A tilde not at the start is not a home reference.
79        assert_eq!(expand_tilde("a/~/b", home), PathBuf::from("a/~/b"));
80    }
81}