diff --git a/Cargo.lock b/Cargo.lock index eba65b8a0..77a278528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10182,6 +10182,7 @@ dependencies = [ "lz4", "md-5 0.11.0", "netif", + "proptest", "regex", "rustix 1.1.4", "serde", diff --git a/crates/ecstore/src/bucket/utils.rs b/crates/ecstore/src/bucket/utils.rs index c165a12e6..dced6a0a1 100644 --- a/crates/ecstore/src/bucket/utils.rs +++ b/crates/ecstore/src/bucket/utils.rs @@ -353,6 +353,7 @@ pub fn check_put_object_args(bucket: &str, object: &str) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; // Test validation functions #[test] @@ -526,4 +527,28 @@ mod tests { assert!(check_put_object_args("", "test-object").is_err()); assert!(check_put_object_args("test-bucket", "").is_err()); } + + proptest! { + #[test] + fn valid_object_prefixes_preserve_prefix_invariants(input in any::()) { + if is_valid_object_prefix(&input) { + prop_assert!(!has_bad_path_component(&input)); + prop_assert!(!input.contains("//")); + prop_assert!(!input.contains('\0')); + } + } + + #[test] + fn valid_object_names_preserve_name_invariants(input in any::()) { + if is_valid_object_name(&input) { + prop_assert!(!input.is_empty()); + prop_assert!(is_valid_object_prefix(&input)); + } + } + + #[test] + fn object_name_validity_matches_prefix_validity_plus_non_empty(input in any::()) { + prop_assert_eq!(is_valid_object_name(&input), !input.is_empty() && is_valid_object_prefix(&input)); + } + } } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 2257c5d06..0d06966a5 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -60,6 +60,7 @@ zstd = { workspace = true, optional = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } temp-env = { workspace = true } +proptest = "1" [target.'cfg(windows)'.dependencies] windows = { workspace = true, optional = true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } diff --git a/crates/utils/src/path.rs b/crates/utils/src/path.rs index e3919f89a..bd6df8530 100644 --- a/crates/utils/src/path.rs +++ b/crates/utils/src/path.rs @@ -553,6 +553,7 @@ pub fn trim_etag(etag: &str) -> String { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; #[test] fn test_trim_etag() { @@ -892,4 +893,29 @@ mod tests { assert_eq!(bucket, "s3-test-bucket"); assert_eq!(object, "中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f"); } + + proptest! { + #[test] + fn clean_is_idempotent(input in any::()) { + let once = clean(&input); + let twice = clean(&once); + prop_assert_eq!(twice, once); + } + + #[test] + fn clean_output_has_no_redundant_current_dir_or_empty_segments(input in any::()) { + let cleaned = clean(&input); + + prop_assert!(!cleaned.contains("//"), "clean output should not contain doubled separators"); + prop_assert!(!cleaned.contains("/./"), "clean output should not contain embedded current-dir segments"); + prop_assert!(!cleaned.ends_with("/."), "clean output should not end with a current-dir segment"); + + if cleaned != "/" { + prop_assert!( + !cleaned.ends_with('/') || cleaned.is_empty(), + "clean output should not preserve a trailing slash" + ); + } + } + } }