use std::path::Path;
use crate::hash::git_sha256::compute_git_sha256_from_reader;
pub async fn compute_file_git_sha256(filepath: impl AsRef<Path>) -> Result<String, std::io::Error> {
let filepath = filepath.as_ref();
let file = tokio::fs::File::open(filepath).await?;
let metadata = file.metadata().await?;
if !metadata.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("git sha256: {} is not a regular file", filepath.display()),
));
}
let file_size = metadata.len();
let reader = tokio::io::BufReader::new(file);
compute_git_sha256_from_reader(file_size, reader).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::git_sha256::compute_git_sha256_from_bytes;
#[tokio::test]
async fn test_compute_file_git_sha256_matches_bytes() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let content = b"Hello, World!";
tokio::fs::write(&file_path, content).await.unwrap();
let file_hash = compute_file_git_sha256(&file_path).await.unwrap();
let bytes_hash = compute_git_sha256_from_bytes(content);
assert_eq!(file_hash, bytes_hash);
}
#[tokio::test]
async fn test_compute_file_git_sha256_empty_file() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("empty.txt");
tokio::fs::write(&file_path, b"").await.unwrap();
let file_hash = compute_file_git_sha256(&file_path).await.unwrap();
let bytes_hash = compute_git_sha256_from_bytes(b"");
assert_eq!(file_hash, bytes_hash);
}
#[tokio::test]
async fn test_compute_file_git_sha256_not_found() {
let result = compute_file_git_sha256("/nonexistent/file.txt").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_compute_file_git_sha256_large_content() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("large.bin");
let content: Vec<u8> = (0..20000).map(|i| (i % 256) as u8).collect();
tokio::fs::write(&file_path, &content).await.unwrap();
let file_hash = compute_file_git_sha256(&file_path).await.unwrap();
let bytes_hash = compute_git_sha256_from_bytes(&content);
assert_eq!(file_hash, bytes_hash);
}
#[tokio::test]
async fn test_compute_file_git_sha256_rejects_directory() {
let dir = tempfile::tempdir().unwrap();
let result = compute_file_git_sha256(dir.path()).await;
let err = result.expect_err("hashing a directory must error");
#[cfg(not(windows))]
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
let empty_blob = compute_git_sha256_from_bytes(b"");
assert_ne!(
err.to_string(),
empty_blob,
"directory should error, never produce the empty-blob hash"
);
}
#[cfg(unix)]
#[tokio::test]
async fn test_compute_file_git_sha256_follows_symlink_to_file() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.txt");
let link = dir.path().join("link.txt");
let content = b"symlinked content";
tokio::fs::write(&target, content).await.unwrap();
tokio::fs::symlink(&target, &link).await.unwrap();
let link_hash = compute_file_git_sha256(&link).await.unwrap();
let bytes_hash = compute_git_sha256_from_bytes(content);
assert_eq!(link_hash, bytes_hash);
}
#[cfg(unix)]
#[tokio::test]
async fn test_compute_file_git_sha256_rejects_symlink_to_directory() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("subdir");
let link = dir.path().join("dirlink");
tokio::fs::create_dir(&subdir).await.unwrap();
tokio::fs::symlink(&subdir, &link).await.unwrap();
let result = compute_file_git_sha256(&link).await;
let err = result.expect_err("symlink to a directory must error");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(unix)]
#[tokio::test]
async fn test_compute_file_git_sha256_broken_symlink_errors() {
let dir = tempfile::tempdir().unwrap();
let link = dir.path().join("dangling");
tokio::fs::symlink(dir.path().join("does-not-exist"), &link)
.await
.unwrap();
let result = compute_file_git_sha256(&link).await;
assert!(result.is_err(), "dangling symlink must error");
}
}