diff --git a/rustfs/src/app/multipart_usecase.rs b/rustfs/src/app/multipart_usecase.rs index 3275aad5b..de96c0110 100644 --- a/rustfs/src/app/multipart_usecase.rs +++ b/rustfs/src/app/multipart_usecase.rs @@ -38,7 +38,7 @@ use rustfs_ecstore::compress::is_compressible; use rustfs_ecstore::error::{StorageError, is_err_object_not_found, is_err_version_not_found}; use rustfs_ecstore::new_object_layer_fn; use rustfs_ecstore::set_disk::{MAX_PARTS_COUNT, is_valid_storage_class}; -use rustfs_ecstore::store_api::{CompletePart, MultipartUploadResult, ObjectIO, ObjectOptions, PutObjReader}; +use rustfs_ecstore::store_api::{CompletePart, HTTPRangeSpec, MultipartUploadResult, ObjectIO, ObjectOptions, PutObjReader}; use rustfs_ecstore::store_api::{MultipartOperations, ObjectOperations}; use rustfs_filemeta::{ReplicationStatusType, ReplicationType}; use rustfs_rio::{CompressReader, HashReader, Reader, WarpReader}; @@ -58,6 +58,18 @@ use tokio::sync::RwLock; use tokio_util::io::StreamReader; use tracing::{info, instrument, warn}; +/// Returns InvalidRange error if CopySourceRange end exceeds the source object size. +/// Used by execute_upload_part_copy to reject out-of-bounds ranges per S3 spec. +fn validate_copy_source_range_not_exceeds(range_spec: &HTTPRangeSpec, object_size: i64) -> S3Result<()> { + if range_spec.end >= object_size { + return Err(S3Error::with_message( + S3ErrorCode::InvalidRange, + "The requested range is not satisfiable".to_string(), + )); + } + Ok(()) +} + #[derive(Clone, Default)] pub struct DefaultMultipartUsecase { context: Option>, @@ -973,6 +985,8 @@ impl DefaultMultipartUsecase { _ => src_info.size, }; + validate_copy_source_range_not_exceeds(range_spec, validation_size)?; + range_spec .get_offset_length(validation_size) .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRange, e.to_string()))? @@ -1217,6 +1231,44 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InternalError); } + #[test] + fn test_validate_copy_source_range_not_exceeds_returns_invalid_range_when_range_exceeds() { + use super::validate_copy_source_range_not_exceeds; + + let range_exceeds = HTTPRangeSpec { + is_suffix_length: false, + start: 0, + end: 21, + }; + let err = validate_copy_source_range_not_exceeds(&range_exceeds, 5).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::InvalidRange); + assert!(err.to_string().contains("not satisfiable")); + } + + #[test] + fn test_validate_copy_source_range_not_exceeds_ok_when_range_valid() { + use super::validate_copy_source_range_not_exceeds; + + let range_valid = HTTPRangeSpec { + is_suffix_length: false, + start: 0, + end: 4, + }; + assert!(validate_copy_source_range_not_exceeds(&range_valid, 5).is_ok()); + } + + #[test] + fn test_validate_copy_source_range_not_exceeds_ok_for_suffix_range() { + use super::validate_copy_source_range_not_exceeds; + + let range_suffix = HTTPRangeSpec { + is_suffix_length: true, + start: -5, + end: -1, + }; + assert!(validate_copy_source_range_not_exceeds(&range_suffix, 5).is_ok()); + } + #[tokio::test] async fn execute_upload_part_rejects_missing_body() { let input = UploadPartInput::builder() diff --git a/scripts/s3-tests/implemented_tests.txt b/scripts/s3-tests/implemented_tests.txt index 2737546dd..1e521eda9 100644 --- a/scripts/s3-tests/implemented_tests.txt +++ b/scripts/s3-tests/implemented_tests.txt @@ -34,8 +34,9 @@ # - CORS: Bucket-level CORS configuration, preflight, origin matching # - HTTP: 100-continue, multipart abort, tagging in POST Object # - DeleteObject: Proper NoSuchBucket for deleted buckets +# - Multipart Copy: InvalidRange when CopySourceRange exceeds source size # -# Total: 393 tests +# Total: 394 tests test_basic_key_count test_bucket_create_naming_bad_short_one @@ -486,3 +487,6 @@ test_100_continue test_abort_multipart_upload_not_found test_post_object_tags_authenticated_request test_object_delete_key_bucket_gone + +# Multipart copy range validation +test_multipart_copy_invalid_range diff --git a/scripts/s3-tests/non_standard_tests.txt b/scripts/s3-tests/non_standard_tests.txt index c17dfb9b9..cb0d5d6a2 100644 --- a/scripts/s3-tests/non_standard_tests.txt +++ b/scripts/s3-tests/non_standard_tests.txt @@ -224,7 +224,6 @@ test_list_buckets_paginated test_list_multipart_upload test_list_multipart_upload_owner test_multipart_checksum_sha256 -test_multipart_copy_invalid_range test_multipart_copy_multiple_sizes test_multipart_copy_versioned test_multipart_get_part