From 0164a6e62a754585877d0a177c0a7fb6ba8ca593 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 May 2026 03:43:21 +0000 Subject: [PATCH 1/4] use anyhow best practices --- rxing-cli/src/main.rs | 2 +- rxing-reader/src/common/bit_array.rs | 49 ++-- rxing-reader/src/common/bit_matrix.rs | 122 ++++----- rxing-reader/src/common/bit_source.rs | 13 +- rxing-reader/src/common/character_set.rs | 54 ++-- .../src/common/default_grid_sampler.rs | 25 +- .../detect/fast_edge_to_edge_counter.rs | 20 +- rxing-reader/src/common/detect/matrix.rs | 19 +- rxing-reader/src/common/detect/pattern.rs | 19 +- .../src/common/detect/regression_line.rs | 13 +- rxing-reader/src/common/detect/util.rs | 8 +- rxing-reader/src/common/eci.rs | 15 +- .../src/common/global_histogram_binarizer.rs | 24 +- rxing-reader/src/common/grid_sampler.rs | 32 +-- rxing-reader/src/common/hybrid_binarizer.rs | 15 +- .../src/common/perspective_transform.rs | 33 ++- rxing-reader/src/common/quad.rs | 15 +- rxing-reader/src/decode.rs | 30 ++- rxing-reader/src/error.rs | 32 +++ rxing-reader/src/luma_source.rs | 46 ++-- .../src/qrcode/error_correction_level.rs | 17 +- rxing-reader/src/qrcode/format_information.rs | 21 +- rxing-reader/src/qrcode/mode.rs | 19 +- .../src/qrcode/reader/bitmatrix_parser.rs | 26 +- rxing-reader/src/qrcode/reader/decoder.rs | 235 ++++++++++-------- rxing-reader/src/qrcode/reader/detector.rs | 105 ++++---- rxing-reader/src/qrcode/reed_solomon.rs | 38 +-- rxing-reader/src/qrcode/version.rs | 74 +++--- rxing-reader/tests/qr_decode.rs | 6 +- rxing-wasm/src/lib.rs | 4 +- 30 files changed, 539 insertions(+), 592 deletions(-) diff --git a/rxing-cli/src/main.rs b/rxing-cli/src/main.rs index ee2611a..f847b9b 100644 --- a/rxing-cli/src/main.rs +++ b/rxing-cli/src/main.rs @@ -101,7 +101,7 @@ fn decode_image_bytes(bytes: &[u8]) -> Result<(Vec, usize, usize)> { } fn decode_symbols(rgba: &[u8], w: usize, h: usize, max: usize) -> Result> { - let luma = rgba_to_luma(rgba, w, h).map_err(anyhow::Error::msg)?; + let luma = rgba_to_luma(rgba, w, h).context("converting RGBA pixels to luma")?; let primary = decode_qr_codes_luma(&luma, w, h, true, true, true, max)?; if !primary.is_empty() { return Ok(primary); diff --git a/rxing-reader/src/common/bit_array.rs b/rxing-reader/src/common/bit_array.rs index da31b6a..893bd9f 100644 --- a/rxing-reader/src/common/bit_array.rs +++ b/rxing-reader/src/common/bit_array.rs @@ -12,8 +12,9 @@ use std::fmt; use bitvec::prelude::*; +use anyhow::{Result, ensure}; + use crate::Error; -use anyhow::Result; type BaseType = super::BitFieldBaseType; const BASE_BITS: usize = super::BIT_FIELD_BASE_BITS; @@ -126,14 +127,12 @@ impl BitArray { pub fn set_range(&mut self, start: usize, end: usize) -> Result<()> { let len = self.bits.len(); - if end < start || end > len { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + end >= start && end <= len, + Error::invalid_argument(format!( "set_range: start={start}, end={end} out of range for bit array of length {len}" - ).into(), - } - .into()); - } + )) + ); if end == start { return Ok(()); } @@ -147,14 +146,12 @@ impl BitArray { pub fn is_range(&self, start: usize, end: usize, value: bool) -> Result { let len = self.bits.len(); - if end < start || end > len { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + end >= start && end <= len, + Error::invalid_argument(format!( "is_range: start={start}, end={end} out of range for bit array of length {len}" - ).into(), - } - .into()); - } + )) + ); if end == start { return Ok(true); } @@ -169,12 +166,10 @@ impl BitArray { /// Appends the least-significant `num_bits` of `value`, from most-significant to /// least-significant. For example, appending 6 bits from 0x1E appends 0,1,1,1,1,0. pub fn append_bits(&mut self, value: BaseType, num_bits: usize) -> Result<()> { - if num_bits > BASE_BITS { - return Err(Error::InvalidArgument { - message: format!("num bits must be between 0 and {}", BaseType::BITS).into(), - } - .into()); - } + ensure!( + num_bits <= BASE_BITS, + Error::invalid_argument(format!("num bits must be between 0 and {}", BaseType::BITS)) + ); for i in (0..num_bits).rev() { self.bits.push((value >> i) & 1 != 0); } @@ -190,16 +185,14 @@ impl BitArray { } pub fn xor(&mut self, other: &BitArray) -> Result<()> { - if self.bits.len() != other.bits.len() { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + self.bits.len() == other.bits.len(), + Error::invalid_argument(format!( "xor: bit array sizes differ (self={}, other={})", self.bits.len(), other.bits.len(), - ).into(), - } - .into()); - } + )) + ); self.bits ^= &other.bits; Ok(()) } diff --git a/rxing-reader/src/common/bit_matrix.rs b/rxing-reader/src/common/bit_matrix.rs index feaeae4..25aba18 100644 --- a/rxing-reader/src/common/bit_matrix.rs +++ b/rxing-reader/src/common/bit_matrix.rs @@ -16,8 +16,9 @@ use std::fmt; +use anyhow::{Context, Result, bail, ensure}; + use crate::{Error, Point, point}; -use anyhow::Result; use super::BitArray; @@ -56,14 +57,12 @@ impl BitMatrix { /// - `width`: bit matrix width /// - `height`: bit matrix height pub fn new(width: usize, height: usize) -> Result { - if width < 1 || height < 1 { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + width >= 1 && height >= 1, + Error::invalid_argument(format!( "BitMatrix::new: both dimensions must be greater than 0 (got width={width}, height={height})" - ).into(), - } - .into()); - } + )) + ); Ok(Self { width, height, @@ -146,12 +145,9 @@ impl BitMatrix { row_length = bits_pos - row_start_pos; } else if bits_pos - row_start_pos != row_length { let this_row_length = bits_pos - row_start_pos; - return Err(Error::InvalidArgument { - message: format!( + bail!(Error::invalid_argument(format!( "parse_strings: row lengths do not match (row {n_rows} has {this_row_length} cells, expected {row_length})" - ).into(), - } - .into()); + ))); } row_start_pos = bits_pos; n_rows += 1; @@ -166,13 +162,10 @@ impl BitMatrix { bits[bits_pos] = false; bits_pos += 1; } else { - return Err(Error::InvalidArgument { - message: format!( + bail!(Error::invalid_argument(format!( "illegal character encountered: {}", &string_representation[pos..] - ).into(), - } - .into()); + ))); } } @@ -182,17 +175,14 @@ impl BitMatrix { row_length = bits_pos - row_start_pos; } else if bits_pos - row_start_pos != row_length { let this_row_length = bits_pos - row_start_pos; - return Err(Error::InvalidArgument { - message: format!( + bail!(Error::invalid_argument(format!( "parse_strings: final row lengths do not match (row {n_rows} has {this_row_length} cells, expected {row_length})" - ).into(), - } - .into()); + ))); } n_rows += 1; } - let mut matrix = BitMatrix::new(row_length, n_rows)?; + let mut matrix = BitMatrix::new(row_length, n_rows).context("building parsed bit matrix")?; for (i, bit) in bits.iter().enumerate().take(bits_pos) { if *bit { matrix.set(i % row_length, i / row_length); @@ -330,17 +320,14 @@ impl BitMatrix { /// /// - `mask`: XOR mask pub fn xor(&mut self, mask: &BitMatrix) -> Result<()> { - if self.width != mask.width || self.height != mask.height || self.row_size != mask.row_size - { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + self.width == mask.width && self.height == mask.height && self.row_size == mask.row_size, + Error::invalid_argument(format!( "BitMatrix::xor: input matrix dimensions do not match (self={}x{} row_size={}, mask={}x{} row_size={})", self.width, self.height, self.row_size, mask.width, mask.height, mask.row_size, - ).into(), - } - .into()); - } + )) + ); for y in 0..self.height { let offset = y * self.row_size; let row_array = mask.row(y); @@ -365,33 +352,29 @@ impl BitMatrix { /// - `width`: The width of the region /// - `height`: The height of the region pub fn set_region(&mut self, left: usize, top: usize, width: usize, height: usize) -> Result<()> { - if height < 1 || width < 1 { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + height >= 1 && width >= 1, + Error::invalid_argument(format!( "set_region: width and height must be at least 1 (got width={width}, height={height})" - ).into(), - } - .into()); - } - let right = left.checked_add(width).ok_or_else(|| Error::InvalidArgument { - message: format!( + )) + ); + let right = left.checked_add(width).with_context(|| { + Error::invalid_argument(format!( "set_region: left + width overflowed (left={left}, width={width})" - ).into(), + )) })?; - let bottom = top.checked_add(height).ok_or_else(|| Error::InvalidArgument { - message: format!( + let bottom = top.checked_add(height).with_context(|| { + Error::invalid_argument(format!( "set_region: top + height overflowed (top={top}, height={height})" - ).into(), + )) })?; - if bottom > self.height || right > self.width { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + bottom <= self.height && right <= self.width, + Error::invalid_argument(format!( "set_region: region (left={left}, top={top}, width={width}, height={height} -> right={right}, bottom={bottom}) does not fit inside matrix {}x{}", self.width, self.height, - ).into(), - } - .into()); - } + )) + ); for y in top..bottom { let offset = y * self.row_size; for x in left..right { @@ -457,12 +440,9 @@ impl BitMatrix { self.rotate180(); Ok(()) } - other => Err(Error::InvalidArgument { - message: format!( + other => bail!(Error::invalid_argument(format!( "rotate: degrees must be 0, 90, 180, or 270 (got {degrees}, normalized to {other})" - ).into(), - } - .into()), + ))), } } @@ -621,26 +601,20 @@ impl BitMatrix { } pub fn crop(&self, top: usize, left: usize, height: usize, width: usize) -> Result { - if width == 0 || height == 0 { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + width != 0 && height != 0, + Error::invalid_argument(format!( "crop: width and height must be greater than 0 (got width={width}, height={height})" - ).into(), - } - .into()); - } - if left.saturating_add(width) > self.width - || top.saturating_add(height) > self.height - { - return Err(Error::InvalidArgument { - message: format!( + )) + ); + ensure!( + left.saturating_add(width) <= self.width && top.saturating_add(height) <= self.height, + Error::invalid_argument(format!( "crop: region (left={left}, top={top}, width={width}, height={height}) does not fit inside matrix {}x{}", self.width, self.height, - ).into(), - } - .into()); - } - let mut new_bm = BitMatrix::new(width, height)?; + )) + ); + let mut new_bm = BitMatrix::new(width, height).context("building cropped bit matrix")?; for y in top..top + height { for x in left..left + width { if self.get(x, y) { diff --git a/rxing-reader/src/common/bit_source.rs b/rxing-reader/src/common/bit_source.rs index fc0c012..56c45ce 100644 --- a/rxing-reader/src/common/bit_source.rs +++ b/rxing-reader/src/common/bit_source.rs @@ -17,8 +17,9 @@ use std::cmp; use std::io::{ErrorKind, Read}; +use anyhow::{Result, bail}; + use crate::Error; -use anyhow::Result; /// This provides an easy abstraction to read bits at a time from a sequence of bytes, where the /// number of bits read is not often a multiple of 8. @@ -55,10 +56,7 @@ impl<'a> BitSource<'a> { if num_bits > available_bits { message.push_str(&format!("; available bits: {available_bits}")); } - return Err(Error::InvalidArgument { - message: message.into(), - } - .into()); + bail!(Error::invalid_argument(message)); } let mut result: u32 = 0; @@ -109,10 +107,7 @@ impl<'a> BitSource<'a> { if num_bits > available_bits { message.push_str(&format!("; available bits: {available_bits}")); } - return Err(Error::InvalidArgument { - message: message.into(), - } - .into()); + bail!(Error::invalid_argument(message)); } let mut bit_offset = self.bit_offset; diff --git a/rxing-reader/src/common/character_set.rs b/rxing-reader/src/common/character_set.rs index 8631c10..b8ad433 100644 --- a/rxing-reader/src/common/character_set.rs +++ b/rxing-reader/src/common/character_set.rs @@ -14,8 +14,9 @@ * limitations under the License. */ +use anyhow::{Context, Result, bail, ensure}; + use crate::Error; -use anyhow::Result; /// Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 /// of ISO 18004. @@ -99,42 +100,30 @@ impl CharacterSet { Ok(String::borrow_from_cp437(input, &CP437_CONTROL)) } CharacterSet::UTF32BE => { - if !input.len().is_multiple_of(4) { - return Err(Error::InvalidFormat { - message: "Invalid UTF-32BE: trailing bytes".into(), - } - .into()); - } + ensure!( + input.len().is_multiple_of(4), + Error::invalid_format("Invalid UTF-32BE: trailing bytes") + ); input .chunks_exact(4) .map(|c| { let val = u32::from_be_bytes([c[0], c[1], c[2], c[3]]); - char::from_u32(val).ok_or_else(|| { - Error::InvalidFormat { - message: "Invalid UTF-32BE".into(), - } - .into() - }) + char::from_u32(val) + .with_context(|| Error::invalid_format("Invalid UTF-32BE")) }) .collect() } CharacterSet::UTF32LE => { - if !input.len().is_multiple_of(4) { - return Err(Error::InvalidFormat { - message: "Invalid UTF-32LE: trailing bytes".into(), - } - .into()); - } + ensure!( + input.len().is_multiple_of(4), + Error::invalid_format("Invalid UTF-32LE: trailing bytes") + ); input .chunks_exact(4) .map(|c| { let val = u32::from_le_bytes([c[0], c[1], c[2], c[3]]); - char::from_u32(val).ok_or_else(|| { - Error::InvalidFormat { - message: "Invalid UTF-32LE".into(), - } - .into() - }) + char::from_u32(val) + .with_context(|| Error::invalid_format("Invalid UTF-32LE")) }) .collect() } @@ -145,10 +134,7 @@ impl CharacterSet { let mut s = String::with_capacity(input.len()); for &b in input { if b > 0x7F { - return Err(Error::InvalidFormat { - message: "Invalid ASCII".into(), - } - .into()); + bail!(Error::invalid_format("Invalid ASCII")); } s.push(char::from(b)); } @@ -158,17 +144,11 @@ impl CharacterSet { if let Some(enc) = self.encoding() { let (res, _, had_errors) = enc.decode(input); if had_errors { - return Err(Error::InvalidFormat { - message: "Could not decode character".into(), - } - .into()); + bail!(Error::invalid_format("Could not decode character")); } Ok(res.into_owned()) } else { - Err(Error::InvalidFormat { - message: "Unsupported encoding".into(), - } - .into()) + bail!(Error::invalid_format("Unsupported encoding")); } } } diff --git a/rxing-reader/src/common/default_grid_sampler.rs b/rxing-reader/src/common/default_grid_sampler.rs index 38931f9..06bcf39 100644 --- a/rxing-reader/src/common/default_grid_sampler.rs +++ b/rxing-reader/src/common/default_grid_sampler.rs @@ -14,8 +14,9 @@ * limitations under the License. */ +use anyhow::{Context, Result, bail}; + use crate::{Error, Point, point}; -use anyhow::Result; use super::{BitMatrix, GridSampler, SamplerControl}; @@ -31,10 +32,7 @@ impl GridSampler for DefaultGridSampler { controls: &[SamplerControl], ) -> Result { if dimension_x == 0 || dimension_y == 0 { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } for SamplerControl { p0, p1, transform } in controls { @@ -50,22 +48,18 @@ impl GridSampler for DefaultGridSampler { || !is_inside(p1.x - 1.0, p1.y - 1.0) || !is_inside(p0.x, p1.y - 1.0) { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } } - let mut bits = BitMatrix::new(dimension_x, dimension_y)?; + let mut bits = BitMatrix::new(dimension_x, dimension_y) + .context("building sampled QR grid matrix")?; for SamplerControl { p0, p1, transform } in controls { for y in (p0.y as usize)..(p1.y as usize) { for x in (p0.x as usize)..(p1.x as usize) { let p = transform .transform_point(point(x as f32, y as f32).centered()) - .ok_or(Error::NotFound { - message: "QR pattern was not detected".into(), - })?; + .with_context(|| Error::not_found("QR pattern was not detected"))?; // Due to a "numerical instability" in the PerspectiveTransform generation/application it has been observed // that even though all boundary grid points get projected inside the image, it can still happen that an @@ -73,10 +67,7 @@ impl GridSampler for DefaultGridSampler { // The following check takes 100% care of the issue and turned out to be less of a performance impact than feared. // TODO: Check some mathematical/numercial property of mod2_pix to determine if it is a perspective transforation. if !image.is_in(p) { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } if image.at_point(p) { diff --git a/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs b/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs index bf0db15..0b3f297 100644 --- a/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs +++ b/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result, ensure}; use crate::{Error, common::BitMatrix}; @@ -22,20 +22,20 @@ impl FastEdgeToEdgeCounter<'_> { let p = cur.p().y as isize * width + cur.p().x as isize; let image_len = width .checked_mul(height) - .ok_or_else(|| Error::InvalidArgument { - message: format!("FastEdgeToEdgeCounter: image size overflow ({width} x {height})").into(), + .with_context(|| { + Error::invalid_argument(format!( + "FastEdgeToEdgeCounter: image size overflow ({width} x {height})" + )) })?; - if !(0..image_len).contains(&p) { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + (0..image_len).contains(&p), + Error::invalid_argument(format!( "FastEdgeToEdgeCounter: cursor index {p} is outside image of size {width}x{height} (cursor=({}, {}))", cur.p().x, cur.p().y, ) - .into(), - } - .into()); - } + ) + ); let p = p as usize; let max_steps_x: i32 = if cur.d().x != 0.0 { diff --git a/rxing-reader/src/common/detect/matrix.rs b/rxing-reader/src/common/detect/matrix.rs index 7754247..ad1090e 100644 --- a/rxing-reader/src/common/detect/matrix.rs +++ b/rxing-reader/src/common/detect/matrix.rs @@ -1,5 +1,6 @@ +use anyhow::{Context, Result, ensure}; + use crate::Error; -use anyhow::Result; #[derive(Default, Clone, PartialEq, Eq)] pub struct Matrix { @@ -12,9 +13,7 @@ impl Matrix { pub fn new(width: usize, height: usize) -> Result> { let size = width .checked_mul(height) - .ok_or_else(|| Error::InvalidArgument { - message: format!("Matrix::new: width * height overflow ({width} x {height})").into(), - })?; + .with_context(|| Error::invalid_argument(format!("Matrix::new: width * height overflow ({width} x {height})")))?; Ok(Self { width, height, @@ -34,15 +33,13 @@ impl Matrix { } pub fn set(&mut self, x: usize, y: usize, value: T) -> Result { - if x >= self.width || y >= self.height { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + x < self.width && y < self.height, + Error::invalid_argument(format!( "set: coordinates ({x}, {y}) outside {}x{} matrix", self.width, self.height - ).into(), - } - .into()); - } + )) + ); let offset = Self::offset(x, y, self.width); self.data[offset] = Some(value); Ok(value) diff --git a/rxing-reader/src/common/detect/pattern.rs b/rxing-reader/src/common/detect/pattern.rs index 5cc9ba0..afbc56b 100644 --- a/rxing-reader/src/common/detect/pattern.rs +++ b/rxing-reader/src/common/detect/pattern.rs @@ -3,7 +3,7 @@ */ // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; +use anyhow::{Context, Result}; use crate::{Error, common::BitMatrix}; @@ -541,19 +541,17 @@ pub fn find_left_guard_by) const PREV_IDX: isize = -1; if view.size() < min_size { - return Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()); + anyhow::bail!(Error::invalid_state("required internal state is missing")); } let mut window = view.sub_view(0, LEN); if window.is_at_first_bar() && is_guard(&window, Some(f32::MAX)) { return Ok(window); } - let end = Into::::into(view.end().ok_or_else(|| Error::InvalidState { - message: "pattern view has no end index".into(), - })?) - min_size; + let end = Into::::into( + view.end() + .with_context(|| Error::invalid_state("pattern view has no end index"))?, + ) - min_size; while (window.start + window.current) < end { let prev = window .try_index(PREV_IDX) @@ -565,10 +563,7 @@ pub fn find_left_guard_by) window.skip_pair(); } - Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()) + anyhow::bail!(Error::invalid_state("required internal state is missing")); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/rxing-reader/src/common/detect/regression_line.rs b/rxing-reader/src/common/detect/regression_line.rs index c592004..ffa6c09 100644 --- a/rxing-reader/src/common/detect/regression_line.rs +++ b/rxing-reader/src/common/detect/regression_line.rs @@ -1,5 +1,6 @@ +use anyhow::{Result, ensure}; + use crate::{Error, Point}; -use anyhow::Result; use super::RegressionLineTrait; @@ -49,12 +50,10 @@ impl RegressionLineTrait for RegressionLine { } fn add(&mut self, p: Point) -> Result<()> { - if self.direction_inward == Point::default() { - return Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()); - } + ensure!( + self.direction_inward != Point::default(), + Error::invalid_state("required internal state is missing") + ); self.points.push(p); if self.points.len() == 1 { self.c = Point::dot(self.normal(), p); diff --git a/rxing-reader/src/common/detect/util.rs b/rxing-reader/src/common/detect/util.rs index 9660177..6c7a501 100644 --- a/rxing-reader/src/common/detect/util.rs +++ b/rxing-reader/src/common/detect/util.rs @@ -1,5 +1,6 @@ +use anyhow::{Result, bail}; + use crate::Error; -use anyhow::Result; use super::Direction; @@ -34,10 +35,7 @@ pub fn to_fixed_len_string>(val: T, len: usize) -> Result val /= 10; } if val != 0 { - return Err(Error::InvalidFormat { - message: "Invalid value".into(), - } - .into()); + bail!(Error::invalid_format("Invalid value")); } Ok(result.iter().collect()) diff --git a/rxing-reader/src/common/eci.rs b/rxing-reader/src/common/eci.rs index 8dc4815..8c12f86 100644 --- a/rxing-reader/src/common/eci.rs +++ b/rxing-reader/src/common/eci.rs @@ -1,5 +1,7 @@ use std::fmt::Display; +use anyhow::ensure; + use crate::Error; use super::CharacterSet; @@ -89,15 +91,12 @@ impl TryFrom for Eci { type Error = anyhow::Error; fn try_from(value: u32) -> Result { - if value > MAX_ECI_ASSIGNMENT_VALUE { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + value <= MAX_ECI_ASSIGNMENT_VALUE, + Error::invalid_argument(format!( "ECI value {value} exceeds maximum assignment value {MAX_ECI_ASSIGNMENT_VALUE}" - ) - .into(), - } - .into()); - } + )) + ); Ok(match value { 0 | 2 => Eci::Cp437, diff --git a/rxing-reader/src/common/global_histogram_binarizer.rs b/rxing-reader/src/common/global_histogram_binarizer.rs index 62084e3..b39fc4b 100644 --- a/rxing-reader/src/common/global_histogram_binarizer.rs +++ b/rxing-reader/src/common/global_histogram_binarizer.rs @@ -21,8 +21,9 @@ use once_cell::sync::OnceCell; +use anyhow::{Context, Result, bail}; + use crate::{Binarizer, Error, LuminanceSource}; -use anyhow::Result; use super::BitMatrix; @@ -60,12 +61,9 @@ impl Binarizer for GlobalHistogramBinarizer { fn black_matrix_mut(&mut self) -> Result<&mut BitMatrix> { self.black_matrix .get_or_try_init(|| Self::build_black_matrix(&self.source))?; - self.black_matrix.get_mut().ok_or_else(|| { - Error::InvalidState { - message: "black matrix cache was not initialized".into(), - } - .into() - }) + self.black_matrix + .get_mut() + .with_context(|| Error::invalid_state("black matrix cache was not initialized")) } } @@ -80,15 +78,16 @@ impl GlobalHistogramBinarizer { pub(super) fn build_black_matrix(source: &LS) -> Result { let width = source.width(); let height = source.height(); - let mut matrix = BitMatrix::new(width, height)?; + let mut matrix = BitMatrix::new(width, height) + .context("building global histogram bit matrix")?; // Quickly calculates the histogram by sampling four rows from the image. This proved to be // more robust on the blackbox tests than sampling a diagonal as we used to do. let mut local_buckets = [0; LUMINANCE_BUCKETS]; for y in 1..5 { let row = height * y / 5; - let local_luminances = source.row(row).ok_or_else(|| Error::InvalidState { - message: format!("luminance source returned no data for sampled row {row}").into(), + let local_luminances = source.row(row).with_context(|| { + Error::invalid_state(format!("luminance source returned no data for sampled row {row}")) })?; let right = (width * 4) / 5; for pixel in &local_luminances[(width / 5)..right] { @@ -152,10 +151,7 @@ impl GlobalHistogramBinarizer { // If there is too little contrast in the image to pick a meaningful black point, throw rather // than waste time trying to decode the image, and risk false positives. if second_peak - first_peak <= BUCKET_COUNT / 16 { - return Err(Error::NotFound { - message: "second_peak - first_peak <= numBuckets / 16 ".into(), - } - .into()); + bail!(Error::not_found("second_peak - first_peak <= numBuckets / 16 ")); } // Find a valley between them that is low and closer to the white peak. diff --git a/rxing-reader/src/common/grid_sampler.rs b/rxing-reader/src/common/grid_sampler.rs index df05be5..c6b9c20 100644 --- a/rxing-reader/src/common/grid_sampler.rs +++ b/rxing-reader/src/common/grid_sampler.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use anyhow::Result; +use anyhow::{Context, Result, bail}; use crate::{Error, Point}; @@ -37,12 +37,10 @@ pub trait GridSampler { controls: &[SamplerControl], ) -> Result { if dimension_x == 0 || dimension_y == 0 { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } - let mut bits = BitMatrix::new(dimension_x, dimension_y)?; + let mut bits = BitMatrix::new(dimension_x, dimension_y) + .context("building sampled QR grid matrix")?; let mut points = vec![Point::default(); dimension_x]; for y in 0..dimension_y { let i_value = y as f32 + 0.5; @@ -61,9 +59,7 @@ pub trait GridSampler { for (x, point) in points.iter().enumerate() { if image .try_at_point(*point) - .ok_or(Error::NotFound { - message: "grid point transformed outside image bounds".into(), - })? + .with_context(|| Error::not_found("grid point transformed outside image bounds"))? { // Black(-ish) pixel bits.set(x, y); @@ -89,11 +85,11 @@ pub trait GridSampler { /// /// Returns a not-found error if an endpoint is lies outside the image boundaries fn check_and_nudge_points(&self, image: &BitMatrix, points: &mut [Point]) -> Result<()> { - let width = i32::try_from(image.width()).map_err(|_| Error::InvalidArgument { - message: format!("image width {} does not fit in i32", image.width()).into(), + let width = i32::try_from(image.width()).with_context(|| { + Error::invalid_argument(format!("image width {} does not fit in i32", image.width())) })?; - let height = i32::try_from(image.height()).map_err(|_| Error::InvalidArgument { - message: format!("image height {} does not fit in i32", image.height()).into(), + let height = i32::try_from(image.height()).with_context(|| { + Error::invalid_argument(format!("image height {} does not fit in i32", image.height())) })?; // Check and nudge points from start until we see some that are OK: let mut nudged; @@ -104,10 +100,7 @@ pub trait GridSampler { for point in points.iter_mut().take(max_offset) { let (x, y) = (point.x as i32, point.y as i32); if x < NUDGE_OUTSIDE_MIN || x > width || y < NUDGE_OUTSIDE_MIN || y > height { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } nudged = false; if x == NUDGE_OUTSIDE_MIN { @@ -132,10 +125,7 @@ pub trait GridSampler { for point in points.iter_mut().rev().take(max_offset).rev() { let (x, y) = (point.x as i32, point.y as i32); if x < NUDGE_OUTSIDE_MIN || x > width || y < NUDGE_OUTSIDE_MIN || y > height { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } nudged = false; if x == NUDGE_OUTSIDE_MIN { diff --git a/rxing-reader/src/common/hybrid_binarizer.rs b/rxing-reader/src/common/hybrid_binarizer.rs index c89585f..cc57aa9 100644 --- a/rxing-reader/src/common/hybrid_binarizer.rs +++ b/rxing-reader/src/common/hybrid_binarizer.rs @@ -16,8 +16,9 @@ use once_cell::sync::OnceCell; +use anyhow::{Context, Result}; + use crate::{Binarizer, Error, LuminanceSource}; -use anyhow::Result; use super::{BitMatrix, GlobalHistogramBinarizer}; @@ -60,12 +61,9 @@ impl Binarizer for HybridBinarizer { fn black_matrix_mut(&mut self) -> Result<&mut BitMatrix> { self.black_matrix .get_or_try_init(|| Self::calculate_black_matrix(&self.ghb))?; - self.black_matrix.get_mut().ok_or_else(|| { - Error::InvalidState { - message: "black matrix cache was not initialized".into(), - } - .into() - }) + self.black_matrix + .get_mut() + .with_context(|| Error::invalid_state("black matrix cache was not initialized")) } } @@ -99,7 +97,8 @@ impl HybridBinarizer { let black_points = Self::calculate_black_points(&luminances, sub_width, sub_height, width, height); - let mut new_matrix = BitMatrix::new(width, height)?; + let mut new_matrix = BitMatrix::new(width, height) + .context("building hybrid binarizer bit matrix")?; Self::calculate_threshold_for_block( &luminances, sub_width, diff --git a/rxing-reader/src/common/perspective_transform.rs b/rxing-reader/src/common/perspective_transform.rs index 56050e9..fad8f93 100644 --- a/rxing-reader/src/common/perspective_transform.rs +++ b/rxing-reader/src/common/perspective_transform.rs @@ -18,7 +18,7 @@ use std::ops::Mul; -use anyhow::Result; +use anyhow::{Context, Result, bail, ensure}; use crate::{Error, Point, point}; @@ -72,14 +72,14 @@ impl PerspectiveTransform { #[allow(clippy::too_many_arguments)] pub fn quadrilateral_to_quadrilateral(dst: Quadrilateral, src: Quadrilateral) -> Result { - if !src.is_convex() || !dst.is_convex() { - return Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()); - } - let q_to_s = PerspectiveTransform::quadrilateral_to_square(dst)?; - let s_to_q = PerspectiveTransform::square_to_quadrilateral(src)?; + ensure!( + src.is_convex() && dst.is_convex(), + Error::invalid_state("required internal state is missing") + ); + let q_to_s = PerspectiveTransform::quadrilateral_to_square(dst) + .context("building destination-to-square perspective transform")?; + let s_to_q = PerspectiveTransform::square_to_quadrilateral(src) + .context("building square-to-source perspective transform")?; Ok(s_to_q * q_to_s) } @@ -98,9 +98,9 @@ impl PerspectiveTransform { pub fn transform_points_single(&self, points: &mut [Point]) -> Result<()> { for point in points.iter_mut() { - *point = self.transform_point(*point).ok_or(Error::NotFound { - message: "QR pattern was not detected".into(), - })?; + *point = self + .transform_point(*point) + .with_context(|| Error::not_found("QR pattern was not detected"))?; } Ok(()) } @@ -129,10 +129,7 @@ impl PerspectiveTransform { let denominator = d1.cross(d2); if denominator.abs() < DENOMINATOR_EPSILON { - return Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()); + bail!(Error::invalid_state("required internal state is missing")); } let a13 = d3.cross(d2) / denominator; let a23 = d1.cross(d3) / denominator; @@ -153,7 +150,9 @@ impl PerspectiveTransform { #[allow(clippy::too_many_arguments)] pub fn quadrilateral_to_square(quad: Quadrilateral) -> Result { // Here, the adjoint serves as the inverse - Ok(PerspectiveTransform::square_to_quadrilateral(quad)?.build_adjoint()) + Ok(PerspectiveTransform::square_to_quadrilateral(quad) + .context("building quadrilateral-to-square perspective transform")? + .build_adjoint()) } const fn build_adjoint(&self) -> Self { diff --git a/rxing-reader/src/common/quad.rs b/rxing-reader/src/common/quad.rs index 9d2e811..46dbbad 100644 --- a/rxing-reader/src/common/quad.rs +++ b/rxing-reader/src/common/quad.rs @@ -253,16 +253,13 @@ impl TryFrom<&[Point]> for Quadrilateral { type Error = anyhow::Error; fn try_from(value: &[Point]) -> Result { - if value.len() == 4 { - Ok(Self([value[0], value[1], value[2], value[3]])) - } else { - Err(Error::InvalidArgument { - message: format!( + anyhow::ensure!( + value.len() == 4, + Error::invalid_argument(format!( "quadrilateral requires exactly 4 points, got {}", value.len() - ).into(), - } - .into()) - } + )) + ); + Ok(Self([value[0], value[1], value[2], value[3]])) } } diff --git a/rxing-reader/src/decode.rs b/rxing-reader/src/decode.rs index a73903c..2b30989 100644 --- a/rxing-reader/src/decode.rs +++ b/rxing-reader/src/decode.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; -use anyhow::Result as RxingResult; +use anyhow::{Context, Result, ensure}; use crate::{ - Binarizer, BinaryBitmap, DecodeHints, Luma8LuminanceSource, + Binarizer, BinaryBitmap, DecodeHints, Error, Luma8LuminanceSource, common::{ Eci, GlobalHistogramBinarizer, HybridBinarizer, SymbologyIdentifier, detect::{DecoderResult, StructuredAppendInfo}, @@ -66,18 +66,19 @@ impl QrSymbol { } } -pub fn rgba_to_luma(rgba: &[u8], width: usize, height: usize) -> Result, String> { +pub fn rgba_to_luma(rgba: &[u8], width: usize, height: usize) -> Result> { let expected = width .checked_mul(height) .and_then(|n| n.checked_mul(4)) - .ok_or_else(|| "Image dimensions overflow".to_string())?; - if rgba.len() != expected { - return Err(format!( + .with_context(|| Error::invalid_argument("rgba_to_luma: image dimensions overflow"))?; + ensure!( + rgba.len() == expected, + Error::invalid_argument(format!( "rgba length {} != width*height*4 ({})", rgba.len(), expected - )); - } + )) + ); Ok(rgba .chunks_exact(4) .map(|p| { @@ -149,11 +150,12 @@ pub fn decode_qr_codes_luma( try_invert: bool, use_hybrid_binarizer: bool, max_number_of_symbols: usize, -) -> RxingResult> { +) -> Result> { let hints = DecodeHints { try_harder }; if !try_harder { - let source = Luma8LuminanceSource::new(luma, width, height)?; + let source = Luma8LuminanceSource::new(luma, width, height) + .context("building luminance source for QR decode")?; return Ok(decode_one_layer( source, &hints, @@ -169,7 +171,10 @@ pub fn decode_qr_codes_luma( let mut cur_h = height; loop { for &close in &[false, true] { - let source = Luma8LuminanceSource::new(cur_luma.as_ref(), cur_w, cur_h)?; + let source = Luma8LuminanceSource::new(cur_luma.as_ref(), cur_w, cur_h) + .with_context(|| { + format!("building luminance source for QR layer {cur_w}x{cur_h}") + })?; let results = decode_one_layer( source, &hints, @@ -188,7 +193,8 @@ pub fn decode_qr_codes_luma( return Ok(Vec::new()); } let (next_luma, next_w, next_h) = - downscale_luma_buffer(cur_luma.as_ref(), cur_w, cur_h, PYRAMID_DOWNSCALE_FACTOR)?; + downscale_luma_buffer(cur_luma.as_ref(), cur_w, cur_h, PYRAMID_DOWNSCALE_FACTOR) + .with_context(|| format!("downscaling QR layer {cur_w}x{cur_h}"))?; if let Cow::Owned(next_luma) = next_luma { cur_luma = Cow::Owned(next_luma); } diff --git a/rxing-reader/src/error.rs b/rxing-reader/src/error.rs index 30083fc..999f7e3 100644 --- a/rxing-reader/src/error.rs +++ b/rxing-reader/src/error.rs @@ -17,3 +17,35 @@ pub enum Error { #[error("checksum failed: {message}")] Checksum { message: ErrorMessage }, } + +impl Error { + pub(crate) fn invalid_argument(message: impl Into) -> Self { + Self::InvalidArgument { + message: message.into(), + } + } + + pub(crate) fn invalid_state(message: impl Into) -> Self { + Self::InvalidState { + message: message.into(), + } + } + + pub(crate) fn not_found(message: impl Into) -> Self { + Self::NotFound { + message: message.into(), + } + } + + pub(crate) fn invalid_format(message: impl Into) -> Self { + Self::InvalidFormat { + message: message.into(), + } + } + + pub(crate) fn checksum(message: impl Into) -> Self { + Self::Checksum { + message: message.into(), + } + } +} diff --git a/rxing-reader/src/luma_source.rs b/rxing-reader/src/luma_source.rs index 2b8f280..b242a2d 100644 --- a/rxing-reader/src/luma_source.rs +++ b/rxing-reader/src/luma_source.rs @@ -1,7 +1,8 @@ use std::borrow::Cow; +use anyhow::{Context, Result, bail, ensure}; + use crate::{Error, LuminanceSource}; -use anyhow::Result; /// A simple luma8 source for bytes. #[derive(Debug)] @@ -37,20 +38,18 @@ impl<'a> Luma8LuminanceSource<'a> { let data = source.into(); let expected = width .checked_mul(height) - .ok_or_else(|| Error::InvalidArgument { - message: format!( + .with_context(|| { + Error::invalid_argument(format!( "Luma8LuminanceSource::new: image dimensions overflow usize ({width} x {height})" - ).into(), + )) })?; - if data.len() != expected { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + data.len() == expected, + Error::invalid_argument(format!( "Luma8LuminanceSource::new: luma length {} != width*height ({expected})", data.len() - ).into(), - } - .into()); - } + )) + ); Ok(Self { dimensions: (width, height), data, @@ -70,27 +69,24 @@ pub fn downscale_luma_buffer<'a>( factor: usize, ) -> Result<(Cow<'a, [u8]>, usize, usize)> { if factor == 0 { - return Err(Error::InvalidArgument { - message: format!("downscale_luma_buffer: factor must be at least 1 (got {factor})").into(), - } - .into()); + bail!(Error::invalid_argument(format!( + "downscale_luma_buffer: factor must be at least 1 (got {factor})" + ))); } let expected = width .checked_mul(height) - .ok_or_else(|| Error::InvalidArgument { - message: format!( + .with_context(|| { + Error::invalid_argument(format!( "downscale_luma_buffer: image dimensions overflow usize ({width} x {height})" - ).into(), + )) })?; - if src.len() != expected { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + src.len() == expected, + Error::invalid_argument(format!( "downscale_luma_buffer: src.len() {} must equal width * height ({expected})", src.len() - ).into(), - } - .into()); - } + )) + ); let new_w = width / factor; let new_h = height / factor; if factor == 1 { diff --git a/rxing-reader/src/qrcode/error_correction_level.rs b/rxing-reader/src/qrcode/error_correction_level.rs index 81c4548..f92af77 100644 --- a/rxing-reader/src/qrcode/error_correction_level.rs +++ b/rxing-reader/src/qrcode/error_correction_level.rs @@ -17,8 +17,9 @@ use std::fmt::{self, Display}; use std::str::FromStr; +use anyhow::{Result, bail}; + use crate::Error; -use anyhow::Result; /// See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels /// defined by the QR code standard. @@ -44,10 +45,9 @@ impl ErrorCorrectionLevel { 1 => Ok(Self::L), 2 => Ok(Self::H), 3 => Ok(Self::Q), - _ => Err(Error::InvalidArgument { - message: format!("{bits} is not a valid bit selection").into(), - } - .into()), + _ => bail!(Error::invalid_argument(format!( + "{bits} is not a valid bit selection" + ))), } } @@ -107,10 +107,9 @@ impl FromStr for ErrorCorrectionLevel { return number_possible.try_into(); } - Err(Error::InvalidArgument { - message: format!("could not parse {s} into an ec level").into(), - } - .into()) + bail!(Error::invalid_argument(format!( + "could not parse {s} into an ec level" + ))); } } diff --git a/rxing-reader/src/qrcode/format_information.rs b/rxing-reader/src/qrcode/format_information.rs index aea746b..ddd56cf 100644 --- a/rxing-reader/src/qrcode/format_information.rs +++ b/rxing-reader/src/qrcode/format_information.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use anyhow::Result; +use anyhow::{Context, Result, bail}; use crate::Error; @@ -67,10 +67,9 @@ impl TryFrom for DataMask { 5 => Ok(Self::Pattern101), 6 => Ok(Self::Pattern110), 7 => Ok(Self::Pattern111), - _ => Err(Error::InvalidArgument { - message: format!("QR data-mask bits {value} out of range (expected 0..=7)").into(), - } - .into()), + _ => bail!(Error::invalid_argument(format!( + "QR data-mask bits {value} out of range (expected 0..=7)" + ))), } } } @@ -158,17 +157,19 @@ impl FormatInformation { ], ) .filter(|candidate| candidate.hamming_distance <= MAX_FORMAT_INFO_ERRORS) - .ok_or_else(|| Error::InvalidFormat { - message: format!( + .with_context(|| { + Error::invalid_format(format!( "QR format information is too damaged to decode (top-left=0x{format_info_bits1:04X}, top-right/bottom-left=0x{format_info_bits2:04X})" ) - .into(), + ) })?; // Use bits 3/4 for error correction, and 0-2 for mask. let error_correction_level = - ErrorCorrectionLevel::for_bits(candidate.data >> FORMAT_INFO_ERROR_CORRECTION_SHIFT)?; - let data_mask = DataMask::try_from(candidate.data & FORMAT_INFO_DATA_MASK)?; + ErrorCorrectionLevel::for_bits(candidate.data >> FORMAT_INFO_ERROR_CORRECTION_SHIFT) + .context("decoding QR format error-correction level")?; + let data_mask = DataMask::try_from(candidate.data & FORMAT_INFO_DATA_MASK) + .context("decoding QR format data mask")?; Ok(Self { hamming_distance: candidate.hamming_distance, diff --git a/rxing-reader/src/qrcode/mode.rs b/rxing-reader/src/qrcode/mode.rs index a495186..283475b 100644 --- a/rxing-reader/src/qrcode/mode.rs +++ b/rxing-reader/src/qrcode/mode.rs @@ -16,8 +16,9 @@ use std::fmt; +use anyhow::{Context, Result, bail}; + use crate::Error; -use anyhow::Result; use super::Version; @@ -63,10 +64,7 @@ impl Mode { { Ok(Self::Hanzi) } - _ => Err(Error::InvalidArgument { - message: format!("{bits} is not valid").into(), - } - .into()), + _ => bail!(Error::invalid_argument(format!("{bits} is not valid"))), } } @@ -121,10 +119,8 @@ impl Mode { /// /// Returns an invalid-format error if `bits` does not correspond to a known mode. pub fn codec_mode_for_bits(bits: u32) -> Result { - Mode::try_from(bits).map_err(|_| Error::InvalidFormat { - message: format!("Invalid QR codec mode bits 0x{bits:X}").into(), - } - .into()) + Mode::try_from(bits) + .with_context(|| Error::invalid_format(format!("Invalid QR codec mode bits 0x{bits:X}"))) } } @@ -146,9 +142,8 @@ impl TryFrom for Mode { type Error = anyhow::Error; fn try_from(value: u32) -> Result { - let value = u8::try_from(value).map_err(|_| Error::InvalidArgument { - message: format!("{value} is not valid").into(), - })?; + let value = u8::try_from(value) + .with_context(|| Error::invalid_argument(format!("{value} is not valid")))?; Self::for_bits(value) } } diff --git a/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs b/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs index 857f3dc..48d859f 100644 --- a/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs +++ b/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs @@ -4,7 +4,7 @@ // */ // // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; +use anyhow::{Context, Result, ensure}; use crate::{ Error, @@ -24,16 +24,16 @@ fn module_bit(bit_matrix: &BitMatrix, x: usize, y: usize, mirrored: bool) -> boo } pub fn read_version(bit_matrix: &BitMatrix) -> Result { - let number = Version::number_from_matrix(bit_matrix).ok_or_else(|| Error::InvalidFormat { - message: format!( + let number = Version::number_from_matrix(bit_matrix).with_context(|| { + Error::invalid_format(format!( "QR data is malformed: matrix size {}x{} is not a valid QR size", bit_matrix.width(), bit_matrix.height(), ) - .into(), + ) })?; - Version::for_number(number) + Version::for_number(number).context("resolving QR version from matrix size") } pub fn read_format_information(bit_matrix: &BitMatrix) -> Result { @@ -71,7 +71,9 @@ pub fn read_codewords( version: VersionRef, format_info: &FormatInformation, ) -> Result> { - let function_pattern: BitMatrix = version.build_function_pattern()?; + let function_pattern: BitMatrix = version + .build_function_pattern() + .context("building QR function pattern before reading codewords")?; let mut result: Vec = Vec::with_capacity(version.total_codewords()); let mut current_byte: u8 = 0; @@ -115,14 +117,12 @@ pub fn read_codewords( } let expected_codewords = version.total_codewords(); let actual_codewords = result.len(); - if actual_codewords != expected_codewords { - return Err(Error::InvalidFormat { - message: format!( + ensure!( + actual_codewords == expected_codewords, + Error::invalid_format(format!( "QR data is malformed: expected {expected_codewords} codewords, found {actual_codewords}" - ).into(), - } - .into()); - } + )) + ); Ok(result) } diff --git a/rxing-reader/src/qrcode/reader/decoder.rs b/rxing-reader/src/qrcode/reader/decoder.rs index 195010d..5067ad8 100644 --- a/rxing-reader/src/qrcode/reader/decoder.rs +++ b/rxing-reader/src/qrcode/reader/decoder.rs @@ -4,7 +4,7 @@ */ // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; +use anyhow::{Context, Result, bail, ensure}; use crate::Error; use crate::common::{ @@ -63,13 +63,10 @@ impl TryFrom for HanziSubset { fn try_from(value: u32) -> Result { match value { GB2312_SUBSET => Ok(Self::Gb2312), - _ => Err(Error::InvalidFormat { - message: format!( + _ => bail!(Error::invalid_format(format!( "Unsupported HANZI subset {value} (only GB2312_SUBSET = {GB2312_SUBSET} is supported)" ) - .into(), - } - .into()), + )), } } } @@ -85,23 +82,24 @@ impl TryFrom for AimApplicationIndicator { fn try_from(value: u32) -> Result { match value { - 0..=AIM_NUMERIC_MAX => Ok(Self::Numeric(usize::try_from(value).map_err(|_| { - Error::InvalidFormat { - message: format!("AIM numeric indicator {value} does not fit in usize").into(), - } - })?)), + 0..=AIM_NUMERIC_MAX => Ok(Self::Numeric( + usize::try_from(value).with_context(|| { + Error::invalid_format(format!( + "AIM numeric indicator {value} does not fit in usize" + )) + })?, + )), 165..=190 | 197..=222 => Ok(Self::Alphabetic( - u8::try_from(value - AIM_ALPHA_OFFSET).map_err(|_| Error::InvalidFormat { - message: format!("AIM alphabetic indicator {value} does not fit in u8").into(), + u8::try_from(value - AIM_ALPHA_OFFSET).with_context(|| { + Error::invalid_format(format!( + "AIM alphabetic indicator {value} does not fit in u8" + )) })?, )), - _ => Err(Error::InvalidFormat { - message: format!( + _ => bail!(Error::invalid_format(format!( "Invalid AIM Application Indicator value {value} (expected 0..=99, 165..=190, or 197..=222)" ) - .into(), - } - .into()), + )), } } } @@ -124,21 +122,20 @@ impl EciValueLength { if (first_byte & ECI_THREE_BYTE_PREFIX_MASK) == ECI_THREE_BYTE_PREFIX { return Ok(Self::ThreeBytes); } - Err(Error::InvalidFormat { - message: format!( + bail!(Error::invalid_format(format!( "parse_eci_value: invalid leading byte 0x{first_byte:02X} (top bits do not match any ECI length encoding)" ) - .into(), - } - .into()) + )); } } fn read_bits_as_usize(bits: &mut BitSource, num_bits: usize, context: &str) -> Result { - usize::try_from(bits.read_bits(num_bits)?).map_err(|_| Error::InvalidFormat { - message: format!("{context} does not fit in usize (num_bits={num_bits})").into(), - } - .into()) + let value = bits + .read_bits(num_bits) + .with_context(|| Error::invalid_format(format!("reading {context}")))?; + usize::try_from(value).with_context(|| { + Error::invalid_format(format!("{context} does not fit in usize (num_bits={num_bits})")) + }) } fn read_count(bits: &mut BitSource, num_bits: usize) -> Result { @@ -146,8 +143,8 @@ fn read_count(bits: &mut BitSource, num_bits: usize) -> Result { } fn append_be_u16(result: &mut ECIStringBuilder, value: u32) -> Result<()> { - let value = u16::try_from(value).map_err(|_| Error::InvalidFormat { - message: format!("double-byte character value 0x{value:X} out of range").into(), + let value = u16::try_from(value).with_context(|| { + Error::invalid_format(format!("double-byte character value 0x{value:X} out of range")) })?; let bytes = value.to_be_bytes(); *result += bytes[0]; @@ -170,7 +167,9 @@ pub fn decode_hanzi_segment( while count > 0 { // Each 13 bits encodes a 2-byte character - let two_bytes = bits.read_bits(DOUBLE_BYTE_SEGMENT_BITS)?; + let two_bytes = bits + .read_bits(DOUBLE_BYTE_SEGMENT_BITS) + .context("reading HANZI double-byte segment")?; let mut assembled_two_bytes = ((two_bytes / HANZI_BYTE_MULTIPLIER) << 8) | (two_bytes % HANZI_BYTE_MULTIPLIER); if assembled_two_bytes < HANZI_LOW_RANGE_LIMIT { @@ -199,7 +198,9 @@ pub fn decode_kanji_segment( while count > 0 { // Each 13 bits encodes a 2-byte character - let two_bytes = bits.read_bits(DOUBLE_BYTE_SEGMENT_BITS)?; + let two_bytes = bits + .read_bits(DOUBLE_BYTE_SEGMENT_BITS) + .context("reading KANJI double-byte segment")?; let mut assembled_two_bytes = ((two_bytes / KANJI_BYTE_MULTIPLIER) << 8) | (two_bytes % KANJI_BYTE_MULTIPLIER); if assembled_two_bytes < KANJI_LOW_RANGE_LIMIT { @@ -224,9 +225,11 @@ pub fn decode_byte_segment( result.reserve(count); for _ in 0..count { - *result += u8::try_from(bits.read_bits(BYTE_BITS)?).map_err(|_| Error::InvalidFormat { - message: "byte segment produced a value outside u8".into(), - })?; + *result += u8::try_from( + bits.read_bits(BYTE_BITS) + .context("reading QR byte segment byte")?, + ) + .with_context(|| Error::invalid_format("byte segment produced a value outside u8"))?; } Ok(()) } @@ -235,13 +238,12 @@ pub fn to_alphanumeric_char(value: usize) -> Result { ALPHANUMERIC_CHARS .get(value) .copied() - .ok_or_else(|| Error::InvalidFormat { - message: format!( + .with_context(|| { + Error::invalid_format(format!( "to_alphanumeric_char: value {value} out of range (expected 0..{})", ALPHANUMERIC_CHARS.len() - ).into(), - } - .into()) + )) + }) } pub fn decode_alphanumeric_segment( @@ -315,19 +317,24 @@ pub fn decode_numeric_segment( } pub fn parse_eci_value(bits: &mut BitSource) -> Result { - let first_byte = bits.read_bits(BYTE_BITS)?; + let first_byte = bits.read_bits(BYTE_BITS).context("reading ECI first byte")?; match EciValueLength::from_first_byte(first_byte)? { - EciValueLength::OneByte => Eci::try_from(first_byte & ECI_ONE_BYTE_PAYLOAD_MASK), + EciValueLength::OneByte => Eci::try_from(first_byte & ECI_ONE_BYTE_PAYLOAD_MASK) + .context("decoding one-byte ECI assignment value"), EciValueLength::TwoBytes => { - let second_byte = bits.read_bits(BYTE_BITS)?; + let second_byte = bits.read_bits(BYTE_BITS).context("reading ECI second byte")?; Eci::try_from(((first_byte & ECI_TWO_BYTE_PAYLOAD_MASK) << BYTE_BITS) | second_byte) + .context("decoding two-byte ECI assignment value") } EciValueLength::ThreeBytes => { - let second_third_bytes = bits.read_bits(2 * BYTE_BITS)?; + let second_third_bytes = bits + .read_bits(2 * BYTE_BITS) + .context("reading ECI second and third bytes")?; Eci::try_from( ((first_byte & ECI_THREE_BYTE_PAYLOAD_MASK) << (2 * BYTE_BITS)) | second_third_bytes, ) + .context("decoding three-byte ECI assignment value") } } } @@ -344,7 +351,11 @@ pub fn parse_eci_value(bits: &mut BitSource) -> Result { pub fn is_end_of_stream(bits: &mut BitSource, version: &Version) -> Result { let bits_required = Mode::terminator_bit_length(version); let bits_available = std::cmp::min(bits.available(), bits_required); - Ok(bits_available == 0 || bits.peek_bits(bits_available)? == 0) + Ok(bits_available == 0 + || bits + .peek_bits(bits_available) + .context("peeking QR mode terminator bits")? + == 0) } /// QR Codes can encode text as bits in one of several modes, and can use multiple modes @@ -368,7 +379,10 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result = (|| { while !is_end_of_stream(&mut bits, version)? { - let mode = Mode::codec_mode_for_bits(bits.read_bits(mode_bit_length)?)?; + let mode_bits = bits + .read_bits(mode_bit_length) + .context("reading QR mode indicator")?; + let mode = Mode::codec_mode_for_bits(mode_bits)?; if !modes_seen.contains(&mode) { modes_seen.push(mode); } @@ -379,12 +393,18 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result } Mode::Fnc1SecondPosition => { - if !result.is_empty() { - return Err(Error::InvalidFormat { message: "AIM Application Indicator (FNC1 in second position) at illegal position".into() }.into()); - } + ensure!( + result.is_empty(), + Error::invalid_format( + "AIM Application Indicator (FNC1 in second position) at illegal position" + ) + ); result.symbology.modifier = b'5'; // ISO/IEC 18004:2015 7.4.8.3 AIM Application Indicator (FNC1 in second position), "00-99" or "A-Za-z" - match AimApplicationIndicator::try_from(bits.read_bits(BYTE_BITS)?)? { + let indicator = bits + .read_bits(BYTE_BITS) + .context("reading AIM application indicator")?; + match AimApplicationIndicator::try_from(indicator)? { AimApplicationIndicator::Numeric(value) => { result += padded_digits(value, 2)?; } @@ -395,9 +415,15 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result { - let index = bits.read_bits(STRUCTURED_APPEND_INDEX_BITS)?; - let count = bits.read_bits(STRUCTURED_APPEND_COUNT_BITS)?; - let parity = bits.read_bits(STRUCTURED_APPEND_PARITY_BITS)?; + let index = bits + .read_bits(STRUCTURED_APPEND_INDEX_BITS) + .context("reading structured-append index")?; + let count = bits + .read_bits(STRUCTURED_APPEND_COUNT_BITS) + .context("reading structured-append count")?; + let parity = bits + .read_bits(STRUCTURED_APPEND_PARITY_BITS) + .context("reading structured-append parity")?; structured_append = Some(StructuredAppendInfo { index: index as u8, count: count as u8, @@ -411,7 +437,10 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result { // First handle Hanzi mode which does not start with character count // chinese mode contains a sub set indicator right after mode indicator - let _subset = HanziSubset::try_from(bits.read_bits(HANZI_SUBSET_BITS)?)?; + let subset = bits + .read_bits(HANZI_SUBSET_BITS) + .context("reading HANZI subset")?; + let _subset = HanziSubset::try_from(subset)?; let count = read_count(&mut bits, mode.character_count_bits(version))?; decode_hanzi_segment(&mut bits, count, &mut result)?; } @@ -427,12 +456,9 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result decode_byte_segment(&mut bits, count, &mut result)?, Mode::Kanji => decode_kanji_segment(&mut bits, count, &mut result)?, other => { - return Err(Error::InvalidFormat { - message: format!( + bail!(Error::invalid_format(format!( "Invalid CodecMode {other:?} encountered in data segment (count={count})" - ).into(), - } - .into()); + ))); } }; } @@ -451,56 +477,52 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result Result { - if !Version::has_valid_size(bits) { - return Err(Error::InvalidFormat { - message: format!("Invalid QR symbol size: {}x{}", bits.width(), bits.height()).into(), - } - .into()); - } - let format_info = read_format_information(bits).map_err(|e| Error::InvalidFormat { - message: format!( - "Invalid format information in {}x{} symbol: {e}", + ensure!( + Version::has_valid_size(bits), + Error::invalid_format(format!("Invalid QR symbol size: {}x{}", bits.width(), bits.height())) + ); + let format_info = read_format_information(bits).with_context(|| { + Error::invalid_format(format!( + "Invalid format information in {}x{} symbol", bits.width(), bits.height() - ).into(), + )) })?; - let version = read_version(bits).map_err(|e| Error::InvalidFormat { - message: format!( - "Invalid version in {}x{} QR symbol: {e}", + let version = read_version(bits).with_context(|| { + Error::invalid_format(format!( + "Invalid version in {}x{} QR symbol", bits.width(), bits.height() - ).into(), + )) })?; // Read codewords - let codewords = read_codewords(bits, version, &format_info)?; - if codewords.is_empty() { - return Err(Error::InvalidFormat { - message: format!( + let codewords = read_codewords(bits, version, &format_info) + .context("reading QR codewords from sampled matrix")?; + ensure!( + !codewords.is_empty(), + Error::invalid_format(format!( "Failed to read codewords for version {} ({}x{} symbol)", version.number(), bits.width(), bits.height() - ).into(), - } - .into()); - } + )) + ); // Separate into data blocks let data_blocks: Vec = - DataBlock::data_blocks(&codewords, version, format_info.error_correction_level)?; - if data_blocks.is_empty() { - return Err(Error::InvalidFormat { - message: format!( + DataBlock::data_blocks(&codewords, version, format_info.error_correction_level) + .context("splitting QR codewords into data blocks")?; + ensure!( + !data_blocks.is_empty(), + Error::invalid_format(format!( "Failed to get data blocks for version {} (codewords={}, ec_level={:?})", version.number(), codewords.len(), format_info.error_correction_level - ).into(), - } - .into()); - } + )) + ); // Count total number of data bytes let op = @@ -514,7 +536,8 @@ pub fn decode(bits: &BitMatrix) -> Result { let num_data_codewords = data_block.num_data_codewords(); let mut codeword_bytes = data_block.codewords; - correct_errors(&mut codeword_bytes, num_data_codewords)?; + correct_errors(&mut codeword_bytes, num_data_codewords) + .context("correcting QR data block errors")?; result_bytes[result_iterator..(result_iterator + num_data_codewords)] .copy_from_slice(&codeword_bytes[..num_data_codewords]); @@ -522,7 +545,8 @@ pub fn decode(bits: &BitMatrix) -> Result { } // decode the contents of that stream of bytes - let decoder_result = decode_bit_stream(&result_bytes, version)?; + let decoder_result = decode_bit_stream(&result_bytes, version) + .context("decoding corrected QR bit stream")?; Ok(decoder_result.with_format( version.number(), format_info.error_correction_level, @@ -548,18 +572,17 @@ impl DataBlock { version: VersionRef, ec_level: ErrorCorrectionLevel, ) -> Result> { - if raw_codewords.len() != version.total_codewords() { - return Err(Error::InvalidArgument { - message: format!( + ensure!( + raw_codewords.len() == version.total_codewords(), + Error::invalid_argument(format!( "raw codewords length {} does not match expected total codewords {}", raw_codewords.len(), version.total_codewords() - ).into(), - } - .into()); - } + )) + ); - let ec_blocks = version.ec_blocks_for_level(ec_level)?; + let ec_blocks = version.ec_blocks_for_level(ec_level) + .context("looking up QR error-correction blocks")?; let mut result = Vec::new(); let mut num_result_blocks = 0; @@ -576,13 +599,10 @@ impl DataBlock { } } - if result.is_empty() { - return Err(Error::InvalidArgument { - message: "result block list is empty; possible data corruption or misconfiguration" - .into(), - } - .into()); - } + ensure!( + !result.is_empty(), + Error::invalid_argument("result block list is empty; possible data corruption or misconfiguration") + ); let shorter_blocks_total_codewords = result[0].codewords.len(); let mut longer_blocks_start_at = result.len() - 1; @@ -633,8 +653,7 @@ impl DataBlock { } fn correct_errors(codeword_bytes: &mut [u8], num_data_codewords: usize) -> Result<()> { - correct_qr_errors(codeword_bytes, num_data_codewords).map_err(|e| Error::Checksum { - message: format!("{e:?}").into(), - })?; + correct_qr_errors(codeword_bytes, num_data_codewords) + .with_context(|| Error::checksum("QR Reed-Solomon error correction failed"))?; Ok(()) } diff --git a/rxing-reader/src/qrcode/reader/detector.rs b/rxing-reader/src/qrcode/reader/detector.rs index 8eee115..16307ff 100644 --- a/rxing-reader/src/qrcode/reader/detector.rs +++ b/rxing-reader/src/qrcode/reader/detector.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result, bail, ensure}; use multimap::MultiMap; use crate::{ @@ -271,15 +271,11 @@ fn estimate_module_size( ) -> Result { let mut cur = EdgeTracer::new(image, a.p, b.p - a.p); if !cur.is_black() { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } - let pattern = read_symmetric_pattern::<5, _>(&mut cur, a.size * 2).ok_or(Error::NotFound { - message: "QR pattern was not detected".into(), - })?; + let pattern = read_symmetric_pattern::<5, _>(&mut cur, a.size * 2) + .with_context(|| Error::not_found("QR pattern was not detected"))?; if !(is_pattern::( &PatternView::from_bars(&pattern), @@ -289,10 +285,7 @@ fn estimate_module_size( 0.0, ) != 0.0) { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } Ok( @@ -501,12 +494,8 @@ pub fn read_version( } fn module_dimension(dimension: i32) -> Result { - usize::try_from(dimension).map_err(|_| { - Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into() - }) + usize::try_from(dimension) + .with_context(|| Error::not_found("QR pattern was not detected")) } pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result { @@ -522,10 +511,7 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result top, (None, Some(left)) => left, (None, None) => { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); + bail!(Error::not_found("QR pattern was not detected")); } }; @@ -545,18 +531,22 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result 21 && let Some(br_cp) = locate_alignment_pattern(image, module_size, br_inter) @@ -591,19 +581,16 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result= Version::dimension_for_number(7) { let version = - read_version(image, dimension_usize, mod_to_pix).map_err(|_| Error::NotFound { - message: "QR pattern was not detected".into(), - })?; + read_version(image, dimension_usize, mod_to_pix) + .with_context(|| Error::not_found("QR pattern was not detected"))?; let version_dimension = version.dimension(); let version_dimension_i32 = version_dimension as i32; // if the version bits are garbage -> discard the detection - if (version_dimension_i32 - dimension).abs() > 8 { - return Err(Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into()); - } + ensure!( + (version_dimension_i32 - dimension).abs() <= 8, + Error::not_found("QR pattern was not detected") + ); if version_dimension_i32 != dimension { dimension = version_dimension_i32; dimension_usize = version_dimension; @@ -614,19 +601,15 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result Result { mod2_pix .transform_point(point(ap_m[x] as f32, ap_m[y] as f32).centered()) - .ok_or_else(|| { - Error::NotFound { - message: "QR pattern was not detected".into(), - } - .into() - }) + .with_context(|| Error::not_found("QR pattern was not detected")) }; let mut find_inner_corner_of_concentric_pattern = @@ -711,9 +694,7 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result Result Result<(), ReedSolomonError> { +) -> Result<()> { let two_s = received .len() .checked_sub(num_data_codewords) - .ok_or(ReedSolomonError::TooManyErrors)?; + .context(ReedSolomonError::TooManyErrors)?; if two_s == 0 { return Ok(()); } if received.len() >= FIELD_SIZE { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } let field = &QR_FIELD; @@ -39,14 +43,14 @@ pub(super) fn correct_qr_errors( for (&location, &magnitude) in error_locations.iter().zip(error_magnitudes.iter()) { let log = field.log(location)?; if log >= received.len() { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } let position = received.len() - 1 - log; received[position] ^= magnitude; } if has_errors(field, received, two_s) { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } Ok(()) @@ -95,16 +99,16 @@ impl QrField { self.exp_table[value] } - fn log(&self, value: u8) -> Result { + fn log(&self, value: u8) -> Result { if value == 0 { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } Ok(self.log_table[usize::from(value)]) } - fn inverse(&self, value: u8) -> Result { + fn inverse(&self, value: u8) -> Result { if value == 0 { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } Ok(self.exp_table[FIELD_ORDER - self.log_table[usize::from(value)]]) } @@ -127,7 +131,7 @@ fn run_euclidean_algorithm( mut a: Vec, mut b: Vec, received_ec_len: usize, -) -> Result<(Vec, Vec), ReedSolomonError> { +) -> Result<(Vec, Vec)> { if poly_degree(&a) < poly_degree(&b) { std::mem::swap(&mut a, &mut b); } @@ -144,7 +148,7 @@ fn run_euclidean_algorithm( t_last = t; if poly_is_zero(&r_last) { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } r = r_last_last; @@ -163,13 +167,13 @@ fn run_euclidean_algorithm( t = poly_add(poly_multiply(field, &q, &t_last), &t_last_last); if poly_degree(&r) >= poly_degree(&r_last) { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } } let sigma_tilde_at_zero = poly_coefficient(&t, 0); if sigma_tilde_at_zero == 0 { - return Err(ReedSolomonError::TooManyErrors); + bail!(ReedSolomonError::TooManyErrors); } let inverse = field.inverse(sigma_tilde_at_zero)?; @@ -182,7 +186,7 @@ fn run_euclidean_algorithm( fn find_error_locations( field: &QrField, error_locator: &[u8], -) -> Result, ReedSolomonError> { +) -> Result> { let num_errors = poly_degree(error_locator); if num_errors == 1 { return Ok(vec![poly_coefficient(error_locator, 1)]); @@ -201,7 +205,7 @@ fn find_error_locations( if result.len() == num_errors { Ok(result) } else { - Err(ReedSolomonError::TooManyErrors) + bail!(ReedSolomonError::TooManyErrors); } } @@ -209,7 +213,7 @@ fn find_error_magnitudes( field: &QrField, error_evaluator: &[u8], error_locations: &[u8], -) -> Result, ReedSolomonError> { +) -> Result> { let mut result = Vec::with_capacity(error_locations.len()); for (i, &location) in error_locations.iter().enumerate() { let xi_inverse = field.inverse(location)?; diff --git a/rxing-reader/src/qrcode/version.rs b/rxing-reader/src/qrcode/version.rs index 23397f3..0d59dfc 100755 --- a/rxing-reader/src/qrcode/version.rs +++ b/rxing-reader/src/qrcode/version.rs @@ -16,8 +16,9 @@ use std::fmt; +use anyhow::{Context, Result, bail, ensure}; + use crate::{Error, common::BitMatrix}; -use anyhow::Result; use super::ErrorCorrectionLevel; @@ -94,15 +95,12 @@ impl Version { pub fn ec_blocks_for_level(&self, ec_level: ErrorCorrectionLevel) -> Result<&ECBlocks> { self.ec_blocks .get(ec_level.ec_blocks_index()) - .ok_or_else(|| { - Error::InvalidArgument { - message: format!( + .with_context(|| { + Error::invalid_argument(format!( "ErrorCorrectionLevel index {} out of range for {} EC blocks", ec_level.ec_blocks_index(), self.ec_blocks.len() - ).into(), - } - .into() + )) }) } @@ -114,25 +112,23 @@ impl Version { /// Version 1 has dimension 21. Returns an invalid-format error if /// dimension is less than 21 or (dimension - 1) % 4 != 0. pub fn provisional_for_dimension(dimension: usize) -> Result { - if dimension % DIMENSION_STEP != DIMENSION_REMAINDER || dimension < MIN_DIMENSION { - return Err(Error::InvalidFormat { - message: format!( + ensure!( + dimension % DIMENSION_STEP == DIMENSION_REMAINDER && dimension >= MIN_DIMENSION, + Error::invalid_format(format!( "QR dimension {dimension} is invalid (expected >= 21 and (dimension - 1) % 4 == 0)" ) - .into(), - } - .into()); - } + ) + ); Self::for_number(((dimension - DIMENSION_VERSION_OFFSET) / DIMENSION_STEP) as u32) } pub fn for_number(version_number: u32) -> Result { - if !(MIN_VERSION_NUMBER..=MAX_VERSION_NUMBER).contains(&version_number) { - return Err(Error::InvalidArgument { - message: format!("QR version {version_number} is out of spec (expected 1..=40)").into(), - } - .into()); - } + ensure!( + (MIN_VERSION_NUMBER..=MAX_VERSION_NUMBER).contains(&version_number), + Error::invalid_argument(format!( + "QR version {version_number} is out of spec (expected 1..=40)" + )) + ); let version_index = (version_number - 1) as usize; Ok(&VERSIONS[version_index]) } @@ -163,10 +159,7 @@ impl Version { { return Self::for_number(best_version as u32); } - Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()) + bail!(Error::invalid_state("required internal state is missing")); } fn is_valid_dimensions(width: usize, height: usize) -> bool { @@ -187,15 +180,20 @@ impl Version { /// See ISO 18004:2006 Annex E pub fn build_function_pattern(&self) -> Result { let dimension = self.dimension(); - let mut bit_matrix = BitMatrix::with_single_dimension(dimension)?; + let mut bit_matrix = BitMatrix::with_single_dimension(dimension) + .context("building QR function-pattern matrix")?; // Top left finder pattern + separator + format - bit_matrix.set_region(0, 0, 9, 9)?; + bit_matrix.set_region(0, 0, 9, 9).context("setting top-left finder pattern")?; // Top right finder pattern + separator + format - bit_matrix.set_region(dimension - 8, 0, 8, 9)?; + bit_matrix + .set_region(dimension - 8, 0, 8, 9) + .context("setting top-right finder pattern")?; // Bottom left finder pattern + separator + format - bit_matrix.set_region(0, dimension - 8, 9, 8)?; + bit_matrix + .set_region(0, dimension - 8, 9, 8) + .context("setting bottom-left finder pattern")?; // Alignment patterns let max = self.alignment_pattern_centers.len(); @@ -203,22 +201,32 @@ impl Version { let i = self.alignment_pattern_centers[x] - 2; for y in 0..max { if (x != 0 || (y != 0 && y != max - 1)) && (x != max - 1 || y != 0) { - bit_matrix.set_region(self.alignment_pattern_centers[y] - 2, i, 5, 5)?; + bit_matrix + .set_region(self.alignment_pattern_centers[y] - 2, i, 5, 5) + .context("setting QR alignment pattern")?; } // else no alignment patterns near the three finder patterns } } // Vertical timing pattern - bit_matrix.set_region(6, 9, 1, dimension - 17)?; + bit_matrix + .set_region(6, 9, 1, dimension - 17) + .context("setting vertical timing pattern")?; // Horizontal timing pattern - bit_matrix.set_region(9, 6, dimension - 17, 1)?; + bit_matrix + .set_region(9, 6, dimension - 17, 1) + .context("setting horizontal timing pattern")?; if self.version_number > 6 { // Version info, top right - bit_matrix.set_region(dimension - 11, 0, 3, 6)?; + bit_matrix + .set_region(dimension - 11, 0, 3, 6) + .context("setting top-right version information")?; // Version info, bottom left - bit_matrix.set_region(0, dimension - 11, 6, 3)?; + bit_matrix + .set_region(0, dimension - 11, 6, 3) + .context("setting bottom-left version information")?; } Ok(bit_matrix) diff --git a/rxing-reader/tests/qr_decode.rs b/rxing-reader/tests/qr_decode.rs index dd863bd..3654950 100644 --- a/rxing-reader/tests/qr_decode.rs +++ b/rxing-reader/tests/qr_decode.rs @@ -450,7 +450,11 @@ fn binarizer_fallback_rescues_vignetted_with_global_primary() { #[test] fn rgba_length_mismatch_is_rejected() { let err = rgba_to_luma(&[0u8; 15], 2, 2).expect_err("expected length-mismatch error"); - assert!(err.contains("rgba length"), "unexpected error: {}", err); + assert!( + err.to_string().contains("rgba length"), + "unexpected error: {}", + err + ); } // --------------------------------------------------------------------------- diff --git a/rxing-wasm/src/lib.rs b/rxing-wasm/src/lib.rs index 97d3183..b60d58c 100644 --- a/rxing-wasm/src/lib.rs +++ b/rxing-wasm/src/lib.rs @@ -103,7 +103,7 @@ pub fn read_qr_codes_rgba( let width = width as usize; let height = height as usize; let max_number_of_symbols = max_number_of_symbols as usize; - let luma = rgba_to_luma(rgba, width, height).map_err(|m| JsValue::from_str(&m))?; + let luma = rgba_to_luma(rgba, width, height).map_err(|e| JsValue::from_str(&e.to_string()))?; let symbols = read_luma( luma, width, @@ -158,7 +158,7 @@ pub fn read_qr_codes_rgba_detailed( let width = width as usize; let height = height as usize; let max_number_of_symbols = max_number_of_symbols as usize; - let luma = rgba_to_luma(rgba, width, height).map_err(|m| JsValue::from_str(&m))?; + let luma = rgba_to_luma(rgba, width, height).map_err(|e| JsValue::from_str(&e.to_string()))?; let symbols = read_luma( luma, width, From f077f1a1993e711d5d3866b8a6e84f94b0ac6d79 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 May 2026 03:51:20 +0000 Subject: [PATCH 2/4] fix: improve error handling in to_fixed_len_string for oversized values --- rxing-reader/src/common/detect/util.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rxing-reader/src/common/detect/util.rs b/rxing-reader/src/common/detect/util.rs index 6c7a501..69abe26 100644 --- a/rxing-reader/src/common/detect/util.rs +++ b/rxing-reader/src/common/detect/util.rs @@ -26,7 +26,8 @@ pub fn update_min_max_float(min: &mut f64, max: &mut f64, val: f64) { } pub fn to_fixed_len_string>(val: T, len: usize) -> Result { - let mut val: usize = val.into(); + let original_val: usize = val.into(); + let mut val = original_val; let mut result = vec!['0'; len]; let mut idx = len; while idx > 0 && val != 0 { @@ -35,7 +36,10 @@ pub fn to_fixed_len_string>(val: T, len: usize) -> Result val /= 10; } if val != 0 { - bail!(Error::invalid_format("Invalid value")); + let actual_len = original_val.to_string().len(); + bail!(Error::invalid_format(format!( + "Value too large for fixed-length string: length {actual_len} exceeds maximum {len}" + ))); } Ok(result.iter().collect()) From 386621b37d7ce37c67d3720ba753a5acc5e28506 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 May 2026 04:08:14 +0000 Subject: [PATCH 3/4] fix: enhance error messages for invalid states in pattern and perspective transform functions --- rxing-reader/src/common/detect/pattern.rs | 8 ++++++-- rxing-reader/src/common/global_histogram_binarizer.rs | 5 ++++- rxing-reader/src/common/perspective_transform.rs | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rxing-reader/src/common/detect/pattern.rs b/rxing-reader/src/common/detect/pattern.rs index afbc56b..ffabe2a 100644 --- a/rxing-reader/src/common/detect/pattern.rs +++ b/rxing-reader/src/common/detect/pattern.rs @@ -541,7 +541,11 @@ pub fn find_left_guard_by) const PREV_IDX: isize = -1; if view.size() < min_size { - anyhow::bail!(Error::invalid_state("required internal state is missing")); + anyhow::bail!(Error::invalid_state(format!( + "view size {} is less than required min_size {}", + view.size(), + min_size + ))); } let mut window = view.sub_view(0, LEN); @@ -563,7 +567,7 @@ pub fn find_left_guard_by) window.skip_pair(); } - anyhow::bail!(Error::invalid_state("required internal state is missing")); + anyhow::bail!(Error::invalid_state("no matching guard pattern found")); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/rxing-reader/src/common/global_histogram_binarizer.rs b/rxing-reader/src/common/global_histogram_binarizer.rs index b39fc4b..f5e8b3d 100644 --- a/rxing-reader/src/common/global_histogram_binarizer.rs +++ b/rxing-reader/src/common/global_histogram_binarizer.rs @@ -151,7 +151,10 @@ impl GlobalHistogramBinarizer { // If there is too little contrast in the image to pick a meaningful black point, throw rather // than waste time trying to decode the image, and risk false positives. if second_peak - first_peak <= BUCKET_COUNT / 16 { - bail!(Error::not_found("second_peak - first_peak <= numBuckets / 16 ")); + bail!(Error::not_found(format!( + "image histogram peaks are too close to determine a threshold (separation {} <= BUCKET_COUNT / 16)", + second_peak - first_peak + ))); } // Find a valley between them that is low and closer to the white peak. diff --git a/rxing-reader/src/common/perspective_transform.rs b/rxing-reader/src/common/perspective_transform.rs index fad8f93..d032aa4 100644 --- a/rxing-reader/src/common/perspective_transform.rs +++ b/rxing-reader/src/common/perspective_transform.rs @@ -74,7 +74,7 @@ impl PerspectiveTransform { pub fn quadrilateral_to_quadrilateral(dst: Quadrilateral, src: Quadrilateral) -> Result { ensure!( src.is_convex() && dst.is_convex(), - Error::invalid_state("required internal state is missing") + Error::invalid_state("src and dst quadrilaterals must be convex") ); let q_to_s = PerspectiveTransform::quadrilateral_to_square(dst) .context("building destination-to-square perspective transform")?; @@ -100,7 +100,7 @@ impl PerspectiveTransform { for point in points.iter_mut() { *point = self .transform_point(*point) - .with_context(|| Error::not_found("QR pattern was not detected"))?; + .with_context(|| Error::not_found("perspective transform degenerate: denominator below epsilon"))?; } Ok(()) } From 628f54d1e14bdff21c98275f36dc712ea039acd4 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 May 2026 04:20:16 +0000 Subject: [PATCH 4/4] chore: bump version to 0.0.11 for rxing-cli, rxing-reader, and rxing-wasm --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c7d21d..31dc64d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -767,7 +767,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rxing-cli" -version = "0.0.10" +version = "0.0.11" dependencies = [ "anyhow", "base64", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "rxing-reader" -version = "0.0.10" +version = "0.0.11" dependencies = [ "anyhow", "base64", @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "rxing-wasm" -version = "0.0.10" +version = "0.0.11" dependencies = [ "js-sys", "rxing-reader", diff --git a/Cargo.toml b/Cargo.toml index ad5ff68..12ac7b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.0.10" +version = "0.0.11" [profile.release] codegen-units = 1