use super::*;
pub(crate) fn parse_multi_object_delete(
bucket: &str,
body: &[u8],
) -> Result<MultiObjectDeleteRequest, RuntimeError> {
let delete = parse_delete_payload(body)?;
Ok(MultiObjectDeleteRequest {
bucket: bucket.to_string(),
quiet: delete.quiet,
objects: delete
.objects
.into_iter()
.map(|entry| MultiObjectDeleteTarget {
key: entry.key,
version_id: entry.version_id,
})
.collect(),
})
}
pub(crate) fn parse_delete_payload(body: &[u8]) -> Result<Delete, RuntimeError> {
let xml = String::from_utf8_lossy(body);
if !xml.contains("<Delete") {
return Err(RuntimeError::InvalidMultiObjectDelete(
"missing Delete root element".to_string(),
));
}
let quiet = text_between(&xml, "<Quiet>", "</Quiet>")
.is_some_and(|value| value.trim().eq_ignore_ascii_case("true"));
let mut objects = Vec::new();
let mut remainder = xml.as_ref();
while let Some(start) = remainder.find("<Object>") {
remainder = &remainder[start + "<Object>".len()..];
let Some(end) = remainder.find("</Object>") else {
return Err(RuntimeError::InvalidMultiObjectDelete(
"unterminated Object element".to_string(),
));
};
let object_xml = &remainder[..end];
remainder = &remainder[end + "</Object>".len()..];
let key = text_between(object_xml, "<Key>", "</Key>")
.map(str::trim)
.filter(|key| !key.is_empty())
.ok_or_else(|| {
RuntimeError::InvalidMultiObjectDelete("Object is missing Key".to_string())
})?;
if !validate_object_key(key) {
return Err(RuntimeError::InvalidObjectKey(key.to_string()));
}
objects.push(DeleteObjectEntry {
key: key.to_string(),
version_id: text_between(object_xml, "<VersionId>", "</VersionId>")
.map(str::trim)
.filter(|version_id| !version_id.is_empty())
.map(str::to_string),
});
}
if objects.is_empty() {
return Err(RuntimeError::InvalidMultiObjectDelete(
"no Object elements found".to_string(),
));
}
Ok(Delete {
quiet,
objects,
})
}
pub(crate) fn multi_object_delete_xml(result: &MultiObjectDeleteResult) -> String {
let deleted = result
.deleted
.iter()
.map(|entry| {
let deleted_object = DeletedObject {
key: entry.key.clone(),
version_id: entry.version_id.clone(),
delete_marker: entry.delete_marker,
delete_marker_version_id: entry.delete_marker_version_id.clone(),
};
let version_id = deleted_object
.version_id
.as_ref()
.map(|value| format!("<VersionId>{}</VersionId>", xml_escape(value)))
.unwrap_or_default();
let delete_marker_version_id = deleted_object
.delete_marker_version_id
.as_ref()
.map(|value| {
format!(
"<DeleteMarkerVersionId>{}</DeleteMarkerVersionId>",
xml_escape(value)
)
})
.unwrap_or_default();
format!(
"<Deleted><Key>{}</Key>{version_id}<DeleteMarker>{}</DeleteMarker>{delete_marker_version_id}</Deleted>",
xml_escape(&deleted_object.key),
deleted_object.delete_marker
)
})
.collect::<String>();
let errors = result
.errors
.iter()
.map(|entry| {
let details = ErrorDetails {
code: entry.code.clone(),
message: entry.message.clone(),
};
let version_id = entry
.version_id
.as_ref()
.map(|value| format!("<VersionId>{}</VersionId>", xml_escape(value)))
.unwrap_or_default();
format!(
"<Error><Key>{}</Key>{version_id}<Code>{}</Code><Message>{}</Message></Error>",
xml_escape(&entry.key),
xml_escape(&details.code),
xml_escape(&details.message)
)
})
.collect::<String>();
format!("<DeleteResult>{deleted}{errors}</DeleteResult>")
}
pub(crate) fn multi_object_delete_error_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::AccessDenied { .. } | RuntimeError::ObjectLocked(_) => "AccessDenied",
RuntimeError::NoSuchBucket(_) => "NoSuchBucket",
RuntimeError::NoSuchKey(_) => "NoSuchKey",
RuntimeError::NoSuchVersion { .. } => "NoSuchVersion",
RuntimeError::InvalidObjectKey(_) => "InvalidObjectKey",
_ => "InternalError",
}
}