1use anyhow::{bail, Context, Result};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct DiscoveryCapabilities(pub u8);
6
7impl DiscoveryCapabilities {
8 pub fn has_soft_ap(self) -> bool { self.0 & 0x01 != 0 }
9 pub fn has_ble(self) -> bool { self.0 & 0x02 != 0 }
10 pub fn has_on_network(self) -> bool { self.0 & 0x04 != 0 }
11}
12
13#[derive(Debug)]
14pub struct OnboardingInfo {
15 pub discriminator: u16,
16 pub passcode: u32,
17 pub is_short_discriminator: bool,
19 pub vendor_id: Option<u16>,
21 pub product_id: Option<u16>,
23 pub discovery_capabilities: Option<DiscoveryCapabilities>,
25}
26
27const BASE38_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-.";
29
30fn base38_val(ch: char) -> Result<u32> {
31 BASE38_CHARS
32 .iter()
33 .position(|&c| c == ch as u8)
34 .map(|p| p as u32)
35 .ok_or_else(|| anyhow::anyhow!("invalid Base38 character '{}'", ch))
36}
37
38fn base38_decode(s: &str) -> Result<Vec<u8>> {
43 let chars: Vec<char> = s.chars().collect();
44 let mut out = Vec::with_capacity(chars.len() * 2 / 3 + 1);
45 let mut i = 0;
46 while i + 2 < chars.len() {
47 let v = base38_val(chars[i])? * 38 * 38
48 + base38_val(chars[i + 1])? * 38
49 + base38_val(chars[i + 2])?;
50 out.push((v & 0xff) as u8);
51 out.push(((v >> 8) & 0xff) as u8);
52 i += 3;
53 }
54 if i + 1 < chars.len() {
55 let v = base38_val(chars[i])? * 38 + base38_val(chars[i + 1])?;
56 out.push((v & 0xff) as u8);
57 i += 2;
58 }
59 if i < chars.len() {
60 bail!("unexpected Base38 input length");
61 }
62 Ok(out)
63}
64
65pub fn decode_qr_payload(qr: &str) -> Result<OnboardingInfo> {
77 let payload = qr.trim().strip_prefix("MT:").unwrap_or(qr.trim());
78 let bytes = base38_decode(payload).context("base38 decode")?;
79 if bytes.len() < 11 {
80 bail!("QR payload too short: {} bytes", bytes.len());
81 }
82
83 let mut bits: u128 = 0;
85 for (i, &b) in bytes.iter().take(11).enumerate() {
86 bits |= (b as u128) << (i * 8);
87 }
88
89 let _version = (bits & 0x7) as u8;
90 let vendor_id = ((bits >> 3) & 0xffff) as u16;
91 let product_id = ((bits >> 19) & 0xffff) as u16;
92 let _custom_flow = ((bits >> 35) & 0x3) as u8;
93 let disc_caps = ((bits >> 37) & 0x7f) as u8;
94 let discriminator = ((bits >> 44) & 0xfff) as u16;
95 let passcode = ((bits >> 56) & 0x7ff_ffff) as u32;
96
97 Ok(OnboardingInfo {
98 discriminator,
99 passcode,
100 is_short_discriminator: false,
101 vendor_id: Some(vendor_id),
102 product_id: Some(product_id),
103 discovery_capabilities: Some(DiscoveryCapabilities(disc_caps)),
104 })
105}
106
107pub fn decode_manual_pairing_code(code: &str) -> Result<OnboardingInfo> {
108 let norm = code.replace("-", "");
109 let first_grp = &norm[0..1];
110 let second_grp = &norm[1..6];
111 let third_grp = &norm[6..10];
112 let first = first_grp.parse::<u32>()?;
113 let second = second_grp.parse::<u32>()?;
114 let third = third_grp.parse::<u32>()?;
115 let passcode = second & 0x3fff | (third << 14);
116 let discriminator = (((first & 3) << 10) | (second >> 6) & 0x300) as u16;
117 Ok(OnboardingInfo {
118 discriminator,
119 passcode,
120 is_short_discriminator: true,
121 vendor_id: None,
122 product_id: None,
123 discovery_capabilities: None,
124 })
125}
126
127static D: [[u8; 10]; 10] = [
128 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
129 [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
130 [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
131 [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
132 [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
133 [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
134 [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
135 [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
136 [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
137 [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
138];
139
140static P: [[u8; 10]; 8] = [
142 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
143 [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
144 [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
145 [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
146 [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
147 [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
148 [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
149 [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
150];
151
152static INV: [u8; 10] = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
154
155fn verhoeff_checksum(num: &str) -> u8 {
156 let mut c: usize = 0;
157 for (i, ch) in num.chars().rev().enumerate() {
158 let digit = ch.to_digit(10).unwrap() as usize;
159 c = D[c][P[(i + 1) % 8][digit] as usize] as usize;
160 }
161 INV[c]
162}
163
164pub fn encode_manual_pairing_code(info: &OnboardingInfo) -> String {
165 let first = (info.discriminator as u32 >> 10) as u8;
166 let second = ((info.discriminator & 0x300) << 6) as u32 | (info.passcode & 0x3fff);
167 let third = info.passcode >> 14;
168 let digits = format!("{:01}{:05}{:04}", first, second, third);
169 let check = verhoeff_checksum(&digits);
170 let num = format!("{}{:05}{:04}{}", first, second, third, check);
171 let mut formatted = String::new();
173 for (i, ch) in num.chars().enumerate() {
174 if i > 0 && i % 4 == 0 {
175 formatted.push('-');
176 }
177 formatted.push(ch);
178 }
179 formatted
180}
181
182#[cfg(test)]
183mod tests {
184 use crate::onboarding::OnboardingInfo;
185
186 use super::decode_manual_pairing_code;
187 use super::encode_manual_pairing_code;
188
189 #[test]
190 pub fn test_1() {
191 let res = decode_manual_pairing_code("2585-103-3238").unwrap();
192 assert_eq!(res.discriminator, 2816);
193 assert_eq!(res.passcode, 54453390);
194 let encoded = encode_manual_pairing_code(&res);
195 assert_eq!(encoded.replace("-", ""), "25851033238");
196 }
197
198 #[test]
199 pub fn test_2() {
200 let res = decode_manual_pairing_code("34970112332").unwrap();
201 assert_eq!(res.discriminator, 3840);
202 assert_eq!(res.passcode, 20202021);
203 let encoded = encode_manual_pairing_code(&res);
204 assert_eq!(encoded.replace("-", ""), "34970112332");
205 }
206 #[test]
207 pub fn test_3() {
208 let oi = OnboardingInfo {
209 discriminator: 3840,
210 passcode: 123456,
211 is_short_discriminator: false,
212 vendor_id: None,
213 product_id: None,
214 discovery_capabilities: None,
215 };
216 let encoded = encode_manual_pairing_code(&oi);
217 println!("Encoded: {}", encoded);
218 }
219
220 #[test]
221 pub fn test_qr_decode() {
222 let info = super::decode_qr_payload("MT:00000003E6RM9A201").unwrap();
223 assert_eq!(info.passcode, 20202021, "passcode mismatch");
224 assert_eq!(info.discriminator, 3840, "discriminator mismatch");
225 assert_eq!(info.vendor_id, Some(0));
226 assert_eq!(info.product_id, Some(0));
227 let dc = info.discovery_capabilities.unwrap();
228 assert!(dc.has_on_network());
229 }
230}