use crate::algorithm::Algorithm;
use crate::hash::FileHashResult;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;
pub const DEFAULT_SECTOR_SIZE: usize = 512;
const READ_BUF_SIZE: usize = 1024 * 1024;
pub fn is_device_path(path: &Path) -> bool {
let s = path.to_string_lossy();
s.starts_with("/dev/") || s.starts_with("\\\\.\\")
}
#[cfg(target_os = "linux")]
pub fn detect_sector_size(path: &Path) -> usize {
use std::os::unix::io::AsRawFd;
const BLKSSZGET: libc::c_ulong = 0x1268;
if let Ok(file) = std::fs::File::open(path) {
let mut sector_size: libc::c_int = 0;
let ret = unsafe { libc::ioctl(file.as_raw_fd(), BLKSSZGET, &mut sector_size as *mut _) };
if ret == 0 && sector_size > 0 {
return sector_size as usize;
}
}
DEFAULT_SECTOR_SIZE
}
#[cfg(target_os = "macos")]
pub fn detect_sector_size(path: &Path) -> usize {
use std::os::unix::io::AsRawFd;
const DKIOCGETBLOCKSIZE: libc::c_ulong = 0x40046418;
if let Ok(file) = std::fs::File::open(path) {
let mut block_size: u32 = 0;
let ret = unsafe {
libc::ioctl(
file.as_raw_fd(),
DKIOCGETBLOCKSIZE,
&mut block_size as *mut _,
)
};
if ret == 0 && block_size > 0 {
return block_size as usize;
}
}
DEFAULT_SECTOR_SIZE
}
#[cfg(target_os = "windows")]
pub fn detect_sector_size(_path: &Path) -> usize {
DEFAULT_SECTOR_SIZE
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
pub fn detect_sector_size(_path: &Path) -> usize {
DEFAULT_SECTOR_SIZE
}
pub fn hash_device(
path: &Path,
algorithms: &[Algorithm],
sector_size: usize,
) -> Result<FileHashResult> {
let sector_size = if sector_size == 0 {
DEFAULT_SECTOR_SIZE
} else {
sector_size
};
let mut file = std::fs::File::open(path)
.with_context(|| format!("failed to open device/file {}", path.display()))?;
let buf_size = ((READ_BUF_SIZE / sector_size) * sector_size).max(sector_size);
let mut buf = vec![0u8; buf_size];
for a in algorithms {
if a.is_fuzzy() || a.needs_full_read() {
anyhow::bail!(
"algorithm {a} is not supported for device hashing (requires full file in memory)"
);
}
}
use digest::Digest as _;
enum DynH {
Blake3(Box<blake3::Hasher>),
Sha256(Box<sha2::Sha256>),
Sha512(Box<sha2::Sha512>),
Sha3_256(Box<sha3::Sha3_256>),
Sha1(Box<sha1::Sha1>),
Md5(Box<md5::Md5>),
Tiger(Box<tiger::Tiger>),
Whirlpool(Box<whirlpool::Whirlpool>),
}
impl DynH {
fn update(&mut self, data: &[u8]) {
match self {
DynH::Blake3(h) => {
h.update(data);
}
DynH::Sha256(h) => {
h.update(data);
}
DynH::Sha512(h) => {
h.update(data);
}
DynH::Sha3_256(h) => {
h.update(data);
}
DynH::Sha1(h) => {
h.update(data);
}
DynH::Md5(h) => {
h.update(data);
}
DynH::Tiger(h) => {
h.update(data);
}
DynH::Whirlpool(h) => {
h.update(data);
}
}
}
fn finalize_hex(self) -> String {
match self {
DynH::Blake3(h) => (*h).finalize().to_hex().to_string(),
DynH::Sha256(h) => hex::encode(h.finalize()),
DynH::Sha512(h) => hex::encode(h.finalize()),
DynH::Sha3_256(h) => hex::encode(h.finalize()),
DynH::Sha1(h) => hex::encode(h.finalize()),
DynH::Md5(h) => hex::encode(h.finalize()),
DynH::Tiger(h) => hex::encode(h.finalize()),
DynH::Whirlpool(h) => hex::encode(h.finalize()),
}
}
}
fn make_dynh(algo: Algorithm) -> Result<DynH> {
Ok(match algo {
Algorithm::Blake3 => DynH::Blake3(Box::new(blake3::Hasher::new())),
Algorithm::Sha256 => DynH::Sha256(Box::new(sha2::Sha256::new())),
Algorithm::Sha512 => DynH::Sha512(Box::new(sha2::Sha512::new())),
Algorithm::Sha3_256 => DynH::Sha3_256(Box::new(sha3::Sha3_256::new())),
Algorithm::Sha1 => DynH::Sha1(Box::new(sha1::Sha1::new())),
Algorithm::Md5 => DynH::Md5(Box::new(md5::Md5::new())),
Algorithm::Tiger => DynH::Tiger(Box::new(tiger::Tiger::new())),
Algorithm::Whirlpool => DynH::Whirlpool(Box::new(whirlpool::Whirlpool::new())),
_ => anyhow::bail!("unsupported algorithm for device hashing: {algo}"),
})
}
let mut hashers: Vec<(Algorithm, DynH)> = algorithms
.iter()
.map(|a| Ok((*a, make_dynh(*a)?)))
.collect::<Result<Vec<_>>>()?;
let mut total_bytes: u64 = 0;
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
for (_, h) in &mut hashers {
h.update(&buf[..n]);
}
total_bytes += n as u64;
}
let mut hashes = HashMap::new();
for (algo, h) in hashers {
hashes.insert(algo, h.finalize_hex());
}
Ok(FileHashResult {
path: path.to_path_buf(),
size: total_bytes,
hashes,
entropy: None,
#[cfg(feature = "yara")]
yara_matches: None,
#[cfg(feature = "yara")]
yara_enrichments: Vec::new(),
})
}