pub(crate) struct TagGenerator {
prefix: u32,
counter: u32,
}
impl TagGenerator {
#[allow(clippy::expect_used)] pub(crate) fn new() -> Self {
let mut bytes = [0u8; 4];
getrandom::getrandom(&mut bytes).expect("getrandom failed — no OS entropy source");
let prefix = u32::from_le_bytes(bytes);
Self { prefix, counter: 0 }
}
#[allow(clippy::expect_used)] pub(crate) fn next(&mut self) -> String {
if self.counter == u32::MAX {
let mut bytes = [0u8; 4];
getrandom::getrandom(&mut bytes).expect("getrandom failed — no OS entropy source");
self.prefix = u32::from_le_bytes(bytes);
self.counter = 0;
} else {
self.counter = self.counter.wrapping_add(1);
}
format!("{:08x}{:08x}", self.prefix, self.counter)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn invariant_i12_tag_wrap_regenerates_prefix() {
let mut gen = TagGenerator::new();
let original_prefix = gen.prefix;
gen.counter = u32::MAX;
let _ = gen.next();
assert_ne!(
gen.prefix, original_prefix,
"wrap did not regenerate prefix"
);
assert_eq!(gen.counter, 0);
}
#[test]
fn tag_format_is_hex() {
let mut gen = TagGenerator::new();
let tag = gen.next();
assert_eq!(tag.len(), 16, "tag should be 16 hex chars");
assert!(
tag.chars().all(|c| c.is_ascii_hexdigit()),
"tag should only contain hex digits, got: {tag}"
);
}
#[test]
fn tags_are_sequential() {
let mut gen = TagGenerator::new();
let tag1 = gen.next();
let tag2 = gen.next();
assert_eq!(
&tag1[..8],
&tag2[..8],
"prefix should not change between sequential tags"
);
let c1 = u32::from_str_radix(&tag1[8..], 16).unwrap();
let c2 = u32::from_str_radix(&tag2[8..], 16).unwrap();
assert_eq!(c2, c1 + 1, "counter should increment by 1");
}
#[test]
fn first_tag_counter_is_one() {
let mut gen = TagGenerator::new();
let tag = gen.next();
let counter = u32::from_str_radix(&tag[8..], 16).unwrap();
assert_eq!(counter, 1);
}
}