1use rustlavel_core::{Error, Result};
24use std::fmt::Write as _;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum Ecc {
36 Low,
38 #[default]
40 Medium,
41 Quartile,
43 High,
45}
46
47impl Ecc {
48 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 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 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
85const MAX_VERSION: u8 = 10;
92
93const TOTAL_CODEWORDS: [u16; MAX_VERSION as usize] = [26, 44, 70, 100, 134, 172, 196, 242, 292, 346];
96
97#[allow(clippy::type_complexity)]
111const BLOCKS: [[(u8, u8, u8, u8); 4]; MAX_VERSION as usize] = [
112 [(7, 1, 19, 0), (10, 1, 16, 0), (13, 1, 13, 0), (17, 1, 9, 0)],
114 [(10, 1, 34, 0), (16, 1, 28, 0), (22, 1, 22, 0), (28, 1, 16, 0)],
116 [(15, 1, 55, 0), (26, 1, 44, 0), (18, 2, 17, 0), (22, 2, 13, 0)],
118 [(20, 1, 80, 0), (18, 2, 32, 0), (26, 2, 24, 0), (16, 4, 9, 0)],
120 [(26, 1, 108, 0), (24, 2, 43, 0), (18, 2, 15, 2), (22, 2, 11, 2)],
122 [(18, 2, 68, 0), (16, 4, 27, 0), (24, 4, 19, 0), (28, 4, 15, 0)],
124 [(20, 2, 78, 0), (18, 4, 31, 0), (18, 2, 14, 4), (26, 4, 13, 1)],
126 [(24, 2, 97, 0), (22, 2, 38, 2), (22, 4, 18, 2), (26, 4, 14, 2)],
128 [(30, 2, 116, 0), (22, 3, 36, 2), (20, 4, 16, 4), (24, 4, 12, 4)],
130 [(18, 2, 68, 2), (26, 4, 43, 1), (24, 6, 19, 2), (28, 6, 15, 2)],
132];
133
134const 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
151const MODE_BYTE: u32 = 0b0100;
153
154const PAD_CODEWORDS: [u8; 2] = [0xEC, 0x11];
159
160const GF_POLY: u16 = 0x11d;
167
168const GF_EXP: [u8; 256] = build_exp();
170const 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 while i < 255 {
195 log[GF_EXP[i] as usize] = i as u8;
196 i += 1;
197 }
198 log
199}
200
201fn 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
211fn generator(degree: usize) -> Vec<u8> {
219 let mut coefficients = vec![0u8; degree];
220 if let Some(last) = coefficients.last_mut() {
223 *last = 1;
224 }
225
226 let mut root = 1u8;
227 for _ in 0..degree {
228 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
242fn remainder(data: &[u8], divisor: &[u8]) -> Vec<u8> {
245 let mut result = vec![0u8; divisor.len()];
246
247 for &byte in data {
248 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
263fn 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
274const fn count_bits(version: u8) -> usize {
278 if version <= 9 {
279 8
280 } else {
281 16
282 }
283}
284
285fn capacity_bytes(version: u8, ecc: Ecc) -> usize {
287 let bits = data_codewords(version, ecc) * 8;
288 (bits - 4 - count_bits(version)) / 8
290}
291
292fn choose_version(length: usize, ecc: Ecc) -> Option<u8> {
294 (1..=MAX_VERSION).find(|&version| length <= capacity_bytes(version, ecc))
295}
296
297#[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#[derive(Debug, Clone)]
324pub struct QrCode {
325 version: u8,
326 size: usize,
327 ecc: Ecc,
328 mask: u8,
329 modules: Vec<bool>,
331}
332
333impl QrCode {
334 pub fn size(&self) -> usize {
336 self.size
337 }
338
339 pub fn version(&self) -> u8 {
341 self.version
342 }
343
344 pub fn ecc(&self) -> Ecc {
346 self.ecc
347 }
348
349 pub fn mask(&self) -> u8 {
352 self.mask
353 }
354
355 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
365pub fn encode(data: &str) -> Result<QrCode> {
367 encode_with(data, Ecc::Medium)
368}
369
370pub 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#[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
407fn 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 let terminator = (capacity * 8 - bits.len()).min(4);
422 bits.push(0, terminator);
423 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 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 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
484struct 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 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 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 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 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 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 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 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
643fn 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
654fn 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 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 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
693fn 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 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 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
734fn 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
762fn penalty(canvas: &Canvas) -> u32 {
767 let size = canvas.size;
768 let mut score = 0u32;
769
770 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 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 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 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
856const DEFAULT_TITLE: &str = "QR code";
862
863impl QrCode {
864 pub fn to_svg_default(&self) -> String {
867 self.to_svg(4, 4)
868 }
869
870 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 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 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
935fn 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("&"),
943 '<' => out.push_str("<"),
944 '>' => out.push_str(">"),
945 '"' => out.push_str("""),
946 '\'' => out.push_str("'"),
947 _ => out.push(character),
948 }
949 }
950 out
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 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 #[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 #[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 #[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 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 #[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 #[test]
1439 fn version_is_the_smallest_that_fits() {
1440 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 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 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 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 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 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 #[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 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 & Co <alice@example.com></title>"), "{svg}");
1645 assert!(!svg.contains("<alice"));
1646 }
1647
1648 #[test]
1649 fn svg_path_covers_exactly_the_dark_modules() {
1650 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}