use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
pub const SUBNET_TAG_PREFIX: &str = "subnet:";
#[expect(
clippy::derived_hash_with_manual_eq,
reason = "manual PartialEq is constant-time but byte-identical to the \
derived one; the Hash/Eq invariant (equal values hash equal) \
holds because both operate on the same 16 bytes"
)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Serialize, Deserialize)]
pub struct SubnetId(pub(crate) [u8; 16]);
impl PartialEq for SubnetId {
fn eq(&self, other: &Self) -> bool {
self.0.ct_eq(&other.0).into()
}
}
impl SubnetId {
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn from_tag(tag: &str) -> Option<Self> {
let hex_part = tag.strip_prefix(SUBNET_TAG_PREFIX)?;
let mut out = [0u8; 16];
hex::decode_to_slice(hex_part, &mut out).ok()?;
Some(Self(out))
}
pub fn to_tag(self) -> String {
format!("{SUBNET_TAG_PREFIX}{}", hex::encode(self.0))
}
}
impl std::fmt::Display for SubnetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_via_tag_form() {
let original = SubnetId([
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x0F, 0xED, 0xCB, 0xA9, 0x87, 0x65,
0x43, 0x21,
]);
let tag = original.to_tag();
assert_eq!(tag, "subnet:123456789abcdef00fedcba987654321");
let decoded = SubnetId::from_tag(&tag).expect("round trip");
assert_eq!(decoded, original);
}
#[test]
fn parse_rejects_missing_prefix() {
assert!(SubnetId::from_tag("123456789abcdef00fedcba987654321").is_none());
}
#[test]
fn parse_rejects_wrong_length() {
assert!(SubnetId::from_tag("subnet:123456789abcdef00fedcba98765432").is_none());
assert!(SubnetId::from_tag("subnet:123456789abcdef00fedcba9876543211").is_none());
}
#[test]
fn parse_rejects_non_hex_chars() {
assert!(SubnetId::from_tag("subnet:gg3456789abcdef00fedcba987654321").is_none());
assert!(SubnetId::from_tag("subnet: ").is_none());
}
#[test]
fn display_matches_tag_hex() {
let s = SubnetId([0xAB; 16]);
let displayed = format!("{}", s);
assert_eq!(displayed, "abababababababababababababababab");
assert_eq!(s.to_tag(), format!("{}{}", SUBNET_TAG_PREFIX, displayed));
}
#[test]
fn serde_round_trip_postcard() {
let s = SubnetId([0xCC; 16]);
let bytes = postcard::to_allocvec(&s).unwrap();
let decoded: SubnetId = postcard::from_bytes(&bytes).unwrap();
assert_eq!(decoded, s);
}
#[test]
fn constant_time_eq_preserves_equality_semantics() {
assert_eq!(SubnetId([0x11; 16]), SubnetId([0x11; 16]));
assert_ne!(SubnetId([0x00; 16]), SubnetId([0xFF; 16]));
let mut first_byte = [0x11; 16];
first_byte[0] = 0x12;
assert_ne!(SubnetId([0x11; 16]), SubnetId(first_byte));
let mut last_byte = [0x11; 16];
last_byte[15] = 0x12;
assert_ne!(SubnetId([0x11; 16]), SubnetId(last_byte));
let mut set = std::collections::HashSet::new();
set.insert(SubnetId([0x11; 16]));
assert!(set.contains(&SubnetId([0x11; 16])));
}
}