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