Skip to content

Commit 3da3c6e

Browse files
committed
bugfix: proper handing of whitespace char in url with Form-decoding instead of Percent-decoding
1 parent e244229 commit 3da3c6e

13 files changed

+248
-126
lines changed

common/src/cleanup_guard.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,18 @@ impl CleanupGuard {
3737

3838
pub async fn cleanup(client: Client, bucket_name: &str) {
3939
tokio::select!(
40-
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
41-
eprintln!("Cleanup timeout after 60s while removing bucket {}", bucket_name);
42-
},
43-
outcome = client.delete_and_purge_bucket(bucket_name) => {
44-
match outcome {
45-
Ok(_) => {
46-
eprintln!("Bucket {} removed successfully", bucket_name);
47-
}
48-
Err(e) => {
49-
eprintln!("Error removing bucket {}: {:?}", bucket_name, e);
50-
}
51-
}
40+
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
41+
eprintln!("Cleanup timeout after 60s while removing bucket {}", bucket_name);
42+
},
43+
outcome = client.delete_and_purge_bucket(bucket_name) => {
44+
match outcome {
45+
Ok(_) => {
46+
//eprintln!("Bucket {} removed successfully", bucket_name);
5247
}
48+
Err(e) => {
49+
eprintln!("Error removing bucket {}: {:?}", bucket_name, e);
50+
}
51+
}
52+
}
5353
);
5454
}

