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
}