Skip to main content

sim_config/
path.rs

1//! Safe path helpers for config roots and per-library files.
2
3use std::path::PathBuf;
4
5use sim_kernel::Symbol;
6use sim_table_core::is_legal_table_segment;
7
8use crate::{ConfigError, ConfigResult};
9
10/// Central and working config roots.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ConfigRoots {
13    /// Central user config root, if one can be derived from the environment.
14    pub home: Option<PathBuf>,
15    /// Working config root.
16    pub work: PathBuf,
17}
18
19impl ConfigRoots {
20    /// Creates roots from explicit values without reading the filesystem.
21    pub fn new(home: Option<PathBuf>, work: PathBuf) -> Self {
22        Self { home, work }
23    }
24
25    /// Derives roots from process environment variables and a work root.
26    pub fn from_env(work_root: PathBuf) -> Self {
27        Self::from_env_values(
28            work_root,
29            std::env::var_os("SIM_CONFIG_HOME").map(PathBuf::from),
30            std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from),
31            std::env::var_os("HOME").map(PathBuf::from),
32        )
33    }
34
35    /// Derives roots from provided environment values.
36    pub fn from_env_values(
37        work_root: PathBuf,
38        sim_config_home: Option<PathBuf>,
39        xdg_config_home: Option<PathBuf>,
40        home: Option<PathBuf>,
41    ) -> Self {
42        let home = sim_config_home
43            .or_else(|| xdg_config_home.map(|root| root.join("sim")))
44            .or_else(|| home.map(|root| root.join(".config").join("sim")));
45        Self {
46            home,
47            work: work_root.join(".sim").join("config"),
48        }
49    }
50}
51
52/// Returns the safe relative path for one library's per-lib config file.
53pub fn lib_config_path(lib: &Symbol) -> ConfigResult<PathBuf> {
54    let mut path = PathBuf::from("libs");
55    for segment in lib.as_qualified_str().split('/') {
56        if !safe_segment(segment) {
57            return Err(ConfigError::InvalidPathSegment {
58                segment: segment.to_owned(),
59            });
60        }
61        path.push(segment);
62    }
63    path.set_extension("toml");
64    Ok(path)
65}
66
67fn safe_segment(segment: &str) -> bool {
68    is_legal_table_segment(segment)
69        && segment
70            .chars()
71            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn lib_config_path_maps_lib_id_under_libs() {
80        let path = lib_config_path(&Symbol::qualified("sim", "cookbook")).unwrap();
81
82        assert_eq!(
83            path,
84            PathBuf::from("libs").join("sim").join("cookbook.toml")
85        );
86    }
87
88    #[test]
89    fn lib_config_path_rejects_unsafe_segments() {
90        let error = lib_config_path(&Symbol::new("..")).unwrap_err();
91
92        assert_eq!(
93            error,
94            ConfigError::InvalidPathSegment {
95                segment: "..".to_owned()
96            }
97        );
98    }
99
100    #[test]
101    fn roots_use_config_home_before_xdg_and_home() {
102        let roots = ConfigRoots::from_env_values(
103            PathBuf::from("/work"),
104            Some(PathBuf::from("/sim-config")),
105            Some(PathBuf::from("/xdg")),
106            Some(PathBuf::from("/user-home/alice")),
107        );
108
109        assert_eq!(roots.home, Some(PathBuf::from("/sim-config")));
110        assert_eq!(roots.work, PathBuf::from("/work/.sim/config"));
111    }
112}