1use anyhow::Result;
4use serde::Deserialize;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Default, Deserialize)]
9pub struct Config {
10 #[serde(default)]
12 pub host_routes: Vec<RouteRule>,
13 #[serde(default)]
15 pub hostpass: Vec<HostPass>,
16}
17
18#[derive(Debug, Clone, Deserialize)]
20pub struct HostPass {
21 pub process: PathBuf,
23}
24
25#[derive(Debug, Clone, Deserialize)]
27pub struct RouteRule {
28 pub destination: String,
30}
31
32impl Config {
33 pub fn load(path: &Path) -> Result<Self> {
35 let content = std::fs::read_to_string(path)?;
36 let config: Config = toml::from_str(&content)?;
37 tracing::info!(routes = config.host_routes.len(), "Loaded configuration");
38 for rule in &config.host_routes {
39 tracing::debug!(destination = %rule.destination, "Host route");
40 }
41 Ok(config)
42 }
43
44 pub fn should_route_to_host(&self, destination: &str) -> bool {
46 self.host_routes
47 .iter()
48 .any(|rule| rule.matches(destination))
49 }
50
51 pub fn has_hostpass(&self, exe_path: &Path) -> bool {
53 self.hostpass.iter().any(|h| h.process == exe_path)
54 }
55}
56
57impl RouteRule {
58 pub fn matches(&self, destination: &str) -> bool {
61 if let Some(prefix) = self.destination.strip_suffix(".*") {
62 if destination == prefix {
65 return true;
66 }
67 if let Some(rest) = destination.strip_prefix(prefix) {
68 return rest.starts_with('.');
69 }
70 false
71 } else {
72 self.destination == destination
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_exact_match() {
84 let rule = RouteRule {
85 destination: "org.freedesktop.DBus".to_string(),
86 };
87 assert!(rule.matches("org.freedesktop.DBus"));
88 assert!(!rule.matches("org.freedesktop.DBus.Peer"));
89 assert!(!rule.matches("org.freedesktop"));
90 }
91
92 #[test]
93 fn test_wildcard_match() {
94 let rule = RouteRule {
95 destination: "org.freedesktop.portal.*".to_string(),
96 };
97 assert!(rule.matches("org.freedesktop.portal.Desktop"));
98 assert!(rule.matches("org.freedesktop.portal.FileChooser"));
99 assert!(rule.matches("org.freedesktop.portal")); assert!(!rule.matches("org.freedesktop.portals")); assert!(!rule.matches("org.freedesktop.DBus"));
102 }
103
104 #[test]
105 fn test_should_route_to_host() {
106 let config = Config {
107 host_routes: vec![
108 RouteRule {
109 destination: "org.freedesktop.DBus".to_string(),
110 },
111 RouteRule {
112 destination: "org.freedesktop.portal.*".to_string(),
113 },
114 ],
115 ..Default::default()
116 };
117
118 assert!(config.should_route_to_host("org.freedesktop.DBus"));
119 assert!(config.should_route_to_host("org.freedesktop.portal.Desktop"));
120 assert!(!config.should_route_to_host("org.example.Test"));
121 }
122
123 #[test]
124 fn test_empty_config() {
125 let config = Config::default();
126 assert!(!config.should_route_to_host("org.freedesktop.DBus"));
127 }
128
129 #[test]
130 fn test_parse_toml() {
131 let toml_str = r#"
132[[host_routes]]
133destination = "org.freedesktop.DBus"
134
135[[host_routes]]
136destination = "org.freedesktop.portal.*"
137"#;
138 let config: Config = toml::from_str(toml_str).unwrap();
139 assert_eq!(config.host_routes.len(), 2);
140 assert_eq!(config.host_routes[0].destination, "org.freedesktop.DBus");
141 assert_eq!(
142 config.host_routes[1].destination,
143 "org.freedesktop.portal.*"
144 );
145 }
146
147 #[test]
148 fn test_has_hostpass() {
149 let config = Config {
150 host_routes: vec![],
151 hostpass: vec![
152 HostPass {
153 process: PathBuf::from("/usr/bin/my-sandbox-app"),
154 },
155 HostPass {
156 process: PathBuf::from("/opt/app/bin/service"),
157 },
158 ],
159 };
160
161 assert!(config.has_hostpass(Path::new("/usr/bin/my-sandbox-app")));
162 assert!(config.has_hostpass(Path::new("/opt/app/bin/service")));
163 assert!(!config.has_hostpass(Path::new("/usr/bin/other-app")));
164 assert!(!config.has_hostpass(Path::new("/usr/bin/my-sandbox-app-extra")));
165 }
166
167 #[test]
168 fn test_parse_toml_with_hostpass() {
169 let toml_str = r#"
170[[host_routes]]
171destination = "org.freedesktop.DBus"
172
173[[hostpass]]
174process = "/usr/bin/my-sandbox-app"
175
176[[hostpass]]
177process = "/opt/app/bin/service"
178"#;
179 let config: Config = toml::from_str(toml_str).unwrap();
180 assert_eq!(config.host_routes.len(), 1);
181 assert_eq!(config.hostpass.len(), 2);
182 assert_eq!(
183 config.hostpass[0].process,
184 PathBuf::from("/usr/bin/my-sandbox-app")
185 );
186 assert_eq!(
187 config.hostpass[1].process,
188 PathBuf::from("/opt/app/bin/service")
189 );
190 }
191}