diff --git a/crates/ecstore/src/pools.rs b/crates/ecstore/src/pools.rs index 02bd94632..21ca3372b 100644 --- a/crates/ecstore/src/pools.rs +++ b/crates/ecstore/src/pools.rs @@ -52,7 +52,7 @@ use rmp_serde::Serializer; use rustfs_common::defer; use rustfs_common::heal_channel::HealOpts; use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams}; -use rustfs_utils::path::{SLASH_SEPARATOR, encode_dir_object, path_join}; +use rustfs_utils::path::{encode_dir_object, path_join, path_to_bucket_object, path_to_bucket_object_with_base_path}; use rustfs_workers::workers::Workers; use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration}; use serde::{Deserialize, Serialize}; @@ -986,25 +986,11 @@ impl PoolMeta { } pub fn path2_bucket_object(name: &str) -> (String, String) { - path2_bucket_object_with_base_path("", name) + path_to_bucket_object(name) } pub fn path2_bucket_object_with_base_path(base_path: &str, path: &str) -> (String, String) { - // Trim the base path and leading slash - let trimmed_path = path - .strip_prefix(base_path) - .unwrap_or(path) - .strip_prefix(SLASH_SEPARATOR) - .unwrap_or(path); - // Find the position of the first '/' - let Some(pos) = trimmed_path.find(SLASH_SEPARATOR) else { - return (trimmed_path.to_string(), "".to_string()); - }; - // Split into bucket and prefix - let bucket = &trimmed_path[0..pos]; - let prefix = &trimmed_path[pos + 1..]; // +1 to skip the '/' character if it exists - - (bucket.to_string(), prefix.to_string()) + path_to_bucket_object_with_base_path(base_path, path) } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -3746,4 +3732,13 @@ mod pools_tests { .contains("failed to start decommission routines: scheduled 1 of 2 expected workers") ); } + + #[test] + #[cfg(windows)] + fn test_path2_bucket_object_with_base_path_supports_windows_separators() { + let (bucket, object) = super::path2_bucket_object_with_base_path("C:\\data", "C:\\data\\my-bucket\\nested\\object.txt"); + + assert_eq!(bucket, "my-bucket"); + assert_eq!(object, "nested/object.txt"); + } } diff --git a/crates/scanner/src/scanner_io.rs b/crates/scanner/src/scanner_io.rs index fb8401c33..76816ed7b 100644 --- a/crates/scanner/src/scanner_io.rs +++ b/crates/scanner/src/scanner_io.rs @@ -40,9 +40,10 @@ use rustfs_ecstore::set_disk::SetDisks; use rustfs_ecstore::store_api::{BucketInfo, BucketOperations, BucketOptions, ObjectInfo}; use rustfs_ecstore::{StorageAPI, error::Result, store::ECStore}; use rustfs_filemeta::FileMeta; -use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; +use rustfs_utils::path::path_join_buf; use s3s::dto::{BucketLifecycleConfiguration, ReplicationConfiguration}; use std::collections::HashMap; +use std::path::Path; use std::time::SystemTime; use std::{fmt::Debug, sync::Arc}; use time::OffsetDateTime; @@ -69,6 +70,13 @@ fn finalize_nsscanner_result(results: &[DataUsageCache], first_err: Option bool { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name == STORAGE_FORMAT_FILE) +} + async fn persist_and_publish_cache_snapshot( store: Arc, updates: &mpsc::Sender, @@ -540,7 +548,7 @@ impl ScannerIODisk for Disk { async fn get_size(&self, mut item: ScannerItem) -> Result { let done_object = Metrics::time(Metric::ScanObject); - if !item.path.ends_with(&format!("{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}")) { + if !is_xl_meta_path(&item.path) { return Err(StorageError::other("skip file".to_string())); } @@ -741,4 +749,15 @@ mod tests { .expect_err("all failed sets should bubble first error"); assert!(err.to_string().contains("set failed")); } + + #[test] + #[cfg(windows)] + fn is_xl_meta_path_accepts_windows_separator() { + assert!(is_xl_meta_path("D:\\data\\bucket\\object\\xl.meta")); + } + + #[test] + fn is_xl_meta_path_accepts_forward_separator() { + assert!(is_xl_meta_path("/data/bucket/object/xl.meta")); + } } diff --git a/crates/utils/src/path.rs b/crates/utils/src/path.rs index f5e3906ea..e3919f89a 100644 --- a/crates/utils/src/path.rs +++ b/crates/utils/src/path.rs @@ -90,7 +90,12 @@ pub fn retain_slash(s: &str) -> String { /// Checks if string `s` starts with `prefix` using case-insensitive comparison. pub fn strings_has_prefix_fold(s: &str, prefix: &str) -> bool { - s.len() >= prefix.len() && (s[..prefix.len()] == *prefix || s[..prefix.len()].eq_ignore_ascii_case(prefix)) + if s.starts_with(prefix) { + return true; + } + + s.get(..prefix.len()) + .is_some_and(|s_prefix| s_prefix.eq_ignore_ascii_case(prefix)) } /// Checks if string `s` starts with `prefix`. @@ -875,4 +880,16 @@ mod tests { assert_eq!(bucket, "bucket"); assert_eq!(object, "object"); } + + #[test] + #[cfg(target_os = "windows")] + fn test_path_to_bucket_object_with_base_path_handles_unicode_without_panicking() { + let (bucket, object) = path_to_bucket_object_with_base_path( + "D:\\Github\\rustfs\\target\\volumes\\test1", + "s3-test-bucket/中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f", + ); + + assert_eq!(bucket, "s3-test-bucket"); + assert_eq!(object, "中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f"); + } } diff --git a/crates/utils/src/string.rs b/crates/utils/src/string.rs index 79ef5746e..c6589bf12 100644 --- a/crates/utils/src/string.rs +++ b/crates/utils/src/string.rs @@ -506,13 +506,12 @@ pub fn parse_ellipses_range(pattern: &str) -> Result> { /// ``` /// pub fn strings_has_prefix_fold(s: &str, prefix: &str) -> bool { - if s.len() < prefix.len() { - return false; + if s.starts_with(prefix) { + return true; } - let s_prefix = &s[..prefix.len()]; - // Test match with case first, then case-insensitive - s_prefix == prefix || s_prefix.to_lowercase() == prefix.to_lowercase() + s.get(..prefix.len()) + .is_some_and(|s_prefix| s_prefix.eq_ignore_ascii_case(prefix)) } #[cfg(test)] @@ -872,4 +871,13 @@ mod tests { } } } + + #[test] + #[cfg(target_os = "windows")] + fn test_strings_has_prefix_fold_handles_unicode_without_panicking() { + assert!(!strings_has_prefix_fold( + "s3-test-bucket/中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f", + "D:\\Github\\rustfs\\target\\volumes\\test1", + )); + } }