use std::fs::File;
use std::path::Path;
use std::time::UNIX_EPOCH;
use blake3::Hasher;
use memmap2::Mmap;
use crate::error::HoldError;
pub fn hash_file(path: &Path) -> Result<String, HoldError> {
let metadata = checked_metadata(path)?;
if metadata.len() == 0 {
let hasher = Hasher::new();
return Ok(hasher.finalize().to_hex().to_string());
}
let file = File::open(path).map_err(|source| HoldError::IoError {
path: path.to_path_buf(),
source,
})?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|source| HoldError::IoError {
path: path.to_path_buf(),
source,
})?;
let mut hasher = Hasher::new();
hasher.update_rayon(&mmap);
Ok(hasher.finalize().to_hex().to_string())
}
pub fn get_file_size(path: &Path) -> Result<u64, HoldError> {
Ok(checked_metadata(path)?.len())
}
pub fn get_file_mtime_nanos(path: &Path) -> Result<u128, HoldError> {
let metadata = checked_metadata(path)?;
let mtime = metadata.modified().map_err(|source| HoldError::IoError {
path: path.to_path_buf(),
source,
})?;
let nanos = mtime
.duration_since(UNIX_EPOCH)
.map_err(|_| HoldError::IoError {
path: path.to_path_buf(),
source: std::io::Error::other("System time is before UNIX epoch"),
})?
.as_nanos();
Ok(nanos)
}
fn checked_metadata(path: &Path) -> Result<std::fs::Metadata, HoldError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| HoldError::IoError {
path: path.to_path_buf(),
source,
})?;
if metadata.is_symlink() {
return Err(HoldError::InvalidFileType(
path.to_path_buf(),
"Symbolic links are not supported".to_string(),
));
}
if metadata.is_dir() {
return Err(HoldError::InvalidFileType(
path.to_path_buf(),
"Directories are not supported".to_string(),
));
}
Ok(metadata)
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
#[test]
fn test_hash_file() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "hello world").unwrap();
let hash = hash_file(&test_file).unwrap();
assert_eq!(
hash,
"d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24"
);
}
#[test]
fn test_hash_empty_file() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("empty.txt");
fs::write(&test_file, "").unwrap();
let hash = hash_file(&test_file).unwrap();
assert_eq!(
hash,
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
}
#[test]
fn test_get_file_size() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("sized.txt");
let content = "hello world";
fs::write(&test_file, content).unwrap();
let size = get_file_size(&test_file).unwrap();
assert_eq!(size, content.len() as u64);
}
#[test]
fn test_hash_nonexistent_file() {
let result = hash_file(Path::new("/nonexistent/file"));
assert!(matches!(result, Err(HoldError::IoError { .. })));
}
#[test]
#[cfg(unix)]
fn test_hash_symlink() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let target = temp_dir.path().join("target.txt");
let link = temp_dir.path().join("link.txt");
fs::write(&target, "content").unwrap();
symlink(&target, &link).unwrap();
let result = hash_file(&link);
assert!(matches!(result, Err(HoldError::InvalidFileType { .. })));
}
#[test]
#[cfg(unix)]
fn test_get_file_size_symlink() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let target = temp_dir.path().join("target.txt");
let link = temp_dir.path().join("link.txt");
fs::write(&target, "content").unwrap();
symlink(&target, &link).unwrap();
let result = get_file_size(&link);
assert!(matches!(result, Err(HoldError::InvalidFileType { .. })));
}
}