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 config = RuntimoConfig::load();
105 assert!(config.allowed_paths.is_empty());
107 }
108
109 #[test]
110 fn get_allowed_prefixes_includes_defaults() {
111 let prefixes = RuntimoConfig::get_allowed_prefixes();
112 assert!(prefixes.iter().any(|p| p == "/tmp"));
113 assert!(prefixes.iter().any(|p| p == "/var/tmp"));
114 assert!(prefixes.iter().any(|p| p == "/home"));
115 }
116
117 #[test]
118 fn save_and_load_roundtrip() {
119 let tmp = std::env::temp_dir().join("runtimo_test_config");
121 std::env::set_var("XDG_CONFIG_HOME", &tmp);
122
123 let mut config = RuntimoConfig::default();
124 config.allowed_paths.push("/srv".to_string());
125 config.allowed_paths.push("/opt".to_string());
126 config.save().expect("save failed");
127
128 let loaded = RuntimoConfig::load();
129 assert_eq!(loaded.allowed_paths, vec!["/srv", "/opt"]);
130
131 let prefixes = RuntimoConfig::get_allowed_prefixes();
132 assert!(prefixes.contains(&"/srv".to_string()));
133 assert!(prefixes.contains(&"/opt".to_string()));
134
135 let _ = std::fs::remove_dir_all(&tmp);
137 std::env::remove_var("XDG_CONFIG_HOME");
138 }
139}