diff --git a/crates/notify/src/rules/config_test.rs b/crates/notify/src/rules/config_test.rs index a2d2dbef1..34a6b57dd 100644 --- a/crates/notify/src/rules/config_test.rs +++ b/crates/notify/src/rules/config_test.rs @@ -161,6 +161,39 @@ mod integration_tests { assert!(targets.is_empty(), "Files not in images/ should not match"); } + #[test] + fn test_capitalized_filter_names_xml() { + let xml = r#" + + + test-queue + arn:rustfs:sqs:ap-northeast-1:primary:webhook + s3:ObjectCreated:* + + + + Prefix + uploads/ + + + Suffix + .csv + + + + +"#; + + let current_region = "ap-northeast-1"; + let arn_list = vec!["arn:rustfs:sqs:ap-northeast-1:primary:webhook".to_string()]; + + let config = BucketNotificationConfig::from_xml(Cursor::new(xml.as_bytes()), current_region, &arn_list).unwrap(); + let rules_map = config.get_rules_map(); + + let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/report.csv"); + assert!(!targets.is_empty(), "capitalized filter names should still build prefix/suffix rules"); + } + /// Test suffix only filter #[test] fn test_suffix_only_filter_xml() { diff --git a/crates/notify/src/rules/xml_config.rs b/crates/notify/src/rules/xml_config.rs index 14987f50d..51b5a1b5f 100644 --- a/crates/notify/src/rules/xml_config.rs +++ b/crates/notify/src/rules/xml_config.rs @@ -66,7 +66,7 @@ pub struct FilterRule { impl FilterRule { fn validate(&self) -> Result<(), ParseConfigError> { - if self.name != "prefix" && self.name != "suffix" { + if !self.name.eq_ignore_ascii_case("prefix") && !self.name.eq_ignore_ascii_case("suffix") { return Err(ParseConfigError::InvalidFilterName(self.name.clone())); } // ValidateFilterRuleValue from Go: @@ -98,12 +98,12 @@ impl S3KeyFilter { let mut has_suffix = false; for rule in &self.filter_rule_list { rule.validate()?; - if rule.name == "prefix" { + if rule.name.eq_ignore_ascii_case("prefix") { if has_prefix { return Err(ParseConfigError::DuplicatePrefixFilter); } has_prefix = true; - } else if rule.name == "suffix" { + } else if rule.name.eq_ignore_ascii_case("suffix") { if has_suffix { return Err(ParseConfigError::DuplicateSuffixFilter); } @@ -126,9 +126,9 @@ impl S3KeyFilter { let mut suffix_val: Option<&str> = None; for rule in &self.filter_rule_list { - if rule.name == "prefix" { + if rule.name.eq_ignore_ascii_case("prefix") { prefix_val = Some(&rule.value); - } else if rule.name == "suffix" { + } else if rule.name.eq_ignore_ascii_case("suffix") { suffix_val = Some(&rule.value); } } diff --git a/rustfs/src/app/bucket_usecase.rs b/rustfs/src/app/bucket_usecase.rs index adb6e7d2c..508c13266 100644 --- a/rustfs/src/app/bucket_usecase.rs +++ b/rustfs/src/app/bucket_usecase.rs @@ -145,22 +145,18 @@ fn validate_notification_filter_rules( return Err(s3_error!(InvalidArgument, "{}", invalid_filter_value_message(&cfg_scope, value))); } - match name.as_str() { - "prefix" => { - if has_prefix { - return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})")); - } - has_prefix = true; + if name.as_str().eq_ignore_ascii_case("prefix") { + if has_prefix { + return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})")); } - "suffix" => { - if has_suffix { - return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'suffix' ({cfg_scope})")); - } - has_suffix = true; - } - other => { - return Err(s3_error!(InvalidArgument, "{}", invalid_filter_name_message(&cfg_scope, other))); + has_prefix = true; + } else if name.as_str().eq_ignore_ascii_case("suffix") { + if has_suffix { + return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'suffix' ({cfg_scope})")); } + has_suffix = true; + } else { + return Err(s3_error!(InvalidArgument, "{}", invalid_filter_name_message(&cfg_scope, name.as_str()))); } } @@ -3047,7 +3043,7 @@ mod tests { #[test] fn validate_notification_configuration_filters_rejects_invalid_filter_name() { - let raw_name = "Prefix".repeat(100); + let raw_name = "unsupported".repeat(100); let cfg = NotificationConfiguration { queue_configurations: Some(vec![QueueConfiguration { id: Some("q1".to_string()), @@ -3072,6 +3068,34 @@ mod tests { assert!(!msg.contains(&raw_name), "error message should not echo full raw filter name"); } + #[test] + fn validate_notification_configuration_filters_accepts_case_insensitive_filter_names() { + let cfg = NotificationConfiguration { + queue_configurations: Some(vec![QueueConfiguration { + id: Some("q1".to_string()), + queue_arn: "arn:rustfs:sqs:us-east-1:1:webhook".to_string(), + events: vec!["s3:ObjectCreated:*".to_string().into()], + filter: Some(NotificationConfigurationFilter { + key: Some(S3KeyFilter { + filter_rules: Some(vec![ + FilterRule { + name: Some(FilterRuleName::from("Prefix".to_string())), + value: Some("uploads/".to_string()), + }, + FilterRule { + name: Some(FilterRuleName::from("Suffix".to_string())), + value: Some(".csv".to_string()), + }, + ]), + }), + }), + }]), + ..Default::default() + }; + + validate_notification_configuration_filters(&cfg).expect("capitalized filter names should be accepted"); + } + #[test] fn validate_notification_configuration_filters_rejects_duplicate_prefix_rules() { let cfg = NotificationConfiguration { @@ -3229,7 +3253,7 @@ mod tests { filter: Some(NotificationConfigurationFilter { key: Some(S3KeyFilter { filter_rules: Some(vec![FilterRule { - name: Some(FilterRuleName::from("Prefix".to_string())), + name: Some(FilterRuleName::from("unsupported".to_string())), value: Some("uploads/".to_string()), }]), }), diff --git a/rustfs/src/storage/ecfs_extend.rs b/rustfs/src/storage/ecfs_extend.rs index 05df4ef2a..63ae8cb6a 100644 --- a/rustfs/src/storage/ecfs_extend.rs +++ b/rustfs/src/storage/ecfs_extend.rs @@ -624,10 +624,10 @@ pub(crate) fn extract_prefix_suffix(filter: Option<&NotificationConfigurationFil if let Some(rules) = &filter_rules.filter_rules { for rule in rules { if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) { - match name.as_str() { - "prefix" => prefix = value.clone(), - "suffix" => suffix = value.clone(), - _ => {} + if name.as_str().eq_ignore_ascii_case("prefix") { + prefix = value.clone(); + } else if name.as_str().eq_ignore_ascii_case("suffix") { + suffix = value.clone(); } } } diff --git a/rustfs/src/storage/ecfs_test.rs b/rustfs/src/storage/ecfs_test.rs index c9b3f6e76..10c2718cd 100644 --- a/rustfs/src/storage/ecfs_test.rs +++ b/rustfs/src/storage/ecfs_test.rs @@ -36,12 +36,12 @@ mod tests { }; use rustfs_zip::CompressionFormat; use s3s::dto::{ - CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, GetBucketAclInput, GetObjectAclInput, - GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration, - ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, - ObjectLockRetentionMode, ObjectLockRule, PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput, - PutObjectLockConfigurationInput, PutObjectRetentionInput, PutObjectTaggingInput, QueueConfiguration, Tag, Tagging, - TopicConfiguration, + CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, FilterRule, FilterRuleName, + GetBucketAclInput, GetObjectAclInput, GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, + LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled, + ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, ObjectLockRule, + PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput, PutObjectLockConfigurationInput, PutObjectRetentionInput, + PutObjectTaggingInput, QueueConfiguration, S3KeyFilter, Tag, Tagging, TopicConfiguration, }; use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; @@ -1734,6 +1734,47 @@ mod tests { assert_eq!(event_rules.len(), 1, "Should add one rule"); } + #[test] + fn test_process_queue_configurations_accepts_capitalized_filter_names() { + use rustfs_targets::arn::{ARN, TargetIDError}; + + let mut event_rules = Vec::new(); + let valid_arn = "arn:rustfs:sqs:us-east-1:1:webhook"; + + let result = process_queue_configurations( + &mut event_rules, + Some(vec![QueueConfiguration { + events: vec!["s3:ObjectCreated:*".to_string().into()], + queue_arn: valid_arn.to_string(), + filter: Some(NotificationConfigurationFilter { + key: Some(S3KeyFilter { + filter_rules: Some(vec![ + FilterRule { + name: Some(FilterRuleName::from("Prefix".to_string())), + value: Some("uploads/".to_string()), + }, + FilterRule { + name: Some(FilterRuleName::from("Suffix".to_string())), + value: Some(".csv".to_string()), + }, + ]), + }), + }), + id: None, + }]), + |arn_str| { + ARN::parse(arn_str) + .map(|arn| arn.target_id) + .map_err(|e| TargetIDError::InvalidFormat(e.to_string())) + }, + ); + + assert!(result.is_ok(), "capitalized filter names should be compatible"); + assert_eq!(event_rules.len(), 1, "Should add one rule"); + assert_eq!(event_rules[0].1, "uploads/"); + assert_eq!(event_rules[0].2, ".csv"); + } + // --- Object tag conditions for bucket policy (s3:ExistingObjectTag) --- /// Verifies that object tags are formatted as ExistingObjectTag/ condition keys