#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AlertKeyError {
#[error("alert rule name {0:?} must be nonempty and contain no newline (RFC 11 §3.1)")]
BadRule(String),
#[error("alert label name {0:?} must contain no newline or '=' (RFC 11 §3.1)")]
BadLabelName(String),
#[error("alert label value {0:?} for {1:?} must contain no newline (RFC 11 §3.1)")]
BadLabelValue(String, String),
}
fn fnv1a_64(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x100_0000_01b3);
}
hash
}
pub fn alert_key(rule: &str, labels: &[(&str, &str)]) -> Result<String, AlertKeyError> {
if rule.is_empty() || rule.contains('\n') {
return Err(AlertKeyError::BadRule(rule.to_string()));
}
let mut discriminating: Vec<(&str, &str)> = Vec::with_capacity(labels.len());
for &(name, value) in labels {
if name == "host" {
continue;
}
if name.is_empty() || name.contains('\n') || name.contains('=') {
return Err(AlertKeyError::BadLabelName(name.to_string()));
}
if value.contains('\n') {
return Err(AlertKeyError::BadLabelValue(
value.to_string(),
name.to_string(),
));
}
discriminating.push((name, value));
}
discriminating.sort_unstable();
let mut input = String::from(rule);
for (name, value) in discriminating {
input.push('\n');
input.push_str(name);
input.push('=');
input.push_str(value);
}
Ok(format!("{:016x}", fnv1a_64(input.as_bytes())))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AlertRefError {
#[error("alert ref origin {0:?} is not an origin chunk (RFC 11 §3.2)")]
BadOrigin(String),
#[error("alert ref producer {0:?} is not a dot-free plain chunk (RFC 11 §3.2)")]
BadProducer(String),
#[error("alert ref alert_key {0:?} is not a plain chunk (RFC 11 §3.2)")]
BadAlertKey(String),
}
pub fn alert_ref(origin: &str, producer: &str, alert_key: &str) -> Result<String, AlertRefError> {
use crate::grammar::{is_valid_host_origin, is_valid_plain_chunk, is_valid_verbatim_chunk};
if !(is_valid_host_origin(origin) || is_valid_verbatim_chunk(origin)) {
return Err(AlertRefError::BadOrigin(origin.to_string()));
}
if !is_valid_plain_chunk(producer) || producer.contains('.') {
return Err(AlertRefError::BadProducer(producer.to_string()));
}
if !is_valid_plain_chunk(alert_key) {
return Err(AlertRefError::BadAlertKey(alert_key.to_string()));
}
Ok(format!("{origin}.{producer}.{alert_key}"))
}
pub fn parse_alert_ref(r: &str) -> Option<(&str, &str, &str)> {
use crate::grammar::{is_valid_host_origin, is_valid_plain_chunk, is_valid_verbatim_chunk};
let mut parts = r.splitn(3, '.');
let origin = parts.next()?;
let producer = parts.next()?;
let alert_key = parts.next()?;
if !(is_valid_host_origin(origin) || is_valid_verbatim_chunk(origin)) {
return None;
}
if !is_valid_plain_chunk(producer) || alert_key.is_empty() {
return None;
}
Some((origin, producer, alert_key))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_rfc_test_vector() {
let key = alert_key(
"link_down",
&[("peer", "r2"), ("port", "eth0"), ("host", "h-3fa9c2d41b7e")],
)
.unwrap();
assert_eq!(key, "a659f813308ad1da");
assert_eq!(
"link_down\npeer=r2\nport=eth0".len(),
27,
"the RFC counts 27 bytes"
);
assert_eq!(
fnv1a_64(b"link_down\npeer=r2\nport=eth0"),
0xa659_f813_308a_d1da
);
}
#[test]
fn ordering_and_host_exclusion_are_canonical() {
let a = alert_key("link_down", &[("port", "eth0"), ("peer", "r2")]).unwrap();
let b = alert_key("link_down", &[("peer", "r2"), ("port", "eth0")]).unwrap();
assert_eq!(a, b);
let c = alert_key(
"link_down",
&[("host", "h-ffffffffffff"), ("peer", "r2"), ("port", "eth0")],
)
.unwrap();
assert_eq!(a, c);
assert_eq!(
alert_key("link_down", &[]).unwrap(),
format!("{:016x}", fnv1a_64(b"link_down"))
);
}
#[test]
fn injectivity_conditions_refuse() {
assert!(matches!(alert_key("", &[]), Err(AlertKeyError::BadRule(_))));
assert!(matches!(
alert_key("a\nb", &[]),
Err(AlertKeyError::BadRule(_))
));
assert!(matches!(
alert_key("r", &[("pe=er", "x")]),
Err(AlertKeyError::BadLabelName(_))
));
assert!(matches!(
alert_key("r", &[("pe\ner", "x")]),
Err(AlertKeyError::BadLabelName(_))
));
assert!(matches!(
alert_key("r", &[("", "x")]),
Err(AlertKeyError::BadLabelName(_))
));
assert!(matches!(
alert_key("r", &[("peer", "x\ny")]),
Err(AlertKeyError::BadLabelValue(..))
));
assert!(alert_key("r", &[("peer", "a=b")]).is_ok());
}
#[test]
fn key_is_a_legal_chunk() {
let key = alert_key("link_down", &[("peer", "r2")]).unwrap();
assert_eq!(key.len(), 16);
assert!(crate::grammar::is_valid_plain_chunk(&key));
}
#[test]
fn the_alert_ref_vector_round_trips() {
let r = alert_ref("h-3fa9c2d41b7e", "netlink", "a659f813308ad1da").unwrap();
assert_eq!(r, "h-3fa9c2d41b7e.netlink.a659f813308ad1da");
assert!(crate::grammar::is_valid_plain_chunk(&r), "a ref is a chunk");
assert_eq!(
parse_alert_ref(&r),
Some(("h-3fa9c2d41b7e", "netlink", "a659f813308ad1da"))
);
let r = alert_ref("@catalog", "catalog", "x.y").unwrap();
assert_eq!(parse_alert_ref(&r), Some(("@catalog", "catalog", "x.y")));
}
#[test]
fn a_ref_that_would_need_escaping_is_refused() {
assert!(matches!(
alert_ref("host", "netlink", "a659f813308ad1da"),
Err(AlertRefError::BadOrigin(_))
));
assert!(matches!(
alert_ref("h-3fa9c2d41b7e", "net.link", "a659f813308ad1da"),
Err(AlertRefError::BadProducer(_))
));
assert!(matches!(
alert_ref("h-3fa9c2d41b7e", "netlink", "A659"),
Err(AlertRefError::BadAlertKey(_))
));
assert_eq!(parse_alert_ref("h-3fa9c2d41b7e.netlink"), None);
assert_eq!(parse_alert_ref("nope.netlink.k"), None);
assert_eq!(parse_alert_ref("h-3fa9c2d41b7e.netlink."), None);
}
}