use crate::{Image, Scanner};
use image::{GrayImage, Luma};
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use qrcode::QrCode;
fn generate_qr_image(data: &[u8]) -> Option<GrayImage> {
let code = QrCode::new(data).ok()?;
let image = code.render::<Luma<u8>>().quiet_zone(true).build();
Some(image)
}
fn decode_qr_image_rqrr(img: &GrayImage) -> Result<Vec<Vec<u8>>, String> {
let width = img.width() as usize;
let height = img.height() as usize;
let raw = img.as_raw();
let mut prepared_img =
rqrr::PreparedImage::prepare_from_greyscale(width, height, |x, y| raw[y * width + x]);
let grids = prepared_img.detect_grids();
if grids.is_empty() {
return Err("rqrr: No grids found".to_string());
}
let mut results = Vec::new();
for grid in grids {
let (_meta, content) = grid
.decode()
.map_err(|e| format!("rqrr: Failed to decode grid: {e:?}"))?;
results.push(content.into_bytes());
}
Ok(results)
}
fn decode_qr_image(img: &GrayImage) -> Result<Vec<Vec<u8>>, String> {
let (width, height) = img.dimensions();
let data = img.as_raw();
let mut zedbar_img = Image::from_gray(data, width, height)
.map_err(|e| format!("Failed to create ZBar image: {e:?}"))?;
let mut scanner = Scanner::new();
let symbols = scanner.scan(&mut zedbar_img);
if symbols.is_empty() {
return Err("No symbols found".to_string());
}
let zedbar_results: Vec<Vec<u8>> = symbols.iter().map(|s| s.data().to_vec()).collect();
let rqrr_results = decode_qr_image_rqrr(img)?;
let mut zedbar_sorted = zedbar_results.clone();
let mut rqrr_sorted = rqrr_results.clone();
zedbar_sorted.sort();
rqrr_sorted.sort();
if zedbar_sorted != rqrr_sorted {
return Err(format!(
"zedbar and rqrr produced different results!\nzedbar: {} symbols\nrqrr: {} symbols",
zedbar_sorted.len(),
rqrr_sorted.len()
));
}
Ok(zedbar_results)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))]
#[test]
fn prop_qr_roundtrip_ascii(data in "[a-zA-Z0-9 ]{1,100}") {
let data_bytes = data.as_bytes();
let img = generate_qr_image(data_bytes)
.ok_or_else(|| TestCaseError::fail("Failed to generate QR code"))?;
let decoded = decode_qr_image(&img)
.map_err(TestCaseError::fail)?;
prop_assert_eq!(decoded.len(), 1, "Expected exactly one decoded symbol");
prop_assert_eq!(&decoded[0], data_bytes, "Decoded data doesn't match original");
}
#[test]
fn prop_qr_roundtrip_numeric(data in "[0-9]{1,100}") {
let data_bytes = data.as_bytes();
let img = generate_qr_image(data_bytes)
.ok_or_else(|| TestCaseError::fail("Failed to generate QR code"))?;
let decoded = decode_qr_image(&img)
.map_err(TestCaseError::fail)?;
prop_assert_eq!(decoded.len(), 1);
prop_assert_eq!(&decoded[0], data_bytes);
}
#[test]
fn prop_qr_roundtrip_binary(data in prop::collection::vec(0u8..128, 1..50)) {
let img = match generate_qr_image(&data) {
Some(img) => img,
None => return Ok(()), };
let decoded = decode_qr_image(&img)
.map_err(TestCaseError::fail)?;
prop_assert_eq!(decoded.len(), 1);
prop_assert_eq!(&decoded[0], &data);
}
#[test]
fn prop_qr_roundtrip_urls(
protocol in "(https?|ftp)",
domain in "[a-z]{3,20}",
tld in "(com|org|net|edu)",
path in prop::option::of("[a-z0-9/]{0,30}")
) {
let url = match path {
Some(p) => format!("{protocol}://{domain}.{tld}/{p}"),
None => format!("{protocol}://{domain}.{tld}"),
};
let data_bytes = url.as_bytes();
let img = generate_qr_image(data_bytes)
.ok_or_else(|| TestCaseError::fail("Failed to generate QR code"))?;
let decoded = decode_qr_image(&img)
.map_err(TestCaseError::fail)?;
prop_assert_eq!(decoded.len(), 1);
prop_assert_eq!(&decoded[0], data_bytes);
}
#[test]
fn prop_qr_empty_or_single_byte(data in prop::collection::vec(0u8..128, 0..2)) {
if data.is_empty() {
if let Some(img) = generate_qr_image(&data)
&& let Ok(decoded) = decode_qr_image(&img) {
prop_assert_eq!(decoded.len(), 1);
prop_assert_eq!(&decoded[0], &data);
}
} else {
let img = generate_qr_image(&data)
.ok_or_else(|| TestCaseError::fail("Failed to generate QR code"))?;
let decoded = decode_qr_image(&img)
.map_err(TestCaseError::fail)?;
prop_assert_eq!(decoded.len(), 1);
prop_assert_eq!(&decoded[0], &data);
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_simple_qr_roundtrip() {
let data = b"Hello, World!";
let img = generate_qr_image(data).expect("Failed to generate QR code");
let decoded = decode_qr_image(&img).expect("Failed to decode QR code");
assert_eq!(decoded.len(), 1);
assert_eq!(&decoded[0], data);
}
#[test]
fn test_numeric_qr_roundtrip() {
let data = b"123456789";
let img = generate_qr_image(data).expect("Failed to generate QR code");
let decoded = decode_qr_image(&img).expect("Failed to decode QR code");
assert_eq!(decoded.len(), 1);
assert_eq!(&decoded[0], data);
}
#[test]
fn test_url_qr_roundtrip() {
let data = b"https://example.com/path/to/resource";
let img = generate_qr_image(data).expect("Failed to generate QR code");
let decoded = decode_qr_image(&img).expect("Failed to decode QR code");
assert_eq!(decoded.len(), 1);
assert_eq!(&decoded[0], data);
}
#[test]
fn test_binary_qr_roundtrip() {
let data = vec![0u8, 1, 2, 3, 65, 66, 67]; let img = generate_qr_image(&data).expect("Failed to generate QR code");
let decoded = decode_qr_image(&img).expect("Failed to decode QR code");
assert_eq!(decoded.len(), 1);
assert_eq!(&decoded[0], &data);
}
#[test]
fn test_special_chars_qr_roundtrip() {
let data = b"!@#$%^&*()_+-=[]{}|;:',.<>?/~`";
let img = generate_qr_image(data).expect("Failed to generate QR code");
let decoded = decode_qr_image(&img).expect("Failed to decode QR code");
assert_eq!(decoded.len(), 1);
assert_eq!(&decoded[0], data);
}
}