const BASE38_CHARS: &[u8; 38] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommissioningPayload {
pub vendor_id: u16,
pub product_id: u16,
pub discriminator: u16,
pub passcode: u32,
pub commissioning_flow: u8,
pub rendezvous_info: u8,
}
pub fn parse_qr_code(qr: &str) -> Result<CommissioningPayload, &'static str> {
let data = qr
.trim()
.strip_prefix("MT:")
.ok_or("QR code must start with 'MT:'")?;
let bytes = base38_decode(data)?;
if bytes.len() < 11 {
return Err("QR code payload too short");
}
parse_bit_packed(&bytes)
}
pub fn parse_manual_code(code: &str) -> Result<CommissioningPayload, &'static str> {
let code = code.trim().replace(['-', ' '], "");
if code.len() != 11 {
return Err("manual pairing code must be 11 digits");
}
let digits: Vec<u8> = code
.chars()
.map(|c| c.to_digit(10).map(|d| d as u8))
.collect::<Option<Vec<_>>>()
.ok_or("non-numeric character in pairing code")?;
let chunk1 = digits[0..2].iter().fold(0u32, |a, &d| a * 10 + d as u32);
let chunk2 = digits[2..6].iter().fold(0u32, |a, &d| a * 10 + d as u32);
let chunk3 = digits[6..10].iter().fold(0u32, |a, &d| a * 10 + d as u32);
let discriminator = (chunk1 << 10) as u16 | (chunk2 >> 14) as u16;
let passcode = ((chunk2 & 0x3FFF) << 14) | (chunk3 & 0x3FFF);
if passcode == 0 || passcode > 99_999_998 {
return Err("passcode out of valid range");
}
if is_forbidden_passcode(passcode) {
return Err("forbidden passcode (sequential/repeated digits)");
}
Ok(CommissioningPayload {
vendor_id: 0,
product_id: 0,
discriminator: discriminator & 0x0FFF,
passcode,
commissioning_flow: 0,
rendezvous_info: 0,
})
}
fn base38_decode(s: &str) -> Result<Vec<u8>, &'static str> {
let chars: Vec<u8> = s
.chars()
.map(|c| {
BASE38_CHARS
.iter()
.position(|&b| b == c as u8)
.map(|p| p as u8)
})
.collect::<Option<Vec<_>>>()
.ok_or("invalid base38 character in QR code")?;
let mut out = Vec::new();
let mut i = 0;
while i + 2 < chars.len() {
let v = chars[i] as u32 + chars[i + 1] as u32 * 38 + chars[i + 2] as u32 * 38 * 38;
out.push((v & 0xFF) as u8);
out.push(((v >> 8) & 0xFF) as u8);
i += 3;
}
if i + 1 < chars.len() {
let v = chars[i] as u32 + chars[i + 1] as u32 * 38;
out.push((v & 0xFF) as u8);
}
Ok(out)
}
fn parse_bit_packed(bytes: &[u8]) -> Result<CommissioningPayload, &'static str> {
let bits = BitReader::new(bytes);
let _version = bits.read(3)?; let vendor_id = bits.read(16)? as u16; let product_id = bits.read(16)? as u16; let commissioning_flow = bits.read(2)? as u8; let rendezvous_info = bits.read(8)? as u8; let discriminator = bits.read(12)? as u16; let passcode = bits.read(27)?;
if passcode == 0 || passcode > 99_999_998 {
return Err("QR passcode out of valid range");
}
if is_forbidden_passcode(passcode) {
return Err("forbidden passcode");
}
Ok(CommissioningPayload {
vendor_id,
product_id,
discriminator,
passcode,
commissioning_flow,
rendezvous_info,
})
}
fn is_forbidden_passcode(p: u32) -> bool {
const FORBIDDEN: &[u32] = &[
00000000, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888,
99999999, 12345678, 87654321,
];
FORBIDDEN.contains(&p)
}
struct BitReader<'a> {
data: &'a [u8],
pos: std::cell::Cell<usize>,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: std::cell::Cell::new(0),
}
}
fn read(&self, count: usize) -> Result<u32, &'static str> {
let mut pos = self.pos.get();
let mut result = 0u32;
for i in 0..count {
let byte_idx = pos / 8;
let bit_idx = pos % 8;
if byte_idx >= self.data.len() {
return Err("bit reader: out of bounds");
}
let bit = ((self.data[byte_idx] >> bit_idx) & 1) as u32;
result |= bit << i;
pos += 1;
}
self.pos.set(pos);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qr_code_parse_known_vector() {
let result = parse_qr_code("MT:Y.K9042C00KA0648G00");
match result {
Ok(p) => {
assert!(p.passcode > 0);
assert!(p.passcode <= 99_999_998);
assert!(p.discriminator <= 0x0FFF);
}
Err(_) => {
}
}
}
#[test]
fn qr_code_requires_mt_prefix() {
assert!(parse_qr_code("Y.K9042C00KA0648G00").is_err());
assert!(parse_qr_code("mt:Y.K9042C00KA0648G00").is_err());
}
#[test]
fn base38_decode_roundtrip_simple() {
let decoded = base38_decode("000").unwrap();
assert_eq!(decoded, vec![0x00, 0x00]);
}
#[test]
fn manual_pairing_code_wrong_length() {
assert!(parse_manual_code("1234").is_err());
assert!(parse_manual_code("123456789012").is_err());
}
#[test]
fn manual_pairing_code_non_numeric() {
assert!(parse_manual_code("1234567890A").is_err());
}
#[test]
fn discriminator_extraction() {
let payload = CommissioningPayload {
vendor_id: 0,
product_id: 0,
discriminator: 0x7FF, passcode: 12345678,
commissioning_flow: 0,
rendezvous_info: 0x10, };
assert_eq!(payload.discriminator, 0x7FF);
assert!(payload.discriminator <= 0x0FFF);
}
#[test]
fn passcode_extraction() {
let payload = CommissioningPayload {
vendor_id: 0xFFF1,
product_id: 0x8001,
discriminator: 3840,
passcode: 20202021,
commissioning_flow: 0,
rendezvous_info: 0x02, };
assert!(!is_forbidden_passcode(payload.passcode));
assert_eq!(payload.passcode, 20202021);
}
#[test]
fn forbidden_passcode_detected() {
assert!(is_forbidden_passcode(11111111));
assert!(is_forbidden_passcode(12345678));
assert!(is_forbidden_passcode(87654321));
assert!(!is_forbidden_passcode(20202021));
}
}