use super::codec::percent_encode;
pub fn build_qr_url(resolver_base: &str, gtin: &str, serial: &str, batch: Option<&str>) -> String {
let mut uri = format!("{}/01/{}", resolver_base.trim_end_matches('/'), gtin);
if let Some(b) = batch {
uri.push_str(&format!("/10/{}", percent_encode(b)));
}
uri.push_str(&format!("/21/{}", percent_encode(serial)));
uri
}
pub fn short_serial(uuid_bytes: &[u8; 16]) -> String {
let mut serial = String::with_capacity(20);
for &byte in &uuid_bytes[6..] {
serial.push_str(&format!("{byte:02x}"));
}
serial
}
#[cfg(test)]
mod tests {
use super::*;
use crate::digital_link::{DigitalLink, DigitalLinkError};
#[test]
fn short_serial_is_twenty_hex_chars() {
let uuid = [
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba,
0xdc, 0xfe,
];
let serial = short_serial(&uuid);
assert_eq!(serial, "cdef1032547698badcfe");
assert_eq!(serial.len(), 20);
assert!(serial.bytes().all(|b| b.is_ascii_hexdigit()));
}
#[test]
fn short_serial_is_deterministic() {
let uuid = [0x7f; 16];
assert_eq!(short_serial(&uuid), short_serial(&uuid));
}
#[test]
fn derived_serial_round_trips_through_parse() {
let uuid = [0xa1; 16];
let serial = short_serial(&uuid);
let url = build_qr_url("https://id.odal-node.io", "09506000134352", &serial, None);
let parsed = DigitalLink::parse(&url).expect("a derived serial must parse");
assert_eq!(parsed.serial.as_deref(), Some(serial.as_str()));
assert!(serial.chars().count() <= 20);
}
#[test]
fn raw_uuid_serial_is_rejected_by_parse() {
let url = build_qr_url(
"https://id.odal-node.io",
"09506000134352",
"0190a9f0-1234-7abc-8def-0123456789ab", None,
);
assert!(matches!(
DigitalLink::parse(&url),
Err(DigitalLinkError::ValueTooLong { code, max_len: 20, actual: 36 }) if code == "21"
));
}
#[test]
fn serial_does_not_embed_the_uuid_timestamp() {
let mut uuid = [0u8; 16];
uuid[..6].copy_from_slice(&[0x01, 0x9f, 0x99, 0xa0, 0xde, 0xad]);
uuid[6] = 0x7a;
uuid[7..].copy_from_slice(&[0xbc, 0x8d, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
let serial = short_serial(&uuid);
assert!(
!serial.contains("019f99a0dead"),
"serial must not carry the UUIDv7 timestamp: {serial}"
);
assert_eq!(serial.len(), 20);
}
#[test]
fn serials_do_not_order_by_creation_time() {
let mut earlier = [0u8; 16];
earlier[..6].copy_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x01]);
earlier[6..].copy_from_slice(&[0x7f, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
let mut later = [0u8; 16];
later[..6].copy_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x02]);
later[6..].copy_from_slice(&[0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
assert!(
short_serial(&later) < short_serial(&earlier),
"a later UUID must be free to sort before an earlier one"
);
}
#[test]
fn serial_uses_the_random_tail_not_the_timestamp_head() {
let mut a = [0u8; 16];
a[..6].copy_from_slice(&[0xaa; 6]);
a[6..].copy_from_slice(&[0x11; 10]);
let mut b = [0u8; 16];
b[..6].copy_from_slice(&[0xbb; 6]);
b[6..].copy_from_slice(&[0x11; 10]);
assert_eq!(short_serial(&a), short_serial(&b));
assert_eq!(short_serial(&a), "11".repeat(10));
}
}