fold-rs 0.1.0

Minimal, dependency-free resolver for standard base directories
Documentation
#![allow(unused_variables, dead_code)]
use crate::{Error, Result};
use std::path::PathBuf;

pub(crate) struct Triple {
    pub var: Option<&'static str>,
    pub rel: Option<&'static str>,
    pub abs: Option<&'static str>,
}

impl Triple {
    pub(crate) fn resolve(&self) -> Option<PathBuf> {
        if let Some(v) = self.var
            && let Some(p) = resolve_env(v)
        {
            // Resolve variable
            Some(p)
        } else if let Some(rel) = self.rel
            && let Ok(home) = home()
        {
            // Fallback to relative path if HOME is found and one is given
            Some(home.join(rel))
        } else {
            self.abs.map(PathBuf::from)
        }
    }
}

pub(crate) fn resolve_env(var: &str) -> Option<PathBuf> {
    std::env::var_os(var)
        .filter(|v: &std::ffi::OsString| !v.is_empty() && std::path::Path::new(v).is_absolute())
        .map(PathBuf::from)
}

pub(crate) fn home() -> Result<PathBuf> {
    resolve_env("HOME")
        .or_else(|| resolve_env("USERPROFILE"))
        .ok_or(Error::NoHome)
}

pub(crate) struct Dir {
    pub(crate) xdg: Triple,
    pub(crate) mac: Triple,
    pub(crate) win: Triple,
}

impl Dir {
    pub(crate) fn resolve(&self) -> Option<PathBuf> {
        #[cfg(any(feature = "xdg", target_os = "linux"))]
        {
            self.xdg.resolve()
        }
        #[cfg(all(not(feature = "xdg"), target_os = "macos"))]
        {
            self.mac.resolve()
        }
        #[cfg(all(not(feature = "xdg"), target_os = "windows"))]
        {
            self.win.resolve()
        }
        #[cfg(all(
            not(feature = "xdg"),
            not(target_os = "linux"),
            not(target_os = "macos"),
            not(target_os = "windows")
        ))]
        {
            self.xdg.resolve()
        }
    }

    pub(crate) const fn new() -> Self {
        Self {
            xdg: Triple {
                var: None,
                rel: None,
                abs: None,
            },
            mac: Triple {
                var: None,
                rel: None,
                abs: None,
            },
            win: Triple {
                var: None,
                rel: None,
                abs: None,
            },
        }
    }

    pub(crate) const fn xdg_var(mut self, var: &'static str) -> Self {
        self.xdg.var = Some(var);
        self
    }

    pub(crate) const fn xdg_rel(mut self, rel: &'static str) -> Self {
        self.xdg.rel = Some(rel);
        self
    }

    pub(crate) const fn xdg_abs(mut self, abs: &'static str) -> Self {
        self.xdg.abs = Some(abs);
        self
    }

    pub(crate) const fn mac_var(mut self, var: &'static str) -> Self {
        self.mac.var = Some(var);
        self
    }

    pub(crate) const fn mac_rel(mut self, rel: &'static str) -> Self {
        self.mac.rel = Some(rel);
        self
    }

    pub(crate) const fn mac_abs(mut self, abs: &'static str) -> Self {
        self.mac.abs = Some(abs);
        self
    }

    pub(crate) const fn win_var(mut self, var: &'static str) -> Self {
        self.win.var = Some(var);
        self
    }

    pub(crate) const fn win_rel(mut self, rel: &'static str) -> Self {
        self.win.rel = Some(rel);
        self
    }

    pub(crate) const fn win_abs(mut self, abs: &'static str) -> Self {
        self.win.abs = Some(abs);
        self
    }
}

pub fn app_path(qualifier: &str, org: &str, app: &str) -> PathBuf {
    #[cfg(any(feature = "xdg", target_os = "linux"))]
    {
        PathBuf::from(app)
    }

    #[cfg(all(not(feature = "xdg"), target_os = "macos"))]
    {
        PathBuf::from(format!("{qualifier}.{org}.{app}"))
    }

    #[cfg(all(not(feature = "xdg"), target_os = "windows"))]
    {
        PathBuf::from(org).join(app)
    }

    #[cfg(all(
        not(feature = "xdg"),
        not(target_os = "linux"),
        not(target_os = "macos"),
        not(target_os = "windows"),
    ))]
    {
        PathBuf::from(app)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // All env-mutating assertions live in one test fn: std::env is process-global
    // and #[test] fns run in parallel, so splitting these would race.
    #[test]
    fn env_resolution() {
        let prev_home = std::env::var_os("HOME");
        let prev_userprofile = std::env::var_os("USERPROFILE");
        let prev_test_var = std::env::var_os("FOLD_TEST_VAR");

        // resolve_env cases
        unsafe { std::env::remove_var("FOLD_TEST_VAR") };
        assert_eq!(resolve_env("FOLD_TEST_VAR"), None);

        unsafe { std::env::set_var("FOLD_TEST_VAR", "") };
        assert_eq!(resolve_env("FOLD_TEST_VAR"), None);

        unsafe { std::env::set_var("FOLD_TEST_VAR", "relative/x") };
        assert_eq!(resolve_env("FOLD_TEST_VAR"), None);

        unsafe { std::env::set_var("FOLD_TEST_VAR", "/abs/x") };
        assert_eq!(resolve_env("FOLD_TEST_VAR"), Some(PathBuf::from("/abs/x")));

        // Triple::resolve: var set and absolute wins
        unsafe { std::env::set_var("FOLD_TEST_VAR", "/x") };
        let t = Triple {
            var: Some("FOLD_TEST_VAR"),
            rel: Some(".cfg"),
            abs: None,
        };
        assert_eq!(t.resolve(), Some(PathBuf::from("/x")));

        // Triple::resolve: var unset, rel used, HOME set
        unsafe { std::env::remove_var("FOLD_TEST_VAR") };
        unsafe { std::env::set_var("HOME", "/home/u") };
        let t = Triple {
            var: Some("FOLD_TEST_VAR"),
            rel: Some(".cfg"),
            abs: None,
        };
        assert_eq!(t.resolve(), Some(PathBuf::from("/home/u/.cfg")));

        // Triple::resolve: home missing, fall through to abs
        unsafe { std::env::remove_var("HOME") };
        unsafe { std::env::remove_var("USERPROFILE") };
        let t = Triple {
            var: None,
            rel: Some(".cfg"),
            abs: Some("/etc/x"),
        };
        assert_eq!(t.resolve(), Some(PathBuf::from("/etc/x")));

        // Triple::resolve: all none
        let t = Triple {
            var: None,
            rel: None,
            abs: None,
        };
        assert_eq!(t.resolve(), None);

        // restore prior env
        match prev_home {
            Some(v) => unsafe { std::env::set_var("HOME", v) },
            None => unsafe { std::env::remove_var("HOME") },
        }
        match prev_userprofile {
            Some(v) => unsafe { std::env::set_var("USERPROFILE", v) },
            None => unsafe { std::env::remove_var("USERPROFILE") },
        }
        match prev_test_var {
            Some(v) => unsafe { std::env::set_var("FOLD_TEST_VAR", v) },
            None => unsafe { std::env::remove_var("FOLD_TEST_VAR") },
        }
    }
}