1use std::path::PathBuf;
4
5use sim_kernel::Symbol;
6use sim_table_core::is_legal_table_segment;
7
8use crate::{ConfigError, ConfigResult};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ConfigRoots {
13 pub home: Option<PathBuf>,
15 pub work: PathBuf,
17}
18
19impl ConfigRoots {
20 pub fn new(home: Option<PathBuf>, work: PathBuf) -> Self {
22 Self { home, work }
23 }
24
25 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 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
52pub 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}