Skip to main content

rustlavel_auth/
qr.rs

1//! A QR Code encoder, byte mode, versions 1 to 10, to ISO/IEC 18004.
2//!
3//! Enrolling a TOTP authenticator means putting an `otpauth://` URI in front of
4//! the user as a QR code, and both of the easy ways to do that are wrong here.
5//! A JavaScript QR library would have to be allowed by `script-src`, and the
6//! auth starter kit is built around `style-src 'self'; script-src 'self'` with
7//! no `unsafe-inline`; a chart API on someone else's domain would be handed the
8//! shared TOTP secret in a query string, which is the one thing that secret
9//! must never travel in. So the symbol is computed here and drawn as an SVG
10//! made only of presentation attributes.
11//!
12//! The scope is exactly what an `otpauth://` URI needs and nothing else: byte
13//! mode (the URI is ASCII, and byte mode is also the only mode that survives
14//! UTF-8 in a label), versions 1 through 10 picked as the smallest that fits,
15//! and all four error-correction levels. Longer data is refused rather than
16//! truncated — a truncated QR code still scans, and hands the user a secret
17//! that is quietly wrong.
18//!
19//! Correctness here is not something the eye can check, so the module matrices
20//! in the tests were compared bit for bit against libqrencode's output for the
21//! same payloads and then frozen into this file as literals.
22
23use rustlavel_core::{Error, Result};
24use std::fmt::Write as _;
25
26// ---------------------------------------------------------------------------
27// Error correction levels
28// ---------------------------------------------------------------------------
29
30/// The four error-correction levels of ISO/IEC 18004 §7.5.
31///
32/// Higher levels survive more damage but leave less room for data, so a longer
33/// URI at `High` needs a bigger symbol than the same URI at `Low`.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum Ecc {
36    /// Recovers roughly 7% of the codewords.
37    Low,
38    /// Recovers roughly 15%. The default, and what authenticator apps expect.
39    #[default]
40    Medium,
41    /// Recovers roughly 25%.
42    Quartile,
43    /// Recovers roughly 30%.
44    High,
45}
46
47impl Ecc {
48    /// Position in the per-level tables below, in the standard's own order.
49    const fn index(self) -> usize {
50        match self {
51            Ecc::Low => 0,
52            Ecc::Medium => 1,
53            Ecc::Quartile => 2,
54            Ecc::High => 3,
55        }
56    }
57
58    /// The two-bit level indicator that goes into the format information,
59    /// ISO/IEC 18004 table 12.
60    ///
61    /// Deliberately not the same order as the levels themselves: L is 01 and M
62    /// is 00. Reading this off the enum discriminant is a classic way to
63    /// produce a symbol that no reader can decode, because the format bits then
64    /// name the wrong level and every codeword is split into the wrong blocks.
65    const fn format_bits(self) -> u32 {
66        match self {
67            Ecc::Low => 1,
68            Ecc::Medium => 0,
69            Ecc::Quartile => 3,
70            Ecc::High => 2,
71        }
72    }
73
74    /// For error messages, where "Medium" reads better than "Ecc::Medium".
75    const fn name(self) -> &'static str {
76        match self {
77            Ecc::Low => "Low",
78            Ecc::Medium => "Medium",
79            Ecc::Quartile => "Quartile",
80            Ecc::High => "High",
81        }
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Tables from the standard
87// ---------------------------------------------------------------------------
88
89/// The largest symbol this encoder builds. Version 10 is 57x57 modules and
90/// holds 271 bytes at `Low`, which is far more than any `otpauth://` URI.
91const MAX_VERSION: u8 = 10;
92
93/// Total codewords, data plus error correction, per version. ISO/IEC 18004
94/// table 1, column "total number of codewords". Indexed by version minus one.
95const TOTAL_CODEWORDS: [u16; MAX_VERSION as usize] = [26, 44, 70, 100, 134, 172, 196, 242, 292, 346];
96
97/// Error-correction block structure per version and level, from ISO/IEC 18004
98/// tables 13 to 22. Each entry is
99/// `(ecc codewords per block, group-1 blocks, data codewords per group-1
100/// block, group-2 blocks)`.
101///
102/// Group 2 is only ever needed because the data codewords do not divide evenly
103/// among the blocks, so a group-2 block always holds exactly one codeword more
104/// than a group-1 block. That is a property of the whole table, not an accident
105/// of these ten versions, which is why the second data count is not stored.
106///
107/// The inner array is ordered `[Low, Medium, Quartile, High]`, matching
108/// `Ecc::index`. `tables_are_self_consistent` checks every row against
109/// `TOTAL_CODEWORDS`.
110#[allow(clippy::type_complexity)]
111const BLOCKS: [[(u8, u8, u8, u8); 4]; MAX_VERSION as usize] = [
112    // Version 1
113    [(7, 1, 19, 0), (10, 1, 16, 0), (13, 1, 13, 0), (17, 1, 9, 0)],
114    // Version 2
115    [(10, 1, 34, 0), (16, 1, 28, 0), (22, 1, 22, 0), (28, 1, 16, 0)],
116    // Version 3
117    [(15, 1, 55, 0), (26, 1, 44, 0), (18, 2, 17, 0), (22, 2, 13, 0)],
118    // Version 4
119    [(20, 1, 80, 0), (18, 2, 32, 0), (26, 2, 24, 0), (16, 4, 9, 0)],
120    // Version 5
121    [(26, 1, 108, 0), (24, 2, 43, 0), (18, 2, 15, 2), (22, 2, 11, 2)],
122    // Version 6
123    [(18, 2, 68, 0), (16, 4, 27, 0), (24, 4, 19, 0), (28, 4, 15, 0)],
124    // Version 7
125    [(20, 2, 78, 0), (18, 4, 31, 0), (18, 2, 14, 4), (26, 4, 13, 1)],
126    // Version 8
127    [(24, 2, 97, 0), (22, 2, 38, 2), (22, 4, 18, 2), (26, 4, 14, 2)],
128    // Version 9
129    [(30, 2, 116, 0), (22, 3, 36, 2), (20, 4, 16, 4), (24, 4, 12, 4)],
130    // Version 10
131    [(18, 2, 68, 2), (26, 4, 43, 1), (24, 6, 19, 2), (28, 6, 15, 2)],
132];
133
134/// Alignment-pattern centre coordinates per version, ISO/IEC 18004 annex E
135/// table E.1. Version 1 has no alignment patterns at all; every other version
136/// puts one at each pairing of these coordinates except the three that would
137/// land on a finder pattern.
138const ALIGNMENT: [&[usize]; MAX_VERSION as usize] = [
139    &[],
140    &[6, 18],
141    &[6, 22],
142    &[6, 26],
143    &[6, 30],
144    &[6, 34],
145    &[6, 22, 38],
146    &[6, 24, 42],
147    &[6, 26, 46],
148    &[6, 28, 50],
149];
150
151/// Mode indicator for byte mode, ISO/IEC 18004 table 2. Four bits, `0100`.
152const MODE_BYTE: u32 = 0b0100;
153
154/// The two padding codewords the standard prescribes, §7.4.10. They alternate,
155/// starting with `0xEC`, until the data capacity is full. Their only job is to
156/// be a fixed, high-contrast pattern, so a reader that gets this far knows it
157/// has run off the end of the message.
158const PAD_CODEWORDS: [u8; 2] = [0xEC, 0x11];
159
160// ---------------------------------------------------------------------------
161// GF(256)
162// ---------------------------------------------------------------------------
163
164/// The field polynomial for QR's GF(2^8), ISO/IEC 18004 §7.5.2:
165/// x^8 + x^4 + x^3 + x^2 + 1, which is `0x11D`.
166const GF_POLY: u16 = 0x11d;
167
168/// Antilog table: `GF_EXP[i]` is the primitive element 2 raised to `i`.
169const GF_EXP: [u8; 256] = build_exp();
170/// Log table: `GF_LOG[v]` is the exponent `i` with `GF_EXP[i] == v`. The entry
171/// for zero is meaningless and never read, because `gf_mul` short-circuits it.
172const GF_LOG: [u8; 256] = build_log();
173
174const fn build_exp() -> [u8; 256] {
175    let mut exp = [0u8; 256];
176    let mut value: u16 = 1;
177    let mut i = 0;
178    while i < 256 {
179        exp[i] = value as u8;
180        value <<= 1;
181        if value & 0x100 != 0 {
182            value ^= GF_POLY;
183        }
184        i += 1;
185    }
186    exp
187}
188
189const fn build_log() -> [u8; 256] {
190    let mut log = [0u8; 256];
191    let mut i = 0;
192    // The cycle has length 255, so stopping there leaves GF_EXP[255] == 1
193    // mapping back to 0 rather than overwriting it with 255.
194    while i < 255 {
195        log[GF_EXP[i] as usize] = i as u8;
196        i += 1;
197    }
198    log
199}
200
201/// Multiply in GF(256). Addition in this field is XOR; multiplication is
202/// addition of logarithms modulo 255.
203fn gf_mul(a: u8, b: u8) -> u8 {
204    if a == 0 || b == 0 {
205        return 0;
206    }
207    let sum = GF_LOG[a as usize] as usize + GF_LOG[b as usize] as usize;
208    GF_EXP[sum % 255]
209}
210
211/// The Reed-Solomon generator polynomial for `degree` error-correction
212/// codewords, returned without its leading monic term.
213///
214/// It is the product of `(x - 2^i)` for `i` in `0..degree`, which is what
215/// ISO/IEC 18004 annex A tabulates. Deriving it beats transcribing thirteen
216/// rows of exponents out of the standard by hand — `generator_polynomials_match_annex_a`
217/// checks two of those rows against the printed values.
218fn generator(degree: usize) -> Vec<u8> {
219    let mut coefficients = vec![0u8; degree];
220    // The polynomial starts as the constant 1; coefficients run highest degree
221    // first, so that sits at the end.
222    if let Some(last) = coefficients.last_mut() {
223        *last = 1;
224    }
225
226    let mut root = 1u8;
227    for _ in 0..degree {
228        // Multiply through by (x + root): scale every coefficient by the root,
229        // then fold in the term one degree up, which is the shift by x.
230        for i in 0..coefficients.len() {
231            coefficients[i] = gf_mul(coefficients[i], root);
232            if i + 1 < coefficients.len() {
233                coefficients[i] ^= coefficients[i + 1];
234            }
235        }
236        root = gf_mul(root, 2);
237    }
238
239    coefficients
240}
241
242/// The Reed-Solomon remainder of `data` divided by `divisor`, which is the
243/// block's error-correction codewords.
244fn remainder(data: &[u8], divisor: &[u8]) -> Vec<u8> {
245    let mut result = vec![0u8; divisor.len()];
246
247    for &byte in data {
248        // Synthetic division, one data codeword at a time: the term leaving the
249        // top of the register decides how much of the divisor to subtract.
250        let factor = byte ^ result[0];
251        result.rotate_left(1);
252        if let Some(last) = result.last_mut() {
253            *last = 0;
254        }
255        for (slot, &coefficient) in result.iter_mut().zip(divisor) {
256            *slot ^= gf_mul(coefficient, factor);
257        }
258    }
259
260    result
261}
262
263// ---------------------------------------------------------------------------
264// Capacity
265// ---------------------------------------------------------------------------
266
267/// Data codewords available in a version at a level, summed over its blocks.
268fn data_codewords(version: u8, ecc: Ecc) -> usize {
269    let (_, group1, data_per_block, group2) = BLOCKS[version as usize - 1][ecc.index()];
270    group1 as usize * data_per_block as usize
271        + group2 as usize * (data_per_block as usize + 1)
272}
273
274/// Width of the character-count indicator for byte mode, ISO/IEC 18004 table 3.
275/// Eight bits up to version 9, sixteen from version 10 — which is why version
276/// 10 does not simply hold one byte more than version 9 in every case.
277const fn count_bits(version: u8) -> usize {
278    if version <= 9 {
279        8
280    } else {
281        16
282    }
283}
284
285/// The most bytes a version and level can carry in byte mode.
286fn capacity_bytes(version: u8, ecc: Ecc) -> usize {
287    let bits = data_codewords(version, ecc) * 8;
288    // Four bits of mode indicator plus the character count come off the top.
289    (bits - 4 - count_bits(version)) / 8
290}
291
292/// The smallest version that fits `length` bytes, or `None` past version 10.
293fn choose_version(length: usize, ecc: Ecc) -> Option<u8> {
294    (1..=MAX_VERSION).find(|&version| length <= capacity_bytes(version, ecc))
295}
296
297// ---------------------------------------------------------------------------
298// Bit buffer
299// ---------------------------------------------------------------------------
300
301/// A big-endian bit accumulator. The whole encoder writes bits before it ever
302/// writes bytes, so this is simpler than shifting bytes in place.
303#[derive(Default)]
304struct Bits(Vec<bool>);
305
306impl Bits {
307    fn push(&mut self, value: u32, width: usize) {
308        for shift in (0..width).rev() {
309            self.0.push((value >> shift) & 1 == 1);
310        }
311    }
312
313    fn len(&self) -> usize {
314        self.0.len()
315    }
316}
317
318// ---------------------------------------------------------------------------
319// The code itself
320// ---------------------------------------------------------------------------
321
322/// A finished QR symbol: a square grid of dark and light modules.
323#[derive(Debug, Clone)]
324pub struct QrCode {
325    version: u8,
326    size: usize,
327    ecc: Ecc,
328    mask: u8,
329    /// Row-major, `size * size` entries, `true` meaning dark.
330    modules: Vec<bool>,
331}
332
333impl QrCode {
334    /// Side length in modules, `4 * version + 17`.
335    pub fn size(&self) -> usize {
336        self.size
337    }
338
339    /// The symbol version, 1 to 10.
340    pub fn version(&self) -> u8 {
341        self.version
342    }
343
344    /// The error-correction level the symbol was built at.
345    pub fn ecc(&self) -> Ecc {
346        self.ecc
347    }
348
349    /// Which of the eight data masks the penalty rules selected. Recorded in
350    /// the format information, so a reader does not have to guess.
351    pub fn mask(&self) -> u8 {
352        self.mask
353    }
354
355    /// Whether the module at `(x, y)` is dark. Out-of-range coordinates read as
356    /// light, which is what the quiet zone is anyway.
357    pub fn module(&self, x: usize, y: usize) -> bool {
358        if x >= self.size || y >= self.size {
359            return false;
360        }
361        self.modules[y * self.size + x]
362    }
363}
364
365/// Encode `data` at the default `Medium` error-correction level.
366pub fn encode(data: &str) -> Result<QrCode> {
367    encode_with(data, Ecc::Medium)
368}
369
370/// Encode `data` at a chosen error-correction level, in the smallest version
371/// that fits.
372///
373/// Fails rather than truncating when the data will not fit a version 10 symbol.
374pub fn encode_with(data: &str, ecc: Ecc) -> Result<QrCode> {
375    let bytes = data.as_bytes();
376
377    let version = choose_version(bytes.len(), ecc).ok_or_else(|| {
378        Error::msg(format!(
379            "QR code data is {} bytes, which is too long: the largest symbol this encoder \
380             builds is version {} at {} error correction, which holds {} bytes",
381            bytes.len(),
382            MAX_VERSION,
383            ecc.name(),
384            capacity_bytes(MAX_VERSION, ecc)
385        ))
386    })?;
387
388    let codewords = encode_codewords(bytes, version, ecc);
389    Ok(build_symbol(version, ecc, &codewords, None))
390}
391
392/// Encode with a chosen mask instead of the one the penalty rules would pick.
393///
394/// Only the tests use this, and only to compare against another encoder's
395/// output: the two agree on every module of the symbol but may disagree about
396/// which mask is prettiest, so holding the mask fixed is what makes a
397/// module-by-module comparison meaningful.
398#[cfg(test)]
399fn encode_forced(data: &str, ecc: Ecc, mask: u8) -> Result<QrCode> {
400    let bytes = data.as_bytes();
401    let version = choose_version(bytes.len(), ecc)
402        .ok_or_else(|| Error::msg("QR code data is too long"))?;
403    let codewords = encode_codewords(bytes, version, ecc);
404    Ok(build_symbol(version, ecc, &codewords, Some(mask)))
405}
406
407/// Bit stream, padding, error correction and interleaving: everything between
408/// the input bytes and the sequence of codewords that gets drawn.
409fn encode_codewords(bytes: &[u8], version: u8, ecc: Ecc) -> Vec<u8> {
410    let capacity = data_codewords(version, ecc);
411
412    let mut bits = Bits::default();
413    bits.push(MODE_BYTE, 4);
414    bits.push(bytes.len() as u32, count_bits(version));
415    for &byte in bytes {
416        bits.push(u32::from(byte), 8);
417    }
418
419    // Terminator, §7.4.9: up to four zero bits, and fewer if the message ends
420    // that close to capacity.
421    let terminator = (capacity * 8 - bits.len()).min(4);
422    bits.push(0, terminator);
423    // Then zeros up to a codeword boundary, so the padding below lands aligned.
424    bits.push(0, (8 - bits.len() % 8) % 8);
425
426    let mut data = Vec::with_capacity(capacity);
427    for chunk in bits.0.chunks(8) {
428        let mut byte = 0u8;
429        for (index, &bit) in chunk.iter().enumerate() {
430            if bit {
431                byte |= 1 << (7 - index);
432            }
433        }
434        data.push(byte);
435    }
436    for index in 0..capacity - data.len() {
437        data.push(PAD_CODEWORDS[index % 2]);
438    }
439
440    // Split into blocks. The short (group 1) blocks come first, and the long
441    // ones carry exactly one codeword more.
442    let (ecc_per_block, group1, data_per_block, group2) =
443        BLOCKS[version as usize - 1][ecc.index()];
444    let divisor = generator(ecc_per_block as usize);
445
446    let mut data_blocks: Vec<&[u8]> = Vec::new();
447    let mut ecc_blocks: Vec<Vec<u8>> = Vec::new();
448    let mut offset = 0;
449    for block in 0..(group1 + group2) as usize {
450        let length = if block < group1 as usize {
451            data_per_block as usize
452        } else {
453            data_per_block as usize + 1
454        };
455        let slice = &data[offset..offset + length];
456        offset += length;
457        ecc_blocks.push(remainder(slice, &divisor));
458        data_blocks.push(slice);
459    }
460
461    // Interleave, §7.6: take the first codeword of every block, then the second
462    // of every block, and so on, skipping blocks that have already run out.
463    // Then the same for the error-correction codewords, which are all the same
464    // length. Interleaving is what makes a burst of damage land across many
465    // blocks instead of destroying one of them outright.
466    let mut out = Vec::with_capacity(TOTAL_CODEWORDS[version as usize - 1] as usize);
467    let longest = data_per_block as usize + usize::from(group2 > 0);
468    for index in 0..longest {
469        for block in &data_blocks {
470            if let Some(&byte) = block.get(index) {
471                out.push(byte);
472            }
473        }
474    }
475    for index in 0..ecc_per_block as usize {
476        for block in &ecc_blocks {
477            out.push(block[index]);
478        }
479    }
480
481    out
482}
483
484// ---------------------------------------------------------------------------
485// Drawing
486// ---------------------------------------------------------------------------
487
488/// The grid under construction: the modules themselves, plus which of them
489/// belong to function patterns and so must not receive data or be masked.
490struct Canvas {
491    size: usize,
492    modules: Vec<bool>,
493    function: Vec<bool>,
494}
495
496impl Canvas {
497    fn new(size: usize) -> Self {
498        Canvas { size, modules: vec![false; size * size], function: vec![false; size * size] }
499    }
500
501    fn get(&self, x: usize, y: usize) -> bool {
502        self.modules[y * self.size + x]
503    }
504
505    fn set(&mut self, x: usize, y: usize, dark: bool) {
506        self.modules[y * self.size + x] = dark;
507    }
508
509    /// Set a module and mark it as a function pattern in the same breath, so no
510    /// caller can set one without the other.
511    fn set_function(&mut self, x: usize, y: usize, dark: bool) {
512        self.modules[y * self.size + x] = dark;
513        self.function[y * self.size + x] = true;
514    }
515
516    fn is_function(&self, x: usize, y: usize) -> bool {
517        self.function[y * self.size + x]
518    }
519}
520
521fn build_symbol(version: u8, ecc: Ecc, codewords: &[u8], forced: Option<u8>) -> QrCode {
522    let size = 4 * version as usize + 17;
523    let mut canvas = Canvas::new(size);
524
525    draw_function_patterns(&mut canvas, version);
526    draw_codewords(&mut canvas, codewords);
527
528    let masked = |mask: u8| {
529        let mut candidate =
530            Canvas { size, modules: canvas.modules.clone(), function: canvas.function.clone() };
531        apply_mask(&mut candidate, mask);
532        draw_format_info(&mut candidate, ecc, mask);
533        candidate
534    };
535
536    // Every mask has to be tried: the standard picks the one with the lowest
537    // penalty score, and a reader recovers which was used by reading the format
538    // information rather than by guessing.
539    let (mask, modules) = match forced {
540        Some(mask) => (mask, masked(mask).modules),
541        None => {
542            let mut best: Option<(u32, u8, Vec<bool>)> = None;
543            for mask in 0..8u8 {
544                let candidate = masked(mask);
545                let score = penalty(&candidate);
546                if best.as_ref().is_none_or(|(current, _, _)| score < *current) {
547                    best = Some((score, mask, candidate.modules));
548                }
549            }
550            let (_, mask, modules) = best.expect("eight masks are always tried");
551            (mask, modules)
552        }
553    };
554
555    QrCode { version, size, ecc, mask, modules }
556}
557
558fn draw_function_patterns(canvas: &mut Canvas, version: u8) {
559    let size = canvas.size;
560
561    // Timing patterns: alternating modules along row 6 and column 6, which give
562    // a reader its module pitch. They are drawn across the full width first and
563    // the finder patterns are drawn over them, because the two overlap at the
564    // ends and the finder pattern is what has to survive there.
565    for i in 0..size {
566        canvas.set_function(i, 6, i % 2 == 0);
567        canvas.set_function(6, i, i % 2 == 0);
568    }
569
570    // Finder patterns, with their separators. Drawing them by Chebyshev
571    // distance from the centre gets the 7x7 pattern and the light separator
572    // ring around it in one expression: rings 0, 1 and 3 are dark; ring 2 is
573    // the light square inside, and ring 4 is the separator.
574    for &(cx, cy) in &[(3usize, 3usize), (size - 4, 3), (3, size - 4)] {
575        for dy in -4i32..=4 {
576            for dx in -4i32..=4 {
577                let (x, y) = (cx as i32 + dx, cy as i32 + dy);
578                if x < 0 || y < 0 || x >= size as i32 || y >= size as i32 {
579                    continue;
580                }
581                let ring = dx.abs().max(dy.abs());
582                canvas.set_function(x as usize, y as usize, ring != 2 && ring != 4);
583            }
584        }
585    }
586
587    // Alignment patterns, at every pairing of the version's coordinates except
588    // the three corners already occupied by finder patterns.
589    let coordinates = ALIGNMENT[version as usize - 1];
590    let last = coordinates.len().saturating_sub(1);
591    for (i, &cx) in coordinates.iter().enumerate() {
592        for (j, &cy) in coordinates.iter().enumerate() {
593            let corner = (i == 0 && j == 0) || (i == 0 && j == last) || (i == last && j == 0);
594            if corner {
595                continue;
596            }
597            for dy in -2i32..=2 {
598                for dx in -2i32..=2 {
599                    let ring = dx.abs().max(dy.abs());
600                    let (x, y) = ((cx as i32 + dx) as usize, (cy as i32 + dy) as usize);
601                    canvas.set_function(x, y, ring != 1);
602                }
603            }
604        }
605    }
606
607    // Version information, versions 7 and up, §7.10: two 3x6 blocks near the
608    // bottom-left and top-right finder patterns, transposes of each other.
609    if version >= 7 {
610        let bits = version_info(version);
611        for i in 0..18 {
612            let dark = (bits >> i) & 1 == 1;
613            let a = size - 11 + i % 3;
614            let b = i / 3;
615            canvas.set_function(a, b, dark);
616            canvas.set_function(b, a, dark);
617        }
618    }
619
620    // Reserve the format-information modules. The values depend on the mask, so
621    // they are written later, once per candidate; all that matters here is that
622    // data placement steps over them. The one exception is the module below the
623    // top-left corner of the bottom-left finder, which is always dark (§7.9.1)
624    // and belongs to no field at all.
625    for i in 0..=5 {
626        canvas.set_function(8, i, false);
627    }
628    canvas.set_function(8, 7, false);
629    canvas.set_function(8, 8, false);
630    canvas.set_function(7, 8, false);
631    for i in 0..8 {
632        canvas.set_function(size - 1 - i, 8, false);
633    }
634    for i in 0..7 {
635        canvas.set_function(8, size - 7 + i, false);
636    }
637    for i in 0..6 {
638        canvas.set_function(i, 8, false);
639    }
640    canvas.set_function(8, size - 8, true);
641}
642
643/// The 18-bit version information: six bits of version number followed by a
644/// BCH(18,6) check computed with the generator 0x1F25 (§7.10 / annex D).
645fn version_info(version: u8) -> u32 {
646    let data = u32::from(version);
647    let mut rest = data;
648    for _ in 0..12 {
649        rest = (rest << 1) ^ (((rest >> 11) & 1) * 0x1f25);
650    }
651    (data << 12) | rest
652}
653
654/// The 15-bit format information: two bits of error-correction level and three
655/// of mask, protected by a BCH(15,5) code with generator 0x537, then XORed with
656/// the mask pattern 0x5412 so an all-zero field cannot occur (§7.9 / annex C).
657fn format_info(ecc: Ecc, mask: u8) -> u32 {
658    let data = (ecc.format_bits() << 3) | u32::from(mask);
659    let mut rest = data;
660    for _ in 0..10 {
661        rest = (rest << 1) ^ (((rest >> 9) & 1) * 0x537);
662    }
663    ((data << 10) | rest) ^ 0x5412
664}
665
666fn draw_format_info(canvas: &mut Canvas, ecc: Ecc, mask: u8) {
667    let size = canvas.size;
668    let bits = format_info(ecc, mask);
669    let bit = |i: u32| (bits >> i) & 1 == 1;
670
671    // First copy, wrapped around the top-left finder pattern. The two jogs at
672    // bits 6 to 8 are where the sequence steps over the timing patterns.
673    for i in 0..=5u32 {
674        canvas.set_function(8, i as usize, bit(i));
675    }
676    canvas.set_function(8, 7, bit(6));
677    canvas.set_function(8, 8, bit(7));
678    canvas.set_function(7, 8, bit(8));
679    for i in 9..15u32 {
680        canvas.set_function(14 - i as usize, 8, bit(i));
681    }
682
683    // Second copy, split between the other two finder patterns, so a symbol
684    // with one damaged corner is still readable.
685    for i in 0..8u32 {
686        canvas.set_function(size - 1 - i as usize, 8, bit(i));
687    }
688    for i in 8..15u32 {
689        canvas.set_function(8, size - 15 + i as usize, bit(i));
690    }
691}
692
693/// Lay the codewords into the symbol in the zig-zag order of §7.7.3: two
694/// modules wide, working right to left, alternating upward and downward, and
695/// stepping over every function module.
696fn draw_codewords(canvas: &mut Canvas, codewords: &[u8]) {
697    let size = canvas.size;
698    let mut index = 0usize;
699    let mut upward = true;
700    let mut right = size - 1;
701
702    loop {
703        // Column 6 is the vertical timing pattern, so the two-module columns
704        // shift one to the left once they reach it. Without this the whole
705        // lower-left of the symbol is off by one column.
706        if right == 6 {
707            right -= 1;
708        }
709
710        for step in 0..size {
711            let y = if upward { size - 1 - step } else { step };
712            for x in [right, right - 1] {
713                if canvas.is_function(x, y) {
714                    continue;
715                }
716                // Bits past the end of the codewords are the remainder bits of
717                // §7.7.3, which are simply zero.
718                let dark = codewords
719                    .get(index / 8)
720                    .is_some_and(|byte| (byte >> (7 - index % 8)) & 1 == 1);
721                canvas.set(x, y, dark);
722                index += 1;
723            }
724        }
725
726        upward = !upward;
727        if right < 2 {
728            break;
729        }
730        right -= 2;
731    }
732}
733
734/// The eight data masks of ISO/IEC 18004 table 10. A mask is XORed over the
735/// data modules only, to break up the large blocks of one colour that make a
736/// symbol hard to read.
737fn mask_condition(mask: u8, x: usize, y: usize) -> bool {
738    match mask {
739        0 => (x + y).is_multiple_of(2),
740        1 => y.is_multiple_of(2),
741        2 => x.is_multiple_of(3),
742        3 => (x + y).is_multiple_of(3),
743        4 => (y / 2 + x / 3).is_multiple_of(2),
744        5 => (x * y) % 2 + (x * y) % 3 == 0,
745        6 => ((x * y) % 2 + (x * y) % 3).is_multiple_of(2),
746        7 => ((x + y) % 2 + (x * y) % 3).is_multiple_of(2),
747        _ => unreachable!("masks are 0 to 7"),
748    }
749}
750
751fn apply_mask(canvas: &mut Canvas, mask: u8) {
752    for y in 0..canvas.size {
753        for x in 0..canvas.size {
754            if !canvas.is_function(x, y) && mask_condition(mask, x, y) {
755                let dark = canvas.get(x, y);
756                canvas.set(x, y, !dark);
757            }
758        }
759    }
760}
761
762/// The four penalty rules of ISO/IEC 18004 table 11. The mask with the lowest
763/// total wins; nothing about the result is a correctness matter, only
764/// readability, but every encoder has to agree on the arithmetic or two
765/// encoders will disagree about which symbol is canonical.
766fn penalty(canvas: &Canvas) -> u32 {
767    let size = canvas.size;
768    let mut score = 0u32;
769
770    // Rule 1: runs of five or more modules of one colour in a row or column
771    // score 3, plus one for each module beyond five.
772    let mut run_penalty = |run: usize| {
773        if run >= 5 {
774            score += 3 + (run as u32 - 5);
775        }
776    };
777    for line in 0..size {
778        for horizontal in [true, false] {
779            let mut run = 0usize;
780            let mut previous = false;
781            for i in 0..size {
782                let dark = if horizontal { canvas.get(i, line) } else { canvas.get(line, i) };
783                if i > 0 && dark == previous {
784                    run += 1;
785                } else {
786                    run_penalty(run);
787                    run = 1;
788                }
789                previous = dark;
790            }
791            run_penalty(run);
792        }
793    }
794
795    // Rule 2: every 2x2 block of one colour scores 3. Overlapping blocks each
796    // count, which is what makes a large solid area expensive.
797    for y in 0..size - 1 {
798        for x in 0..size - 1 {
799            let corner = canvas.get(x, y);
800            if canvas.get(x + 1, y) == corner
801                && canvas.get(x, y + 1) == corner
802                && canvas.get(x + 1, y + 1) == corner
803            {
804                score += 3;
805            }
806        }
807    }
808
809    // Rule 3: the finder-pattern-like sequence 1:1:3:1:1 with four light
810    // modules on one side scores 40 wherever it appears, because a reader
811    // hunting for finder patterns would be misled by it.
812    //
813    // The standard writes this as a *ratio*, and implementations read that
814    // differently. This one matches ZXing and Nayuki's qrcodegen: the literal
815    // eleven-module sequence, at one module per unit. libqrencode instead reads
816    // the ratio as scale-invariant, so it also penalises 2:2:6:2:2 and picks a
817    // different mask for some inputs. Nothing turns on the disagreement — the
818    // format information names the mask, so all eight produce a readable
819    // symbol, and this encoder's output was checked module by module against
820    // libqrencode's under a held-equal mask.
821    const FINDER_LIKE: [bool; 11] =
822        [true, false, true, true, true, false, true, false, false, false, false];
823    if size >= 11 {
824        for line in 0..size {
825            for horizontal in [true, false] {
826                for start in 0..=size - 11 {
827                    let at = |i: usize| {
828                        if horizontal {
829                            canvas.get(start + i, line)
830                        } else {
831                            canvas.get(line, start + i)
832                        }
833                    };
834                    let forward = (0..11).all(|i| at(i) == FINDER_LIKE[i]);
835                    let backward = (0..11).all(|i| at(i) == FINDER_LIKE[10 - i]);
836                    if forward || backward {
837                        score += 40;
838                    }
839                }
840            }
841        }
842    }
843
844    // Rule 4: 10 points for every 5% the proportion of dark modules strays from
845    // half. Written with integer arithmetic so it does not depend on how a
846    // float rounds at a boundary.
847    let total = size * size;
848    let dark = canvas.modules.iter().filter(|&&d| d).count();
849    let deviation = (dark * 20).abs_diff(total * 10);
850    let steps = (deviation.div_ceil(total)).saturating_sub(1);
851    score += 10 * steps as u32;
852
853    score
854}
855
856// ---------------------------------------------------------------------------
857// SVG
858// ---------------------------------------------------------------------------
859
860/// The title used when the caller does not supply one.
861const DEFAULT_TITLE: &str = "QR code";
862
863impl QrCode {
864    /// Render as a standalone `<svg>` element, four pixels per module and a
865    /// four-module quiet zone. The common case for an enrolment page.
866    pub fn to_svg_default(&self) -> String {
867        self.to_svg(4, 4)
868    }
869
870    /// Render as a standalone `<svg>` element with a default title.
871    ///
872    /// `module_pixels` is the on-screen size of one module and `quiet_zone` the
873    /// margin around the symbol, in modules. The standard asks for at least
874    /// four, and readers really do need it — a QR code butted up against
875    /// surrounding content often will not scan.
876    pub fn to_svg(&self, module_pixels: u32, quiet_zone: u32) -> String {
877        self.to_svg_titled(module_pixels, quiet_zone, DEFAULT_TITLE)
878    }
879
880    /// Render as a standalone `<svg>` element with a caller-supplied title.
881    ///
882    /// The output deliberately contains no `<style>` element, no `style`
883    /// attribute and no script: it is embedded in a page served under
884    /// `style-src 'self'; script-src 'self'` with no `unsafe-inline`, so
885    /// anything inline would simply be dropped by the browser and the code
886    /// would render as a blank square. Colours are presentation attributes,
887    /// which CSP does not police.
888    ///
889    /// All of the dark modules go into one `<path>`, accumulated as horizontal
890    /// runs. A version 10 symbol has 3249 modules, and one element each would
891    /// be a document large enough to notice on every page load.
892    pub fn to_svg_titled(&self, module_pixels: u32, quiet_zone: u32, title: &str) -> String {
893        let span = self.size as u32 + 2 * quiet_zone;
894        let pixels = span * module_pixels;
895
896        // Coordinates stay in module units and the viewBox does the scaling, so
897        // the path data is short whatever the requested pixel size.
898        let mut path = String::new();
899        for y in 0..self.size {
900            let mut x = 0;
901            while x < self.size {
902                if !self.module(x, y) {
903                    x += 1;
904                    continue;
905                }
906                let start = x;
907                while x < self.size && self.module(x, y) {
908                    x += 1;
909                }
910                let run = x - start;
911                let _ = write!(
912                    path,
913                    "M{} {}h{run}v1h-{run}z",
914                    start as u32 + quiet_zone,
915                    y as u32 + quiet_zone
916                );
917            }
918        }
919
920        let mut svg = String::with_capacity(path.len() + 512);
921        let _ = write!(
922            svg,
923            "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{pixels}\" height=\"{pixels}\" \
924             viewBox=\"0 0 {span} {span}\" role=\"img\" aria-label=\"{}\" \
925             shape-rendering=\"crispEdges\"><title>{}</title>\
926             <rect width=\"{span}\" height=\"{span}\" fill=\"#fff\"/>\
927             <path fill=\"#000\" d=\"{path}\"/></svg>",
928            escape(title),
929            escape(title)
930        );
931        svg
932    }
933}
934
935/// Escape the five characters that cannot appear literally in XML text or in a
936/// quoted attribute value. The title is caller-supplied, and an application
937/// name with an ampersand in it should not be able to break the document.
938fn escape(text: &str) -> String {
939    let mut out = String::with_capacity(text.len());
940    for character in text.chars() {
941        match character {
942            '&' => out.push_str("&amp;"),
943            '<' => out.push_str("&lt;"),
944            '>' => out.push_str("&gt;"),
945            '"' => out.push_str("&quot;"),
946            '\'' => out.push_str("&#39;"),
947            _ => out.push(character),
948        }
949    }
950    out
951}
952
953#[cfg(test)]
954mod tests {
955    use super::*;
956
957    /// Render a symbol as one string per row, `#` dark and `.` light, which is
958    /// the form the reference matrices below are written in.
959    fn rows(code: &QrCode) -> Vec<String> {
960        (0..code.size())
961            .map(|y| {
962                (0..code.size())
963                    .map(|x| if code.module(x, y) { '#' } else { '.' })
964                    .collect()
965            })
966            .collect()
967    }
968
969    fn assert_matches(code: &QrCode, expected: &[&str]) {
970        let actual = rows(code);
971        assert_eq!(actual.len(), expected.len(), "symbol is the wrong size");
972        for (y, (got, want)) in actual.iter().zip(expected).enumerate() {
973            assert_eq!(got, want, "row {y} differs\n  got  {got}\n  want {want}");
974        }
975    }
976
977    // -----------------------------------------------------------------------
978    // Tables
979    // -----------------------------------------------------------------------
980
981    #[test]
982    fn tables_are_self_consistent() {
983        for version in 1..=MAX_VERSION {
984            for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
985                let (ecc_per_block, group1, _, group2) =
986                    BLOCKS[version as usize - 1][ecc.index()];
987                let blocks = group1 as usize + group2 as usize;
988                let total = data_codewords(version, ecc) + blocks * ecc_per_block as usize;
989                assert_eq!(
990                    total,
991                    TOTAL_CODEWORDS[version as usize - 1] as usize,
992                    "version {version} at {} does not add up",
993                    ecc.name()
994                );
995            }
996        }
997    }
998
999    /// The block tables claim a certain number of codewords; the geometry of
1000    /// the symbol has to actually have room for them. Counting the non-function
1001    /// modules checks the finder, timing, alignment, format and version pattern
1002    /// placement against `TOTAL_CODEWORDS` without trusting either one.
1003    #[test]
1004    fn geometry_matches_the_codeword_counts() {
1005        for version in 1..=MAX_VERSION {
1006            let size = 4 * version as usize + 17;
1007            let mut canvas = Canvas::new(size);
1008            draw_function_patterns(&mut canvas, version);
1009            let free = canvas.function.iter().filter(|&&f| !f).count();
1010            assert_eq!(
1011                free / 8,
1012                TOTAL_CODEWORDS[version as usize - 1] as usize,
1013                "version {version} has room for {} codewords, not the tabulated number",
1014                free / 8
1015            );
1016        }
1017    }
1018
1019    /// Two rows of ISO/IEC 18004 annex A, as the exponents of the primitive
1020    /// element. If the field arithmetic or the polynomial construction were
1021    /// wrong these would not match, and every symbol would still *look* fine.
1022    #[test]
1023    fn generator_polynomials_match_annex_a() {
1024        let logs = |degree: usize| -> Vec<u8> {
1025            generator(degree).iter().map(|&c| GF_LOG[c as usize]).collect()
1026        };
1027        assert_eq!(logs(7), vec![87, 229, 146, 149, 238, 102, 21]);
1028        assert_eq!(logs(10), vec![251, 67, 46, 61, 118, 70, 64, 94, 32, 45]);
1029    }
1030
1031    // -----------------------------------------------------------------------
1032    // Reference symbols
1033    //
1034    // Every matrix below was produced by this encoder and then compared, module
1035    // by module, against libqrencode 4.1.1 running on the same payload
1036    // (`qrencode -8 -l <level> -t PNG -s 1 -m 0`). They are frozen here so the
1037    // tests need no external tool.
1038    // -----------------------------------------------------------------------
1039
1040    const HELLO: &str = "HELLO WORLD";
1041    const OTPAUTH: &str = "otpauth://totp/Rustlavel:alice@example.com?secret=JBSWY3DPEHPK3PXP\
1042                           &issuer=Rustlavel&algorithm=SHA1&digits=6&period=30";
1043    const LONG_OTPAUTH: &str = "otpauth://totp/Rustlavel%20Framework:alice+qr@example.com\
1044                                ?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DP\
1045                                &issuer=Rustlavel%20Framework%20Demonstration%20Deployment\
1046                                &algorithm=SHA256&digits=8&period=60";
1047
1048    /// Version 1, L, mask 4: 21x21 modules.
1049    const HELLO_LOW: &[&str] = &[
1050        "#######.#####.#######",
1051        "#.....#.#.#.#.#.....#",
1052        "#.###.#.#.....#.###.#",
1053        "#.###.#.####..#.###.#",
1054        "#.###.#.....#.#.###.#",
1055        "#.....#.##.#..#.....#",
1056        "#######.#.#.#.#######",
1057        ".........####........",
1058        "##..###..#.#...#.####",
1059        "..#.##..#####....####",
1060        "##..#.#..###.##.#..#.",
1061        "#.#.#....#...#.......",
1062        "#..#..#.#...#.##..##.",
1063        "........##..####.#.##",
1064        "#######..##.#.#.##.#.",
1065        "#.....#.#.####.##..##",
1066        "#.###.#.#.##.##...##.",
1067        "#.###.#.....#...##.##",
1068        "#.###.#..#.#...###...",
1069        "#.....#.#.##.#.......",
1070        "#######.#.#######.#.#",
1071    ];
1072
1073    /// Version 1, M, mask 3: 21x21 modules.
1074    const HELLO_MEDIUM: &[&str] = &[
1075        "#######.#...#.#######",
1076        "#.....#.#...#.#.....#",
1077        "#.###.#.......#.###.#",
1078        "#.###.#.#.#.#.#.###.#",
1079        "#.###.#..###..#.###.#",
1080        "#.....#...###.#.....#",
1081        "#######.#.#.#.#######",
1082        "........#####........",
1083        "#.##.###.#.##.#..#.##",
1084        ".##....#.#######.##..",
1085        ".....#####.#.#.#...##",
1086        "#.#.##.##..#...#.#.#.",
1087        "#...#.##.##.##....#.#",
1088        "........#.##..##..#.#",
1089        "#######.#.#######....",
1090        "#.....#.###..#.#.####",
1091        "#.###.#..#..#.#..#...",
1092        "#.###.#.###...#..###.",
1093        "#.###.#.##..#..#..#..",
1094        "#.....#..###.####...#",
1095        "#######.##.#.#.#.....",
1096    ];
1097
1098    /// Version 1, Q, mask 0: 21x21 modules.
1099    const HELLO_QUARTILE: &[&str] = &[
1100        "#######.#..#..#######",
1101        "#.....#.###...#.....#",
1102        "#.###.#.#.....#.###.#",
1103        "#.###.#.#.##..#.###.#",
1104        "#.###.#.##..#.#.###.#",
1105        "#.....#..#....#.....#",
1106        "#######.#.#.#.#######",
1107        "........#..##........",
1108        ".##.#.##....#.#.#####",
1109        "..####...#....##...#.",
1110        "......#..#####.######",
1111        "#..###.####.#...#..#.",
1112        ".#.##.#.#.##.####.#..",
1113        "........###.##....##.",
1114        "#######.##.....##.###",
1115        "#.....#..####..#....#",
1116        "#.###.#.###.#.#.#.#..",
1117        "#.###.#...##..###.##.",
1118        "#.###.#.#.#.#.#.#.#.#",
1119        "#.....#.#..#....#..#.",
1120        "#######...###.##..###",
1121    ];
1122
1123    /// Version 2, H, mask 3: 25x25 modules.
1124    const HELLO_HIGH: &[&str] = &[
1125        "#######......###..#######",
1126        "#.....#...#.#.#...#.....#",
1127        "#.###.#..###..#...#.###.#",
1128        "#.###.#..##..#..#.#.###.#",
1129        "#.###.#.##...#....#.###.#",
1130        "#.....#....#.##.#.#.....#",
1131        "#######.#.#.#.#.#.#######",
1132        "........#..###.#.........",
1133        "..##..###.....#..##.#....",
1134        "#..##....##..####.##..#.#",
1135        "...##.#...#..#..#.#......",
1136        "...##...#.#.##.#...#.###.",
1137        "#..##.###..#.#.#.###.#.##",
1138        ".....#...#.#..#.##.#####.",
1139        ".###.###.##.#..###..#.##.",
1140        "#.###...##..##..#.###...#",
1141        "...#..#...##...######..##",
1142        "........#...##.##...##...",
1143        "#######.###.##.##.#.#.###",
1144        "#.....#..#.##.#.#...#...#",
1145        "#.###.#......#..#####...#",
1146        "#.###.#.#..#.#....#..#..#",
1147        "#.###.#.##....#..#.##..#.",
1148        "#.....#..#.##...###.###..",
1149        "#######..#...##.#..#...##",
1150    ];
1151
1152    /// Version 7, M, mask 3: 45x45 modules.
1153    const OTPAUTH_MEDIUM: &[&str] = &[
1154        "#######.#.##.#..#.##.###.#.##.#..#..#.#######",
1155        "#.....#.####..#..#####.#...#.##.##.#..#.....#",
1156        "#.###.#..#...#.###....#..##.##..#..#..#.###.#",
1157        "#.###.#.##...#..###.#.##.#...##....##.#.###.#",
1158        "#.###.#....##...#.##########...######.#.###.#",
1159        "#.....#......####..##...####..#.#.....#.....#",
1160        "#######.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#######",
1161        "........#...##.#...##...##..#..#.####........",
1162        "#.##.###..###.#....#######.###...#.##.#..#.##",
1163        "#.##.#..#...#######.#.##.#.#.##.##.###...####",
1164        ".####.#.....##..##..#..#...#..#.#.#.##.#...##",
1165        ".....#.#...###...##.#######..#.##.#..#.#.#..#",
1166        "##..###.#####.#####.###.#.#....#..#....#.#.##",
1167        "..#.#..##.#...##.###...####.##.#.#...###.#.#.",
1168        ".######..##.##..#.###.##.#..#.##....###.####.",
1169        "###..#..#......#..##.#.###..##.##..#.#.#####.",
1170        "###.###.#.#..#..#......#.###..####.##......#.",
1171        "...#.#..#..###...#.#...#...###.#.##.....###.#",
1172        "..#.###..#..#.#..##..####.##.###.##...##.##..",
1173        "#.###..##..#..##.#.###..##.###.#.#.##.###..#.",
1174        ".#..#####.###.####.######.#.####.#..######.##",
1175        "....#...###.........#...##.#.####..##...###.#",
1176        "###.#.#.#....###..#.#.#.#..#..#.#.#.#.#.##.##",
1177        "###.#...#..#...#..###...##.#.....#..#...##.#.",
1178        "##..########.#.###..######...#.#..#.######...",
1179        "###..#.##..##.#.##.#..#..####..###.#####.##..",
1180        "###.#.##.###.#..#.##..#.#.#.###.##..##.##....",
1181        "...###.#..###..####.#.###...#####.##..#.####.",
1182        "####.##.#.###.#...##.#.....#.################",
1183        "..###.....####...#.#..##...###..####.#.##..##",
1184        "..##.##.###.#.......#..#.#...#.####.##....##.",
1185        "###....#.#.#.#..#.#.####.#..##.#.#..........#",
1186        "..#.#.##.....##.##.#####.#.##.#..##.#.#.##.##",
1187        "..#.#....#.###.#.#######...#.####.........#..",
1188        "....#.###..#.#...##..####..##.##.######...###",
1189        ".####..#.##.##...##.#..#...#....#..#.##..#.#.",
1190        "#..##.##.....##.#.#######......#....#####.#..",
1191        "........##.###...#.##...#.##....##.##...##..#",
1192        "#######.#.#....#..###.#.#.###.#..#..#.#.#....",
1193        "#.....#.#.#.#.#.#####...##..#####...#...####.",
1194        "#.###.#..##.#.#####.#####..#.#..#...#########",
1195        "#.###.#.#....##..###.#..#..###.####.#####...#",
1196        "#.###.#.#..###.##..#.#.##.#......#####...###.",
1197        "#.....#...#.###.##........#.##.#.######.....#",
1198        "#######.#.#.#..#.##..###..###..#.###..#...#..",
1199    ];
1200
1201    /// Version 10, M, mask 2: 57x57 modules.
1202    const LONG_OTPAUTH_MEDIUM: &[&str] = &[
1203        "#######....##.#...#.#.####..####...###.#..##..##..#######",
1204        "#.....#..##...##.#####...#.#...##.###.##.#.###.#..#.....#",
1205        "#.###.#.##.#######.#.#####.####....######.#.####..#.###.#",
1206        "#.###.#.#.##.#..##.....###...#####.##....#...#.#..#.###.#",
1207        "#.###.#.#.######.#.######.#####.#..#.#...##....#..#.###.#",
1208        "#.....#.##.#..#.###.#.#..##...#####.#.#####..##...#.....#",
1209        "#######.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#######",
1210        "........##.#.##.#...###.###...#.#.##.#.##.###.###........",
1211        "#.#####..#.####.#.#...#...######....##.#.#.#..#.#.#####..",
1212        ".####...#..#.#..#.#..#.###.#####.#.###.#.###.#.###.##..##",
1213        "####..#.#....#..#...##.##.##..#.#.######.#..###.#.##.###.",
1214        "#.......##...##.##.#.....#.#.#####.#.#.######.#.#.##..###",
1215        "..###.####..####.##..#.##.#.#.##.##.#.#...##.#.##....#..#",
1216        ".#...#.#..#.#.####..#...#..#######..##.####.#..###...#..#",
1217        ".#.##.###..###...#..#.####..#.....###.##.#.#####.....###.",
1218        ".##.#...#.##.##.###.##..#..###......######.#.#..##..#####",
1219        "#.#.####.......#..##...#.#.#.###..#####..#.#.#...#.....#.",
1220        ".##.#..######..######.###.##.##.....##.#.##.#..###......#",
1221        "..#.#.##.#.#.#...#.##..#.##.........#.#..##.....###...##.",
1222        "##.###.##.#######..#...#...##.#.##...#..##..#...#.#.###..",
1223        "##.#..##...#...#..###.#.###..###.#####...#.#.#.........#.",
1224        "#.......#.#...###.####.##..##.##.#.###.######...##...#..#",
1225        "##..###.......##.#...##.#....##...#####..#.####...##.#.#.",
1226        "##.##..##....#..##...##.##..####...##..######.....##.###.",
1227        "###..##...#..#....##.##.##.#.##....###...#.#.###.....#...",
1228        "#####...#.#..##..##...#####.####.....#.#.##.....##..#.##.",
1229        ".#..#####...#.......###...#####...#.###..###.###########.",
1230        "..#.#...####..#.####......#...###....#...##.#.#.#...#####",
1231        ".#.##.#.#....##.####..#...#.#.##.##.#.....##..###.#.##.#.",
1232        "#.###...#...#..###..#####.#...####..#..#.####...#...#.#.#",
1233        "#########.#.#.#...###...#########.##.##..#.#..########...",
1234        "...###.####.#......#####...###.###....#.#.#.#..###.#.###.",
1235        "#...####.#.##.#.#.#.#.#.#...#..#.#.###...#.#....#.#.#.##.",
1236        ".###.#..#...##.#.##.##.#..#...##...#.#..#.####.####..##..",
1237        "###.###.....###.....#.#..##.####.#.#.###.#.####.....#####",
1238        ".####..#.##.#......#####.#..####.###.##.#.#..####.#.###..",
1239        ".#..###..#..##...#..##...##..#.#....#.#....#.#.....#.#...",
1240        "...#....###.#..####...####.#..#..#......###.#..###...#.##",
1241        "#####.###.#..#..#....#.#..#..###..#.#.##.#..#..#...#.###.",
1242        ".#.#...#####....#.####.###.#....#.##...##.#.#..#.###..##.",
1243        "#..####.#.###..##..#####.#..####...##.##.#.#.....#.###..#",
1244        "#...#.....#.###..#.####.#....##.#..#.#.#.####........##.#",
1245        "##.#.##.#....#....#.#....######.....#.###....###.#.#..##.",
1246        "##..#...#.####.##...#.#.##..#....#..##..#..##..##.##.##.#",
1247        "..#.###...#.#.####..#...#...##.###.##.#..###.#..#..###...",
1248        ".#.#...#......##....##.##.......#....#....###...##....##.",
1249        "#.#..##....##...#..#.##.#.#..#.####...##.#.#####.#.#...#.",
1250        "#####....##.##.##.##..#.#.##..#.##...###..#.#..##.##.####",
1251        "......#.#.....##..#..#...#######.#.###.....#.#..#####.#..",
1252        "........#.#.#......#...##.#...#..#.###..####....#...###.#",
1253        "#######....##.##.##.##.####.#.####.##..#....#####.#.#.##.",
1254        "#.....#.###.###.#..###..#.#...###......###..#.#.#...#####",
1255        "#.###.#.##.##.###.#..####.######.######..###.##.#######..",
1256        "#.###.#.#..####.#..###..####.##.#..###....##...#.....##..",
1257        "#.###.#.##...#....#....####..#....#..##..#.####.#....#...",
1258        "#.....#......####...###.#..#.##..#.#.#..###..#.###...##..",
1259        "#######.####.#.########..#.#...#..#.##...###.#.##...#..#.",
1260    ];
1261
1262    /// Every matrix above is libqrencode 4.1.1's own output for that payload
1263    /// and level, so this compares two independent encoders rather than this
1264    /// one against itself. The mask is held equal because mask *selection* is a
1265    /// readability heuristic the two read differently (see `penalty`); every
1266    /// other decision — the mode and count indicators, the padding, the
1267    /// Reed-Solomon codewords, the block split and interleave, the function
1268    /// patterns, the format and version information and the placement order —
1269    /// has to agree module for module for this to pass.
1270    #[test]
1271    fn matches_libqrencode_module_for_module() {
1272        let cases: &[(&str, Ecc, u8, &[&str])] = &[
1273            (HELLO, Ecc::Low, 4, HELLO_LOW),
1274            (HELLO, Ecc::Medium, 3, HELLO_MEDIUM),
1275            (HELLO, Ecc::Quartile, 0, HELLO_QUARTILE),
1276            (HELLO, Ecc::High, 3, HELLO_HIGH),
1277            (OTPAUTH, Ecc::Medium, 3, OTPAUTH_MEDIUM),
1278            (LONG_OTPAUTH, Ecc::Medium, 2, LONG_OTPAUTH_MEDIUM),
1279        ];
1280
1281        for &(payload, ecc, mask, expected) in cases {
1282            let code = encode_forced(payload, ecc, mask).unwrap();
1283            assert_eq!(code.size(), expected.len());
1284            assert_matches(&code, expected);
1285        }
1286    }
1287
1288    /// The same payloads, encoded the ordinary way. The version and the size
1289    /// have to come out as libqrencode chose them even though the mask need
1290    /// not, because version selection is arithmetic and not a heuristic.
1291    #[test]
1292    fn version_selection_agrees_with_libqrencode() {
1293        assert_eq!(encode_with(HELLO, Ecc::Low).unwrap().version(), 1);
1294        assert_eq!(encode_with(HELLO, Ecc::Medium).unwrap().version(), 1);
1295        assert_eq!(encode_with(HELLO, Ecc::Quartile).unwrap().version(), 1);
1296        assert_eq!(encode_with(HELLO, Ecc::High).unwrap().version(), 2);
1297        assert_eq!(encode_with(OTPAUTH, Ecc::Medium).unwrap().version(), 7);
1298        assert_eq!(encode_with(LONG_OTPAUTH, Ecc::Medium).unwrap().version(), 10);
1299        assert_eq!(LONG_OTPAUTH.len(), 199);
1300    }
1301
1302    // -----------------------------------------------------------------------
1303    // Reading the message back
1304    // -----------------------------------------------------------------------
1305
1306    /// Undo everything `encode_with` did and recover the original string:
1307    /// unmask, walk the zig-zag, regroup the codewords into blocks, drop the
1308    /// error correction and parse the bit stream.
1309    ///
1310    /// This is not a QR decoder — it trusts the format information rather than
1311    /// searching for it, and it does not use the error-correction codewords to
1312    /// repair anything. It exists so that placement, interleaving, padding and
1313    /// the character-count indicator can be checked on every version and level
1314    /// without an external tool, which the frozen matrices above cannot do for
1315    /// more than the six payloads they cover.
1316    fn read_back(code: &QrCode) -> String {
1317        let size = code.size();
1318        let mut canvas = Canvas::new(size);
1319        draw_function_patterns(&mut canvas, code.version());
1320
1321        // The same walk as `draw_codewords`, reading instead of writing and
1322        // removing the mask on the way past.
1323        let mut bits: Vec<bool> = Vec::new();
1324        let mut upward = true;
1325        let mut right = size - 1;
1326        loop {
1327            if right == 6 {
1328                right -= 1;
1329            }
1330            for step in 0..size {
1331                let y = if upward { size - 1 - step } else { step };
1332                for x in [right, right - 1] {
1333                    if canvas.is_function(x, y) {
1334                        continue;
1335                    }
1336                    bits.push(code.module(x, y) ^ mask_condition(code.mask(), x, y));
1337                }
1338            }
1339            upward = !upward;
1340            if right < 2 {
1341                break;
1342            }
1343            right -= 2;
1344        }
1345
1346        let interleaved: Vec<u8> = bits
1347            .chunks(8)
1348            .filter(|chunk| chunk.len() == 8)
1349            .map(|chunk| {
1350                chunk.iter().enumerate().fold(
1351                    0u8,
1352                    |byte, (i, &bit)| if bit { byte | 1 << (7 - i) } else { byte },
1353                )
1354            })
1355            .collect();
1356
1357        // Undo the interleave: the codewords were taken one per block in turn,
1358        // so hand them back out the same way.
1359        let (_, group1, data_per_block, group2) =
1360            BLOCKS[code.version() as usize - 1][code.ecc().index()];
1361        let lengths: Vec<usize> = (0..(group1 + group2) as usize)
1362            .map(|block| data_per_block as usize + usize::from(block >= group1 as usize))
1363            .collect();
1364        let mut blocks: Vec<Vec<u8>> = vec![Vec::new(); lengths.len()];
1365        let mut source = interleaved.iter();
1366        for index in 0..data_per_block as usize + usize::from(group2 > 0) {
1367            for (block, &length) in blocks.iter_mut().zip(&lengths) {
1368                if index < length {
1369                    block.push(*source.next().expect("the symbol holds every data codeword"));
1370                }
1371            }
1372        }
1373        let data = blocks.concat();
1374
1375        let mut cursor = 0usize;
1376        let mut take = |width: usize| {
1377            let mut value = 0usize;
1378            for _ in 0..width {
1379                let bit = (data[cursor / 8] >> (7 - cursor % 8)) & 1;
1380                value = value << 1 | bit as usize;
1381                cursor += 1;
1382            }
1383            value
1384        };
1385        assert_eq!(take(4), MODE_BYTE as usize, "mode indicator is not byte mode");
1386        let length = take(count_bits(code.version()));
1387        let message: Vec<u8> = (0..length).map(|_| take(8) as u8).collect();
1388        String::from_utf8(message).expect("byte mode round trip is not valid UTF-8")
1389    }
1390
1391    #[test]
1392    fn every_version_and_level_reads_back() {
1393        for version in 1..=MAX_VERSION {
1394            for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1395                let full = capacity_bytes(version, ecc);
1396                let smallest = if version == 1 { 0 } else { capacity_bytes(version - 1, ecc) + 1 };
1397                for length in [smallest, (smallest + full) / 2, full] {
1398                    // A payload that is not all one byte, so a placement error
1399                    // cannot cancel itself out.
1400                    let payload: String =
1401                        (0..length).map(|i| (b'!' + (i % 90) as u8) as char).collect();
1402                    let code = encode_with(&payload, ecc).unwrap();
1403                    assert_eq!(code.version(), version, "{length} bytes at {}", ecc.name());
1404                    assert_eq!(read_back(&code), payload, "version {version} at {}", ecc.name());
1405                }
1406            }
1407        }
1408    }
1409
1410    #[test]
1411    fn the_payloads_that_matter_read_back() {
1412        for payload in [HELLO, OTPAUTH, LONG_OTPAUTH, "", "a", "\u{e9}\u{4e2d}\u{1f512}"] {
1413            for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1414                let Ok(code) = encode_with(payload, ecc) else { continue };
1415                assert_eq!(read_back(&code), payload, "{payload:?} at {}", ecc.name());
1416            }
1417        }
1418    }
1419
1420    /// All eight masks have to be applied correctly, not just the one the
1421    /// penalty rules happen to like. Each was also compared against
1422    /// libqrencode's output for a payload where libqrencode chose that mask.
1423    #[test]
1424    fn every_mask_reads_back() {
1425        for mask in 0..8u8 {
1426            for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1427                let code = encode_forced(OTPAUTH, ecc, mask).unwrap();
1428                assert_eq!(code.mask(), mask);
1429                assert_eq!(read_back(&code), OTPAUTH, "mask {mask} at {}", ecc.name());
1430            }
1431        }
1432    }
1433
1434    // -----------------------------------------------------------------------
1435    // Behaviour
1436    // -----------------------------------------------------------------------
1437
1438    #[test]
1439    fn version_is_the_smallest_that_fits() {
1440        // Version 1 at Medium holds 14 bytes; one more needs version 2.
1441        assert_eq!(capacity_bytes(1, Ecc::Medium), 14);
1442        assert_eq!(encode(&"a".repeat(14)).unwrap().version(), 1);
1443        assert_eq!(encode(&"a".repeat(15)).unwrap().version(), 2);
1444
1445        // Version 9 at Medium holds 180 bytes. Version 10 widens the character
1446        // count indicator from 8 bits to 16, so it gains eight bits less than
1447        // its extra codewords suggest.
1448        assert_eq!(capacity_bytes(9, Ecc::Medium), 180);
1449        assert_eq!(capacity_bytes(10, Ecc::Medium), 213);
1450        assert_eq!(encode(&"a".repeat(180)).unwrap().version(), 9);
1451        assert_eq!(encode(&"a".repeat(181)).unwrap().version(), 10);
1452    }
1453
1454    #[test]
1455    fn size_follows_the_version() {
1456        for version in 1..=MAX_VERSION {
1457            let length = capacity_bytes(version, Ecc::Medium);
1458            let code = encode(&"a".repeat(length)).unwrap();
1459            assert_eq!(code.version(), version);
1460            assert_eq!(code.size(), 4 * version as usize + 17);
1461        }
1462    }
1463
1464    #[test]
1465    fn too_long_is_an_error_not_a_truncation() {
1466        for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1467            let limit = capacity_bytes(MAX_VERSION, ecc);
1468            assert!(encode_with(&"a".repeat(limit), ecc).is_ok());
1469
1470            let error = encode_with(&"a".repeat(limit + 1), ecc).unwrap_err().to_string();
1471            assert!(error.contains("too long"), "unhelpful message: {error}");
1472            assert!(error.contains(&limit.to_string()), "message omits the limit: {error}");
1473            assert!(error.contains(ecc.name()), "message omits the level: {error}");
1474        }
1475    }
1476
1477    #[test]
1478    fn every_level_encodes_the_same_input() {
1479        let uri = "otpauth://totp/Rustlavel:alice@example.com?secret=JBSWY3DPEHPK3PXP\
1480                   &issuer=Rustlavel&algorithm=SHA1&digits=6&period=30";
1481        let mut versions = Vec::new();
1482        for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1483            let code = encode_with(uri, ecc).unwrap();
1484            assert_eq!(code.ecc(), ecc);
1485            assert_eq!(code.size(), 4 * code.version() as usize + 17);
1486            versions.push(code.version());
1487        }
1488        // More error correction never needs a smaller symbol.
1489        assert!(versions.windows(2).all(|pair| pair[0] <= pair[1]), "{versions:?}");
1490    }
1491
1492    #[test]
1493    fn empty_input_is_a_valid_symbol() {
1494        let code = encode("").unwrap();
1495        assert_eq!(code.version(), 1);
1496        assert_eq!(code.size(), 21);
1497    }
1498
1499    #[test]
1500    fn format_information_records_the_chosen_mask() {
1501        // Read the format bits back out of the first copy, undo the 0x5412
1502        // mask, and check the five data bits are the level and the mask the
1503        // code says it used. A symbol whose format bits disagree with its
1504        // masking is unreadable, and looks perfectly normal.
1505        for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
1506            let code = encode_with("format information", ecc).unwrap();
1507            let size = code.size();
1508
1509            let mut bits = 0u32;
1510            let mut read = |i: u32, x: usize, y: usize| {
1511                if code.module(x, y) {
1512                    bits |= 1 << i;
1513                }
1514            };
1515            for i in 0..=5u32 {
1516                read(i, 8, i as usize);
1517            }
1518            read(6, 8, 7);
1519            read(7, 8, 8);
1520            read(8, 7, 8);
1521            for i in 9..15u32 {
1522                read(i, 14 - i as usize, 8);
1523            }
1524
1525            let data = (bits ^ 0x5412) >> 10;
1526            assert_eq!(data >> 3, ecc.format_bits(), "level bits are wrong");
1527            assert_eq!(data & 7, u32::from(code.mask()), "mask bits are wrong");
1528            assert!(code.mask() < 8);
1529
1530            // And the second copy has to say the same thing.
1531            let mut second = 0u32;
1532            for i in 0..8u32 {
1533                if code.module(size - 1 - i as usize, 8) {
1534                    second |= 1 << i;
1535                }
1536            }
1537            for i in 8..15u32 {
1538                if code.module(8, size - 15 + i as usize) {
1539                    second |= 1 << i;
1540                }
1541            }
1542            assert_eq!(second, bits, "the two copies of the format information differ");
1543        }
1544    }
1545
1546    #[test]
1547    fn version_information_is_present_from_version_seven() {
1548        for version in 1..=MAX_VERSION {
1549            let code = encode(&"a".repeat(capacity_bytes(version, Ecc::Medium))).unwrap();
1550            let size = code.size();
1551            // The bottom-left version block sits at rows size-11..size-9,
1552            // columns 0..6. Below version 7 those modules are ordinary data,
1553            // so this only checks the bits where they are meaningful.
1554            if version < 7 {
1555                continue;
1556            }
1557            let mut bits = 0u32;
1558            for i in 0..18u32 {
1559                if code.module(size - 11 + (i % 3) as usize, (i / 3) as usize) {
1560                    bits |= 1 << i;
1561                }
1562            }
1563            assert_eq!(bits, version_info(version), "version {version} block is wrong");
1564            assert_eq!(bits >> 12, u32::from(version));
1565        }
1566    }
1567
1568    #[test]
1569    fn finder_patterns_are_where_they_belong() {
1570        let code = encode("finders").unwrap();
1571        let size = code.size();
1572        for &(cx, cy) in &[(3usize, 3usize), (size - 4, 3), (3, size - 4)] {
1573            for dy in -4i32..=4 {
1574                for dx in -4i32..=4 {
1575                    let (x, y) = (cx as i32 + dx, cy as i32 + dy);
1576                    if x < 0 || y < 0 || x >= size as i32 || y >= size as i32 {
1577                        continue;
1578                    }
1579                    let ring = dx.abs().max(dy.abs());
1580                    assert_eq!(
1581                        code.module(x as usize, y as usize),
1582                        ring != 2 && ring != 4,
1583                        "finder at ({cx},{cy}) is wrong at ({dx},{dy})"
1584                    );
1585                }
1586            }
1587        }
1588    }
1589
1590    #[test]
1591    fn the_dark_module_is_always_dark() {
1592        for version in 1..=MAX_VERSION {
1593            let code = encode(&"a".repeat(capacity_bytes(version, Ecc::Medium))).unwrap();
1594            assert!(code.module(8, code.size() - 8), "version {version}");
1595        }
1596    }
1597
1598    #[test]
1599    fn modules_outside_the_symbol_are_light() {
1600        let code = encode("quiet zone").unwrap();
1601        assert!(!code.module(code.size(), 0));
1602        assert!(!code.module(0, code.size()));
1603        assert!(!code.module(usize::MAX, usize::MAX));
1604    }
1605
1606    // -----------------------------------------------------------------------
1607    // SVG
1608    // -----------------------------------------------------------------------
1609
1610    #[test]
1611    fn svg_carries_nothing_the_policy_would_block() {
1612        let code = encode("otpauth://totp/Rustlavel:alice@example.com?secret=JBSWY3DPEHPK3PXP")
1613            .unwrap();
1614        for svg in [code.to_svg_default(), code.to_svg(8, 4), code.to_svg_titled(4, 0, "Scan me")] {
1615            assert!(!svg.contains("style"), "inline style would be dropped by the policy");
1616            assert!(!svg.contains("script"), "a script would be dropped by the policy");
1617            assert!(!svg.contains("onload"));
1618            assert!(svg.starts_with("<svg "));
1619            assert!(svg.ends_with("</svg>"));
1620            assert!(svg.contains("role=\"img\""));
1621            assert!(svg.contains("<title>"));
1622            assert!(svg.contains("fill=\"#000\""));
1623            // One path for the whole symbol, not one element per module.
1624            assert_eq!(svg.matches("<path").count(), 1);
1625        }
1626    }
1627
1628    #[test]
1629    fn svg_geometry_follows_the_arguments() {
1630        let code = encode("geometry").unwrap();
1631        let span = code.size() + 8;
1632        let svg = code.to_svg(6, 4);
1633        assert!(svg.contains(&format!("width=\"{}\"", span * 6)), "{svg}");
1634        assert!(svg.contains(&format!("viewBox=\"0 0 {span} {span}\"")), "{svg}");
1635
1636        let tight = code.to_svg(1, 0);
1637        assert!(tight.contains(&format!("viewBox=\"0 0 {} {}\"", code.size(), code.size())));
1638    }
1639
1640    #[test]
1641    fn svg_title_is_escaped() {
1642        let code = encode("escaping").unwrap();
1643        let svg = code.to_svg_titled(4, 4, "Acme & Co <alice@example.com>");
1644        assert!(svg.contains("<title>Acme &amp; Co &lt;alice@example.com&gt;</title>"), "{svg}");
1645        assert!(!svg.contains("<alice"));
1646    }
1647
1648    #[test]
1649    fn svg_path_covers_exactly_the_dark_modules() {
1650        // Each run in the path is one `M`, so counting them against the number
1651        // of horizontal dark runs in the matrix catches a path that has dropped
1652        // or duplicated a module.
1653        let code = encode("runs").unwrap();
1654        let mut runs = 0;
1655        for y in 0..code.size() {
1656            let mut previous = false;
1657            for x in 0..code.size() {
1658                let dark = code.module(x, y);
1659                if dark && !previous {
1660                    runs += 1;
1661                }
1662                previous = dark;
1663            }
1664        }
1665        assert_eq!(code.to_svg(4, 4).matches('M').count(), runs);
1666    }
1667}