Skip to main content

dbus_router/
config.rs

1//! Configuration file parsing for routing rules
2
3use anyhow::Result;
4use serde::Deserialize;
5use std::path::{Path, PathBuf};
6
7/// Router configuration loaded from TOML file.
8#[derive(Debug, Clone, Default, Deserialize)]
9pub struct Config {
10    /// Routes that should go to the host bus instead of sandbox.
11    #[serde(default)]
12    pub host_routes: Vec<RouteRule>,
13    /// Processes allowed to register services on the host bus.
14    #[serde(default)]
15    pub hostpass: Vec<HostPass>,
16}
17
18/// A process allowed to register services on the host bus.
19#[derive(Debug, Clone, Deserialize)]
20pub struct HostPass {
21    /// Path to the executable.
22    pub process: PathBuf,
23}
24
25/// A routing rule that matches destinations.
26#[derive(Debug, Clone, Deserialize)]
27pub struct RouteRule {
28    /// Destination to match. Supports exact match or "org.foo.*" wildcard.
29    pub destination: String,
30}
31
32impl Config {
33    /// Load configuration from a TOML file.
34    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    /// Check if a destination should be routed to the host bus.
45    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    /// Check if a process is allowed to register services on the host bus.
52    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    /// Check if the destination matches this rule.
59    /// Supports exact match and "org.foo.*" wildcard pattern.
60    pub fn matches(&self, destination: &str) -> bool {
61        if let Some(prefix) = self.destination.strip_suffix(".*") {
62            // Wildcard: destination must start with prefix and either equal it
63            // or have a dot after the prefix
64            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            // Exact match
73            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")); // prefix itself matches
100        assert!(!rule.matches("org.freedesktop.portals")); // no dot separator
101        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}