use super::names::{namespace_of, RELAY};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Route {
Local,
Forward(String),
Deny,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ForwardingTable {
entries: Vec<(String, String)>,
}
impl ForwardingTable {
pub fn new() -> Self {
Self::default()
}
pub fn allow(&mut self, namespace_prefix: &str, endpoint: &str) -> Result<(), String> {
let trimmed = namespace_prefix.trim();
let head = trimmed.split('.').next().unwrap_or_default();
if head.is_empty() {
return Err(format!("'{namespace_prefix}' carries no namespace"));
}
if head == RELAY {
return Err(format!(
"the '{RELAY}' namespace is reserved for local system control"
));
}
self.entries
.push((trimmed.to_string(), endpoint.to_string()));
Ok(())
}
pub fn route(&self, method: &str) -> Route {
if namespace_of(method) == RELAY {
return Route::Local;
}
let mut best: Option<(usize, &str)> = None;
for (prefix, endpoint) in &self.entries {
let matches = method == prefix.as_str()
|| method
.strip_prefix(prefix.as_str())
.is_some_and(|rest| rest.starts_with('.'));
if matches {
let len = prefix.len();
if best.is_none_or(|(blen, _)| len >= blen) {
best = Some((len, endpoint));
}
}
}
match best {
Some((_, endpoint)) => Route::Forward(endpoint.to_string()),
None => Route::Deny,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tauri_namespace_always_local() {
let mut table = ForwardingTable::new();
table.allow("net", "public").unwrap();
assert_eq!(table.route("relay.net.set_proxy"), Route::Local);
}
#[test]
fn tauri_cannot_be_captured() {
let mut table = ForwardingTable::new();
assert!(table.allow("relay", "evil").is_err());
assert!(table.allow("relay.evil", "evil").is_err());
}
#[test]
fn longest_prefix_wins_and_unlisted_denied() {
let mut table = ForwardingTable::new();
table.allow("device", "gateway").unwrap();
table.allow("device.internal", "lab").unwrap();
assert_eq!(
table.route("device.register"),
Route::Forward("gateway".into())
);
assert_eq!(
table.route("device.internal.diag"),
Route::Forward("lab".into())
);
assert_eq!(table.route("enrollment.mint"), Route::Deny);
assert_eq!(table.route("deviceX.anything"), Route::Deny);
}
#[test]
fn latest_edit_wins_on_tie() {
let mut table = ForwardingTable::new();
table.allow("device", "gateway").unwrap();
table.allow("device", "gateway-2").unwrap();
assert_eq!(table.route("device.x"), Route::Forward("gateway-2".into()));
}
}