diff --git a/src/actions/list_objects_v2.rs b/src/actions/list_objects_v2.rs index 19e768c..435bfe3 100644 --- a/src/actions/list_objects_v2.rs +++ b/src/actions/list_objects_v2.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::time::Duration; +use percent_encoding::percent_decode; use serde::Deserialize; use time::OffsetDateTime; use url::Url; @@ -47,8 +48,8 @@ pub struct ListObjectsV2Response { pub max_keys: Option, #[serde(rename = "CommonPrefixes", default)] pub common_prefixes: Vec, - // #[serde(rename = "EncodingType")] - // encoding_type: String, + #[serde(rename = "EncodingType")] + encoding_type: String, // #[serde(rename = "KeyCount")] // key_count: u16, // #[serde(rename = "ContinuationToken")] @@ -164,6 +165,7 @@ impl<'a> ListObjectsV2<'a> { pub fn parse_response(s: &str) -> Result { let mut parsed: ListObjectsV2Response = quick_xml::de::from_str(s)?; + let url_encoded = parsed.encoding_type == "url"; // S3 returns an Owner with an empty DisplayName and ID when fetch-owner is disabled for content in parsed.contents.iter_mut() { @@ -172,6 +174,11 @@ impl<'a> ListObjectsV2<'a> { content.owner = None; } } + if url_encoded { + if let Ok(Cow::Owned(s)) = percent_decode(content.key.as_bytes()).decode_utf8() { + content.key = s; + } + } } Ok(parsed) @@ -372,4 +379,40 @@ mod tests { assert!(parsed.next_continuation_token.is_none()); assert!(parsed.start_after.is_none()); } + + #[test] + fn parse_url_encoded_key() { + let input = r#" + + + test + + 0 + 4500 + + false + + 100%25tamo.jpg + 2020-12-01T20:43:11.794Z + "bfd537a51d15208163231b0711e0b1f3" + 4274 + + + + + STANDARD + + url + + "#; + + let parsed = ListObjectsV2::parse_response(input).unwrap(); + assert_eq!(parsed.contents.len(), 1); + assert_eq!(parsed.contents[0].key, "100%tamo.jpg"); + + assert_eq!(parsed.max_keys, Some(4500)); + assert!(parsed.common_prefixes.is_empty()); + assert!(parsed.next_continuation_token.is_none()); + assert!(parsed.start_after.is_none()); + } }