fix(s3): normalize GetObjectAttributes ETag XML response (#2002)

This commit is contained in:
安正超
2026-02-28 14:53:53 +08:00
committed by GitHub
parent 274b6f8bc7
commit 3f5ccb20fc
8 changed files with 655 additions and 39 deletions
@@ -45,6 +45,7 @@ const _ERR_XML_NOT_WELL_FORMED: &str =
const ERR_LIFECYCLE_BUCKET_LOCKED: &str =
"ExpiredObjectAllVersions element and DelMarkerExpiration action cannot be used on an retention bucket";
const ERR_LIFECYCLE_TOO_MANY_RULES: &str = "Lifecycle configuration should have at most 1000 rules";
const ERR_LIFECYCLE_INVALID_EXPIRATION_DAYS: &str = "Lifecycle expiration days must be greater than 0";
pub use rustfs_common::metrics::IlmAction;
@@ -232,6 +233,13 @@ impl Lifecycle for BucketLifecycleConfiguration {
}
for r in &self.rules {
if let Some(expiration) = &r.expiration {
if let Some(days) = expiration.days {
if days <= 0 {
return Err(std::io::Error::other(ERR_LIFECYCLE_INVALID_EXPIRATION_DAYS));
}
}
}
r.validate()?;
/*if let Some(object_lock_enabled) = lr.object_lock_enabled.as_ref() {
if let Some(expiration) = r.expiration.as_ref() {
@@ -770,3 +778,59 @@ impl Default for TransitionOptions {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn validate_rejects_non_positive_expiration_days() {
let lc = BucketLifecycleConfiguration {
rules: vec![LifecycleRule {
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
expiration: Some(LifecycleExpiration {
days: Some(0),
..Default::default()
}),
abort_incomplete_multipart_upload: None,
filter: None,
id: None,
noncurrent_version_expiration: None,
noncurrent_version_transitions: None,
prefix: None,
transitions: None,
}],
};
let err = lc
.validate(&ObjectLockConfiguration::default())
.await
.expect_err("expected validation error");
assert_eq!(err.to_string(), ERR_LIFECYCLE_INVALID_EXPIRATION_DAYS);
}
#[tokio::test]
async fn validate_accepts_positive_expiration_days() {
let lc = BucketLifecycleConfiguration {
rules: vec![LifecycleRule {
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
expiration: Some(LifecycleExpiration {
days: Some(30),
..Default::default()
}),
abort_incomplete_multipart_upload: None,
filter: None,
id: None,
noncurrent_version_expiration: None,
noncurrent_version_transitions: None,
prefix: None,
transitions: None,
}],
};
lc.validate(&ObjectLockConfiguration::default())
.await
.expect("expected validation to pass");
}
}
+105 -2
View File
@@ -387,8 +387,12 @@ impl ObjectInfo {
}
// Check if object is encrypted
// Encrypted objects store original size in x-rustfs-encryption-original-size metadata
if let Some(size_str) = self.user_defined.get("x-rustfs-encryption-original-size")
// Managed SSE stores original size in x-rustfs-encryption-original-size metadata
// SSE-C stores original size in x-amz-server-side-encryption-customer-original-size
if let Some(size_str) = self
.user_defined
.get("x-rustfs-encryption-original-size")
.or_else(|| self.user_defined.get("x-amz-server-side-encryption-customer-original-size"))
&& !size_str.is_empty()
{
let size = size_str
@@ -1010,3 +1014,102 @@ pub struct ObjectInfoOrErr {
pub item: Option<ObjectInfo>,
pub err: Option<Error>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_actual_size_prefers_actual_size_field() {
let info = ObjectInfo {
size: 5,
actual_size: 10,
..Default::default()
};
assert_eq!(info.get_actual_size().unwrap(), 10);
}
#[test]
fn get_actual_size_uses_compressed_metadata_size() {
let user_defined = {
let mut map = HashMap::new();
map.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}compression"), "zstd".to_string());
map.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}actual-size"), "42".to_string());
map
};
let info = ObjectInfo {
size: 100,
actual_size: 0,
user_defined,
..Default::default()
};
assert_eq!(info.get_actual_size().unwrap(), 42);
}
#[test]
fn get_actual_size_falls_back_to_encrypted_original_size_metadata() {
let user_defined = {
let mut map = HashMap::new();
map.insert("x-amz-server-side-encryption-customer-original-size".to_string(), "77".to_string());
map
};
let info = ObjectInfo {
size: 100,
actual_size: 0,
user_defined,
..Default::default()
};
assert_eq!(info.get_actual_size().unwrap(), 77);
}
#[test]
fn get_actual_size_uses_compressed_parts_actual_size_when_metadata_missing() {
let user_defined = {
let mut map = HashMap::new();
map.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}compression"), "zstd".to_string());
map
};
let info = ObjectInfo {
size: 12,
actual_size: 0,
user_defined,
parts: vec![
rustfs_filemeta::ObjectPartInfo {
actual_size: 4,
..Default::default()
},
rustfs_filemeta::ObjectPartInfo {
actual_size: 5,
..Default::default()
},
],
..Default::default()
};
assert_eq!(info.get_actual_size().unwrap(), 9);
}
#[test]
fn get_actual_size_returns_error_when_compressed_parts_missing_and_size_mismatch() {
let user_defined = {
let mut map = HashMap::new();
map.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}compression"), "zstd".to_string());
map
};
let info = ObjectInfo {
size: 12,
actual_size: 0,
user_defined,
..Default::default()
};
assert!(info.get_actual_size().is_err());
}
}