use std::path::Path;
pub const SEARCH_PATH_SEPARATOR: &str = ":";
pub fn split_search_path(path: &str) -> Vec<&str> {
path.split(':').collect()
}
pub fn is_absolute(p: &str) -> bool {
#[cfg(windows)]
{
isabs_nt(p)
}
#[cfg(not(windows))]
{
p.starts_with('/') || Path::new(p).is_absolute()
}
}
pub fn join(a: &str, b: &str) -> String {
#[cfg(windows)]
{
join_nt(a, b)
}
#[cfg(not(windows))]
{
join_posix(a, b)
}
}
pub fn normpath(p: &str) -> String {
#[cfg(windows)]
{
normpath_nt(p)
}
#[cfg(not(windows))]
{
normpath_posix(p)
}
}
#[cfg_attr(windows, allow(dead_code))]
fn join_posix(a: &str, b: &str) -> String {
if b.is_empty() {
return a.to_string();
}
if a.is_empty() || b.starts_with('/') || Path::new(b).is_absolute() {
return b.to_string();
}
if a.ends_with('/') || a.ends_with('\\') {
format!("{a}{b}")
} else {
format!("{a}/{b}")
}
}
pub fn normpath_posix(p: &str) -> String {
if p.is_empty() {
return ".".to_string();
}
let p = p.replace('\\', "/");
let absolute = p.starts_with('/');
let mut prefix = String::new();
let mut rest = p.as_str();
if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
prefix = rest[..2].to_string();
rest = &rest[2..];
}
let mut parts: Vec<&str> = Vec::new();
for comp in rest.split('/') {
match comp {
"" | "." => {}
".." => {
if let Some(last) = parts.last() {
if *last != ".." {
parts.pop();
continue;
}
}
if !absolute {
parts.push("..");
}
}
c => parts.push(c),
}
}
let body = parts.join("/");
let mut out = prefix;
if absolute || rest.starts_with('/') {
out.push('/');
}
out.push_str(&body);
if out.is_empty() {
".".to_string()
} else {
out
}
}
fn splitdrive_nt(p: &str) -> (&str, &str) {
if p.len() >= 2 && p.as_bytes()[1] == b':' && p.is_char_boundary(2) {
(&p[..2], &p[2..])
} else {
("", p)
}
}
#[cfg_attr(not(windows), allow(dead_code))]
fn isabs_nt(p: &str) -> bool {
let (_, path) = splitdrive_nt(p);
path.starts_with('/') || path.starts_with('\\')
}
#[cfg_attr(not(windows), allow(dead_code))]
fn join_nt(a: &str, b: &str) -> String {
let mut path = a.to_string();
let mut b_nts = false;
if path.is_empty() {
b_nts = true;
} else if isabs_nt(b) {
let pb = path.as_bytes();
let bb = b.as_bytes();
if (pb.len() >= 2 && pb[1] != b':') || (bb.len() >= 2 && bb[1] == b':') {
b_nts = true;
} else if path.len() > 3
|| (path.len() == 3 && !path.ends_with('/') && !path.ends_with('\\'))
{
b_nts = true;
}
}
if b_nts {
return b.to_string();
}
let b_sep = b.starts_with('/') || b.starts_with('\\');
if path.ends_with('/') || path.ends_with('\\') {
path.push_str(if b_sep { &b[1..] } else { b });
} else if path.ends_with(':') {
path.push_str(b);
} else if !b.is_empty() {
if !b_sep {
path.push('\\');
}
path.push_str(b);
} else {
path.push('\\');
}
path
}
#[cfg_attr(not(windows), allow(dead_code))]
fn normpath_nt(p: &str) -> String {
let replaced = p.replace('/', "\\");
let (drive, mut path) = splitdrive_nt(&replaced);
let mut prefix = drive.to_string();
if prefix.is_empty() {
while let Some(rest) = path.strip_prefix('\\') {
prefix.push('\\');
path = rest;
}
} else if path.starts_with('\\') {
prefix.push('\\');
path = path.trim_start_matches('\\');
}
let mut comps: Vec<&str> = path.split('\\').collect();
let mut i = 0;
while i < comps.len() {
if comps[i].is_empty() || comps[i] == "." {
comps.remove(i);
} else if comps[i] == ".." {
if i > 0 && comps[i - 1] != ".." {
comps.drain(i - 1..=i);
i -= 1;
} else if i == 0 && prefix.ends_with('\\') {
comps.remove(i);
} else {
i += 1;
}
} else {
i += 1;
}
}
if prefix.is_empty() && comps.is_empty() {
comps.push(".");
}
prefix + &comps.join("\\")
}
pub fn file_exists(p: &str) -> bool {
Path::new(p).is_file()
}
pub fn dirname(p: &str) -> String {
match p.rfind(['/', '\\']) {
Some(0) => "/".to_string(),
Some(i) => p[..i].to_string(),
None => String::new(),
}
}
pub fn basename(p: &str) -> String {
match p.rfind(['/', '\\']) {
Some(i) => p[i + 1..].to_string(),
None => p.to_string(),
}
}
pub fn extension(p: &str) -> String {
let b = basename(p);
match b.rfind('.') {
Some(i) if i > 0 || b.len() > 1 => b[i + 1..].to_ascii_lowercase(),
_ => String::new(),
}
}
pub fn absolute(p: &str) -> String {
if is_absolute(p) {
return normpath(p);
}
match std::env::current_dir() {
Ok(cwd) => normpath(&join(&cwd.to_string_lossy(), p)),
Err(_) => normpath(p),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn norm() {
assert_eq!(normpath_posix("/a//b/./c/../d"), "/a/b/d");
assert_eq!(normpath_posix("a/../../b"), "../b");
assert_eq!(normpath_posix("./"), ".");
assert_eq!(join_posix("/a/b", "c.lut"), "/a/b/c.lut");
assert_eq!(join_posix("/a/b", "/c.lut"), "/c.lut");
assert_eq!(extension("/x/y/lut.SPI1D"), "spi1d");
assert_eq!(dirname("/x/y/lut.spi1d"), "/x/y");
assert_eq!(split_search_path(".:luts"), vec![".", "luts"]);
}
#[test]
fn isabs_windows() {
assert!(isabs_nt("C:/luts/a.cube"));
assert!(isabs_nt("C:\\luts\\a.cube"));
assert!(isabs_nt("\\\\server\\share\\a.cube"));
assert!(isabs_nt("/luts/a.cube"));
assert!(!isabs_nt("C:lut.cube"));
assert!(!isabs_nt("C:"));
assert!(!isabs_nt("luts/a.cube"));
}
#[test]
fn join_windows() {
assert_eq!(join_nt("C:\\a", "b.cube"), "C:\\a\\b.cube");
assert_eq!(join_nt("C:\\a\\", "b.cube"), "C:\\a\\b.cube");
assert_eq!(join_nt("C:/a/", "/b"), "/b");
assert_eq!(join_nt("c:", "/a"), "c:/a");
assert_eq!(join_nt("c:/", "/a"), "c:/a");
assert_eq!(join_nt("c:/a", "/b"), "/b");
assert_eq!(join_nt("c:", "d:/"), "d:/");
assert_eq!(join_nt("c:/", "d:/"), "d:/");
assert_eq!(join_nt("C:", "lut.cube"), "C:lut.cube");
assert_eq!(join_nt("", "lut.cube"), "lut.cube");
assert_eq!(join_nt("a", ""), "a\\");
}
#[test]
fn normpath_windows() {
assert_eq!(
normpath_nt("D:\\a\\ocio-rs/tests/./data//files/../x.spimtx"),
"D:\\a\\ocio-rs\\tests\\data\\x.spimtx"
);
assert_eq!(
normpath_nt("\\\\server\\share/luts/../a.cube"),
"\\\\server\\share\\a.cube"
);
assert_eq!(
normpath_nt("//server/share/a.cube"),
"\\\\server\\share\\a.cube"
);
assert_eq!(normpath_nt("C://a"), "C:\\a");
assert_eq!(normpath_nt("C:\\..\\a"), "C:\\a");
assert_eq!(normpath_nt("a/../../b"), "..\\b");
assert_eq!(normpath_nt("./"), ".");
assert_eq!(normpath_nt("C:"), "C:");
}
}