src/s3/builders/copy_object.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::s3::response::{
2626
use crate::s3::sse::{Sse, SseCustomerKey};
2727
use crate::s3::types::{Directive, PartInfo, Retention, S3Api, S3Request, ToS3Request};
2828
use crate::s3::utils::{
29-
UtcTime, check_bucket_name, check_object_name, to_http_header_value, to_iso8601utc, urlencode,
29+
UtcTime, check_bucket_name, check_object_name, to_http_header_value, to_iso8601utc, url_encode,
3030
};
3131
use async_recursion::async_recursion;
3232
use http::Method;
@@ -254,9 +254,9 @@ impl ToS3Request for CopyObjectInternal {
254254
if !tagging.is_empty() {
255255
tagging.push('&');
256256
}
257-
tagging.push_str(&urlencode(key));
257+
tagging.push_str(&url_encode(key));
258258
tagging.push('=');
259-
tagging.push_str(&urlencode(value));
259+
tagging.push_str(&url_encode(value));
260260
}
261261
if !tagging.is_empty() {
262262
headers.add("x-amz-tagging", tagging);
@@ -285,7 +285,7 @@ impl ToS3Request for CopyObjectInternal {
285285
copy_source.push_str(&self.source.object);
286286
if let Some(v) = &self.source.version_id {
287287
copy_source.push_str("?versionId=");
288-
copy_source.push_str(&urlencode(v));
288+
copy_source.push_str(&url_encode(v));
289289
}
290290
headers.add("x-amz-copy-source", copy_source);
291291

@@ -1032,7 +1032,7 @@ impl ComposeSource {
10321032
copy_source.push_str(&self.object);
10331033
if let Some(v) = &self.version_id {
10341034
copy_source.push_str("?versionId=");
1035-
copy_source.push_str(&urlencode(v));
1035+
copy_source.push_str(&url_encode(v));
10361036
}
10371037
headers.add("x-amz-copy-source", copy_source);
10381038

@@ -1155,9 +1155,9 @@ fn into_headers_copy_object(
11551155
if !tagging.is_empty() {
11561156
tagging.push('&');
11571157
}
1158-
tagging.push_str(&urlencode(key));
1158+
tagging.push_str(&url_encode(key));
11591159
tagging.push('=');
1160-
tagging.push_str(&urlencode(value));
1160+
tagging.push_str(&url_encode(value));
11611161
}
11621162

11631163
if !tagging.is_empty() {

src/s3/builders/put_object.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::s3::{
2929
},
3030
sse::Sse,
3131
types::{PartInfo, Retention, S3Api, S3Request, ToS3Request},
32-
utils::{check_bucket_name, md5sum_hash, to_iso8601utc, urlencode},
32+
utils::{check_bucket_name, md5sum_hash, to_iso8601utc, url_encode},
3333
};
3434
use bytes::{Bytes, BytesMut};
3535
use http::Method;
@@ -201,7 +201,7 @@ impl ToS3Request for AbortMultipartUpload {
201201

202202
let headers: Multimap = self.extra_headers.unwrap_or_default();
203203
let mut query_params: Multimap = self.extra_query_params.unwrap_or_default();
204-
query_params.add("uploadId", urlencode(&self.upload_id).to_string());
204+
query_params.add("uploadId", url_encode(&self.upload_id).to_string());
205205

206206
Ok(S3Request::new(self.client, Method::DELETE)
207207
.region(self.region)
@@ -885,9 +885,9 @@ fn into_headers_put_object(
885885
if !tagging.is_empty() {
886886
tagging.push('&');
887887
}
888-
tagging.push_str(&urlencode(key));
888+
tagging.push_str(&url_encode(key));
889889
tagging.push('=');
890-
tagging.push_str(&urlencode(value));
890+
tagging.push_str(&url_encode(value));
891891
}
892892

893893
if !tagging.is_empty() {

src/s3/client/delete_bucket.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use super::Client;
1717
use crate::s3::builders::{DeleteBucket, DeleteObject, ObjectToDelete};
1818
use crate::s3::error::{Error, ErrorCode};
19-
use crate::s3::response::DeleteResult;
19+
use crate::s3::response::{BucketExistsResponse, DeleteResult};
2020
use crate::s3::response::{
2121
DeleteBucketResponse, DeleteObjectResponse, DeleteObjectsResponse, PutObjectLegalHoldResponse,
2222
};
@@ -57,6 +57,17 @@ impl Client {
5757
bucket: S,
5858
) -> Result<DeleteBucketResponse, Error> {
5959
let bucket: String = bucket.into();
60+
61+
let resp: BucketExistsResponse = self.bucket_exists(&bucket).send().await?;
62+
if !resp.exists {
63+
// if the bucket does not exist, we can return early
64+
return Ok(DeleteBucketResponse {
65+
request: Default::default(), //TODO consider how to handle this
66+
body: Bytes::new(),
67+
headers: Default::default(),
68+
});
69+
}
70+
6071
let is_express = self.is_minio_express().await;
6172

6273
let mut stream = self

src/s3/multimap.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
use crate::s3::utils::urlencode;
16+
use crate::s3::utils::url_encode;
1717
use lazy_static::lazy_static;
1818
use multimap::MultiMap;
1919
use regex::Regex;
2020
use std::collections::BTreeMap;
21-
pub use urlencoding::decode as urldecode;
2221

2322
/// Multimap for string key and string value
2423
pub type Multimap = MultiMap<String, String>;
@@ -71,9 +70,9 @@ impl MultimapExt for Multimap {
7170
if !query.is_empty() {
7271
query.push('&');
7372
}
74-
query.push_str(&urlencode(key));
73+
query.push_str(&url_encode(key));
7574
query.push('=');
76-
query.push_str(&urlencode(value));
75+
query.push_str(&url_encode(value));
7776
}
7877
}
7978
query
@@ -94,9 +93,9 @@ impl MultimapExt for Multimap {
9493
if !query.is_empty() {
9594
query.push('&');
9695
}
97-
query.push_str(&urlencode(key.as_str()));
96+
query.push_str(&url_encode(key.as_str()));
9897
query.push('=');
99-
query.push_str(&urlencode(value));
98+
query.push_str(&url_encode(value));
10099
}
101100
}
102101
None => todo!(), // This never happens.

src/s3/response/list_objects.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,26 @@ use crate::s3::error::Error;
1515
use crate::s3::response::a_response_traits::HasS3Fields;
1616
use crate::s3::types::{FromS3Response, ListEntry, S3Request};
1717
use crate::s3::utils::xml::{Element, MergeXmlElements};
18-
use crate::s3::utils::{from_iso8601utc, parse_tags, urldecode};
18+
use crate::s3::utils::{from_iso8601utc, parse_tags, url_decode};
1919
use async_trait::async_trait;
2020
use bytes::{Buf, Bytes};
2121
use reqwest::header::HeaderMap;
2222
use std::collections::HashMap;
2323
use std::mem;
2424

25-
fn url_decode(
25+
fn url_decode_w_enc(
2626
encoding_type: &Option<String>,
27-
prefix: Option<String>,
27+
s: Option<String>,
2828
) -> Result<Option<String>, Error> {
2929
if let Some(v) = encoding_type.as_ref() {
3030
if v == "url" {
31-
if let Some(v) = prefix {
32-
return Ok(Some(urldecode(&v)?.to_string()));
31+
if let Some(raw) = s {
32+
return Ok(Some(url_decode(&raw).to_string()));
3333
}
3434
}
3535
}
3636

37-
if let Some(v) = prefix.as_ref() {
37+
if let Some(v) = s.as_ref() {
3838
return Ok(Some(v.to_string()));
3939
}
4040

@@ -56,7 +56,7 @@ fn parse_common_list_objects_response(
5656
Error,
5757
> {
5858
let encoding_type = root.get_child_text("EncodingType");
59-
let prefix = url_decode(
59+
let prefix = url_decode_w_enc(
6060
&encoding_type,
6161
Some(root.get_child_text("Prefix").unwrap_or_default()),
6262
)?;
@@ -90,7 +90,7 @@ fn parse_list_objects_contents(
9090
let merged = MergeXmlElements::new(&children1, &children2);
9191
for content in merged {
9292
let etype = encoding_type.as_ref().cloned();
93-
let key = url_decode(&etype, Some(content.get_child_text_or_error("Key")?))?.unwrap();
93+
let key = url_decode_w_enc(&etype, Some(content.get_child_text_or_error("Key")?))?.unwrap();
9494
let last_modified = Some(from_iso8601utc(
9595
&content.get_child_text_or_error("LastModified")?,
9696
)?);
@@ -156,7 +156,7 @@ fn parse_list_objects_common_prefixes(
156156
) -> Result<(), Error> {
157157
for (_, common_prefix) in root.get_matching_children("CommonPrefixes") {
158158
contents.push(ListEntry {
159-
name: url_decode(
159+
name: url_decode_w_enc(
160160
encoding_type,
161161
Some(common_prefix.get_child_text_or_error("Prefix")?),
162162
)?
@@ -214,8 +214,8 @@ impl FromS3Response for ListObjectsV1Response {
214214
let root = Element::from(&xmltree_root);
215215
let (name, encoding_type, prefix, delimiter, is_truncated, max_keys) =
216216
parse_common_list_objects_response(&root)?;
217-
let marker = url_decode(&encoding_type, root.get_child_text("Marker"))?;
218-
let mut next_marker = url_decode(&encoding_type, root.get_child_text("NextMarker"))?;
217+
let marker = url_decode_w_enc(&encoding_type, root.get_child_text("Marker"))?;
218+
let mut next_marker = url_decode_w_enc(&encoding_type, root.get_child_text("NextMarker"))?;
219219
let mut contents: Vec<ListEntry> = Vec::new();
220220
parse_list_objects_contents(&mut contents, &root, "Contents", &encoding_type, false)?;
221221
if is_truncated && next_marker.is_none() {
@@ -281,7 +281,7 @@ impl FromS3Response for ListObjectsV2Response {
281281
.get_child_text("KeyCount")
282282
.map(|x| x.parse::<u16>())
283283
.transpose()?;
284-
let start_after = url_decode(&encoding_type, root.get_child_text("StartAfter"))?;
284+
let start_after = url_decode_w_enc(&encoding_type, root.get_child_text("StartAfter"))?;
285285
let continuation_token = root.get_child_text("ContinuationToken");
286286
let next_continuation_token = root.get_child_text("NextContinuationToken");
287287
let mut contents: Vec<ListEntry> = Vec::new();
@@ -344,8 +344,9 @@ impl FromS3Response for ListObjectVersionsResponse {
344344
let root = Element::from(&xmltree_root);
345345
let (name, encoding_type, prefix, delimiter, is_truncated, max_keys) =
346346
parse_common_list_objects_response(&root)?;
347-
let key_marker = url_decode(&encoding_type, root.get_child_text("KeyMarker"))?;
348-
let next_key_marker = url_decode(&encoding_type, root.get_child_text("NextKeyMarker"))?;
347+
let key_marker = url_decode_w_enc(&encoding_type, root.get_child_text("KeyMarker"))?;
348+
let next_key_marker =
349+
url_decode_w_enc(&encoding_type, root.get_child_text("NextKeyMarker"))?;
349350
let version_id_marker = root.get_child_text("VersionIdMarker");
350351
let next_version_id_marker = root.get_child_text("NextVersionIdMarker");
351352
let mut contents: Vec<ListEntry> = Vec::new();

src/s3/utils.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,41 @@ use regex::Regex;
3232
use ring::digest::{Context, SHA256};
3333
#[cfg(not(feature = "ring"))]
3434
use sha2::{Digest, Sha256};
35+
use std::borrow::Cow;
3536
use std::collections::HashMap;
37+
use std::string::FromUtf8Error;
3638
use std::sync::Arc;
37-
pub use urlencoding::decode as urldecode;
38-
pub use urlencoding::encode as urlencode;
3939
use xmltree::Element;
4040

4141
/// Date and time with UTC timezone
4242
pub type UtcTime = DateTime<Utc>;
4343

44+
use url::form_urlencoded;
45+
46+
// Great stuff to get confused about.
47+
// String "a b+c" in Percent-Encoding (RFC 3986) becomes "a%20b%2Bc".
48+
// S3 sometimes returns Form-Encoding (application/x-www-form-urlencoded) rendering string "a%20b%2Bc" into "a+b%2Bc"
49+
// If you were to do Percent-Decoding on "a+b%2Bc" you would get "a+b+c", which is wrong.
50+
// If you use Form-Decoding on "a+b%2Bc" you would get "a b+c", which is correct.
51+
52+
/// Decodes a URL-encoded string, TODO
53+
pub fn url_decode_old(s: &str) -> Result<Cow<str>, FromUtf8Error> {
54+
urlencoding::decode(s) //NOTE: Unencoded + is preserved literally, and not changed to a space.
55+
}
56+
57+
/// Decodes a URL-encoded string, TODO
58+
pub fn url_decode(s: &str) -> Cow<str> {
59+
let result: String = form_urlencoded::parse(s.as_bytes())
60+
.map(|(k, _)| k)
61+
.collect();
62+
Cow::Owned(result)
63+
}
64+
65+
/// Encodes a string using URL encoding, TODO
66+
pub fn url_encode(s: &str) -> Cow<str> {
67+
urlencoding::encode(s)
68+
}
69+
4470
/// Encodes data using base64 algorithm
4571
pub fn b64encode(input: impl AsRef<[u8]>) -> String {
4672
BASE64.encode(input)
@@ -245,7 +271,7 @@ pub fn match_region(value: &str) -> bool {
245271
|| value.ends_with('_')
246272
}
247273

248-
/// Validates given bucket name
274+
/// Validates given bucket name. TODO S3Express has slightly different rules for bucket names
249275
pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(), Error> {
250276
let bucket_name: &str = bucket_name.as_ref().trim();
251277
let bucket_name_len = bucket_name.len();
@@ -302,6 +328,7 @@ pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(
302328
Ok(())
303329
}
304330

331+
/// Validates given object name. TODO S3Express has slightly different rules for object names
305332
pub fn check_object_name(object_name: impl AsRef<str>) -> Result<(), Error> {
306333
let object_name: &str = object_name.as_ref();
307334
let object_name_n_bytes = object_name.len();

0 commit comments

Comments
 (0)