use crate::types::string;
const SEP: char = std::path::MAIN_SEPARATOR;
use super::Joinable;
#[allow(non_snake_case)]
pub fn Join(parts: impl Joinable) -> string {
let joined: Vec<&str> = parts.__join_parts().into_iter().filter(|s| !s.is_empty()).collect();
if joined.is_empty() {
return "".into();
}
let combined = joined.join(&SEP.to_string());
Clean(combined)
}
#[allow(non_snake_case)]
pub fn Base(path: impl AsRef<str>) -> string {
let s = path.as_ref();
if s.is_empty() {
return ".".into();
}
let trimmed = s.trim_end_matches(SEP);
if trimmed.is_empty() {
return SEP.to_string().into();
}
match trimmed.rsplit_once(SEP) {
Some((_, tail)) => tail.into(),
None => trimmed.into(),
}
}
#[allow(non_snake_case)]
pub fn Dir(path: impl AsRef<str>) -> string {
let s = path.as_ref();
let trimmed = s.trim_end_matches(SEP);
match trimmed.rsplit_once(SEP) {
Some((head, _)) => {
if head.is_empty() {
SEP.to_string().into()
} else {
Clean(head.to_string())
}
}
None => ".".into(),
}
}
#[allow(non_snake_case)]
pub fn Ext(path: impl AsRef<str>) -> string {
let s = path.as_ref();
let base = Base(s);
match base.rfind('.') {
Some(i) if i > 0 => base[i..].into(),
_ => "".into(),
}
}
#[allow(non_snake_case)]
pub fn Clean(path: impl Into<String>) -> string {
let s: String = path.into();
if s.is_empty() {
return ".".into();
}
let absolute = s.starts_with(SEP);
let mut stack: Vec<&str> = Vec::new();
for part in s.split(SEP) {
match part {
"" | "." => continue,
".." => {
if stack.last().map_or(false, |t| *t != "..") && !stack.is_empty() {
stack.pop();
} else if !absolute {
stack.push("..");
}
}
other => stack.push(other),
}
}
let joined = stack.join(&SEP.to_string());
if absolute {
format!("{}{}", SEP, joined).into()
} else if joined.is_empty() {
".".into()
} else {
joined.into()
}
}
#[allow(non_snake_case)]
pub fn Split(path: impl AsRef<str>) -> (string, string) {
let s = path.as_ref();
match s.rfind(SEP) {
Some(i) => (s[..=i].into(), s[i+1..].into()),
None => ("".into(), s.into()),
}
}
#[allow(non_snake_case)]
pub fn SplitList(path: impl AsRef<str>) -> crate::types::slice<string> {
let s = path.as_ref();
if s.is_empty() { return crate::types::slice::new(); }
let list_sep = if cfg!(windows) { ';' } else { ':' };
s.split(list_sep).map(|s| s.into()).collect()
}
#[allow(non_snake_case)]
pub fn IsAbs(path: impl AsRef<str>) -> bool {
path.as_ref().starts_with(SEP)
}
#[allow(non_snake_case)]
pub fn FromSlash(path: impl AsRef<str>) -> string {
if SEP == '/' { path.as_ref().into() }
else { path.as_ref().replace('/', &SEP.to_string()).into() }
}
#[allow(non_snake_case)]
pub fn ToSlash(path: impl AsRef<str>) -> string {
if SEP == '/' { path.as_ref().into() }
else { path.as_ref().replace(SEP, "/").into() }
}
#[allow(non_snake_case)]
pub fn IsLocal(path: impl AsRef<str>) -> bool {
let s = path.as_ref();
if s.is_empty() { return false; }
if IsAbs(s) { return false; }
let mut depth: i64 = 0;
for part in s.split(SEP) {
match part {
"" | "." => {}
".." => {
depth -= 1;
if depth < 0 { return false; }
}
_ => { depth += 1; }
}
}
true
}
#[allow(non_snake_case)]
pub fn Match(pattern: impl AsRef<str>, name: impl AsRef<str>) -> (bool, crate::errors::error) {
let p = pattern.as_ref();
let n = name.as_ref();
match glob_match(p, n) {
Ok(m) => (m, crate::errors::nil),
Err(e) => (false, crate::errors::New(e)),
}
}
fn glob_match(mut pattern: &str, mut name: &str) -> Result<bool, &'static str> {
'outer: loop {
let mut star = false;
while !pattern.is_empty() && pattern.starts_with('*') {
pattern = &pattern[1..];
star = true;
}
let (chunk, rest) = scan_chunk_no_leading_star(pattern);
pattern = rest;
if chunk.is_empty() {
if star {
return Ok(!name.contains(SEP));
}
return Ok(name.is_empty());
}
if !star {
match match_chunk(chunk, name)? {
Some(rest_name) => { name = rest_name; continue 'outer; }
None => return Ok(false),
}
}
let mut i = 0;
loop {
match match_chunk(chunk, &name[i..])? {
Some(rest_name) => {
if pattern.is_empty() && !rest_name.is_empty() {
} else {
name = rest_name;
continue 'outer;
}
}
None => {}
}
if i >= name.len() { break; }
let first = name[i..].chars().next().unwrap();
if first == SEP { break; }
i += first.len_utf8();
}
return Ok(false);
}
}
fn scan_chunk_no_leading_star(pattern: &str) -> (&str, &str) {
let bytes = pattern.as_bytes();
let mut i = 0;
let mut in_range = false;
while i < bytes.len() {
match bytes[i] {
b'\\' if !cfg!(windows) => {
i += 1;
if i < bytes.len() { i += 1; }
continue;
}
b'[' => { in_range = true; i += 1; }
b']' => { in_range = false; i += 1; }
b'*' if !in_range => break,
_ => { i += 1; }
}
}
(&pattern[..i], &pattern[i..])
}
fn match_chunk<'a>(chunk: &str, name: &'a str) -> Result<Option<&'a str>, &'static str> {
let chunk_bytes = chunk.as_bytes();
let mut pi = 0;
let mut failed = false;
let mut rest_name: &'a str = name;
while pi < chunk_bytes.len() {
let c = chunk_bytes[pi];
match c {
b'[' => {
let mut nc: char = '\0';
let mut have_nc = false;
if !failed && !rest_name.is_empty() {
nc = rest_name.chars().next().unwrap();
rest_name = &rest_name[nc.len_utf8()..];
have_nc = true;
} else {
failed = true;
}
pi += 1;
let mut negate = false;
if pi < chunk_bytes.len() && (chunk_bytes[pi] == b'^' || chunk_bytes[pi] == b'!') {
negate = true;
pi += 1;
}
let mut matched = false;
let mut nrange = 0;
loop {
if pi < chunk_bytes.len() && chunk_bytes[pi] == b']' && nrange > 0 {
pi += 1;
break;
}
if pi >= chunk_bytes.len() {
return Err("syntax error in pattern");
}
let (lo, np) = get_esc(chunk_bytes, pi)?;
pi = np;
let hi = if pi < chunk_bytes.len() && chunk_bytes[pi] == b'-' {
pi += 1;
let (h, np2) = get_esc(chunk_bytes, pi)?;
pi = np2;
h
} else { lo };
if have_nc && lo <= nc && nc <= hi { matched = true; }
nrange += 1;
}
if matched == negate { failed = true; }
}
b'?' => {
if !failed && !rest_name.is_empty() {
let nc = rest_name.chars().next().unwrap();
if nc == SEP { failed = true; }
else { rest_name = &rest_name[nc.len_utf8()..]; }
} else { failed = true; }
pi += 1;
}
b'\\' if !cfg!(windows) => {
pi += 1;
if pi >= chunk_bytes.len() { return Err("syntax error in pattern"); }
if !failed && !rest_name.is_empty() {
let nc = rest_name.chars().next().unwrap();
if chunk_bytes[pi] as char != nc { failed = true; }
else { rest_name = &rest_name[nc.len_utf8()..]; }
} else { failed = true; }
pi += 1;
}
_ => {
if !failed && !rest_name.is_empty() {
let nc = rest_name.chars().next().unwrap();
if (c as char) != nc { failed = true; }
else { rest_name = &rest_name[nc.len_utf8()..]; }
} else { failed = true; }
pi += 1;
}
}
}
if failed { Ok(None) } else { Ok(Some(rest_name)) }
}
fn get_esc(chunk: &[u8], pi: usize) -> Result<(char, usize), &'static str> {
if pi >= chunk.len() || chunk[pi] == b']' {
return Err("syntax error in pattern");
}
if chunk[pi] == b'\\' && !cfg!(windows) {
if pi + 1 >= chunk.len() { return Err("syntax error in pattern"); }
return Ok((chunk[pi + 1] as char, pi + 2));
}
let s = std::str::from_utf8(&chunk[pi..]).unwrap_or("");
let c = s.chars().next().ok_or("syntax error in pattern")?;
Ok((c, pi + c.len_utf8()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_basic() {
assert_eq!(Join(&["a", "b", "c"]), "a/b/c");
assert_eq!(Join(&["/a", "b"]), "/a/b");
assert_eq!(Join(&["a", "", "b"]), "a/b");
}
#[test]
fn base_cases() {
assert_eq!(Base("/a/b/c.txt"), "c.txt");
assert_eq!(Base("c.txt"), "c.txt");
assert_eq!(Base("/"), "/");
assert_eq!(Base(""), ".");
}
#[test]
fn dir_cases() {
assert_eq!(Dir("/a/b/c.txt"), "/a/b");
assert_eq!(Dir("c.txt"), ".");
assert_eq!(Dir("/a"), "/");
}
#[test]
fn ext_cases() {
assert_eq!(Ext("a/b.txt"), ".txt");
assert_eq!(Ext("name.tar.gz"), ".gz");
assert_eq!(Ext("noext"), "");
assert_eq!(Ext(".hidden"), ""); }
#[test]
fn clean_normalizes() {
assert_eq!(Clean("a/./b//c/../d"), "a/b/d");
assert_eq!(Clean("/a/../b"), "/b");
assert_eq!(Clean(""), ".");
assert_eq!(Clean("/"), "/");
}
}