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    /// Explicit central user config root, when supplied by the host.
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 provided environment values.
26    pub fn from_env_values(
27        work_root: PathBuf,
28        sim_config_home: Option<PathBuf>,
29        xdg_config_home: Option<PathBuf>,
30        home: Option<PathBuf>,
31    ) -> Self {
32        let home = sim_config_home
33            .or_else(|| xdg_config_home.map(|root| root.join("sim")))
34            .or_else(|| home.map(|root| root.join(".config").join("sim")));
35        Self {
36            home,
37            work: work_root.join(".sim").join("config"),
38        }
39    }
40}
41
42/// Returns the safe relative path for one library's per-lib config file.
43pub fn lib_config_path(lib: &Symbol) -> ConfigResult<PathBuf> {
44    let mut path = PathBuf::from("libs");
45    for segment in lib.as_qualified_str().split('/') {
46        if !safe_segment(segment) {
47            return Err(ConfigError::InvalidPathSegment {
48                segment: segment.to_owned(),
49            });
50        }
51        path.push(segment);
52    }
53    path.set_extension("toml");
54    Ok(path)
55}
56
57fn safe_segment(segment: &str) -> bool {
58    is_legal_table_segment(segment)
59        && segment
60            .chars()
61            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn lib_config_path_maps_lib_id_under_libs() {
70        let path = lib_config_path(&Symbol::qualified("sim", "cookbook")).unwrap();
71
72        assert_eq!(
73            path,
74            PathBuf::from("libs").join("sim").join("cookbook.toml")
75        );
76    }
77
78    #[test]
79    fn lib_config_path_rejects_unsafe_segments() {
80        let error = lib_config_path(&Symbol::new("..")).unwrap_err();
81
82        assert_eq!(
83            error,
84            ConfigError::InvalidPathSegment {
85                segment: "..".to_owned()
86            }
87        );
88    }
89
90    #[test]
91    fn roots_use_config_home_before_xdg_and_home() {
92        let roots = ConfigRoots::from_env_values(
93            PathBuf::from("/work"),
94            Some(PathBuf::from("/sim-config")),
95            Some(PathBuf::from("/xdg")),
96            Some(PathBuf::from("/user-home/alice")),
97        );
98
99        assert_eq!(roots.home, Some(PathBuf::from("/sim-config")));
100        assert_eq!(roots.work, PathBuf::from("/work/.sim/config"));
101    }
102}