mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 01:48:21 +00:00
baadaccc30
backlog#1833 PR1 prescribed deduplicating crates/replication/src/http.rs onto the canonical rustfs-utils http modules via a re-export facade. That plan conflicts with a standing architecture guard the issue's review missed: check_architecture_migration_rules.sh rejects any rustfs-utils import or dependency from the replication crate ("replication crate HTTP/helper contracts must not import or depend on rustfs-utils"), the same way it bans rustfs-filemeta and rustfs-storage-api — the wire-contract crate deliberately has zero internal dependencies.
So this lands the issue's fallback shape instead (the same bidirectional do-not-merge pattern the issue itself prescribes for the policy path.rs cluster): a module doc on replication/http.rs naming the canonical owners and the guard that forces the local copy, mirror notes on utils' metadata_compat.rs and header_compat.rs, and a new test pinning every duplicated constant to its literal wire value so the two copies cannot drift silently.
No production code changed.
Ref rustfs/backlog#1833 (PR1).
196 lines
8.8 KiB
Rust
196 lines
8.8 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.
|
|
|
|
//! DELIBERATE DUPLICATION — do not merge these declarations into the
|
|
//! rustfs-utils http module without a maintainer decision on the crate
|
|
//! boundary.
|
|
//!
|
|
//! The canonical owners of these interop-contract values live in the
|
|
//! rustfs-utils crate: `crates/utils/src/http/metadata_compat.rs` (dual
|
|
//! x-rustfs-internal-/x-minio-internal- metadata keys),
|
|
//! `crates/utils/src/http/header_compat.rs` (x-rustfs-/x-minio- header pairs),
|
|
//! and `crates/utils/src/http/headers.rs` (standard S3 header names). This
|
|
//! crate keeps a local copy because
|
|
//! `rustfs-replication` is a wire-contract crate that must stay free of
|
|
//! internal dependencies: `scripts/check_architecture_migration_rules.sh`
|
|
//! rejects any `rustfs-utils` import or dependency here ("replication crate
|
|
//! HTTP/helper contracts must not import or depend on rustfs-utils"), and the
|
|
//! same rule bans `rustfs-filemeta` and `rustfs-storage-api`.
|
|
//!
|
|
//! Drift protection lives in the test module below: every constant's literal
|
|
//! wire value is pinned, so a change on either side that breaks interop fails
|
|
//! this crate's tests rather than silently forking the contract.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
const RUSTFS_INTERNAL_PREFIX: &str = "x-rustfs-internal-";
|
|
const MINIO_INTERNAL_PREFIX: &str = "x-minio-internal-";
|
|
const RUSTFS_HEADER_PREFIX: &str = "x-rustfs-";
|
|
const MINIO_HEADER_PREFIX: &str = "x-minio-";
|
|
|
|
pub(crate) const AMZ_BUCKET_REPLICATION_STATUS: &str = "X-Amz-Replication-Status";
|
|
pub(crate) const AMZ_OBJECT_LOCK_LEGAL_HOLD: &str = "X-Amz-Object-Lock-Legal-Hold";
|
|
pub(crate) const AMZ_OBJECT_LOCK_MODE: &str = "X-Amz-Object-Lock-Mode";
|
|
pub(crate) const AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE: &str = "X-Amz-Object-Lock-Retain-Until-Date";
|
|
pub(crate) const AMZ_OBJECT_TAGGING: &str = "X-Amz-Tagging";
|
|
pub(crate) const AMZ_WEBSITE_REDIRECT_LOCATION: &str = "x-amz-website-redirect-location";
|
|
pub(crate) const CACHE_CONTROL: &str = "Cache-Control";
|
|
pub(crate) const CONTENT_DISPOSITION: &str = "Content-Disposition";
|
|
pub(crate) const CONTENT_ENCODING: &str = "Content-Encoding";
|
|
pub(crate) const CONTENT_LANGUAGE: &str = "Content-Language";
|
|
pub(crate) const EXPIRES: &str = "Expires";
|
|
pub(crate) const SSEC_ALGORITHM_HEADER: &str = "x-amz-server-side-encryption-customer-algorithm";
|
|
pub(crate) const SSEC_KEY_HEADER: &str = "x-amz-server-side-encryption-customer-key";
|
|
pub(crate) const SSEC_KEY_MD5_HEADER: &str = "x-amz-server-side-encryption-customer-key-md5";
|
|
pub(crate) const SUFFIX_ACTUAL_SIZE: &str = "actual-size";
|
|
pub(crate) const SUFFIX_REPLICATION_STATUS: &str = "replication-status";
|
|
pub(crate) const SUFFIX_REPLICATION_RESET_STATUS: &str = "replication-reset-status";
|
|
|
|
fn internal_keys(suffix: &str) -> (String, String) {
|
|
(format!("{RUSTFS_INTERNAL_PREFIX}{suffix}"), format!("{MINIO_INTERNAL_PREFIX}{suffix}"))
|
|
}
|
|
|
|
fn rustfs_header_key(suffix: &str) -> String {
|
|
format!("{RUSTFS_HEADER_PREFIX}{suffix}")
|
|
}
|
|
|
|
fn minio_header_key(suffix: &str) -> String {
|
|
format!("{MINIO_HEADER_PREFIX}{suffix}")
|
|
}
|
|
|
|
pub(crate) fn internal_key_rustfs(suffix: &str) -> String {
|
|
format!("{RUSTFS_INTERNAL_PREFIX}{suffix}")
|
|
}
|
|
|
|
pub(crate) fn get_internal_metadata(map: &HashMap<String, String>, suffix: &str) -> Option<String> {
|
|
let (rustfs_key, minio_key) = internal_keys(suffix);
|
|
map.get(&rustfs_key)
|
|
.cloned()
|
|
.or_else(|| map.get(&minio_key).cloned())
|
|
.or_else(|| {
|
|
map.iter()
|
|
.find(|(key, _)| key.eq_ignore_ascii_case(&rustfs_key) || key.eq_ignore_ascii_case(&minio_key))
|
|
.map(|(_, value)| value.clone())
|
|
})
|
|
}
|
|
|
|
pub(crate) fn get_header_metadata(map: &HashMap<String, String>, suffix: &str) -> Option<String> {
|
|
let rustfs_key = rustfs_header_key(suffix);
|
|
let minio_key = minio_header_key(suffix);
|
|
map.get(&rustfs_key).cloned().or_else(|| map.get(&minio_key).cloned())
|
|
}
|
|
|
|
pub(crate) fn has_prefix_fold(s: &str, prefix: &str) -> bool {
|
|
if s.starts_with(prefix) {
|
|
return true;
|
|
}
|
|
|
|
s.get(..prefix.len())
|
|
.is_some_and(|s_prefix| s_prefix.eq_ignore_ascii_case(prefix))
|
|
}
|
|
|
|
pub(crate) fn trim_etag(etag: &str) -> String {
|
|
etag.trim_matches('"').to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn insert_internal_metadata(map: &mut HashMap<String, String>, suffix: &str, value: String) {
|
|
let (rustfs_key, minio_key) = internal_keys(suffix);
|
|
map.insert(rustfs_key, value.clone());
|
|
map.insert(minio_key, value);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
SUFFIX_ACTUAL_SIZE, SUFFIX_REPLICATION_RESET_STATUS, get_header_metadata, get_internal_metadata, has_prefix_fold,
|
|
insert_internal_metadata, internal_key_rustfs, trim_etag,
|
|
};
|
|
use std::collections::HashMap;
|
|
|
|
#[test]
|
|
fn internal_metadata_prefers_rustfs_and_falls_back_to_minio() {
|
|
let mut metadata = HashMap::from([("x-minio-internal-actual-size".to_string(), "10".to_string())]);
|
|
|
|
assert_eq!(get_internal_metadata(&metadata, SUFFIX_ACTUAL_SIZE).as_deref(), Some("10"));
|
|
|
|
metadata.insert("x-rustfs-internal-actual-size".to_string(), "11".to_string());
|
|
assert_eq!(get_internal_metadata(&metadata, SUFFIX_ACTUAL_SIZE).as_deref(), Some("11"));
|
|
}
|
|
|
|
#[test]
|
|
fn internal_metadata_keeps_case_insensitive_lookup_compatibility() {
|
|
let metadata = HashMap::from([("X-RustFS-Internal-Actual-Size".to_string(), "12".to_string())]);
|
|
|
|
assert_eq!(get_internal_metadata(&metadata, SUFFIX_ACTUAL_SIZE).as_deref(), Some("12"));
|
|
}
|
|
|
|
#[test]
|
|
fn internal_metadata_insert_writes_rustfs_and_minio_keys() {
|
|
let mut metadata = HashMap::new();
|
|
insert_internal_metadata(&mut metadata, SUFFIX_ACTUAL_SIZE, "13".to_string());
|
|
|
|
assert_eq!(metadata.get("x-rustfs-internal-actual-size").map(String::as_str), Some("13"));
|
|
assert_eq!(metadata.get("x-minio-internal-actual-size").map(String::as_str), Some("13"));
|
|
}
|
|
|
|
#[test]
|
|
fn header_metadata_prefers_rustfs_then_minio() {
|
|
let mut metadata = HashMap::from([("x-minio-replication-reset-status".to_string(), "old".to_string())]);
|
|
|
|
assert_eq!(get_header_metadata(&metadata, SUFFIX_REPLICATION_RESET_STATUS).as_deref(), Some("old"));
|
|
|
|
metadata.insert("x-rustfs-replication-reset-status".to_string(), "new".to_string());
|
|
assert_eq!(get_header_metadata(&metadata, SUFFIX_REPLICATION_RESET_STATUS).as_deref(), Some("new"));
|
|
}
|
|
|
|
#[test]
|
|
fn helper_contracts_match_replication_wire_rules() {
|
|
assert_eq!(
|
|
internal_key_rustfs("replication-reset-arn:target"),
|
|
"x-rustfs-internal-replication-reset-arn:target"
|
|
);
|
|
assert_eq!(trim_etag("\"abc\""), "abc");
|
|
assert!(has_prefix_fold("X-Amz-Meta-Foo", "x-amz-meta-"));
|
|
assert!(!has_prefix_fold("X-Amz-Meta-Foo", "amz-meta"));
|
|
}
|
|
|
|
/// Pins every duplicated interop constant to its literal wire value. The
|
|
/// canonical owner lives in the rustfs-utils crate (see the module doc);
|
|
/// an arch guard forbids depending on it from this crate, so byte-for-byte
|
|
/// pinning here is what keeps the two copies from drifting apart.
|
|
#[test]
|
|
fn duplicated_interop_constants_pin_canonical_wire_values() {
|
|
use super::*;
|
|
|
|
assert_eq!(AMZ_BUCKET_REPLICATION_STATUS, "X-Amz-Replication-Status");
|
|
assert_eq!(AMZ_OBJECT_LOCK_LEGAL_HOLD, "X-Amz-Object-Lock-Legal-Hold");
|
|
assert_eq!(AMZ_OBJECT_LOCK_MODE, "X-Amz-Object-Lock-Mode");
|
|
assert_eq!(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, "X-Amz-Object-Lock-Retain-Until-Date");
|
|
assert_eq!(AMZ_OBJECT_TAGGING, "X-Amz-Tagging");
|
|
assert_eq!(AMZ_WEBSITE_REDIRECT_LOCATION, "x-amz-website-redirect-location");
|
|
assert_eq!(CACHE_CONTROL, "Cache-Control");
|
|
assert_eq!(CONTENT_DISPOSITION, "Content-Disposition");
|
|
assert_eq!(CONTENT_ENCODING, "Content-Encoding");
|
|
assert_eq!(CONTENT_LANGUAGE, "Content-Language");
|
|
assert_eq!(EXPIRES, "Expires");
|
|
assert_eq!(SSEC_ALGORITHM_HEADER, "x-amz-server-side-encryption-customer-algorithm");
|
|
assert_eq!(SSEC_KEY_HEADER, "x-amz-server-side-encryption-customer-key");
|
|
assert_eq!(SSEC_KEY_MD5_HEADER, "x-amz-server-side-encryption-customer-key-md5");
|
|
assert_eq!(SUFFIX_ACTUAL_SIZE, "actual-size");
|
|
assert_eq!(SUFFIX_REPLICATION_STATUS, "replication-status");
|
|
assert_eq!(SUFFIX_REPLICATION_RESET_STATUS, "replication-reset-status");
|
|
}
|
|
}
|