1use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13const DEFAULT_PREFIXES: &[&str] = &["/tmp", "/var/tmp", "/home"];
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18#[allow(clippy::exhaustive_structs)]
19pub struct RuntimoConfig {
20 #[serde(default)]
22 pub allowed_paths: Vec<String>,
23}
24
25impl RuntimoConfig {
26 #[allow(clippy::expect_used)]
34 pub fn config_path() -> PathBuf {
35 std::env::var("XDG_CONFIG_HOME")
36 .ok()
37 .map(PathBuf::from)
38 .or_else(|| {
39 std::env::var("HOME")
40 .ok()
41 .map(|h| PathBuf::from(h).join(".config"))
42 })
43 .expect("Cannot determine config path: set XDG_CONFIG_HOME or HOME")
44 .join("runtimo/config.toml")
45 }
46
47 #[must_use]
49 pub fn load() -> Self {
50 let path = Self::config_path();
51 if path.exists() {
52 let content = std::fs::read_to_string(&path).unwrap_or_default();
53 toml::from_str(&content).unwrap_or_default()
54 } else {
55 Self::default()
56 }
57 }
58
59 pub fn save(&self) -> Result<(), String> {
66 let path = Self::config_path();
67 if let Some(parent) = path.parent() {
68 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
69 }
70 let content = toml::to_string_pretty(self).map_err(|e| e.to_string())?;
71 std::fs::write(&path, content).map_err(|e| e.to_string())?;
72 Ok(())
73 }
74
75 #[must_use]
82 pub fn get_allowed_prefixes() -> Vec<String> {
83 let mut prefixes: Vec<String> = DEFAULT_PREFIXES.iter().map(|s| s.to_string()).collect();
84
85 if let Ok(env_paths) = std::env::var("RUNTIMO_ALLOWED_PATHS") {
87 for p in env_paths.split(':').filter(|s| !s.is_empty()) {
88 let trimmed = p.trim().to_string();
89 if !prefixes.contains(&trimmed) {
90 prefixes.push(trimmed);
91 }
92 }
93 }
94
95 let config = Self::load();
97 for p in &config.allowed_paths {
98 let trimmed = p.trim().to_string();
99 if !prefixes.contains(&trimmed) {
100 prefixes.push(trimmed);
101 }
102 }
103
104 prefixes
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn config_path_is_absolute() {
114 let path = RuntimoConfig::config_path();
115 assert!(path.is_absolute());
116 }
117
118 #[test]
119 fn load_returns_defaults_when_no_file() {
120 let tmp = std::env::temp_dir().join("runtimo_test_config_defaults");
121 let _ = std::fs::remove_dir_all(&tmp);
122 std::env::set_var("XDG_CONFIG_HOME", &tmp);
123
124 let config = RuntimoConfig::load();
125 assert!(config.allowed_paths.is_empty());
126
127 let _ = std::fs::remove_dir_all(&tmp);
128 std::env::remove_var("XDG_CONFIG_HOME");
129 }
130
131 #[test]
132 fn get_allowed_prefixes_includes_defaults() {
133 let prefixes = RuntimoConfig::get_allowed_prefixes();
134 assert!(prefixes.iter().any(|p| p == "/tmp"));
135 assert!(prefixes.iter().any(|p| p == "/var/tmp"));
136 assert!(prefixes.iter().any(|p| p == "/home"));
137 }
138
139 #[test]
140 fn save_and_load_roundtrip() {
141 let tmp = std::env::temp_dir().join("runtimo_test_config");
143 std::env::set_var("XDG_CONFIG_HOME", &tmp);
144
145 let mut config = RuntimoConfig::default();
146 config.allowed_paths.push("/srv".to_string());
147 config.allowed_paths.push("/opt".to_string());
148 config.save().expect("save failed");
149
150 let loaded = RuntimoConfig::load();
151 assert_eq!(loaded.allowed_paths, vec!["/srv", "/opt"]);
152
153 let prefixes = RuntimoConfig::get_allowed_prefixes();
154 assert!(prefixes.contains(&"/srv".to_string()));
155 assert!(prefixes.contains(&"/opt".to_string()));
156
157 let _ = std::fs::remove_dir_all(&tmp);
159 std::env::remove_var("XDG_CONFIG_HOME");
160 }
161}