use std::io;
use std::path;
#[allow(clippy::needless_lifetimes)]
pub fn resolve_root<'a, P>(
prefix: P,
pattern: &'a str,
) -> Result<(path::PathBuf, &'a str), io::Error>
where
P: AsRef<path::Path>,
{
let mut root = path::PathBuf::from(prefix.as_ref());
let mut rest = path::PathBuf::new();
if pattern.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty pattern"));
}
if !root.as_path().exists() {
return Err(io::Error::from(io::ErrorKind::NotFound));
}
if path::Path::new(pattern).is_absolute() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("'{pattern}' is an absolute path"),
));
}
let mut push_root = true;
path::Path::new(pattern).components().for_each(|c| {
if push_root {
root.push(c);
if !root.exists() {
root.pop();
rest.push(c);
push_root = false;
}
} else {
rest.push(c);
}
});
if rest.components().count() == 0 {
if let Some(c) = root.components().next_back() {
if let path::Component::Normal(_) = c {
rest.push(c);
root.pop();
}
}
}
if rest
.components()
.any(|c| matches!(c, path::Component::ParentDir))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"pattern remainder '{}' contains unresolved relative path components",
rest.to_str().unwrap()
),
));
}
let rest = &pattern[pattern.len() - rest.to_str().unwrap().len()..];
Ok((root, rest))
}
pub(crate) fn to_upper(s: String) -> String {
let mut c = s.chars();
match c.next() {
None => s,
Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
}
}
pub fn is_hidden_entry<P>(path: P) -> bool
where
P: AsRef<path::Path>,
{
let is_hidden = path
.as_ref()
.file_name()
.unwrap_or_else(|| path.as_ref().as_os_str())
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false);
is_hidden
}
pub fn is_hidden_path<P>(path: P) -> bool
where
P: AsRef<path::Path>,
{
let has_hidden = path.as_ref().components().find(|c| {
c.as_os_str()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
});
has_hidden.is_some()
}
#[cfg(test)]
mod tests {
use super::resolve_root;
use std::{io, path};
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn outside_root() -> Result<(), std::io::Error> {
let root = path::Path::new(env!("CARGO_MANIFEST_DIR"));
let levels = vec!["../"; root.components().count() * 2];
let pattern = levels.join("") + "*.txt";
let (root, rest) = resolve_root(root, pattern.as_str())?;
let root = root.canonicalize()?;
let root = root
.to_str()
.ok_or_else(|| io::Error::from(io::ErrorKind::Other))?;
if !cfg!(windows) {
assert_eq!(root, "/");
assert_eq!(rest, "*.txt");
}
Ok(())
}
#[test]
fn patterns() -> Result<(), String> {
fn tst(root: &str, pattern: &str, exp_root: &str, exp_pattern: &str) -> Result<(), String> {
let root = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), root);
let (root, pattern) = resolve_root(root, pattern).map_err(|err| err.to_string())?;
let root = root.canonicalize().map_err(|err| err.to_string())?;
let root = root
.to_str()
.ok_or_else(|| io::Error::from(io::ErrorKind::Other))
.map_err(|err| err.to_string())?;
let exp_root = format!(
"{}{}",
env!("CARGO_MANIFEST_DIR"),
match exp_root {
"" => "".to_string(),
p => format!("/{p}"),
}
);
let exp_root = path::PathBuf::from(exp_root)
.canonicalize()
.map_err(|err| err.to_string())?;
let exp_root = exp_root
.to_str()
.ok_or_else(|| io::Error::from(io::ErrorKind::Other))
.map_err(|err| err.to_string())?;
assert_eq!(root, exp_root);
assert_eq!(pattern, exp_pattern);
Ok(())
}
tst("test-files/c-simple/a", "../a", "test-files/c-simple", "a")?;
tst(
"test-files/c-simple",
"*.txt",
"test-files/c-simple",
"*.txt",
)?;
tst("test-files/c-simple/a/a0", "../../../../*.txt", "", "*.txt")?;
tst(
"test-files/c-simple/a/a0",
"a0_0.txt",
"test-files/c-simple/a/a0",
"a0_0.txt",
)?;
tst(
"test-files/c-simple/a/a0",
"../a0/a0_0.txt",
"test-files/c-simple/a/a0",
"a0_0.txt",
)?;
Ok(())
}
}