mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 20:36:38 +00:00
fix(ecstore): normalize internal metadata aliases
This commit is contained in:
@@ -1620,15 +1620,100 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_latest_object_info_candidates_rejects_user_defined_identity_conflict() {
|
||||
fn resolve_latest_object_info_candidates_accepts_internal_metadata_aliases() {
|
||||
let base = object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string()));
|
||||
let mut divergent = base.clone();
|
||||
divergent.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
let mut rustfs_alias = base.clone();
|
||||
rustfs_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
"x-rustfs-internal-compression".to_string(),
|
||||
"zstd".to_string(),
|
||||
)]));
|
||||
let mut minio_alias = base.clone();
|
||||
minio_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
"X-MINIO-INTERNAL-COMPRESSION".to_string(),
|
||||
"zstd".to_string(),
|
||||
)]));
|
||||
|
||||
let (_, idx) = resolve_latest_object_info_candidates(
|
||||
vec![
|
||||
LatestObjectInfoCandidate {
|
||||
info: Some(rustfs_alias),
|
||||
idx: 0,
|
||||
err: None,
|
||||
},
|
||||
LatestObjectInfoCandidate {
|
||||
info: Some(minio_alias),
|
||||
idx: 1,
|
||||
err: None,
|
||||
},
|
||||
],
|
||||
"bucket",
|
||||
"object",
|
||||
&ObjectOptions::default(),
|
||||
)
|
||||
.expect("same-value internal aliases should resolve");
|
||||
assert_eq!(idx, 1);
|
||||
|
||||
let mut dual_alias = base.clone();
|
||||
dual_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([
|
||||
("x-rustfs-internal-compression".to_string(), "zstd".to_string()),
|
||||
("x-minio-internal-compression".to_string(), "zstd".to_string()),
|
||||
]));
|
||||
let mut single_alias = base;
|
||||
single_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
"x-rustfs-internal-compression".to_string(),
|
||||
"zstd".to_string(),
|
||||
)]));
|
||||
|
||||
assert_equal_time_identity_conflict(base, divergent);
|
||||
let (_, idx) = resolve_latest_object_info_candidates(
|
||||
vec![
|
||||
LatestObjectInfoCandidate {
|
||||
info: Some(dual_alias),
|
||||
idx: 0,
|
||||
err: None,
|
||||
},
|
||||
LatestObjectInfoCandidate {
|
||||
info: Some(single_alias),
|
||||
idx: 1,
|
||||
err: None,
|
||||
},
|
||||
],
|
||||
"bucket",
|
||||
"object",
|
||||
&ObjectOptions::default(),
|
||||
)
|
||||
.expect("dual-key and single-key internal metadata should resolve");
|
||||
assert_eq!(idx, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_latest_object_info_candidates_rejects_different_internal_metadata_alias_values() {
|
||||
let base = object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string()));
|
||||
let mut rustfs_alias = base.clone();
|
||||
rustfs_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
"x-rustfs-internal-compression".to_string(),
|
||||
"zstd".to_string(),
|
||||
)]));
|
||||
let mut minio_alias = base;
|
||||
minio_alias.user_defined = std::sync::Arc::new(std::collections::HashMap::from([(
|
||||
"x-minio-internal-compression".to_string(),
|
||||
"snappy".to_string(),
|
||||
)]));
|
||||
|
||||
assert_equal_time_identity_conflict(rustfs_alias, minio_alias);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_latest_object_info_candidates_rejects_conflicting_internal_metadata_aliases_in_one_candidate() {
|
||||
let base = object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string()));
|
||||
let mut first = base.clone();
|
||||
first.user_defined = std::sync::Arc::new(std::collections::HashMap::from([
|
||||
("x-rustfs-internal-compression".to_string(), "zstd".to_string()),
|
||||
("x-minio-internal-compression".to_string(), "snappy".to_string()),
|
||||
]));
|
||||
let mut second = base;
|
||||
second.user_defined = first.user_defined.clone();
|
||||
|
||||
assert_equal_time_identity_conflict(first, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error::{Error, Result, StorageError, is_err_object_not_found, is_err_version_not_found};
|
||||
use crate::object_api::{ObjectInfo, ObjectOptions};
|
||||
use rustfs_utils::http::metadata_compat::strip_internal_prefix;
|
||||
use rustfs_utils::path::decode_dir_object;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -151,6 +154,45 @@ fn same_transition_identity(left: &ObjectInfo, right: &ObjectInfo) -> bool {
|
||||
&& left.transitioned_object.status == right.transitioned_object.status
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct LatestUserDefinedIdentity {
|
||||
internal: HashMap<String, String>,
|
||||
other: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn normalize_user_defined_identity(user_defined: &HashMap<String, String>) -> Option<LatestUserDefinedIdentity> {
|
||||
let mut identity = LatestUserDefinedIdentity {
|
||||
internal: HashMap::with_capacity(user_defined.len()),
|
||||
other: HashMap::with_capacity(user_defined.len()),
|
||||
};
|
||||
|
||||
for (key, value) in user_defined {
|
||||
if let Some(suffix) = strip_internal_prefix(key) {
|
||||
if identity
|
||||
.internal
|
||||
.insert(suffix, value.clone())
|
||||
.is_some_and(|previous| previous != *value)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
identity.other.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Some(identity)
|
||||
}
|
||||
|
||||
fn same_user_defined_identity(left: &ObjectInfo, right: &ObjectInfo) -> bool {
|
||||
match (
|
||||
normalize_user_defined_identity(&left.user_defined),
|
||||
normalize_user_defined_identity(&right.user_defined),
|
||||
) {
|
||||
(Some(left), Some(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pool-specific erasure geometry is intentionally excluded: `get_object_info`
|
||||
/// returns each pool's own `data_blocks`/`parity_blocks`, so those values can
|
||||
/// differ for the same object version while the selected winner still carries
|
||||
@@ -167,7 +209,7 @@ fn same_latest_object_info_identity(left: &ObjectInfo, right: &ObjectInfo) -> bo
|
||||
&& left.size == right.size
|
||||
&& left.actual_size == right.actual_size
|
||||
&& left.is_dir == right.is_dir
|
||||
&& left.user_defined == right.user_defined
|
||||
&& same_user_defined_identity(left, right)
|
||||
&& left.user_tags == right.user_tags
|
||||
&& left.version_id == right.version_id
|
||||
&& left.data_dir == right.data_dir
|
||||
|
||||
Reference in New Issue
Block a user