1use std::collections::HashMap;
2use std::fmt;
3use std::fmt::{Debug, Formatter};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use aws_sdk_s3::operation::get_object::GetObjectOutput;
8use aws_sdk_s3::operation::head_object::HeadObjectOutput;
9use aws_sdk_s3::primitives::DateTime;
10use aws_sdk_s3::types::{ChecksumAlgorithm, ChecksumType, ObjectPart, Tag};
11use sha1::{Digest, Sha1};
12use zeroize_derive::{Zeroize, ZeroizeOnDrop};
13
14pub mod async_callback;
15pub mod error;
16
17pub mod filter_message;
18pub mod token;
19
20pub const S3SYNC_ORIGIN_LAST_MODIFIED_METADATA_KEY: &str = "s3sync_origin_last_modified";
21pub const SYNC_REPORT_SUMMERY_NAME: &str = "REPORT_SUMMARY";
22pub const SYNC_REPORT_RECORD_NAME: &str = "SYNC_STATUS";
23pub const SYNC_REPORT_EXISTENCE_TYPE: &str = "EXISTENCE";
24pub const SYNC_REPORT_ETAG_TYPE: &str = "ETAG";
25pub const SYNC_REPORT_CHECKSUM_TYPE: &str = "CHECKSUM";
26pub const SYNC_REPORT_METADATA_TYPE: &str = "METADATA";
27pub const SYNC_REPORT_TAGGING_TYPE: &str = "TAGGING";
28pub const SYNC_REPORT_CONTENT_DISPOSITION_METADATA_KEY: &str = "Content-Disposition";
29pub const SYNC_REPORT_CONTENT_ENCODING_METADATA_KEY: &str = "Content-Encoding";
30pub const SYNC_REPORT_CONTENT_LANGUAGE_METADATA_KEY: &str = "Content-Language";
31pub const SYNC_REPORT_CONTENT_TYPE_METADATA_KEY: &str = "Content-Type";
32pub const SYNC_REPORT_CACHE_CONTROL_METADATA_KEY: &str = "Cache-Control";
33pub const SYNC_REPORT_EXPIRES_METADATA_KEY: &str = "Expires";
34pub const SYNC_REPORT_WEBSITE_REDIRECT_METADATA_KEY: &str = "x-amz-website-redirect-location";
35pub const SYNC_REPORT_USER_DEFINED_METADATA_KEY: &str = "x-amz-meta-";
36
37pub const METADATA_SYNC_REPORT_LOG_NAME: &str = "METADATA_SYNC_STATUS";
38pub const TAGGING_SYNC_REPORT_LOG_NAME: &str = "TAGGING_SYNC_STATUS";
39pub const SYNC_STATUS_MATCHES: &str = "MATCHES";
40pub const SYNC_STATUS_MISMATCH: &str = "MISMATCH";
41pub const SYNC_STATUS_NOT_FOUND: &str = "NOT_FOUND";
42pub const SYNC_STATUS_UNKNOWN: &str = "UNKNOWN";
43
44pub type Sha1Digest = [u8; 20];
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum ObjectKey {
48 KeyString(String),
49 KeySHA1Digest(Sha1Digest),
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub struct ObjectEntry {
54 pub last_modified: DateTime,
55 pub content_length: i64,
56 pub e_tag: Option<String>,
57}
58
59pub type ObjectKeyMap = Arc<Mutex<HashMap<ObjectKey, ObjectEntry>>>;
60
61#[derive(Debug, Clone, Default)]
62pub struct SyncStatsReport {
63 pub number_of_objects: usize,
64 pub not_found: usize,
65 pub etag_matches: usize,
66 pub etag_mismatch: usize,
67 pub etag_unknown: usize,
68 pub checksum_matches: usize,
69 pub checksum_mismatch: usize,
70 pub checksum_unknown: usize,
71 pub metadata_matches: usize,
72 pub metadata_mismatch: usize,
73 pub tagging_matches: usize,
74 pub tagging_mismatch: usize,
75}
76
77impl SyncStatsReport {
78 pub fn increment_number_of_objects(&mut self) {
79 self.number_of_objects += 1;
80 }
81 pub fn increment_not_found(&mut self) {
82 self.not_found += 1;
83 }
84 pub fn increment_etag_matches(&mut self) {
85 self.etag_matches += 1;
86 }
87 pub fn increment_etag_mismatch(&mut self) {
88 self.etag_mismatch += 1;
89 }
90 pub fn increment_etag_unknown(&mut self) {
91 self.etag_unknown += 1;
92 }
93 pub fn increment_checksum_matches(&mut self) {
94 self.checksum_matches += 1;
95 }
96 pub fn increment_checksum_mismatch(&mut self) {
97 self.checksum_mismatch += 1;
98 }
99 pub fn increment_checksum_unknown(&mut self) {
100 self.checksum_unknown += 1;
101 }
102 pub fn increment_metadata_matches(&mut self) {
103 self.metadata_matches += 1;
104 }
105 pub fn increment_metadata_mismatch(&mut self) {
106 self.metadata_mismatch += 1;
107 }
108 pub fn increment_tagging_matches(&mut self) {
109 self.tagging_matches += 1;
110 }
111 pub fn increment_tagging_mismatch(&mut self) {
112 self.tagging_mismatch += 1;
113 }
114}
115
116#[derive(Clone, Default)]
117pub struct ObjectChecksum {
118 pub key: String,
119 pub version_id: Option<String>,
120 pub checksum_algorithm: Option<ChecksumAlgorithm>,
121 pub checksum_type: Option<ChecksumType>,
122 pub object_parts: Option<Vec<ObjectPart>>,
123 pub final_checksum: Option<String>,
124}
125
126pub fn format_metadata(metadata: &HashMap<String, String>) -> String {
127 let mut sorted_keys: Vec<&String> = metadata.keys().collect();
128 sorted_keys.sort();
129
130 sorted_keys
131 .iter()
132 .map(|key| {
133 let value = urlencoding::encode(&metadata[*key]).to_string();
134 format!("{key}={value}")
135 })
136 .collect::<Vec<String>>()
137 .join(",")
138}
139
140pub fn format_tags(tags: &[Tag]) -> String {
141 let mut tags = tags
142 .iter()
143 .map(|tag| (tag.key(), tag.value()))
144 .collect::<Vec<_>>();
145
146 tags.sort_by(|a, b| a.0.cmp(b.0));
147
148 tags.iter()
149 .map(|(key, value)| {
150 let escaped_key = urlencoding::encode(key).to_string();
151 let encoded_value = urlencoding::encode(value).to_string();
152 format!("{escaped_key}={encoded_value}")
153 })
154 .collect::<Vec<String>>()
155 .join("&")
156}
157
158#[allow(deprecated)]
161pub fn sha1_digest_from_key(key: &str) -> Sha1Digest {
162 let digest = Sha1::digest(key);
163 TryInto::<Sha1Digest>::try_into(digest.as_slice()).unwrap()
164}
165
166pub fn detect_additional_checksum(
174 get_object_output: &GetObjectOutput,
175) -> Option<(ChecksumAlgorithm, String)> {
176 if let Some(v) = get_object_output.checksum_sha256() {
177 return Some((ChecksumAlgorithm::Sha256, v.to_string()));
178 }
179 if let Some(v) = get_object_output.checksum_sha1() {
180 return Some((ChecksumAlgorithm::Sha1, v.to_string()));
181 }
182 if let Some(v) = get_object_output.checksum_crc32_c() {
183 return Some((ChecksumAlgorithm::Crc32C, v.to_string()));
184 }
185 if let Some(v) = get_object_output.checksum_crc32() {
186 return Some((ChecksumAlgorithm::Crc32, v.to_string()));
187 }
188 if let Some(v) = get_object_output.checksum_crc64_nvme() {
189 return Some((ChecksumAlgorithm::Crc64Nvme, v.to_string()));
190 }
191 None
192}
193
194pub fn detect_additional_checksum_with_head_object(
202 head_object_output: &HeadObjectOutput,
203) -> Option<(ChecksumAlgorithm, String)> {
204 if let Some(v) = head_object_output.checksum_sha256() {
205 return Some((ChecksumAlgorithm::Sha256, v.to_string()));
206 }
207 if let Some(v) = head_object_output.checksum_sha1() {
208 return Some((ChecksumAlgorithm::Sha1, v.to_string()));
209 }
210 if let Some(v) = head_object_output.checksum_crc32_c() {
211 return Some((ChecksumAlgorithm::Crc32C, v.to_string()));
212 }
213 if let Some(v) = head_object_output.checksum_crc32() {
214 return Some((ChecksumAlgorithm::Crc32, v.to_string()));
215 }
216 if let Some(v) = head_object_output.checksum_crc64_nvme() {
217 return Some((ChecksumAlgorithm::Crc64Nvme, v.to_string()));
218 }
219 None
220}
221
222pub fn get_additional_checksum(
223 get_object_output: &GetObjectOutput,
224 checksum_algorithm: Option<ChecksumAlgorithm>,
225) -> Option<String> {
226 checksum_algorithm.as_ref()?;
227
228 match checksum_algorithm.unwrap() {
229 ChecksumAlgorithm::Sha256 => get_object_output
230 .checksum_sha256()
231 .map(|checksum| checksum.to_string()),
232 ChecksumAlgorithm::Sha1 => get_object_output
233 .checksum_sha1()
234 .map(|checksum| checksum.to_string()),
235 ChecksumAlgorithm::Crc32 => get_object_output
236 .checksum_crc32()
237 .map(|checksum| checksum.to_string()),
238 ChecksumAlgorithm::Crc32C => get_object_output
239 .checksum_crc32_c()
240 .map(|checksum| checksum.to_string()),
241 ChecksumAlgorithm::Crc64Nvme => get_object_output
242 .checksum_crc64_nvme()
243 .map(|checksum| checksum.to_string()),
244 _ => {
245 panic!("unknown algorithm")
246 }
247 }
248}
249
250pub fn get_additional_checksum_with_head_object(
251 head_object_output: &HeadObjectOutput,
252 checksum_algorithm: Option<ChecksumAlgorithm>,
253) -> Option<String> {
254 checksum_algorithm.as_ref()?;
255
256 match checksum_algorithm.unwrap() {
257 ChecksumAlgorithm::Sha256 => head_object_output
258 .checksum_sha256()
259 .map(|checksum| checksum.to_string()),
260 ChecksumAlgorithm::Sha1 => head_object_output
261 .checksum_sha1()
262 .map(|checksum| checksum.to_string()),
263 ChecksumAlgorithm::Crc32 => head_object_output
264 .checksum_crc32()
265 .map(|checksum| checksum.to_string()),
266 ChecksumAlgorithm::Crc32C => head_object_output
267 .checksum_crc32_c()
268 .map(|checksum| checksum.to_string()),
269 ChecksumAlgorithm::Crc64Nvme => head_object_output
270 .checksum_crc64_nvme()
271 .map(|checksum| checksum.to_string()),
272 _ => {
273 panic!("unknown algorithm")
274 }
275 }
276}
277
278pub fn is_full_object_checksum(checksum: &Option<String>) -> bool {
279 if checksum.is_none() {
280 return false;
281 }
282
283 let find_result = checksum.as_ref().unwrap().find('-');
286 find_result.is_none()
287}
288
289#[derive(Debug, PartialEq)]
290pub enum SyncStatistics {
291 SyncBytes(u64),
292 SyncComplete { key: String },
293 SyncError { key: String },
294 SyncWarning { key: String },
295 ETagVerified { key: String },
296 ETagMismatch { key: String },
297 ChecksumVerified { key: String },
298 ChecksumMismatch { key: String },
299}
300
301#[derive(Debug, Clone)]
302pub enum StoragePath {
303 S3 { bucket: String, prefix: String },
304 Local(PathBuf),
305 Stdio,
306}
307
308#[derive(Debug, Clone)]
309pub struct ClientConfigLocation {
310 pub aws_config_file: Option<PathBuf>,
311 pub aws_shared_credentials_file: Option<PathBuf>,
312}
313
314#[derive(Debug, Clone)]
315#[non_exhaustive]
316pub enum S3Credentials {
317 Profile(String),
318 Credentials { access_keys: AccessKeys },
319 FromEnvironment,
320 NoSignRequest,
321}
322
323#[derive(Clone, Zeroize, ZeroizeOnDrop)]
324pub struct AccessKeys {
325 pub access_key: String,
326 pub secret_access_key: String,
327 pub session_token: Option<String>,
328}
329
330impl Debug for AccessKeys {
331 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
332 let mut keys = f.debug_struct("AccessKeys");
333 let session_token = self
334 .session_token
335 .as_ref()
336 .map_or("None", |_| "** redacted **");
337 keys.field("access_key", &self.access_key)
338 .field("secret_access_key", &"** redacted **")
339 .field("session_token", &session_token);
340 keys.finish()
341 }
342}
343
344#[derive(Clone, Zeroize, ZeroizeOnDrop)]
345pub struct SseKmsKeyId {
346 pub id: Option<String>,
347}
348
349impl Debug for SseKmsKeyId {
350 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
351 let mut keys = f.debug_struct("SseKmsKeyId");
352 let sse_kms_key_id = self.id.as_ref().map_or("None", |_| "** redacted **");
353 keys.field("sse_kms_key_id", &sse_kms_key_id);
354 keys.finish()
355 }
356}
357
358#[derive(Clone, Zeroize, ZeroizeOnDrop)]
359pub struct SseCustomerKey {
360 pub key: Option<String>,
361}
362
363impl Debug for SseCustomerKey {
364 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
365 let mut keys = f.debug_struct("SseCustomerKey");
366 let sse_c_key = self.key.as_ref().map_or("None", |_| "** redacted **");
367 keys.field("key", &sse_c_key);
368 keys.finish()
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn debug_print_access_keys() {
378 init_dummy_tracing_subscriber();
379
380 let access_keys = AccessKeys {
381 access_key: "access_key".to_string(),
382 secret_access_key: "secret_access_key".to_string(),
383 session_token: Some("session_token".to_string()),
384 };
385 let debug_string = format!("{access_keys:?}");
386
387 assert!(debug_string.contains("secret_access_key: \"** redacted **\""));
388 assert!(debug_string.contains("session_token: \"** redacted **\""));
389 }
390
391 #[test]
392 fn test_sse_kms_keyid_debug_string() {
393 let secret = SseKmsKeyId {
394 id: Some("secret".to_string()),
395 };
396
397 let debug_string = format!("{:?}", secret);
398 assert!(debug_string.contains("redacted"))
399 }
400
401 #[test]
402 fn test_sse_customer_key_debug_string() {
403 let secret = SseCustomerKey {
404 key: Some("secret".to_string()),
405 };
406
407 let debug_string = format!("{:?}", secret);
408 assert!(debug_string.contains("redacted"))
409 }
410
411 #[test]
412 fn test_format_metadata() {
413 let metadata = HashMap::from([
414 ("key3".to_string(), "value3".to_string()),
415 ("key1".to_string(), "value1".to_string()),
416 ("abc".to_string(), "\u{2603}".to_string()),
417 ("xyz_abc".to_string(), "value_xyz".to_string()),
418 ("key_comma".to_string(), "value,comma".to_string()),
419 ("key2".to_string(), "value2".to_string()),
420 ]);
421
422 let formatted = format_metadata(&metadata);
423 assert_eq!(
424 formatted,
425 "abc=%E2%98%83,key1=value1,key2=value2,key3=value3,key_comma=value%2Ccomma,xyz_abc=value_xyz"
426 );
427 }
428
429 #[test]
430 fn test_format_tags() {
431 let tags = vec![
432 Tag::builder().key("key3").value("value3").build().unwrap(),
433 Tag::builder().key("key1").value("value1").build().unwrap(),
434 Tag::builder().key("abc").value("\u{2603}").build().unwrap(),
435 Tag::builder()
436 .key("\u{2603}")
437 .value("value")
438 .build()
439 .unwrap(),
440 Tag::builder()
441 .key("xyz_abc")
442 .value("value_xyz")
443 .build()
444 .unwrap(),
445 Tag::builder()
446 .key("key_comma")
447 .value("value,comma")
448 .build()
449 .unwrap(),
450 Tag::builder()
451 .key("key_and")
452 .value("value&and")
453 .build()
454 .unwrap(),
455 Tag::builder().key("key2").value("value2").build().unwrap(),
456 ];
457
458 let formatted = format_tags(tags.as_slice());
459 assert_eq!(
460 formatted,
461 "abc=%E2%98%83&key1=value1&key2=value2&key3=value3&key_and=value%26and&key_comma=value%2Ccomma&xyz_abc=value_xyz&%E2%98%83=value"
462 );
463 }
464
465 fn init_dummy_tracing_subscriber() {
466 let _ = tracing_subscriber::fmt()
467 .with_env_filter("dummy=trace")
468 .try_init();
469 }
470
471 #[test]
472 fn is_full_object_checksum_none() {
473 assert!(!is_full_object_checksum(&None));
474 }
475
476 #[test]
477 fn is_full_object_checksum_no_dash_means_full_object() {
478 assert!(is_full_object_checksum(&Some("abc123==".to_string())));
479 }
480
481 #[test]
482 fn is_full_object_checksum_dash_means_composite() {
483 assert!(!is_full_object_checksum(&Some("abc123==-4".to_string())));
485 }
486
487 #[test]
488 fn detect_additional_checksum_returns_none_when_no_checksum_present() {
489 let get = GetObjectOutput::builder().build();
490 assert!(detect_additional_checksum(&get).is_none());
491 }
492
493 #[test]
494 fn detect_additional_checksum_returns_sha256_when_present() {
495 let get = GetObjectOutput::builder()
496 .checksum_sha256("sha256-value")
497 .build();
498 let (algo, value) = detect_additional_checksum(&get).unwrap();
499 assert!(matches!(algo, ChecksumAlgorithm::Sha256));
500 assert_eq!(value, "sha256-value");
501 }
502
503 #[test]
504 fn detect_additional_checksum_prefers_explicit_over_auto_added_crc64nvme() {
505 let get = GetObjectOutput::builder()
508 .checksum_sha256("sha256-value")
509 .checksum_crc64_nvme("crc64-value")
510 .build();
511 let (algo, value) = detect_additional_checksum(&get).unwrap();
512 assert!(matches!(algo, ChecksumAlgorithm::Sha256));
513 assert_eq!(value, "sha256-value");
514 }
515
516 #[test]
517 fn detect_additional_checksum_returns_crc64nvme_when_only_one_present() {
518 let get = GetObjectOutput::builder()
519 .checksum_crc64_nvme("crc64-value")
520 .build();
521 let (algo, value) = detect_additional_checksum(&get).unwrap();
522 assert!(matches!(algo, ChecksumAlgorithm::Crc64Nvme));
523 assert_eq!(value, "crc64-value");
524 }
525
526 #[test]
527 fn detect_additional_checksum_with_head_object_returns_none_when_no_checksum_present() {
528 let head = HeadObjectOutput::builder().build();
529 assert!(detect_additional_checksum_with_head_object(&head).is_none());
530 }
531
532 #[test]
533 fn detect_additional_checksum_with_head_object_returns_sha256_when_present() {
534 let head = HeadObjectOutput::builder()
535 .checksum_sha256("sha256-value")
536 .build();
537 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
538 assert!(matches!(algo, ChecksumAlgorithm::Sha256));
539 assert_eq!(value, "sha256-value");
540 }
541
542 #[test]
543 fn detect_additional_checksum_with_head_object_prefers_explicit_over_auto_added_crc64nvme() {
544 let head = HeadObjectOutput::builder()
547 .checksum_sha256("sha256-value")
548 .checksum_crc64_nvme("crc64-value")
549 .build();
550 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
551 assert!(matches!(algo, ChecksumAlgorithm::Sha256));
552 assert_eq!(value, "sha256-value");
553 }
554
555 #[test]
556 fn detect_additional_checksum_with_head_object_returns_crc64nvme_when_only_one_present() {
557 let head = HeadObjectOutput::builder()
558 .checksum_crc64_nvme("crc64-value")
559 .build();
560 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
561 assert!(matches!(algo, ChecksumAlgorithm::Crc64Nvme));
562 assert_eq!(value, "crc64-value");
563 }
564
565 #[test]
566 fn get_additional_checksum_returns_none_when_algorithm_none() {
567 let get = GetObjectOutput::builder()
568 .checksum_sha256("ignored")
569 .build();
570 assert!(get_additional_checksum(&get, None).is_none());
571 }
572
573 #[test]
574 fn get_additional_checksum_extracts_requested_algorithm() {
575 let get = GetObjectOutput::builder()
576 .checksum_sha256("sha256-value")
577 .checksum_sha1("sha1-value")
578 .checksum_crc32("crc32-value")
579 .checksum_crc32_c("crc32c-value")
580 .checksum_crc64_nvme("crc64-value")
581 .build();
582 assert_eq!(
583 get_additional_checksum(&get, Some(ChecksumAlgorithm::Sha256)).unwrap(),
584 "sha256-value"
585 );
586 assert_eq!(
587 get_additional_checksum(&get, Some(ChecksumAlgorithm::Sha1)).unwrap(),
588 "sha1-value"
589 );
590 assert_eq!(
591 get_additional_checksum(&get, Some(ChecksumAlgorithm::Crc32)).unwrap(),
592 "crc32-value"
593 );
594 assert_eq!(
595 get_additional_checksum(&get, Some(ChecksumAlgorithm::Crc32C)).unwrap(),
596 "crc32c-value"
597 );
598 assert_eq!(
599 get_additional_checksum(&get, Some(ChecksumAlgorithm::Crc64Nvme)).unwrap(),
600 "crc64-value"
601 );
602 let empty = GetObjectOutput::builder().build();
604 assert!(get_additional_checksum(&empty, Some(ChecksumAlgorithm::Sha1)).is_none());
605 }
606
607 #[test]
608 fn get_additional_checksum_with_head_object_extracts_correct_field() {
609 use aws_sdk_s3::operation::head_object::HeadObjectOutput;
610 let head = HeadObjectOutput::builder()
611 .checksum_sha256("head-sha256")
612 .checksum_sha1("head-sha1")
613 .checksum_crc32("head-crc32")
614 .checksum_crc32_c("head-crc32c")
615 .checksum_crc64_nvme("head-crc64")
616 .build();
617 assert!(get_additional_checksum_with_head_object(&head, None).is_none());
618 assert_eq!(
619 get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Sha256))
620 .unwrap(),
621 "head-sha256"
622 );
623 assert_eq!(
624 get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Sha1)).unwrap(),
625 "head-sha1"
626 );
627 assert_eq!(
628 get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Crc32))
629 .unwrap(),
630 "head-crc32"
631 );
632 assert_eq!(
633 get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Crc32C))
634 .unwrap(),
635 "head-crc32c"
636 );
637 assert_eq!(
638 get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Crc64Nvme))
639 .unwrap(),
640 "head-crc64"
641 );
642 let empty = HeadObjectOutput::builder().build();
643 assert!(
644 get_additional_checksum_with_head_object(&empty, Some(ChecksumAlgorithm::Sha256))
645 .is_none()
646 );
647 }
648
649 #[test]
650 fn sha1_digest_from_key_is_deterministic_and_correct() {
651 let key = "some-object-key.dat";
652 let a = sha1_digest_from_key(key);
653 let b = sha1_digest_from_key(key);
654 assert_eq!(a, b);
655 assert_ne!(sha1_digest_from_key("different-key"), a);
658 }
659
660 #[test]
661 fn sync_stats_report_increments_advance_each_field() {
662 let mut r = SyncStatsReport::default();
663 r.increment_number_of_objects();
664 r.increment_not_found();
665 r.increment_etag_matches();
666 r.increment_etag_mismatch();
667 r.increment_etag_unknown();
668 r.increment_checksum_matches();
669 r.increment_checksum_mismatch();
670 r.increment_checksum_unknown();
671 r.increment_metadata_matches();
672 r.increment_metadata_mismatch();
673 r.increment_tagging_matches();
674 r.increment_tagging_mismatch();
675
676 assert_eq!(r.number_of_objects, 1);
677 assert_eq!(r.not_found, 1);
678 assert_eq!(r.etag_matches, 1);
679 assert_eq!(r.etag_mismatch, 1);
680 assert_eq!(r.etag_unknown, 1);
681 assert_eq!(r.checksum_matches, 1);
682 assert_eq!(r.checksum_mismatch, 1);
683 assert_eq!(r.checksum_unknown, 1);
684 assert_eq!(r.metadata_matches, 1);
685 assert_eq!(r.metadata_mismatch, 1);
686 assert_eq!(r.tagging_matches, 1);
687 assert_eq!(r.tagging_mismatch, 1);
688 }
689
690 #[test]
691 fn sync_stats_report_increments_accumulate() {
692 let mut r = SyncStatsReport::default();
693 for _ in 0..5 {
694 r.increment_etag_matches();
695 }
696 assert_eq!(r.etag_matches, 5);
697 }
698
699 #[test]
700 #[should_panic(expected = "unknown algorithm")]
701 fn get_additional_checksum_panics_on_unsupported_algorithm() {
702 let get = GetObjectOutput::builder().build();
706 let _ = get_additional_checksum(&get, Some(ChecksumAlgorithm::Md5));
707 }
708
709 #[test]
710 #[should_panic(expected = "unknown algorithm")]
711 fn get_additional_checksum_with_head_object_panics_on_unsupported_algorithm() {
712 let head = HeadObjectOutput::builder().build();
713 let _ = get_additional_checksum_with_head_object(&head, Some(ChecksumAlgorithm::Md5));
714 }
715
716 #[test]
717 fn detect_additional_checksum_returns_sha1_when_only_sha1_present() {
718 let get = GetObjectOutput::builder()
719 .checksum_sha1("sha1-value")
720 .build();
721 let (algo, value) = detect_additional_checksum(&get).unwrap();
722 assert!(matches!(algo, ChecksumAlgorithm::Sha1));
723 assert_eq!(value, "sha1-value");
724 }
725
726 #[test]
727 fn detect_additional_checksum_returns_crc32c_when_only_crc32c_present() {
728 let get = GetObjectOutput::builder()
729 .checksum_crc32_c("crc32c-value")
730 .build();
731 let (algo, value) = detect_additional_checksum(&get).unwrap();
732 assert!(matches!(algo, ChecksumAlgorithm::Crc32C));
733 assert_eq!(value, "crc32c-value");
734 }
735
736 #[test]
737 fn detect_additional_checksum_returns_crc32_when_only_crc32_present() {
738 let get = GetObjectOutput::builder()
739 .checksum_crc32("crc32-value")
740 .build();
741 let (algo, value) = detect_additional_checksum(&get).unwrap();
742 assert!(matches!(algo, ChecksumAlgorithm::Crc32));
743 assert_eq!(value, "crc32-value");
744 }
745
746 #[test]
747 fn detect_additional_checksum_with_head_object_returns_sha1_when_only_sha1_present() {
748 let head = HeadObjectOutput::builder().checksum_sha1("v").build();
749 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
750 assert!(matches!(algo, ChecksumAlgorithm::Sha1));
751 assert_eq!(value, "v");
752 }
753
754 #[test]
755 fn detect_additional_checksum_with_head_object_returns_crc32c_when_only_crc32c_present() {
756 let head = HeadObjectOutput::builder().checksum_crc32_c("v").build();
757 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
758 assert!(matches!(algo, ChecksumAlgorithm::Crc32C));
759 assert_eq!(value, "v");
760 }
761
762 #[test]
763 fn detect_additional_checksum_with_head_object_returns_crc32_when_only_crc32_present() {
764 let head = HeadObjectOutput::builder().checksum_crc32("v").build();
765 let (algo, value) = detect_additional_checksum_with_head_object(&head).unwrap();
766 assert!(matches!(algo, ChecksumAlgorithm::Crc32));
767 assert_eq!(value, "v");
768 }
769
770 #[test]
771 fn is_full_object_checksum_dash_at_start_treated_as_composite() {
772 assert!(!is_full_object_checksum(&Some("-foo".to_string())));
776 }
777
778 #[test]
779 fn object_key_equality_and_hash_consistency() {
780 use std::collections::HashSet;
782 let a = ObjectKey::KeyString("foo/bar".to_string());
783 let a2 = ObjectKey::KeyString("foo/bar".to_string());
784 let b = ObjectKey::KeyString("foo/baz".to_string());
785 let digest = sha1_digest_from_key("foo/bar");
786 let c = ObjectKey::KeySHA1Digest(digest);
787 let c2 = ObjectKey::KeySHA1Digest(digest);
788
789 assert_eq!(a, a2);
790 assert_ne!(a, b);
791 assert_eq!(c, c2);
792 assert_ne!(a, c);
794
795 let mut set = HashSet::new();
796 set.insert(a.clone());
797 set.insert(c.clone());
798 assert!(set.contains(&a2));
799 assert!(set.contains(&c2));
800 assert!(!set.contains(&b));
801 }
802
803 #[test]
804 fn object_checksum_default_is_empty() {
805 let oc = ObjectChecksum::default();
806 assert_eq!(oc.key, "");
807 assert!(oc.version_id.is_none());
808 assert!(oc.checksum_algorithm.is_none());
809 assert!(oc.checksum_type.is_none());
810 assert!(oc.object_parts.is_none());
811 assert!(oc.final_checksum.is_none());
812 }
813
814 #[test]
815 fn format_metadata_empty_returns_empty_string() {
816 let formatted = format_metadata(&HashMap::new());
817 assert_eq!(formatted, "");
818 }
819
820 #[test]
821 fn format_tags_empty_returns_empty_string() {
822 let formatted = format_tags(&[]);
823 assert_eq!(formatted, "");
824 }
825
826 #[test]
827 fn s3_credentials_variants_are_constructible() {
828 let c1 = S3Credentials::Profile("p".to_string());
830 let c2 = S3Credentials::FromEnvironment;
831 let c3 = S3Credentials::NoSignRequest;
832 let c4 = S3Credentials::Credentials {
833 access_keys: AccessKeys {
834 access_key: "a".to_string(),
835 secret_access_key: "s".to_string(),
836 session_token: None,
837 },
838 };
839 for c in [c1, c2, c3, c4] {
840 let s = format!("{c:?}");
842 assert!(!s.is_empty());
843 }
844 }
845
846 #[test]
847 fn access_keys_debug_with_no_session_token_redacts_correctly() {
848 let access_keys = AccessKeys {
849 access_key: "AKIA".to_string(),
850 secret_access_key: "secret".to_string(),
851 session_token: None,
852 };
853 let debug_string = format!("{access_keys:?}");
854 assert!(debug_string.contains("session_token: \"None\""));
855 assert!(!debug_string.contains("redacted") || debug_string.contains("secret_access_key"));
856 }
857
858 #[test]
859 fn sse_kms_keyid_debug_with_none_id_does_not_redact() {
860 let secret = SseKmsKeyId { id: None };
861 let debug_string = format!("{secret:?}");
862 assert!(debug_string.contains("None"));
863 assert!(!debug_string.contains("redacted"));
864 }
865
866 #[test]
867 fn sse_customer_key_debug_with_none_key_does_not_redact() {
868 let secret = SseCustomerKey { key: None };
869 let debug_string = format!("{secret:?}");
870 assert!(debug_string.contains("None"));
871 assert!(!debug_string.contains("redacted"));
872 }
873}