fix: decode form-urlencoded object names in webhook/mqtt Key field (#1210)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Copilot
2025-12-20 01:31:09 +08:00
committed by GitHub
parent 8e0aeb4fdc
commit 8dd3e8b534
6 changed files with 87 additions and 14 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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<String, TargetError> {
let replaced = encoded.replace("+", " ");
urlencoding::decode(&replaced)
.map(|s| s.into_owned())
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))
}

View File

@@ -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);

View File

@@ -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<E>) -> 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::<String>();
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::<String>();
let decoded = decode_object_name(&form_encoded).unwrap();
assert_eq!(decoded, object_name);
}
}

View File

@@ -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"]