bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
use super::*;

pub(crate) fn parse_bucket_lifecycle_configuration(
    bucket: &str,
    body: &[u8],
) -> Result<BucketLifecycleConfiguration, RuntimeError> {
    let xml = String::from_utf8_lossy(body);
    let mut rules = Vec::new();
    let mut remainder = xml.as_ref();
    while let Some(start) = remainder.find("<Rule>") {
        remainder = &remainder[start + "<Rule>".len()..];
        let Some(end) = remainder.find("</Rule>") else {
            return Err(RuntimeError::InvalidLifecycleConfiguration(
                "unclosed Rule".to_string(),
            ));
        };
        let rule_xml = &remainder[..end];
        remainder = &remainder[end + "</Rule>".len()..];
        let id = text_between(rule_xml, "<ID>", "</ID>")
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or("rule")
            .to_string();
        let prefix = text_between(rule_xml, "<Prefix>", "</Prefix>")
            .or_else(|| text_between(rule_xml, "<Filter><Prefix>", "</Prefix></Filter>"))
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string);
        let tag_filter = parse_rule_tag_filter(rule_xml)?;
        let object_size_greater_than = text_between(
            rule_xml,
            "<ObjectSizeGreaterThan>",
            "</ObjectSizeGreaterThan>",
        )
        .map(str::trim)
        .map(parse_positive_u64)
        .transpose()?;
        let object_size_less_than =
            text_between(rule_xml, "<ObjectSizeLessThan>", "</ObjectSizeLessThan>")
                .map(str::trim)
                .map(parse_positive_u64)
                .transpose()?;
        let status = text_between(rule_xml, "<Status>", "</Status>")
            .map(str::trim)
            .unwrap_or("Disabled")
            .to_string();
        let expiration_days = text_between(rule_xml, "<Expiration>", "</Expiration>")
            .and_then(|xml| text_between(xml, "<Days>", "</Days>"))
            .map(str::trim)
            .map(parse_positive_u64)
            .transpose()?;
        let noncurrent_expiration_days = text_between(
            rule_xml,
            "<NoncurrentVersionExpiration>",
            "</NoncurrentVersionExpiration>",
        )
        .and_then(|xml| text_between(xml, "<NoncurrentDays>", "</NoncurrentDays>"))
        .map(str::trim)
        .map(parse_positive_u64)
        .transpose()?;
        let abort_incomplete_multipart_upload_days = text_between(
            rule_xml,
            "<AbortIncompleteMultipartUpload>",
            "</AbortIncompleteMultipartUpload>",
        )
        .and_then(|xml| text_between(xml, "<DaysAfterInitiation>", "</DaysAfterInitiation>"))
        .map(str::trim)
        .map(parse_positive_u64)
        .transpose()?;
        rules.push(LifecycleRule {
            id,
            prefix,
            tag_filter,
            object_size_greater_than,
            object_size_less_than,
            status,
            expiration_days,
            noncurrent_expiration_days,
            abort_incomplete_multipart_upload_days,
        });
    }
    validate_lifecycle_rules(&rules)?;
    Ok(BucketLifecycleConfiguration {
        bucket: bucket.to_string(),
        rules,
    })
}

pub(crate) fn validate_lifecycle_rules(rules: &[LifecycleRule]) -> Result<(), RuntimeError> {
    if rules.is_empty() {
        return Err(RuntimeError::InvalidLifecycleConfiguration(
            "at least one lifecycle Rule is required".to_string(),
        ));
    }
    for rule in rules {
        if rule.status != "Enabled" && rule.status != "Disabled" {
            return Err(RuntimeError::InvalidLifecycleConfiguration(format!(
                "unsupported lifecycle status: {}",
                rule.status
            )));
        }
        if rule.expiration_days.is_none()
            && rule.noncurrent_expiration_days.is_none()
            && rule.abort_incomplete_multipart_upload_days.is_none()
        {
            return Err(RuntimeError::InvalidLifecycleConfiguration(format!(
                "rule {} has no supported action",
                rule.id
            )));
        }
        if rule
            .object_size_greater_than
            .zip(rule.object_size_less_than)
            .is_some_and(|(min, max)| min >= max)
        {
            return Err(RuntimeError::InvalidLifecycleConfiguration(format!(
                "rule {} has an invalid object size filter range",
                rule.id
            )));
        }
    }
    Ok(())
}