s3m-core 0.21.0

Reusable S3 client and streaming engine behind the s3m CLI
use crate::s3::checksum::Checksum;
use rkyv::{Archive, Deserialize, Serialize};

#[derive(Debug, Default, Clone, Archive, Serialize, Deserialize)]
#[rkyv(derive(Debug))]
pub struct Part {
    etag: String,
    number: u16,
    checksum: Option<Checksum>,
    seek: u64,
    chunk: u64,
}

impl Part {
    #[must_use]
    pub const fn new(number: u16, seek: u64, chunk: u64, checksum: Option<Checksum>) -> Self {
        Self {
            number,
            seek,
            chunk,
            etag: String::new(),
            checksum,
        }
    }

    #[must_use]
    pub fn set_etag(mut self, etag: String) -> Self {
        self.etag = etag;
        self
    }

    #[must_use]
    pub fn set_checksum(mut self, checksum: Option<Checksum>) -> Self {
        self.checksum = checksum;
        self
    }

    #[must_use]
    pub fn get_etag(&self) -> &str {
        &self.etag
    }

    #[must_use]
    pub const fn get_number(&self) -> u16 {
        self.number
    }

    #[must_use]
    pub fn get_checksum(&self) -> Option<Checksum> {
        self.checksum.clone()
    }

    #[must_use]
    pub const fn get_seek(&self) -> u64 {
        self.seek
    }

    #[must_use]
    pub const fn get_chunk(&self) -> u64 {
        self.chunk
    }
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    clippy::unnecessary_wraps
)]
mod tests {
    use super::*;
    use crate::s3::checksum::{Checksum, ChecksumAlgorithm};
    use rkyv::{from_bytes, rancor::Error as RkyvError, to_bytes};

    #[test]
    fn test_part() {
        let part = Part::new(1, 0, 0, None);
        assert_eq!(part.get_number(), 1);
        assert_eq!(part.get_seek(), 0);
        assert_eq!(part.get_chunk(), 0);
        assert_eq!(part.get_etag(), "");
        assert!(part.get_checksum().is_none());
    }

    #[test]
    fn test_part_set_etag() {
        let part = Part::new(1, 0, 0, None).set_etag("etag".to_string());
        assert_eq!(part.get_etag(), "etag");
    }

    #[test]
    fn test_part_set_checksum() {
        let checksum = Checksum::new(ChecksumAlgorithm::Crc32c);
        let part = Part::new(1, 0, 0, None).set_checksum(Some(checksum.clone()));
        assert_eq!(
            part.get_checksum().unwrap().algorithm,
            ChecksumAlgorithm::Crc32c
        );
        assert_eq!(part.get_checksum().unwrap().checksum, "");
    }

    #[test]
    fn test_part_rkyv_serialize_deserialize_basic() {
        let part = Part::new(1, 1024, 4096, None);
        let bytes = to_bytes::<RkyvError>(&part).unwrap();
        let deserialized: Part = from_bytes::<Part, RkyvError>(&bytes).unwrap();

        assert_eq!(deserialized.get_number(), 1);
        assert_eq!(deserialized.get_seek(), 1024);
        assert_eq!(deserialized.get_chunk(), 4096);
        assert_eq!(deserialized.get_etag(), "");
        assert!(deserialized.get_checksum().is_none());
    }

    #[test]
    fn test_part_rkyv_serialize_deserialize_with_etag() {
        let part = Part::new(42, 0, 5_242_880, None)
            .set_etag("\"d41d8cd98f00b204e9800998ecf8427e\"".to_string());
        let bytes = to_bytes::<RkyvError>(&part).unwrap();
        let deserialized: Part = from_bytes::<Part, RkyvError>(&bytes).unwrap();

        assert_eq!(deserialized.get_number(), 42);
        assert_eq!(
            deserialized.get_etag(),
            "\"d41d8cd98f00b204e9800998ecf8427e\""
        );
    }

    #[test]
    fn test_part_rkyv_serialize_deserialize_with_checksum() {
        let mut checksum = Checksum::new(ChecksumAlgorithm::Sha256);
        checksum.checksum = "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=".to_string();
        let part = Part::new(100, 52_428_800, 5_242_880, Some(checksum));
        let bytes = to_bytes::<RkyvError>(&part).unwrap();
        let deserialized: Part = from_bytes::<Part, RkyvError>(&bytes).unwrap();

        assert_eq!(deserialized.get_number(), 100);
        assert_eq!(deserialized.get_seek(), 52_428_800);
        assert_eq!(deserialized.get_chunk(), 5_242_880);
        let checksum = deserialized.get_checksum().unwrap();
        assert_eq!(checksum.algorithm, ChecksumAlgorithm::Sha256);
        assert_eq!(
            checksum.checksum,
            "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
        );
    }

    #[test]
    fn test_part_rkyv_serialize_deserialize_full() {
        let mut checksum = Checksum::new(ChecksumAlgorithm::Crc32c);
        checksum.checksum = "yZRlqg==".to_string();
        let part = Part::new(9999, u64::MAX, u64::MAX - 1, Some(checksum))
            .set_etag("\"abc123\"".to_string());
        let bytes = to_bytes::<RkyvError>(&part).unwrap();
        let deserialized: Part = from_bytes::<Part, RkyvError>(&bytes).unwrap();

        assert_eq!(deserialized.get_number(), 9999);
        assert_eq!(deserialized.get_seek(), u64::MAX);
        assert_eq!(deserialized.get_chunk(), u64::MAX - 1);
        assert_eq!(deserialized.get_etag(), "\"abc123\"");
        let checksum = deserialized.get_checksum().unwrap();
        assert_eq!(checksum.algorithm, ChecksumAlgorithm::Crc32c);
        assert_eq!(checksum.checksum, "yZRlqg==");
    }

    #[test]
    fn test_part_rkyv_roundtrip_all_checksum_algorithms() {
        for algorithm in [
            ChecksumAlgorithm::Crc32,
            ChecksumAlgorithm::Crc32c,
            ChecksumAlgorithm::Sha1,
            ChecksumAlgorithm::Sha256,
            ChecksumAlgorithm::Md5,
        ] {
            let mut checksum = Checksum::new(algorithm.clone());
            checksum.checksum = "test_checksum_value".to_string();
            let part = Part::new(1, 0, 1024, Some(checksum));
            let bytes = to_bytes::<RkyvError>(&part).unwrap();
            let deserialized: Part = from_bytes::<Part, RkyvError>(&bytes).unwrap();

            let recovered_checksum = deserialized.get_checksum().unwrap();
            assert_eq!(recovered_checksum.algorithm, algorithm);
            assert_eq!(recovered_checksum.checksum, "test_checksum_value");
        }
    }
}