use std::net::IpAddr;
use crate::grammar::is_valid_plain_chunk;
pub fn ip_slug(ip: IpAddr) -> String {
ip.to_string().replace(['.', ':'], "-")
}
pub fn ip_slug_str(text: &str) -> Option<String> {
text.parse::<IpAddr>().ok().map(ip_slug)
}
pub fn ulid_slug(id: &str) -> Option<String> {
fn crockford(b: u8) -> bool {
let b = b.to_ascii_uppercase();
b.is_ascii_digit() || (b.is_ascii_uppercase() && !matches!(b, b'I' | b'L' | b'O' | b'U'))
}
(id.len() == 26 && id.bytes().all(crockford)).then(|| id.to_ascii_lowercase())
}
pub fn chunk_slug(value: &str) -> String {
if is_valid_plain_chunk(value) {
return value.to_string();
}
let bytes = value.as_bytes();
let mut out = String::with_capacity(value.len() + 8);
for (i, &b) in bytes.iter().enumerate() {
let c = b as char;
let alnum = c.is_ascii_lowercase() || c.is_ascii_digit();
let legal_inner = alnum || c == '.' || c == '_' || c == '-';
let boundary = i == 0 || i == bytes.len() - 1;
if legal_inner && (alnum || !boundary) {
out.push(c);
} else {
out.push_str(&format!("_x{b:02x}_"));
}
}
if out.starts_with('_') {
out.insert(0, 'x');
}
if out.ends_with('_') {
out.push('x');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn ipv4_always_slugged() {
assert_eq!(ip_slug_str("10.0.0.7").unwrap(), "10-0-0-7");
assert_eq!(ip_slug_str("93.184.216.34").unwrap(), "93-184-216-34");
}
#[test]
fn ipv6_rfc5952_canonical_before_slugging() {
let a = ip_slug_str("2001:db8::1").unwrap();
let b = ip_slug_str("2001:db8:0:0:0:0:0:1").unwrap();
let c = ip_slug_str("2001:DB8::1").unwrap();
assert_eq!(a, "2001-db8--1");
assert_eq!(a, b);
assert_eq!(a, c);
}
#[test]
fn legal_values_stay_literal() {
assert_eq!(chunk_slug("sshd.service"), "sshd.service");
assert_eq!(chunk_slug("cam0"), "cam0");
}
#[test]
fn escape_is_injective() {
let a = chunk_slug("foo@1.service");
let b = chunk_slug("foo-1.service");
assert_ne!(a, b);
assert_eq!(a, "foo_x40_1.service");
let corpus = [
"getty@tty1.service",
"getty-tty1.service",
"a b",
"a_b",
"a-b",
"A",
"a",
"Ab",
"a.b",
".ab",
"ab.",
"café",
"unit@.service",
"_myns",
"e_myns",
"x_myns",
"myns_",
"_",
];
let slugs: Vec<String> = corpus.iter().map(|v| chunk_slug(v)).collect();
let unique: HashSet<&String> = slugs.iter().collect();
assert_eq!(unique.len(), corpus.len(), "collision in {slugs:?}");
for s in &slugs {
assert!(
crate::grammar::is_valid_plain_chunk(s),
"illegal slug {s:?}"
);
}
}
#[test]
fn ulid_shapes_lowercase_and_others_refuse() {
let canonical = "01JGXQZ4YQK8V6TXW3M9F2A7CD";
assert_eq!(
ulid_slug(canonical).as_deref(),
Some("01jgxqz4yqk8v6txw3m9f2a7cd")
);
assert_eq!(
ulid_slug("01jgxqz4yqk8v6txw3m9f2a7cd").as_deref(),
Some("01jgxqz4yqk8v6txw3m9f2a7cd"),
"already-lowercase is the fixed point"
);
assert_eq!(ulid_slug("01HQXK8F9C2N4PZQ"), None, "16 chars");
assert_eq!(ulid_slug("01JGXQZ4YQK8V6TXW3M9F2A7CI"), None, "I excluded");
assert_eq!(ulid_slug("01jgxqz4yqk8v6txw3m9f2a7c."), None);
assert_eq!(ulid_slug(""), None);
}
#[test]
fn event_ids_are_ulid_slugs() {
let id = ulid_slug("01JGXQZ4YQK8V6TXW3M9F2A7CD").unwrap();
let key = crate::grammar::data_key(
&crate::grammar::Origin::Host(crate::origin::HostId::parse("h-3fa9c2d41b7e").unwrap()),
crate::grammar::Class::Events,
Some(&crate::grammar::Producer::new("netring").unwrap()),
&["capture", &id],
)
.unwrap();
assert_eq!(
key,
"v1/h-3fa9c2d41b7e/events/netring/capture/01jgxqz4yqk8v6txw3m9f2a7cd"
);
}
#[test]
fn erratum_boundary_escape_is_the_rfcs() {
assert_eq!(chunk_slug("_myns"), "x_x5f_myns");
assert_ne!(chunk_slug("_myns"), chunk_slug("e_myns"));
assert_eq!(chunk_slug("e_myns"), "e_myns");
assert_eq!(chunk_slug("myns_"), "myns_x5f_x");
assert_eq!(chunk_slug(".ab"), "x_x2e_ab");
}
}