#![cfg(feature = "barcode")]
use rxing::RXingResult;
use crate::error::{Error, Result};
use crate::tools::workspace::Workspace;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decoded {
pub text: String,
pub format: String,
}
impl From<&RXingResult> for Decoded {
fn from(r: &RXingResult) -> Self {
Self {
text: r.getText().to_string(),
format: format!("{:?}", r.getBarcodeFormat()),
}
}
}
pub fn decode(ws: &Workspace, rel: &str) -> Result<Vec<Decoded>> {
let bytes = ws.read_bytes(rel)?;
let img = image::load_from_memory(&bytes)
.map_err(|e| Error::Config(format!("{rel} is not a readable image: {e}")))?;
let (width, height) = (img.width(), img.height());
let luma = img.to_luma8().into_raw();
let Ok(first) = rxing::helpers::detect_in_luma(luma.clone(), width, height, None) else {
return Ok(Vec::new());
};
match rxing::helpers::detect_multiple_in_luma(luma, width, height) {
Ok(all) if !all.is_empty() => Ok(all.iter().map(Decoded::from).collect()),
_ => Ok(vec![Decoded::from(&first)]),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::Policy;
use rxing::{BarcodeFormat, Writer};
fn dir() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn guarded(root: &std::path::Path) -> Workspace {
Workspace::with_policy(
root,
Policy::default()
.layer("base")
.allow_read("*")
.allow_write("*")
.deny_read("secrets/*"),
)
}
fn symbol(ws: &Workspace, rel: &str, text: &str, format: BarcodeFormat, w: i32, h: i32) {
let matrix = rxing::MultiFormatWriter
.encode(text, &format, w, h)
.unwrap();
let img: image::DynamicImage = (&matrix).into();
let mut png = std::io::Cursor::new(Vec::new());
img.write_to(&mut png, image::ImageFormat::Png).unwrap();
ws.write_bytes(rel, &png.into_inner()).unwrap();
}
fn blank(ws: &Workspace, rel: &str) {
let img = image::DynamicImage::ImageLuma8(image::GrayImage::from_pixel(
240,
240,
image::Luma([255u8]),
));
let mut png = std::io::Cursor::new(Vec::new());
img.write_to(&mut png, image::ImageFormat::Png).unwrap();
ws.write_bytes(rel, &png.into_inner()).unwrap();
}
#[test]
fn a_qr_code_decodes_to_the_payload_it_was_written_with() {
let d = dir();
let ws = Workspace::new(d.path());
symbol(
&ws,
"qr.png",
"io-harness 0.14.0",
BarcodeFormat::QR_CODE,
300,
300,
);
assert_eq!(
decode(&ws, "qr.png").unwrap(),
vec![Decoded {
text: "io-harness 0.14.0".to_string(),
format: "QR_CODE".to_string(),
}]
);
}
#[test]
fn a_one_dimensional_symbology_decodes_to_the_payload_it_was_written_with() {
let d = dir();
let ws = Workspace::new(d.path());
symbol(
&ws,
"c128.png",
"IO-HARNESS-0140",
BarcodeFormat::CODE_128,
400,
120,
);
let found = decode(&ws, "c128.png").unwrap();
assert_eq!(
found,
vec![Decoded {
text: "IO-HARNESS-0140".to_string(),
format: "CODE_128".to_string(),
}],
"got {found:?}"
);
}
#[test]
fn an_image_with_no_barcode_in_it_returns_an_empty_vec_not_an_error() {
let d = dir();
let ws = Workspace::new(d.path());
blank(&ws, "blank.png");
let found = decode(&ws, "blank.png");
assert!(
matches!(&found, Ok(v) if v.is_empty()),
"an empty image is an observation the model can act on, \
so this is Ok(vec![]) and not an Err: got {found:?}"
);
}
#[test]
fn a_file_that_is_not_an_image_is_an_error_not_a_panic() {
let d = dir();
let ws = Workspace::new(d.path());
ws.write_bytes("notes.png", b"this is plainly not a PNG")
.unwrap();
let shown = decode(&ws, "notes.png").unwrap_err().to_string();
assert!(
shown.contains("notes.png") && shown.contains("not a readable image"),
"the message names the file and what is wrong with it, got {shown}"
);
assert!(decode(&ws, "gone.png")
.unwrap_err()
.to_string()
.contains("no such file"));
}
#[test]
fn a_denied_path_is_refused_before_a_pixel_is_decoded() {
let d = dir();
symbol(
&Workspace::new(d.path()),
"secrets/badge.png",
"employee-4471",
BarcodeFormat::QR_CODE,
300,
300,
);
let ws = guarded(d.path());
let refused = decode(&ws, "secrets/badge.png");
assert!(
matches!(&refused, Err(Error::Refused { act, target, .. })
if act == "read" && target == "secrets/badge.png"),
"got {refused:?}"
);
}
#[test]
fn the_same_decode_succeeds_on_a_path_the_policy_allows() {
let d = dir();
let ws = guarded(d.path());
symbol(
&ws,
"open/badge.png",
"employee-4471",
BarcodeFormat::QR_CODE,
300,
300,
);
assert_eq!(
decode(&ws, "open/badge.png").unwrap(),
vec![Decoded {
text: "employee-4471".to_string(),
format: "QR_CODE".to_string(),
}]
);
}
}