Fix S3 versioning (live-object VersionId, DM PUT/DELETE), harden DeleteObjects/ListObjects conformance, and run hot paths on blocking threads

This commit is contained in:
2026-04-23 22:40:38 +08:00
parent bd405cc2fe
commit f2df64479c
9 changed files with 994 additions and 221 deletions

View File

@@ -86,6 +86,11 @@ pub fn parse_complete_multipart_upload(xml: &str) -> Result<CompleteMultipartUpl
}
pub fn parse_delete_objects(xml: &str) -> Result<DeleteObjectsRequest, String> {
let trimmed = xml.trim();
if trimmed.is_empty() {
return Err("Request body is empty".to_string());
}
let mut reader = Reader::from_str(xml);
let mut result = DeleteObjectsRequest::default();
let mut buf = Vec::new();
@@ -93,18 +98,43 @@ pub fn parse_delete_objects(xml: &str) -> Result<DeleteObjectsRequest, String> {
let mut current_key: Option<String> = None;
let mut current_version_id: Option<String> = None;
let mut in_object = false;
let mut saw_delete_root = false;
let mut first_element_seen = false;
loop {
match reader.read_event_into(&mut buf) {
let event = reader.read_event_into(&mut buf);
match event {
Ok(Event::Start(ref e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
current_tag = name.clone();
if name == "Object" {
if !first_element_seen {
first_element_seen = true;
if name != "Delete" {
return Err(format!(
"Expected <Delete> root element, found <{}>",
name
));
}
saw_delete_root = true;
} else if name == "Object" {
in_object = true;
current_key = None;
current_version_id = None;
}
}
Ok(Event::Empty(ref e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
if !first_element_seen {
first_element_seen = true;
if name != "Delete" {
return Err(format!(
"Expected <Delete> root element, found <{}>",
name
));
}
saw_delete_root = true;
}
}
Ok(Event::Text(ref e)) => {
let text = e.unescape().map_err(|e| e.to_string())?.to_string();
match current_tag.as_str() {
@@ -139,6 +169,13 @@ pub fn parse_delete_objects(xml: &str) -> Result<DeleteObjectsRequest, String> {
buf.clear();
}
if !saw_delete_root {
return Err("Expected <Delete> root element".to_string());
}
if result.objects.is_empty() {
return Err("Delete request must contain at least one <Object>".to_string());
}
Ok(result)
}

View File

@@ -8,10 +8,21 @@ pub fn format_s3_datetime(dt: &DateTime<Utc>) -> String {
dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}
pub fn rate_limit_exceeded_xml() -> String {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Error><Code>SlowDown</Code><Message>Rate limit exceeded</Message><Resource></Resource><RequestId></RequestId></Error>"
.to_string()
pub fn rate_limit_exceeded_xml(resource: &str, request_id: &str) -> String {
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Error><Code>SlowDown</Code><Message>Please reduce your request rate</Message><Resource>{}</Resource><RequestId>{}</RequestId></Error>",
xml_escape(resource),
xml_escape(request_id),
)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
pub fn list_buckets_xml(owner_id: &str, owner_name: &str, buckets: &[BucketMeta]) -> String {