mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 14:23:13 +00:00
709 lines
28 KiB
Rust
709 lines
28 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! CopyObject checksum compatibility tests. Covers all supported algorithms,
|
|
//! source-checksum preservation, explicit override, and fail-closed handling of
|
|
//! unsupported algorithms before destination mutation.
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::common::{RustFSTestEnvironment, init_logging};
|
|
use aws_sdk_s3::config::{Credentials, Region, RequestChecksumCalculation};
|
|
use aws_sdk_s3::error::ProvideErrorMetadata;
|
|
use aws_sdk_s3::primitives::ByteStream;
|
|
use aws_sdk_s3::types::{
|
|
BucketVersioningStatus, ChecksumAlgorithm, ChecksumMode, ChecksumType, CompletedMultipartUpload, CompletedPart,
|
|
VersioningConfiguration,
|
|
};
|
|
use aws_smithy_http_client::Builder as SmithyHttpClientBuilder;
|
|
use base64::Engine as _;
|
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
|
use rustfs_rio::{Checksum, ChecksumType as RioChecksumType};
|
|
use serial_test::serial;
|
|
use sha2::{Digest, Sha256};
|
|
use tracing::info;
|
|
|
|
async fn create_versioned_bucket(client: &aws_sdk_s3::Client, bucket: &str) {
|
|
client
|
|
.create_bucket()
|
|
.bucket(bucket)
|
|
.send()
|
|
.await
|
|
.expect("Failed to create bucket");
|
|
client
|
|
.put_bucket_versioning()
|
|
.bucket(bucket)
|
|
.versioning_configuration(
|
|
VersioningConfiguration::builder()
|
|
.status(BucketVersioningStatus::Enabled)
|
|
.build(),
|
|
)
|
|
.send()
|
|
.await
|
|
.expect("Failed to enable versioning");
|
|
}
|
|
|
|
fn create_s3_client_no_auto_checksum(env: &RustFSTestEnvironment) -> aws_sdk_s3::Client {
|
|
let credentials = Credentials::new(&env.access_key, &env.secret_key, None, None, "copy-checksum-e2e");
|
|
let config = aws_sdk_s3::Config::builder()
|
|
.credentials_provider(credentials)
|
|
.region(Region::new("us-east-1"))
|
|
.endpoint_url(format!("http://{}", env.address))
|
|
.force_path_style(true)
|
|
.behavior_version_latest()
|
|
.request_checksum_calculation(RequestChecksumCalculation::WhenRequired)
|
|
.http_client(SmithyHttpClientBuilder::new().build_http())
|
|
.build();
|
|
aws_sdk_s3::Client::from_conf(config)
|
|
}
|
|
|
|
fn algorithms() -> [(ChecksumAlgorithm, RioChecksumType); 10] {
|
|
[
|
|
(ChecksumAlgorithm::Crc32, RioChecksumType::CRC32),
|
|
(ChecksumAlgorithm::Crc32C, RioChecksumType::CRC32C),
|
|
(ChecksumAlgorithm::Crc64Nvme, RioChecksumType::CRC64_NVME),
|
|
(ChecksumAlgorithm::Sha1, RioChecksumType::SHA1),
|
|
(ChecksumAlgorithm::Sha256, RioChecksumType::SHA256),
|
|
(ChecksumAlgorithm::Md5, RioChecksumType::MD5),
|
|
(ChecksumAlgorithm::Sha512, RioChecksumType::SHA512),
|
|
(ChecksumAlgorithm::Xxhash3, RioChecksumType::XXHASH3),
|
|
(ChecksumAlgorithm::Xxhash64, RioChecksumType::XXHASH64),
|
|
(ChecksumAlgorithm::Xxhash128, RioChecksumType::XXHASH128),
|
|
]
|
|
}
|
|
|
|
fn result_checksums(result: &aws_sdk_s3::types::CopyObjectResult) -> [Option<&str>; 10] {
|
|
[
|
|
result.checksum_crc32(),
|
|
result.checksum_crc32_c(),
|
|
result.checksum_crc64_nvme(),
|
|
result.checksum_sha1(),
|
|
result.checksum_sha256(),
|
|
result.checksum_md5(),
|
|
result.checksum_sha512(),
|
|
result.checksum_xxhash3(),
|
|
result.checksum_xxhash64(),
|
|
result.checksum_xxhash128(),
|
|
]
|
|
}
|
|
|
|
fn head_checksums(output: &aws_sdk_s3::operation::head_object::HeadObjectOutput) -> [Option<&str>; 10] {
|
|
[
|
|
output.checksum_crc32(),
|
|
output.checksum_crc32_c(),
|
|
output.checksum_crc64_nvme(),
|
|
output.checksum_sha1(),
|
|
output.checksum_sha256(),
|
|
output.checksum_md5(),
|
|
output.checksum_sha512(),
|
|
output.checksum_xxhash3(),
|
|
output.checksum_xxhash64(),
|
|
output.checksum_xxhash128(),
|
|
]
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_supports_all_checksum_algorithms() {
|
|
init_logging();
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = create_s3_client_no_auto_checksum(&env);
|
|
let src_bucket = "copy-all-checksums-src";
|
|
let dst_bucket = "copy-all-checksums-dst";
|
|
let src_key = "objects/source.bin";
|
|
let content = b"deterministic CopyObject payload for all ten checksum algorithms";
|
|
|
|
create_versioned_bucket(&client, src_bucket).await;
|
|
create_versioned_bucket(&client, dst_bucket).await;
|
|
client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(src_key)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("PUT source failed");
|
|
|
|
for (index, (sdk_algorithm, rio_algorithm)) in algorithms().into_iter().enumerate() {
|
|
let expected = Checksum::new_from_data(rio_algorithm, content)
|
|
.expect("supported checksum must be computable")
|
|
.encoded;
|
|
let dst_key = format!("objects/destination-{index}.bin");
|
|
let copy = client
|
|
.copy_object()
|
|
.bucket(dst_bucket)
|
|
.key(&dst_key)
|
|
.copy_source(format!("{src_bucket}/{src_key}"))
|
|
.checksum_algorithm(sdk_algorithm)
|
|
.send()
|
|
.await
|
|
.expect("CopyObject with supported checksum must succeed");
|
|
let result = copy.copy_object_result().expect("CopyObject result");
|
|
let checksums = result_checksums(result);
|
|
assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: response checksum");
|
|
assert_eq!(
|
|
checksums.iter().filter(|checksum| checksum.is_some()).count(),
|
|
1,
|
|
"{rio_algorithm}: only the requested checksum may be returned"
|
|
);
|
|
|
|
let head = client
|
|
.head_object()
|
|
.bucket(dst_bucket)
|
|
.key(&dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD destination failed");
|
|
let checksums = head_checksums(&head);
|
|
assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: persisted checksum");
|
|
assert_eq!(
|
|
checksums.iter().filter(|checksum| checksum.is_some()).count(),
|
|
1,
|
|
"{rio_algorithm}: destination must persist only the requested checksum"
|
|
);
|
|
|
|
let body = client
|
|
.get_object()
|
|
.bucket(dst_bucket)
|
|
.key(&dst_key)
|
|
.send()
|
|
.await
|
|
.expect("GET destination failed")
|
|
.body
|
|
.collect()
|
|
.await
|
|
.expect("collect destination body")
|
|
.into_bytes();
|
|
assert_eq!(body.as_ref(), content, "{rio_algorithm}: full copied body");
|
|
}
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_without_algorithm_preserves_every_supported_source_checksum() {
|
|
init_logging();
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = create_s3_client_no_auto_checksum(&env);
|
|
let src_bucket = "copy-preserve-all-src";
|
|
let dst_bucket = "copy-preserve-all-dst";
|
|
let content = b"source checksum preservation payload for all ten algorithms";
|
|
|
|
create_versioned_bucket(&client, src_bucket).await;
|
|
create_versioned_bucket(&client, dst_bucket).await;
|
|
|
|
for (index, (_sdk_algorithm, rio_algorithm)) in algorithms().into_iter().enumerate() {
|
|
let expected = Checksum::new_from_data(rio_algorithm, content)
|
|
.expect("supported checksum must be computable")
|
|
.encoded;
|
|
let checksum_header = rio_algorithm.key().expect("supported checksum header");
|
|
let request_checksum = expected.clone();
|
|
let src_key = format!("objects/source-{index}.bin");
|
|
let dst_key = format!("objects/destination-{index}.bin");
|
|
client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(&src_key)
|
|
.body(ByteStream::from_static(content))
|
|
.customize()
|
|
.mutate_request(move |request| {
|
|
request.headers_mut().insert(checksum_header, request_checksum.clone());
|
|
})
|
|
.send()
|
|
.await
|
|
.expect("PUT checksummed source failed");
|
|
|
|
let copy = client
|
|
.copy_object()
|
|
.bucket(dst_bucket)
|
|
.key(&dst_key)
|
|
.copy_source(format!("{src_bucket}/{src_key}"))
|
|
.send()
|
|
.await
|
|
.expect("CopyObject without algorithm must succeed");
|
|
let result = copy.copy_object_result().expect("CopyObject result");
|
|
let checksums = result_checksums(result);
|
|
assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: preserved response checksum");
|
|
assert_eq!(checksums.iter().filter(|checksum| checksum.is_some()).count(), 1);
|
|
|
|
let head = client
|
|
.head_object()
|
|
.bucket(dst_bucket)
|
|
.key(&dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD destination failed");
|
|
let checksums = head_checksums(&head);
|
|
assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: preserved stored checksum");
|
|
assert_eq!(checksums.iter().filter(|checksum| checksum.is_some()).count(), 1);
|
|
}
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_without_algorithm_preserves_composite_checksum_type() {
|
|
init_logging();
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = create_s3_client_no_auto_checksum(&env);
|
|
let bucket = "copy-preserve-composite";
|
|
let source_key = "objects/multipart-source.bin";
|
|
let destination_key = "objects/copied-multipart.bin";
|
|
let content = b"multipart source checksum must remain composite";
|
|
|
|
create_versioned_bucket(&client, bucket).await;
|
|
let created = client
|
|
.create_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(source_key)
|
|
.checksum_algorithm(ChecksumAlgorithm::Sha256)
|
|
.send()
|
|
.await
|
|
.expect("CreateMultipartUpload failed");
|
|
let upload_id = created.upload_id().expect("multipart upload ID");
|
|
let checksum = Checksum::new_from_data(RioChecksumType::SHA256, content)
|
|
.expect("SHA256 checksum")
|
|
.encoded;
|
|
let uploaded = client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(source_key)
|
|
.upload_id(upload_id)
|
|
.part_number(1)
|
|
.checksum_sha256(&checksum)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("UploadPart failed");
|
|
let completed_part = CompletedPart::builder()
|
|
.part_number(1)
|
|
.e_tag(uploaded.e_tag().expect("part ETag"))
|
|
.checksum_sha256(uploaded.checksum_sha256().expect("part checksum"))
|
|
.build();
|
|
client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(source_key)
|
|
.upload_id(upload_id)
|
|
.multipart_upload(CompletedMultipartUpload::builder().parts(completed_part).build())
|
|
.send()
|
|
.await
|
|
.expect("CompleteMultipartUpload failed");
|
|
|
|
let source_head = client
|
|
.head_object()
|
|
.bucket(bucket)
|
|
.key(source_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD multipart source failed");
|
|
let source_checksum = source_head.checksum_sha256().expect("multipart source checksum");
|
|
assert_eq!(source_head.checksum_type(), Some(&ChecksumType::Composite));
|
|
|
|
let copied = client
|
|
.copy_object()
|
|
.bucket(bucket)
|
|
.key(destination_key)
|
|
.copy_source(format!("{bucket}/{source_key}"))
|
|
.send()
|
|
.await
|
|
.expect("CopyObject without algorithm failed");
|
|
let result = copied.copy_object_result().expect("CopyObject result");
|
|
assert_eq!(result.checksum_sha256(), Some(source_checksum));
|
|
assert_eq!(result.checksum_type(), Some(&ChecksumType::Composite));
|
|
|
|
let destination_head = client
|
|
.head_object()
|
|
.bucket(bucket)
|
|
.key(destination_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD copied multipart object failed");
|
|
assert_eq!(destination_head.checksum_sha256(), Some(source_checksum));
|
|
assert_eq!(destination_head.checksum_type(), Some(&ChecksumType::Composite));
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_rejects_unknown_algorithm_without_destination_mutation() {
|
|
init_logging();
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = env.create_s3_client();
|
|
let bucket = "copy-reject-unknown-checksum";
|
|
let src_key = "objects/source.bin";
|
|
let dst_key = "objects/destination.bin";
|
|
let source = b"source must never replace destination";
|
|
let destination = b"pre-existing destination must remain byte-for-byte unchanged";
|
|
let expected = Checksum::new_from_data(RioChecksumType::SHA256, destination)
|
|
.expect("SHA256 checksum")
|
|
.encoded;
|
|
|
|
create_versioned_bucket(&client, bucket).await;
|
|
client
|
|
.put_object()
|
|
.bucket(bucket)
|
|
.key(src_key)
|
|
.body(ByteStream::from_static(source))
|
|
.send()
|
|
.await
|
|
.expect("PUT source failed");
|
|
let original = client
|
|
.put_object()
|
|
.bucket(bucket)
|
|
.key(dst_key)
|
|
.metadata("state", "original")
|
|
.checksum_algorithm(ChecksumAlgorithm::Sha256)
|
|
.body(ByteStream::from_static(destination))
|
|
.send()
|
|
.await
|
|
.expect("PUT destination failed");
|
|
let original_version = original.version_id().expect("versioned PUT must return a version id");
|
|
|
|
let missing_source_error = client
|
|
.copy_object()
|
|
.bucket(bucket)
|
|
.key(dst_key)
|
|
.copy_source(format!("{bucket}/objects/missing-source.bin"))
|
|
.checksum_algorithm(ChecksumAlgorithm::from("BLAKE3"))
|
|
.send()
|
|
.await
|
|
.expect_err("checksum validation must precede source lookup");
|
|
assert_eq!(
|
|
missing_source_error.as_service_error().and_then(|value| value.code()),
|
|
Some("InvalidArgument")
|
|
);
|
|
assert_eq!(missing_source_error.raw_response().map(|response| response.status().as_u16()), Some(400));
|
|
|
|
let error = client
|
|
.copy_object()
|
|
.bucket(bucket)
|
|
.key(dst_key)
|
|
.copy_source(format!("{bucket}/{src_key}"))
|
|
.checksum_algorithm(ChecksumAlgorithm::from("BLAKE3"))
|
|
.send()
|
|
.await
|
|
.expect_err("unsupported checksum algorithm must fail");
|
|
assert_eq!(error.as_service_error().and_then(|value| value.code()), Some("InvalidArgument"));
|
|
assert_eq!(error.raw_response().map(|response| response.status().as_u16()), Some(400));
|
|
|
|
let head = client
|
|
.head_object()
|
|
.bucket(bucket)
|
|
.key(dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD unchanged destination");
|
|
assert_eq!(head.version_id(), Some(original_version));
|
|
assert_eq!(
|
|
head.metadata().and_then(|metadata| metadata.get("state").map(String::as_str)),
|
|
Some("original")
|
|
);
|
|
assert_eq!(head.checksum_sha256(), Some(expected.as_str()));
|
|
|
|
let body = client
|
|
.get_object()
|
|
.bucket(bucket)
|
|
.key(dst_key)
|
|
.send()
|
|
.await
|
|
.expect("GET unchanged destination")
|
|
.body
|
|
.collect()
|
|
.await
|
|
.expect("collect unchanged destination")
|
|
.into_bytes();
|
|
assert_eq!(body.as_ref(), destination);
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
/// Requested algorithm: a CopyObject asking for SHA256 must compute it over the copied
|
|
/// bytes, return it in `CopyObjectResult.ChecksumSHA256`, and persist it so a checksum-mode
|
|
/// HEAD on the destination returns the identical value.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_with_checksum_algorithm_returns_and_persists_sha256() {
|
|
init_logging();
|
|
info!("Issue #4996: CopyObject with ChecksumAlgorithm=SHA256 must return and persist the checksum");
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = env.create_s3_client();
|
|
let src_bucket = "copy-checksum-req-src";
|
|
let dst_bucket = "copy-checksum-req-dst";
|
|
let src_key = "objects/source.bin";
|
|
let dst_key = "objects/dest.bin";
|
|
|
|
create_versioned_bucket(&client, src_bucket).await;
|
|
create_versioned_bucket(&client, dst_bucket).await;
|
|
|
|
let content = b"deterministic synthetic payload for copy-object checksum #4996";
|
|
let expected_sha256 = BASE64.encode(Sha256::digest(content));
|
|
|
|
client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(src_key)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("PUT source failed");
|
|
|
|
let copy_out = client
|
|
.copy_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.copy_source(format!("{src_bucket}/{src_key}"))
|
|
.checksum_algorithm(ChecksumAlgorithm::Sha256)
|
|
.send()
|
|
.await
|
|
.expect("CopyObject with ChecksumAlgorithm must succeed");
|
|
|
|
// (3) The response must carry the freshly computed SHA-256 of the copied bytes.
|
|
let result = copy_out
|
|
.copy_object_result()
|
|
.expect("issue #4996: CopyObject must return a CopyObjectResult");
|
|
assert_eq!(
|
|
result.checksum_sha256(),
|
|
Some(expected_sha256.as_str()),
|
|
"issue #4996: CopyObjectResult.ChecksumSHA256 must equal the SHA-256 of the copied bytes"
|
|
);
|
|
|
|
// (4) A checksum-mode HEAD on the destination must return the same SHA-256.
|
|
let head = client
|
|
.head_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD destination failed");
|
|
assert_eq!(
|
|
head.checksum_sha256(),
|
|
Some(expected_sha256.as_str()),
|
|
"issue #4996: destination checksum-mode HEAD must return the same SHA-256 the copy reported"
|
|
);
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
/// No algorithm requested: when the source object already carries a checksum, the copy must
|
|
/// preserve it on the destination (AWS default), visible via a checksum-mode HEAD.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_without_algorithm_preserves_source_checksum() {
|
|
init_logging();
|
|
info!("Issue #4996: CopyObject without ChecksumAlgorithm must preserve the source object's checksum");
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = env.create_s3_client();
|
|
let src_bucket = "copy-checksum-preserve-src";
|
|
let dst_bucket = "copy-checksum-preserve-dst";
|
|
let src_key = "objects/source.bin";
|
|
let dst_key = "objects/dest.bin";
|
|
|
|
create_versioned_bucket(&client, src_bucket).await;
|
|
create_versioned_bucket(&client, dst_bucket).await;
|
|
|
|
let content = b"another deterministic payload whose source checksum must survive the copy";
|
|
let expected_sha256 = BASE64.encode(Sha256::digest(content));
|
|
|
|
// Store the source WITH a SHA-256 checksum so it has one to preserve.
|
|
let put_src = client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(src_key)
|
|
.checksum_algorithm(ChecksumAlgorithm::Sha256)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("PUT source with checksum failed");
|
|
assert_eq!(
|
|
put_src.checksum_sha256(),
|
|
Some(expected_sha256.as_str()),
|
|
"source PUT must report the SHA-256 it stored"
|
|
);
|
|
|
|
// Copy WITHOUT specifying a checksum algorithm.
|
|
let copy_out = client
|
|
.copy_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.copy_source(format!("{src_bucket}/{src_key}"))
|
|
.send()
|
|
.await
|
|
.expect("CopyObject without ChecksumAlgorithm must succeed");
|
|
|
|
// The response should echo the preserved source checksum.
|
|
let result = copy_out
|
|
.copy_object_result()
|
|
.expect("issue #4996: CopyObject must return a CopyObjectResult");
|
|
assert_eq!(
|
|
result.checksum_sha256(),
|
|
Some(expected_sha256.as_str()),
|
|
"issue #4996: a no-algorithm copy must preserve and report the source object's SHA-256"
|
|
);
|
|
|
|
// And a checksum-mode HEAD on the destination must return that same preserved SHA-256.
|
|
let head = client
|
|
.head_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD destination failed");
|
|
assert_eq!(
|
|
head.checksum_sha256(),
|
|
Some(expected_sha256.as_str()),
|
|
"issue #4996: destination checksum-mode HEAD must return the preserved source SHA-256"
|
|
);
|
|
|
|
env.stop_server();
|
|
}
|
|
|
|
/// Requested algorithm differs from the source's: a source stored with SHA256, copied while
|
|
/// requesting CRC32, must return/persist the freshly computed CRC32 and must NOT carry the
|
|
/// source's SHA256 through. Guards the request-over-source precedence and the destination's
|
|
/// checksum-not-inherited path, and exercises the CRC32 code path (a different branch of
|
|
/// ChecksumType::from_string than SHA256).
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_copy_requested_algorithm_overrides_source_checksum() {
|
|
init_logging();
|
|
info!("Issue #4996: a requested CopyObject checksum algorithm must override the source object's algorithm");
|
|
|
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
|
|
|
let client = env.create_s3_client();
|
|
let src_bucket = "copy-checksum-override-src";
|
|
let dst_bucket = "copy-checksum-override-dst";
|
|
let src_key = "objects/source.bin";
|
|
let ref_key = "objects/reference-crc32.bin";
|
|
let dst_key = "objects/dest.bin";
|
|
|
|
create_versioned_bucket(&client, src_bucket).await;
|
|
create_versioned_bucket(&client, dst_bucket).await;
|
|
|
|
let content = b"payload whose copy must be re-checksummed with a different algorithm";
|
|
let expected_sha256 = BASE64.encode(Sha256::digest(content));
|
|
|
|
// Source is stored WITH a SHA-256 checksum.
|
|
client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(src_key)
|
|
.checksum_algorithm(ChecksumAlgorithm::Sha256)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("PUT source with SHA256 failed");
|
|
|
|
// Establish the canonical CRC32 the server computes for this content via a reference PUT,
|
|
// so the copy's CRC32 can be asserted against an exact server-computed value.
|
|
let ref_put = client
|
|
.put_object()
|
|
.bucket(src_bucket)
|
|
.key(ref_key)
|
|
.checksum_algorithm(ChecksumAlgorithm::Crc32)
|
|
.body(ByteStream::from_static(content))
|
|
.send()
|
|
.await
|
|
.expect("reference PUT with CRC32 failed");
|
|
let expected_crc32 = ref_put
|
|
.checksum_crc32()
|
|
.expect("reference PUT must report a CRC32")
|
|
.to_string();
|
|
|
|
// Copy the SHA256 source while requesting CRC32.
|
|
let copy_out = client
|
|
.copy_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.copy_source(format!("{src_bucket}/{src_key}"))
|
|
.checksum_algorithm(ChecksumAlgorithm::Crc32)
|
|
.send()
|
|
.await
|
|
.expect("CopyObject requesting a different algorithm must succeed");
|
|
|
|
let result = copy_out
|
|
.copy_object_result()
|
|
.expect("issue #4996: CopyObject must return a CopyObjectResult");
|
|
// The requested CRC32 must be computed and returned.
|
|
assert_eq!(
|
|
result.checksum_crc32(),
|
|
Some(expected_crc32.as_str()),
|
|
"issue #4996: a requested CRC32 must be computed fresh over the copied bytes"
|
|
);
|
|
// The source's SHA256 must NOT leak through — the requested algorithm wins.
|
|
assert_eq!(
|
|
result.checksum_sha256(),
|
|
None,
|
|
"issue #4996: the source object's SHA256 must not be inherited when a different algorithm is requested"
|
|
);
|
|
assert_ne!(
|
|
result.checksum_crc32(),
|
|
Some(expected_sha256.as_str()),
|
|
"sanity: CRC32 field must not carry the SHA256 value"
|
|
);
|
|
|
|
// The destination must persist CRC32 (and only CRC32) for a checksum-mode HEAD.
|
|
let head = client
|
|
.head_object()
|
|
.bucket(dst_bucket)
|
|
.key(dst_key)
|
|
.checksum_mode(ChecksumMode::Enabled)
|
|
.send()
|
|
.await
|
|
.expect("HEAD destination failed");
|
|
assert_eq!(
|
|
head.checksum_crc32(),
|
|
Some(expected_crc32.as_str()),
|
|
"issue #4996: destination checksum-mode HEAD must return the requested CRC32"
|
|
);
|
|
assert_eq!(
|
|
head.checksum_sha256(),
|
|
None,
|
|
"issue #4996: destination must not report the source's SHA256 after an override copy"
|
|
);
|
|
|
|
env.stop_server();
|
|
}
|
|
}
|