diff --git a/Cargo.lock b/Cargo.lock index 5790c8ed6..eca17e871 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,11 +224,11 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "ar_archive_writer" -version = "0.2.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object 0.32.2", + "object", ] [[package]] @@ -687,9 +687,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.3" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -698,9 +698,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" dependencies = [ "cc", "cmake", @@ -1156,7 +1156,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link", ] @@ -1453,9 +1453,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "jobserver", @@ -3169,7 +3169,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3481,7 +3481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4466,9 +4466,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +checksum = "b41fb3dc24fe72c2e3a4685eed55917c2fb228851257f4a8f2d985da9443c3e5" dependencies = [ "typenum", "zeroize", @@ -4859,7 +4859,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4927,7 +4927,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5201,9 +5201,9 @@ dependencies = [ [[package]] name = "liblzma-sys" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186" dependencies = [ "cc", "libc", @@ -5212,9 +5212,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" @@ -5557,9 +5557,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ "async-lock", "crossbeam-channel", @@ -5707,9 +5707,12 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.10.0", +] [[package]] name = "ntapi" @@ -5726,7 +5729,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5903,15 +5906,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "object" version = "0.37.3" @@ -5980,9 +5974,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" @@ -6536,9 +6530,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.8" +version = "0.11.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77089aec8290d0b7bb01b671b091095cf1937670725af4fd73d47249f03b12c0" +checksum = "b226d2cc389763951db8869584fd800cbbe2962bf454e2edeb5172b31ee99774" dependencies = [ "der 0.8.0-rc.10", "spki 0.8.0-rc.4", @@ -6661,9 +6655,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" [[package]] name = "pprof" @@ -6766,9 +6760,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -6926,9 +6920,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "1fa96cb91275ed31d6da3e983447320c4eb219ac180fa1679a0889ff32861e2d" dependencies = [ "ar_archive_writer", "cc", @@ -7045,14 +7039,14 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -7452,7 +7446,7 @@ dependencies = [ "crypto-primes", "digest 0.11.0-rc.5", "pkcs1", - "pkcs8 0.11.0-rc.8", + "pkcs8 0.11.0-rc.10", "rand_core 0.10.0-rc-3", "sha2 0.11.0-rc.3", "signature 3.0.0-rc.6", @@ -7888,7 +7882,6 @@ dependencies = [ "bytesize", "chrono", "criterion", - "dunce", "enumset", "faster-hex", "flatbuffers", @@ -8447,7 +8440,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8709,9 +8702,9 @@ dependencies = [ [[package]] name = "sec1" -version = "0.8.0-rc.11" +version = "0.8.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2568531a8ace88b848310caa98fb2115b151ef924d54aa523e659c21b9d32d71" +checksum = "7a2400ed44a13193820aa528a19f376c3843141a8ce96ff34b11104cc79763f2" dependencies = [ "base16ct 1.0.0", "hybrid-array", @@ -9333,7 +9326,7 @@ dependencies = [ "ed25519-dalek 3.0.0-pre.4", "rand_core 0.10.0-rc-3", "rsa", - "sec1 0.8.0-rc.11", + "sec1 0.8.0-rc.13", "sha2 0.11.0-rc.3", "signature 3.0.0-rc.6", "ssh-cipher 0.3.0-rc.5", @@ -9679,7 +9672,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10300,9 +10293,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "tz-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14eff19b8dc1ace5bf7e4d920b2628ae3837f422ff42210cb1567cbf68b5accf" +checksum = "8b2df019c305771deac375d9657fc75fa394bde69102a870c923e779c3746642" [[package]] name = "tzdb" @@ -10681,7 +10674,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -11103,18 +11096,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" dependencies = [ "proc-macro2", "quote", @@ -11231,9 +11224,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 389aab699..76c5cb665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,7 +187,6 @@ criterion = { version = "0.8", features = ["html_reports"] } crossbeam-queue = "0.3.12" datafusion = "52.1.0" derive_builder = "0.20.2" -dunce = "1.0.5" enumset = "1.1.10" faster-hex = "0.10.0" flate2 = "1.1.8" @@ -209,7 +208,7 @@ matchit = "0.9.1" md-5 = "0.11.0-rc.3" md5 = "0.8.0" mime_guess = "2.0.5" -moka = { version = "0.12.12", features = ["future"] } +moka = { version = "0.12.13", features = ["future"] } netif = "0.1.6" nix = { version = "0.31.1", features = ["fs"] } nu-ansi-term = "0.50.3" diff --git a/crates/ecstore/Cargo.toml b/crates/ecstore/Cargo.toml index 5d6e2ff13..858f7f3c7 100644 --- a/crates/ecstore/Cargo.toml +++ b/crates/ecstore/Cargo.toml @@ -48,7 +48,6 @@ async-trait.workspace = true bytes.workspace = true byteorder = { workspace = true } chrono.workspace = true -dunce.workspace = true glob = { workspace = true } thiserror.workspace = true flatbuffers.workspace = true diff --git a/crates/ecstore/src/bucket/utils.rs b/crates/ecstore/src/bucket/utils.rs index ee7012a37..4721498b1 100644 --- a/crates/ecstore/src/bucket/utils.rs +++ b/crates/ecstore/src/bucket/utils.rs @@ -15,7 +15,7 @@ use crate::disk::RUSTFS_META_BUCKET; use crate::error::{Error, Result, StorageError}; use regex::Regex; -use rustfs_utils::path::SLASH_SEPARATOR_STR; +use rustfs_utils::path::SLASH_SEPARATOR; use s3s::xml; use tracing::instrument; @@ -193,7 +193,7 @@ pub fn is_valid_object_name(object: &str) -> bool { return false; } - if object.ends_with(SLASH_SEPARATOR_STR) { + if object.ends_with(SLASH_SEPARATOR) { return false; } @@ -205,7 +205,7 @@ pub fn check_object_name_for_length_and_slash(bucket: &str, object: &str) -> Res return Err(StorageError::ObjectNameTooLong(bucket.to_owned(), object.to_owned())); } - if object.starts_with(SLASH_SEPARATOR_STR) { + if object.starts_with(SLASH_SEPARATOR) { return Err(StorageError::ObjectNamePrefixAsSlash(bucket.to_owned(), object.to_owned())); } diff --git a/crates/ecstore/src/config/com.rs b/crates/ecstore/src/config/com.rs index 3ad8256ab..b1010bf0f 100644 --- a/crates/ecstore/src/config/com.rs +++ b/crates/ecstore/src/config/com.rs @@ -18,7 +18,7 @@ use crate::error::{Error, Result}; use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI}; use http::HeaderMap; use rustfs_config::DEFAULT_DELIMITER; -use rustfs_utils::path::SLASH_SEPARATOR_STR; +use rustfs_utils::path::SLASH_SEPARATOR; use std::collections::HashSet; use std::sync::Arc; use std::sync::LazyLock; @@ -29,7 +29,7 @@ const CONFIG_FILE: &str = "config.json"; pub const STORAGE_CLASS_SUB_SYS: &str = "storage_class"; -static CONFIG_BUCKET: LazyLock = LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR_STR}{CONFIG_PREFIX}")); +static CONFIG_BUCKET: LazyLock = LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR}{CONFIG_PREFIX}")); static SUB_SYSTEMS_DYNAMIC: LazyLock> = LazyLock::new(|| { let mut h = HashSet::new(); @@ -129,7 +129,7 @@ async fn new_and_save_server_config(api: Arc) -> Result String { - format!("{CONFIG_PREFIX}{SLASH_SEPARATOR_STR}{CONFIG_FILE}") + format!("{CONFIG_PREFIX}{SLASH_SEPARATOR}{CONFIG_FILE}") } /// Handle the situation where the configuration file does not exist, create and save a new configuration diff --git a/crates/ecstore/src/data_usage.rs b/crates/ecstore/src/data_usage.rs index e38188a2b..57b9650b9 100644 --- a/crates/ecstore/src/data_usage.rs +++ b/crates/ecstore/src/data_usage.rs @@ -26,7 +26,7 @@ pub use local_snapshot::{ use rustfs_common::data_usage::{ BucketTargetUsageInfo, BucketUsageInfo, DataUsageCache, DataUsageEntry, DataUsageInfo, DiskUsageStatus, SizeSummary, }; -use rustfs_utils::path::SLASH_SEPARATOR_STR; +use rustfs_utils::path::SLASH_SEPARATOR; use std::{ collections::{HashMap, hash_map::Entry}, sync::{Arc, OnceLock}, @@ -37,7 +37,7 @@ use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; // Data usage storage constants -pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR_STR; +pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR; const DATA_USAGE_OBJ_NAME: &str = ".usage.json"; const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin"; pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin"; @@ -61,17 +61,17 @@ fn cache_updating() -> &'static CacheUpdating { lazy_static::lazy_static! { pub static ref DATA_USAGE_BUCKET: String = format!("{}{}{}", crate::disk::RUSTFS_META_BUCKET, - SLASH_SEPARATOR_STR, + SLASH_SEPARATOR, crate::disk::BUCKET_META_PREFIX ); pub static ref DATA_USAGE_OBJ_NAME_PATH: String = format!("{}{}{}", crate::disk::BUCKET_META_PREFIX, - SLASH_SEPARATOR_STR, + SLASH_SEPARATOR, DATA_USAGE_OBJ_NAME ); pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = format!("{}{}{}", crate::disk::BUCKET_META_PREFIX, - SLASH_SEPARATOR_STR, + SLASH_SEPARATOR, DATA_USAGE_BLOOM_NAME ); } diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index eb5ed090c..978b0c679 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -40,8 +40,8 @@ use rustfs_filemeta::{ use rustfs_utils::HashAlgorithm; use rustfs_utils::os::get_info; use rustfs_utils::path::{ - GLOBAL_DIR_SUFFIX, GLOBAL_DIR_SUFFIX_WITH_SLASH, SLASH_SEPARATOR_STR, clean, decode_dir_object, encode_dir_object, - has_suffix, path_join, path_join_buf, + GLOBAL_DIR_SUFFIX, GLOBAL_DIR_SUFFIX_WITH_SLASH, SLASH_SEPARATOR, clean, decode_dir_object, encode_dir_object, has_suffix, + path_join, path_join_buf, }; use std::collections::HashMap; use std::collections::HashSet; @@ -122,7 +122,7 @@ impl LocalDisk { debug!("Creating local disk"); // Use optimized path resolution instead of absolutize() for better performance // Use dunce::canonicalize instead of std::fs::canonicalize to avoid UNC paths on Windows - let root = match dunce::canonicalize(ep.get_file_path()) { + let root = match rustfs_utils::canonicalize(ep.get_file_path()) { Ok(path) => path, Err(e) => { if e.kind() == ErrorKind::NotFound { @@ -269,8 +269,22 @@ impl LocalDisk { } async fn cleanup_deleted_objects(root: PathBuf) -> Result<()> { - let trash = path_join(&[root, RUSTFS_META_TMP_DELETED_BUCKET.into()]); - let mut entries = fs::read_dir(&trash).await?; + #[cfg(windows)] + let trash_path = RUSTFS_META_TMP_DELETED_BUCKET.replace('/', "\\"); + #[cfg(not(windows))] + let trash_path = RUSTFS_META_TMP_DELETED_BUCKET.to_string(); + + let trash = root.join(trash_path); + let mut entries = match fs::read_dir(&trash).await { + Ok(entries) => entries, + Err(e) => { + if e.kind() == ErrorKind::NotFound { + return Ok(()); + } + return Err(e.into()); + } + }; + while let Some(entry) = entries.next_entry().await? { let name = entry.file_name().to_string_lossy().to_string(); if name.is_empty() || name == "." || name == ".." { @@ -279,7 +293,7 @@ impl LocalDisk { let file_type = entry.file_type().await?; - let path = path_join(&[trash.clone(), name.into()]); + let path = trash.join(name); if file_type.is_dir() { if let Err(e) = tokio::fs::remove_dir_all(path).await @@ -362,7 +376,14 @@ impl LocalDisk { let abs_path = if path_ref.is_absolute() { path_ref.to_path_buf() } else { - self.root.join(path_ref) + #[cfg(windows)] + { + self.root.join(path_str.replace('/', "\\")) + } + #[cfg(not(windows))] + { + self.root.join(path_ref) + } }; // Normalize path components to avoid filesystem calls @@ -396,14 +417,22 @@ impl LocalDisk { path_join_buf(&[bucket, key]) }; + #[cfg(windows)] + let path = self.root.join(cache_key.replace('/', "\\")); + #[cfg(not(windows))] let path = self.root.join(cache_key); + self.check_valid_path(&path)?; Ok(path) } // Get the absolute path of a bucket pub fn get_bucket_path(&self, bucket: &str) -> Result { + #[cfg(windows)] + let bucket_path = self.root.join(bucket.replace('/', "\\")); + #[cfg(not(windows))] let bucket_path = self.root.join(bucket); + self.check_valid_path(&bucket_path)?; Ok(bucket_path) } @@ -440,7 +469,11 @@ impl LocalDisk { if !cache_misses.is_empty() { let mut new_entries = Vec::new(); for (i, _bucket, _key, cache_key) in cache_misses { + #[cfg(windows)] + let path = self.root.join(cache_key.replace('/', "\\")); + #[cfg(not(windows))] let path = self.root.join(&cache_key); + results.push((i, path.clone())); new_entries.push((cache_key, path)); } @@ -555,7 +588,7 @@ impl LocalDisk { .err() }; - if immediate_purge || delete_path.to_string_lossy().ends_with(SLASH_SEPARATOR_STR) { + if immediate_purge || delete_path.to_string_lossy().ends_with(SLASH_SEPARATOR) { let trash_path2 = self.get_object_path(RUSTFS_META_TMP_DELETED_BUCKET, Uuid::new_v4().to_string().as_str())?; let _ = rename_all( encode_dir_object(delete_path.to_string_lossy().as_ref()), @@ -1022,16 +1055,15 @@ impl LocalDisk { continue; } - if entry.ends_with(SLASH_SEPARATOR_STR) { + if entry.ends_with(SLASH_SEPARATOR) { if entry.ends_with(GLOBAL_DIR_SUFFIX_WITH_SLASH) { - let entry = - format!("{}{}", entry.as_str().trim_end_matches(GLOBAL_DIR_SUFFIX_WITH_SLASH), SLASH_SEPARATOR_STR); + let entry = format!("{}{}", entry.as_str().trim_end_matches(GLOBAL_DIR_SUFFIX_WITH_SLASH), SLASH_SEPARATOR); dir_objes.insert(entry.clone()); *item = entry; continue; } - *item = entry.trim_end_matches(SLASH_SEPARATOR_STR).to_owned(); + *item = entry.trim_end_matches(SLASH_SEPARATOR).to_owned(); continue; } @@ -1043,7 +1075,7 @@ impl LocalDisk { .await?; let entry = entry.strip_suffix(STORAGE_FORMAT_FILE).unwrap_or_default().to_owned(); - let name = entry.trim_end_matches(SLASH_SEPARATOR_STR); + let name = entry.trim_end_matches(SLASH_SEPARATOR); let name = decode_dir_object(format!("{}/{}", ¤t, &name).as_str()); // if opts.limit > 0 @@ -1126,7 +1158,7 @@ impl LocalDisk { Ok(res) => { if is_dir_obj { meta.name = meta.name.trim_end_matches(GLOBAL_DIR_SUFFIX_WITH_SLASH).to_owned(); - meta.name.push_str(SLASH_SEPARATOR_STR); + meta.name.push_str(SLASH_SEPARATOR); } meta.metadata = res.to_vec(); @@ -1144,7 +1176,7 @@ impl LocalDisk { // NOT an object, append to stack (with slash) // If dirObject, but no metadata (which is unexpected) we skip it. if !is_dir_obj && !is_empty_dir(self.get_object_path(&opts.bucket, &meta.name)?).await { - meta.name.push_str(SLASH_SEPARATOR_STR); + meta.name.push_str(SLASH_SEPARATOR); dir_stack.push(meta.name); } } @@ -1625,8 +1657,8 @@ impl DiskAPI for LocalDisk { super::fs::access_std(&dst_volume_dir).map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))? } - let src_is_dir = has_suffix(src_path, SLASH_SEPARATOR_STR); - let dst_is_dir = has_suffix(dst_path, SLASH_SEPARATOR_STR); + let src_is_dir = has_suffix(src_path, SLASH_SEPARATOR); + let dst_is_dir = has_suffix(dst_path, SLASH_SEPARATOR); if !src_is_dir && dst_is_dir || src_is_dir && !dst_is_dir { warn!( @@ -1692,8 +1724,8 @@ impl DiskAPI for LocalDisk { .map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))?; } - let src_is_dir = has_suffix(src_path, SLASH_SEPARATOR_STR); - let dst_is_dir = has_suffix(dst_path, SLASH_SEPARATOR_STR); + let src_is_dir = has_suffix(src_path, SLASH_SEPARATOR); + let dst_is_dir = has_suffix(dst_path, SLASH_SEPARATOR); if (dst_is_dir || src_is_dir) && (!dst_is_dir || !src_is_dir) { return Err(Error::from(DiskError::FileAccessDenied)); } @@ -1844,7 +1876,7 @@ impl DiskAPI for LocalDisk { } let volume_dir = self.get_bucket_path(volume)?; - let dir_path_abs = self.get_object_path(volume, dir_path.trim_start_matches(SLASH_SEPARATOR_STR))?; + let dir_path_abs = self.get_object_path(volume, dir_path.trim_start_matches(SLASH_SEPARATOR))?; let entries = match os::read_dir(&dir_path_abs, count).await { Ok(res) => res, @@ -1880,12 +1912,12 @@ impl DiskAPI for LocalDisk { let mut objs_returned = 0; - if opts.base_dir.ends_with(SLASH_SEPARATOR_STR) { + if opts.base_dir.ends_with(SLASH_SEPARATOR) { if let Ok(data) = self .read_metadata( &opts.bucket, path_join_buf(&[ - format!("{}{}", opts.base_dir.trim_end_matches(SLASH_SEPARATOR_STR), GLOBAL_DIR_SUFFIX).as_str(), + format!("{}{}", opts.base_dir.trim_end_matches(SLASH_SEPARATOR), GLOBAL_DIR_SUFFIX).as_str(), STORAGE_FORMAT_FILE, ]) .as_str(), @@ -2135,7 +2167,7 @@ impl DiskAPI for LocalDisk { let entries = os::read_dir(&self.root, -1).await.map_err(to_volume_error)?; for entry in entries { - if !has_suffix(&entry, SLASH_SEPARATOR_STR) || !Self::is_valid_volname(clean(&entry).as_str()) { + if !has_suffix(&entry, SLASH_SEPARATOR) || !Self::is_valid_volname(clean(&entry).as_str()) { continue; } @@ -2357,7 +2389,7 @@ impl DiskAPI for LocalDisk { force_del_marker: bool, opts: DeleteOptions, ) -> Result<()> { - if path.starts_with(SLASH_SEPARATOR_STR) { + if path.starts_with(SLASH_SEPARATOR) { return self .delete( volume, @@ -2418,7 +2450,7 @@ impl DiskAPI for LocalDisk { if !meta.versions.is_empty() { let buf = meta.marshal_msg()?; return self - .write_all_meta(volume, format!("{path}{SLASH_SEPARATOR_STR}{STORAGE_FORMAT_FILE}").as_str(), &buf, true) + .write_all_meta(volume, format!("{path}{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}").as_str(), &buf, true) .await; } @@ -2428,11 +2460,11 @@ impl DiskAPI for LocalDisk { { let src_path = path_join(&[ file_path.as_path(), - Path::new(format!("{old_data_dir}{SLASH_SEPARATOR_STR}{STORAGE_FORMAT_FILE_BACKUP}").as_str()), + Path::new(format!("{old_data_dir}{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE_BACKUP}").as_str()), ]); let dst_path = path_join(&[ file_path.as_path(), - Path::new(format!("{path}{SLASH_SEPARATOR_STR}{STORAGE_FORMAT_FILE}").as_str()), + Path::new(format!("{path}{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}").as_str()), ]); return rename_all(src_path, dst_path, file_path).await; } @@ -2584,7 +2616,7 @@ async fn get_disk_info(drive_path: PathBuf) -> Result<(rustfs_utils::os::DiskInf if root_disk_threshold > 0 { disk_info.total <= root_disk_threshold } else { - is_root_disk(&drive_path, SLASH_SEPARATOR_STR).unwrap_or_default() + is_root_disk(&drive_path, SLASH_SEPARATOR).unwrap_or_default() } } else { false diff --git a/crates/ecstore/src/disk/os.rs b/crates/ecstore/src/disk/os.rs index 926779576..730f90e4d 100644 --- a/crates/ecstore/src/disk/os.rs +++ b/crates/ecstore/src/disk/os.rs @@ -15,7 +15,7 @@ use crate::disk::error::DiskError; use crate::disk::error::Result; use crate::disk::error_conv::to_file_error; -use rustfs_utils::path::SLASH_SEPARATOR_STR; +use rustfs_utils::path::SLASH_SEPARATOR; use std::{ io, path::{Component, Path}, @@ -116,7 +116,7 @@ pub async fn read_dir(path: impl AsRef, count: i32) -> std::io::Result (Strin let trimmed_path = path .strip_prefix(base_path) .unwrap_or(path) - .strip_prefix(SLASH_SEPARATOR_STR) + .strip_prefix(SLASH_SEPARATOR) .unwrap_or(path); // Find the position of the first '/' - let Some(pos) = trimmed_path.find(SLASH_SEPARATOR_STR) else { + let Some(pos) = trimmed_path.find(SLASH_SEPARATOR) else { return (trimmed_path.to_string(), "".to_string()); }; // Split into bucket and prefix diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 53e6e0b67..a35bd81e4 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -83,7 +83,7 @@ use rustfs_utils::http::headers::{AMZ_OBJECT_TAGGING, RESERVED_METADATA_PREFIX, use rustfs_utils::{ HashAlgorithm, crypto::hex, - path::{SLASH_SEPARATOR_STR, encode_dir_object, has_suffix, path_join_buf}, + path::{SLASH_SEPARATOR, encode_dir_object, has_suffix, path_join_buf}, }; use rustfs_workers::workers::Workers; use s3s::header::X_AMZ_RESTORE; @@ -5232,7 +5232,7 @@ impl StorageAPI for SetDisks { &upload_id_path, fi.data_dir.map(|v| v.to_string()).unwrap_or_default().as_str(), ]), - SLASH_SEPARATOR_STR + SLASH_SEPARATOR ); let mut part_numbers = match Self::list_parts(&online_disks, &part_path, read_quorum).await { @@ -5370,7 +5370,7 @@ impl StorageAPI for SetDisks { let mut populated_upload_ids = HashSet::new(); for upload_id in upload_ids.iter() { - let upload_id = upload_id.trim_end_matches(SLASH_SEPARATOR_STR).to_string(); + let upload_id = upload_id.trim_end_matches(SLASH_SEPARATOR).to_string(); if populated_upload_ids.contains(&upload_id) { continue; } @@ -6125,7 +6125,7 @@ impl StorageAPI for SetDisks { None }; - if has_suffix(object, SLASH_SEPARATOR_STR) { + if has_suffix(object, SLASH_SEPARATOR) { let (result, err) = self.heal_object_dir_locked(bucket, object, opts.dry_run, opts.remove).await?; return Ok((result, err.map(|e| e.into()))); } @@ -6672,6 +6672,16 @@ async fn get_disks_info(disks: &[Option], eps: &[Endpoint]) -> Vec], eps: &[Endpoint]) -> rustfs_madmin::StorageInfo { + // let mut disks = get_disks_info(disks, eps).await; + // disks.sort_by(|a, b| a.total_space.cmp(&b.total_space)); + // + // rustfs_madmin::StorageInfo { + // disks, + // backend: rustfs_madmin::BackendInfo { + // backend_type: rustfs_madmin::BackendByte::Erasure, + // ..Default::default() + // }, + // } let mut disks = get_disks_info(disks, eps).await; disks.sort_by(|a, b| a.total_space.cmp(&b.total_space)); diff --git a/crates/ecstore/src/store_list_objects.rs b/crates/ecstore/src/store_list_objects.rs index 6967027d3..0426c226f 100644 --- a/crates/ecstore/src/store_list_objects.rs +++ b/crates/ecstore/src/store_list_objects.rs @@ -34,7 +34,7 @@ use rustfs_filemeta::{ MetaCacheEntries, MetaCacheEntriesSorted, MetaCacheEntriesSortedResult, MetaCacheEntry, MetadataResolutionParams, merge_file_meta_versions, }; -use rustfs_utils::path::{self, SLASH_SEPARATOR_STR, base_dir_from_prefix}; +use rustfs_utils::path::{self, SLASH_SEPARATOR, base_dir_from_prefix}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::broadcast::{self}; @@ -132,7 +132,7 @@ impl ListPathOptions { return; } - let s = SLASH_SEPARATOR_STR.chars().next().unwrap_or_default(); + let s = SLASH_SEPARATOR.chars().next().unwrap_or_default(); self.filter_prefix = { let fp = self.prefix.trim_start_matches(&self.base_dir).trim_matches(s); @@ -350,7 +350,7 @@ impl ECStore { if let Some(delimiter) = &delimiter { if obj.is_dir && obj.mod_time.is_none() { let mut found = false; - if delimiter != SLASH_SEPARATOR_STR { + if delimiter != SLASH_SEPARATOR { for p in prefixes.iter() { if found { break; @@ -477,7 +477,7 @@ impl ECStore { if let Some(delimiter) = &delimiter { if obj.is_dir && obj.mod_time.is_none() { let mut found = false; - if delimiter != SLASH_SEPARATOR_STR { + if delimiter != SLASH_SEPARATOR { for p in prefixes.iter() { if found { break; @@ -509,7 +509,7 @@ impl ECStore { // warn!("list_path opt {:?}", &o); check_list_objs_args(&o.bucket, &o.prefix, &o.marker)?; - // if opts.prefix.ends_with(SLASH_SEPARATOR_STR) { + // if opts.prefix.ends_with(SLASH_SEPARATOR) { // return Err(Error::msg("eof")); // } @@ -527,11 +527,11 @@ impl ECStore { return Err(Error::Unexpected); } - if o.prefix.starts_with(SLASH_SEPARATOR_STR) { + if o.prefix.starts_with(SLASH_SEPARATOR) { return Err(Error::Unexpected); } - let slash_separator = Some(SLASH_SEPARATOR_STR.to_owned()); + let slash_separator = Some(SLASH_SEPARATOR.to_owned()); o.include_directories = o.separator == slash_separator; @@ -781,8 +781,8 @@ impl ECStore { let mut filter_prefix = { prefix .trim_start_matches(&path) - .trim_start_matches(SLASH_SEPARATOR_STR) - .trim_end_matches(SLASH_SEPARATOR_STR) + .trim_start_matches(SLASH_SEPARATOR) + .trim_end_matches(SLASH_SEPARATOR) .to_owned() }; @@ -1137,7 +1137,7 @@ async fn merge_entry_channels( if path::clean(&best_entry.name) == path::clean(&other_entry.name) { let dir_matches = best_entry.is_dir() && other_entry.is_dir(); let suffix_matches = - best_entry.name.ends_with(SLASH_SEPARATOR_STR) == other_entry.name.ends_with(SLASH_SEPARATOR_STR); + best_entry.name.ends_with(SLASH_SEPARATOR) == other_entry.name.ends_with(SLASH_SEPARATOR); if dir_matches && suffix_matches { to_merge.push(other_idx); diff --git a/crates/ecstore/src/tier/tier.rs b/crates/ecstore/src/tier/tier.rs index 37079e272..1d11ad9ec 100644 --- a/crates/ecstore/src/tier/tier.rs +++ b/crates/ecstore/src/tier/tier.rs @@ -51,7 +51,7 @@ use crate::{ store_api::{ObjectOptions, PutObjReader}, }; use rustfs_rio::HashReader; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join}; use s3s::S3ErrorCode; use super::{ @@ -403,7 +403,7 @@ impl TierConfigMgr { pub async fn save_tiering_config(&self, api: Arc) -> std::result::Result<(), std::io::Error> { let data = self.marshal()?; - let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR_STR, TIER_CONFIG_FILE); + let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, TIER_CONFIG_FILE); self.save_config(api, &config_file, data).await } @@ -483,7 +483,7 @@ async fn new_and_save_tiering_config(api: Arc) -> Result) -> std::result::Result { - let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR_STR, TIER_CONFIG_FILE); + let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, TIER_CONFIG_FILE); let data = read_config(api.clone(), config_file.as_str()).await; if let Err(err) = data { if is_err_config_not_found(&err) { diff --git a/crates/ecstore/src/tier/warm_backend_s3.rs b/crates/ecstore/src/tier/warm_backend_s3.rs index 854533119..f6a679e9b 100644 --- a/crates/ecstore/src/tier/warm_backend_s3.rs +++ b/crates/ecstore/src/tier/warm_backend_s3.rs @@ -34,7 +34,7 @@ use crate::tier::{ tier_config::TierS3, warm_backend::{WarmBackend, WarmBackendGetOpts}, }; -use rustfs_utils::path::SLASH_SEPARATOR_STR; +use rustfs_utils::path::SLASH_SEPARATOR; pub struct WarmBackendS3 { pub client: Arc, @@ -176,7 +176,7 @@ impl WarmBackend for WarmBackendS3 { async fn in_use(&self) -> Result { let result = self .core - .list_objects_v2(&self.bucket, &self.prefix, "", "", SLASH_SEPARATOR_STR, 1) + .list_objects_v2(&self.bucket, &self.prefix, "", "", SLASH_SEPARATOR, 1) .await?; Ok(result.common_prefixes.len() > 0 || result.contents.len() > 0) diff --git a/crates/iam/src/store/object.rs b/crates/iam/src/store/object.rs index 2cadfd71f..c66d8c9b7 100644 --- a/crates/iam/src/store/object.rs +++ b/crates/iam/src/store/object.rs @@ -32,7 +32,7 @@ use rustfs_ecstore::{ store_api::{ObjectInfo, ObjectOptions}, }; use rustfs_policy::{auth::UserIdentity, policy::PolicyDoc}; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join_buf}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; use serde::{Serialize, de::DeserializeOwned}; use std::sync::LazyLock; use std::{collections::HashMap, sync::Arc}; @@ -188,7 +188,7 @@ impl ObjectStore { } else { info.name }; - let name = object_name.trim_start_matches(&prefix).trim_end_matches(SLASH_SEPARATOR_STR); + let name = object_name.trim_start_matches(&prefix).trim_end_matches(SLASH_SEPARATOR); let _ = sender .send(StringOrErr { item: Some(name.to_owned()), diff --git a/crates/scanner/src/data_usage_define.rs b/crates/scanner/src/data_usage_define.rs index 369dcd52c..293a77783 100644 --- a/crates/scanner/src/data_usage_define.rs +++ b/crates/scanner/src/data_usage_define.rs @@ -32,12 +32,12 @@ use rustfs_ecstore::{ error::{Error, Result as StorageResult, StorageError}, store_api::{ObjectInfo, ObjectOptions}, }; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join_buf}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; use tokio::time::{Duration, sleep, timeout}; use tracing::{error, warn}; // Data usage constants -pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR_STR; +pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR; const DATA_USAGE_OBJ_NAME: &str = ".usage.json"; @@ -47,16 +47,16 @@ pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin"; // Data usage paths (computed at runtime) pub static DATA_USAGE_BUCKET: LazyLock = - LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR_STR}{BUCKET_META_PREFIX}")); + LazyLock::new(|| format!("{RUSTFS_META_BUCKET}{SLASH_SEPARATOR}{BUCKET_META_PREFIX}")); pub static DATA_USAGE_OBJ_NAME_PATH: LazyLock = - LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR_STR}{DATA_USAGE_OBJ_NAME}")); + LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR}{DATA_USAGE_OBJ_NAME}")); pub static DATA_USAGE_BLOOM_NAME_PATH: LazyLock = - LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR_STR}{DATA_USAGE_BLOOM_NAME}")); + LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR}{DATA_USAGE_BLOOM_NAME}")); pub static BACKGROUND_HEAL_INFO_PATH: LazyLock = - LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR_STR}.background-heal.json")); + LazyLock::new(|| format!("{BUCKET_META_PREFIX}{SLASH_SEPARATOR}.background-heal.json")); #[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct TierStats { diff --git a/crates/scanner/src/scanner_folder.rs b/crates/scanner/src/scanner_folder.rs index f88521533..a9d649241 100644 --- a/crates/scanner/src/scanner_folder.rs +++ b/crates/scanner/src/scanner_folder.rs @@ -44,7 +44,7 @@ use rustfs_ecstore::pools::{path2_bucket_object, path2_bucket_object_with_base_p use rustfs_ecstore::store_api::{ObjectInfo, ObjectToDelete}; use rustfs_ecstore::store_utils::is_reserved_or_invalid_bucket; use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams, ReplicationStatusType}; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join_buf}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; use s3s::dto::{BucketLifecycleConfiguration, ObjectLockConfiguration}; use tokio::select; use tokio::sync::mpsc; @@ -110,7 +110,7 @@ impl ScannerItem { /// This converts a directory path like "bucket/dir1/dir2/file" to prefix="bucket/dir1/dir2" and object_name="file" pub fn transform_meta_dir(&mut self) { let prefix = self.prefix.clone(); // Clone to avoid borrow checker issues - let split: Vec<&str> = prefix.split(SLASH_SEPARATOR_STR).collect(); + let split: Vec<&str> = prefix.split(SLASH_SEPARATOR).collect(); if split.len() > 1 { let prefix_parts: Vec<&str> = split[..split.len() - 1].to_vec(); diff --git a/crates/scanner/src/scanner_io.rs b/crates/scanner/src/scanner_io.rs index 5531cb8c4..580b495b0 100644 --- a/crates/scanner/src/scanner_io.rs +++ b/crates/scanner/src/scanner_io.rs @@ -22,7 +22,7 @@ use rustfs_ecstore::set_disk::SetDisks; use rustfs_ecstore::store_api::{BucketInfo, BucketOptions, ObjectInfo}; use rustfs_ecstore::{StorageAPI, error::Result, store::ECStore}; use rustfs_filemeta::FileMeta; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join_buf}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; use s3s::dto::{BucketLifecycleConfiguration, ReplicationConfiguration}; use std::collections::HashMap; use std::time::SystemTime; @@ -469,7 +469,7 @@ impl ScannerIOCache for SetDisks { #[async_trait::async_trait] impl ScannerIODisk for Disk { async fn get_size(&self, mut item: ScannerItem) -> Result { - if !item.path.ends_with(&format!("{SLASH_SEPARATOR_STR}{STORAGE_FORMAT_FILE}")) { + if !item.path.ends_with(&format!("{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}")) { return Err(StorageError::other("skip file".to_string())); } diff --git a/crates/utils/src/dunce.rs b/crates/utils/src/dunce.rs new file mode 100644 index 000000000..b6405057c --- /dev/null +++ b/crates/utils/src/dunce.rs @@ -0,0 +1,415 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utility functions to handle Windows paths, specifically converting UNC paths to regular paths where possible. +//! This is useful for compatibility with tools that do not support UNC paths (starting with `\\?\`). + +#[cfg(any(windows, test))] +use std::ffi::OsStr; + +#[cfg(windows)] +use std::path::{Component, Prefix}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +/// Takes any path, and when possible, converts Windows UNC paths to regular paths. +/// If the path can't be converted, it's returned unmodified. +/// +/// On non-Windows this is a no-op. +/// +/// `\\?\C:\Windows` will be converted to `C:\Windows`, +/// but `\\?\C:\COM` will be left as-is (due to a reserved filename). +/// +/// Use this to pass arbitrary paths to programs that may not be UNC-aware. +/// +/// It's generally safe to pass UNC paths to legacy programs, because +/// these paths contain a reserved prefix, so will gracefully fail +/// if used with legacy APIs that don't support UNC. +/// +/// This function does not perform any I/O. +/// +/// Currently paths with unpaired surrogates aren't converted even if they +/// could be, due to limitations of Rust's `OsStr` API. +/// +/// To check if a path remained as UNC, use [`is_simplified()`] or `path.as_os_str().as_encoded_bytes().starts_with(b"\\\\")`. +/// +/// # Arguments +/// +/// * `path` - The path to simplify. +/// +/// # Returns +/// +/// A reference to the simplified path if it could be simplified, or the original path otherwise. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use rustfs_utils::simplified; +/// +/// #[cfg(windows)] +/// { +/// let unc_path = Path::new(r"\\?\C:\Windows"); +/// let simple_path = simplified(unc_path); +/// assert_eq!(simple_path, Path::new(r"C:\Windows")); +/// } +/// ``` +#[inline] +#[must_use] +pub fn simplified(path: &Path) -> &Path { + try_simplified(path).unwrap_or(path) +} + +/// Like `std::fs::canonicalize()`, but on Windows it outputs the most +/// compatible form of a path instead of UNC. +/// +/// This function first canonicalizes the path (resolving symlinks and absolute path), +/// and then attempts to simplify it using [`simplified()`] on Windows. +/// +/// # Arguments +/// +/// * `path` - The path to canonicalize. +/// +/// # Returns +/// +/// The canonicalized and simplified path, or an error if canonicalization fails. +#[inline(always)] +pub fn canonicalize>(path: P) -> io::Result { + let path = path.as_ref(); + + #[cfg(not(windows))] + { + fs::canonicalize(path) + } + #[cfg(windows)] + { + canonicalize_win(path) + } +} + +#[cfg(windows)] +fn canonicalize_win(path: &Path) -> io::Result { + let real_path = fs::canonicalize(path)?; + Ok(try_simplified(&real_path).map(PathBuf::from).unwrap_or(real_path)) +} + +/// Returns `true` if the path is relative or starts with a disk prefix (like "C:"). +/// +/// This checks if the path is in a "simplified" format. On Windows, this means it +/// is either a relative path, a rooted path without a prefix, or starts with a drive letter (e.g., `C:\`). +/// UNC paths (starting with `\\`) and verbatim paths (starting with `\\?\`) will return `false`. +/// +/// On non-Windows platforms, this always returns `true`. +/// +/// # Arguments +/// +/// * `path` - The path to check. +/// +/// # Returns +/// +/// `true` if the path is simplified, `false` otherwise. +#[cfg_attr(not(windows), allow(unused))] +#[must_use] +pub fn is_simplified(path: &Path) -> bool { + #[cfg(windows)] + if let Some(Component::Prefix(prefix)) = path.components().next() { + return matches!(prefix.kind(), Prefix::Disk(..)); + } + true +} + +#[doc(hidden)] +pub use self::canonicalize as realpath; + +/// Checks if the filename is valid for a regular Windows path. +/// +/// This checks for: +/// - Length limit (255 chars) +/// - Invalid characters (< > : " / \ | ? * and control chars) +/// - Trailing spaces or dots +#[cfg(any(windows, test))] +fn is_valid_filename(file_name: &OsStr) -> bool { + // Convert to str first. If not valid UTF-8, it's not a valid "simplified" filename candidate + // (since we require the whole path to be UTF-8 at the end). + let s = match file_name.to_str() { + Some(s) => s, + None => return false, + }; + + // Windows filename limit is 255 characters (UTF-16 code units) + // We check byte length first as a fast path optimization. + if s.len() > 255 && s.encode_utf16().count() > 255 { + return false; + } + + if s.is_empty() { + return false; + } + + // Only ASCII subset is checked, and WTF-8/UTF-8 is safe for that + // Invalid chars: < > : " / \ | ? * and control chars (0-31) + let byte_str = s.as_bytes(); + if byte_str + .iter() + .any(|&c| matches!(c, 0..=31 | b'<' | b'>' | b':' | b'"' | b'/' | b'\\' | b'|' | b'?' | b'*')) + { + return false; + } + // Filename can't end with . or space + if matches!(byte_str.last(), Some(b' ' | b'.')) { + return false; + } + true +} + +/// Checks if the filename is a reserved DOS device name (e.g., CON, PRN, COM1). +/// +/// Reserved names are case-insensitive and apply even if an extension is present (e.g., "CON.txt" is reserved). +#[cfg(any(windows, test))] +fn is_reserved>(file_name: P) -> bool { + let s = match file_name.as_ref().to_str() { + Some(s) => s, + None => return false, // Reserved names are all ASCII + }; + + if s.is_empty() { + return false; + } + + // Get the stem (part before the first dot) + // "CON.txt" -> "CON" + // "CON" -> "CON" + let stem = match s.find('.') { + Some(idx) => &s[..idx], + None => s, + }; + + // Trim trailing spaces (Windows ignores them, so "CON " is "CON") + let stem = stem.trim_end(); + + // Reserved names are 3 or 4 chars + if stem.len() != 3 && stem.len() != 4 { + return false; + } + + if stem.len() == 3 { + return stem.eq_ignore_ascii_case("CON") + || stem.eq_ignore_ascii_case("PRN") + || stem.eq_ignore_ascii_case("AUX") + || stem.eq_ignore_ascii_case("NUL"); + } + + // Check COM1-9, LPT1-9 + let (base, suffix) = stem.split_at(3); + if base.eq_ignore_ascii_case("COM") || base.eq_ignore_ascii_case("LPT") { + return suffix.chars().next().map(|c| c.is_ascii_digit() && c != '0').unwrap_or(false); + } + + false +} + +#[cfg(not(windows))] +#[inline] +const fn try_simplified(_path: &Path) -> Option<&Path> { + None +} + +#[cfg(windows)] +fn try_simplified(path: &Path) -> Option<&Path> { + let mut components = path.components(); + // Check if it starts with \\?\ + match components.next() { + Some(Component::Prefix(p)) => match p.kind() { + Prefix::VerbatimDisk(..) => {} + _ => return None, // Other kinds of UNC paths or regular paths + }, + _ => return None, // relative or empty + } + + // Validate all subsequent components + for component in components { + match component { + Component::RootDir => {} + Component::Normal(file_name) => { + if !is_valid_filename(file_name) || is_reserved(file_name) { + return None; + } + } + _ => return None, // UNC paths take things like ".." literally, which we can't simplify to + }; + } + + // We use to_str() here because we need to slice the string to remove the prefix. + // This implicitly requires the path to be valid UTF-8. + // If it's not, we return None, which is safe (we just don't simplify). + let s = path.to_str()?; + // Remove "\\?\" (4 chars) + let simplified = &s[4..]; + + // Check length limit (MAX_PATH is 260) + if simplified.len() > 260 && simplified.encode_utf16().count() > 260 { + return None; + } + Some(Path::new(simplified)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsStr; + use std::path::Path; + + #[test] + fn test_reserved_names() { + let reserved = [ + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + "con", + "Con", + "CON.txt", + "con.txt", + "CON .txt", + "con .txt", + "CON.tar.gz", + "con.", + "con .", + ]; + for name in reserved { + assert!(is_reserved(name), "Should be reserved: {}", name); + } + + let not_reserved = [ + "CON0", "COM0", "LPT0", "COM10", "LPT10", "CONsole", "PrNter", "NuLly", "not_con", "con_not", ".CON", "@CON", + "CON。", "foo.con", + ]; + for name in not_reserved { + assert!(!is_reserved(name), "Should NOT be reserved: {}", name); + } + } + + #[test] + fn test_valid_filename() { + let invalid = [ + "", + ".", + "..", + " ", + " .", + "foo ", + "foo.", + "foo/bar", + "foo\\bar", + "foo:bar", + "foo*bar", + "foo?bar", + "foo\"bar", + "foobar", + "foo|bar", + "foo\0bar", + "foo\x1fbar", + ]; + for name in invalid { + assert!(!is_valid_filename(OsStr::new(name)), "Should be invalid: {:?}", name); + } + + let valid = [ + "foo", + "foo.bar", + ".foo", + "foo.bar.baz", + "foo bar", + "foo. bar", // space inside is ok + "中文", + "😀", + ]; + for name in valid { + assert!(is_valid_filename(OsStr::new(name)), "Should be valid: {:?}", name); + } + + // Test length limit + let long_name = "a".repeat(256); + assert!(!is_valid_filename(OsStr::new(&long_name))); + let ok_name = "a".repeat(255); + assert!(is_valid_filename(OsStr::new(&ok_name))); + } + + #[test] + #[cfg(windows)] + fn test_simplified_windows() { + // Basic + assert_eq!(simplified(Path::new(r"\\?\C:\Windows")), Path::new(r"C:\Windows")); + + // Reserved + assert_eq!(simplified(Path::new(r"\\?\C:\CON")), Path::new(r"\\?\C:\CON")); + assert_eq!(simplified(Path::new(r"\\?\C:\aux.txt")), Path::new(r"\\?\C:\aux.txt")); + + // Long paths + let long_path = format!(r"\\?\C:\{}", "a".repeat(300)); + assert_eq!(simplified(Path::new(&long_path)), Path::new(&long_path)); + + let ok_path = format!(r"\\?\C:\{}", "a".repeat(200)); + assert_eq!(simplified(Path::new(&ok_path)), Path::new(&ok_path[4..])); + + // Relative/other prefixes + assert_eq!(simplified(Path::new(r"C:\Windows")), Path::new(r"C:\Windows")); + assert_eq!(simplified(Path::new(r"\\server\share")), Path::new(r"\\server\share")); + assert_eq!(simplified(Path::new(r"\\.\C:\Windows")), Path::new(r"\\.\C:\Windows")); + } + + #[test] + #[cfg(not(windows))] + fn test_simplified_unix() { + let p = Path::new("/tmp/foo"); + assert_eq!(simplified(p), p); + } + + #[test] + fn test_is_simplified() { + #[cfg(windows)] + { + assert!(is_simplified(Path::new(r"C:\Windows"))); + assert!(is_simplified(Path::new(r"relative\path"))); + assert!(!is_simplified(Path::new(r"\\?\C:\Windows"))); + assert!(!is_simplified(Path::new(r"\\server\share"))); + } + #[cfg(not(windows))] + { + assert!(is_simplified(Path::new("/tmp/foo"))); + assert!(is_simplified(Path::new("relative/path"))); + } + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 4c2396bcf..5bf82476b 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -85,5 +85,10 @@ pub use notify::*; #[cfg(feature = "obj")] pub mod obj; +#[cfg(feature = "path")] +mod dunce; +#[cfg(feature = "path")] +pub use dunce::*; mod envs; + pub use envs::*; diff --git a/crates/utils/src/os/windows.rs b/crates/utils/src/os/windows.rs index 5bfa79d1c..e00edc36d 100644 --- a/crates/utils/src/os/windows.rs +++ b/crates/utils/src/os/windows.rs @@ -44,7 +44,7 @@ pub fn get_info(p: impl AsRef) -> std::io::Result { } let total = total_number_of_bytes; - let free = total_number_of_free_bytes; + let free = free_bytes_available; if free > total { return Err(Error::other(format!( @@ -180,8 +180,6 @@ pub fn get_drive_stats(_major: u32, _minor: u32) -> std::io::Result { #[cfg(test)] mod tests { - use crate::os::{get_info, same_disk}; - #[cfg(target_os = "windows")] #[test] fn test_get_info_valid_path() { diff --git a/crates/utils/src/path.rs b/crates/utils/src/path.rs index 0bf750649..6cc0771b3 100644 --- a/crates/utils/src/path.rs +++ b/crates/utils/src/path.rs @@ -15,36 +15,21 @@ use std::path::Path; use std::path::PathBuf; -#[cfg(target_os = "windows")] -const SLASH_SEPARATOR: char = '\\'; -#[cfg(not(target_os = "windows"))] -const SLASH_SEPARATOR: char = '/'; - -/// GLOBAL_DIR_SUFFIX is a special suffix used to denote directory objects -/// in object storage systems that do not have a native directory concept. pub const GLOBAL_DIR_SUFFIX: &str = "__XLDIR__"; -/// SLASH_SEPARATOR_STR is the string representation of the path separator -/// used in the current operating system. -pub const SLASH_SEPARATOR_STR: &str = if cfg!(target_os = "windows") { "\\" } else { "/" }; +pub const SLASH_SEPARATOR: &str = "/"; -/// GLOBAL_DIR_SUFFIX_WITH_SLASH is the directory suffix followed by the -/// platform-specific path separator, used to denote directory objects. -#[cfg(target_os = "windows")] -pub const GLOBAL_DIR_SUFFIX_WITH_SLASH: &str = "__XLDIR__\\"; -#[cfg(not(target_os = "windows"))] pub const GLOBAL_DIR_SUFFIX_WITH_SLASH: &str = "__XLDIR__/"; -/// has_suffix checks if the string `s` ends with the specified `suffix`, -/// performing a case-insensitive comparison on Windows platforms. -/// -/// # Arguments -/// * `s` - A string slice that holds the string to be checked. -/// * `suffix` - A string slice that holds the suffix to check for. -/// -/// # Returns -/// A boolean indicating whether `s` ends with `suffix`. +#[inline] +pub fn is_separator(c: u8) -> bool { + c == b'/' || (cfg!(target_os = "windows") && c == b'\\') +} + +/// Checks if the string `s` ends with `suffix`. /// +/// On Windows, this comparison is case-insensitive. +/// On other platforms, it is case-sensitive. pub fn has_suffix(s: &str, suffix: &str) -> bool { if cfg!(target_os = "windows") { s.to_lowercase().ends_with(&suffix.to_lowercase()) @@ -53,46 +38,31 @@ pub fn has_suffix(s: &str, suffix: &str) -> bool { } } -/// encode_dir_object encodes a directory object by appending -/// a special suffix if it ends with a slash. -/// -/// # Arguments -/// * `object` - A string slice that holds the object to be encoded. -/// -/// # Returns -/// A `String` representing the encoded directory object. +/// Encodes a directory object name by replacing the trailing slash with `GLOBAL_DIR_SUFFIX`. /// +/// If the object name ends with a slash, it is considered a directory object. +/// The trailing slash is removed and `GLOBAL_DIR_SUFFIX` is appended. +/// If it does not end with a slash, the name is returned as is. pub fn encode_dir_object(object: &str) -> String { - if has_suffix(object, SLASH_SEPARATOR_STR) { - format!("{}{}", object.trim_end_matches(SLASH_SEPARATOR_STR), GLOBAL_DIR_SUFFIX) + if has_suffix(object, SLASH_SEPARATOR) { + format!("{}{}", object.trim_end_matches(SLASH_SEPARATOR), GLOBAL_DIR_SUFFIX) } else { object.to_string() } } -/// is_dir_object checks if the given object string represents -/// a directory object by verifying if it ends with the special suffix. -/// -/// # Arguments -/// * `object` - A string slice that holds the object to be checked. -/// -/// # Returns -/// A boolean indicating whether the object is a directory object. +/// Checks if the given object name represents a directory object. /// +/// Returns true if the object name ends with `GLOBAL_DIR_SUFFIX`. pub fn is_dir_object(object: &str) -> bool { let obj = encode_dir_object(object); obj.ends_with(GLOBAL_DIR_SUFFIX) } -/// decode_dir_object decodes a directory object by removing -/// the special suffix if it is present. -/// -/// # Arguments -/// * `object` - A string slice that holds the object to be decoded. -/// -/// # Returns -/// A `String` representing the decoded directory object. +/// Decodes a directory object name by replacing `GLOBAL_DIR_SUFFIX` with a trailing slash. /// +/// If the object name ends with `GLOBAL_DIR_SUFFIX`, it is replaced with a slash. +/// Otherwise, the name is returned as is. #[allow(dead_code)] pub fn decode_dir_object(object: &str) -> String { if has_suffix(object, GLOBAL_DIR_SUFFIX) { @@ -102,50 +72,31 @@ pub fn decode_dir_object(object: &str) -> String { } } -/// retain_slash ensures that the given string `s` ends with a slash. -/// If it does not, a slash is appended. -/// -/// # Arguments -/// * `s` - A string slice that holds the string to be processed. -/// -/// # Returns -/// A `String` that ends with a slash. +/// Ensures that the string ends with a trailing slash if it is not empty. /// +/// If the string is empty, it is returned as is. +/// If it already ends with a slash, it is returned as is. +/// Otherwise, a slash is appended. pub fn retain_slash(s: &str) -> String { if s.is_empty() { return s.to_string(); } - if s.ends_with(SLASH_SEPARATOR_STR) { + if s.ends_with(SLASH_SEPARATOR) { s.to_string() } else { - format!("{s}{SLASH_SEPARATOR_STR}") + format!("{s}{SLASH_SEPARATOR}") } } -/// strings_has_prefix_fold checks if the string `s` starts with the specified `prefix`, -/// performing a case-insensitive comparison on Windows platforms. -/// -/// # Arguments -/// * `s` - A string slice that holds the string to be checked. -/// * `prefix` - A string slice that holds the prefix to check for. -/// -/// # Returns -/// A boolean indicating whether `s` starts with `prefix`. -/// +/// 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)) } -/// has_prefix checks if the string `s` starts with the specified `prefix`, -/// performing a case-insensitive comparison on Windows platforms. -/// -/// # Arguments -/// * `s` - A string slice that holds the string to be checked. -/// * `prefix` - A string slice that holds the prefix to check for. -/// -/// # Returns -/// A boolean indicating whether `s` starts with `prefix`. +/// Checks if string `s` starts with `prefix`. /// +/// On Windows, this comparison is case-insensitive. +/// On other platforms, it is case-sensitive. pub fn has_prefix(s: &str, prefix: &str) -> bool { if cfg!(target_os = "windows") { return strings_has_prefix_fold(s, prefix); @@ -154,45 +105,69 @@ pub fn has_prefix(s: &str, prefix: &str) -> bool { s.starts_with(prefix) } -/// path_join joins multiple path elements into a single PathBuf, -/// ensuring that the resulting path is clean and properly formatted. +/// Joins multiple path components into a single `PathBuf`. /// -/// # Arguments -/// * `elem` - A slice of path elements to be joined. -/// -/// # Returns -/// A PathBuf representing the joined path. +/// This function normalizes the path separators to forward slashes (`/`) internally +/// and cleans the path (resolving `.` and `..`). /// +/// On Windows, backslashes in input components are treated as separators and normalized. pub fn path_join>(elem: &[P]) -> PathBuf { - if elem.is_empty() { - return PathBuf::from("."); + let trailing_slash = !elem.is_empty() + && elem.last().is_some_and(|last| { + let s = last.as_ref().to_string_lossy(); + s.ends_with(SLASH_SEPARATOR) || (cfg!(target_os = "windows") && s.ends_with('\\')) + }); + + let len = elem.iter().map(|s| s.as_ref().to_string_lossy().len()).sum::() + elem.len(); + let mut dst = String::with_capacity(len); + let mut added = 0; + + for e in elem { + let s = e.as_ref().to_string_lossy(); + if added > 0 || !s.is_empty() { + if added > 0 { + dst.push_str(SLASH_SEPARATOR); + } + dst.push_str(&s); + added += s.len(); + } } - // Collect components as owned Strings (lossy for non-UTF8) - let strs: Vec = elem.iter().map(|p| p.as_ref().to_string_lossy().into_owned()).collect(); - // Convert to slice of &str for path_join_buf - let refs: Vec<&str> = strs.iter().map(|s| s.as_str()).collect(); - PathBuf::from(path_join_buf(&refs)) + + if path_needs_clean(dst.as_bytes()) { + let mut clean_path = clean(&dst); + if trailing_slash { + clean_path.push_str(SLASH_SEPARATOR); + } + return clean_path.into(); + } + + if trailing_slash { + dst.push_str(SLASH_SEPARATOR); + } + + dst.into() } -/// path_join_buf joins multiple string path elements into a single String, -/// ensuring that the resulting path is clean and properly formatted. +/// Joins multiple string path components into a single `String`. /// -/// # Arguments -/// * `elements` - A slice of string path elements to be joined. -/// -/// # Returns -/// A String representing the joined path. +/// This function normalizes the path separators to forward slashes (`/`) internally +/// and cleans the path (resolving `.` and `..`). /// +/// On Windows, backslashes in input components are treated as separators and normalized. pub fn path_join_buf(elements: &[&str]) -> String { - let trailing_slash = !elements.is_empty() && elements.last().is_some_and(|last| last.ends_with(SLASH_SEPARATOR_STR)); + let trailing_slash = !elements.is_empty() + && elements + .last() + .is_some_and(|last| last.ends_with(SLASH_SEPARATOR) || (cfg!(target_os = "windows") && last.ends_with('\\'))); - let mut dst = String::new(); + let len = elements.iter().map(|s| s.len()).sum::() + elements.len(); + let mut dst = String::with_capacity(len); let mut added = 0; for e in elements { if added > 0 || !e.is_empty() { if added > 0 { - dst.push(SLASH_SEPARATOR); + dst.push_str(SLASH_SEPARATOR); } dst.push_str(e); added += e.len(); @@ -202,535 +177,196 @@ pub fn path_join_buf(elements: &[&str]) -> String { if path_needs_clean(dst.as_bytes()) { let mut clean_path = clean(&dst); if trailing_slash { - clean_path.push(SLASH_SEPARATOR); + clean_path.push_str(SLASH_SEPARATOR); } return clean_path; } if trailing_slash { - dst.push(SLASH_SEPARATOR); + dst.push_str(SLASH_SEPARATOR); } dst } -/// Platform-aware separator check -fn is_sep(b: u8) -> bool { - #[cfg(target_os = "windows")] - { - b == b'/' || b == b'\\' - } - #[cfg(not(target_os = "windows"))] - { - b == b'/' - } -} - /// path_needs_clean returns whether path cleaning may change the path. /// Will detect all cases that will be cleaned, /// but may produce false positives on non-trivial paths. -/// -/// # Arguments -/// * `path` - A byte slice that holds the path to be checked. -/// -/// # Returns -/// A boolean indicating whether the path needs cleaning. -/// fn path_needs_clean(path: &[u8]) -> bool { if path.is_empty() { return true; } + // Check for backslashes on Windows + if cfg!(target_os = "windows") && path.contains(&b'\\') { + return true; + } + + let rooted = path[0] == b'/'; let n = path.len(); - // On Windows: any forward slash indicates normalization to backslash is required. - #[cfg(target_os = "windows")] - { - if path.iter().any(|&b| b == b'/') { - return true; - } - } + let (mut r, mut w) = if rooted { (1, 1) } else { (0, 0) }; - // Initialize scan index and previous-separator flag. - let mut i = 0usize; - let mut prev_was_sep = false; - - // Platform-aware prefix handling to avoid flagging meaningful leading sequences: - // - Windows: handle drive letter "C:" and UNC leading "\\" - // - Non-Windows: detect and flag double leading '/' (e.g. "//abc") as needing clean - if n >= 1 && is_sep(path[0]) { - #[cfg(target_os = "windows")] - { - // If starts with two separators -> UNC prefix: allow exactly two without flag - if n >= 2 && is_sep(path[1]) { - // If a third leading separator exists, that's redundant (e.g. "///...") -> needs clean - if n >= 3 && is_sep(path[2]) { + while r < n { + match path[r] { + b if b > 127 => { + // Non ascii. + return true; + } + b'/' => { + // multiple / elements + return true; + } + b'.' => { + if r + 1 == n || path[r + 1] == b'/' { + // . element - assume it has to be cleaned. return true; } - // Skip the two UNC leading separators for scanning; do not mark prev_was_sep true - i = 2; - prev_was_sep = false; - } else { - // Single leading separator (rooted) -> mark as seen separator so immediate next sep is duplicate - i = 1; - prev_was_sep = true; + if r + 1 < n && path[r + 1] == b'.' && (r + 2 == n || path[r + 2] == b'/') { + // .. element: remove to last / - assume it has to be cleaned. + return true; + } + // Handle single dot case + if r + 1 == n { + // . element - assume it has to be cleaned. + return true; + } + // Copy the dot + w += 1; + r += 1; } - } - - #[cfg(not(target_os = "windows"))] - { - // POSIX: double leading '/' is redundant and should be cleaned (e.g. "//abc" -> "/abc") - if n >= 2 && is_sep(path[1]) { - return true; - } - i = 1; - prev_was_sep = true; - } - } else { - // If not starting with separator, check for Windows drive-letter prefix like "C:" - #[cfg(target_os = "windows")] - { - if n >= 2 && path[1] == b':' && (path[0] as char).is_ascii_alphabetic() { - // Position after "C:" - i = 2; - // If a separator immediately follows the drive (rooted like "C:\"), - // treat that first separator as seen; if more separators follow, it's redundant. - if i < n && is_sep(path[i]) { - i += 1; // consume the single allowed separator after drive - if i < n && is_sep(path[i]) { - // multiple separators after drive like "C:\\..." -> needs clean + _ => { + // real path element. + // add slash if needed + if (rooted && w != 1) || (!rooted && w != 0) { + w += 1; + } + // copy element + while r < n && path[r] != b'/' { + if cfg!(target_os = "windows") && path[r] == b'\\' { return true; } - prev_was_sep = true; - } else { - prev_was_sep = false; + w += 1; + r += 1; + } + // allow one slash, not at end + if r < n - 1 && path[r] == b'/' { + r += 1; } } } } - // Generic scan for repeated separators and dot / dot-dot components. - while i < n { - let b = path[i]; - if is_sep(b) { - if prev_was_sep { - // Multiple separators (except allowed UNC prefix handled above) - return true; - } - prev_was_sep = true; - i += 1; - continue; - } - - // Not a separator: parse current path element - let start = i; - while i < n && !is_sep(path[i]) { - i += 1; - } - let len = i - start; - if len == 1 && path[start] == b'.' { - // single "." element -> needs cleaning - return true; - } - if len == 2 && path[start] == b'.' && path[start + 1] == b'.' { - // ".." element -> needs cleaning - return true; - } - prev_was_sep = false; + // Turn empty string into "." + if w == 0 { + return true; } - // Trailing separator: if last byte is a separator and path length > 1, then usually needs cleaning, - // except when the path is a platform-specific root form (e.g. "/" on POSIX, "\\" or "C:\" on Windows). - if n > 1 && is_sep(path[n - 1]) { - #[cfg(not(target_os = "windows"))] - { - // POSIX: any trailing separator except the single-root "/" needs cleaning. - return true; - } - #[cfg(target_os = "windows")] - { - // Windows special root forms that are acceptable with trailing separator: - // - UNC root: exactly two leading separators "\" "\" (i.e. "\\") -> n == 2 - if n == 2 && is_sep(path[0]) && is_sep(path[1]) { - return false; - } - // - Drive root: pattern "C:\" or "C:/" (len == 3) - if n == 3 && path[1] == b':' && (path[0] as char).is_ascii_alphabetic() && is_sep(path[2]) { - return false; - } - // Otherwise, trailing separator should be cleaned. - return true; - } - } - - // No conditions triggered: assume path is already clean. false } -/// path_to_bucket_object_with_base_path splits a given path into bucket and object components, -/// considering a base path to trim from the start. +/// Splits a path into bucket and object names, removing a specified base path. /// -/// # Arguments -/// * `base_path` - A string slice that holds the base path to be trimmed. -/// * `path` - A string slice that holds the path to be split. -/// -/// # Returns -/// A tuple containing the bucket and object as `String`s. +/// The path is first trimmed of the `base_path` prefix and any leading slashes. +/// Then it is split at the first slash into bucket and object. +/// If no slash is found, the whole path is returned as the bucket name, and object name is empty. /// +/// On Windows, this function handles backslashes as separators and performs case-insensitive prefix matching for `base_path`. +/// The returned bucket and object names are normalized to use forward slashes. pub fn path_to_bucket_object_with_base_path(base_path: &str, path: &str) -> (String, String) { - let path = path.trim_start_matches(base_path).trim_start_matches(SLASH_SEPARATOR); - if let Some(m) = path.find(SLASH_SEPARATOR) { - return (path[..m].to_string(), path[m + SLASH_SEPARATOR_STR.len()..].to_string()); + let mut p = path; + + // 1. Trim base_path + if cfg!(target_os = "windows") { + if strings_has_prefix_fold(p, base_path) { + p = &p[base_path.len()..]; + } + } else if p.starts_with(base_path) { + p = &p[base_path.len()..]; } - (path.to_string(), "".to_string()) + // 2. Trim leading separators + while let Some(c) = p.chars().next() { + if is_separator(c as u8) { + p = &p[c.len_utf8()..]; + } else { + break; + } + } + + // 3. Find split point + let idx = if cfg!(target_os = "windows") { + p.find(['/', '\\']) + } else { + p.find('/') + }; + + match idx { + Some(i) => { + let bucket = &p[..i]; + let object = &p[i + 1..]; // Skip the separator + + // Normalize separators on Windows + let bucket = if cfg!(target_os = "windows") { + bucket.replace('\\', "/") + } else { + bucket.to_string() + }; + let object = if cfg!(target_os = "windows") { + object.replace('\\', "/") + } else { + object.to_string() + }; + + (bucket, object) + } + None => { + let bucket = if cfg!(target_os = "windows") { + p.replace('\\', "/") + } else { + p.to_string() + }; + (bucket, "".to_string()) + } + } } -/// path_to_bucket_object splits a given path into bucket and object components. -/// -/// # Arguments -/// * `s` - A string slice that holds the path to be split. -/// -/// # Returns -/// A tuple containing the bucket and object as `String`s. +/// Splits a path into bucket and object names. /// +/// The path is trimmed of any leading slashes. +/// Then it is split at the first slash into bucket and object. +/// If no slash is found, the whole path is returned as the bucket name, and object name is empty. pub fn path_to_bucket_object(s: &str) -> (String, String) { path_to_bucket_object_with_base_path("", s) } -/// contains_any_sep_str checks if the given string contains any path separators. -/// -/// # Arguments -/// * `s` - A string slice that holds the string to be checked. -/// -/// # Returns -/// A boolean indicating whether the string contains any path separators. -fn contains_any_sep_str(s: &str) -> bool { - #[cfg(target_os = "windows")] - { - s.contains('/') || s.contains('\\') - } - #[cfg(not(target_os = "windows"))] - { - s.contains('/') - } -} - -/// base_dir_from_prefix extracts the base directory from a given prefix. -/// -/// # Arguments -/// * `prefix` - A string slice that holds the prefix to be processed. -/// -/// # Returns -/// A `String` representing the base directory extracted from the prefix. +/// Extracts the base directory from a prefix string. /// +/// It returns the directory part of the prefix. +/// If the prefix does not contain a separator, or resolves to root/current dir, an empty string is returned. +/// The result ensures a trailing slash if not empty. pub fn base_dir_from_prefix(prefix: &str) -> String { - if !contains_any_sep_str(prefix) { - return String::new(); + let mut base_dir = dir(prefix).to_owned(); + if base_dir == "." || base_dir == "./" || base_dir == "/" { + base_dir = "".to_owned(); } - let mut base_dir = dir(prefix); - if base_dir == "." || base_dir == SLASH_SEPARATOR_STR { - base_dir.clear(); + + let has_separator = if cfg!(target_os = "windows") { + prefix.contains(['/', '\\']) + } else { + prefix.contains('/') + }; + + if !has_separator { + base_dir = "".to_owned(); } - if !base_dir.is_empty() && !base_dir.ends_with(SLASH_SEPARATOR_STR) { - base_dir.push_str(SLASH_SEPARATOR_STR); + if !base_dir.is_empty() && !base_dir.ends_with(SLASH_SEPARATOR) { + base_dir.push_str(SLASH_SEPARATOR); } base_dir } -/// clean returns the shortest path name equivalent to path -/// by purely lexical processing. It applies the following rules -/// iteratively until no further processing can be done: -/// -/// 1. Replace multiple slashes with a single slash. -/// 2. Eliminate each . path name element (the current directory). -/// 3. Eliminate each inner .. path name element (the parent directory) -/// along with the non-.. element that precedes it. -/// 4. Eliminate .. elements that begin a rooted path, -/// that is, replace "/.." by "/" at the beginning of a path. -/// -/// If the result of this process is an empty string, clean returns the string ".". -/// -/// This function is adapted to work cross-platform by using the appropriate path separator. -/// On Windows, this function is aware of drive letters (e.g., `C:`) and UNC paths -/// (e.g., `\\server\share`) and cleans them using the appropriate separator. -/// -/// # Arguments -/// * `path` - A string slice that holds the path to be cleaned. -/// -/// # Returns -/// A `String` representing the cleaned path. -/// -pub fn clean(path: &str) -> String { - if path.is_empty() { - return ".".to_string(); - } - - #[cfg(target_os = "windows")] - { - use std::borrow::Cow; - let bytes = path.as_bytes(); - let n = bytes.len(); - // Windows-aware handling - let mut i = 0usize; - let mut drive: Option = None; - let mut rooted = false; - let mut preserve_leading_double_sep = false; - - // Drive letter detection - if n >= 2 && bytes[1] == b':' && (bytes[0] as char).is_ascii_alphabetic() { - drive = Some(bytes[0] as char); - i = 2; - // If next is separator, it's an absolute drive-root (e.g., "C:\") - if i < n && is_sep(bytes[i]) { - rooted = true; - // consume all leading separators after drive - while i < n && is_sep(bytes[i]) { - i += 1; - } - } - } else { - // UNC or absolute by separators - if n >= 2 && is_sep(bytes[0]) && is_sep(bytes[1]) { - rooted = true; - preserve_leading_double_sep = true; - i = 2; - // consume extra leading separators - while i < n && is_sep(bytes[i]) { - i += 1; - } - } else if is_sep(bytes[0]) { - rooted = true; - i = 1; - while i < n && is_sep(bytes[i]) { - i += 1; - } - } - } - - // Component stack - let mut comps: Vec> = Vec::new(); - let mut r = i; - while r < n { - // find next sep or end - let start = r; - while r < n && !is_sep(bytes[r]) { - r += 1; - } - // component bytes [start..r) - let comp = String::from_utf8_lossy(&bytes[start..r]); - if comp == "." { - // skip - } else if comp == ".." { - if !comps.is_empty() { - // pop last component - comps.pop(); - } else if !rooted { - // relative path with .. at front must be kept - comps.push(Cow::Owned("..".to_string())); - } else { - // rooted and at root => ignore - } - } else { - comps.push(comp); - } - // skip separators - while r < n && is_sep(bytes[r]) { - r += 1; - } - } - - // Build result - let mut out = String::new(); - if let Some(d) = drive { - out.push(d); - out.push(':'); - if rooted { - out.push(SLASH_SEPARATOR); - } - } else if preserve_leading_double_sep { - out.push(SLASH_SEPARATOR); - out.push(SLASH_SEPARATOR); - } else if rooted { - out.push(SLASH_SEPARATOR); - } - - // Join components - for (idx, c) in comps.iter().enumerate() { - if !out.is_empty() && !out.ends_with(SLASH_SEPARATOR_STR) { - out.push(SLASH_SEPARATOR); - } - out.push_str(c); - } - - // Special cases: - if out.is_empty() { - // No drive, no components -> "." - return ".".to_string(); - } - - // If output is just "C:" (drive without components and not rooted), keep as "C:" - if drive.is_some() { - if out.len() == 2 && out.as_bytes()[1] == b':' { - return out; - } - // If drive+colon+sep and no components, return "C:\" - if out.len() == 3 && out.as_bytes()[1] == b':' && is_sep(out.as_bytes()[2]) { - return out; - } - } - - // Remove trailing separator unless it's a root form (single leading sep or drive root or UNC) - if out.len() > 1 && out.ends_with(SLASH_SEPARATOR_STR) { - // Determine if it's a root form: "/" or "\\" or "C:\" - let is_root = { - // "/" (non-drive single sep) - if out == SLASH_SEPARATOR_STR { - true - } else if out.starts_with(SLASH_SEPARATOR_STR) && out == format!("{}{}", SLASH_SEPARATOR_STR, SLASH_SEPARATOR_STR) - { - // only double separator - true - } else { - // drive root "C:\" length >=3 with pattern X:\ - if out.len() == 3 && out.as_bytes()[1] == b':' && is_sep(out.as_bytes()[2]) { - true - } else { - false - } - } - }; - if !is_root { - out.pop(); - } - } - - out - } - - #[cfg(not(target_os = "windows"))] - { - // POSIX-like behavior (original implementation but simplified) - let rooted = path.starts_with('/'); - let n = path.len(); - let mut out = LazyBuf::new(path.to_string()); - let mut r = 0usize; - let mut dotdot = 0usize; - - if rooted { - out.append(b'/'); - r = 1; - dotdot = 1; - } - - while r < n { - match path.as_bytes()[r] { - b'/' => { - // Empty path element - r += 1; - } - b'.' if r + 1 == n || path.as_bytes()[r + 1] == b'/' => { - // . element - r += 1; - } - b'.' if path.as_bytes()[r + 1] == b'.' && (r + 2 == n || path.as_bytes()[r + 2] == b'/') => { - // .. element: remove to last / - r += 2; - - if out.w > dotdot { - // Can backtrack - out.w -= 1; - while out.w > dotdot && out.index(out.w) != b'/' { - out.w -= 1; - } - } else if !rooted { - // Cannot backtrack but not rooted, so append .. element. - if out.w > 0 { - out.append(b'/'); - } - out.append(b'.'); - out.append(b'.'); - dotdot = out.w; - } - } - _ => { - // Real path element. - // Add slash if needed - if (rooted && out.w != 1) || (!rooted && out.w != 0) { - out.append(b'/'); - } - - // Copy element - while r < n && path.as_bytes()[r] != b'/' { - out.append(path.as_bytes()[r]); - r += 1; - } - } - } - } - - // Turn empty string into "." - if out.w == 0 { - return ".".to_string(); - } - - out.string() - } -} - -/// split splits path immediately after the final slash, -/// separating it into a directory and file name component. -/// If there is no slash in path, split returns -/// ("", path). -/// -/// # Arguments -/// * `path` - A string slice that holds the path to be split. -/// -/// # Returns -/// A tuple containing the directory and file name as string slices. -/// -pub fn split(path: &str) -> (&str, &str) { - // Find the last occurrence of the '/' character - if let Some(i) = path.rfind(SLASH_SEPARATOR_STR) { - // Return the directory (up to and including the last '/') and the file name - return (&path[..i + 1], &path[i + 1..]); - } - // If no '/' is found, return an empty string for the directory and the whole path as the file name - (path, "") -} - -/// dir returns all but the last element of path, -/// typically the path's directory. After dropping the final -/// element, the path is cleaned. If the path is empty, -/// dir returns ".". -/// -/// # Arguments -/// * `path` - A string slice that holds the path to be processed. -/// -/// # Returns -/// A `String` representing the directory part of the path. -/// -pub fn dir(path: &str) -> String { - let (a, _) = split(path); - clean(a) -} - -/// trim_etag removes surrounding double quotes from an ETag string. -/// -/// # Arguments -/// * `etag` - A string slice that holds the ETag to be trimmed. -/// -/// # Returns -/// A `String` representing the trimmed ETag. -/// -pub fn trim_etag(etag: &str) -> String { - etag.trim_matches('"').to_string() -} - -/// LazyBuf is a structure that efficiently builds a byte buffer -/// from a string by delaying the allocation of the buffer until -/// a modification is necessary. It allows appending bytes and -/// retrieving the current string representation. +/// A helper struct for lazy buffer allocation during string processing. pub struct LazyBuf { s: String, buf: Option>, @@ -738,17 +374,12 @@ pub struct LazyBuf { } impl LazyBuf { - /// Creates a new LazyBuf with the given string. - /// - /// # Arguments - /// * `s` - A string to initialize the LazyBuf. - /// - /// # Returns - /// A new instance of LazyBuf. + /// Creates a new `LazyBuf` with the given source string. pub fn new(s: String) -> Self { LazyBuf { s, buf: None, w: 0 } } + /// Returns the byte at index `i`, either from the buffer or the source string. pub fn index(&self, i: usize) -> u8 { if let Some(ref buf) = self.buf { buf[i] @@ -757,6 +388,11 @@ impl LazyBuf { } } + /// Appends a byte to the buffer. + /// + /// If the buffer hasn't been allocated yet, it checks if the byte matches the source string + /// at the current write position. If it matches, it just advances the write position. + /// If it doesn't match, it allocates the buffer and copies the prefix. pub fn append(&mut self, c: u8) { if self.buf.is_none() { if self.w < self.s.len() && self.s.as_bytes()[self.w] == c { @@ -774,6 +410,7 @@ impl LazyBuf { } } + /// Returns the resulting string. pub fn string(&self) -> String { if let Some(ref buf) = self.buf { String::from_utf8(buf[..self.w].to_vec()).unwrap() @@ -783,84 +420,135 @@ impl LazyBuf { } } +/// Returns the shortest path name equivalent to the given path. +/// +/// It applies the following rules iteratively until no further processing can be done: +/// 1. Replace multiple separators with a single one. +/// 2. Eliminate each `.` path name element (the current directory). +/// 3. Eliminate each inner `..` path name element (the parent directory) along with the non-`..` element that precedes it. +/// 4. Eliminate `..` elements that begin a rooted path: that is, replace `/..` by `/` at the beginning of a path. +/// +/// On Windows, backslashes are converted to forward slashes. +/// The returned path ends in a slash only if it represents a root directory, such as `/` on Unix or `C:/` on Windows. +/// +/// If the result of this process is an empty string, `clean` returns the string `.`. +pub fn clean(path: &str) -> String { + if path.is_empty() { + return ".".to_string(); + } + + let rooted = path.starts_with('/') || (cfg!(target_os = "windows") && path.starts_with('\\')); + let n = path.len(); + let mut out = LazyBuf::new(path.to_string()); + let mut r = 0; + let mut dotdot = 0; + + if rooted { + out.append(b'/'); + r = 1; + dotdot = 1; + } + + while r < n { + let c = path.as_bytes()[r]; + + if is_separator(c) { + // Empty path element + r += 1; + } else if c == b'.' && (r + 1 == n || is_separator(path.as_bytes()[r + 1])) { + // . element + r += 1; + } else if c == b'.' + && (r + 1 < n && path.as_bytes()[r + 1] == b'.') + && (r + 2 == n || is_separator(path.as_bytes()[r + 2])) + { + // .. element: remove to last / + r += 2; + + if out.w > dotdot { + // Can backtrack + out.w -= 1; + while out.w > dotdot && out.index(out.w) != b'/' { + out.w -= 1; + } + } else if !rooted { + // Cannot backtrack but not rooted, so append .. element. + if out.w > 0 { + out.append(b'/'); + } + out.append(b'.'); + out.append(b'.'); + dotdot = out.w; + } + } else { + // Real path element. + // Add slash if needed + if (rooted && out.w != 1) || (!rooted && out.w != 0) { + out.append(b'/'); + } + + // Copy element + while r < n { + let c = path.as_bytes()[r]; + if is_separator(c) { + break; + } + out.append(c); + r += 1; + } + } + } + + // Turn empty string into "." + if out.w == 0 { + return ".".to_string(); + } + + out.string() +} + +/// Splits path immediately following the final separator, separating it into a directory and file name component. +/// +/// If there is no separator in path, `split` returns an empty dir and file set to path. +/// The returned values have the property that path = dir + file. +/// +/// On Windows, both `/` and `\` are treated as separators. +pub fn split(path: &str) -> (&str, &str) { + // Find the last occurrence of the separator + let idx = if cfg!(target_os = "windows") { + path.rfind(['/', '\\']) + } else { + path.rfind('/') + }; + + if let Some(i) = idx { + // Return the directory (up to and including the last separator) and the file name + return (&path[..i + 1], &path[i + 1..]); + } + // If no separator is found, return an empty string for the directory and the whole path as the file name + (path, "") +} + +/// Returns all but the last element of path, typically the path's directory. +/// +/// After dropping the final element, `dir` calls `clean` on the path and trails the result. +/// If the path is empty, `dir` returns ".". +/// If the path consists entirely of separators, `dir` returns a single separator. +/// The returned path does not end in a separator unless it is the root directory. +pub fn dir(path: &str) -> String { + let (a, _) = split(path); + clean(a) +} + +/// Trims double quotes from an ETag string. +pub fn trim_etag(etag: &str) -> String { + etag.trim_matches('"').to_string() +} + #[cfg(test)] mod tests { use super::*; - #[test] - fn test_path_join_buf() { - #[cfg(not(target_os = "windows"))] - { - // Basic joining - assert_eq!(path_join_buf(&["a", "b"]), "a/b"); - assert_eq!(path_join_buf(&["a/", "b"]), "a/b"); - - // Empty array input - assert_eq!(path_join_buf(&[]), "."); - - // Single element - assert_eq!(path_join_buf(&["a"]), "a"); - - // Multiple elements - assert_eq!(path_join_buf(&["a", "b", "c"]), "a/b/c"); - - // Elements with trailing separators - assert_eq!(path_join_buf(&["a/", "b/"]), "a/b/"); - - // Elements requiring cleaning (with "." and "..") - assert_eq!(path_join_buf(&["a", ".", "b"]), "a/b"); - assert_eq!(path_join_buf(&["a", "..", "b"]), "b"); - assert_eq!(path_join_buf(&["a", "b", ".."]), "a"); - - // Preservation of trailing slashes - assert_eq!(path_join_buf(&["a", "b/"]), "a/b/"); - assert_eq!(path_join_buf(&["a/", "b/"]), "a/b/"); - - // Empty elements - assert_eq!(path_join_buf(&["a", "", "b"]), "a/b"); - - // Double slashes (cleaning) - assert_eq!(path_join_buf(&["a//", "b"]), "a/b"); - } - #[cfg(target_os = "windows")] - { - // Basic joining - assert_eq!(path_join_buf(&["a", "b"]), "a\\b"); - assert_eq!(path_join_buf(&["a\\", "b"]), "a\\b"); - - // Empty array input - assert_eq!(path_join_buf(&[]), "."); - - // Single element - assert_eq!(path_join_buf(&["a"]), "a"); - - // Multiple elements - assert_eq!(path_join_buf(&["a", "b", "c"]), "a\\b\\c"); - - // Elements with trailing separators - assert_eq!(path_join_buf(&["a\\", "b\\"]), "a\\b\\"); - - // Elements requiring cleaning (with "." and "..") - assert_eq!(path_join_buf(&["a", ".", "b"]), "a\\b"); - assert_eq!(path_join_buf(&["a", "..", "b"]), "b"); - assert_eq!(path_join_buf(&["a", "b", ".."]), "a"); - - // Mixed separator handling - assert_eq!(path_join_buf(&["a/b", "c"]), "a\\b\\c"); - assert_eq!(path_join_buf(&["a\\", "b/c"]), "a\\b\\c"); - - // Preservation of trailing slashes - assert_eq!(path_join_buf(&["a", "b\\"]), "a\\b\\"); - assert_eq!(path_join_buf(&["a\\", "b\\"]), "a\\b\\"); - - // Empty elements - assert_eq!(path_join_buf(&["a", "", "b"]), "a\\b"); - - // Double slashes (cleaning) - assert_eq!(path_join_buf(&["a\\\\", "b"]), "a\\b"); - } - } - #[test] fn test_trim_etag() { // Test with quoted ETag @@ -892,56 +580,43 @@ mod tests { #[test] fn test_clean() { - #[cfg(not(target_os = "windows"))] - { - assert_eq!(clean(""), "."); - assert_eq!(clean("abc"), "abc"); - assert_eq!(clean("abc/def"), "abc/def"); - assert_eq!(clean("a/b/c"), "a/b/c"); - assert_eq!(clean("."), "."); - assert_eq!(clean(".."), ".."); - assert_eq!(clean("../.."), "../.."); - assert_eq!(clean("../../abc"), "../../abc"); - assert_eq!(clean("/abc"), "/abc"); - assert_eq!(clean("/"), "/"); - assert_eq!(clean("abc/"), "abc"); - assert_eq!(clean("abc/def/"), "abc/def"); - assert_eq!(clean("a/b/c/"), "a/b/c"); - assert_eq!(clean("./"), "."); - assert_eq!(clean("../"), ".."); - assert_eq!(clean("../../"), "../.."); - assert_eq!(clean("/abc/"), "/abc"); - assert_eq!(clean("abc//def//ghi"), "abc/def/ghi"); - assert_eq!(clean("//abc"), "/abc"); - assert_eq!(clean("///abc"), "/abc"); - assert_eq!(clean("//abc//"), "/abc"); - assert_eq!(clean("abc//"), "abc"); - assert_eq!(clean("abc/./def"), "abc/def"); - assert_eq!(clean("/./abc/def"), "/abc/def"); - assert_eq!(clean("abc/."), "abc"); - assert_eq!(clean("abc/./../def"), "def"); - assert_eq!(clean("abc//./../def"), "def"); - assert_eq!(clean("abc/../../././../def"), "../../def"); + assert_eq!(clean(""), "."); + assert_eq!(clean("abc"), "abc"); + assert_eq!(clean("abc/def"), "abc/def"); + assert_eq!(clean("a/b/c"), "a/b/c"); + assert_eq!(clean("."), "."); + assert_eq!(clean(".."), ".."); + assert_eq!(clean("../.."), "../.."); + assert_eq!(clean("../../abc"), "../../abc"); + assert_eq!(clean("/abc"), "/abc"); + assert_eq!(clean("/"), "/"); + assert_eq!(clean("abc/"), "abc"); + assert_eq!(clean("abc/def/"), "abc/def"); + assert_eq!(clean("a/b/c/"), "a/b/c"); + assert_eq!(clean("./"), "."); + assert_eq!(clean("../"), ".."); + assert_eq!(clean("../../"), "../.."); + assert_eq!(clean("/abc/"), "/abc"); + assert_eq!(clean("abc//def//ghi"), "abc/def/ghi"); + assert_eq!(clean("//abc"), "/abc"); + assert_eq!(clean("///abc"), "/abc"); + assert_eq!(clean("//abc//"), "/abc"); + assert_eq!(clean("abc//"), "abc"); + assert_eq!(clean("abc/./def"), "abc/def"); + assert_eq!(clean("/./abc/def"), "/abc/def"); + assert_eq!(clean("abc/."), "abc"); + assert_eq!(clean("abc/./../def"), "def"); + assert_eq!(clean("abc//./../def"), "def"); + assert_eq!(clean("abc/../../././../def"), "../../def"); - assert_eq!(clean("abc/def/ghi/../jkl"), "abc/def/jkl"); - assert_eq!(clean("abc/def/../ghi/../jkl"), "abc/jkl"); - assert_eq!(clean("abc/def/.."), "abc"); - assert_eq!(clean("abc/def/../.."), "."); - assert_eq!(clean("/abc/def/../.."), "/"); - assert_eq!(clean("abc/def/../../.."), ".."); - assert_eq!(clean("/abc/def/../../.."), "/"); - assert_eq!(clean("abc/def/../../../ghi/jkl/../../../mno"), "../../mno"); - } - - #[cfg(target_os = "windows")] - { - assert_eq!(clean("a\\b\\..\\c"), "a\\c"); - assert_eq!(clean("a\\\\b"), "a\\b"); - assert_eq!(clean("C:\\"), "C:\\"); - assert_eq!(clean("C:\\a\\..\\b"), "C:\\b"); - assert_eq!(clean("C:a\\b\\..\\c"), "C:a\\c"); - assert_eq!(clean("\\\\server\\share\\a\\\\b"), "\\\\server\\share\\a\\b"); - } + assert_eq!(clean("abc/def/ghi/../jkl"), "abc/def/jkl"); + assert_eq!(clean("abc/def/../ghi/../jkl"), "abc/jkl"); + assert_eq!(clean("abc/def/.."), "abc"); + assert_eq!(clean("abc/def/../.."), "."); + assert_eq!(clean("/abc/def/../.."), "/"); + assert_eq!(clean("abc/def/../../.."), ".."); + assert_eq!(clean("/abc/def/../../.."), "/"); + assert_eq!(clean("abc/def/../../../ghi/jkl/../../../mno"), "../../mno"); } #[test] @@ -1168,4 +843,36 @@ mod tests { ]); assert_eq!(result, PathBuf::from("b/c")); } + + #[test] + #[cfg(target_os = "windows")] + fn test_windows_paths() { + // Test clean with backslashes + assert_eq!(clean("a\\b\\c"), "a/b/c"); + assert_eq!(clean("a\\b/c"), "a/b/c"); + assert_eq!(clean("a\\.\\b"), "a/b"); + assert_eq!(clean("a\\b\\..\\c"), "a/c"); + assert_eq!(clean("\\a\\b"), "/a/b"); + assert_eq!(clean("a\\\\b"), "a/b"); + + // Test path_join with backslashes + let result = path_join(&[PathBuf::from("a"), PathBuf::from("b\\c")]); + assert_eq!(result, PathBuf::from("a/b/c")); + + // Test trailing backslash + let result = path_join(&[PathBuf::from("a"), PathBuf::from("b\\")]); + assert_eq!(result, PathBuf::from("a/b/")); + + // Test path_needs_clean + assert!(path_needs_clean(b"a\\b")); + + // Test path_to_bucket_object_with_base_path + let (bucket, object) = path_to_bucket_object_with_base_path("C:\\tmp", "C:\\tmp\\bucket\\object"); + assert_eq!(bucket, "bucket"); + assert_eq!(object, "object"); + + let (bucket, object) = path_to_bucket_object_with_base_path("c:\\tmp", "C:\\tmp\\bucket\\object"); + assert_eq!(bucket, "bucket"); + assert_eq!(object, "object"); + } } diff --git a/rustfs/src/admin/handlers/bucket_meta.rs b/rustfs/src/admin/handlers/bucket_meta.rs index 5fc2d9344..b011cb98c 100644 --- a/rustfs/src/admin/handlers/bucket_meta.rs +++ b/rustfs/src/admin/handlers/bucket_meta.rs @@ -44,7 +44,7 @@ use rustfs_policy::policy::{ BucketPolicy, action::{Action, AdminAction}, }; -use rustfs_utils::path::{SLASH_SEPARATOR_STR, path_join_buf}; +use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; use s3s::{ Body, S3Request, S3Response, S3Result, dto::{ @@ -423,7 +423,7 @@ impl Operation for ImportBucketMetadata { // Extract bucket names let mut bucket_names = Vec::new(); for (file_path, _) in &file_contents { - let file_path_split = file_path.split(SLASH_SEPARATOR_STR).collect::>(); + let file_path_split = file_path.split(SLASH_SEPARATOR).collect::>(); if file_path_split.len() < 2 { warn!("file path is invalid: {}", file_path); @@ -462,7 +462,7 @@ impl Operation for ImportBucketMetadata { // Second pass: process file contents for (file_path, content) in file_contents { - let file_path_split = file_path.split(SLASH_SEPARATOR_STR).collect::>(); + let file_path_split = file_path.split(SLASH_SEPARATOR).collect::>(); if file_path_split.len() < 2 { warn!("file path is invalid: {}", file_path);