use smol_str::{format_smolstr, SmolStr, SmolStrBuilder};
use std::path::{Path, PathBuf};
pub fn is_url(path: &Path) -> bool {
let Some(path) = path.to_str() else {
return false;
};
to_url(path).is_some()
}
fn to_url(path: &str) -> Option<url::Url> {
let Ok(url) = url::Url::parse(path) else {
return None;
};
if url.scheme().len() == 1 {
None
} else {
Some(url)
}
}
#[test]
fn test_to_url() {
#[track_caller]
fn th(input: &str, expected: bool) {
assert_eq!(to_url(input).is_some(), expected);
}
th("https://foo.bar/", true);
th("builtin:/foo/bar/", true);
th("user://foo/bar.rs", true);
th("/foo/bar/", false);
th("../foo/bar", false);
th("foo/bar", false);
th("./foo/bar", false);
th("C:\\Documents\\Newsletters\\Summer2018.pdf", false);
th("\\Program Files\\Custom Utilities\\StringFinder.exe", false);
th("2018\\January.xlsx", false);
th("..\\Publications\\TravelBrochure.pdf", false);
th("C:\\Projects\\library\\library.sln", false);
th("C:Projects\\library\\library.sln", false);
th("\\\\system07\\C$\\", false);
th("\\\\Server2\\Share\\Test\\Foo.txt", false);
th("\\\\.\\C:\\Test\\Foo.txt", false);
th("\\\\?\\C:\\Test\\Foo.txt", false);
th("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", false);
th("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", false);
th("C:/Documents/Newsletters/Summer2018.pdf", false);
th("/Program Files/Custom Utilities/StringFinder.exe", false);
th("2018/January.xlsx", false);
th("../Publications/TravelBrochure.pdf", false);
th("C:/Projects/library/library.sln", false);
th("C:Projects/library/library.sln", false);
th("//system07/C$/", false);
th("//Server2/Share/Test/Foo.txt", false);
th("//./C:/Test/Foo.txt", false);
th("//?/C:/Test/Foo.txt", false);
th("//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", false);
th("//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", false);
th("C:///Documents/Newsletters/Summer2018.pdf", false);
th("/http://foo/bar/", false);
th("../http://foo/bar", false);
th("foo/http://foo/bar", false);
th("./http://foo/bar", false);
th("", false);
}
pub fn is_absolute(path: &Path) -> bool {
let Some(path) = path.to_str() else {
return false;
};
if to_url(path).is_some() {
return true;
}
matches!(components(path, 0, &None), Some((PathComponent::Root(_), _, _)))
}
#[test]
fn test_is_absolute() {
#[track_caller]
fn th(input: &str, expected: bool) {
let path = PathBuf::from(input);
assert_eq!(is_absolute(&path), expected);
}
th("https://foo.bar/", true);
th("builtin:/foo/bar/", true);
th("user://foo/bar.rs", true);
th("/foo/bar/", true);
th("../foo/bar", false);
th("foo/bar", false);
th("./foo/bar", false);
th("C:\\Documents\\Newsletters\\Summer2018.pdf", true);
th("\\Program Files\\Custom Utilities\\StringFinder.exe", true);
th("2018\\January.xlsx", false);
th("..\\Publications\\TravelBrochure.pdf", false);
th("C:\\Projects\\library\\library.sln", true);
th("C:Projects\\library\\library.sln", false);
th("\\\\system07\\C$\\", true);
th("\\\\Server2\\Share\\Test\\Foo.txt", true);
th("\\\\.\\C:\\Test\\Foo.txt", true);
th("\\\\?\\C:\\Test\\Foo.txt", true);
th("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", true);
th("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt", true);
th("C:/Documents/Newsletters/Summer2018.pdf", true);
th("/Program Files/Custom Utilities/StringFinder.exe", true);
th("2018/January.xlsx", false);
th("../Publications/TravelBrochure.pdf", false);
th("C:/Projects/library/library.sln", true);
th("C:Projects/library/library.sln", false);
th("//system07/C$/", true);
th("//Server2/Share/Test/Foo.txt", true);
th("//./C:/Test/Foo.txt", true);
th("//?/C:/Test/Foo.txt", true);
th("//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", true);
th("//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt", true);
th("C:///Documents/Newsletters/Summer2018.pdf", true);
th("C:", false);
th("C:\\", true);
th("C:/", true);
th("", false);
}
#[derive(Debug, PartialEq)]
enum PathComponent<'a> {
Root(&'a str),
Empty,
SameDirectory(&'a str),
ParentDirectory(&'a str),
Directory(&'a str),
File(&'a str),
}
fn find_path_separator(path: &str) -> char {
for c in path.chars() {
if c == '/' || c == '\\' {
return c;
}
}
'/'
}
fn components<'a>(
path: &'a str,
offset: usize,
separator: &Option<char>,
) -> Option<(PathComponent<'a>, usize, char)> {
use PathComponent as PC;
if offset >= path.len() {
return None;
}
let b = path.as_bytes();
if offset == 0 {
if b.len() >= 3
&& b[0].is_ascii_alphabetic()
&& b[1] == b':'
&& (b[2] == b'\\' || b[2] == b'/')
{
return Some((PC::Root(&path[0..3]), 3, b[2] as char));
}
if b.len() >= 2 && b[0] == b'\\' && b[1] == b'\\' {
let second_bs = path[2..]
.find('\\')
.map(|pos| pos + 2 + 1)
.and_then(|pos1| path[pos1..].find('\\').map(|pos2| pos2 + pos1));
if let Some(end_offset) = second_bs {
return Some((PC::Root(&path[0..=end_offset]), end_offset + 1, '\\'));
}
}
if b[0] == b'/' || b[0] == b'\\' {
return Some((PC::Root(&path[0..1]), 1, b[0] as char));
}
}
let separator = separator.unwrap_or_else(|| find_path_separator(path));
let next_component = path[offset..].find(separator).map(|p| p + offset).unwrap_or(path.len());
if &path[offset..next_component] == "." {
return Some((
PC::SameDirectory(&path[offset..next_component]),
next_component + 1,
separator,
));
}
if &path[offset..next_component] == ".." {
return Some((
PC::ParentDirectory(&path[offset..next_component]),
next_component + 1,
separator,
));
}
if next_component == path.len() {
Some((PC::File(&path[offset..next_component]), next_component, separator))
} else if next_component == offset {
Some((PC::Empty, next_component + 1, separator))
} else {
Some((PC::Directory(&path[offset..next_component]), next_component + 1, separator))
}
}
#[test]
fn test_components() {
use PathComponent as PC;
#[track_caller]
fn th(input: &str, expected: Option<(PathComponent, usize, char)>) {
assert_eq!(components(input, 0, &None), expected);
}
th("/foo/bar/", Some((PC::Root("/"), 1, '/')));
th("../foo/bar", Some((PC::ParentDirectory(".."), 3, '/')));
th("foo/bar", Some((PC::Directory("foo"), 4, '/')));
th("./foo/bar", Some((PC::SameDirectory("."), 2, '/')));
th("C:\\Documents\\Newsletters\\Summer2018.pdf", Some((PC::Root("C:\\"), 3, '\\')));
th("\\Program Files\\Custom Utilities\\StringFinder.exe", Some((PC::Root("\\"), 1, '\\')));
th("2018\\January.xlsx", Some((PC::Directory("2018"), 5, '\\')));
th("..\\Publications\\TravelBrochure.pdf", Some((PC::ParentDirectory(".."), 3, '\\')));
th("C:Projects\\library\\library.sln", Some((PC::Directory("C:Projects"), 11, '\\')));
th("\\\\system07\\C$\\", Some((PC::Root("\\\\system07\\C$\\"), 14, '\\')));
th("\\\\Server2\\Share\\Test\\Foo.txt", Some((PC::Root("\\\\Server2\\Share\\"), 16, '\\')));
th("\\\\.\\C:\\Test\\Foo.txt", Some((PC::Root("\\\\.\\C:\\"), 7, '\\')));
th("\\\\?\\C:\\Test\\Foo.txt", Some((PC::Root("\\\\?\\C:\\"), 7, '\\')));
th(
"\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt",
Some((PC::Root("\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\"), 49, '\\')),
);
th(
"\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt",
Some((PC::Root("\\\\?\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\"), 49, '\\')),
);
th("C:/Documents/Newsletters/Summer2018.pdf", Some((PC::Root("C:/"), 3, '/')));
th("/Program Files/Custom Utilities/StringFinder.exe", Some((PC::Root("/"), 1, '/')));
th("//system07/C$/", Some((PC::Root("/"), 1, '/')));
th("//Server2/Share/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
th("//./C:/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
th("//?/C:/Test/Foo.txt", Some((PC::Root("/"), 1, '/')));
th(
"//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt",
Some((PC::Root("/"), 1, '/')),
);
th(
"//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt",
Some((PC::Root("/"), 1, '/')),
);
th("C:", Some((PC::File("C:"), 2, '/')));
th("foo", Some((PC::File("foo"), 3, '/')));
th("foo/", Some((PC::Directory("foo"), 4, '/')));
th("foo\\", Some((PC::Directory("foo"), 4, '\\')));
th("", None);
}
struct Components<'a> {
path: &'a str,
offset: usize,
separator: Option<char>,
}
fn component_iter(path: &str) -> Components<'_> {
Components { path, offset: 0, separator: None }
}
impl<'a> Iterator for Components<'a> {
type Item = PathComponent<'a>;
fn next(&mut self) -> Option<Self::Item> {
let (result, new_offset, separator) = components(self.path, self.offset, &self.separator)?;
self.offset = new_offset;
self.separator = Some(separator);
Some(result)
}
}
fn clean_path_string(path: &str) -> SmolStr {
use PathComponent as PC;
let separator = find_path_separator(path);
let path = if separator == '\\' {
path.replace('/', &format!("{separator}"))
} else {
path.replace('\\', "/")
};
let mut clean_components = Vec::new();
for component in component_iter(&path) {
match component {
PC::Root(v) => {
clean_components = vec![PC::Root(v)];
}
PC::Empty | PC::SameDirectory(_) => { }
PC::ParentDirectory(v) => {
match clean_components.last() {
Some(PC::Directory(_)) => {
clean_components.pop();
}
Some(PC::File(_)) => unreachable!("Must be the last component"),
Some(PC::SameDirectory(_) | PC::Empty) => {
unreachable!("Will never be in a the vector")
}
Some(PC::ParentDirectory(_)) => {
clean_components.push(PC::ParentDirectory(v));
}
Some(PC::Root(_)) => { }
None => {
clean_components.push(PC::ParentDirectory(v));
}
};
}
PC::Directory(v) => clean_components.push(PC::Directory(v)),
PC::File(v) => clean_components.push(PC::File(v)),
}
}
if clean_components.is_empty() {
SmolStr::new_static(".")
} else {
let mut result = SmolStrBuilder::default();
for c in clean_components {
match c {
PC::Root(v) => {
result.push_str(v);
}
PC::Empty | PC::SameDirectory(_) => {
unreachable!("Never in the vector!")
}
PC::ParentDirectory(v) => {
result.push_str(&format_smolstr!("{v}{separator}"));
}
PC::Directory(v) => result.push_str(&format_smolstr!("{v}{separator}")),
PC::File(v) => {
result.push_str(v);
}
}
}
result.finish()
}
}
#[test]
fn test_clean_path_string() {
#[track_caller]
fn th(input: &str, expected: &str) {
let result = clean_path_string(input);
assert_eq!(result, expected);
}
th("../../ab/.././hello.txt", "../../hello.txt");
th("/../../ab/.././hello.txt", "/hello.txt");
th("ab/.././cb/././///./..", ".");
th("ab/.././cb/.\\.\\\\\\\\./..", ".");
th("ab\\..\\.\\cb\\././///./..", ".");
}
pub fn clean_path(path: &Path) -> PathBuf {
let Some(path_str) = path.to_str() else {
return path.to_owned();
};
if let Some(url) = to_url(path_str) {
PathBuf::from(url.to_string())
} else {
PathBuf::from(clean_path_string(path_str).to_string())
}
}
fn dirname_string(path: &str) -> String {
let separator = find_path_separator(path);
let mut result = String::new();
for component in component_iter(path) {
match component {
PathComponent::Root(v) => result = v.to_string(),
PathComponent::Empty => result.push(separator),
PathComponent::SameDirectory(v)
| PathComponent::ParentDirectory(v)
| PathComponent::Directory(v) => result += &format!("{v}{separator}"),
PathComponent::File(_) => { }
};
}
if result.is_empty() {
String::from(".")
} else {
result
}
}
#[test]
fn test_dirname() {
#[track_caller]
fn th(input: &str, expected: &str) {
let result = dirname_string(input);
assert_eq!(result, expected);
}
th("/../../ab/.././", "/../../ab/.././");
th("ab/.././cb/./././..", "ab/.././cb/./././../");
th("hello.txt", ".");
th("../hello.txt", "../");
th("/hello.txt", "/");
}
pub fn dirname(path: &Path) -> PathBuf {
let Some(path_str) = path.to_str() else {
return path.to_owned();
};
PathBuf::from(dirname_string(path_str))
}
pub fn join(base: &Path, path: &Path) -> Option<PathBuf> {
if is_absolute(path) {
return Some(path.to_owned());
}
let Some(base_str) = base.to_str() else {
return Some(path.to_owned());
};
let Some(path_str) = path.to_str() else {
return Some(path.to_owned());
};
let path_separator = find_path_separator(path_str);
if let Some(mut base_url) = to_url(base_str) {
let path_str = if path_separator != '/' {
path_str.replace(path_separator, "/")
} else {
path_str.to_string()
};
let base_path = base_url.path();
if !base_path.is_empty() && !base_path.ends_with('/') {
base_url.set_path(&format_smolstr!("{base_path}/"));
}
Some(PathBuf::from(base_url.join(&path_str).ok()?.to_string()))
} else {
let base_separator = find_path_separator(base_str);
let path_str = if path_separator != base_separator {
path_str.replace(path_separator, &base_separator.to_string())
} else {
path_str.to_string()
};
let joined = clean_path_string(&format_smolstr!("{base_str}{base_separator}{path_str}"));
Some(PathBuf::from(joined.to_string()))
}
}
#[test]
fn test_join() {
#[track_caller]
fn th(base: &str, path: &str, expected: Option<&str>) {
let base = PathBuf::from(base);
let path = PathBuf::from(path);
let expected = expected.map(|e| PathBuf::from(e));
let result = join(&base, &path);
assert_eq!(result, expected);
}
th("https://slint.dev/", "/hello.txt", Some("/hello.txt"));
th("https://slint.dev/", "../../hello.txt", Some("https://slint.dev/hello.txt"));
th("/../../ab/.././", "hello.txt", Some("/hello.txt"));
th("ab/.././cb/./././..", "../.././hello.txt", Some("../../hello.txt"));
th("builtin:/foo", "..\\bar.slint", Some("builtin:/bar.slint"));
th("builtin:/", "..\\bar.slint", Some("builtin:/bar.slint"));
th("builtin:/foo/baz", "..\\bar.slint", Some("builtin:/foo/bar.slint"));
th("builtin:/foo", "bar.slint", Some("builtin:/foo/bar.slint"));
th("builtin:/foo/", "bar.slint", Some("builtin:/foo/bar.slint"));
th("builtin:/", "..\\bar.slint", Some("builtin:/bar.slint"));
}