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)]
18pub struct RuntimoConfig {
19 #[serde(default)]
21 pub allowed_paths: Vec<String>,
22}
23
24impl RuntimoConfig {
25 pub fn config_path() -> PathBuf {
29 std::env::var("XDG_CONFIG_HOME")
30 .ok()
31 .map(PathBuf::from)
32 .or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config")))
33 .unwrap_or_else(|| PathBuf::from("/tmp"))
34 .join("runtimo/config.toml")
35 }
36
37 pub fn load() -> Self {
39 let path = Self::config_path();
40 if path.exists() {
41 let content = std::fs::read_to_string(&path).unwrap_or_default();
42 toml::from_str(&content).unwrap_or_default()
43 } else {
44 Self::default()
45 }
46 }
47
48 pub fn save(&self) -> Result<(), String> {
50 let path = Self::config_path();
51 if let Some(parent) = path.parent() {
52 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
53 }
54 let content = toml::to_string_pretty(self).map_err(|e| e.to_string())?;
55 std::fs::write(&path, content).map_err(|e| e.to_string())?;
56 Ok(())
57 }
58
59 pub fn get_allowed_prefixes() -> Vec<String> {
66 let mut prefixes: Vec<String> = DEFAULT_PREFIXES.iter().map(|s| s.to_string()).collect();
67
68 if let Ok(env_paths) = std::env::var("RUNTIMO_ALLOWED_PATHS") {
70 for p in env_paths.split(':').filter(|s| !s.is_empty()) {
71 let trimmed = p.trim().to_string();
72 if !prefixes.contains(&trimmed) {
73 prefixes.push(trimmed);
74 }
75 }
76 }
77
78 let config = Self::load();
80 for p in &config.allowed_paths {
81 let trimmed = p.trim().to_string();
82 if !prefixes.contains(&trimmed) {
83 prefixes.push(trimmed);
84 }
85 }
86
87 prefixes
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn config_path_is_absolute() {
97 let path = RuntimoConfig::config_path();
98 assert!(path.is_absolute() || path.to_string_lossy().starts_with("/tmp"));
99 }
100
101 #[test]
102 fn load_returns_defaults_when_no_file() {
103 let tmp = std::env::temp_dir().join("runtimo_test_config_defaults");
104 let _ = std::fs::remove_dir_all(&tmp);
105 std::env::set_var("XDG_CONFIG_HOME", &tmp);
106
107 let config = RuntimoConfig::load();
108 assert!(config.allowed_paths.is_empty());
109
110 let _ = std::fs::remove_dir_all(&tmp);
111 std::env::remove_var("XDG_CONFIG_HOME");
112 }
113
114 #[test]
115 fn get_allowed_prefixes_includes_defaults() {
116 let prefixes = RuntimoConfig::get_allowed_prefixes();
117 assert!(prefixes.iter().any(|p| p == "/tmp"));
118 assert!(prefixes.iter().any(|p| p == "/var/tmp"));
119 assert!(prefixes.iter().any(|p| p == "/home"));
120 }
121
122 #[test]
123 fn save_and_load_roundtrip() {
124 let tmp = std::env::temp_dir().join("runtimo_test_config");
126 std::env::set_var("XDG_CONFIG_HOME", &tmp);
127
128 let mut config = RuntimoConfig::default();
129 config.allowed_paths.push("/srv".to_string());
130 config.allowed_paths.push("/opt".to_string());
131 config.save().expect("save failed");
132
133 let loaded = RuntimoConfig::load();
134 assert_eq!(loaded.allowed_paths, vec!["/srv", "/opt"]);
135
136 let prefixes = RuntimoConfig::get_allowed_prefixes();
137 assert!(prefixes.contains(&"/srv".to_string()));
138 assert!(prefixes.contains(&"/opt".to_string()));
139
140 let _ = std::fs::remove_dir_all(&tmp);
142 std::env::remove_var("XDG_CONFIG_HOME");
143 }
144}