use crate::Error;
use crate::FileError;
use crate::StorageError;
use crate::SystemError;
use crate::file_io;
use crate::file_io::convert_vec_checksum;
use crate::file_io::create_parent_dir_if_not_exist;
use crate::file_io::delete_file;
use nix::libc::LOCK_EX;
use nix::libc::flock;
use sha2::Digest;
use sha2::Sha256;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
use tempfile::tempdir;
use tokio::fs;
use tracing_test::traced_test;
#[tokio::test]
#[traced_test]
async fn test_create_parent_dir_for_file() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path().join("test_create_parent_dir_for_file");
let file_path = temp_path.join("files").join("data.txt");
create_parent_dir_if_not_exist(&file_path).unwrap();
let parent_dir = file_path.parent().unwrap();
assert!(file_io::is_dir(parent_dir).await.unwrap());
assert!(parent_dir.exists());
assert!(!file_path.exists());
}
#[tokio::test]
#[traced_test]
async fn test_create_parent_dir_for_directory_without_trailing_separator() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir
.path()
.join("test_create_parent_dir_for_directory_without_trailing_separator");
let dir_path = temp_path.join("dir").join("subdir");
create_parent_dir_if_not_exist(&dir_path).unwrap();
let parent_dir = dir_path.parent().unwrap();
assert!(parent_dir.exists());
assert!(file_io::is_dir(parent_dir).await.unwrap());
}
#[tokio::test]
#[traced_test]
async fn test_create_parent_dir_for_directory_with_trailing_separator() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir
.path()
.join("test_create_parent_dir_for_directory_with_trailing_separator");
let dir_path = temp_path.join("dir").join("subdir").join(""); create_parent_dir_if_not_exist(&dir_path).unwrap();
assert!(dir_path.exists());
assert!(file_io::is_dir(&dir_path).await.unwrap());
}
#[tokio::test]
#[traced_test]
async fn test_delete_file_success() {
let mut file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
writeln!(file, "test content").unwrap();
let result = delete_file(&path).await;
assert!(result.is_ok(), "Should successfully delete file");
assert!(!path.exists(), "File should be deleted");
}
#[tokio::test]
#[traced_test]
async fn test_delete_nonexistent_file() {
let e = delete_file("nonexistent.txt").await.unwrap_err();
assert!(
matches!(
e,
Error::System(SystemError::Storage(StorageError::File(
FileError::NotFound(_)
)))
),
"Should return NotFound error"
);
}
#[tokio::test]
#[traced_test]
async fn test_delete_directory() {
let dir = tempdir().unwrap();
let dir_path = dir.path().to_owned();
let e = delete_file(&dir_path).await.unwrap_err();
assert!(
matches!(
e,
Error::System(SystemError::Storage(StorageError::File(
FileError::IsDirectory(_)
)))
),
"Should return IsDirectory error"
);
}
#[tokio::test]
#[traced_test]
async fn test_delete_busy_file() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path().join("test_delete_busy_file");
let dir_path = temp_path.to_owned();
tokio::fs::create_dir_all(&dir_path).await.unwrap();
let file_path = dir_path.join("test_file.txt");
#[cfg(windows)]
{
let _file_handle =
std::fs::OpenOptions::new().write(true).create(true).open(&file_path).unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o644) .open(&file_path)
.unwrap();
unsafe {
flock(file.as_raw_fd(), LOCK_EX);
}
let _file_handle = file;
}
let e = delete_file(&file_path).await;
#[cfg(unix)]
assert!(e.is_ok());
#[cfg(windows)]
assert!(e.is_err());
}
#[tokio::test]
#[cfg(unix)] async fn test_delete_permission_denied() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path().join("test_delete_permission_denied");
let dir_path = temp_path.to_owned();
tokio::fs::create_dir_all(&dir_path).await.unwrap();
let file_path = dir_path.join("test_file.txt");
fs::write(&file_path, b"test").await.unwrap();
let mut perms = fs::metadata(&dir_path).await.unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(&dir_path, perms.clone()).await.unwrap();
let e = delete_file(&file_path).await.unwrap_err();
println!("{:?}", &e);
assert!(
matches!(
e,
Error::System(SystemError::Storage(StorageError::File(
FileError::PermissionDenied(_)
)))
),
"Should return PermissionDenied error"
);
perms.set_mode(0o700);
fs::set_permissions(&dir_path, perms).await.unwrap();
}
#[test]
fn test_convert_vec_checksum_converts_valid_checksum() {
let input = vec![1; 32];
let result = convert_vec_checksum(input).unwrap();
assert_eq!(result, [1; 32]);
}
#[test]
fn test_convert_vec_checksum_rejects_short_checksum() {
let input = vec![0; 31];
let result = convert_vec_checksum(input);
assert!(result.is_err());
}
#[test]
fn test_convert_vec_checksum_rejects_long_checksum() {
let input = vec![0; 33];
let result = convert_vec_checksum(input);
assert!(result.is_err());
}
#[test]
fn test_convert_vec_checksum_rejects_empty_checksum() {
let input = vec![];
let result = convert_vec_checksum(input);
assert!(result.is_err());
}
#[test]
fn test_convert_vec_checksum_preserves_byte_order() {
let mut input = vec![0; 32];
input[31] = 0xFF;
let result = convert_vec_checksum(input).unwrap();
assert_eq!(result[31], 0xFF);
assert_eq!(result[0], 0x00);
}
#[cfg(test)]
mod validate_compressed_format_tests {
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
use tracing::trace;
use super::*;
use crate::Result;
use crate::file_io::validate_compressed_format;
#[tokio::test]
async fn valid_compressed_files() -> Result<()> {
let test_cases = &[
("valid.tar.gz", [0x1f, 0x8b]),
("archive.tgz", [0x1f, 0x8b]),
("data.snap", [0x1f, 0x8b]),
];
for (filename, header) in test_cases {
let dir = tempdir().unwrap();
let path = dir.path().join(filename);
let mut file = File::create(&path).unwrap();
file.write_all(header).unwrap();
file.write_all(b"dummy content").unwrap();
let result = validate_compressed_format(&path);
assert!(result.is_ok(), "Failed case: {filename}");
}
Ok(())
}
#[tokio::test]
async fn invalid_extensions() {
let cases = vec![
("/tmp/text.zip", "zip"),
("/tmp/data.rar", "rar"),
("/tmp/no_extension", ""),
];
for (filename, _expected_ext) in cases {
let dir = tempdir().unwrap();
let path = dir.path().join(filename);
let mut file = File::create(&path).unwrap();
file.write_all(b"dummy content").unwrap();
let result = validate_compressed_format(&path);
trace!("{result:?}",);
assert!(
matches!(
result,
Err(Error::System(SystemError::Storage(StorageError::File(FileError::InvalidExt(msg)))))
if msg.contains("Invalid compression extension") || msg.contains("Invalid file extension")
),
"Failed case: {filename}",
);
}
}
#[tokio::test]
async fn invalid_magic_numbers() -> Result<()> {
let dir = tempdir().unwrap();
let path = dir.path().join("invalid.gz");
let temp_dir = tempdir().unwrap();
let temp_path = temp_dir.path().join("temp_invalid.gz");
{
let mut file = File::create(&temp_path).unwrap();
for _ in 1..=10 {
file.write_all(&[0x89, 0x50]).unwrap(); }
}
tokio::fs::copy(&temp_path, &path).await.unwrap();
let result = validate_compressed_format(&path);
trace!("{result:?}",);
assert!(matches!(
result,
Err(Error::System(SystemError::Storage(StorageError::File(FileError::InvalidGzipHeader(msg)))))
if msg.contains("Invalid GZIP header")
));
Ok(())
}
#[test]
fn empty_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("empty.gz");
File::create(&path).unwrap(); let result = validate_compressed_format(&path);
trace!("{result:?}",);
assert!(matches!(
result,
Err(Error::System(SystemError::Storage(StorageError::File(
FileError::TooSmall(_)
))))
));
}
}
#[cfg(test)]
mod compute_checksum_from_file_path_tests {
use super::*;
use crate::file_io::compute_checksum_from_file_path;
#[tokio::test]
async fn test_compute_checksum_from_path_empty_file() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("empty.txt");
tokio::fs::write(&file_path, b"").await.unwrap();
let checksum = compute_checksum_from_file_path(&file_path)
.await
.expect("Should compute checksum for empty file");
let hasher = Sha256::new();
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(
checksum, expected,
"Checksum for empty file should be SHA-256 of empty data"
);
}
#[tokio::test]
async fn test_compute_checksum_from_path_small_file() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
tokio::fs::write(&file_path, b"Hello, world!").await.unwrap();
let checksum = compute_checksum_from_file_path(&file_path)
.await
.expect("Should compute checksum for file");
let mut hasher = Sha256::new();
hasher.update(b"Hello, world!");
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(
checksum, expected,
"Checksum should match SHA-256 of file content"
);
}
#[tokio::test]
async fn test_compute_checksum_from_path_large_file() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("large.bin");
let data: Vec<u8> = (0..5 * 1024 * 1024).map(|_| rand::random::<u8>()).collect();
tokio::fs::write(&file_path, &data).await.unwrap();
let checksum = compute_checksum_from_file_path(&file_path)
.await
.expect("Should compute checksum for large file");
let mut hasher = Sha256::new();
hasher.update(&data);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(
checksum, expected,
"Checksum should match SHA-256 of large file content"
);
}
#[tokio::test]
async fn test_compute_checksum_nonexistent_file() {
let temp_dir = tempdir().unwrap();
let non_existent_path = temp_dir.path().join("does_not_exist.txt");
let result = compute_checksum_from_file_path(&non_existent_path).await;
assert!(result.is_err(), "Should return error for non-existent file");
match result.unwrap_err() {
Error::System(SystemError::Storage(StorageError::IoError(_))) => {} other => panic!("Expected IoError, got {other:?}"),
}
}
#[tokio::test]
async fn test_compute_checksum_consistency() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("data.bin");
tokio::fs::write(&file_path, b"Consistent data").await.unwrap();
let checksum1 = compute_checksum_from_file_path(&file_path)
.await
.expect("First computation should succeed");
let checksum2 = compute_checksum_from_file_path(&file_path)
.await
.expect("Second computation should succeed");
assert_eq!(
checksum1, checksum2,
"Checksum should be consistent across multiple computations"
);
}
#[tokio::test]
async fn test_compute_checksum_content_change() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("data.txt");
tokio::fs::write(&file_path, b"Version 1").await.unwrap();
let checksum1 = compute_checksum_from_file_path(&file_path)
.await
.expect("First computation should succeed");
tokio::fs::write(&file_path, b"Version 2").await.unwrap();
let checksum2 = compute_checksum_from_file_path(&file_path)
.await
.expect("Second computation should succeed");
assert_ne!(
checksum1, checksum2,
"Checksum should change when file content changes"
);
}
}