use core::fmt::Write as _;
use crate::setup::{
verhoeff, CommissioningFlow, DiscoveryCapabilities, Discriminator, Error, Passcode, Result,
SetupPayload,
};
pub(super) fn pack(payload: &SetupPayload) -> String {
let has_vid_pid = payload.vendor_id.is_some() && payload.product_id.is_some();
let discriminator = u32::from(payload.discriminator.as_u16());
let passcode = payload.passcode.as_u32();
let chunk0 = (discriminator >> 10) | (u32::from(has_vid_pid) << 2);
let chunk1 = ((discriminator & 0x300) << 6) | (passcode & 0x3FFF);
let chunk2 = passcode >> 14;
let mut s = format!("{chunk0:01}{chunk1:05}{chunk2:04}");
if has_vid_pid {
let vid = payload.vendor_id.unwrap_or(0);
let pid = payload.product_id.unwrap_or(0);
let _ = write!(s, "{vid:05}{pid:05}");
}
s.push((verhoeff::check_digit(&s) + b'0') as char);
s
}
pub(super) fn unpack(s: &str) -> Result<SetupPayload> {
if s.len() != 11 && s.len() != 21 {
return Err(Error::ManualCodeWrongLength(s.len()));
}
for (i, ch) in s.char_indices() {
if !ch.is_ascii_digit() {
return Err(Error::ManualCodeNonDigit(ch, i));
}
}
if !verhoeff::verify(s) {
return Err(Error::ManualCodeBadChecksum);
}
let chunk0: u32 = s[0..1]
.parse()
.map_err(|_| Error::ManualCodeNonDigit('?', 0))?;
let chunk1: u32 = s[1..6]
.parse()
.map_err(|_| Error::ManualCodeNonDigit('?', 1))?;
let chunk2: u32 = s[6..10]
.parse()
.map_err(|_| Error::ManualCodeNonDigit('?', 6))?;
let has_vid_pid = ((chunk0 >> 2) & 0b1) == 1;
let short_upper = chunk0 & 0b11;
let short_lower = (chunk1 >> 14) & 0b11;
#[allow(clippy::cast_possible_truncation)] let short = ((short_upper << 2) | short_lower) as u16;
let passcode_lo = chunk1 & 0x3FFF; let passcode_hi = chunk2 & 0x1FFF; let passcode = passcode_lo | (passcode_hi << 14);
let (vendor_id, product_id) = if has_vid_pid {
if s.len() != 21 {
return Err(Error::ManualCodeWrongLength(s.len()));
}
let vid: u32 = s[10..15]
.parse()
.map_err(|_| Error::ManualCodeNonDigit('?', 10))?;
let pid: u32 = s[15..20]
.parse()
.map_err(|_| Error::ManualCodeNonDigit('?', 15))?;
let vid = u16::try_from(vid).map_err(|_| Error::FieldOutOfRange {
field: "vendor_id",
value: vid,
})?;
let pid = u16::try_from(pid).map_err(|_| Error::FieldOutOfRange {
field: "product_id",
value: pid,
})?;
(Some(vid), Some(pid))
} else {
if s.len() != 11 {
return Err(Error::ManualCodeWrongLength(s.len()));
}
(None, None)
};
let long_discriminator = short << 8;
Ok(SetupPayload {
version: 0,
vendor_id,
product_id,
commissioning_flow: CommissioningFlow::Standard,
discovery_capabilities: DiscoveryCapabilities::empty(),
discriminator: Discriminator::new(long_discriminator)?,
passcode: Passcode::new(passcode)?,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::{pack, unpack};
use crate::setup::*;
fn payload_11(short_disc: u8, passcode: u32) -> SetupPayload {
SetupPayload {
version: 0,
vendor_id: None,
product_id: None,
commissioning_flow: CommissioningFlow::Standard,
discovery_capabilities: DiscoveryCapabilities::empty(),
discriminator: Discriminator::new(u16::from(short_disc) << 8).unwrap(),
passcode: Passcode::new(passcode).unwrap(),
}
}
fn payload_21(short_disc: u8, passcode: u32, vid: u16, pid: u16) -> SetupPayload {
SetupPayload {
version: 0,
vendor_id: Some(vid),
product_id: Some(pid),
commissioning_flow: CommissioningFlow::Standard,
discovery_capabilities: DiscoveryCapabilities::empty(),
discriminator: Discriminator::new(u16::from(short_disc) << 8).unwrap(),
passcode: Passcode::new(passcode).unwrap(),
}
}
#[test]
fn pack_matches_canonical_chip_test_vector() {
let s = pack(&payload_11(0x0F, 20_202_021));
assert_eq!(s, "34970112332");
}
#[test]
fn pack_11_digits_length_is_11() {
let s = pack(&payload_11(0xA, 20_202_021));
assert_eq!(s.len(), 11);
assert!(s.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn pack_21_digits_length_is_21() {
let s = pack(&payload_21(0xA, 20_202_021, 0xFFF1, 0x8000));
assert_eq!(s.len(), 21);
assert!(s.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn pack_unpack_roundtrip_11() {
let p = payload_11(0xA, 20_202_021);
let s = pack(&p);
let back = unpack(&s).unwrap();
assert_eq!(back, p);
}
#[test]
fn pack_unpack_roundtrip_21() {
let p = payload_21(0xA, 20_202_021, 0xFFF1, 0x8000);
let s = pack(&p);
let back = unpack(&s).unwrap();
assert_eq!(back, p);
}
#[test]
fn unpack_rejects_wrong_length() {
assert!(matches!(
unpack("123").unwrap_err(),
Error::ManualCodeWrongLength(3)
));
assert!(matches!(
unpack("123456789012345").unwrap_err(),
Error::ManualCodeWrongLength(15)
));
}
#[test]
fn unpack_rejects_non_digit() {
assert!(matches!(
unpack("3497011233A").unwrap_err(),
Error::ManualCodeNonDigit('A', 10)
));
}
#[test]
fn unpack_rejects_bad_checksum() {
let mut s = pack(&payload_11(0xA, 20_202_021));
let last = s.pop().unwrap();
let bad = if last == '0' { '1' } else { '0' };
s.push(bad);
assert!(matches!(
unpack(&s).unwrap_err(),
Error::ManualCodeBadChecksum
));
}
#[test]
fn unpack_rejects_out_of_range_vid() {
let mut s = pack(&payload_21(0xA, 20_202_021, 0x1234, 0x5678));
s.truncate(20); s.replace_range(10..15, "99999"); s.push((super::verhoeff::check_digit(&s) + b'0') as char);
match unpack(&s).unwrap_err() {
Error::FieldOutOfRange { field, value } => {
assert_eq!(field, "vendor_id");
assert_eq!(value, 99999);
}
other => panic!("expected FieldOutOfRange, got {other:?}"),
}
}
#[test]
fn unpack_rejects_out_of_range_pid() {
let mut s = pack(&payload_21(0xA, 20_202_021, 0x1234, 0x5678));
s.truncate(20);
s.replace_range(15..20, "70000"); s.push((super::verhoeff::check_digit(&s) + b'0') as char);
match unpack(&s).unwrap_err() {
Error::FieldOutOfRange { field, value } => {
assert_eq!(field, "product_id");
assert_eq!(value, 70000);
}
other => panic!("expected FieldOutOfRange, got {other:?}"),
}
}
#[test]
fn unpack_accepts_in_range_vid_pid_boundary() {
let p = payload_21(0xA, 20_202_021, 0xFFFF, 0xFFFF);
let s = pack(&p);
let back = unpack(&s).unwrap();
assert_eq!(back, p);
}
#[test]
fn roundtrip_edge_short_discriminators() {
for short in 0u8..=0xF {
let p = payload_11(short, 20_202_021);
let s = pack(&p);
let back = unpack(&s).unwrap();
assert_eq!(back, p, "failed at short=0x{short:x}");
}
}
#[test]
fn roundtrip_edge_passcodes() {
for passcode in [1u32, 12_345_679, super::super::MAX_PASSCODE] {
if super::super::DISALLOWED_PASSCODES.contains(&passcode) {
continue;
}
let p = payload_11(0x5, passcode);
let s = pack(&p);
let back = unpack(&s).unwrap();
assert_eq!(back, p);
}
}
}