use std::path::Path;
use std::path::PathBuf;
use crate::Res;
use crate::io::remote::HostChecksums;
use crate::io::remote::HostConfig;
use crate::io::storage::Storage;
use crate::manifest::ManifestRow;
use crate::object_hash::Crc64Hash;
use crate::object_hash::Hash;
use crate::object_hash::ObjectHash;
use crate::object_hash::Sha256ChunkedHash;
use crate::object_hash::Sha256Hash;
pub async fn refresh_hash(
storage: &impl Storage,
path: &PathBuf,
row: ManifestRow,
) -> Res<Option<ManifestRow>> {
let file = storage.open_file(path).await?;
let file_metadata = file.metadata().await?;
let size = file_metadata.len();
let computed_hash = match &row.hash {
ObjectHash::Crc64(_) => Crc64Hash::from_reader(file, size).await?.into(),
ObjectHash::Sha256(_) => Sha256Hash::from_reader(file, size).await?.into(),
ObjectHash::Sha256Chunked(_) => Sha256ChunkedHash::from_reader(file, size).await?.into(),
};
Ok((computed_hash != row.hash).then(|| ManifestRow {
hash: computed_hash,
size,
..row
}))
}
pub async fn calculate_hash(
storage: &impl Storage,
path: &Path,
logical_key: &Path,
host_config: &HostConfig,
) -> Res<ManifestRow> {
let file = storage.open_file(path).await?;
let file_metadata = file.metadata().await?;
let size = file_metadata.len();
let hash = match host_config.checksums {
HostChecksums::Crc64 => Crc64Hash::from_reader(file, size).await?.into(),
HostChecksums::Sha256Chunked => Sha256ChunkedHash::from_reader(file, size).await?.into(),
};
Ok(ManifestRow {
logical_key: logical_key.to_path_buf(),
physical_key: format!("file://{}", path.display()),
size,
hash,
..ManifestRow::default()
})
}
pub async fn verify_hash(
storage: &impl Storage,
path: &PathBuf,
row: ManifestRow,
host_config: &HostConfig,
) -> Res<Option<ManifestRow>> {
if let Some(modified) = refresh_hash(storage, path, row).await? {
if modified.hash.algorithm() == host_config.checksums.algorithm_code() {
Ok(Some(modified))
} else {
let calculated_row =
calculate_hash(storage, path, &modified.logical_key, host_config).await?;
Ok(Some(calculated_row))
}
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use std::path::Path;
use aws_sdk_s3::primitives::ByteStream;
use multihash::Multihash;
use crate::Res;
use crate::io::storage::LocalStorage;
use crate::io::storage::Storage;
use crate::io::storage::StorageExt;
use crate::io::storage::mocks::MockStorage;
use crate::object_hash::MULTIHASH_CRC64_NVME;
use crate::object_hash::MULTIHASH_SHA256;
use crate::object_hash::MULTIHASH_SHA256_CHUNKED;
#[test(tokio::test)]
async fn test_refresh_hash_unchanged_file() -> Res {
let storage = MockStorage::default();
let file_content = b"anything";
let file_path = Path::new("foo");
storage
.write_byte_stream(file_path, ByteStream::from_static(file_content))
.await?;
let file = storage.open_file(file_path).await?;
let hash: Multihash<256> = Sha256Hash::from_reader(file, file_content.len() as u64)
.await?
.into();
let manifest_row = ManifestRow {
logical_key: PathBuf::from("bar"),
hash: hash.try_into()?,
size: file_content.len() as u64,
..ManifestRow::default()
};
let result = refresh_hash(&storage, &file_path.to_path_buf(), manifest_row).await?;
assert!(result.is_none(), "Unchanged file should return None");
Ok(())
}
#[test(tokio::test)]
async fn test_refresh_hash_changed_file() -> Res {
let storage = MockStorage::default();
let file_content = b"anything";
let file_path = Path::new("foo");
storage
.write_byte_stream(file_path, ByteStream::from_static(file_content))
.await?;
let file = storage.open_file(file_path).await?;
let hash: Multihash<256> = Sha256Hash::from_reader(file, file_content.len() as u64)
.await?
.into();
let wrong_hash = Multihash::wrap(MULTIHASH_SHA256, b"wrong_hash_data")?;
let test_manifest_row = ManifestRow {
logical_key: PathBuf::from("bar"),
hash: wrong_hash.try_into()?,
size: 999, ..ManifestRow::default()
};
let result = refresh_hash(&storage, &file_path.to_path_buf(), test_manifest_row).await?;
let refreshed_row = result.expect("Changed hash should return Some");
assert_eq!(refreshed_row.hash, hash.try_into()?);
assert_eq!(refreshed_row.size, file_content.len() as u64);
Ok(())
}
#[test(tokio::test)]
async fn test_refresh_hash_unknown_algorithm() -> Res {
let storage = MockStorage::default();
let file_content = b"anything";
let file_path = Path::new("foo");
storage
.write_byte_stream(file_path, ByteStream::from_static(file_content))
.await?;
let valid_hash = Crc64Hash::default().into();
let manifest_row = ManifestRow {
logical_key: PathBuf::from("bar"),
hash: valid_hash,
size: file_content.len() as u64,
..ManifestRow::default()
};
let result = refresh_hash(&storage, &file_path.to_path_buf(), manifest_row).await;
assert!(result.is_ok());
Ok(())
}
#[test(tokio::test)]
async fn test_calculate_hash_crc64() -> Res {
let storage = LocalStorage::default();
let file_path = Path::new("fixtures/user-settings.mkfg");
let host_config = HostConfig::default_crc64();
let logical_key = PathBuf::from("foo");
let row = calculate_hash(&storage, file_path, &logical_key, &host_config).await?;
assert_eq!(row.hash.algorithm(), MULTIHASH_CRC64_NVME);
assert_eq!(row.logical_key, logical_key);
assert_eq!(row.size, storage.read_bytes(file_path).await?.len() as u64);
assert_eq!(row.hash.to_string(), "LZmmpqbBItw=");
Ok(())
}
#[test(tokio::test)]
async fn test_calculate_hash_sha256_chunked() -> Res {
let storage = MockStorage::default();
let host_config = HostConfig::default_sha256_chunked();
let file_content = crate::fixtures::objects::less_than_8mb();
let file_path = Path::new("foo");
storage
.write_byte_stream(file_path, ByteStream::from_static(file_content))
.await?;
let logical_key = PathBuf::from("bar");
let row = calculate_hash(&storage, file_path, &logical_key, &host_config).await?;
assert_eq!(row.hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
assert_eq!(row.logical_key, logical_key);
assert_eq!(row.size, file_content.len() as u64);
assert_eq!(
row.hash.to_string(),
crate::fixtures::objects::LESS_THAN_8MB_HASH_B64
);
Ok(())
}
#[test(tokio::test)]
async fn test_verify_hash_crc64() -> Res {
let storage = MockStorage::default();
let local_storage = LocalStorage::default();
let test_path = Path::new("foo");
let initial_content = b"lorem ipsum";
storage
.write_byte_stream(test_path, ByteStream::from_static(initial_content))
.await?;
let sha256_host_config = HostConfig::default_sha256_chunked();
let crc64_host_config = HostConfig::default_crc64();
let logical_key = PathBuf::from("bar");
let manifest_row =
calculate_hash(&storage, test_path, &logical_key, &sha256_host_config).await?;
assert!(
verify_hash(
&storage,
&test_path.to_path_buf(),
manifest_row.clone(),
&crc64_host_config,
)
.await?
.is_none(),
"Unchanged file should return None, even when algorithms don't match"
);
let fixture_path = Path::new("fixtures/user-settings.mkfg");
let fixture_content = local_storage.read_byte_stream(fixture_path).await?;
storage
.write_byte_stream(test_path, fixture_content)
.await?;
let result = verify_hash(
&storage,
&test_path.to_path_buf(),
manifest_row,
&crc64_host_config,
)
.await?;
assert!(result.is_some(), "Modified file should return Some");
let modified_row = result.unwrap();
assert_eq!(modified_row.hash.algorithm(), MULTIHASH_CRC64_NVME);
assert_eq!(modified_row.hash.to_string(), "LZmmpqbBItw=");
Ok(())
}
#[test(tokio::test)]
async fn test_verify_hash_sha256_chunked() -> Res {
let storage = MockStorage::default();
let test_path = Path::new("foo");
let initial_content = b"lorem ipsum";
storage
.write_byte_stream(test_path, ByteStream::from_static(initial_content))
.await?;
let sha256_host_config = HostConfig::default_sha256_chunked();
let crc64_host_config = HostConfig::default_crc64();
let logical_key = PathBuf::from("bar");
let manifest_row =
calculate_hash(&storage, test_path, &logical_key, &crc64_host_config).await?;
assert!(
verify_hash(
&storage,
&test_path.to_path_buf(),
manifest_row.clone(),
&sha256_host_config,
)
.await?
.is_none(),
"Unchanged file should return None, even when algorithms don't match"
);
let fixture_content = crate::fixtures::objects::less_than_8mb();
storage
.write_byte_stream(test_path, ByteStream::from_static(fixture_content))
.await?;
let result = verify_hash(
&storage,
&test_path.to_path_buf(),
manifest_row,
&sha256_host_config,
)
.await?;
assert!(result.is_some(), "Modified file should return Some");
let modified_row = result.unwrap();
assert_eq!(modified_row.hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
assert_eq!(modified_row.size, fixture_content.len() as u64);
assert_eq!(
modified_row.hash.to_string(),
crate::fixtures::objects::LESS_THAN_8MB_HASH_B64
);
Ok(())
}
}