Skip to content

Commit ed0966d

Browse files
authored
feat: finish production readiness issues
Closes #6, closes #8, closes #11, closes #12, closes #13, closes #14, closes #15, closes #17, closes #18, and closes #19.
1 parent 2d52e72 commit ed0966d

29 files changed

Lines changed: 2469 additions & 183 deletions

crates/fastqr_core/src/decode.rs

Lines changed: 169 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,13 @@ fn deinterleave_and_verify(
231231
} else {
232232
&block[data_len..data_len + ecc_len]
233233
};
234-
if !reed_solomon::check_segments(&block[..data_len], ecc_slice, ecc_len) {
234+
let mut corrected = Vec::with_capacity(data_len + ecc_len);
235+
corrected.extend_from_slice(&block[..data_len]);
236+
corrected.extend_from_slice(ecc_slice);
237+
if !reed_solomon::correct(&mut corrected, ecc_len) {
235238
return Err(QrError::Checksum);
236239
}
237-
data.extend_from_slice(&block[..data_len]);
240+
data.extend_from_slice(&corrected[..data_len]);
238241
}
239242

240243
Ok(data)
@@ -448,9 +451,69 @@ impl<'a> BitReader<'a> {
448451

449452
#[cfg(test)]
450453
mod tests {
451-
use crate::{BitGrid, QrError};
454+
use crate::{
455+
BitGrid, EncodeOptions, ErrorCorrectionLevel, MaskPattern, QrError, Version, encode_text,
456+
tables::num_data_codewords,
457+
};
458+
459+
use super::{build_function_modules, decode_modules, decode_payload};
460+
461+
fn version_one_options(error_correction: ErrorCorrectionLevel) -> EncodeOptions {
462+
EncodeOptions {
463+
min_version: Version::MIN,
464+
max_version: Version::MIN,
465+
min_error_correction: error_correction,
466+
boost_error_correction: false,
467+
mask: Some(MaskPattern::new(0).expect("valid mask")),
468+
}
469+
}
452470

453-
use super::decode_modules;
471+
fn fixed_version_options(
472+
version: Version,
473+
error_correction: ErrorCorrectionLevel,
474+
) -> EncodeOptions {
475+
EncodeOptions {
476+
min_version: version,
477+
max_version: version,
478+
min_error_correction: error_correction,
479+
boost_error_correction: false,
480+
mask: Some(MaskPattern::new(0).expect("valid mask")),
481+
}
482+
}
483+
484+
fn flip_codeword_bit(modules: &mut BitGrid, version: Version, codeword: usize, bit: usize) {
485+
let functions = build_function_modules(version);
486+
let size = modules.size();
487+
let target_bit = codeword * 8 + bit;
488+
let mut bit_index = 0_usize;
489+
let mut right = size as isize - 1;
490+
while right >= 1 {
491+
if right == 6 {
492+
right = 5;
493+
}
494+
for vertical in 0..size {
495+
for delta in 0..2 {
496+
let x = (right - delta) as usize;
497+
let upward = ((right + 1) & 2) == 0;
498+
let y = if upward {
499+
size - 1 - vertical
500+
} else {
501+
vertical
502+
};
503+
if functions.get(x, y) {
504+
continue;
505+
}
506+
if bit_index == target_bit {
507+
modules.invert(x, y);
508+
return;
509+
}
510+
bit_index += 1;
511+
}
512+
}
513+
right -= 2;
514+
}
515+
panic!("codeword bit {target_bit} is outside the matrix payload");
516+
}
454517

455518
#[test]
456519
fn rejects_matrix_sizes_above_qr_version_40() {
@@ -469,4 +532,106 @@ mod tests {
469532
Err(QrError::InvalidMatrixSize(1045)),
470533
);
471534
}
535+
536+
#[test]
537+
fn rejects_matrix_sizes_below_qr_version_1() {
538+
let undersized = BitGrid::new(20);
539+
assert_eq!(
540+
decode_modules(&undersized),
541+
Err(QrError::InvalidMatrixSize(20)),
542+
);
543+
}
544+
545+
#[test]
546+
fn rejects_malformed_format_information() {
547+
let matrix = BitGrid::new(Version::MIN.size());
548+
assert_eq!(
549+
decode_modules(&matrix),
550+
Err(QrError::InvalidFormatInformation),
551+
);
552+
}
553+
554+
#[test]
555+
fn rejects_unsupported_payload_mode() {
556+
assert_eq!(
557+
decode_payload(&[0b0011_0000], Version::MIN),
558+
Err(QrError::UnsupportedMode(0x3)),
559+
);
560+
}
561+
562+
#[test]
563+
fn rejects_truncated_byte_payload() {
564+
assert_eq!(
565+
decode_payload(&[0x40, 0x10], Version::MIN),
566+
Err(QrError::MissingTerminator),
567+
);
568+
}
569+
570+
#[test]
571+
fn rejects_payload_values_that_fail_mode_checksum() {
572+
assert_eq!(
573+
decode_payload(&[0x10, 0x0F, 0xE8], Version::MIN),
574+
Err(QrError::Checksum),
575+
);
576+
}
577+
578+
#[test]
579+
fn rejects_codeword_damage_beyond_error_correction_capacity() {
580+
let code =
581+
encode_text("HELLO", version_one_options(ErrorCorrectionLevel::Low)).expect("encodes");
582+
let mut modules = code.modules().clone();
583+
584+
for codeword in 0..4 {
585+
flip_codeword_bit(&mut modules, code.version(), codeword, 0);
586+
}
587+
588+
assert_eq!(decode_modules(&modules), Err(QrError::Checksum));
589+
}
590+
591+
#[test]
592+
fn decodes_with_correctable_data_codeword_damage() {
593+
let code =
594+
encode_text("HELLO", version_one_options(ErrorCorrectionLevel::High)).expect("encodes");
595+
let mut modules = code.modules().clone();
596+
597+
for codeword in [0, 2, 4] {
598+
flip_codeword_bit(&mut modules, code.version(), codeword, 0);
599+
}
600+
601+
let decoded = decode_modules(&modules).expect("corrects damaged data codewords");
602+
assert_eq!(decoded.text.as_deref(), Some("HELLO"));
603+
}
604+
605+
#[test]
606+
fn decodes_with_correctable_ecc_codeword_damage() {
607+
let code =
608+
encode_text("HELLO", version_one_options(ErrorCorrectionLevel::High)).expect("encodes");
609+
let mut modules = code.modules().clone();
610+
let first_ecc_codeword = num_data_codewords(code.version(), code.error_correction());
611+
612+
for codeword in first_ecc_codeword..first_ecc_codeword + 3 {
613+
flip_codeword_bit(&mut modules, code.version(), codeword, 0);
614+
}
615+
616+
let decoded = decode_modules(&modules).expect("corrects damaged ecc codewords");
617+
assert_eq!(decoded.text.as_deref(), Some("HELLO"));
618+
}
619+
620+
#[test]
621+
fn decodes_with_correctable_damage_across_interleaved_blocks() {
622+
let version = Version::new(4).expect("valid version");
623+
let code = encode_text(
624+
"INTERLEAVED BLOCKS",
625+
fixed_version_options(version, ErrorCorrectionLevel::High),
626+
)
627+
.expect("encodes");
628+
let mut modules = code.modules().clone();
629+
630+
for codeword in 0..4 {
631+
flip_codeword_bit(&mut modules, code.version(), codeword, 0);
632+
}
633+
634+
let decoded = decode_modules(&modules).expect("corrects damaged interleaved blocks");
635+
assert_eq!(decoded.text.as_deref(), Some("INTERLEAVED BLOCKS"));
636+
}
472637
}

crates/fastqr_core/src/encode.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,11 +571,23 @@ mod tests {
571571
let code = encode_text("FASTQR", EncodeOptions::default()).expect("encodes");
572572

573573
for coordinate in 0..=6 {
574-
assert!(code.module(coordinate, 6), "top-left finder bottom edge at ({coordinate}, 6)");
575-
assert!(code.module(6, coordinate), "top-left finder right edge at (6, {coordinate})");
574+
assert!(
575+
code.module(coordinate, 6),
576+
"top-left finder bottom edge at ({coordinate}, 6)"
577+
);
578+
assert!(
579+
code.module(6, coordinate),
580+
"top-left finder right edge at (6, {coordinate})"
581+
);
576582
}
577-
assert!(!code.module(7, 6), "separator next to top-left finder must stay light");
578-
assert!(!code.module(6, 7), "separator below top-left finder must stay light");
583+
assert!(
584+
!code.module(7, 6),
585+
"separator next to top-left finder must stay light"
586+
);
587+
assert!(
588+
!code.module(6, 7),
589+
"separator below top-left finder must stay light"
590+
);
579591

580592
let size = code.size();
581593
for coordinate in size - 7..size {

crates/fastqr_core/src/gf256.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,25 @@ pub fn mul(left: u8, right: u8) -> u8 {
2828
}
2929
TABLES.0[usize::from(TABLES.1[left as usize]) + usize::from(TABLES.1[right as usize])]
3030
}
31+
32+
#[inline]
33+
pub fn exp(power: usize) -> u8 {
34+
TABLES.0[power % 255]
35+
}
36+
37+
#[inline]
38+
pub fn div(left: u8, right: u8) -> u8 {
39+
debug_assert_ne!(right, 0);
40+
if left == 0 {
41+
return 0;
42+
}
43+
let left_log = usize::from(TABLES.1[usize::from(left)]);
44+
let right_log = usize::from(TABLES.1[usize::from(right)]);
45+
TABLES.0[(left_log + 255 - right_log) % 255]
46+
}
47+
48+
#[inline]
49+
pub fn inverse(value: u8) -> u8 {
50+
debug_assert_ne!(value, 0);
51+
TABLES.0[255 - usize::from(TABLES.1[usize::from(value)])]
52+
}

0 commit comments

Comments
 (0)