1use multihash::Multihash;
4use serde::Deserialize;
5use serde::Serialize;
6use std::fmt;
7use std::path::Path;
8use std::path::PathBuf;
9
10use crate::Res;
11use crate::error::ChecksumError;
12use crate::io::remote::HostChecksums;
13use crate::io::remote::HostConfig;
14use crate::io::storage::Storage;
15use crate::manifest::ManifestRow;
16
17mod crc64nvme;
18mod hash;
19mod remote;
20mod sha256;
21mod sha256_chunked;
22
23pub use crc64nvme::Crc64Hash;
24pub use crc64nvme::MULTIHASH_CRC64_NVME;
25pub use hash::Hash;
26pub use remote::hash_sha256_checksum;
27pub use sha256::MULTIHASH_SHA256;
28pub use sha256::Sha256Hash;
29pub use sha256_chunked::MULTIHASH_SHA256_CHUNKED;
30pub use sha256_chunked::Sha256ChunkedHash;
31pub(crate) use sha256_chunked::chunksize_and_parts;
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(untagged)]
37pub enum ObjectHash {
38 Sha256(Sha256Hash),
40 Sha256Chunked(Sha256ChunkedHash),
42 Crc64(Crc64Hash),
44}
45
46impl Default for ObjectHash {
47 fn default() -> Self {
48 ObjectHash::Crc64(Crc64Hash::default())
49 }
50}
51
52pub async fn refresh_hash(
55 storage: &impl Storage,
56 path: &PathBuf,
57 row: ManifestRow,
58) -> Res<Option<ManifestRow>> {
59 let file = storage.open_file(path).await?;
60 let file_metadata = file.metadata().await?;
61 let size = file_metadata.len();
62
63 let computed_hash = match &row.hash {
64 ObjectHash::Crc64(_) => Crc64Hash::from_file(file).await?.into(),
65 ObjectHash::Sha256(_) => Sha256Hash::from_file(file).await?.into(),
66 ObjectHash::Sha256Chunked(_) => Sha256ChunkedHash::from_file(file).await?.into(),
67 };
68
69 Ok((computed_hash != row.hash).then(|| ManifestRow {
70 hash: computed_hash,
71 size,
72 ..row
73 }))
74}
75
76pub async fn calculate_hash(
78 storage: &impl Storage,
79 path: &Path,
80 logical_key: &Path,
81 host_config: &HostConfig,
82) -> Res<ManifestRow> {
83 let file = storage.open_file(path).await?;
84 let file_metadata = file.metadata().await?;
85 let size = file_metadata.len();
86
87 let hash = match host_config.checksums {
88 HostChecksums::Crc64 => Crc64Hash::from_file(file).await?.into(),
89 HostChecksums::Sha256Chunked => Sha256ChunkedHash::from_file(file).await?.into(),
90 };
91
92 Ok(ManifestRow {
93 logical_key: logical_key.to_path_buf(),
94 physical_key: format!("file://{}", path.display()),
95 size,
96 hash,
97 ..ManifestRow::default()
98 })
99}
100
101pub async fn verify_hash(
104 storage: &impl Storage,
105 path: &PathBuf,
106 row: ManifestRow,
107 host_config: &HostConfig,
108) -> Res<Option<ManifestRow>> {
109 if let Some(modified) = refresh_hash(storage, path, row).await? {
110 if modified.hash.algorithm() == host_config.checksums.algorithm_code() {
112 Ok(Some(modified))
114 } else {
115 let calculated_row =
117 calculate_hash(storage, path, &modified.logical_key, host_config).await?;
118 Ok(Some(calculated_row))
119 }
120 } else {
121 Ok(None)
122 }
123}
124
125impl TryFrom<Multihash<256>> for ObjectHash {
126 type Error = crate::Error;
127
128 fn try_from(multihash: Multihash<256>) -> Result<Self, Self::Error> {
129 match multihash.code() {
130 MULTIHASH_SHA256 => Ok(ObjectHash::Sha256(Sha256Hash::try_from(multihash)?)),
131 MULTIHASH_SHA256_CHUNKED => Ok(ObjectHash::Sha256Chunked(Sha256ChunkedHash::try_from(
132 multihash,
133 )?)),
134 MULTIHASH_CRC64_NVME => Ok(ObjectHash::Crc64(Crc64Hash::try_from(multihash)?)),
135 _ => Err(crate::Error::Checksum(ChecksumError::InvalidMultihash(
136 format!("Unsupported multihash code: {:#06x}", multihash.code()),
137 ))),
138 }
139 }
140}
141
142impl From<ObjectHash> for Multihash<256> {
143 fn from(object_hash: ObjectHash) -> Self {
144 match object_hash {
145 ObjectHash::Sha256(hash) => hash.into(),
146 ObjectHash::Sha256Chunked(hash) => hash.into(),
147 ObjectHash::Crc64(hash) => hash.into(),
148 }
149 }
150}
151
152impl From<Sha256Hash> for ObjectHash {
153 fn from(hash: Sha256Hash) -> Self {
154 ObjectHash::Sha256(hash)
155 }
156}
157
158impl From<Sha256ChunkedHash> for ObjectHash {
159 fn from(hash: Sha256ChunkedHash) -> Self {
160 ObjectHash::Sha256Chunked(hash)
161 }
162}
163
164impl From<Crc64Hash> for ObjectHash {
165 fn from(hash: Crc64Hash) -> Self {
166 ObjectHash::Crc64(hash)
167 }
168}
169
170impl ObjectHash {
171 pub fn multihash(&self) -> &Multihash<256> {
173 match self {
174 ObjectHash::Sha256(hash) => hash.multihash(),
175 ObjectHash::Sha256Chunked(hash) => hash.multihash(),
176 ObjectHash::Crc64(hash) => hash.multihash(),
177 }
178 }
179
180 pub fn algorithm(&self) -> u64 {
182 match self {
183 ObjectHash::Sha256(hash) => hash.algorithm(),
184 ObjectHash::Sha256Chunked(hash) => hash.algorithm(),
185 ObjectHash::Crc64(hash) => hash.algorithm(),
186 }
187 }
188
189 pub fn digest(&self) -> &[u8] {
191 match self {
192 ObjectHash::Sha256(hash) => hash.digest(),
193 ObjectHash::Sha256Chunked(hash) => hash.digest(),
194 ObjectHash::Crc64(hash) => hash.digest(),
195 }
196 }
197}
198
199impl fmt::Display for ObjectHash {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 ObjectHash::Sha256(hash) => hash.fmt(f),
203 ObjectHash::Sha256Chunked(hash) => hash.fmt(f),
204 ObjectHash::Crc64(hash) => hash.fmt(f),
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 use test_log::test;
214
215 use std::path::Path;
216
217 use aws_sdk_s3::primitives::ByteStream;
218
219 use crate::Res;
220 use crate::io::storage::LocalStorage;
221 use crate::io::storage::Storage;
222 use crate::io::storage::StorageExt;
223 use crate::io::storage::mocks::MockStorage;
224
225 #[test]
226 fn test_conversion_errors() {
227 let sha256_hash = multihash::Multihash::wrap(MULTIHASH_SHA256, b"test").unwrap();
229 let result = Sha256ChunkedHash::try_from(sha256_hash);
230 assert!(result.is_err());
231 assert!(
232 result
233 .unwrap_err()
234 .to_string()
235 .contains("Expected SHA256 chunked hash")
236 );
237
238 let sha256_chunked_hash =
240 multihash::Multihash::wrap(MULTIHASH_SHA256_CHUNKED, b"test").unwrap();
241 let result = Sha256Hash::try_from(sha256_chunked_hash);
242 assert!(result.is_err());
243 assert!(
244 result
245 .unwrap_err()
246 .to_string()
247 .contains("Expected SHA256 hash")
248 );
249 }
250
251 #[test]
252 fn test_object_hash_display() -> Res {
253 let object_hash = ObjectHash::Sha256(Sha256Hash::try_from("deadbeef")?);
255 assert_eq!(object_hash.to_string(), "deadbeef");
256
257 let object_hash = ObjectHash::Sha256Chunked(Sha256ChunkedHash::try_from("Zm9vYmFy")?);
259 assert_eq!(object_hash.to_string(), "Zm9vYmFy");
260
261 let object_hash = ObjectHash::Crc64(Crc64Hash::try_from("aGVsbG8gd29ybGQ=")?);
263 assert_eq!(object_hash.to_string(), "aGVsbG8gd29ybGQ=");
264
265 Ok(())
266 }
267
268 #[test]
269 fn test_object_hash_conversions() -> Res {
270 let sha256_multihash = multihash::Multihash::wrap(MULTIHASH_SHA256, b"test_data").unwrap();
272 let object_hash = ObjectHash::try_from(sha256_multihash)?;
273 let back_to_multihash: Multihash<256> = object_hash.clone().into();
274 assert_eq!(sha256_multihash, back_to_multihash);
275 assert_eq!(object_hash.algorithm(), MULTIHASH_SHA256);
276
277 let sha256_chunked_multihash =
279 multihash::Multihash::wrap(MULTIHASH_SHA256_CHUNKED, b"test_data").unwrap();
280 let object_hash = ObjectHash::try_from(sha256_chunked_multihash)?;
281 let back_to_multihash: Multihash<256> = object_hash.clone().into();
282 assert_eq!(sha256_chunked_multihash, back_to_multihash);
283 assert_eq!(object_hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
284
285 let crc64_multihash =
287 multihash::Multihash::wrap(MULTIHASH_CRC64_NVME, b"test_data").unwrap();
288 let object_hash = ObjectHash::try_from(crc64_multihash)?;
289 let back_to_multihash: Multihash<256> = object_hash.clone().into();
290 assert_eq!(crc64_multihash, back_to_multihash);
291 assert_eq!(object_hash.algorithm(), MULTIHASH_CRC64_NVME);
292
293 let invalid_multihash = multihash::Multihash::wrap(0x9999, b"invalid_data").unwrap();
294 let result = ObjectHash::try_from(invalid_multihash);
295
296 assert!(result.is_err());
297 assert!(
298 result
299 .unwrap_err()
300 .to_string()
301 .contains("Unsupported multihash code: 0x9999")
302 );
303
304 Ok(())
305 }
306
307 #[test]
308 fn test_object_hash_from_individual_types() -> Res {
309 let sha256_hash =
311 Sha256Hash::try_from(multihash::Multihash::wrap(MULTIHASH_SHA256, b"test").unwrap())?;
312 let object_hash: ObjectHash = sha256_hash.clone().into();
313 assert_eq!(object_hash.algorithm(), MULTIHASH_SHA256);
314
315 let sha256_chunked_hash = Sha256ChunkedHash::try_from(
316 multihash::Multihash::wrap(MULTIHASH_SHA256_CHUNKED, b"test").unwrap(),
317 )?;
318 let object_hash: ObjectHash = sha256_chunked_hash.clone().into();
319 assert_eq!(object_hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
320
321 let crc64_hash = Crc64Hash::try_from(
322 multihash::Multihash::wrap(MULTIHASH_CRC64_NVME, b"test").unwrap(),
323 )?;
324 let object_hash: ObjectHash = crc64_hash.clone().into();
325 assert_eq!(object_hash.algorithm(), MULTIHASH_CRC64_NVME);
326
327 Ok(())
328 }
329
330 #[test]
331 fn test_object_hash_serde() -> Res {
332 let sha256_hash = Sha256Hash::try_from(
334 multihash::Multihash::wrap(MULTIHASH_SHA256, b"test_data").unwrap(),
335 )?;
336 let object_hash = ObjectHash::Sha256(sha256_hash);
337 let serialized = serde_json::to_string(&object_hash)?;
338 let deserialized: ObjectHash = serde_json::from_str(&serialized)?;
339 assert_eq!(object_hash, deserialized);
340
341 let sha256_chunked_hash = Sha256ChunkedHash::try_from(
343 multihash::Multihash::wrap(MULTIHASH_SHA256_CHUNKED, b"test_data").unwrap(),
344 )?;
345 let object_hash = ObjectHash::Sha256Chunked(sha256_chunked_hash);
346 let serialized = serde_json::to_string(&object_hash)?;
347 let deserialized: ObjectHash = serde_json::from_str(&serialized)?;
348 assert_eq!(object_hash, deserialized);
349
350 let crc64_hash = Crc64Hash::try_from(
352 multihash::Multihash::wrap(MULTIHASH_CRC64_NVME, b"test_data").unwrap(),
353 )?;
354 let object_hash = ObjectHash::Crc64(crc64_hash);
355 let serialized = serde_json::to_string(&object_hash)?;
356 let deserialized: ObjectHash = serde_json::from_str(&serialized)?;
357 assert_eq!(object_hash, deserialized);
358
359 Ok(())
360 }
361
362 #[test]
363 fn test_object_hash_json_format_translation() -> Res {
364 let sha256_json = r#"{"type":"SHA256","value":"7465737464617461000000000000000000000000000000000000000000000000"}"#;
366 let object_hash: ObjectHash = serde_json::from_str(sha256_json)?;
367 match object_hash {
368 ObjectHash::Sha256(hash) => {
369 assert_eq!(hash.algorithm(), MULTIHASH_SHA256);
370 assert_eq!(
371 hex::encode(hash.digest()),
372 "7465737464617461000000000000000000000000000000000000000000000000"
373 );
374 }
375 _ => {
376 return Err(crate::Error::Checksum(ChecksumError::InvalidMultihash(
377 "Expected ObjectHash::Sha256 variant".to_string(),
378 )));
379 }
380 }
381
382 let sha256_chunked_json =
384 r#"{"type":"sha2-256-chunked","value":"dGVzdGRhdGEAAAAAAAAAAAAAAAAAAAAA"}"#;
385 let object_hash: ObjectHash = serde_json::from_str(sha256_chunked_json)?;
386 match object_hash {
387 ObjectHash::Sha256Chunked(hash) => {
388 assert_eq!(hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
389 assert_eq!(
390 &multibase::encode(multibase::Base::Base64Pad, hash.digest())[1..],
391 "dGVzdGRhdGEAAAAAAAAAAAAAAAAAAAAA"
392 );
393 }
394 _ => {
395 return Err(crate::Error::Checksum(ChecksumError::InvalidMultihash(
396 "Expected ObjectHash::Sha256Chunked variant".to_string(),
397 )));
398 }
399 }
400
401 let crc64_json = r#"{"type":"CRC64NVME","value":"dGVzdGRhdGEAAAAAAAAAAAAAAAAAAAAA"}"#;
403 let object_hash: ObjectHash = serde_json::from_str(crc64_json)?;
404 match object_hash {
405 ObjectHash::Crc64(hash) => {
406 assert_eq!(hash.algorithm(), MULTIHASH_CRC64_NVME);
407 assert_eq!(
408 &multibase::encode(multibase::Base::Base64Pad, hash.digest())[1..],
409 "dGVzdGRhdGEAAAAAAAAAAAAAAAAAAAAA"
410 );
411 }
412 _ => {
413 return Err(crate::Error::Checksum(ChecksumError::InvalidMultihash(
414 "Expected ObjectHash::Crc64 variant".to_string(),
415 )));
416 }
417 }
418
419 let hex_bytes =
421 hex::decode("7465737464617461000000000000000000000000000000000000000000000000")
422 .map_err(|e| ChecksumError::InvalidMultihash(e.to_string()))?;
423 let sha256_hash =
424 Sha256Hash::try_from(multihash::Multihash::wrap(MULTIHASH_SHA256, &hex_bytes)?)?;
425 let object_hash = ObjectHash::Sha256(sha256_hash);
426 let serialized = serde_json::to_string(&object_hash)?;
427 assert!(serialized.contains("\"type\":\"SHA256\""));
428 assert!(serialized.contains(
429 "\"value\":\"7465737464617461000000000000000000000000000000000000000000000000\""
430 ));
431
432 Ok(())
433 }
434
435 #[test]
436 fn test_object_hash_json_invalid_type() {
437 let invalid_json = r#"{"type":"UNKNOWN","value":"deadbeef"}"#;
439 let result: Result<ObjectHash, _> = serde_json::from_str(invalid_json);
440 assert!(result.is_err());
441
442 let mismatched_json = r#"{"type":"SHA256","value":"dGVzdA=="}"#; let result: Result<ObjectHash, _> = serde_json::from_str(mismatched_json);
445 assert!(result.is_err());
446 }
447
448 #[test(tokio::test)]
449 async fn test_hash_trait_polymorphism() -> Res {
450 let storage = MockStorage::default();
451 let test_data = b"test data for Hash trait";
452 let test_path = Path::new("hash_trait_test.txt");
453 storage
454 .write_byte_stream(test_path, ByteStream::from_static(test_data))
455 .await?;
456
457 let file = storage.open_file(test_path).await?;
459 let sha256_hash: Sha256Hash = <Sha256Hash as Hash>::from_file(file).await?;
460
461 let file = storage.open_file(test_path).await?;
462 let sha256_chunked_hash = <Sha256ChunkedHash as Hash>::from_file(file).await?;
463
464 let file = storage.open_file(test_path).await?;
465 let crc64_hash = <Crc64Hash as Hash>::from_file(file).await?;
466
467 assert_eq!(sha256_hash.algorithm(), MULTIHASH_SHA256);
469 assert_eq!(sha256_chunked_hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
470 assert_eq!(crc64_hash.algorithm(), MULTIHASH_CRC64_NVME);
471
472 assert!(!sha256_hash.digest().is_empty());
473 assert!(!sha256_chunked_hash.digest().is_empty());
474 assert!(!crc64_hash.digest().is_empty());
475
476 fn check_hash_trait<T: Hash>(hash: &T) -> u64 {
478 hash.algorithm()
479 }
480
481 assert_eq!(check_hash_trait(&sha256_hash), MULTIHASH_SHA256);
482 assert_eq!(
483 check_hash_trait(&sha256_chunked_hash),
484 MULTIHASH_SHA256_CHUNKED
485 );
486 assert_eq!(check_hash_trait(&crc64_hash), MULTIHASH_CRC64_NVME);
487
488 Ok(())
489 }
490
491 #[test(tokio::test)]
492 async fn test_refresh_hash_unchanged_file() -> Res {
493 let storage = MockStorage::default();
494 let file_content = b"anything";
495 let file_path = Path::new("foo");
496 storage
497 .write_byte_stream(file_path, ByteStream::from_static(file_content))
498 .await?;
499
500 let file = storage.open_file(file_path).await?;
501 let hash: Multihash<256> = Sha256Hash::from_file(file).await?.into();
502
503 let manifest_row = ManifestRow {
504 logical_key: PathBuf::from("bar"),
505 hash: hash.try_into()?,
506 size: file_content.len() as u64,
507 ..ManifestRow::default()
508 };
509 let result = refresh_hash(&storage, &file_path.to_path_buf(), manifest_row).await?;
510 assert!(result.is_none(), "Unchanged file should return None");
511
512 Ok(())
513 }
514
515 #[test(tokio::test)]
516 async fn test_refresh_hash_changed_file() -> Res {
517 let storage = MockStorage::default();
518 let file_content = b"anything";
519 let file_path = Path::new("foo");
520 storage
521 .write_byte_stream(file_path, ByteStream::from_static(file_content))
522 .await?;
523
524 let file = storage.open_file(file_path).await?;
526 let hash: Multihash<256> = Sha256Hash::from_file(file).await?.into();
527
528 let wrong_hash = Multihash::wrap(MULTIHASH_SHA256, b"wrong_hash_data")?;
530 let test_manifest_row = ManifestRow {
531 logical_key: PathBuf::from("bar"),
532 hash: wrong_hash.try_into()?,
533 size: 999, ..ManifestRow::default()
535 };
536 let result = refresh_hash(&storage, &file_path.to_path_buf(), test_manifest_row).await?;
537 let refreshed_row = result.expect("Changed hash should return Some");
538 assert_eq!(refreshed_row.hash, hash.try_into()?);
539 assert_eq!(refreshed_row.size, file_content.len() as u64);
540
541 Ok(())
542 }
543
544 #[test(tokio::test)]
545 async fn test_refresh_hash_unknown_algorithm() -> Res {
546 let storage = MockStorage::default();
547 let file_content = b"anything";
548 let file_path = Path::new("foo");
549 storage
550 .write_byte_stream(file_path, ByteStream::from_static(file_content))
551 .await?;
552
553 let valid_hash = Crc64Hash::default().into();
557 let manifest_row = ManifestRow {
558 logical_key: PathBuf::from("bar"),
559 hash: valid_hash,
560 size: file_content.len() as u64,
561 ..ManifestRow::default()
562 };
563
564 let result = refresh_hash(&storage, &file_path.to_path_buf(), manifest_row).await;
565 assert!(result.is_ok());
567
568 Ok(())
569 }
570
571 #[test(tokio::test)]
572 async fn test_calculate_hash_crc64() -> Res {
573 let storage = LocalStorage::default();
574
575 let file_path = Path::new("fixtures/user-settings.mkfg");
576 let host_config = HostConfig::default_crc64();
577 let logical_key = PathBuf::from("foo");
578
579 let row = calculate_hash(&storage, file_path, &logical_key, &host_config).await?;
580
581 assert_eq!(row.hash.algorithm(), MULTIHASH_CRC64_NVME);
582 assert_eq!(row.logical_key, logical_key);
583
584 assert_eq!(row.size, storage.read_bytes(file_path).await?.len() as u64);
585 assert_eq!(row.hash.to_string(), "LZmmpqbBItw=");
586
587 Ok(())
588 }
589
590 #[test(tokio::test)]
591 async fn test_calculate_hash_sha256_chunked() -> Res {
592 let storage = MockStorage::default();
593
594 let host_config = HostConfig::default_sha256_chunked();
595
596 let file_content = crate::fixtures::objects::less_than_8mb();
597 let file_path = Path::new("foo");
598 storage
599 .write_byte_stream(file_path, ByteStream::from_static(file_content))
600 .await?;
601
602 let logical_key = PathBuf::from("bar");
603
604 let row = calculate_hash(&storage, file_path, &logical_key, &host_config).await?;
605
606 assert_eq!(row.hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
607 assert_eq!(row.logical_key, logical_key);
608 assert_eq!(row.size, file_content.len() as u64);
609 assert_eq!(
610 row.hash.to_string(),
611 crate::fixtures::objects::LESS_THAN_8MB_HASH_B64
612 );
613
614 Ok(())
615 }
616
617 #[test(tokio::test)]
618 async fn test_verify_hash_crc64() -> Res {
619 let storage = MockStorage::default();
620 let local_storage = LocalStorage::default();
621
622 let test_path = Path::new("foo");
624 let initial_content = b"lorem ipsum";
625 storage
626 .write_byte_stream(test_path, ByteStream::from_static(initial_content))
627 .await?;
628
629 let sha256_host_config = HostConfig::default_sha256_chunked();
630 let crc64_host_config = HostConfig::default_crc64();
631 let logical_key = PathBuf::from("bar");
632
633 let manifest_row =
634 calculate_hash(&storage, test_path, &logical_key, &sha256_host_config).await?;
635
636 assert!(
637 verify_hash(
638 &storage,
639 &test_path.to_path_buf(),
640 manifest_row.clone(),
641 &crc64_host_config,
642 )
643 .await?
644 .is_none(),
645 "Unchanged file should return None, even when algorithms don't match"
646 );
647
648 let fixture_path = Path::new("fixtures/user-settings.mkfg");
649 let fixture_content = local_storage.read_byte_stream(fixture_path).await?;
650 storage
651 .write_byte_stream(test_path, fixture_content)
652 .await?;
653
654 let result = verify_hash(
655 &storage,
656 &test_path.to_path_buf(),
657 manifest_row,
658 &crc64_host_config,
659 )
660 .await?;
661 assert!(result.is_some(), "Modified file should return Some");
662
663 let modified_row = result.unwrap();
664 assert_eq!(modified_row.hash.algorithm(), MULTIHASH_CRC64_NVME);
665
666 assert_eq!(modified_row.hash.to_string(), "LZmmpqbBItw=");
667
668 Ok(())
669 }
670
671 #[test(tokio::test)]
672 async fn test_verify_hash_sha256_chunked() -> Res {
673 let storage = MockStorage::default();
674
675 let test_path = Path::new("foo");
677 let initial_content = b"lorem ipsum";
678 storage
679 .write_byte_stream(test_path, ByteStream::from_static(initial_content))
680 .await?;
681
682 let sha256_host_config = HostConfig::default_sha256_chunked();
683 let crc64_host_config = HostConfig::default_crc64();
684 let logical_key = PathBuf::from("bar");
685
686 let manifest_row =
687 calculate_hash(&storage, test_path, &logical_key, &crc64_host_config).await?;
688
689 assert!(
690 verify_hash(
691 &storage,
692 &test_path.to_path_buf(),
693 manifest_row.clone(),
694 &sha256_host_config,
695 )
696 .await?
697 .is_none(),
698 "Unchanged file should return None, even when algorithms don't match"
699 );
700
701 let fixture_content = crate::fixtures::objects::less_than_8mb();
703 storage
704 .write_byte_stream(test_path, ByteStream::from_static(fixture_content))
705 .await?;
706
707 let result = verify_hash(
708 &storage,
709 &test_path.to_path_buf(),
710 manifest_row,
711 &sha256_host_config,
712 )
713 .await?;
714 assert!(result.is_some(), "Modified file should return Some");
715
716 let modified_row = result.unwrap();
717 assert_eq!(modified_row.hash.algorithm(), MULTIHASH_SHA256_CHUNKED);
718 assert_eq!(modified_row.size, fixture_content.len() as u64);
719
720 assert_eq!(
721 modified_row.hash.to_string(),
722 crate::fixtures::objects::LESS_THAN_8MB_HASH_B64
723 );
724
725 Ok(())
726 }
727}