1172 lines
38 KiB
Rust
1172 lines
38 KiB
Rust
// 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.
|
|
|
|
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 { "/" };
|
|
|
|
/// 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`.
|
|
///
|
|
pub fn has_suffix(s: &str, suffix: &str) -> bool {
|
|
if cfg!(target_os = "windows") {
|
|
s.to_lowercase().ends_with(&suffix.to_lowercase())
|
|
} else {
|
|
s.ends_with(suffix)
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
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)
|
|
} 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.
|
|
///
|
|
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.
|
|
///
|
|
#[allow(dead_code)]
|
|
pub fn decode_dir_object(object: &str) -> String {
|
|
if has_suffix(object, GLOBAL_DIR_SUFFIX) {
|
|
format!("{}{}", object.trim_end_matches(GLOBAL_DIR_SUFFIX), SLASH_SEPARATOR)
|
|
} else {
|
|
object.to_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.
|
|
///
|
|
pub fn retain_slash(s: &str) -> String {
|
|
if s.is_empty() {
|
|
return s.to_string();
|
|
}
|
|
if s.ends_with(SLASH_SEPARATOR_STR) {
|
|
s.to_string()
|
|
} else {
|
|
format!("{s}{SLASH_SEPARATOR_STR}")
|
|
}
|
|
}
|
|
|
|
/// 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`.
|
|
///
|
|
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`.
|
|
///
|
|
pub fn has_prefix(s: &str, prefix: &str) -> bool {
|
|
if cfg!(target_os = "windows") {
|
|
return strings_has_prefix_fold(s, prefix);
|
|
}
|
|
|
|
s.starts_with(prefix)
|
|
}
|
|
|
|
/// path_join joins multiple path elements into a single PathBuf,
|
|
/// ensuring that the resulting path is clean and properly formatted.
|
|
///
|
|
/// # Arguments
|
|
/// * `elem` - A slice of path elements to be joined.
|
|
///
|
|
/// # Returns
|
|
/// A PathBuf representing the joined path.
|
|
///
|
|
pub fn path_join<P: AsRef<Path>>(elem: &[P]) -> PathBuf {
|
|
if elem.is_empty() {
|
|
return PathBuf::from(".");
|
|
}
|
|
// Collect components as owned Strings (lossy for non-UTF8)
|
|
let strs: Vec<String> = 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))
|
|
}
|
|
|
|
/// path_join_buf joins multiple string path elements into a single String,
|
|
/// ensuring that the resulting path is clean and properly formatted.
|
|
///
|
|
/// # Arguments
|
|
/// * `elements` - A slice of string path elements to be joined.
|
|
///
|
|
/// # Returns
|
|
/// A String representing the joined path.
|
|
///
|
|
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 mut dst = String::new();
|
|
let mut added = 0;
|
|
|
|
for e in elements {
|
|
if added > 0 || !e.is_empty() {
|
|
if added > 0 {
|
|
dst.push(SLASH_SEPARATOR);
|
|
}
|
|
dst.push_str(e);
|
|
added += e.len();
|
|
}
|
|
}
|
|
|
|
if path_needs_clean(dst.as_bytes()) {
|
|
let mut clean_path = clean(&dst);
|
|
if trailing_slash {
|
|
clean_path.push(SLASH_SEPARATOR);
|
|
}
|
|
return clean_path;
|
|
}
|
|
|
|
if trailing_slash {
|
|
dst.push(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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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]) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
#[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
|
|
return true;
|
|
}
|
|
prev_was_sep = true;
|
|
} else {
|
|
prev_was_sep = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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.
|
|
///
|
|
/// # 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.
|
|
///
|
|
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());
|
|
}
|
|
|
|
(path.to_string(), "".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.
|
|
///
|
|
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.
|
|
///
|
|
pub fn base_dir_from_prefix(prefix: &str) -> String {
|
|
if !contains_any_sep_str(prefix) {
|
|
return String::new();
|
|
}
|
|
let mut base_dir = dir(prefix);
|
|
if base_dir == "." || base_dir == SLASH_SEPARATOR_STR {
|
|
base_dir.clear();
|
|
}
|
|
if !base_dir.is_empty() && !base_dir.ends_with(SLASH_SEPARATOR_STR) {
|
|
base_dir.push_str(SLASH_SEPARATOR_STR);
|
|
}
|
|
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<char> = 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<Cow<'_, str>> = 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.
|
|
pub struct LazyBuf {
|
|
s: String,
|
|
buf: Option<Vec<u8>>,
|
|
w: usize,
|
|
}
|
|
|
|
impl LazyBuf {
|
|
/// Creates a new LazyBuf with the given string.
|
|
///
|
|
/// # Arguments
|
|
/// * `s` - A string to initialize the LazyBuf.
|
|
///
|
|
/// # Returns
|
|
/// A new instance of LazyBuf.
|
|
pub fn new(s: String) -> Self {
|
|
LazyBuf { s, buf: None, w: 0 }
|
|
}
|
|
|
|
pub fn index(&self, i: usize) -> u8 {
|
|
if let Some(ref buf) = self.buf {
|
|
buf[i]
|
|
} else {
|
|
self.s.as_bytes()[i]
|
|
}
|
|
}
|
|
|
|
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 {
|
|
self.w += 1;
|
|
return;
|
|
}
|
|
let mut new_buf = vec![0; self.s.len()];
|
|
new_buf[..self.w].copy_from_slice(&self.s.as_bytes()[..self.w]);
|
|
self.buf = Some(new_buf);
|
|
}
|
|
|
|
if let Some(ref mut buf) = self.buf {
|
|
buf[self.w] = c;
|
|
self.w += 1;
|
|
}
|
|
}
|
|
|
|
pub fn string(&self) -> String {
|
|
if let Some(ref buf) = self.buf {
|
|
String::from_utf8(buf[..self.w].to_vec()).unwrap()
|
|
} else {
|
|
self.s[..self.w].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
|
|
assert_eq!(trim_etag("\"abc123\""), "abc123");
|
|
|
|
// Test with unquoted ETag
|
|
assert_eq!(trim_etag("abc123"), "abc123");
|
|
|
|
// Test with empty string
|
|
assert_eq!(trim_etag(""), "");
|
|
|
|
// Test with only quotes
|
|
assert_eq!(trim_etag("\"\""), "");
|
|
|
|
// Test with MD5 hash
|
|
assert_eq!(trim_etag("\"2c7ab85a893283e98c931e9511add182\""), "2c7ab85a893283e98c931e9511add182");
|
|
|
|
// Test with multipart ETag format
|
|
assert_eq!(trim_etag("\"098f6bcd4621d373cade4e832627b4f6-2\""), "098f6bcd4621d373cade4e832627b4f6-2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_base_dir_from_prefix() {
|
|
let a = "da/";
|
|
// Test base_dir_from_prefix function
|
|
let result = base_dir_from_prefix(a);
|
|
assert!(!result.is_empty());
|
|
}
|
|
|
|
#[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("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");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_needs_clean() {
|
|
struct PathTest {
|
|
path: &'static str,
|
|
result: &'static str,
|
|
}
|
|
|
|
let cleantests = vec![
|
|
// Already clean
|
|
PathTest { path: "", result: "." },
|
|
PathTest {
|
|
path: "abc",
|
|
result: "abc",
|
|
},
|
|
PathTest {
|
|
path: "abc/def",
|
|
result: "abc/def",
|
|
},
|
|
PathTest {
|
|
path: "a/b/c",
|
|
result: "a/b/c",
|
|
},
|
|
PathTest { path: ".", result: "." },
|
|
PathTest {
|
|
path: "..",
|
|
result: "..",
|
|
},
|
|
PathTest {
|
|
path: "../..",
|
|
result: "../..",
|
|
},
|
|
PathTest {
|
|
path: "../../abc",
|
|
result: "../../abc",
|
|
},
|
|
PathTest {
|
|
path: "/abc",
|
|
result: "/abc",
|
|
},
|
|
PathTest {
|
|
path: "/abc/def",
|
|
result: "/abc/def",
|
|
},
|
|
PathTest { path: "/", result: "/" },
|
|
// Remove trailing slash
|
|
PathTest {
|
|
path: "abc/",
|
|
result: "abc",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/",
|
|
result: "abc/def",
|
|
},
|
|
PathTest {
|
|
path: "a/b/c/",
|
|
result: "a/b/c",
|
|
},
|
|
PathTest { path: "./", result: "." },
|
|
PathTest {
|
|
path: "../",
|
|
result: "..",
|
|
},
|
|
PathTest {
|
|
path: "../../",
|
|
result: "../..",
|
|
},
|
|
PathTest {
|
|
path: "/abc/",
|
|
result: "/abc",
|
|
},
|
|
// Remove doubled slash
|
|
PathTest {
|
|
path: "abc//def//ghi",
|
|
result: "abc/def/ghi",
|
|
},
|
|
PathTest {
|
|
path: "//abc",
|
|
result: "/abc",
|
|
},
|
|
PathTest {
|
|
path: "///abc",
|
|
result: "/abc",
|
|
},
|
|
PathTest {
|
|
path: "//abc//",
|
|
result: "/abc",
|
|
},
|
|
PathTest {
|
|
path: "abc//",
|
|
result: "abc",
|
|
},
|
|
// Remove . elements
|
|
PathTest {
|
|
path: "abc/./def",
|
|
result: "abc/def",
|
|
},
|
|
PathTest {
|
|
path: "/./abc/def",
|
|
result: "/abc/def",
|
|
},
|
|
PathTest {
|
|
path: "abc/.",
|
|
result: "abc",
|
|
},
|
|
// Remove .. elements
|
|
PathTest {
|
|
path: "abc/def/ghi/../jkl",
|
|
result: "abc/def/jkl",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/../ghi/../jkl",
|
|
result: "abc/jkl",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/..",
|
|
result: "abc",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/../..",
|
|
result: ".",
|
|
},
|
|
PathTest {
|
|
path: "/abc/def/../..",
|
|
result: "/",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/../../..",
|
|
result: "..",
|
|
},
|
|
PathTest {
|
|
path: "/abc/def/../../..",
|
|
result: "/",
|
|
},
|
|
PathTest {
|
|
path: "abc/def/../../../ghi/jkl/../../../mno",
|
|
result: "../../mno",
|
|
},
|
|
// Combinations
|
|
PathTest {
|
|
path: "abc/./../def",
|
|
result: "def",
|
|
},
|
|
PathTest {
|
|
path: "abc//./../def",
|
|
result: "def",
|
|
},
|
|
PathTest {
|
|
path: "abc/../../././../def",
|
|
result: "../../def",
|
|
},
|
|
];
|
|
|
|
for test in cleantests {
|
|
let want = test.path != test.result;
|
|
let got = path_needs_clean(test.path.as_bytes());
|
|
if want && !got {
|
|
panic!("input: {:?}, want {}, got {}", test.path, want, got);
|
|
}
|
|
|
|
assert_eq!(clean(test.path), test.result);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_join() {
|
|
// Test empty input
|
|
let result = path_join::<&str>(&[]);
|
|
assert_eq!(result, PathBuf::from("."));
|
|
|
|
// Test single path
|
|
let result = path_join(&[PathBuf::from("abc")]);
|
|
assert_eq!(result, PathBuf::from("abc"));
|
|
|
|
// Test single absolute path
|
|
let result = path_join(&[PathBuf::from("/abc")]);
|
|
assert_eq!(result, PathBuf::from("/abc"));
|
|
|
|
// Test multiple relative paths
|
|
let result = path_join(&[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]);
|
|
assert_eq!(result, PathBuf::from("a/b/c"));
|
|
|
|
// Test absolute path with relative paths
|
|
let result = path_join(&[PathBuf::from("/a"), PathBuf::from("b"), PathBuf::from("c")]);
|
|
assert_eq!(result, PathBuf::from("/a/b/c"));
|
|
|
|
// Test paths with dots
|
|
let result = path_join(&[PathBuf::from("a"), PathBuf::from("."), PathBuf::from("b")]);
|
|
assert_eq!(result, PathBuf::from("a/b"));
|
|
|
|
// Test paths with double dots
|
|
let result = path_join(&[
|
|
PathBuf::from("a"),
|
|
PathBuf::from("b"),
|
|
PathBuf::from(".."),
|
|
PathBuf::from("c"),
|
|
]);
|
|
assert_eq!(result, PathBuf::from("a/c"));
|
|
|
|
// Test paths that need cleaning
|
|
let result = path_join(&[PathBuf::from("a//b"), PathBuf::from("c")]);
|
|
assert_eq!(result, PathBuf::from("a/b/c"));
|
|
|
|
// Test trailing slash preservation
|
|
let result = path_join(&[PathBuf::from("a"), PathBuf::from("b/")]);
|
|
assert_eq!(result, PathBuf::from("a/b/"));
|
|
|
|
// Test empty path in middle
|
|
let result = path_join(&[PathBuf::from("a"), PathBuf::from(""), PathBuf::from("b")]);
|
|
assert_eq!(result, PathBuf::from("a/b"));
|
|
|
|
// Test multiple absolute paths (should concatenate)
|
|
let result = path_join(&[PathBuf::from("/a"), PathBuf::from("/b"), PathBuf::from("c")]);
|
|
assert_eq!(result, PathBuf::from("/a/b/c"));
|
|
|
|
// Test complex case with various path elements
|
|
let result = path_join(&[
|
|
PathBuf::from("a"),
|
|
PathBuf::from(".."),
|
|
PathBuf::from("b"),
|
|
PathBuf::from("."),
|
|
PathBuf::from("c"),
|
|
]);
|
|
assert_eq!(result, PathBuf::from("b/c"));
|
|
}
|
|
}
|