use crate::error::{Result, SpliceError};
use sha2::{Digest, Sha256};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumAlgorithm {
Sha256,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Checksum {
pub value: String,
pub algorithm: ChecksumAlgorithm,
pub size: usize,
}
impl Checksum {
pub fn new(value: String, algorithm: ChecksumAlgorithm, size: usize) -> Self {
Self {
value,
algorithm,
size,
}
}
pub fn as_hex(&self) -> &str {
&self.value
}
}
pub fn checksum_file(path: &Path) -> Result<Checksum> {
let contents = std::fs::read(path).map_err(|e| SpliceError::IoContext {
context: format!("Failed to read file for checksum: {}", path.display()),
source: e,
})?;
let size = contents.len();
let mut hasher = Sha256::new();
hasher.update(&contents);
let result = hasher.finalize();
let value = format!("{:x}", result);
Ok(Checksum::new(value, ChecksumAlgorithm::Sha256, size))
}
pub fn checksum_span(path: &Path, start: usize, end: usize) -> Result<Checksum> {
let contents = std::fs::read(path).map_err(|e| SpliceError::IoContext {
context: format!("Failed to read file for span checksum: {}", path.display()),
source: e,
})?;
if start > end || end > contents.len() {
return Err(SpliceError::InvalidSpan {
file: path.to_path_buf(),
start,
end,
file_size: contents.len(),
});
}
let span = &contents[start..end];
let size = span.len();
let mut hasher = Sha256::new();
hasher.update(span);
let result = hasher.finalize();
let value = format!("{:x}", result);
Ok(Checksum::new(value, ChecksumAlgorithm::Sha256, size))
}
pub fn checksum_line_range(path: &Path, line_start: usize, line_end: usize) -> Result<Checksum> {
let contents = std::fs::read(path).map_err(|e| SpliceError::IoContext {
context: format!(
"Failed to read file for line range checksum: {}",
path.display()
),
source: e,
})?;
let text = std::str::from_utf8(&contents).map_err(|e| SpliceError::InvalidUtf8 {
file: path.to_path_buf(),
source: e,
})?;
let lines: Vec<&str> = text.lines().collect();
if line_start < 1 || line_start > line_end || line_end > lines.len() {
return Err(SpliceError::InvalidLineRange {
file: path.to_path_buf(),
line_start,
line_end,
total_lines: lines.len(),
});
}
let range_text = lines[line_start - 1..line_end].join("\n");
let size = range_text.len();
let mut hasher = Sha256::new();
hasher.update(range_text.as_bytes());
let result = hasher.finalize();
let value = format!("{:x}", result);
Ok(Checksum::new(value, ChecksumAlgorithm::Sha256, size))
}
pub fn verify_file(path: &Path, expected: &Checksum) -> Result<bool> {
let actual = checksum_file(path)?;
Ok(actual.value == expected.value)
}
pub fn has_file_changed(path: &Path, expected_checksum: &str) -> Result<bool> {
let actual = checksum_file(path)?;
Ok(actual.as_hex() != expected_checksum)
}
pub fn checksum_diff(path: &Path, changes: &[(usize, usize, &str)]) -> Result<Checksum> {
let contents = std::fs::read(path).map_err(|e| SpliceError::IoContext {
context: format!("Failed to read file for diff checksum: {}", path.display()),
source: e,
})?;
let file_size = contents.len();
for (start, end, _) in changes {
if *start > *end || *end > file_size {
return Err(SpliceError::InvalidSpan {
file: path.to_path_buf(),
start: *start,
end: *end,
file_size,
});
}
}
let diff_content: String = changes
.iter()
.map(|(_, _, replacement)| *replacement)
.collect();
let size = diff_content.len();
let mut hasher = Sha256::new();
hasher.update(diff_content.as_bytes());
let result = hasher.finalize();
let value = format!("{:x}", result);
Ok(Checksum::new(value, ChecksumAlgorithm::Sha256, size))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_checksum_file_consistent() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"Hello, World!").unwrap();
let checksum1 = checksum_file(file.path()).unwrap();
let checksum2 = checksum_file(file.path()).unwrap();
assert_eq!(checksum1.value, checksum2.value);
assert_eq!(checksum1.size, 13);
}
#[test]
fn test_checksum_span() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
let checksum = checksum_span(file.path(), 2, 7).unwrap();
assert_eq!(checksum.size, 5);
let full_checksum = checksum_file(file.path()).unwrap();
assert_ne!(checksum.value, full_checksum.value);
}
#[test]
fn test_checksum_line_range() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line1").unwrap();
writeln!(file, "line2").unwrap();
writeln!(file, "line3").unwrap();
let checksum = checksum_line_range(file.path(), 1, 2).unwrap();
assert_eq!(checksum.size, 11);
let checksum2 = checksum_line_range(file.path(), 2, 3).unwrap();
assert_ne!(checksum.value, checksum2.value);
}
#[test]
fn test_verify_file_mismatch() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"replaced content").unwrap();
let checksum = checksum_file(file.path()).unwrap();
file.write_all(b"modified content").unwrap();
let matches = verify_file(file.path(), &checksum).unwrap();
assert!(!matches);
}
#[test]
fn test_verify_file_match() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"unchanged content").unwrap();
let checksum = checksum_file(file.path()).unwrap();
let matches = verify_file(file.path(), &checksum).unwrap();
assert!(matches);
}
#[test]
fn test_has_file_changed() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"initial content").unwrap();
let checksum = checksum_file(file.path()).unwrap();
let changed = has_file_changed(file.path(), checksum.as_hex()).unwrap();
assert!(!changed);
file.write_all(b"new content").unwrap();
let changed = has_file_changed(file.path(), checksum.as_hex()).unwrap();
assert!(changed);
}
#[test]
fn test_checksum_span_invalid_bounds() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
assert!(checksum_span(file.path(), 7, 2).is_err());
assert!(checksum_span(file.path(), 5, 20).is_err());
}
#[test]
fn test_checksum_line_range_invalid_bounds() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line1").unwrap();
writeln!(file, "line2").unwrap();
assert!(checksum_line_range(file.path(), 0, 1).is_err());
assert!(checksum_line_range(file.path(), 1, 5).is_err());
assert!(checksum_line_range(file.path(), 2, 1).is_err());
}
#[test]
fn test_checksum_diff_single_change() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
let changes = vec![(2, 5, "abc")];
let checksum = checksum_diff(file.path(), &changes).unwrap();
assert_eq!(checksum.size, 3); assert_eq!(checksum.algorithm, ChecksumAlgorithm::Sha256);
}
#[test]
fn test_checksum_diff_multiple_changes() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789abcdefghijklmn").unwrap();
let changes = vec![(2, 5, "abc"), (10, 15, "xyz")];
let checksum = checksum_diff(file.path(), &changes).unwrap();
assert_eq!(checksum.size, 6); }
#[test]
fn test_checksum_diff_same_content_same_checksum() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
let changes1 = vec![(2, 5, "abc")];
let changes2 = vec![(2, 5, "abc")];
let checksum1 = checksum_diff(file.path(), &changes1).unwrap();
let checksum2 = checksum_diff(file.path(), &changes2).unwrap();
assert_eq!(checksum1.value, checksum2.value);
}
#[test]
fn test_checksum_diff_different_content_different_checksum() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
let changes1 = vec![(2, 5, "abc")];
let changes2 = vec![(2, 5, "def")];
let checksum1 = checksum_diff(file.path(), &changes1).unwrap();
let checksum2 = checksum_diff(file.path(), &changes2).unwrap();
assert_ne!(checksum1.value, checksum2.value);
}
#[test]
fn test_checksum_diff_invalid_bounds() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"0123456789").unwrap();
let changes = vec![(5, 2, "abc")];
assert!(checksum_diff(file.path(), &changes).is_err());
let changes = vec![(5, 20, "abc")];
assert!(checksum_diff(file.path(), &changes).is_err());
}
}