diff --git a/crates/s3-client/src/api_list.rs b/crates/s3-client/src/api_list.rs index c2771d081..dab74cf84 100644 --- a/crates/s3-client/src/api_list.rs +++ b/crates/s3-client/src/api_list.rs @@ -471,4 +471,58 @@ mod tests { assert_eq!(parsed.delete_markers.len(), 1); assert_eq!(parsed.delete_markers[0].version_id, "marker-a"); } + + // Regression test for backlog#2076: a ListObjectsV2 response for a delimited + // listing over a bucket that holds nested-key objects (e.g. any warm-tier + // target that already stores more than one flat object) includes a + // ... element. `CommonPrefix` + // previously had no `rename_all = "PascalCase"`, so quick_xml looked for a + // lowercase `` child, never found one, and (with no `#[serde(default)]` + // either) failed the whole response with "missing field `prefix`" — surfacing to + // callers of `WarmBackendS3::in_use()` (tier add/remove) as `TierPermErr`. + #[test] + fn list_objects_v2_xml_parses_common_prefixes() { + let xml = br#" + + warm-bucket + + / + 1 + false + + subdir/ + + + "#; + + let parsed = quick_xml::de::from_reader::<_, ListBucketV2Result>(xml.as_slice()).expect("ListObjectsV2 XML should parse"); + + assert_eq!(parsed.common_prefixes.len(), 1); + assert_eq!(parsed.common_prefixes[0].prefix, "subdir/"); + } + + // Same fixture shape as list_object_versions_query hits (ListVersionsResult + // reuses the same CommonPrefix type). + #[test] + fn list_object_versions_xml_parses_common_prefixes() { + let xml = br#" + + warm-bucket + + + + 1 + false + + subdir/ + + + "#; + + let parsed = + quick_xml::de::from_reader::<_, ListVersionsResult>(xml.as_slice()).expect("ListObjectVersions XML should parse"); + + assert_eq!(parsed.common_prefixes.len(), 1); + assert_eq!(parsed.common_prefixes[0].prefix, "subdir/"); + } } diff --git a/crates/s3-client/src/api_s3_datatypes.rs b/crates/s3-client/src/api_s3_datatypes.rs index d17eb6cbb..d7797e96b 100644 --- a/crates/s3-client/src/api_s3_datatypes.rs +++ b/crates/s3-client/src/api_s3_datatypes.rs @@ -30,6 +30,7 @@ use crate::utils::base64_decode; use super::transition_api; #[derive(Debug, Default, Serialize, Deserialize)] +#[serde(default, rename_all = "PascalCase")] pub struct CommonPrefix { pub prefix: String, }