mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 15:16:56 +00:00
fix: restore s3 compatibility regressions and CI coverage (#1793)
This commit is contained in:
@@ -4265,7 +4265,9 @@ impl S3 for FS {
|
||||
return Err(s3_error!(MalformedPolicy));
|
||||
}
|
||||
|
||||
let data = serde_json::to_vec(&cfg).map_err(|e| s3_error!(InternalError, "parse policy failed {:?}", e))?;
|
||||
// Preserve the original JSON text so GetBucketPolicy can return byte-for-byte content.
|
||||
// s3-tests expects exact string round-trip equality.
|
||||
let data = policy.into_bytes();
|
||||
|
||||
metadata_sys::update(&bucket, BUCKET_POLICY_CONFIG, data)
|
||||
.await
|
||||
|
||||
@@ -27,6 +27,7 @@ mod tests {
|
||||
use rustfs_config::MI_B;
|
||||
use rustfs_ecstore::set_disk::DEFAULT_READ_BUFFER_SIZE;
|
||||
use rustfs_ecstore::store_api::ObjectInfo;
|
||||
use rustfs_policy::policy::{BucketPolicy, Validator};
|
||||
use rustfs_utils::http::{AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, RESERVED_METADATA_PREFIX_LOWER};
|
||||
use rustfs_zip::CompressionFormat;
|
||||
use s3s::dto::{
|
||||
@@ -1012,6 +1013,34 @@ mod tests {
|
||||
assert_eq!(filtered_version_marker.unwrap(), "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucket_policy_round_trip_preserves_original_json_text() {
|
||||
let policy = r#"{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::example-bucket",
|
||||
"arn:aws:s3:::example-bucket/*"
|
||||
]
|
||||
}]
|
||||
}"#;
|
||||
|
||||
let parsed: BucketPolicy = serde_json::from_str(policy).unwrap();
|
||||
assert!(parsed.is_valid().is_ok());
|
||||
|
||||
// Normalized serialization can differ (for example, Action becomes an array).
|
||||
let normalized = serde_json::to_string(&parsed).unwrap();
|
||||
assert_ne!(normalized, policy);
|
||||
|
||||
// Stored raw policy bytes must preserve exact text for GetBucketPolicy round trip.
|
||||
let stored = policy.as_bytes().to_vec();
|
||||
let round_trip = String::from_utf8(stored).unwrap();
|
||||
assert_eq!(round_trip, policy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_origin_pattern_exact_match() {
|
||||
// Test exact match
|
||||
|
||||
@@ -392,13 +392,9 @@ pub fn extract_metadata_from_mime_with_object_name(
|
||||
pub(crate) fn filter_object_metadata(metadata: &HashMap<String, String>) -> Option<HashMap<String, String>> {
|
||||
// HTTP headers that should NOT be returned in the Metadata field.
|
||||
// These headers are returned as separate response headers, not user metadata.
|
||||
//
|
||||
// Note: content-type and content-disposition are intentionally NOT excluded here.
|
||||
// They remain in the filtered metadata so they can continue to be exposed via
|
||||
// x-amz-meta-* style user metadata for backward compatibility, while the HEAD
|
||||
// implementation also mirrors their values into the standard Content-Type and
|
||||
// Content-Disposition response headers where appropriate.
|
||||
const EXCLUDED_HEADERS: &[&str] = &[
|
||||
"content-type",
|
||||
"content-disposition",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"cache-control",
|
||||
@@ -437,7 +433,6 @@ pub(crate) fn filter_object_metadata(metadata: &HashMap<String, String>) -> Opti
|
||||
}
|
||||
|
||||
// Skip excluded HTTP headers (they are returned as separate headers, not metadata)
|
||||
// Note: content-type and content-disposition are NOT excluded and will be treated as user metadata
|
||||
if EXCLUDED_HEADERS.contains(&lower_key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
@@ -1209,6 +1204,34 @@ mod tests {
|
||||
assert_eq!(metadata.get("content-type"), Some(&"custom/type".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_object_metadata_excludes_standard_headers() {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("content-type".to_string(), "application/octet-stream".to_string());
|
||||
metadata.insert("content-disposition".to_string(), "inline".to_string());
|
||||
metadata.insert("cache-control".to_string(), "no-cache".to_string());
|
||||
metadata.insert("x-amz-storage-class".to_string(), "STANDARD".to_string());
|
||||
metadata.insert("custom-key".to_string(), "custom-value".to_string());
|
||||
|
||||
let filtered = filter_object_metadata(&metadata).unwrap();
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered.get("custom-key"), Some(&"custom-value".to_string()));
|
||||
assert!(!filtered.contains_key("content-type"));
|
||||
assert!(!filtered.contains_key("content-disposition"));
|
||||
assert!(!filtered.contains_key("cache-control"));
|
||||
assert!(!filtered.contains_key("x-amz-storage-class"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_object_metadata_returns_none_for_only_content_type() {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("content-type".to_string(), "application/octet-stream".to_string());
|
||||
|
||||
let filtered = filter_object_metadata(&metadata);
|
||||
assert!(filtered.is_none(), "content-type must not be exposed as user metadata");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_content_type_from_object_name() {
|
||||
// Test Parquet files (our custom handling)
|
||||
|
||||
Reference in New Issue
Block a user