bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
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",
    }
}