use crate::{ConcurrencyExt, Dimension, FileInfo, WallSwitchResult, exec_cmd};
use blake3::Hasher;
use image::image_dimensions;
use std::{
fs::File,
io::{BufReader, Read},
path::PathBuf,
process::Command,
thread,
};
const BUFFER_SIZE: usize = 64 * 1024;
pub fn probe_image_dimension(path: &PathBuf, verbose: bool) -> WallSwitchResult<Dimension> {
match image_dimensions(path) {
Ok((width, height)) => Ok(Dimension {
width: width as u64,
height: height as u64,
}),
Err(_err) => {
probe_image_dimension_fallback(path, verbose)
}
}
}
pub fn probe_image_dimension_fallback(
path: &PathBuf,
verbose: bool,
) -> WallSwitchResult<Dimension> {
let mut cmd = Command::new("identify");
let identify_cmd = cmd
.arg("-format")
.arg("%wx%h") .arg(path);
let identify_out = exec_cmd(identify_cmd, verbose, "identify")?;
let std_output = String::from_utf8(identify_out.stdout)?;
Dimension::new(&std_output)
}
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 Ok(file) = File::open(&file_info.path) {
let reader = BufReader::with_capacity(BUFFER_SIZE, file);
if let Ok(hash) = get_hash(reader) {
file_info.hash = hash;
}
}
}
});
}
});
}
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 std::fs;
#[test]
fn test_get_hash_in_memory() {
let data = b"hello wallswitch";
let expected_hash = "5cde4798fe09c816b40570b6a00f62a7149de218a3f2ab37e69f761d01d038e5";
let result = get_hash(&data[..]);
assert!(
result.is_ok(),
"Hashing should not fail on valid memory reads"
);
assert_eq!(result.unwrap(), expected_hash);
}
#[test]
fn test_compute_hashes_parallel() {
let temp_dir = std::env::temp_dir();
let file1_path = temp_dir.join("wallswitch_test_hash_1.txt");
let file2_path = temp_dir.join("wallswitch_test_hash_2.txt");
fs::write(&file1_path, b"content A").expect("Failed to write temp file 1");
fs::write(&file2_path, b"content B").expect("Failed to write temp file 2");
let mut files = vec![
FileInfo {
path: file1_path.clone(),
..Default::default()
},
FileInfo {
path: file2_path.clone(),
..Default::default()
},
];
assert!(files[0].hash.is_empty());
compute_hashes_parallel(&mut files);
let expected_hash_1 = "4f081fea7fb11c55156d6d6e7f44fa43c956a987c1e1d43d2f8cdeb2162dd5e2";
let expected_hash_2 = "0ececda959d7107819dfa5c37d52db957aa075149cdebd4622d96e0160813ff3";
assert!(
!files[0].hash.is_empty(),
"Hash for file 1 should be populated"
);
assert!(
!files[1].hash.is_empty(),
"Hash for file 2 should be populated"
);
assert_ne!(
files[0].hash, files[1].hash,
"Different contents must produce different hashes"
);
assert_eq!(files[0].hash, expected_hash_1);
assert_eq!(files[1].hash, expected_hash_2);
let _ = fs::remove_file(file1_path);
let _ = fs::remove_file(file2_path);
}
#[test]
fn test_probe_image_dimension() {
if Command::new("identify").arg("-version").output().is_err() {
println!("Skipping test: ImageMagick 'identify' command not found.");
return;
}
let ppm_data = "P3\n2 2\n255\n255 0 0 0 255 0\n0 0 255 255 255 255\n";
let temp_dir = std::env::temp_dir();
let img_path = temp_dir.join("wallswitch_test_image.ppm");
fs::write(&img_path, ppm_data).expect("Failed to write temp image file");
let result = probe_image_dimension(&img_path, false);
assert!(
result.is_ok(),
"ImageMagick failed to probe the test image. Result: {:?}",
result
);
let dim = result.unwrap();
assert_eq!(dim.width, 2, "Expected width to be 2");
assert_eq!(dim.height, 2, "Expected height to be 2");
let _ = fs::remove_file(img_path);
}
}