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_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
42pub 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}