diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca5f1104..f73d6156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,8 +112,8 @@ jobs: include: - arch: x86_64 runner: ubicloud-standard-4 - - arch: aarch64 - runner: ubicloud-standard-4-arm + # - arch: aarch64 + # runner: ubicloud-standard-4-arm steps: - name: Checkout repository uses: actions/checkout@v6 @@ -152,14 +152,14 @@ jobs: include: - arch: x86_64 runner: ubicloud-standard-4 - - arch: aarch64 - runner: ubicloud-standard-4-arm + # - arch: aarch64 + # runner: ubicloud-standard-4-arm steps: - name: Checkout repository uses: actions/checkout@v6 - name: Clean up previous test run - run: |matrix.arch }}-${{ + run: | rm -rf /tmp/rustfs rm -f /tmp/rustfs.log @@ -185,7 +185,7 @@ jobs: cargo build -p rustfs --bins --jobs 4 - name: Run end-to-end tests - run: |matrix.arch }}-${{ + run: | s3s-e2e --version ./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs diff --git a/crates/targets/src/store.rs b/crates/targets/src/store.rs index 139e32be..be15a9b4 100644 --- a/crates/targets/src/store.rs +++ b/crates/targets/src/store.rs @@ -312,7 +312,7 @@ where compress: true, }; - let data = serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?; + let data = serde_json::to_vec(&*item).map_err(|e| StoreError::Serialization(e.to_string()))?; self.write_file(&key, &data)?; Ok(key) diff --git a/crates/targets/src/target/mod.rs b/crates/targets/src/target/mod.rs index 627fa8d0..876f186b 100644 --- a/crates/targets/src/target/mod.rs +++ b/crates/targets/src/target/mod.rs @@ -159,3 +159,30 @@ impl std::fmt::Display for TargetType { } } } + +/// Decodes a form-urlencoded object name to its original form. +/// +/// This function properly handles form-urlencoded strings where spaces are +/// represented as `+` symbols. It first replaces `+` with spaces, then +/// performs standard percent-decoding. +/// +/// # Arguments +/// * `encoded` - The form-urlencoded string to decode +/// +/// # Returns +/// The decoded string, or an error if decoding fails +/// +/// # Example +/// ``` +/// use rustfs_targets::target::decode_object_name; +/// +/// let encoded = "greeting+file+%282%29.csv"; +/// let decoded = decode_object_name(encoded).unwrap(); +/// assert_eq!(decoded, "greeting file (2).csv"); +/// ``` +pub fn decode_object_name(encoded: &str) -> Result { + let replaced = encoded.replace("+", " "); + urlencoding::decode(&replaced) + .map(|s| s.into_owned()) + .map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}"))) +} diff --git a/crates/targets/src/target/mqtt.rs b/crates/targets/src/target/mqtt.rs index 61cb93c0..9de8ac94 100644 --- a/crates/targets/src/target/mqtt.rs +++ b/crates/targets/src/target/mqtt.rs @@ -32,7 +32,6 @@ use std::{ use tokio::sync::{Mutex, OnceCell, mpsc}; use tracing::{debug, error, info, instrument, trace, warn}; use url::Url; -use urlencoding; const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15); const EVENT_LOOP_POLL_TIMEOUT: Duration = Duration::from_secs(10); // For initial connection check in task @@ -258,8 +257,8 @@ where .as_ref() .ok_or_else(|| TargetError::Configuration("MQTT client not initialized".to_string()))?; - let object_name = urlencoding::decode(&event.object_name) - .map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))?; + // Decode form-urlencoded object name + let object_name = crate::target::decode_object_name(&event.object_name)?; let key = format!("{}/{}", event.bucket_name, object_name); diff --git a/crates/targets/src/target/webhook.rs b/crates/targets/src/target/webhook.rs index c9564274..5c505e3b 100644 --- a/crates/targets/src/target/webhook.rs +++ b/crates/targets/src/target/webhook.rs @@ -36,7 +36,6 @@ use std::{ use tokio::net::lookup_host; use tokio::sync::mpsc; use tracing::{debug, error, info, instrument}; -use urlencoding; /// Arguments for configuring a Webhook target #[derive(Debug, Clone)] @@ -221,8 +220,8 @@ where async fn send(&self, event: &EntityTarget) -> Result<(), TargetError> { info!("Webhook Sending event to webhook target: {}", self.id); - let object_name = urlencoding::decode(&event.object_name) - .map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))?; + // Decode form-urlencoded object name + let object_name = crate::target::decode_object_name(&event.object_name)?; let key = format!("{}/{}", event.bucket_name, object_name); @@ -421,3 +420,51 @@ where self.args.enable } } + +#[cfg(test)] +mod tests { + use crate::target::decode_object_name; + use url::form_urlencoded; + + #[test] + fn test_decode_object_name_with_spaces() { + // Test case from the issue: "greeting file (2).csv" + let object_name = "greeting file (2).csv"; + + // Simulate what event.rs does: form-urlencoded encoding (spaces become +) + let form_encoded = form_urlencoded::byte_serialize(object_name.as_bytes()).collect::(); + assert_eq!(form_encoded, "greeting+file+%282%29.csv"); + + // Test the decode_object_name helper function + let decoded = decode_object_name(&form_encoded).unwrap(); + assert_eq!(decoded, object_name); + assert!(!decoded.contains('+'), "Decoded string should not contain + symbols"); + } + + #[test] + fn test_decode_object_name_with_special_chars() { + // Test with various special characters + let test_cases = vec![ + ("folder/greeting file (2).csv", "folder%2Fgreeting+file+%282%29.csv"), + ("test file.txt", "test+file.txt"), + ("my file (copy).pdf", "my+file+%28copy%29.pdf"), + ("file with spaces and (parentheses).doc", "file+with+spaces+and+%28parentheses%29.doc"), + ]; + + for (original, form_encoded) in test_cases { + // Test the decode_object_name helper function + let decoded = decode_object_name(form_encoded).unwrap(); + assert_eq!(decoded, original, "Failed to decode: {}", form_encoded); + } + } + + #[test] + fn test_decode_object_name_without_spaces() { + // Test that files without spaces still work correctly + let object_name = "simple-file.txt"; + let form_encoded = form_urlencoded::byte_serialize(object_name.as_bytes()).collect::(); + + let decoded = decode_object_name(&form_encoded).unwrap(); + assert_eq!(decoded, object_name); + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 5a0bd187..9b05e84e 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -84,7 +84,7 @@ tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls charac net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:thiserror", "dep:tokio"] # network features with DNS resolver io = ["dep:tokio"] path = [] -notify = ["dep:hyper", "dep:s3s", "dep:hashbrown", "dep:thiserror", "dep:serde", "dep:libc"] # file system notification features +notify = ["dep:hyper", "dep:s3s", "dep:hashbrown", "dep:thiserror", "dep:serde", "dep:libc", "dep:url", "dep:regex"] # file system notification features compress = ["dep:flate2", "dep:brotli", "dep:snap", "dep:lz4", "dep:zstd"] string = ["dep:regex", "dep:rand"] crypto = ["dep:base64-simd", "dep:hex-simd", "dep:hmac", "dep:hyper", "dep:sha1"]