use serde::{Deserialize, Serialize};
pub const DEFAULT_MAX_HOPS: u8 = 4;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RelayContext {
#[serde(default)]
pub hops: u8,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub via: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HopGuard {
pub max_hops: u8,
}
impl Default for HopGuard {
fn default() -> Self {
Self {
max_hops: DEFAULT_MAX_HOPS,
}
}
}
impl HopGuard {
pub fn admit(&self, context: &RelayContext) -> Result<(), String> {
if context.hops >= self.max_hops {
return Err(format!(
"relay hop limit reached ({} >= {}); refusing to forward — possible loop",
context.hops, self.max_hops
));
}
Ok(())
}
pub fn step(context: &RelayContext, hop_name: &str) -> RelayContext {
let mut via = context.via.clone();
via.push(hop_name.to_string());
RelayContext {
hops: context.hops + 1,
via,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refuses_beyond_limit_and_steps_trace() {
let guard = HopGuard::default();
let ctx = RelayContext {
hops: 4,
via: vec![],
};
assert!(guard.admit(&ctx).is_err());
let ok = RelayContext {
hops: 1,
via: vec!["edge".into()],
};
assert!(guard.admit(&ok).is_ok());
let stepped = HopGuard::step(&ok, "gateway");
assert_eq!(stepped.hops, 2);
assert_eq!(stepped.via, vec!["edge".to_string(), "gateway".to_string()]);
}
}