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(())
}