use crate::*;
use std::hash::{BuildHasher, Hasher};
pub fn build_abs_path(root: &str, path: &str) -> String {
debug_assert!(root.starts_with('/'), "root must start with /");
debug_assert!(root.ends_with('/'), "root must end with /");
let p = root[1..].to_string();
if path == "/" {
p
} else {
debug_assert!(!path.starts_with('/'), "path must not start with /");
p + path
}
}
pub fn build_rooted_abs_path(root: &str, path: &str) -> String {
debug_assert!(root.starts_with('/'), "root must start with /");
debug_assert!(root.ends_with('/'), "root must end with /");
let p = root.to_string();
if path == "/" {
p
} else {
debug_assert!(!path.starts_with('/'), "path must not start with /");
p + path
}
}
pub fn build_rel_path(root: &str, path: &str) -> String {
debug_assert!(root != path, "get rel path with root is invalid");
if path.starts_with('/') {
debug_assert!(
path.starts_with(root),
"path {path} doesn't start with root {root}"
);
path[root.len()..].to_string()
} else {
debug_assert!(
path.starts_with(&root[1..]),
"path {path} doesn't start with root {root}"
);
path[root.len() - 1..].to_string()
}
}
pub fn normalize_path(path: &str) -> String {
let path = path.trim().trim_start_matches('/');
if path.is_empty() {
return "/".to_string();
}
let has_trailing = path.ends_with('/');
let mut p = path
.split('/')
.filter(|v| !v.is_empty())
.collect::<Vec<&str>>()
.join("/");
if has_trailing {
p.push('/');
}
p
}
pub fn normalize_root(v: &str) -> String {
let mut v = v
.split('/')
.filter(|v| !v.is_empty())
.collect::<Vec<&str>>()
.join("/");
if !v.starts_with('/') {
v.insert(0, '/');
}
if !v.ends_with('/') {
v.push('/')
}
v
}
pub fn get_basename(path: &str) -> &str {
if path == "/" {
return "/";
}
if !path.ends_with('/') {
return path
.split('/')
.next_back()
.expect("file path without name is invalid");
}
let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
match idx {
Some(v) => {
let (_, name) = path.split_at(v);
name
}
None => path,
}
}
pub fn get_parent(path: &str) -> &str {
if path == "/" {
return "/";
}
if !path.ends_with('/') {
let idx = path.rfind('/');
return match idx {
Some(v) => {
let (parent, _) = path.split_at(v + 1);
parent
}
None => "/",
};
}
let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
match idx {
Some(v) => {
let (parent, _) = path.split_at(v);
parent
}
None => "/",
}
}
const RANDOM_TMP_PATH_POSTFIX_LENGTH: usize = 8;
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const CHARS_LENGTH: u64 = CHARS.len() as u64;
#[inline]
pub fn build_tmp_path_of(path: &str) -> String {
let name = get_basename(path);
let mut buf = String::with_capacity(name.len() + RANDOM_TMP_PATH_POSTFIX_LENGTH);
buf.push_str(name);
buf.push('.');
for _ in 0..RANDOM_TMP_PATH_POSTFIX_LENGTH {
let random = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish();
let choice: usize = (random % CHARS_LENGTH).try_into().unwrap();
buf.push(CHARS[choice] as char);
}
buf
}
#[inline]
pub fn validate_path(path: &str, mode: EntryMode) -> bool {
debug_assert!(!path.is_empty(), "input path should not be empty");
match mode {
EntryMode::FILE => !path.ends_with('/'),
EntryMode::DIR => path.ends_with('/'),
EntryMode::Unknown => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
let cases = vec![
("file path", "abc", "abc"),
("dir path", "abc/", "abc/"),
("empty path", "", "/"),
("root path", "/", "/"),
("root path with extra /", "///", "/"),
("abs file path", "/abc/def", "abc/def"),
("abs dir path", "/abc/def/", "abc/def/"),
("abs file path with extra /", "///abc/def", "abc/def"),
("abs dir path with extra /", "///abc/def/", "abc/def/"),
("file path contains ///", "abc///def", "abc/def"),
("dir path contains ///", "abc///def///", "abc/def/"),
("file with whitespace", "abc/def ", "abc/def"),
];
for (name, input, expect) in cases {
assert_eq!(normalize_path(input), expect, "{name}")
}
}
#[test]
fn test_normalize_root() {
let cases = vec![
("dir path", "abc/", "/abc/"),
("empty path", "", "/"),
("root path", "/", "/"),
("root path with extra /", "///", "/"),
("abs dir path", "/abc/def/", "/abc/def/"),
("abs file path with extra /", "///abc/def", "/abc/def/"),
("abs dir path with extra /", "///abc/def/", "/abc/def/"),
("dir path contains ///", "abc///def///", "/abc/def/"),
];
for (name, input, expect) in cases {
assert_eq!(normalize_root(input), expect, "{name}")
}
}
#[test]
fn test_get_basename() {
let cases = vec![
("file abs path", "foo/bar/baz.txt", "baz.txt"),
("file rel path", "bar/baz.txt", "baz.txt"),
("file walk", "foo/bar/baz", "baz"),
("dir rel path", "bar/baz/", "baz/"),
("dir root", "/", "/"),
("dir walk", "foo/bar/baz/", "baz/"),
];
for (name, input, expect) in cases {
let actual = get_basename(input);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_get_parent() {
let cases = vec![
("file abs path", "foo/bar/baz.txt", "foo/bar/"),
("file rel path", "bar/baz.txt", "bar/"),
("file walk", "foo/bar/baz", "foo/bar/"),
("dir rel path", "bar/baz/", "bar/"),
("dir root", "/", "/"),
("dir abs path", "/foo/bar/", "/foo/"),
("dir walk", "foo/bar/baz/", "foo/bar/"),
];
for (name, input, expect) in cases {
let actual = get_parent(input);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_build_abs_path() {
let cases = vec![
("input abs file", "/abc/", "/", "abc/"),
("input dir", "/abc/", "def/", "abc/def/"),
("input file", "/abc/", "def", "abc/def"),
("input abs file with root /", "/", "/", ""),
("input empty with root /", "/", "", ""),
("input dir with root /", "/", "def/", "def/"),
("input file with root /", "/", "def", "def"),
];
for (name, root, input, expect) in cases {
let actual = build_abs_path(root, input);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_build_rooted_abs_path() {
let cases = vec![
("input abs file", "/abc/", "/", "/abc/"),
("input dir", "/abc/", "def/", "/abc/def/"),
("input file", "/abc/", "def", "/abc/def"),
("input abs file with root /", "/", "/", "/"),
("input dir with root /", "/", "def/", "/def/"),
("input file with root /", "/", "def", "/def"),
];
for (name, root, input, expect) in cases {
let actual = build_rooted_abs_path(root, input);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_build_rel_path() {
let cases = vec![
("input abs file", "/abc/", "/abc/def", "def"),
("input dir", "/abc/", "/abc/def/", "def/"),
("input file", "/abc/", "abc/def", "def"),
("input dir with root /", "/", "def/", "def/"),
("input file with root /", "/", "def", "def"),
];
for (name, root, input, expect) in cases {
let actual = build_rel_path(root, input);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_validate_path() {
let cases = vec![
("input file with mode file", "abc", EntryMode::FILE, true),
("input file with mode dir", "abc", EntryMode::DIR, false),
("input dir with mode file", "abc/", EntryMode::FILE, false),
("input dir with mode dir", "abc/", EntryMode::DIR, true),
("root with mode dir", "/", EntryMode::DIR, true),
(
"input file with mode unknown",
"abc",
EntryMode::Unknown,
false,
),
(
"input dir with mode unknown",
"abc/",
EntryMode::Unknown,
false,
),
];
for (name, path, mode, expect) in cases {
let actual = validate_path(path, mode);
assert_eq!(actual, expect, "{name}")
}
}
#[test]
fn test_build_tmp_path_of() {
let cases = vec![
("a file path", "example.txt", "example.txt."),
(
"a file path in a directory",
"folder/example.txt",
"example.txt.",
),
];
for (name, path, expect_starts_with) in cases {
let actual = build_tmp_path_of(path);
assert!(
actual.starts_with(expect_starts_with),
"{name}: got `{actual}`, but expect `{expect_starts_with}`"
);
assert_eq!(
actual.len(),
expect_starts_with.len() + 8, "{name}: got `{actual}`, but expect `{expect_starts_with}`"
)
}
}
}