#[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())))
}
#[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));
}
}