|
6 | 6 | //! + [Bucket naming rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html) |
7 | 7 |
|
8 | 8 | use crate::validation::{AwsNameValidation, NameValidation}; |
| 9 | +#[cfg(feature = "normalize_forward_slash")] |
| 10 | +use std::borrow::Cow; |
9 | 11 | use std::net::IpAddr; |
10 | 12 |
|
11 | 13 | /// A path in the S3 storage |
@@ -156,6 +158,40 @@ pub const fn check_key(key: &str) -> bool { |
156 | 158 | key.len() <= 1024 |
157 | 159 | } |
158 | 160 |
|
| 161 | +/// Normalizes a path: |
| 162 | + /// - If the path does not start with `/`, no normalization is performed. |
| 163 | + /// - If the path starts with `/`, normalization: |
| 164 | + /// - removes all leading slashes (unless the entire path is one or more `/`) |
| 165 | + /// - replaces consecutive internal slashes with a single slash |
| 166 | + /// - reduces one or more trailing slashes to a single trailing slash |
| 167 | +/// - Examples: |
| 168 | +/// - "/" -> "/" |
| 169 | +/// - "/keyname" -> "keyname" |
| 170 | +/// - "//keyname" -> "keyname" |
| 171 | +/// - "//keyname/" -> "keyname/" |
| 172 | +/// - "//dir////keyname" -> "dir/keyname" |
| 173 | +/// - "dir///sub//file" -> "dir///sub//file" |
| 174 | +/// - "/dir///sub//file" -> "dir/sub/file" |
| 175 | +#[cfg(feature = "normalize_forward_slash")] |
| 176 | +fn normalize_path(path: &str) -> Cow<'_, str> { |
| 177 | + if !path.starts_with('/') { |
| 178 | + return Cow::Borrowed(path); |
| 179 | + } |
| 180 | + let end_with_slash = path.ends_with('/'); |
| 181 | + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); |
| 182 | + |
| 183 | + if parts.is_empty() { |
| 184 | + Cow::Borrowed("/") |
| 185 | + } else { |
| 186 | + let joined = parts.join("/"); |
| 187 | + if end_with_slash { |
| 188 | + Cow::Owned(format!("{joined}/")) |
| 189 | + } else { |
| 190 | + Cow::Owned(joined) |
| 191 | + } |
| 192 | + } |
| 193 | +} |
| 194 | + |
159 | 195 | /// Parses a path-style request |
160 | 196 | /// # Errors |
161 | 197 | /// Returns an `Err` if the s3 path is invalid |
@@ -185,6 +221,11 @@ pub fn parse_path_style_with_validation(uri_path: &str, validation: &dyn NameVal |
185 | 221 |
|
186 | 222 | let Some(key) = key else { return Ok(S3Path::bucket(bucket)) }; |
187 | 223 |
|
| 224 | + #[cfg(feature = "normalize_forward_slash")] |
| 225 | + let normalized_key = normalize_path(key); |
| 226 | + #[cfg(feature = "normalize_forward_slash")] |
| 227 | + let key = normalized_key.as_ref(); |
| 228 | + |
188 | 229 | if !check_key(key) { |
189 | 230 | return Err(ParseS3PathError::KeyTooLong); |
190 | 231 | } |
@@ -219,6 +260,11 @@ pub fn parse_virtual_hosted_style_with_validation( |
219 | 260 | return Ok(S3Path::Bucket { bucket: bucket.into() }); |
220 | 261 | } |
221 | 262 |
|
| 263 | + #[cfg(feature = "normalize_forward_slash")] |
| 264 | + let normalized_key = normalize_path(key); |
| 265 | + #[cfg(feature = "normalize_forward_slash")] |
| 266 | + let key = normalized_key.as_ref(); |
| 267 | + |
222 | 268 | if !check_key(key) { |
223 | 269 | return Err(ParseS3PathError::KeyTooLong); |
224 | 270 | } |
@@ -393,6 +439,46 @@ mod tests { |
393 | 439 | assert_eq!(result1.is_err(), result2.is_err()); |
394 | 440 | } |
395 | 441 |
|
| 442 | + #[test] |
| 443 | + #[cfg(feature = "normalize_forward_slash")] |
| 444 | + fn test_path_style_normalize_forward_slash() { |
| 445 | + let cases = [ |
| 446 | + ("/bucket-name//", Ok(S3Path::object("bucket-name", "/"))), |
| 447 | + ("/bucket-name/object-name", Ok(S3Path::object("bucket-name", "object-name"))), |
| 448 | + ("/bucket-name//object-name", Ok(S3Path::object("bucket-name", "object-name"))), |
| 449 | + ("/bucket-name///object-name/", Ok(S3Path::object("bucket-name", "object-name/"))), |
| 450 | + ("/bucket-name/dir/object-name", Ok(S3Path::object("bucket-name", "dir/object-name"))), |
| 451 | + ("/bucket-name/dir//object-name", Ok(S3Path::object("bucket-name", "dir//object-name"))), |
| 452 | + ("/bucket-name//dir/object-name/", Ok(S3Path::object("bucket-name", "dir/object-name/"))), |
| 453 | + ]; |
| 454 | + |
| 455 | + for (uri_path, expected) in cases { |
| 456 | + assert_eq!(parse_path_style_with_validation(uri_path, &AwsNameValidation::new()), expected); |
| 457 | + } |
| 458 | + } |
| 459 | + |
| 460 | + #[test] |
| 461 | + #[cfg(feature = "normalize_forward_slash")] |
| 462 | + fn test_virtual_hosted_style_normalize_forward_slash() { |
| 463 | + let cases = [ |
| 464 | + ("/", Ok(S3Path::bucket("bucket-name"))), |
| 465 | + ("//", Ok(S3Path::object("bucket-name", "/"))), |
| 466 | + ("///", Ok(S3Path::object("bucket-name", "/"))), |
| 467 | + ("/object-name", Ok(S3Path::object("bucket-name", "object-name"))), |
| 468 | + ("//object-name", Ok(S3Path::object("bucket-name", "object-name"))), |
| 469 | + ("///object-name/", Ok(S3Path::object("bucket-name", "object-name/"))), |
| 470 | + ("/dir//object-name/", Ok(S3Path::object("bucket-name", "dir//object-name/"))), |
| 471 | + ("//dir//object-name/", Ok(S3Path::object("bucket-name", "dir/object-name/"))), |
| 472 | + ]; |
| 473 | + |
| 474 | + for (uri_path, expected) in cases { |
| 475 | + assert_eq!( |
| 476 | + parse_virtual_hosted_style_with_validation(Some("bucket-name"), uri_path, &AwsNameValidation::new()), |
| 477 | + expected |
| 478 | + ); |
| 479 | + } |
| 480 | + } |
| 481 | + |
396 | 482 | // --- S3Path accessor coverage --- |
397 | 483 |
|
398 | 484 | #[test] |
@@ -481,4 +567,26 @@ mod tests { |
481 | 567 | let err = ParseS3PathError::KeyTooLong; |
482 | 568 | assert!(!format!("{err}").is_empty()); |
483 | 569 | } |
| 570 | + |
| 571 | + #[test] |
| 572 | + #[cfg(feature = "normalize_forward_slash")] |
| 573 | + fn key_name_normalize_forward_slash() { |
| 574 | + let cases = [ |
| 575 | + ("/", "/"), |
| 576 | + ("/////", "/"), |
| 577 | + ("object-name", "object-name"), |
| 578 | + ("object-name/", "object-name/"), |
| 579 | + ("object-name///", "object-name///"), |
| 580 | + ("/object-name", "object-name"), |
| 581 | + ("///object-name", "object-name"), |
| 582 | + ("///object-name/", "object-name/"), |
| 583 | + ("/dir/object-name", "dir/object-name"), |
| 584 | + ("///dir/object-name", "dir/object-name"), |
| 585 | + ("/dir////object-name", "dir/object-name"), |
| 586 | + ("///dir1////dir2/object-name////////", "dir1/dir2/object-name/"), |
| 587 | + ]; |
| 588 | + for (input, expected) in &cases { |
| 589 | + assert_eq!(normalize_path(input).as_ref(), *expected); |
| 590 | + } |
| 591 | + } |
484 | 592 | } |
0 commit comments