bucketwarden-server 0.1.0

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

pub(crate) fn parse_bucket_notification_configuration(
    bucket: &str,
    body: &[u8],
) -> Result<BucketNotificationConfiguration, RuntimeError> {
    let xml = String::from_utf8_lossy(body);
    let mut rules = Vec::new();
    parse_notification_rule_blocks(
        &xml,
        "QueueConfiguration",
        "queue",
        &["Queue", "QueueArn"],
        &mut rules,
    )?;
    parse_notification_rule_blocks(
        &xml,
        "TopicConfiguration",
        "topic",
        &["Topic", "TopicArn"],
        &mut rules,
    )?;
    parse_notification_rule_blocks(
        &xml,
        "CloudFunctionConfiguration",
        "lambda",
        &["CloudFunction", "LambdaFunction", "LambdaFunctionArn"],
        &mut rules,
    )?;
    parse_notification_rule_blocks(
        &xml,
        "LambdaFunctionConfiguration",
        "lambda",
        &["LambdaFunction", "LambdaFunctionArn", "CloudFunction"],
        &mut rules,
    )?;
    parse_eventbridge_configuration(&xml, &mut rules);
    validate_notification_rules(&rules)?;
    Ok(BucketNotificationConfiguration {
        bucket: bucket.to_string(),
        rules,
    })
}

fn parse_eventbridge_configuration(xml: &str, rules: &mut Vec<NotificationRule>) {
    if xml.contains("<EventBridgeConfiguration") {
        rules.push(NotificationRule {
            id: "eventbridge".to_string(),
            target_kind: "eventbridge".to_string(),
            target_arn: "arn:bucketwarden:eventbridge:default".to_string(),
            events: supported_notification_event_patterns()
                .iter()
                .map(|value| (*value).to_string())
                .collect(),
            filter_prefix: None,
            filter_suffix: None,
        });
    }
}

fn supported_notification_event_patterns() -> &'static [&'static str] {
    &[
        "s3:ObjectCreated:*",
        "s3:ObjectCreated:Put",
        "s3:ObjectCreated:Post",
        "s3:ObjectCreated:Copy",
        "s3:ObjectCreated:CompleteMultipartUpload",
        "s3:ObjectRemoved:*",
        "s3:ObjectRemoved:Delete",
        "s3:ObjectRemoved:DeleteMarkerCreated",
        "s3:ObjectRestore:*",
        "s3:ObjectRestore:Completed",
        "s3:Replication:*",
        "s3:Replication:ObjectReplicated",
        "s3:Replication:DeleteMarkerReplicated",
        "s3:Replication:ObjectDeleted",
        "s3:LifecycleExpiration:*",
        "s3:LifecycleExpiration:Delete",
        "s3:LifecycleExpiration:DeleteMarkerCreated",
        "s3:LifecycleExpiration:AbortMultipartUpload",
        "s3:ObjectLock:*",
        "s3:ObjectLock:LegalHoldUpdated",
        "s3:ObjectLock:RetentionUpdated",
    ]
}

pub(crate) fn parse_rule_tag_filter(
    rule_xml: &str,
) -> Result<BTreeMap<String, String>, RuntimeError> {
    let mut tags = BTreeMap::new();
    let mut remainder = rule_xml;
    while let Some(start) = remainder.find("<Tag>") {
        remainder = &remainder[start + "<Tag>".len()..];
        let Some(end) = remainder.find("</Tag>") else {
            return Err(RuntimeError::InvalidTagging("unclosed Tag".to_string()));
        };
        let tag_xml = &remainder[..end];
        remainder = &remainder[end + "</Tag>".len()..];
        let key = text_between(tag_xml, "<Key>", "</Key>")
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| RuntimeError::InvalidTagging("tag key is required".to_string()))?;
        let value = text_between(tag_xml, "<Value>", "</Value>")
            .map(str::trim)
            .ok_or_else(|| RuntimeError::InvalidTagging("tag value is required".to_string()))?;
        tags.insert(key.to_string(), value.to_string());
    }
    validate_tags(&tags)?;
    Ok(tags)
}

pub(crate) fn parse_notification_rule_blocks(
    xml: &str,
    block_tag: &str,
    target_kind: &str,
    target_tags: &[&str],
    rules: &mut Vec<NotificationRule>,
) -> Result<(), RuntimeError> {
    let open = format!("<{block_tag}>");
    let close = format!("</{block_tag}>");
    let mut remainder = xml;
    while let Some(start) = remainder.find(&open) {
        remainder = &remainder[start + open.len()..];
        let Some(end) = remainder.find(&close) else {
            return Err(RuntimeError::InvalidNotificationConfiguration(format!(
                "unclosed {block_tag}"
            )));
        };
        let rule_xml = &remainder[..end];
        remainder = &remainder[end + close.len()..];
        let id = text_between(rule_xml, "<Id>", "</Id>")
            .or_else(|| text_between(rule_xml, "<ID>", "</ID>"))
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .unwrap_or("notification")
            .to_string();
        let target_arn = target_tags
            .iter()
            .find_map(|target_tag| {
                text_between(
                    rule_xml,
                    &format!("<{target_tag}>"),
                    &format!("</{target_tag}>"),
                )
                .map(str::trim)
                .filter(|value| !value.is_empty())
            })
            .ok_or_else(|| {
                RuntimeError::InvalidNotificationConfiguration(format!(
                    "{} is required",
                    target_tags
                        .first()
                        .copied()
                        .unwrap_or("notification target")
                ))
            })?
            .to_string();
        let events = text_values(rule_xml, "Event");
        let (filter_prefix, filter_suffix) = parse_notification_filters(rule_xml);
        rules.push(NotificationRule {
            id,
            target_kind: target_kind.to_string(),
            target_arn,
            events,
            filter_prefix,
            filter_suffix,
        });
    }
    Ok(())
}

pub(crate) fn parse_notification_filters(rule_xml: &str) -> (Option<String>, Option<String>) {
    let filter_rules = parse_notification_filter_rules(rule_xml);
    let mut prefix = None;
    let mut suffix = None;
    for rule in filter_rules {
        match rule.name.as_str() {
            "prefix" => prefix = Some(rule.value),
            "suffix" => suffix = Some(rule.value),
            _ => {}
        }
    }
    (prefix, suffix)
}

pub(crate) fn parse_notification_filter_rules(rule_xml: &str) -> Vec<FilterRule> {
    let mut rules = Vec::new();
    let mut remainder = rule_xml;
    while let Some(start) = remainder.find("<FilterRule>") {
        remainder = &remainder[start + "<FilterRule>".len()..];
        let Some(end) = remainder.find("</FilterRule>") else {
            break;
        };
        let filter_xml = &remainder[..end];
        remainder = &remainder[end + "</FilterRule>".len()..];
        let name = text_between(filter_xml, "<Name>", "</Name>")
            .map(str::trim)
            .unwrap_or_default()
            .to_string();
        let value = text_between(filter_xml, "<Value>", "</Value>")
            .map(str::trim)
            .unwrap_or_default()
            .to_string();
        rules.push(FilterRule { name, value });
    }
    rules
}