use crate::{ConcurrencyExt, Dimension, DimensionError, FileInfo, WallSwitchResult};
use blake3::Hasher;
use image::ImageReader;
use std::{
fs::File,
io::{BufReader, Error, Read},
path::PathBuf,
thread,
};
const BUFFER_SIZE: usize = 64 * 1024;
pub fn probe_image_dimension(path: &PathBuf) -> WallSwitchResult<Dimension> {
let reader = ImageReader::open(path)?.with_guessed_format()?;
let (width, height) = reader
.into_dimensions()
.map_err(|err| DimensionError::ReadFailed {
path: path.clone(),
source: err,
})?;
Ok(Dimension {
width: width as u64,
height: height as u64,
})
}
fn compute_single_hash(file_info: &mut FileInfo) -> WallSwitchResult<()> {
let file = File::open(&file_info.path)?;
let reader = BufReader::with_capacity(BUFFER_SIZE, file);
let hash = get_hash(reader).map_err(|err| Error::other(err.to_string()))?;
file_info.hash = hash;
Ok(())
}
pub fn compute_hashes_parallel(files: &mut [FileInfo]) {
let chunk_size = files.get_chunk_size(files.len());
thread::scope(|scope| {
for chunk in files.chunks_mut(chunk_size) {
scope.spawn(move || {
for file_info in chunk {
if let Err(err) = compute_single_hash(file_info) {
eprintln!(
"Failed to compute BLAKE3 hash for '{}': {}",
file_info.path.display(),
err
);
}
}
});
}
});
}
pub fn get_hash(mut reader: impl Read) -> WallSwitchResult<String> {
let mut hasher = Hasher::new();
let mut buffer = [0_u8; BUFFER_SIZE];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
Ok(hasher.finalize().to_hex().to_string())
}
#[cfg(test)]
mod tests_metadata {
use super::*;
use image::{ImageFormat, RgbImage};
use std::{env, fs};
#[test]
fn test_probe_image_dimension_supported_formats() {
let formats_to_test = vec![
(ImageFormat::Png, "png"),
(ImageFormat::Jpeg, "jpg"),
(ImageFormat::WebP, "webp"),
(ImageFormat::Tiff, "tif"),
];
let temp_dir = env::temp_dir();
let expected_width = 120;
let expected_height = 80;
for (format, ext) in formats_to_test {
let file_path = temp_dir.join(format!("wallswitch_test_probe.{ext}"));
let img = RgbImage::new(expected_width, expected_height);
if let Err(err) = img.save_with_format(&file_path, format) {
eprintln!(
"Skipping format {:?}: encoder failed or missing feature. Details: {}",
format, err
);
continue;
}
match probe_image_dimension(&file_path) {
Ok(dim) => assert_eq!(
dim,
Dimension {
width: expected_width as u64,
height: expected_height as u64,
}
),
Err(err) => panic!("Expected Ok for format {format:?}, but got error: {err:?}"),
}
let _ = fs::remove_file(file_path);
}
}
#[test]
fn test_probe_image_dimension_invalid_file() {
let temp_dir = env::temp_dir();
let file_path = temp_dir.join("wallswitch_invalid_test.png");
let magic_bytes_only = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
fs::write(&file_path, magic_bytes_only).unwrap();
let result = probe_image_dimension(&file_path);
assert!(
result.is_err(),
"Expected dimension probing to fail on incomplete header file"
);
let _ = fs::remove_file(file_path);
}
}