Skip to main content

rs_matter/pairing/
qr.rs

1/*
2 *
3 *    Copyright (c) 2023-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18use core::iter::Empty;
19
20use qrcodegen_no_heap::{QrCode, QrCodeEcc, Version};
21
22use verhoeff::Verhoeff;
23
24use crate::error::ErrorCode;
25use crate::tlv::{EitherIter, TLVElement, TLVTag, TLV};
26use crate::transport::network::mdns::CommissionableFilter;
27use crate::utils::codec::base38;
28use crate::utils::storage::WriteBuf;
29
30use super::*;
31
32#[cfg(feature = "qr-scan")]
33pub mod scan;
34
35/// The prefix of a Matter onboarding QR-code text payload.
36pub const QR_PREFIX: &str = "MT:";
37
38// See the spec. QR Code in the Matter specification
39const LONG_BITS: usize = 12;
40const VERSION_FIELD_LENGTH_IN_BITS: usize = 3;
41const VENDOR_IDFIELD_LENGTH_IN_BITS: usize = 16;
42const PRODUCT_IDFIELD_LENGTH_IN_BITS: usize = 16;
43const COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS: usize = 2;
44const RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS: usize = 8;
45const PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS: usize = LONG_BITS;
46const SETUP_PINCODE_FIELD_LENGTH_IN_BITS: usize = 27;
47const PADDING_FIELD_LENGTH_IN_BITS: usize = 4;
48const TOTAL_PAYLOAD_DATA_SIZE_IN_BITS: usize = VERSION_FIELD_LENGTH_IN_BITS
49    + VENDOR_IDFIELD_LENGTH_IN_BITS
50    + PRODUCT_IDFIELD_LENGTH_IN_BITS
51    + COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS
52    + RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS
53    + PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS
54    + SETUP_PINCODE_FIELD_LENGTH_IN_BITS
55    + PADDING_FIELD_LENGTH_IN_BITS;
56
57pub const TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES: usize = TOTAL_PAYLOAD_DATA_SIZE_IN_BITS / 8;
58
59// Spec CHIP-Common Reserved Tags
60pub const SERIAL_NUMBER_TAG: u8 = 0x00;
61pub const PBKDFITERATIONS_TAG: u8 = 0x01;
62pub const BPKFSALT_TAG: u8 = 0x02;
63pub const NUMBER_OFDEVICES_TAG: u8 = 0x03;
64pub const COMMISSIONING_TIMEOUT_TAG: u8 = 0x04;
65
66/// Commissioning flow type as per the Matter Core spec
67#[repr(u8)]
68#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70pub enum CommFlowType {
71    /// Standard commissioning flow
72    Standard = 0,
73    /// Enhanced commissioning flow with user intent
74    UserIntent = 1,
75    /// Custom commissioning flow
76    Custom = 2,
77}
78
79/// Type alias for no optional data function for the QR payload
80pub type NoOptionalData = fn() -> Empty<Result<u8, Error>>;
81
82/// Function that provides no optional data for the QR payload
83pub fn no_optional_data() -> Empty<Result<u8, Error>> {
84    core::iter::empty()
85}
86
87/// QR Code payload type
88#[derive(Debug, Clone)]
89#[cfg_attr(feature = "defmt", derive(defmt::Format))]
90pub struct QrPayload<'a, T> {
91    /// Payload version. Always 0
92    version: u8,
93    /// Discovery capabilities of the device
94    discovery_capabilities: DiscoveryCapabilities,
95    /// Commissioning flow type
96    comm_flow: CommFlowType,
97    /// Basic commissioning data
98    comm_data: BasicCommData,
99    /// Vendor ID
100    vid: u16,
101    /// Product ID
102    pid: u16,
103    /// Serial number of the device
104    serial_no: &'a str,
105    /// Optional extra data
106    /// The data must be ordered by the tag of each TLV element in ascending order.
107    optional_data: T,
108}
109
110impl<'a, T, I> QrPayload<'a, T>
111where
112    T: Fn() -> I,
113    I: Iterator<Item = Result<u8, Error>> + 'a,
114{
115    /// Create a new QR payload from the device basic info config
116    ///
117    /// # Arguments
118    /// - `discovery_capabilities` - Discovery capabilities of the device
119    /// - `comm_flow` - Commissioning flow type
120    /// - `comm_data` - Basic commissioning data
121    /// - `dev_det` - Device basic info config
122    /// - `optional_data` - Function that provides an iterator over optional TLV data bytes.
123    ///   NOTE: Should be ordered by tag number in ascending order.
124    pub const fn new_from_basic_info(
125        discovery_capabilities: DiscoveryCapabilities,
126        comm_flow: CommFlowType,
127        comm_data: BasicCommData,
128        dev_det: &'a BasicInfoConfig,
129        optional_data: T,
130    ) -> Self {
131        Self::new(
132            discovery_capabilities,
133            comm_flow,
134            comm_data,
135            dev_det.vid,
136            dev_det.pid,
137            dev_det.serial_no,
138            optional_data,
139        )
140    }
141
142    /// Create a new QR payload
143    ///
144    /// # Arguments
145    /// - `discovery_capabilities` - Discovery capabilities of the device
146    /// - `comm_flow` - Commissioning flow type
147    /// - `comm_data` - Basic commissioning data
148    /// - `vid` - Vendor ID
149    /// - `pid` - Product ID
150    /// - `serial_no` - Serial number of the device
151    /// - `optional_data` - Function that provides an iterator over optional TLV data bytes.
152    ///   NOTE: Should be ordered by tag number in ascending order.
153    pub const fn new(
154        discovery_capabilities: DiscoveryCapabilities,
155        comm_flow: CommFlowType,
156        comm_data: BasicCommData,
157        vid: u16,
158        pid: u16,
159        serial_no: &'a str,
160        optional_data: T,
161    ) -> Self {
162        const DEFAULT_VERSION: u8 = 0;
163
164        Self {
165            version: DEFAULT_VERSION,
166            discovery_capabilities,
167            comm_flow,
168            comm_data,
169            vid,
170            pid,
171            serial_no,
172            optional_data,
173        }
174    }
175
176    /// Check if the QR payload is valid
177    ///
178    /// # Returns
179    /// - `true` if the payload is valid
180    /// - `false` otherwise
181    pub fn is_valid(&self) -> bool {
182        // 3-bit value specifying the QR code payload version.
183        if self.version >= 1 << VERSION_FIELD_LENGTH_IN_BITS {
184            return false;
185        }
186
187        if self.discovery_capabilities.is_empty() {
188            return false;
189        }
190
191        let password = u32::from_le_bytes(*self.comm_data.password.access());
192        if password >= 1 << SETUP_PINCODE_FIELD_LENGTH_IN_BITS {
193            return false;
194        }
195
196        self.check_payload_common_constraints()
197    }
198
199    fn check_payload_common_constraints(&self) -> bool {
200        #[repr(u16)]
201        enum VendorId {
202            CommonOrUnspecified = 0x0000,
203            TestVendor4 = 0xFFF4,
204        }
205
206        impl VendorId {
207            fn is_valid_operationally(vendor_id: u16) -> bool {
208                (vendor_id != Self::CommonOrUnspecified as u16)
209                    && (vendor_id <= Self::TestVendor4 as u16)
210            }
211        }
212
213        // A version not equal to 0 would be invalid for v1 and would indicate new format (e.g. version 2)
214        if self.version != 0 {
215            return false;
216        }
217
218        if !Self::is_valid_setup_pin(u32::from_le_bytes(*self.comm_data.password.access())) {
219            return false;
220        }
221
222        // VendorID must be unspecified (0) or in valid range expected.
223        if VendorId::is_valid_operationally(self.vid)
224            && (self.vid != VendorId::CommonOrUnspecified as u16)
225        {
226            return false;
227        }
228
229        // A value of 0x0000 SHALL NOT be assigned to a product since Product ID = 0x0000 is used for these specific cases:
230        //  * To announce an anonymized Product ID as part of device discovery
231        //  * To indicate an OTA software update file applies to multiple Product IDs equally.
232        //  * To avoid confusion when presenting the Onboarding Payload for ECM with multiple nodes
233        if self.pid == 0 && self.vid != VendorId::CommonOrUnspecified as u16 {
234            return false;
235        }
236
237        true
238    }
239
240    fn is_valid_setup_pin(setup_pin: u32) -> bool {
241        const SETUP_PINCODE_MAXIMUM_VALUE: u32 = 99999998;
242        const SETUP_PINCODE_UNDEFINED_VALUE: u32 = 0;
243
244        // SHALL be restricted to the values 0x0000001 to 0x5F5E0FE (00000001 to 99999998 in decimal), excluding the invalid Passcode
245        // values.
246        if setup_pin == SETUP_PINCODE_UNDEFINED_VALUE
247            || setup_pin > SETUP_PINCODE_MAXIMUM_VALUE
248            || setup_pin == 11111111
249            || setup_pin == 22222222
250            || setup_pin == 33333333
251            || setup_pin == 44444444
252            || setup_pin == 55555555
253            || setup_pin == 66666666
254            || setup_pin == 77777777
255            || setup_pin == 88888888
256            || setup_pin == 12345678
257            || setup_pin == 87654321
258        {
259            return false;
260        }
261
262        true
263    }
264
265    /// Encode the QR text of this payload as a string into the provided buffer
266    ///
267    /// # Arguments
268    /// - `buf` - Buffer to store the QR code string
269    ///
270    /// # Returns
271    /// - On success, returns a tuple containing the QR code string and the remaining buffer
272    /// - On failure, returns an error
273    pub fn as_str<'b>(&self, buf: &'b mut [u8]) -> Result<(&'b str, &'b mut [u8]), Error> {
274        let str_len = self.emit_chars().count();
275
276        let (str_buf, remaining_buf) = buf.split_at_mut(str_len);
277
278        let mut wb = WriteBuf::new(str_buf);
279        for ch in self.emit_chars() {
280            wb.le_u8(ch? as u8)?;
281        }
282
283        // Can't fail as `emit_chars` generates a valid UTF-8 string
284        let str = unwrap!(core::str::from_utf8(str_buf).map_err(|_| ErrorCode::InvalidData));
285
286        Ok((str, remaining_buf))
287    }
288
289    /// Emit the QR text of this payload as an iterator of characters
290    pub fn emit_chars(&self) -> impl Iterator<Item = Result<char, Error>> + '_ {
291        struct PackedBitsIterator<I>(I);
292
293        impl<I> Iterator for PackedBitsIterator<I>
294        where
295            I: Iterator<Item = Result<bool, Error>>,
296        {
297            type Item = Result<(u32, u8), Error>;
298
299            fn next(&mut self) -> Option<Self::Item> {
300                let mut chunk = 0;
301                let mut packed_bits = 0;
302
303                for index in 0..24 {
304                    // Up to 24 bits as we are enclding with Base38, which means up to 3 bytes at once
305                    if let Some(bit) = self.0.next() {
306                        let bit = match bit {
307                            Ok(bit) => bit,
308                            Err(err) => return Some(Err(err)),
309                        };
310
311                        chunk |= (bit as u32) << index;
312                        packed_bits += 1;
313                    } else {
314                        break;
315                    }
316                }
317
318                if packed_bits > 0 {
319                    assert!(packed_bits % 8 == 0);
320
321                    Some(Ok((chunk, packed_bits)))
322                } else {
323                    None
324                }
325            }
326        }
327
328        "MT:"
329            .chars()
330            .map(Result::Ok)
331            .chain(
332                PackedBitsIterator(self.emit_all_bits()).flat_map(|bits| match bits {
333                    Ok((bits, bits_count)) => {
334                        EitherIter::First(base38::encode_bits(bits, bits_count).map(Result::Ok))
335                    }
336                    Err(err) => EitherIter::Second(core::iter::once(Err(err))),
337                }),
338            )
339    }
340
341    fn emit_all_bits(&self) -> impl Iterator<Item = Result<bool, Error>> + '_ {
342        Self::emit_bits(self.version as _, VERSION_FIELD_LENGTH_IN_BITS)
343            .chain(Self::emit_bits(
344                self.vid as _,
345                VENDOR_IDFIELD_LENGTH_IN_BITS,
346            ))
347            .chain(Self::emit_bits(
348                self.pid as _,
349                PRODUCT_IDFIELD_LENGTH_IN_BITS,
350            ))
351            .chain(Self::emit_bits(
352                self.comm_flow as _,
353                COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS,
354            ))
355            .chain(Self::emit_bits(
356                self.discovery_capabilities.bits() as _,
357                RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS,
358            ))
359            .chain(Self::emit_bits(
360                self.comm_data.discriminator as _,
361                PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS,
362            ))
363            .chain(Self::emit_bits(
364                u32::from_le_bytes(*self.comm_data.password.access()),
365                SETUP_PINCODE_FIELD_LENGTH_IN_BITS,
366            ))
367            .chain(Self::emit_bits(0, PADDING_FIELD_LENGTH_IN_BITS))
368            .chain(
369                self.emit_optional_tlv_data()
370                    .flat_map(|bits| Self::emit_maybe_bits(bits.map(|bits| (bits as _, 8)))),
371            )
372    }
373
374    fn emit_bits(input: u32, len: usize) -> impl Iterator<Item = Result<bool, Error>> {
375        (0..len).map(move |i| Ok((input >> i) & 1 == 1))
376    }
377
378    fn emit_maybe_bits(
379        bits: Result<(u32, usize), Error>,
380    ) -> impl Iterator<Item = Result<bool, Error>> {
381        match bits {
382            Ok((input, len)) => EitherIter::First(Self::emit_bits(input, len)),
383            Err(err) => EitherIter::Second(core::iter::once(Err(err))),
384        }
385    }
386
387    fn emit_optional_tlv_data(&self) -> impl Iterator<Item = Result<u8, Error>> + '_ {
388        if self.serial_no.is_empty() && (self.optional_data)().next().is_none() {
389            return EitherIter::First(core::iter::empty());
390        }
391
392        let serial_no = if self.serial_no.is_empty() {
393            EitherIter::First(core::iter::empty())
394        } else {
395            EitherIter::Second(
396                TLV::utf8(TLVTag::Context(SERIAL_NUMBER_TAG), self.serial_no).into_tlv_iter(),
397            )
398        };
399
400        EitherIter::Second(
401            TLV::structure(TLVTag::Anonymous)
402                .into_tlv_iter()
403                .chain(serial_no)
404                .flat_map(TLV::result_into_bytes_iter)
405                .chain((self.optional_data)())
406                .chain(
407                    TLV::end_container()
408                        .into_tlv_iter()
409                        .flat_map(TLV::result_into_bytes_iter),
410                ),
411        )
412    }
413}
414
415impl<'a> QrPayload<'a, &'a [u8]> {
416    /// Parse a Matter onboarding QR-code text (an `MT:` payload) into a [`QrPayload`].
417    ///
418    /// This is the inverse of the [`QrPayload::as_str`] / [`QrPayload::emit_chars`]
419    /// encoding path, and the entry point a commissioner uses to turn a scanned QR
420    /// string into the discriminator, passcode, VID/PID etc. it needs to commission
421    /// the device.
422    ///
423    /// `buf` is a scratch buffer that the parsed payload borrows from: the trailing
424    /// optional-TLV data (and hence any serial number decoded out of it) points into
425    /// it, so `buf` must outlive the returned payload. A buffer of
426    /// [`TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES`] plus the optional-data length is enough;
427    /// the base38 body never decodes to more bytes than the input string.
428    ///
429    /// The returned payload's `optional_data` is the raw optional-TLV byte slice (an
430    /// anonymous TLV structure), which is empty when the QR carries no optional data.
431    ///
432    /// # Errors
433    /// Returns [`ErrorCode::InvalidData`] if the string is not a well-formed `MT:`
434    /// payload (missing prefix, invalid base38, too short, or an out-of-range field).
435    pub fn parse(qr: &str, buf: &'a mut [u8]) -> Result<Self, Error> {
436        // Strip the `MT:` prefix.
437        let body = qr.strip_prefix(QR_PREFIX).ok_or(ErrorCode::InvalidData)?;
438
439        // Base38-decode the body into `buf`.
440        let mut len = 0;
441        for byte in base38::decode(body) {
442            let byte = byte?;
443            *buf.get_mut(len).ok_or(ErrorCode::BufferTooSmall)? = byte;
444            len += 1;
445        }
446        let decoded = &buf[..len];
447
448        // The fixed part of the payload is `TOTAL_PAYLOAD_DATA_SIZE_IN_BITS` bits
449        // (`TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES` bytes); anything beyond it is the
450        // optional-TLV data.
451        if decoded.len() < TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES {
452            return Err(ErrorCode::InvalidData.into());
453        }
454
455        let mut reader = BitReader::new(decoded);
456
457        // Read the fixed fields in the same order (and LSB-first bit order) as
458        // `emit_all_bits` writes them.
459        let version = reader.read(VERSION_FIELD_LENGTH_IN_BITS)? as u8;
460        let vid = reader.read(VENDOR_IDFIELD_LENGTH_IN_BITS)? as u16;
461        let pid = reader.read(PRODUCT_IDFIELD_LENGTH_IN_BITS)? as u16;
462        let comm_flow =
463            CommFlowType::from_bits(reader.read(COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS)? as u8)?;
464        let discovery_capabilities = DiscoveryCapabilities::from_bits_truncate(
465            reader.read(RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS)? as u8,
466        );
467        let discriminator = reader.read(PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS)? as u16;
468        let passcode = reader.read(SETUP_PINCODE_FIELD_LENGTH_IN_BITS)?;
469        // Padding bits (must be present but are ignored).
470        let _ = reader.read(PADDING_FIELD_LENGTH_IN_BITS)?;
471
472        // The remaining whole bytes are the optional-TLV data.
473        let optional_data = &decoded[TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES..];
474
475        // If present, the serial number is a context-`SERIAL_NUMBER_TAG` UTF-8 string
476        // in the anonymous optional-TLV structure. Borrow it out of the blob.
477        let serial_no = Self::parse_serial_no(optional_data).unwrap_or("");
478
479        let payload = Self {
480            version,
481            discovery_capabilities,
482            comm_flow,
483            comm_data: BasicCommData {
484                password: passcode.to_le_bytes().into(),
485                discriminator,
486            },
487            vid,
488            pid,
489            serial_no,
490            optional_data,
491        };
492
493        Ok(payload)
494    }
495
496    /// Extract the serial number (context tag [`SERIAL_NUMBER_TAG`]) from the
497    /// optional-TLV structure, if present.
498    fn parse_serial_no(optional_data: &'a [u8]) -> Option<&'a str> {
499        if optional_data.is_empty() {
500            return None;
501        }
502
503        let root = TLVElement::new(optional_data);
504        let serial = root.structure().ok()?.find_ctx(SERIAL_NUMBER_TAG).ok()?;
505
506        serial.utf8().ok()
507    }
508
509    /// The payload version (always 0 for v1 QR codes).
510    pub const fn version(&self) -> u8 {
511        self.version
512    }
513
514    /// The device's advertised discovery capabilities.
515    pub const fn discovery_capabilities(&self) -> DiscoveryCapabilities {
516        self.discovery_capabilities
517    }
518
519    /// The commissioning flow type.
520    pub const fn comm_flow(&self) -> CommFlowType {
521        self.comm_flow
522    }
523
524    /// The 12-bit discriminator.
525    pub const fn discriminator(&self) -> u16 {
526        self.comm_data.discriminator
527    }
528
529    /// The setup passcode (PIN).
530    pub fn passcode(&self) -> u32 {
531        u32::from_le_bytes(*self.comm_data.password.access())
532    }
533
534    /// The Vendor ID.
535    pub const fn vid(&self) -> u16 {
536        self.vid
537    }
538
539    /// The Product ID.
540    pub const fn pid(&self) -> u16 {
541        self.pid
542    }
543
544    /// The serial number, or an empty string if the QR carries none.
545    pub const fn serial_no(&self) -> &'a str {
546        self.serial_no
547    }
548
549    /// The raw optional-TLV data (an anonymous TLV structure), empty if absent.
550    pub const fn optional_data(&self) -> &'a [u8] {
551        self.optional_data
552    }
553
554    /// A [`CommissionableFilter`] that discovers the device this QR code describes.
555    ///
556    /// A QR code carries the **full 12-bit** discriminator, so this filters on
557    /// `discriminator` and can narrow discovery to a single device. (Contrast
558    /// [`QrPayload::<()>::commissionable_filter`], built from a manual pairing code,
559    /// which can only filter on the short discriminator.)
560    ///
561    /// Only the discriminator is set. The vendor and product IDs are deliberately
562    /// *not* included even though the QR carries them: per the Matter Core spec a
563    /// device may advertise an **anonymized** Product ID of 0 during discovery, so
564    /// filtering on the QR's PID would fail to find such a device. Add them to the
565    /// returned filter if the extra selectivity is wanted and the device is known
566    /// not to anonymize.
567    pub fn commissionable_filter(&self) -> CommissionableFilter {
568        CommissionableFilter {
569            discriminator: Some(self.discriminator()),
570            ..Default::default()
571        }
572    }
573}
574
575impl<'a> QrPayload<'a, ()> {
576    /// Parse a Matter **manual pairing code** (the 11- or 21-digit decimal string
577    /// printed on a device, e.g. `34970112332` / `3497-0112-332`) into a
578    /// [`QrPayload`].
579    ///
580    /// This is the "typed by a human" onboarding format, and it is the counterpart
581    /// of [`BasicCommData::compute_pairing_code`]. It is deliberately a *different*
582    /// `T` from [`QrPayload::parse`] (`()` rather than `&[u8]`), because a manual
583    /// pairing code carries strictly less information than a QR code, and the
584    /// resulting payload must not be mistaken for one:
585    ///
586    /// - Only the **upper 4 bits** of the discriminator are carried (the "short
587    ///   discriminator"). Per the Matter Core spec: *"For machine-readable formats,
588    ///   the full 12-bit Discriminator is used. For the Manual Pairing Code, only
589    ///   the upper 4 bits out of the 12-bit Discriminator are used."* Read it via
590    ///   [`Self::short_discriminator`] - there is deliberately no `discriminator()`
591    ///   accessor on this `T`, so a 4-bit value can never be mistaken for a 12-bit
592    ///   one.
593    /// - **No discovery capabilities** are carried, so a commissioner cannot know
594    ///   whether to look for the device over BLE, SoftAP or IP, and must try all of
595    ///   them.
596    /// - **No serial number and no optional TLV data** are carried (hence `T = ()`).
597    /// - The commissioning flow is only *implied* - see [`Self::comm_flow`].
598    ///
599    /// The Verhoeff check digit is verified. Separators (`-` and spaces) are
600    /// ignored, so both the compact and the "pretty" printed forms are accepted.
601    ///
602    /// # Errors
603    /// Returns [`ErrorCode::InvalidData`] if the code is not a well-formed v1 manual
604    /// pairing code: wrong length, a non-digit, a bad check digit, a first digit of
605    /// 8 or 9 (which per spec indicates a future format version), a VID/PID-present
606    /// flag inconsistent with the length, or an out-of-range digit group.
607    pub fn parse_pairing_code(code: &str) -> Result<Self, Error> {
608        /// Length of the manual pairing code without vendor/product IDs.
609        const SHORT_CODE_LEN: usize = 11;
610        /// Length of the manual pairing code with vendor/product IDs.
611        const LONG_CODE_LEN: usize = 21;
612
613        // Strip the separators of the "pretty" form (e.g. `3497-0112-332`).
614        let mut digits: heapless::String<LONG_CODE_LEN> = heapless::String::new();
615        for ch in code.chars() {
616            if matches!(ch, '-' | ' ') {
617                continue;
618            }
619
620            if !ch.is_ascii_digit() {
621                return Err(ErrorCode::InvalidData.into());
622            }
623
624            digits
625                .push(ch)
626                .map_err(|_| Error::from(ErrorCode::InvalidData))?;
627        }
628
629        let long_form = match digits.len() {
630            SHORT_CODE_LEN => false,
631            LONG_CODE_LEN => true,
632            _ => return Err(ErrorCode::InvalidData.into()),
633        };
634
635        // The trailing check digit protects against typos and transpositions.
636        if !digits.validate_verhoeff_check_digit() {
637            return Err(ErrorCode::InvalidData.into());
638        }
639
640        // DIGIT[1] := (VID_PID_PRESENT << 2) | (DISCRIMINATOR >> 10)
641        //
642        // A leading digit of 8 or 9 is invalid for v1 and would indicate a future
643        // payload version.
644        let digit1 = Self::digits_at(&digits, 0, 1)?;
645        if digit1 > 7 {
646            return Err(ErrorCode::InvalidData.into());
647        }
648
649        let vid_pid_present = digit1 >> 2 == 1;
650        // The flag and the code length must agree.
651        if vid_pid_present != long_form {
652            return Err(ErrorCode::InvalidData.into());
653        }
654
655        // The two most significant bits (11..10) of the discriminator.
656        let disc_bits_11_10 = (digit1 & 0x3) as u16;
657
658        // DIGIT[2..6] := ((DISCRIMINATOR & 0x300) << 6) | (PASSCODE & 0x3FFF)
659        let group = Self::digits_at(&digits, 1, 5)?;
660        if group > 0xFFFF {
661            return Err(ErrorCode::InvalidData.into());
662        }
663
664        // Bits 9..8 of the discriminator, and the low 14 bits of the passcode.
665        let disc_bits_9_8 = ((group >> 14) & 0x3) as u16;
666        let passcode_low = group & 0x3FFF;
667
668        // DIGIT[7..10] := (PASSCODE >> 14)
669        let passcode_high = Self::digits_at(&digits, 6, 4)?;
670        if passcode_high > 0x1FFF {
671            return Err(ErrorCode::InvalidData.into());
672        }
673
674        let passcode = (passcode_high << 14) | passcode_low;
675
676        // The short discriminator is exactly the upper 4 bits (11..8) of the full
677        // 12-bit discriminator.
678        let short_discriminator = (disc_bits_11_10 << 2) | disc_bits_9_8;
679
680        // DIGIT[11..15] := VENDOR_ID, DIGIT[16..20] := PRODUCT_ID (long form only)
681        let (vid, pid) = if long_form {
682            let vid = Self::digits_at(&digits, 10, 5)?;
683            let pid = Self::digits_at(&digits, 15, 5)?;
684
685            if vid > 0xFFFF || pid > 0xFFFF {
686                return Err(ErrorCode::InvalidData.into());
687            }
688
689            (vid as u16, pid as u16)
690        } else {
691            (0, 0)
692        };
693
694        Ok(Self {
695            // Not carried; a valid v1 code is version 0 by construction (a leading
696            // digit of 8 or 9 - i.e. a future version - is rejected above).
697            version: 0,
698            // Not carried at all. Empty means "unknown - try every transport",
699            // rather than "the device supports none".
700            discovery_capabilities: DiscoveryCapabilities::empty(),
701            // Not carried directly; per spec the *variant* implies it, and doubles as
702            // our short/long-form discriminant. See `Self::comm_flow`.
703            comm_flow: if long_form {
704                CommFlowType::Custom
705            } else {
706                CommFlowType::Standard
707            },
708            comm_data: BasicCommData {
709                password: passcode.to_le_bytes().into(),
710                // NOTE: this 12-bit field holds the 4-bit *short* discriminator for a
711                // manual pairing code. Only `short_discriminator()` exposes it.
712                discriminator: short_discriminator,
713            },
714            vid,
715            pid,
716            // Not carried.
717            serial_no: "",
718            // A manual pairing code has no optional data - hence `T = ()`.
719            optional_data: (),
720        })
721    }
722
723    /// Parse `len` decimal digits starting at `offset`.
724    fn digits_at(digits: &str, offset: usize, len: usize) -> Result<u32, Error> {
725        digits
726            .get(offset..offset + len)
727            .ok_or(ErrorCode::InvalidData)?
728            .parse()
729            .map_err(|_| ErrorCode::InvalidData.into())
730    }
731
732    /// The **short** (4-bit) discriminator - the upper 4 bits of the device's full
733    /// 12-bit discriminator.
734    ///
735    /// This is all a manual pairing code carries, so it can only narrow discovery to
736    /// 1-in-16 devices. Feed it to
737    /// [`CommissionableFilter::short_discriminator`](crate::transport::network::mdns::CommissionableFilter),
738    /// *not* to the full-discriminator filter.
739    pub const fn short_discriminator(&self) -> u8 {
740        self.comm_data.discriminator as u8
741    }
742
743    /// The setup passcode (PIN). Carried in full (27 bits).
744    pub fn passcode(&self) -> u32 {
745        u32::from_le_bytes(*self.comm_data.password.access())
746    }
747
748    /// The Vendor and Product IDs, if this is the 21-digit variant that carries them
749    /// (`None` for the 11-digit variant).
750    pub fn vid_pid(&self) -> Option<(u16, u16)> {
751        (!matches!(self.comm_flow, CommFlowType::Standard)).then_some((self.vid, self.pid))
752    }
753
754    /// The commissioning flow, as far as the code determines it.
755    ///
756    /// A manual pairing code has no dedicated commissioning-flow field; per the
757    /// Matter Core spec the *variant* implies it:
758    /// - The 11-digit variant (no VID/PID) means the commissioner *"SHALL assume it
759    ///   is a standard flow device"* - so this returns `Some(CommFlowType::Standard)`.
760    /// - The 21-digit variant (with VID/PID) is used for **both** the User-intent and
761    ///   the Custom flow, and the code alone cannot distinguish them - so this
762    ///   returns `None`. To resolve it, look the VID/PID up in the Distributed
763    ///   Compliance Ledger (see [`Self::vid_pid`]).
764    pub fn comm_flow(&self) -> Option<CommFlowType> {
765        matches!(self.comm_flow, CommFlowType::Standard).then_some(CommFlowType::Standard)
766    }
767
768    /// A [`CommissionableFilter`] that discovers the device this manual pairing code
769    /// describes.
770    ///
771    /// A manual pairing code only carries the **short** (4-bit) discriminator, so
772    /// this necessarily filters on `short_discriminator` - which narrows discovery
773    /// to 1-in-16 devices rather than to exactly one. Getting this right is the
774    /// whole point of the method: the short value must not end up in the filter's
775    /// full-discriminator field, where it would match nothing.
776    ///
777    /// The vendor and product IDs are not included even when the 21-digit variant
778    /// carries them, since a device may advertise an anonymized Product ID of 0
779    /// during discovery.
780    pub fn commissionable_filter(&self) -> CommissionableFilter {
781        CommissionableFilter {
782            short_discriminator: Some(self.short_discriminator()),
783            ..Default::default()
784        }
785    }
786}
787
788impl CommFlowType {
789    /// Decode a 2-bit commissioning-flow field.
790    fn from_bits(bits: u8) -> Result<Self, Error> {
791        match bits {
792            0 => Ok(Self::Standard),
793            1 => Ok(Self::UserIntent),
794            2 => Ok(Self::Custom),
795            _ => Err(ErrorCode::InvalidData.into()),
796        }
797    }
798}
799
800/// A little-endian, LSB-first bit reader over a byte slice - the inverse of the
801/// `emit_bits` bit order used by the QR encoder (`(input >> i) & 1`).
802struct BitReader<'a> {
803    data: &'a [u8],
804    /// Absolute bit position of the next bit to read.
805    pos: usize,
806}
807
808impl<'a> BitReader<'a> {
809    const fn new(data: &'a [u8]) -> Self {
810        Self { data, pos: 0 }
811    }
812
813    /// Read `len` bits (0..=32) as a little-endian value, LSB-first.
814    fn read(&mut self, len: usize) -> Result<u32, Error> {
815        debug_assert!(len <= 32);
816
817        if self.pos + len > self.data.len() * 8 {
818            return Err(ErrorCode::InvalidData.into());
819        }
820
821        let mut value = 0u32;
822        for i in 0..len {
823            let bit_pos = self.pos + i;
824            let byte = self.data[bit_pos / 8];
825            let bit = (byte >> (bit_pos % 8)) & 1;
826            value |= (bit as u32) << i;
827        }
828
829        self.pos += len;
830
831        Ok(value)
832    }
833}
834
835/// QR Code text type
836///
837/// Used when emitting the QR code in different text formats
838#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
839#[cfg_attr(feature = "defmt", derive(defmt::Format))]
840pub enum QrTextType {
841    /// Pure ASCII text
842    /// Compatible with all consoles
843    Ascii,
844    /// ANSI
845    Ansi,
846    /// Unicode
847    Unicode,
848}
849
850/// QR Code representation
851pub struct Qr<'a>(QrCode<'a>);
852
853impl<'a> Qr<'a> {
854    /// Create a new QR code from the given text
855    ///
856    /// # Arguments
857    /// - `text` - Text to encode in the QR code
858    /// - `tmp_buf` - Temporary buffer for QR code generation
859    /// - `out_buf` - Output buffer for the QR code
860    ///
861    /// # Returns
862    /// - On success, returns the generated QR code
863    /// - On failure, returns an error
864    pub fn compute(text: &str, tmp_buf: &mut [u8], out_buf: &'a mut [u8]) -> Result<Self, Error> {
865        let needed_version = Version::new(Self::version(text));
866
867        let qr = QrCode::encode_text(
868            text,
869            tmp_buf,
870            out_buf,
871            QrCodeEcc::Medium,
872            needed_version,
873            needed_version,
874            None,
875            false,
876        )
877        .map_err(|_| ErrorCode::BufferTooSmall)?;
878
879        Ok(Self(qr))
880    }
881
882    /// Get the size of the QR code
883    pub fn size(&self) -> u32 {
884        self.0.size() as _
885    }
886
887    /// Get the module value at the given coordinates
888    pub fn get_module(&self, x: i32, y: i32) -> bool {
889        self.0.get_module(x, y)
890    }
891
892    /// Encode the QR as a string into the provided buffer
893    ///
894    /// # Arguments
895    /// - `text_type` - Type of text to return (ASCII, ANSI, Unicode)
896    /// - `border` - Border size
897    /// - `invert` - Whether to invert the colors (black on a white background)
898    /// - `out_buf` - Output buffer for the rendered string
899    ///
900    /// # Returns
901    /// - On success, returns a tuple containing the rendered string and the remaining buffer
902    /// - On failure, returns an error
903    pub fn as_str<'b>(
904        &self,
905        text_type: QrTextType,
906        border: u8,
907        invert: bool,
908        out_buf: &'b mut [u8],
909    ) -> Result<(&'b str, &'b mut [u8]), Error> {
910        let mut offset = 0;
911
912        for c in self.emit_chars(text_type, border, invert) {
913            let mut dst = [0; 4];
914            let bytes = c.encode_utf8(&mut dst).as_bytes();
915
916            if offset + bytes.len() > out_buf.len() {
917                return Err(ErrorCode::BufferTooSmall.into());
918            } else {
919                out_buf[offset..offset + bytes.len()].copy_from_slice(bytes);
920                offset += bytes.len();
921            }
922        }
923
924        let (str_buf, remaining_buf) = out_buf.split_at_mut(offset);
925
926        // Can't fail as `emit_chars` generates a valid UTF-8 string
927        let str = unwrap!(core::str::from_utf8(str_buf).map_err(|_| ErrorCode::InvalidData));
928
929        Ok((str, remaining_buf))
930    }
931
932    /// Encode a single line of the QR as a string into the provided buffer
933    ///
934    /// # Arguments
935    /// - `text_type` - Type of text to return (ASCII, ANSI, Unicode)
936    /// - `border` - Border size
937    /// - `invert` - Whether to invert the colors (black on a white background)
938    /// - `nl` - Whether to add a newline at the end of the line
939    /// - `y` - Y coordinate of the line to render
940    /// - `out_buf` - Output buffer for the rendered string
941    ///
942    /// # Returns
943    /// - On success, returns a tuple containing the rendered string and the remaining buffer
944    /// - On failure, returns an error
945    pub fn line_as_str<'b>(
946        &self,
947        text_type: QrTextType,
948        border: u8,
949        invert: bool,
950        nl: bool,
951        y: i32,
952        out_buf: &'b mut [u8],
953    ) -> Result<(&'b str, &'b mut [u8]), Error> {
954        let mut offset = 0;
955
956        for c in self.emit_line_chars(text_type, border, invert, nl, y) {
957            let mut dst = [0; 4];
958            let bytes = c.encode_utf8(&mut dst).as_bytes();
959
960            if offset + bytes.len() > out_buf.len() {
961                return Err(ErrorCode::BufferTooSmall.into());
962            } else {
963                out_buf[offset..offset + bytes.len()].copy_from_slice(bytes);
964                offset += bytes.len();
965            }
966        }
967
968        let (str_buf, remaining_buf) = out_buf.split_at_mut(offset);
969
970        // Can't fail as `emit_chars` generates a valid UTF-8 string
971        let str = unwrap!(core::str::from_utf8(str_buf).map_err(|_| ErrorCode::InvalidData));
972
973        Ok((str, remaining_buf))
974    }
975
976    /// Get an iterator over the indexes of the lines of the QR code including borders
977    ///
978    /// # Arguments
979    /// - `text_type` - Type of text to return (ASCII, ANSI, Unicode)
980    /// - `border` - Border size
981    pub fn lines_range(
982        &self,
983        text_type: QrTextType,
984        border: u8,
985    ) -> impl Iterator<Item = i32> + '_ + 'a {
986        let iborder: i32 = border as _;
987
988        (-iborder..self.size() as i32 + iborder)
989            .filter(move |y| !matches!(text_type, QrTextType::Unicode) || (*y - -iborder) % 2 == 0)
990    }
991
992    /// Get an iterator over the characters of the rendered QR code
993    ///
994    /// # Arguments
995    /// - `text_type` - Type of text to return (ASCII, ANSI, Unicode)
996    /// - `border` - Border size
997    /// - `invert` - Whether to invert the colors (black on a white background)
998    ///
999    /// # Returns
1000    /// - An iterator over the characters of the rendered QR code
1001    pub fn emit_chars(
1002        &self,
1003        text_type: QrTextType,
1004        border: u8,
1005        invert: bool,
1006    ) -> impl Iterator<Item = char> + use<'_, 'a> {
1007        self.lines_range(text_type, border)
1008            .flat_map(move |y| self.emit_line_chars(text_type, border, invert, true, y))
1009    }
1010
1011    /// Get an iterator over the characters of a single line of the rendered QR code
1012    ///
1013    /// # Arguments
1014    /// - `text_type` - Type of text to return (ASCII, ANSI, Unicode)
1015    /// - `border` - Border size
1016    /// - `invert` - Whether to invert the colors (black on a white background)
1017    /// - `nl` - Whether to add a newline at the end of the line
1018    /// - `y` - Y coordinate of the line to render
1019    ///
1020    /// # Returns
1021    /// - An iterator over the characters of the rendered line
1022    pub fn emit_line_chars(
1023        &self,
1024        text_type: QrTextType,
1025        border: u8,
1026        invert: bool,
1027        nl: bool,
1028        y: i32,
1029    ) -> impl Iterator<Item = char> + use<'_, 'a> {
1030        let border: i32 = border as _;
1031
1032        (-border..self.size() as i32 + border + 1)
1033            .map(move |x| (x, y))
1034            .map(move |(x, y)| {
1035                if x < self.size() as i32 + border {
1036                    let white = !self.get_module(x, y) ^ invert;
1037
1038                    match text_type {
1039                        QrTextType::Ascii => {
1040                            if white {
1041                                "#"
1042                            } else {
1043                                " "
1044                            }
1045                        }
1046                        QrTextType::Ansi => {
1047                            let prev_white = if x > -border {
1048                                Some(self.get_module(x - 1, y))
1049                            } else {
1050                                None
1051                            }
1052                            .map(|prev_white| !prev_white ^ invert);
1053
1054                            if prev_white != Some(white) {
1055                                if white {
1056                                    "\x1b[47m "
1057                                } else {
1058                                    "\x1b[40m "
1059                                }
1060                            } else {
1061                                " "
1062                            }
1063                        }
1064                        QrTextType::Unicode => {
1065                            if white == !self.get_module(x, y + 1) ^ invert {
1066                                if white {
1067                                    "\u{2588}"
1068                                } else {
1069                                    " "
1070                                }
1071                            } else if white {
1072                                "\u{2580}"
1073                            } else {
1074                                "\u{2584}"
1075                            }
1076                        }
1077                    }
1078                } else {
1079                    match text_type {
1080                        QrTextType::Ascii => {
1081                            if nl {
1082                                "\n"
1083                            } else {
1084                                ""
1085                            }
1086                        }
1087                        _ => {
1088                            if nl {
1089                                "\x1b[0m\n"
1090                            } else {
1091                                "\x1b[0m"
1092                            }
1093                        }
1094                    }
1095                }
1096            })
1097            .flat_map(str::chars)
1098    }
1099
1100    fn version(qr_code_text: &str) -> u8 {
1101        match qr_code_text.len() {
1102            0..=38 => 2,
1103            39..=61 => 3,
1104            62..=90 => 4,
1105            _ => 5,
1106        }
1107    }
1108}
1109
1110/// QR Code text renderer
1111pub enum QrTextRenderer<'a> {
1112    /// ASCII renderer
1113    Ascii(Qr<'a>),
1114    /// ANSI renderer
1115    Ansi(Qr<'a>),
1116    /// Unicode renderer
1117    Unicode(Qr<'a>),
1118}
1119
1120impl<'a> QrTextRenderer<'a> {
1121    /// Render the complete QR code as a string into the provided buffer
1122    ///
1123    /// # Arguments
1124    /// - `border` - Border size
1125    /// - `invert` - Whether to invert the colors (black on a white background)
1126    /// - `out_buf` - Output buffer for the rendered string
1127    ///
1128    /// # Returns
1129    /// - On success, returns a tuple containing the rendered string and the remaining buffer
1130    /// - On failure, returns an error
1131    pub fn render<'b>(
1132        &self,
1133        border: u8,
1134        invert: bool,
1135        out_buf: &'b mut [u8],
1136    ) -> Result<(&'b str, &'b mut [u8]), Error> {
1137        let mut offset = 0;
1138
1139        for c in self.render_iter(border, invert) {
1140            let mut dst = [0; 4];
1141            let bytes = c.encode_utf8(&mut dst).as_bytes();
1142
1143            if offset + bytes.len() > out_buf.len() {
1144                return Err(ErrorCode::BufferTooSmall.into());
1145            } else {
1146                out_buf[offset..offset + bytes.len()].copy_from_slice(bytes);
1147                offset += bytes.len();
1148            }
1149        }
1150
1151        let (str_buf, remaining_buf) = out_buf.split_at_mut(offset);
1152
1153        // Can't fail as `emit_chars` generates a valid UTF-8 string
1154        let str = unwrap!(core::str::from_utf8(str_buf).map_err(|_| ErrorCode::InvalidData));
1155
1156        Ok((str, remaining_buf))
1157    }
1158
1159    /// Render a single line of the QR code as a string into the provided buffer
1160    ///
1161    /// # Arguments
1162    /// - `border` - Border size
1163    /// - `invert` - Whether to invert the colors (black on a white background)
1164    /// - `nl` - Whether to add a newline at the end of the line
1165    /// - `y` - Y coordinate of the line to render
1166    /// - `out_buf` - Output buffer for the rendered string
1167    ///
1168    /// # Returns
1169    /// - On success, returns a tuple containing the rendered string and the remaining buffer
1170    /// - On failure, returns an error
1171    pub fn render_line<'b>(
1172        &self,
1173        border: u8,
1174        invert: bool,
1175        nl: bool,
1176        y: i32,
1177        out_buf: &'b mut [u8],
1178    ) -> Result<(&'b str, &'b mut [u8]), Error> {
1179        let mut offset = 0;
1180
1181        for c in self.render_line_iter(border, invert, nl, y) {
1182            let mut dst = [0; 4];
1183            let bytes = c.encode_utf8(&mut dst).as_bytes();
1184
1185            if offset + bytes.len() > out_buf.len() {
1186                return Err(ErrorCode::BufferTooSmall.into());
1187            } else {
1188                out_buf[offset..offset + bytes.len()].copy_from_slice(bytes);
1189                offset += bytes.len();
1190            }
1191        }
1192
1193        let (str_buf, remaining_buf) = out_buf.split_at_mut(offset);
1194
1195        // Can't fail as `emit_chars` generates a valid UTF-8 string
1196        let str = unwrap!(core::str::from_utf8(str_buf).map_err(|_| ErrorCode::InvalidData));
1197
1198        Ok((str, remaining_buf))
1199    }
1200
1201    /// Get an iterator over the indexes of the lines of the QR code including borders
1202    ///
1203    /// # Arguments
1204    /// - `border` - Border size
1205    pub fn lines_range(&self, border: u8) -> impl Iterator<Item = i32> + '_ + 'a {
1206        let unicode = matches!(self, Self::Unicode(_));
1207        let iborder: i32 = border as _;
1208
1209        (-iborder..self.qr().size() as i32 + iborder)
1210            .filter(move |y| !unicode || (*y - -iborder) % 2 == 0)
1211    }
1212
1213    /// Get an iterator over the characters of the rendered QR code
1214    ///
1215    /// # Arguments
1216    /// - `border` - Border size
1217    /// - `invert` - Whether to invert the colors (black on a white background)
1218    ///
1219    /// # Returns
1220    /// - An iterator over the characters of the rendered QR code
1221    pub fn render_iter(
1222        &self,
1223        border: u8,
1224        invert: bool,
1225    ) -> impl Iterator<Item = char> + use<'_, 'a> {
1226        self.lines_range(border)
1227            .flat_map(move |y| self.render_line_iter(border, invert, true, y))
1228    }
1229
1230    /// Get an iterator over the characters of a single line of the rendered QR code
1231    ///
1232    /// # Arguments
1233    /// - `border` - Border size
1234    /// - `invert` - Whether to invert the colors (black on a white background)
1235    /// - `nl` - Whether to add a newline at the end of the line
1236    /// - `y` - Y coordinate of the line to render
1237    ///
1238    /// # Returns
1239    /// - An iterator over the characters of the rendered line
1240    pub fn render_line_iter(
1241        &self,
1242        border: u8,
1243        invert: bool,
1244        nl: bool,
1245        y: i32,
1246    ) -> impl Iterator<Item = char> + use<'_, 'a> {
1247        let border: i32 = border as _;
1248
1249        (-border..self.qr().size() as i32 + border + 1)
1250            .map(move |x| (x, y))
1251            .map(move |(x, y)| {
1252                if x < self.qr().size() as i32 + border {
1253                    let white = !self.qr().get_module(x, y) ^ invert;
1254
1255                    match self {
1256                        Self::Ascii(_) => {
1257                            if white {
1258                                "#"
1259                            } else {
1260                                " "
1261                            }
1262                        }
1263                        Self::Ansi(_) => {
1264                            let prev_white = if x > -border {
1265                                Some(self.qr().get_module(x - 1, y))
1266                            } else {
1267                                None
1268                            }
1269                            .map(|prev_white| !prev_white ^ invert);
1270
1271                            if prev_white != Some(white) {
1272                                if white {
1273                                    "\x1b[47m "
1274                                } else {
1275                                    "\x1b[40m "
1276                                }
1277                            } else {
1278                                " "
1279                            }
1280                        }
1281                        Self::Unicode(_) => {
1282                            if white == !self.qr().get_module(x, y + 1) ^ invert {
1283                                if white {
1284                                    "\u{2588}"
1285                                } else {
1286                                    " "
1287                                }
1288                            } else if white {
1289                                "\u{2580}"
1290                            } else {
1291                                "\u{2584}"
1292                            }
1293                        }
1294                    }
1295                } else {
1296                    match self {
1297                        Self::Ascii(_) => {
1298                            if nl {
1299                                "\n"
1300                            } else {
1301                                ""
1302                            }
1303                        }
1304                        _ => {
1305                            if nl {
1306                                "\x1b[0m\n"
1307                            } else {
1308                                "\x1b[0m"
1309                            }
1310                        }
1311                    }
1312                }
1313            })
1314            .flat_map(str::chars)
1315    }
1316
1317    #[inline(always)]
1318    pub fn qr(&self) -> &Qr<'a> {
1319        match self {
1320            Self::Ascii(qr) => qr,
1321            Self::Ansi(qr) => qr,
1322            Self::Unicode(qr) => qr,
1323        }
1324    }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329    use super::*;
1330
1331    #[test]
1332    fn can_base38_encode() {
1333        const QR_CODE: &str = "MT:YNJV7VSC00CMVH7SR00";
1334
1335        let comm_data = BasicCommData {
1336            password: 34567890_u32.to_le_bytes().into(),
1337            discriminator: 2976,
1338        };
1339        let dev_det = BasicInfoConfig {
1340            vid: 9050,
1341            pid: 65279,
1342            ..Default::default()
1343        };
1344
1345        let disc_cap = DiscoveryCapabilities::BLE;
1346        let qr_code_data = QrPayload::new_from_basic_info(
1347            disc_cap,
1348            CommFlowType::Standard,
1349            comm_data,
1350            &dev_det,
1351            no_optional_data,
1352        );
1353        let mut buf = [0; 1024];
1354        let data_str = unwrap!(qr_code_data.as_str(&mut buf), "Failed to encode").0;
1355        assert_eq!(data_str, QR_CODE)
1356    }
1357
1358    #[test]
1359    fn can_base38_encode_with_vendor_data() {
1360        const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40";
1361
1362        let comm_data = BasicCommData {
1363            password: 20202021_u32.to_le_bytes().into(),
1364            discriminator: 3840,
1365        };
1366        let dev_det = BasicInfoConfig {
1367            vid: 65521,
1368            pid: 32769,
1369            serial_no: "1234567890",
1370            ..Default::default()
1371        };
1372
1373        let disc_cap = DiscoveryCapabilities::IP;
1374        let qr_code_data = QrPayload::new_from_basic_info(
1375            disc_cap,
1376            CommFlowType::Standard,
1377            comm_data,
1378            &dev_det,
1379            no_optional_data,
1380        );
1381        let mut buf = [0; 1024];
1382        let data_str = unwrap!(qr_code_data.as_str(&mut buf), "Failed to encode").0;
1383        assert_eq!(data_str, QR_CODE)
1384    }
1385
1386    #[test]
1387    fn can_base38_encode_with_optional_data() {
1388        const QR_CODE: &str =
1389            "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1UXH34YY0V3KY.O3DKN440F710Q940";
1390        const OPTIONAL_DEFAULT_STRING_TAG: u8 = 0x82; // Vendor "test" tag
1391        const OPTIONAL_DEFAULT_STRING_VALUE: &str = "myData";
1392
1393        const OPTIONAL_DEFAULT_INT_TAG: u8 = 0x83; // Vendor "test" tag
1394        const OPTIONAL_DEFAULT_INT_VALUE: i32 = 65550;
1395
1396        let comm_data = BasicCommData {
1397            password: 20202021_u32.to_le_bytes().into(),
1398            discriminator: 3840,
1399        };
1400        let dev_det = BasicInfoConfig {
1401            vid: 65521,
1402            pid: 32769,
1403            serial_no: "1234567890",
1404            ..Default::default()
1405        };
1406
1407        let disc_cap = DiscoveryCapabilities::IP;
1408        let optional_data = || {
1409            TLV::utf8(
1410                TLVTag::Context(OPTIONAL_DEFAULT_STRING_TAG),
1411                OPTIONAL_DEFAULT_STRING_VALUE,
1412            )
1413            .into_tlv_iter()
1414            .chain(
1415                TLV::i32(
1416                    TLVTag::Context(OPTIONAL_DEFAULT_INT_TAG),
1417                    OPTIONAL_DEFAULT_INT_VALUE,
1418                )
1419                .into_tlv_iter(),
1420            )
1421            .flat_map(TLV::result_into_bytes_iter)
1422        };
1423
1424        let qr_code_data = QrPayload::new_from_basic_info(
1425            disc_cap,
1426            CommFlowType::Standard,
1427            comm_data,
1428            &dev_det,
1429            optional_data,
1430        );
1431
1432        let mut buf = [0; 1024];
1433        let data_str = unwrap!(qr_code_data.as_str(&mut buf), "Failed to encode").0;
1434        assert_eq!(data_str, QR_CODE)
1435    }
1436
1437    #[test]
1438    fn parse_qr_basic() {
1439        // Same vector as `can_base38_encode` (BLE, no optional data).
1440        const QR_CODE: &str = "MT:YNJV7VSC00CMVH7SR00";
1441
1442        let mut buf = [0; 128];
1443        let payload = unwrap!(QrPayload::parse(QR_CODE, &mut buf), "Failed to parse");
1444
1445        assert_eq!(payload.version(), 0);
1446        assert_eq!(payload.vid(), 9050);
1447        assert_eq!(payload.pid(), 65279);
1448        assert_eq!(payload.comm_flow(), CommFlowType::Standard);
1449        assert_eq!(payload.discovery_capabilities(), DiscoveryCapabilities::BLE);
1450        assert_eq!(payload.discriminator(), 2976);
1451        assert_eq!(payload.passcode(), 34567890);
1452        assert_eq!(payload.serial_no(), "");
1453        assert!(payload.optional_data().is_empty());
1454    }
1455
1456    #[test]
1457    fn parse_qr_with_serial_no() {
1458        // Same vector as `can_base38_encode_with_vendor_data` (IP, serial number).
1459        const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40";
1460
1461        let mut buf = [0; 128];
1462        let payload = unwrap!(QrPayload::parse(QR_CODE, &mut buf), "Failed to parse");
1463
1464        assert_eq!(payload.vid(), 65521);
1465        assert_eq!(payload.pid(), 32769);
1466        assert_eq!(payload.discovery_capabilities(), DiscoveryCapabilities::IP);
1467        assert_eq!(payload.discriminator(), 3840);
1468        assert_eq!(payload.passcode(), 20202021);
1469        assert_eq!(payload.serial_no(), "1234567890");
1470        assert!(!payload.optional_data().is_empty());
1471    }
1472
1473    #[test]
1474    fn parse_qr_round_trips_through_encode() {
1475        // Parse a QR, then re-encode from the parsed *fields* and expect the same
1476        // string. (We rebuild via the fields rather than replaying the raw optional
1477        // blob, since the encoder re-wraps the serial number into the optional-TLV
1478        // structure itself - here the serial is the only optional content.)
1479        const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40";
1480
1481        let mut buf = [0; 128];
1482        let payload = unwrap!(QrPayload::parse(QR_CODE, &mut buf), "Failed to parse");
1483
1484        let reencoded = QrPayload::new(
1485            payload.discovery_capabilities(),
1486            payload.comm_flow(),
1487            payload.comm_data.clone(),
1488            payload.vid(),
1489            payload.pid(),
1490            payload.serial_no(),
1491            no_optional_data,
1492        );
1493
1494        let mut out = [0; 256];
1495        let s = unwrap!(reencoded.as_str(&mut out), "Failed to encode").0;
1496        assert_eq!(s, QR_CODE);
1497    }
1498
1499    #[test]
1500    fn parse_pairing_code_round_trips_with_encoder() {
1501        // The two vectors from `code.rs`'s `can_compute_pairing_code`, parsed back.
1502        for (code, passcode, discriminator) in [
1503            ("00876800071", 123456_u32, 250_u16),
1504            ("26318621095", 34567890, 2976),
1505        ] {
1506            let payload = unwrap!(QrPayload::parse_pairing_code(code), "Failed to parse");
1507
1508            assert_eq!(payload.passcode(), passcode);
1509            // Only the upper 4 bits of the discriminator survive a manual code.
1510            assert_eq!(payload.short_discriminator(), (discriminator >> 8) as u8);
1511            // The 11-digit variant implies the standard flow and carries no VID/PID.
1512            assert_eq!(payload.comm_flow(), Some(CommFlowType::Standard));
1513            assert_eq!(payload.vid_pid(), None);
1514            // Nothing tells us how to reach the device.
1515            assert!(payload.discovery_capabilities.is_empty());
1516
1517            // And the code we parsed re-computes from the parsed passcode + a
1518            // discriminator whose upper 4 bits match.
1519            let comm_data = BasicCommData {
1520                password: payload.passcode().to_le_bytes().into(),
1521                discriminator,
1522            };
1523            assert_eq!(comm_data.compute_pairing_code(), code);
1524        }
1525    }
1526
1527    #[test]
1528    fn commissionable_filter_uses_the_right_discriminator_field() {
1529        // A QR carries the full 12-bit discriminator...
1530        let mut buf = [0; 128];
1531        let qr = unwrap!(
1532            QrPayload::parse("MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40", &mut buf),
1533            "Failed to parse"
1534        );
1535        let filter = qr.commissionable_filter();
1536
1537        assert_eq!(filter.discriminator, Some(3840));
1538        assert_eq!(filter.short_discriminator, None);
1539
1540        // ... while a manual pairing code only carries the upper 4 bits, which must
1541        // land in `short_discriminator` (0xF00 >> 8 == 15) and *not* in the
1542        // full-discriminator field, where it would match nothing.
1543        let manual = unwrap!(QrPayload::parse_pairing_code("34970112332"), "Failed");
1544        let filter = manual.commissionable_filter();
1545
1546        assert_eq!(filter.short_discriminator, Some(15));
1547        assert_eq!(filter.discriminator, None);
1548
1549        // Neither constrains vendor/product: a device may advertise an anonymized
1550        // Product ID of 0 during discovery.
1551        assert_eq!(filter.vendor_id, None);
1552        assert_eq!(filter.product_id, None);
1553    }
1554
1555    #[test]
1556    fn parse_pairing_code_accepts_pretty_form() {
1557        // The canonical CHIP test device: discriminator 3840, passcode 20202021.
1558        let compact = unwrap!(QrPayload::parse_pairing_code("34970112332"), "compact");
1559        let pretty = unwrap!(QrPayload::parse_pairing_code("3497-0112-332"), "pretty");
1560
1561        assert_eq!(compact.passcode(), 20202021);
1562        assert_eq!(compact.short_discriminator(), 15); // 3840 >> 8
1563        assert_eq!(pretty.passcode(), compact.passcode());
1564        assert_eq!(pretty.short_discriminator(), compact.short_discriminator());
1565    }
1566
1567    #[test]
1568    fn parse_pairing_code_rejects_bad_input() {
1569        // Bad Verhoeff check digit (last digit of the valid code bumped).
1570        assert!(QrPayload::parse_pairing_code("34970112333").is_err());
1571        // Not a digit.
1572        assert!(QrPayload::parse_pairing_code("3497011233X").is_err());
1573        // Wrong length (neither 11 nor 21).
1574        assert!(QrPayload::parse_pairing_code("3497011233").is_err());
1575        // A leading digit of 8/9 indicates a future payload version, not v1.
1576        assert!(QrPayload::parse_pairing_code("94970112332").is_err());
1577        // VID/PID-present flag set (leading digit >= 4) but only 11 digits.
1578        assert!(QrPayload::parse_pairing_code("74970112332").is_err());
1579    }
1580
1581    #[test]
1582    fn parse_qr_rejects_bad_input() {
1583        let mut buf = [0; 128];
1584
1585        // Missing `MT:` prefix.
1586        assert!(QrPayload::parse("YNJV7VSC00CMVH7SR00", &mut buf).is_err());
1587        // Invalid base38 character (`!`).
1588        assert!(QrPayload::parse("MT:YNJV7VSC00CMVH7SR0!", &mut buf).is_err());
1589        // Too short to hold the fixed payload.
1590        assert!(QrPayload::parse("MT:00", &mut buf).is_err());
1591    }
1592}