sett 0.4.0

Rust port of sett (data compression, encryption and transfer tool).
Documentation
#[derive(Debug, serde::Serialize)]
pub(super) struct KeyStatusPayload<'a, T: AsRef<str>> {
    #[serde(with = "serde_fingerprints")]
    pub(super) fingerprints: &'a [T],
}

mod serde_fingerprints {
    use super::normalize_fingerprint;
    pub fn serialize<S, T>(obj: &[T], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
        T: AsRef<str>,
    {
        use serde::ser::SerializeSeq;
        let mut seq = serializer.serialize_seq(Some(obj.len()))?;
        for fingerprint in obj {
            seq.serialize_element(
                &normalize_fingerprint(fingerprint.as_ref()).map_err(serde::ser::Error::custom)?,
            )?;
        }
        seq.end()
    }
}

fn normalize_fingerprint(raw_fingerprint: &str) -> Result<String, String> {
    let fingerprint = raw_fingerprint
        .parse()
        .map_err(|e| format!("Invalid fingerprint '{raw_fingerprint}': {e}"))?;
    let sequoia_openpgp::Fingerprint::V4(_) = &fingerprint else {
        return Err(format!("Invalid fingerprint '{raw_fingerprint}'"));
    };
    Ok(fingerprint.to_hex())
}

/// Serialize package metadata to a JSON string.
fn metadata_as_json_string<S>(
    metadata: &crate::package::Metadata,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: serde::ser::Serializer,
{
    serializer.serialize_str(&serde_json::to_string(metadata).map_err(serde::ser::Error::custom)?)
}

#[derive(Debug, serde::Serialize)]
pub(super) struct CheckPackage<'a> {
    #[serde(serialize_with = "metadata_as_json_string")]
    pub(super) metadata: &'a crate::package::Metadata,
    pub(super) file_name: &'a str,
}

#[cfg(feature = "auth")]
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub(super) enum Sts<'a> {
    Read { resource: StsResource<'a> },
    Write { dtr: u32 },
}

#[cfg(feature = "auth")]
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub(super) enum StsResource<'a> {
    Dtr(u32),
    ObjectName(&'a str),
}

#[cfg(feature = "auth")]
impl<'a> Sts<'a> {
    pub(super) fn from_permission(permission: &'a crate::remote::s3::AccessPermission) -> Self {
        use crate::remote::s3::AccessPermission;
        match permission {
            AccessPermission::GetObject(name) => Self::Read {
                resource: StsResource::ObjectName(name),
            },
            AccessPermission::ListObjects(dtr) => Self::Read {
                resource: StsResource::Dtr(*dtr),
            },
            AccessPermission::PutObject(dtr) => Self::Write { dtr: *dtr },
        }
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use crate::portal::{ApprovalStatus, KeyStatus};

    #[test]
    fn test_err_fingerprints() {
        for fingerprint in ["1234af", &"M".repeat(4), &"1234af".repeat(40)] {
            assert!(normalize_fingerprint(fingerprint).is_err())
        }
    }

    #[test]
    fn test_ok_fingerprints() {
        for fingerprint in [
            "D99AD936FC83C9BABDE7C33E1CF8C1A2076818C3",   // Normal
            "d99ad936fc83c9babde7c33e1cf8c1a2076818c3",   // all lowercase
            "0xd99ad936fc83c9babde7c33e1cf8c1a2076818c3", // starting with `0x`
            "01AB 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567", // with spaces
        ] {
            assert!(
                normalize_fingerprint(fingerprint).is_ok(),
                "'{fingerprint}' is NOT OK"
            )
        }
    }

    #[test]
    fn test_serialize_keystatus() {
        let fingerprints = KeyStatusPayload {
            fingerprints: &[
                "D99AD936FC83C9BABDE7C33E1CF8C1A2076818C3",
                "95D6F0110B446D6EABFEA10C6B52BA2D01E77CA5",
            ],
        };
        let serialized = serde_json::to_string(&fingerprints).unwrap();
        assert_eq!(
            &serialized,
            r#"{"fingerprints":["D99AD936FC83C9BABDE7C33E1CF8C1A2076818C3","95D6F0110B446D6EABFEA10C6B52BA2D01E77CA5"]}"#
        );
    }

    #[test]
    fn test_deserialize_fingerprints() {
        let payload = r#"[{"fingerprint":"D99AD936FC83C9BABDE7C33E1CF8C1A2076818C3","status":"APPROVED"},
        {"fingerprint":"95D6F0110B446D6EABFEA10C6B52BA2D01E77CA5","status":"APPROVAL_REVOKED"}]"#;
        let key_statuses = serde_json::from_str::<Vec<KeyStatus>>(payload).unwrap();
        let f1 = KeyStatus {
            fingerprint: String::from("D99AD936FC83C9BABDE7C33E1CF8C1A2076818C3"),
            status: ApprovalStatus::Approved,
        };
        let f2 = KeyStatus {
            fingerprint: String::from("95D6F0110B446D6EABFEA10C6B52BA2D01E77CA5"),
            status: ApprovalStatus::ApprovalRevoked,
        };
        assert!(
            &key_statuses
                .iter()
                .zip([f1, f2].iter())
                .all(|(a, b)| a == b)
        );
    }

    #[cfg(feature = "auth")]
    #[test]
    fn serialize_sts_payload() {
        assert_eq!(
            r#"{"dtr":42}"#,
            &serde_json::to_string(&Sts::Write { dtr: 42 }).unwrap()
        );
        assert_eq!(
            r#"{"resource":42}"#,
            &serde_json::to_string(&Sts::Read {
                resource: StsResource::Dtr(42)
            })
            .unwrap()
        );
        assert_eq!(
            r#"{"resource":"foo.zip"}"#,
            &serde_json::to_string(&Sts::Read {
                resource: StsResource::ObjectName("foo.zip")
            })
            .unwrap()
        );
    }
}