feat: improve legacy metadata and admin compatibility (#2202)

This commit is contained in:
weisd
2026-03-18 21:05:09 +08:00
committed by GitHub
parent 84077adf17
commit b9b7d86ae4
133 changed files with 11707 additions and 1945 deletions
+293 -27
View File
@@ -13,6 +13,7 @@
// limitations under the License.
use std::{
collections::BTreeSet,
collections::HashSet,
env,
sync::{Mutex, OnceLock},
@@ -30,7 +31,7 @@ use tracing::warn;
/// - `i8`: The parsed value as i8 if successful, otherwise the default value.
///
pub fn get_env_i8(key: &str, default: i8) -> i8 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -43,7 +44,7 @@ pub fn get_env_i8(key: &str, default: i8) -> i8 {
/// - `Option<i8>`: The parsed value as i8 if successful, otherwise None
///
pub fn get_env_opt_i8(key: &str) -> Option<i8> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -57,7 +58,7 @@ pub fn get_env_opt_i8(key: &str) -> Option<i8> {
/// - `u8`: The parsed value as u8 if successful, otherwise the default value.
///
pub fn get_env_u8(key: &str, default: u8) -> u8 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -70,7 +71,7 @@ pub fn get_env_u8(key: &str, default: u8) -> u8 {
/// - `Option<u8>`: The parsed value as u8 if successful, otherwise None
///
pub fn get_env_opt_u8(key: &str) -> Option<u8> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
static WARNED_ENV_MESSAGES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
@@ -86,19 +87,182 @@ fn log_once(key: &str, message: impl FnOnce() -> String) {
}
}
fn external_alias_for_key(key: &str) -> Option<String> {
let suffix = key.strip_prefix("RUSTFS_")?;
if is_external_compatible_suffix(suffix) {
Some(format!("{}{}", external_env_prefix(), suffix))
} else {
None
}
}
fn resolve_env_with_aliases(key: &str, deprecated: &[&str]) -> Option<(String, String)> {
if let Ok(value) = env::var(key) {
return Some((key.to_string(), value));
}
let (alias, value) = deprecated
if let Some((alias, value)) = deprecated
.iter()
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))?;
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))
{
let deprecated_key = format!("env_alias:{alias}->{key}");
log_once(&deprecated_key, || {
format!("Environment variable {alias} is deprecated, use {key} instead")
});
return Some((alias.to_string(), value));
}
let alias = external_alias_for_key(key)?;
let value = env::var(&alias).ok()?;
let deprecated_key = format!("env_alias:{alias}->{key}");
log_once(&deprecated_key, || {
format!("Environment variable {alias} is deprecated, use {key} instead")
});
Some((alias.to_string(), value))
Some((alias, value))
}
const EXTERNAL_ENV_PREFIX_BYTES: [u8; 6] = [77, 73, 78, 73, 79, 95];
const EXTERNAL_COMPATIBLE_SUFFIXES: &[&str] = &[
"ACCESS_KEY",
"ACCESS_KEY_FILE",
"ADDRESS",
"API_XFF_HEADER",
"AUDIT_WEBHOOK_AUTH_TOKEN",
"AUDIT_WEBHOOK_CLIENT_CERT",
"AUDIT_WEBHOOK_CLIENT_KEY",
"AUDIT_WEBHOOK_ENABLE",
"AUDIT_WEBHOOK_ENDPOINT",
"AUDIT_WEBHOOK_QUEUE_DIR",
"COMPRESS_ENABLE",
"COMPRESS_EXTENSIONS",
"COMPRESS_MIME_TYPES",
"CONSOLE_ADDRESS",
"DRIVE_ACTIVE_MONITORING",
"ERASURE_SET_DRIVE_COUNT",
"IDENTITY_OPENID_CLAIM_NAME",
"IDENTITY_OPENID_CLAIM_PREFIX",
"IDENTITY_OPENID_CLIENT_ID",
"IDENTITY_OPENID_CLIENT_SECRET",
"IDENTITY_OPENID_CONFIG_URL",
"IDENTITY_OPENID_DISPLAY_NAME",
"IDENTITY_OPENID_REDIRECT_URI",
"IDENTITY_OPENID_SCOPES",
"ILM_EXPIRATION_WORKERS",
"LICENSE",
"NOTIFY_MQTT_BROKER",
"NOTIFY_MQTT_ENABLE",
"NOTIFY_MQTT_KEEP_ALIVE_INTERVAL",
"NOTIFY_MQTT_PASSWORD",
"NOTIFY_MQTT_QOS",
"NOTIFY_MQTT_QUEUE_DIR",
"NOTIFY_MQTT_QUEUE_LIMIT",
"NOTIFY_MQTT_RECONNECT_INTERVAL",
"NOTIFY_MQTT_TOPIC",
"NOTIFY_MQTT_USERNAME",
"NOTIFY_WEBHOOK_AUTH_TOKEN",
"NOTIFY_WEBHOOK_CLIENT_CERT",
"NOTIFY_WEBHOOK_CLIENT_KEY",
"NOTIFY_WEBHOOK_ENABLE",
"NOTIFY_WEBHOOK_ENDPOINT",
"NOTIFY_WEBHOOK_QUEUE_DIR",
"NOTIFY_WEBHOOK_QUEUE_LIMIT",
"POLICY_PLUGIN_AUTH_TOKEN",
"POLICY_PLUGIN_URL",
"PORT",
"REGION",
"ROOT_PASSWORD",
"ROOT_USER",
"SECRET_KEY",
"SECRET_KEY_FILE",
"STORAGE_CLASS_INLINE_BLOCK",
"STORAGE_CLASS_OPTIMIZE",
"STORAGE_CLASS_RRS",
"STORAGE_CLASS_STANDARD",
"VERSION",
"VOLUMES",
];
const EXTERNAL_DYNAMIC_COMPATIBLE_PREFIXES: &[&str] = &["AUDIT_MQTT_", "AUDIT_WEBHOOK_", "NOTIFY_MQTT_", "NOTIFY_WEBHOOK_"];
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ExternalEnvCompatReport {
pub mapped_pairs: Vec<(String, String)>,
pub conflict_keys: Vec<String>,
}
impl ExternalEnvCompatReport {
pub fn mapped_count(&self) -> usize {
self.mapped_pairs.len()
}
pub fn conflict_count(&self) -> usize {
self.conflict_keys.len()
}
}
fn external_env_prefix() -> &'static str {
static PREFIX: OnceLock<String> = OnceLock::new();
PREFIX
.get_or_init(|| EXTERNAL_ENV_PREFIX_BYTES.iter().map(|&byte| char::from(byte)).collect())
.as_str()
}
fn is_external_compatible_suffix(suffix: &str) -> bool {
EXTERNAL_COMPATIBLE_SUFFIXES.contains(&suffix)
|| EXTERNAL_DYNAMIC_COMPATIBLE_PREFIXES
.iter()
.any(|prefix| suffix.starts_with(prefix))
}
fn build_external_env_compat_report_from_entries<I>(entries: I) -> ExternalEnvCompatReport
where
I: IntoIterator<Item = (String, String)>,
{
let env_map: std::collections::BTreeMap<String, String> = entries.into_iter().collect();
let mut mapped_pairs = BTreeSet::new();
let mut conflict_keys = BTreeSet::new();
let source_prefix = external_env_prefix();
for (source_key, source_value) in env_map.iter() {
let Some(suffix) = source_key.strip_prefix(source_prefix) else {
continue;
};
if !is_external_compatible_suffix(suffix) {
continue;
}
let rustfs_key = format!("RUSTFS_{suffix}");
match env_map.get(&rustfs_key) {
None => {
mapped_pairs.insert((source_key.clone(), rustfs_key));
}
Some(rustfs_value) if rustfs_value != source_value => {
conflict_keys.insert(rustfs_key);
}
Some(_) => {}
}
}
ExternalEnvCompatReport {
mapped_pairs: mapped_pairs.into_iter().collect(),
conflict_keys: conflict_keys.into_iter().collect(),
}
}
/// Build compatibility plan between source-prefixed variables and `RUSTFS_*`.
///
/// Precedence rule:
/// - If both `RUSTFS_*` and source-prefixed variables exist, keep `RUSTFS_*` and record a conflict.
/// - If only source-prefixed variables exist, mark them as mappable to `RUSTFS_*`.
pub fn build_external_env_compat_report() -> ExternalEnvCompatReport {
build_external_env_compat_report_from_entries(env::vars())
}
fn parse_env_value<T>(key: &str) -> Option<T>
where
T: std::str::FromStr,
{
resolve_env_with_aliases(key, &[]).and_then(|(_, value)| value.parse().ok())
}
pub fn get_env_str_with_aliases(key: &str, deprecated: &[&str], default: &str) -> String {
@@ -134,7 +298,7 @@ pub fn get_env_bool_with_aliases(key: &str, deprecated: &[&str], default: bool)
/// - `i16`: The parsed value as i16 if successful, otherwise the default value.
///
pub fn get_env_i16(key: &str, default: i16) -> i16 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: signed i16
@@ -146,7 +310,7 @@ pub fn get_env_i16(key: &str, default: i16) -> i16 {
/// - `Option<i16>`: The parsed value as i16 if successful, otherwise None
///
pub fn get_env_opt_i16(key: &str) -> Option<i16> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -160,7 +324,7 @@ pub fn get_env_opt_i16(key: &str) -> Option<i16> {
/// - `u16`: The parsed value as u16 if successful, otherwise the default value.
///
pub fn get_env_u16(key: &str, default: u16) -> u16 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: unsigned u16
@@ -172,7 +336,7 @@ pub fn get_env_u16(key: &str, default: u16) -> u16 {
/// - `Option<u16>`: The parsed value as u16 if successful, otherwise None
///
pub fn get_env_u16_opt(key: &str) -> Option<u16> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: unsigned u16
@@ -197,7 +361,7 @@ pub fn get_env_opt_u16(key: &str) -> Option<u16> {
/// - `i32`: The parsed value as i32 if successful, otherwise the default value.
///
pub fn get_env_i32(key: &str, default: i32) -> i32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 32-bit type: signed i32
@@ -209,7 +373,7 @@ pub fn get_env_i32(key: &str, default: i32) -> i32 {
/// - `Option<i32>`: The parsed value as i32 if successful, otherwise None
///
pub fn get_env_opt_i32(key: &str) -> Option<i32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -223,7 +387,7 @@ pub fn get_env_opt_i32(key: &str) -> Option<i32> {
/// - `u32`: The parsed value as u32 if successful, otherwise the default value.
///
pub fn get_env_u32(key: &str, default: u32) -> u32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 32-bit type: unsigned u32
@@ -235,7 +399,7 @@ pub fn get_env_u32(key: &str, default: u32) -> u32 {
/// - `Option<u32>`: The parsed value as u32 if successful, otherwise None
///
pub fn get_env_opt_u32(key: &str) -> Option<u32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
///
@@ -247,7 +411,7 @@ pub fn get_env_opt_u32(key: &str) -> Option<u32> {
/// - `f32`: The parsed value as f32 if successful, otherwise the default value
///
pub fn get_env_f32(key: &str, default: f32) -> f32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -258,7 +422,7 @@ pub fn get_env_f32(key: &str, default: f32) -> f32 {
/// - `Option<f32>`: The parsed value as f32 if successful, otherwise None
///
pub fn get_env_opt_f32(key: &str) -> Option<f32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -271,7 +435,7 @@ pub fn get_env_opt_f32(key: &str) -> Option<f32> {
/// - `i64`: The parsed value as i64 if successful, otherwise the default value
///
pub fn get_env_i64(key: &str, default: i64) -> i64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -282,7 +446,7 @@ pub fn get_env_i64(key: &str, default: i64) -> i64 {
/// - `Option<i64>`: The parsed value as i64 if successful, otherwise None
///
pub fn get_env_opt_i64(key: &str) -> Option<i64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning Option<Option<i64>> if not set or parsing fails.
@@ -294,7 +458,7 @@ pub fn get_env_opt_i64(key: &str) -> Option<i64> {
/// - `Option<Option<i64>>`: The parsed value as i64 if successful, otherwise None
///
pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
env::var(key).ok().map(|v| v.parse().ok())
resolve_env_with_aliases(key, &[]).map(|(_, value)| value.parse().ok())
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -307,7 +471,7 @@ pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
/// - `u64`: The parsed value as u64 if successful, otherwise the default value.
///
pub fn get_env_u64(key: &str, default: u64) -> u64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as an unsigned 64-bit integer, returning `None` if not set or parsing fails.
@@ -341,7 +505,7 @@ pub fn get_env_opt_u64_with_aliases(key: &str, deprecated: &[&str]) -> Option<u6
/// - `Option<u64>`: The parsed value as u64 if successful, otherwise None
///
pub fn get_env_opt_u64(key: &str) -> Option<u64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -354,7 +518,7 @@ pub fn get_env_opt_u64(key: &str) -> Option<u64> {
/// - `f64`: The parsed value as f64 if successful, otherwise the default value.
///
pub fn get_env_f64(key: &str, default: f64) -> f64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -366,7 +530,7 @@ pub fn get_env_f64(key: &str, default: f64) -> f64 {
/// - `Option<f64>`: The parsed value as f64 if successful, otherwise None
///
pub fn get_env_opt_f64(key: &str) -> Option<f64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -379,7 +543,7 @@ pub fn get_env_opt_f64(key: &str) -> Option<f64> {
/// - `usize`: The parsed value as usize if successful, otherwise the default value.
///
pub fn get_env_usize(key: &str, default: usize) -> usize {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -390,7 +554,7 @@ pub fn get_env_usize(key: &str, default: usize) -> usize {
/// - `Option<usize>`: The parsed value as usize if successful, otherwise None
///
pub fn get_env_usize_opt(key: &str) -> Option<usize> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -427,7 +591,7 @@ pub fn get_env_str(key: &str, default: &str) -> String {
/// - `Option<String>`: The environment variable value if set, otherwise None.
///
pub fn get_env_opt_str(key: &str) -> Option<String> {
env::var(key).ok()
resolve_env_with_aliases(key, &[]).map(|(_, value)| value)
}
/// Retrieve an environment variable as a boolean, with a default value if not set or parsing fails.
@@ -476,3 +640,105 @@ pub fn get_env_opt_bool(key: &str) -> Option<bool> {
None
})
}
/// Copy supported external-prefix variables such as `MINIO_*` into their
/// canonical `RUSTFS_*` names in the current process when the canonical key is
/// missing.
#[allow(unsafe_code)]
pub fn apply_external_env_compat() -> ExternalEnvCompatReport {
let report = build_external_env_compat_report();
for (source_key, rustfs_key) in &report.mapped_pairs {
if let Ok(value) = env::var(source_key) {
// Safety: this helper is intended for early startup bootstrap
// before any background threads are created.
unsafe {
env::set_var(rustfs_key, value);
}
}
}
report
}
#[cfg(test)]
mod tests {
use super::{apply_external_env_compat, build_external_env_compat_report_from_entries, get_env_str};
fn source_key(suffix: &str) -> String {
let mut key = super::external_env_prefix().to_string();
key.push_str(suffix);
key
}
#[test]
fn source_value_is_mapped_when_rustfs_missing() {
let report =
build_external_env_compat_report_from_entries(vec![(source_key("STORAGE_CLASS_STANDARD"), "EC:2".to_string())]);
assert_eq!(report.mapped_count(), 1);
assert!(
report
.mapped_pairs
.iter()
.any(|(input_key, rustfs_key)| input_key == &source_key("STORAGE_CLASS_STANDARD")
&& rustfs_key == "RUSTFS_STORAGE_CLASS_STANDARD")
);
assert_eq!(report.conflict_count(), 0);
}
#[test]
fn rustfs_value_takes_precedence_on_conflict() {
let report = build_external_env_compat_report_from_entries(vec![
("RUSTFS_ERASURE_SET_DRIVE_COUNT".to_string(), "8".to_string()),
(source_key("ERASURE_SET_DRIVE_COUNT"), "16".to_string()),
]);
assert_eq!(report.mapped_count(), 0);
assert_eq!(report.conflict_count(), 1);
assert!(report.conflict_keys.iter().any(|key| key == "RUSTFS_ERASURE_SET_DRIVE_COUNT"));
}
#[test]
fn dynamic_notify_suffix_is_mapped() {
let report =
build_external_env_compat_report_from_entries(vec![(source_key("NOTIFY_WEBHOOK_ENABLE_PRIMARY"), "on".to_string())]);
assert_eq!(report.mapped_count(), 1);
assert!(
report
.mapped_pairs
.iter()
.any(|(input_key, rustfs_key)| input_key == &source_key("NOTIFY_WEBHOOK_ENABLE_PRIMARY")
&& rustfs_key == "RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY")
);
assert_eq!(report.conflict_count(), 0);
}
#[test]
fn unrelated_source_key_is_ignored() {
let report = build_external_env_compat_report_from_entries(vec![(source_key("UNKNOWN_COMPAT_TEST"), "1".to_string())]);
assert_eq!(report.mapped_count(), 0);
assert_eq!(report.conflict_count(), 0);
}
#[test]
fn minio_alias_is_used_for_rustfs_reads() {
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
assert_eq!(get_env_str("RUSTFS_ROOT_USER", "default-user"), "compat-admin");
});
});
}
#[test]
fn apply_external_env_compat_copies_missing_rustfs_keys() {
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
let report = apply_external_env_compat();
assert!(
report
.mapped_pairs
.iter()
.any(|(source_key, rustfs_key)| source_key == "MINIO_ROOT_USER" && rustfs_key == "RUSTFS_ROOT_USER")
);
assert_eq!(std::env::var("RUSTFS_ROOT_USER").as_deref(), Ok("compat-admin"));
});
});
}
}
+101 -11
View File
@@ -12,13 +12,30 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use blake2::{Blake2b512, Digest as Blake2Digest};
use highway::{HighwayHash, HighwayHasher, Key};
use md5::{Digest, Md5};
use md5::Md5;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
/// The fixed key for HighwayHash256. DO NOT change for compatibility.
const HIGHWAY_HASH256_KEY: [u64; 4] = [3, 4, 2, 1];
/// Magic HH-256 key: HH-256 hash of first 100 decimals of π as utf-8 with zero key.
const MAGIC_HIGHWAY_HASH256_KEY: [u8; 32] = [
0x4b, 0xe7, 0x34, 0xfa, 0x8e, 0x23, 0x8a, 0xcd, 0x26, 0x3e, 0x83, 0xe6, 0xbb, 0x96, 0x85, 0x52, 0x04, 0x0f, 0x93, 0x5d, 0xa3,
0x9f, 0x44, 0x14, 0x97, 0xe0, 0x9d, 0x13, 0x22, 0xde, 0x36, 0xa0,
];
/// Legacy HH-256 key (main branch): fixed [3,4,2,1] as u64 LE.
const LEGACY_HIGHWAY_HASH256_KEY: [u8; 32] = [
3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
];
fn highway_key_from_bytes(bytes: &[u8; 32]) -> [u64; 4] {
let mut key = [0u64; 4];
for (i, chunk) in bytes.chunks_exact(8).enumerate() {
key[i] = u64::from_le_bytes(chunk.try_into().unwrap());
}
key
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone, Eq, Hash)]
/// Supported hash algorithms for bitrot protection.
@@ -30,6 +47,8 @@ pub enum HashAlgorithm {
// HighwayHash256S represents the Streaming HighwayHash-256 hash function
#[default]
HighwayHash256S,
/// Legacy HighwayHash256S (main branch) with fixed key [3,4,2,1]
HighwayHash256SLegacy,
// BLAKE2b512 represents the BLAKE2b-512 hash function
BLAKE2b512,
/// MD5 (128-bit)
@@ -43,7 +62,8 @@ enum HashEncoded {
Sha256([u8; 32]),
HighwayHash256([u8; 32]),
HighwayHash256S([u8; 32]),
Blake2b512(blake3::Hash),
HighwayHash256SLegacy([u8; 32]),
Blake2b512([u8; 64]),
None,
}
@@ -55,7 +75,8 @@ impl AsRef<[u8]> for HashEncoded {
HashEncoded::Sha256(hash) => hash.as_ref(),
HashEncoded::HighwayHash256(hash) => hash.as_ref(),
HashEncoded::HighwayHash256S(hash) => hash.as_ref(),
HashEncoded::Blake2b512(hash) => hash.as_bytes(),
HashEncoded::HighwayHash256SLegacy(hash) => hash.as_ref(),
HashEncoded::Blake2b512(hash) => hash.as_ref(),
HashEncoded::None => &[],
}
}
@@ -83,17 +104,30 @@ impl HashAlgorithm {
match self {
HashAlgorithm::Md5 => HashEncoded::Md5(Md5::digest(data).into()),
HashAlgorithm::HighwayHash256 => {
let mut hasher = HighwayHasher::new(Key(HIGHWAY_HASH256_KEY));
let key = Key(highway_key_from_bytes(&MAGIC_HIGHWAY_HASH256_KEY));
let mut hasher = HighwayHasher::new(key);
hasher.append(data);
HashEncoded::HighwayHash256(u8x32_from_u64x4(hasher.finalize256()))
}
HashAlgorithm::SHA256 => HashEncoded::Sha256(Sha256::digest(data).into()),
HashAlgorithm::HighwayHash256S => {
let mut hasher = HighwayHasher::new(Key(HIGHWAY_HASH256_KEY));
let key = Key(highway_key_from_bytes(&MAGIC_HIGHWAY_HASH256_KEY));
let mut hasher = HighwayHasher::new(key);
hasher.append(data);
HashEncoded::HighwayHash256S(u8x32_from_u64x4(hasher.finalize256()))
}
HashAlgorithm::BLAKE2b512 => HashEncoded::Blake2b512(blake3::hash(data)),
HashAlgorithm::HighwayHash256SLegacy => {
let key = Key(highway_key_from_bytes(&LEGACY_HIGHWAY_HASH256_KEY));
let mut hasher = HighwayHasher::new(key);
hasher.append(data);
HashEncoded::HighwayHash256SLegacy(u8x32_from_u64x4(hasher.finalize256()))
}
HashAlgorithm::BLAKE2b512 => {
let hash = Blake2b512::digest(data);
let mut out = [0u8; 64];
out.copy_from_slice(hash.as_ref());
HashEncoded::Blake2b512(out)
}
HashAlgorithm::None => HashEncoded::None,
}
}
@@ -108,7 +142,8 @@ impl HashAlgorithm {
HashAlgorithm::SHA256 => 32,
HashAlgorithm::HighwayHash256 => 32,
HashAlgorithm::HighwayHash256S => 32,
HashAlgorithm::BLAKE2b512 => 32, // blake3 outputs 32 bytes by default
HashAlgorithm::HighwayHash256SLegacy => 32,
HashAlgorithm::BLAKE2b512 => 64,
HashAlgorithm::Md5 => 16,
HashAlgorithm::None => 0,
}
@@ -167,7 +202,7 @@ mod tests {
assert_eq!(HashAlgorithm::HighwayHash256.size(), 32);
assert_eq!(HashAlgorithm::HighwayHash256S.size(), 32);
assert_eq!(HashAlgorithm::SHA256.size(), 32);
assert_eq!(HashAlgorithm::BLAKE2b512.size(), 32);
assert_eq!(HashAlgorithm::BLAKE2b512.size(), 64);
assert_eq!(HashAlgorithm::None.size(), 0);
}
@@ -220,13 +255,68 @@ mod tests {
let data = b"test data";
let hash = HashAlgorithm::BLAKE2b512.hash_encode(data);
let hash = hash.as_ref();
assert_eq!(hash.len(), 32); // blake3 outputs 32 bytes by default
assert_eq!(hash.len(), 64);
// BLAKE2b512 should be deterministic
let hash2 = HashAlgorithm::BLAKE2b512.hash_encode(data);
let hash2 = hash2.as_ref();
assert_eq!(hash, hash2);
}
#[test]
fn test_bitrot_selftest() {
let checksums: [(HashAlgorithm, &str); 5] = [
(HashAlgorithm::SHA256, "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004"),
(
HashAlgorithm::BLAKE2b512,
"e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
),
(
HashAlgorithm::HighwayHash256,
"39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
),
(
HashAlgorithm::HighwayHash256S,
"39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
),
(
HashAlgorithm::HighwayHash256SLegacy,
"a5592a831588836b0f61bff43da4bd957c376d9b6412a9ecbbd144a3ecf34649",
),
];
for (algo, expected_hex) in checksums {
let block_size = match algo {
HashAlgorithm::SHA256 => 64,
HashAlgorithm::BLAKE2b512 => 128,
HashAlgorithm::HighwayHash256 | HashAlgorithm::HighwayHash256S | HashAlgorithm::HighwayHash256SLegacy => 32,
_ => continue,
};
let mut msg = Vec::new();
let mut sum = Vec::new();
for _ in 0..block_size {
sum = algo.hash_encode(&msg).as_ref().to_vec();
msg.extend_from_slice(&sum);
}
let got = hex_simd::encode_to_string(&sum, hex_simd::AsciiCase::Lower);
assert_eq!(got, expected_hex, "{:?} selftest mismatch: got {} want {}", algo, got, expected_hex);
}
}
/// Generates 7557 bytes
/// Pattern: (i*7+13)%256 for each byte.
fn generate_compat_test_data(size: usize) -> Vec<u8> {
(0..size).map(|i| ((i * 7 + 13) % 256) as u8).collect()
}
/// Run: cargo test -p rustfs-utils test_highwayhash_compat
#[test]
fn test_highwayhash_compat() {
let data = generate_compat_test_data(7557);
let hash = HashAlgorithm::HighwayHash256S.hash_encode(&data);
let got = hex_simd::encode_to_string(hash.as_ref(), hex_simd::AsciiCase::Lower);
let expected = "06543bf1c637e67386922a43b71cca08e5faa0f9131105a2bf96dec880529551";
assert_eq!(got, expected, "HighwayHash256S must match: got {} want {}", got, expected);
}
#[test]
fn test_different_data_different_hashes() {
let data1 = b"test data 1";
+123
View File
@@ -0,0 +1,123 @@
// 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.
//! HTTP header compatibility: read both x-rustfs-* and x-minio-* headers for MinIO
//! interoperability. Write both when sending replication requests.
//!
//! Use suffix-based API: `get_header(headers, SUFFIX_FORCE_DELETE)` queries both
//! x-rustfs-force-delete and x-minio-force-delete.
use http::{HeaderMap, HeaderValue};
use std::borrow::Cow;
const RUSTFS_PREFIX: &str = "x-rustfs-";
const MINIO_PREFIX: &str = "x-minio-";
const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-";
const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-";
// Suffix constants (part after x-rustfs- or x-minio-). Use with get_header/insert_header.
pub const SUFFIX_FORCE_DELETE: &str = "force-delete";
pub const SUFFIX_INCLUDE_DELETED: &str = "include-deleted";
pub const SUFFIX_REPLICATION_RESET_STATUS: &str = "replication-reset-status";
pub const SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE: &str = "replication-actual-object-size";
pub const SUFFIX_SOURCE_VERSION_ID: &str = "source-version-id";
pub const SUFFIX_SOURCE_MTIME: &str = "source-mtime";
pub const SUFFIX_SOURCE_ETAG: &str = "source-etag";
pub const SUFFIX_SOURCE_DELETEMARKER: &str = "source-deletemarker";
pub const SUFFIX_SOURCE_PROXY_REQUEST: &str = "source-proxy-request";
pub const SUFFIX_SOURCE_REPLICATION_REQUEST: &str = "source-replication-request";
pub const SUFFIX_SOURCE_REPLICATION_CHECK: &str = "source-replication-check";
pub const SUFFIX_REPLICATION_SSEC_CRC: &str = "replication-ssec-crc";
/// Returns true if the key is an internal encryption metadata key (x-rustfs-encryption-* or
/// x-minio-encryption-*). Case-insensitive for metadata filtering.
pub fn is_encryption_metadata_key(key: &str) -> bool {
let lower = key.to_lowercase();
lower.starts_with(RUSTFS_ENCRYPTION_PREFIX) || lower.starts_with(MINIO_ENCRYPTION_PREFIX)
}
fn rustfs_key(suffix: &str) -> String {
format!("{RUSTFS_PREFIX}{suffix}")
}
fn minio_key(suffix: &str) -> String {
format!("{MINIO_PREFIX}{suffix}")
}
/// Get header value: tries x-rustfs-{suffix} first, then x-minio-{suffix}. Case-insensitive.
pub fn get_header<'a>(headers: &'a HeaderMap, suffix: &str) -> Option<Cow<'a, str>> {
let rk = rustfs_key(suffix);
let mk = minio_key(suffix);
headers
.get(&rk)
.or_else(|| headers.get(&mk))
.and_then(|v| v.to_str().ok().map(Cow::Borrowed))
}
/// Insert header with both x-rustfs-{suffix} and x-minio-{suffix}.
pub fn insert_header(headers: &mut HeaderMap, suffix: &str, value: impl AsRef<[u8]>) {
if let Ok(v) = HeaderValue::from_bytes(value.as_ref()) {
if let Ok(k1) = rustfs_key(suffix).parse::<http::HeaderName>() {
headers.insert(k1, v.clone());
}
if let Ok(k2) = minio_key(suffix).parse::<http::HeaderName>() {
headers.insert(k2, v);
}
}
}
/// Get from HashMap: tries x-rustfs-{suffix} first, then x-minio-{suffix}.
pub fn get_header_map(map: &std::collections::HashMap<String, String>, suffix: &str) -> Option<String> {
let rk = rustfs_key(suffix);
let mk = minio_key(suffix);
map.get(&rk).cloned().or_else(|| map.get(&mk).cloned())
}
/// Insert into HashMap with both x-rustfs-{suffix} and x-minio-{suffix}.
pub fn insert_header_map(map: &mut std::collections::HashMap<String, String>, suffix: &str, value: impl Into<String>) {
let v = value.into();
map.insert(rustfs_key(suffix), v.clone());
map.insert(minio_key(suffix), v);
}
/// Remove from HashMap both x-rustfs-{suffix} and x-minio-{suffix}.
pub fn remove_header_map(map: &mut std::collections::HashMap<String, String>, suffix: &str) {
map.remove(&rustfs_key(suffix));
map.remove(&minio_key(suffix));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_encryption_metadata_key() {
assert!(is_encryption_metadata_key("x-rustfs-encryption-iv"));
assert!(is_encryption_metadata_key("X-Rustfs-Encryption-Key"));
assert!(is_encryption_metadata_key("x-minio-encryption-iv"));
assert!(!is_encryption_metadata_key("x-amz-meta-custom"));
assert!(!is_encryption_metadata_key("x-rustfs-internal-healing"));
}
#[test]
fn test_get_header() {
let mut headers = HeaderMap::new();
headers.insert("x-minio-force-delete", HeaderValue::from_static("true"));
assert_eq!(get_header(&headers, SUFFIX_FORCE_DELETE).as_deref(), Some("true"));
let mut headers2 = HeaderMap::new();
headers2.insert("X-Rustfs-Force-Delete", HeaderValue::from_static("true"));
assert_eq!(get_header(&headers2, SUFFIX_FORCE_DELETE).as_deref(), Some("true"));
}
}
-32
View File
@@ -148,41 +148,9 @@ pub const AMZ_META_NAME: &str = "X-Amz-Meta-Name";
pub const AMZ_META_UNENCRYPTED_CONTENT_LENGTH: &str = "X-Amz-Meta-X-Amz-Unencrypted-Content-Length";
pub const AMZ_META_UNENCRYPTED_CONTENT_MD5: &str = "X-Amz-Meta-X-Amz-Unencrypted-Content-Md5";
pub const RUSTFS_ENCRYPTION: &str = "X-Rustfs-Encryption-";
pub const RUSTFS_ENCRYPTION_LOWER: &str = "x-rustfs-encryption-";
pub const RESERVED_METADATA_PREFIX: &str = "X-RustFS-Internal-";
pub const RESERVED_METADATA_PREFIX_LOWER: &str = "x-rustfs-internal-";
pub const RUSTFS_HEALING: &str = "X-Rustfs-Internal-healing";
// pub const RUSTFS_DATA_MOVE: &str = "X-Rustfs-Internal-data-mov";
// pub const X_RUSTFS_INLINE_DATA: &str = "x-rustfs-inline-data";
pub const VERSION_PURGE_STATUS_KEY: &str = "X-Rustfs-Internal-purgestatus";
pub const X_RUSTFS_HEALING: &str = "X-Rustfs-Internal-healing";
pub const X_RUSTFS_DATA_MOV: &str = "X-Rustfs-Internal-data-mov";
pub const AMZ_TAGGING_DIRECTIVE: &str = "X-Amz-Tagging-Directive";
pub const RUSTFS_DATA_MOVE: &str = "X-Rustfs-Internal-data-mov";
pub const RUSTFS_FORCE_DELETE: &str = "X-Rustfs-Force-Delete";
pub const RUSTFS_INCLUDE_DELETED: &str = "X-Rustfs-Include-Deleted";
pub const RUSTFS_REPLICATION_RESET_STATUS: &str = "X-Rustfs-Replication-Reset-Status";
pub const RUSTFS_REPLICATION_ACTUAL_OBJECT_SIZE: &str = "X-Rustfs-Replication-Actual-Object-Size";
pub const RUSTFS_BUCKET_SOURCE_VERSION_ID: &str = "X-Rustfs-Source-Version-Id";
pub const RUSTFS_BUCKET_SOURCE_MTIME: &str = "X-RustFS-Source-Mtime";
pub const RUSTFS_BUCKET_SOURCE_ETAG: &str = "X-Rustfs-Source-Etag";
pub const RUSTFS_BUCKET_REPLICATION_DELETE_MARKER: &str = "X-Rustfs-Source-DeleteMarker";
pub const RUSTFS_BUCKET_REPLICATION_PROXY_REQUEST: &str = "X-Rustfs-Source-Proxy-Request";
pub const RUSTFS_BUCKET_REPLICATION_REQUEST: &str = "X-Rustfs-Source-Replication-Request";
pub const RUSTFS_BUCKET_REPLICATION_CHECK: &str = "X-Rustfs-Source-Replication-Check";
pub const RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM: &str = "X-Rustfs-Source-Replication-Ssec-Crc";
// SSEC encryption header constants
pub const SSEC_ALGORITHM_HEADER: &str = "x-amz-server-side-encryption-customer-algorithm";
pub const SSEC_KEY_HEADER: &str = "x-amz-server-side-encryption-customer-key";
+181
View File
@@ -0,0 +1,181 @@
// 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.
//! System metadata compatibility: write both x-rustfs-internal-* and x-minio-internal-*
//! for MinIO interoperability. Read prefers RustFS, fallback to MinIO.
use std::collections::HashMap;
pub const RUSTFS_INTERNAL_PREFIX: &str = "x-rustfs-internal-";
pub const MINIO_INTERNAL_PREFIX: &str = "x-minio-internal-";
// Key suffixes (lowercase, no prefix)
pub const SUFFIX_INLINE_DATA: &str = "inline-data";
pub const SUFFIX_DATA_MOVED: &str = "data-moved";
/// Transient flag for data movement
pub const SUFFIX_DATA_MOV: &str = "data-mov";
/// Transient flag for healing
pub const SUFFIX_HEALING: &str = "healing";
pub const SUFFIX_COMPRESSION: &str = "compression";
pub const SUFFIX_COMPRESSION_SIZE: &str = "compression-size";
pub const SUFFIX_ACTUAL_SIZE: &str = "actual-size";
pub const SUFFIX_ACTUAL_OBJECT_SIZE: &str = "actual-object-size";
/// Used by replication; key stored with capital A
pub const SUFFIX_ACTUAL_OBJECT_SIZE_CAP: &str = "Actual-Object-Size";
pub const SUFFIX_CRC: &str = "crc";
pub const SUFFIX_TRANSITION_STATUS: &str = "transition-status";
pub const SUFFIX_TRANSITIONED_OBJECTNAME: &str = "transitioned-object";
pub const SUFFIX_TRANSITIONED_VERSION_ID: &str = "transitioned-versionID";
pub const SUFFIX_TRANSITION_TIER: &str = "transition-tier";
pub const SUFFIX_FREE_VERSION: &str = "free-version";
pub const SUFFIX_PURGESTATUS: &str = "purgestatus";
pub const SUFFIX_REPLICA_STATUS: &str = "replica-status";
pub const SUFFIX_REPLICA_TIMESTAMP: &str = "replica-timestamp";
pub const SUFFIX_REPLICATION_STATUS: &str = "replication-status";
pub const SUFFIX_REPLICATION_TIMESTAMP: &str = "replication-timestamp";
pub const SUFFIX_TAGGING_TIMESTAMP: &str = "tagging-timestamp";
pub const SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP: &str = "objectlock-retention-timestamp";
pub const SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP: &str = "objectlock-legalhold-timestamp";
pub const SUFFIX_REPLICATION_RESET: &str = "replication-reset";
/// Prefix for replication-reset-{arn} keys; use with internal_key_strip_suffix_prefix to extract arn.
pub const SUFFIX_REPLICATION_RESET_ARN_PREFIX: &str = "replication-reset-";
pub const SUFFIX_TIER_FV_ID: &str = "tier-free-versionID";
pub const SUFFIX_TIER_FV_MARKER: &str = "tier-free-marker";
pub const SUFFIX_TIER_SKIP_FV_ID: &str = "tier-skip-fvid";
/// Returns true if the key is an internal metadata key (x-rustfs-internal-* or x-minio-internal-*)
/// for xl.meta compatibility. Case-insensitive.
pub fn is_internal_key(key: &str) -> bool {
let lower = key.to_lowercase();
lower.starts_with(RUSTFS_INTERNAL_PREFIX) || lower.starts_with(MINIO_INTERNAL_PREFIX)
}
/// Returns true if the key matches the given suffix for either x-rustfs-internal-* or x-minio-internal-*.
pub fn has_internal_suffix(key: &str, suffix: &str) -> bool {
let lower = key.to_lowercase();
let rustfs_key = format!("{RUSTFS_INTERNAL_PREFIX}{suffix}");
let minio_key = format!("{MINIO_INTERNAL_PREFIX}{suffix}");
lower == rustfs_key || lower == minio_key
}
/// Strips x-rustfs-internal- or x-minio-internal- prefix from key. Returns the suffix part.
/// Case-insensitive. Returns None if key is not an internal key.
pub fn strip_internal_prefix(key: &str) -> Option<String> {
let lower = key.to_lowercase();
lower
.strip_prefix(RUSTFS_INTERNAL_PREFIX)
.or_else(|| lower.strip_prefix(MINIO_INTERNAL_PREFIX))
.map(|s| s.to_string())
}
/// Returns true if key is internal and its suffix part starts with the given suffix_prefix.
/// E.g. internal_key_starts_with("x-rustfs-internal-replication-reset-arn1", "replication-reset") == true.
pub fn internal_key_starts_with(key: &str, suffix_prefix: &str) -> bool {
strip_internal_prefix(key).is_some_and(|s| s.starts_with(suffix_prefix))
}
/// For keys like x-rustfs-internal-replication-reset-{arn}, strips the internal prefix and suffix_prefix,
/// returning the remainder (e.g. "arn1"). Returns None if key does not match.
pub fn internal_key_strip_suffix_prefix(key: &str, suffix_prefix: &str) -> Option<String> {
let rest = strip_internal_prefix(key)?;
rest.strip_prefix(suffix_prefix).map(|s| s.to_string())
}
fn both_keys(suffix: &str) -> (String, String) {
(format!("{RUSTFS_INTERNAL_PREFIX}{suffix}"), format!("{MINIO_INTERNAL_PREFIX}{suffix}"))
}
/// Builds the RustFS internal key for the given suffix. Use when a single key is needed (e.g. for
/// backward compat). Prefer insert_str/get_str when both keys should be written/read.
pub fn internal_key_rustfs(suffix: &str) -> String {
format!("{RUSTFS_INTERNAL_PREFIX}{suffix}")
}
// === String type (FileInfo.metadata, user_defined) ===
pub fn insert_str(map: &mut HashMap<String, String>, suffix: &str, value: String) {
let (k1, k2) = both_keys(suffix);
map.insert(k1, value.clone());
map.insert(k2, value);
}
pub fn get_str(map: &HashMap<String, String>, suffix: &str) -> Option<String> {
let (k1, k2) = both_keys(suffix);
map.get(&k1).cloned().or_else(|| map.get(&k2).cloned())
}
pub fn contains_key_str(map: &HashMap<String, String>, suffix: &str) -> bool {
let (k1, k2) = both_keys(suffix);
map.contains_key(&k1) || map.contains_key(&k2)
}
pub fn remove_str(map: &mut HashMap<String, String>, suffix: &str) {
let (k1, k2) = both_keys(suffix);
map.remove(&k1);
map.remove(&k2);
}
// === Vec<u8> type (meta_sys) ===
pub fn insert_bytes(map: &mut HashMap<String, Vec<u8>>, suffix: &str, value: Vec<u8>) {
let (k1, k2) = both_keys(suffix);
let v = value.clone();
map.insert(k1, value);
map.insert(k2, v);
}
pub fn get_bytes(map: &HashMap<String, Vec<u8>>, suffix: &str) -> Option<Vec<u8>> {
let (k1, k2) = both_keys(suffix);
map.get(&k1).cloned().or_else(|| map.get(&k2).cloned())
}
pub fn contains_key_bytes(map: &HashMap<String, Vec<u8>>, suffix: &str) -> bool {
let (k1, k2) = both_keys(suffix);
map.contains_key(&k1) || map.contains_key(&k2)
}
pub fn remove_bytes(map: &mut HashMap<String, Vec<u8>>, suffix: &str) {
let (k1, k2) = both_keys(suffix);
map.remove(&k1);
map.remove(&k2);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_internal_key() {
assert!(is_internal_key("x-rustfs-internal-healing"));
assert!(is_internal_key("x-rustfs-internal-purgestatus"));
assert!(is_internal_key("X-RustFS-Internal-purgestatus"));
assert!(is_internal_key("x-minio-internal-compression"));
assert!(is_internal_key("x-minio-internal-replication-status"));
assert!(is_internal_key("X-Minio-Internal-Compression"));
assert!(!is_internal_key("x-amz-meta-custom"));
assert!(!is_internal_key("content-type"));
assert!(!is_internal_key("x-rustfs-meta-custom"));
}
#[test]
fn test_has_internal_suffix() {
assert!(has_internal_suffix("x-rustfs-internal-purgestatus", SUFFIX_PURGESTATUS));
assert!(has_internal_suffix("X-Minio-Internal-purgestatus", SUFFIX_PURGESTATUS));
assert!(has_internal_suffix("x-minio-internal-compression", SUFFIX_COMPRESSION));
assert!(has_internal_suffix("x-rustfs-internal-healing", SUFFIX_HEALING));
assert!(has_internal_suffix("x-minio-internal-data-mov", SUFFIX_DATA_MOV));
assert!(!has_internal_suffix("x-rustfs-internal-purgestatus", SUFFIX_HEALING));
assert!(!has_internal_suffix("x-amz-meta-custom", SUFFIX_PURGESTATUS));
}
}
+4
View File
@@ -12,7 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod header_compat;
pub mod headers;
pub mod ip;
pub mod metadata_compat;
pub use header_compat::*;
pub use headers::*;
pub use ip::*;
pub use metadata_compat::*;
+2 -3
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::http::{RESERVED_METADATA_PREFIX_LOWER, is_minio_header, is_rustfs_header};
use crate::http::{is_internal_key, is_minio_header, is_rustfs_header};
use std::collections::HashMap;
/// Extract user-defined metadata keys from object metadata.
@@ -80,7 +80,7 @@ pub fn extract_user_defined_metadata(metadata: &HashMap<String, String>) -> Hash
for (key, value) in metadata {
let lower_key = key.to_ascii_lowercase();
if lower_key.starts_with(RESERVED_METADATA_PREFIX_LOWER) {
if is_internal_key(key) {
continue;
}
@@ -188,7 +188,6 @@ mod tests {
let mut metadata: HashMap<String, String> = HashMap::new();
metadata.insert("x-rustfs-internal-healing".to_string(), "true".to_string());
metadata.insert("x-rustfs-internal-data-mov".to_string(), "value".to_string());
metadata.insert("X-RustFS-Internal-purgestatus".to_string(), "status".to_string());
metadata.insert("x-rustfs-meta-custom".to_string(), "custom-value".to_string());
metadata.insert("my-key".to_string(), "my-value".to_string());