use super::super::matter::clusters::VENDOR_ATTR_PERSON_COUNT as _;
#[derive(Debug, Clone, Copy)]
pub struct SetupCodeInput {
pub passcode: u32,
pub discriminator: u16,
pub vendor_id: u16,
pub product_id: u16,
}
impl SetupCodeInput {
pub fn dev(passcode: u32, discriminator: u16) -> Self {
Self { passcode, discriminator, vendor_id: 0xFFF1, product_id: 0x8001 }
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.passcode == 0
|| self.passcode == 11111111
|| self.passcode == 22222222
|| self.passcode == 33333333
|| self.passcode == 44444444
|| self.passcode == 55555555
|| self.passcode == 66666666
|| self.passcode == 77777777
|| self.passcode == 88888888
|| self.passcode == 99999999
|| self.passcode == 12345678
|| self.passcode == 87654321
{
return Err("passcode is in the §5.1.6.1 disallowed-values list");
}
if self.passcode >= 1 << 27 {
return Err("passcode exceeds 27-bit range");
}
if self.discriminator >= 1 << 12 {
return Err("discriminator exceeds 12-bit range");
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManualPairingCode(pub String);
impl ManualPairingCode {
pub fn from_input(input: &SetupCodeInput) -> Result<Self, &'static str> {
input.validate()?;
let disc = u32::from(input.discriminator);
let pin = input.passcode;
let vid_pid_present: u32 = 0;
let chunk0 = ((vid_pid_present << 2) | (disc >> 10)) as u64;
let chunk1 = (((disc & 0x300) << 6) | (pin & 0x3FFF)) as u64;
let chunk2 = ((pin >> 14) & 0x1FFF) as u64;
debug_assert!(chunk0 < 10, "chunk0 must be one digit");
debug_assert!(chunk1 < 100_000, "chunk1 must be five digits");
debug_assert!(chunk2 < 10_000, "chunk2 must be four digits");
let body = format!("{:01}{:05}{:04}", chunk0, chunk1, chunk2);
debug_assert_eq!(body.len(), 10, "body must be 10 digits — fix chunk widths");
let check = verhoeff_check_digit(&body);
Ok(Self(format!("{}{}", body, check)))
}
pub fn display_4_3_4(&self) -> String {
let s = &self.0;
format!("{}-{}-{}", &s[0..4], &s[4..7], &s[7..11])
}
pub fn decode(&self) -> Result<DecodedManualCode, &'static str> {
let s = &self.0;
if s.len() != 11 || !s.chars().all(|c| c.is_ascii_digit()) {
return Err("manual code must be exactly 11 ASCII digits");
}
let body = &s[0..10];
let given_check = s[10..11].parse::<u8>().map_err(|_| "bad check digit")?;
if verhoeff_check_digit(body) != given_check {
return Err("Verhoeff check digit mismatch");
}
let chunk0: u32 = body[0..1].parse().map_err(|_| "bad chunk0")?;
let chunk1: u32 = body[1..6].parse().map_err(|_| "bad chunk1")?;
let chunk2: u32 = body[6..10].parse().map_err(|_| "bad chunk2")?;
let vid_pid_present = (chunk0 >> 2) & 0x1;
let disc_hi2 = chunk0 & 0x3;
let disc_mid2 = (chunk1 >> 14) & 0x3;
let short_discriminator = ((disc_hi2 << 2) | disc_mid2) as u8;
let pin_low = chunk1 & 0x3FFF;
let pin_high = chunk2 & 0x1FFF;
let passcode = (pin_high << 14) | pin_low;
Ok(DecodedManualCode {
vid_pid_present: vid_pid_present != 0,
short_discriminator,
passcode,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodedManualCode {
pub vid_pid_present: bool,
pub short_discriminator: u8,
pub passcode: u32,
}
fn verhoeff_check_digit(s: &str) -> u8 {
const D: [[u8; 10]; 10] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
];
const P: [[u8; 10]; 8] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
];
const INV: [u8; 10] = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
let mut c = 0u8;
for (i, ch) in s.chars().rev().enumerate() {
let n = ch.to_digit(10).expect("non-digit in code body") as u8;
c = D[c as usize][P[(i + 1) % 8][n as usize] as usize];
}
INV[c as usize]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dev_constructor_uses_dev_vid_pid() {
let s = SetupCodeInput::dev(20202021, 3840);
assert_eq!(s.vendor_id, 0xFFF1);
assert_eq!(s.product_id, 0x8001);
assert_eq!(s.passcode, 20202021);
assert_eq!(s.discriminator, 3840);
}
#[test]
fn validate_rejects_disallowed_passcodes() {
for &bad in &[
0u32, 11111111, 22222222, 33333333, 44444444, 55555555,
66666666, 77777777, 88888888, 99999999, 12345678, 87654321,
] {
let s = SetupCodeInput::dev(bad, 100);
assert!(s.validate().is_err(), "passcode {} must be rejected", bad);
}
}
#[test]
fn validate_rejects_oversized_passcode() {
let s = SetupCodeInput::dev(1 << 27, 100);
assert!(s.validate().is_err());
}
#[test]
fn validate_rejects_oversized_discriminator() {
let s = SetupCodeInput::dev(20202021, 4096);
assert!(s.validate().is_err());
}
#[test]
fn validate_accepts_canonical_test_vectors() {
for (pin, disc) in &[(20202021u32, 3840u16), (12345678 + 1, 100), (1, 0)] {
let s = SetupCodeInput::dev(*pin, *disc);
assert!(s.validate().is_ok(), "({}, {}) should validate", pin, disc);
}
}
#[test]
fn manual_code_is_11_digits() {
let s = SetupCodeInput::dev(20202021, 3840);
let code = ManualPairingCode::from_input(&s).unwrap();
assert_eq!(code.0.len(), 11);
assert!(code.0.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn manual_code_display_format_is_4_3_4() {
let s = SetupCodeInput::dev(20202021, 3840);
let code = ManualPairingCode::from_input(&s).unwrap();
let pretty = code.display_4_3_4();
assert_eq!(pretty.len(), 13);
assert_eq!(&pretty[4..5], "-");
assert_eq!(&pretty[8..9], "-");
}
#[test]
fn manual_code_is_deterministic_for_same_input() {
let s = SetupCodeInput::dev(20202021, 3840);
let a = ManualPairingCode::from_input(&s).unwrap();
let b = ManualPairingCode::from_input(&s).unwrap();
assert_eq!(a, b);
}
#[test]
fn manual_code_differs_when_passcode_changes() {
let a = ManualPairingCode::from_input(&SetupCodeInput::dev(20202021, 3840))
.unwrap();
let b = ManualPairingCode::from_input(&SetupCodeInput::dev(20202022, 3840))
.unwrap();
assert_ne!(a, b);
}
#[test]
fn manual_code_differs_when_discriminator_changes() {
let a = ManualPairingCode::from_input(&SetupCodeInput::dev(20202021, 3840))
.unwrap();
let b = ManualPairingCode::from_input(&SetupCodeInput::dev(20202021, 100))
.unwrap();
assert_ne!(a, b);
}
#[test]
fn manual_code_matches_canonical_matter_vector() {
let s = SetupCodeInput::dev(20_202_021, 3840);
let code = ManualPairingCode::from_input(&s).unwrap();
assert_eq!(
code.0, "34970112332",
"encoder must match the canonical Matter reference vector"
);
assert_eq!(code.display_4_3_4(), "3497-011-2332");
}
#[test]
fn manual_code_decode_round_trips_passcode_and_short_discriminator() {
let passcode = 20_202_021u32;
let discriminator = 3840u16; let code =
ManualPairingCode::from_input(&SetupCodeInput::dev(passcode, discriminator)).unwrap();
let decoded = code.decode().unwrap();
assert!(!decoded.vid_pid_present);
assert_eq!(decoded.passcode, passcode, "passcode must round-trip exactly");
assert_eq!(
decoded.short_discriminator,
(discriminator >> 8) as u8,
"short discriminator = top 4 bits of the 12-bit discriminator"
);
}
#[test]
fn manual_code_decode_rejects_tampered_check_digit() {
let code = ManualPairingCode::from_input(&SetupCodeInput::dev(20_202_021, 3840)).unwrap();
let last = code.0[10..11].parse::<u8>().unwrap();
let tampered = format!("{}{}", &code.0[0..10], (last + 1) % 10);
let bad = ManualPairingCode(tampered);
assert!(bad.decode().is_err(), "tampered check digit must be rejected");
}
#[test]
fn verhoeff_check_digit_is_self_consistent() {
let s = SetupCodeInput::dev(20202021, 3840);
let code = ManualPairingCode::from_input(&s).unwrap();
let body = &code.0[0..10];
let check_recomputed = verhoeff_check_digit(body);
let body_digit = code.0[10..11].parse::<u8>().unwrap();
assert_eq!(check_recomputed, body_digit);
}
#[test]
fn from_input_rejects_invalid_input() {
let s = SetupCodeInput::dev(11111111, 3840);
assert!(ManualPairingCode::from_input(&s).is_err());
}
use proptest::prelude::*;
const DISALLOWED_PASSCODES: &[u32] = &[
0u32, 11111111, 22222222, 33333333, 44444444, 55555555,
66666666, 77777777, 88888888, 99999999, 12345678, 87654321,
];
proptest! {
#[test]
fn manual_code_shape_invariants(
passcode in 1u32..((1 << 27) - 1),
disc in 0u16..4095,
) {
prop_assume!(!DISALLOWED_PASSCODES.contains(&passcode));
let s = SetupCodeInput::dev(passcode, disc);
let code = ManualPairingCode::from_input(&s);
prop_assert!(code.is_ok(), "valid input rejected: {:?}", code.err());
let code = code.unwrap();
prop_assert_eq!(code.0.len(), 11);
prop_assert!(code.0.chars().all(|c| c.is_ascii_digit()));
let body = &code.0[0..10];
let body_digit = code.0[10..11].parse::<u8>().unwrap();
prop_assert_eq!(verhoeff_check_digit(body), body_digit);
let pretty = code.display_4_3_4();
prop_assert_eq!(pretty.len(), 13);
prop_assert_eq!(&pretty[4..5], "-");
prop_assert_eq!(&pretty[8..9], "-");
}
#[test]
fn disallowed_passcodes_always_rejected(
disc in 0u16..4095,
bad_idx in 0usize..DISALLOWED_PASSCODES.len(),
) {
let bad = DISALLOWED_PASSCODES[bad_idx];
let s = SetupCodeInput::dev(bad, disc);
prop_assert!(s.validate().is_err(), "passcode {} must be rejected", bad);
}
#[test]
fn oversized_inputs_always_rejected(
big_pin in (1u32 << 27)..u32::MAX,
big_disc in 4096u16..,
) {
prop_assert!(SetupCodeInput::dev(big_pin, 100).validate().is_err());
prop_assert!(SetupCodeInput::dev(20202021, big_disc).validate().is_err());
}
#[test]
fn manual_code_deterministic_under_random_input(
passcode in 1u32..((1 << 27) - 1),
disc in 0u16..4095,
) {
prop_assume!(!DISALLOWED_PASSCODES.contains(&passcode));
let s = SetupCodeInput::dev(passcode, disc);
let a = ManualPairingCode::from_input(&s).unwrap();
let b = ManualPairingCode::from_input(&s).unwrap();
prop_assert_eq!(a, b);
}
#[test]
fn manual_code_decode_round_trips_under_random_input(
passcode in 1u32..((1 << 27) - 1),
disc in 0u16..4095,
) {
prop_assume!(!DISALLOWED_PASSCODES.contains(&passcode));
let code =
ManualPairingCode::from_input(&SetupCodeInput::dev(passcode, disc)).unwrap();
let decoded = code.decode().unwrap();
prop_assert_eq!(decoded.passcode, passcode);
prop_assert_eq!(decoded.short_discriminator, (disc >> 8) as u8);
prop_assert!(!decoded.vid_pid_present);
}
}
}