use std::sync::Arc;
use crate::event::AstridEvent;
#[derive(Debug, Clone)]
pub struct TopicMatcher {
pattern: String,
}
impl TopicMatcher {
pub const MAX_TOPIC_DEPTH: usize = 20;
pub fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
}
}
#[must_use]
pub fn matches(&self, event: &AstridEvent) -> bool {
let AstridEvent::Ipc { message, .. } = event else {
return false;
};
self.matches_topic(&message.topic)
}
#[must_use]
pub fn matches_topic(&self, topic: &str) -> bool {
topic_pattern_matches(&self.pattern, topic)
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
}
#[must_use]
pub fn topic_pattern_matches(pattern: &str, topic: &str) -> bool {
if topic.split('.').count() > TopicMatcher::MAX_TOPIC_DEPTH {
return false;
}
if let Some(prefix_pat) = pattern.strip_suffix(".*") {
topic.split('.').count() > prefix_pat.split('.').count()
&& prefix_pat
.split('.')
.zip(topic.split('.'))
.all(|(p, t)| p == "*" || p == t)
} else {
pattern.split('.').count() == topic.split('.').count()
&& pattern
.split('.')
.zip(topic.split('.'))
.all(|(p, t)| p == "*" || p == t)
}
}
#[must_use]
pub fn ipc_size_of(event: &Arc<AstridEvent>) -> usize {
match &**event {
AstridEvent::Ipc { message, .. } => message
.payload
.to_guest_bytes()
.map_or(0, |v| v.len())
.saturating_add(message.topic.len()),
_ => 64,
}
}
#[must_use]
pub fn principal_class_label(principal: Option<&str>) -> &'static str {
match principal {
None => "system",
Some(p) if p.starts_with("agent.") || p.starts_with("agent:") => "agent",
Some(_) => "user",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::EventMetadata;
use crate::ipc::{IpcMessage, IpcPayload};
use serde_json::json;
use uuid::Uuid;
fn ipc(topic: &str) -> Arc<AstridEvent> {
let msg = IpcMessage::new(topic, IpcPayload::RawJson(json!({})), Uuid::nil());
Arc::new(AstridEvent::Ipc {
metadata: EventMetadata::new("test"),
message: msg,
})
}
#[test]
fn topic_matcher_exact() {
let m = TopicMatcher::new("a.b.c");
assert!(m.matches(&ipc("a.b.c")));
assert!(!m.matches(&ipc("a.b.d")));
assert!(!m.matches(&ipc("a.b")));
assert!(!m.matches(&ipc("a.b.c.d")));
}
#[test]
fn topic_matcher_trailing_wildcard() {
let m = TopicMatcher::new("a.b.*");
assert!(m.matches(&ipc("a.b.c")));
assert!(m.matches(&ipc("a.b.c.d")));
assert!(!m.matches(&ipc("a.b")));
assert!(!m.matches(&ipc("a.c.b")));
}
#[test]
fn topic_matcher_middle_wildcard() {
let m = TopicMatcher::new("a.*.c");
assert!(m.matches(&ipc("a.b.c")));
assert!(m.matches(&ipc("a.zz.c")));
assert!(!m.matches(&ipc("a.b.d")));
assert!(!m.matches(&ipc("a.b.c.d")));
}
#[test]
fn matches_topic_subtree_for_acl() {
let m = TopicMatcher::new("astrid.v1.admin.*");
assert!(m.matches_topic("astrid.v1.admin.quota"));
assert!(m.matches_topic("astrid.v1.admin.agent.list"));
assert!(m.matches_topic("astrid.v1.admin.auth.pair.issue"));
assert!(!m.matches_topic("astrid.v1.admin"));
assert!(!m.matches_topic("astrid.v1.registry.get"));
let mid = TopicMatcher::new("tool.v1.execute.*.result");
assert!(mid.matches_topic("tool.v1.execute.read_file.result"));
assert!(!mid.matches_topic("tool.v1.execute.a.b.result"));
let exact = TopicMatcher::new("a.b.c");
assert!(exact.matches_topic("a.b.c"));
assert!(!exact.matches_topic("a.b.c.d"));
}
#[test]
fn matches_topic_enumerated_patterns_stay_compatible() {
let five = TopicMatcher::new("astrid.v1.admin.*.*");
assert!(five.matches_topic("astrid.v1.admin.agent.list")); assert!(five.matches_topic("astrid.v1.admin.auth.pair")); assert!(five.matches_topic("astrid.v1.admin.auth.pair.issue")); assert!(!five.matches_topic("astrid.v1.admin.quota"));
let six = TopicMatcher::new("astrid.v1.admin.*.*.*");
assert!(six.matches_topic("astrid.v1.admin.auth.pair.issue")); assert!(!six.matches_topic("astrid.v1.admin.agent.list")); }
#[test]
fn topic_matcher_rejects_non_ipc() {
let m = TopicMatcher::new("a.*");
let lifecycle = Arc::new(AstridEvent::RuntimeStarted {
metadata: EventMetadata::new("test"),
version: "1".into(),
});
assert!(!m.matches(&lifecycle));
}
#[test]
fn principal_class_label_buckets() {
assert_eq!(principal_class_label(None), "system");
assert_eq!(principal_class_label(Some("alice")), "user");
assert_eq!(principal_class_label(Some("agent.scout")), "agent");
assert_eq!(principal_class_label(Some("agent:scout")), "agent");
}
}