use super::{
GlobEntry, GlobEntryResult, GlobErrorContext, GlobErrorType, GlobPattern, create_glob_error,
};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use cap_primitives::fs::{FollowSymlinks, open_dir_nofollow, open_parent_dir, stat};
use cap_std::{ambient_authority, fs::Dir};
use minijinja::Error;
use std::io;
pub(super) struct GlobRoot {
dir: Dir,
prefix: Utf8PathBuf,
}
impl GlobRoot {
#[cfg(test)]
pub(super) const fn new(dir: Dir, prefix: Utf8PathBuf) -> Self {
Self { dir, prefix }
}
#[cfg(test)]
pub(super) const fn dir(&self) -> &Dir {
&self.dir
}
#[cfg(test)]
pub(super) fn prefix(&self) -> &Utf8Path {
self.prefix.as_path()
}
#[cfg(test)]
pub(super) fn metadata(&self, path: &Utf8Path) -> io::Result<Option<cap_std::fs::Metadata>> {
self.metadata_relative(self.relativise(path)?)
}
fn metadata_relative(&self, relative: &Utf8Path) -> io::Result<Option<cap_std::fs::Metadata>> {
match self.dir.metadata(relative) {
Ok(metadata) => Ok(Some(metadata)),
Err(err) if is_unresolvable_link(&err) && self.traverses_symlink(relative) => Ok(None),
Err(err) => Err(err),
}
}
fn traverses_symlink(&self, relative: &Utf8Path) -> bool {
if self
.dir
.symlink_metadata(relative)
.is_ok_and(|link| link.is_symlink())
{
return true;
}
self.ancestor_is_symlink(relative)
}
fn ancestor_is_symlink(&self, relative: &Utf8Path) -> bool {
let mut ancestor = Utf8PathBuf::new();
let mut components = relative.components();
components.next_back();
for component in components {
ancestor.push(component);
match self.dir.symlink_metadata(&ancestor) {
Ok(metadata) if metadata.is_symlink() => return true,
Ok(_) => {}
Err(_) => return false,
}
}
false
}
pub(super) fn relativise<'a>(&self, path: &'a Utf8Path) -> io::Result<&'a Utf8Path> {
let relative = if self.prefix == "." {
path
} else {
path.strip_prefix(&self.prefix).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"glob match {path} does not start with capability prefix {}",
self.prefix
),
)
})?
};
Ok(if relative.as_str().is_empty() {
Utf8Path::new(".")
} else {
relative
})
}
}
fn is_unresolvable_link(err: &io::Error) -> bool {
matches!(
err.kind(),
io::ErrorKind::PermissionDenied | io::ErrorKind::NotFound
)
}
const LITERAL_ESCAPES: [char; 6] = ['[', ']', '*', '?', '{', '}'];
fn literal_escape(rest: &str) -> Option<(char, usize)> {
let mut chars = rest.chars();
if chars.next()? != '[' {
return None;
}
let escaped = chars.next()?;
if chars.next()? != ']' || !LITERAL_ESCAPES.contains(&escaped) {
return None;
}
Some((
escaped,
'['.len_utf8() + escaped.len_utf8() + ']'.len_utf8(),
))
}
fn first_metacharacter(normalized: &str) -> usize {
let mut idx = 0;
while let Some(rest) = normalized.get(idx..) {
let Some(next) = rest.chars().next() else {
break;
};
if next == '[' {
match literal_escape(rest) {
Some((_, len)) => idx += len,
None => return idx,
}
} else if matches!(next, '*' | '?' | '{') {
return idx;
} else {
idx += next.len_utf8();
}
}
normalized.len()
}
pub(super) fn literal_dir_prefix(normalized: &str) -> &str {
let meta_idx = first_metacharacter(normalized);
let literal = normalized.get(..meta_idx).unwrap_or_default();
literal
.rfind(std::path::MAIN_SEPARATOR)
.and_then(|idx| literal.get(..=idx))
.unwrap_or(".")
}
pub(super) fn unescape_literal_escapes(prefix: &str) -> String {
let mut out = String::with_capacity(prefix.len());
let mut idx = 0;
while let Some(rest) = prefix.get(idx..) {
let Some(next) = rest.chars().next() else {
break;
};
if let Some((escaped, len)) = literal_escape(rest) {
out.push(escaped);
idx += len;
} else {
out.push(next);
idx += next.len_utf8();
}
}
out
}
pub(super) fn open_root_dir(pattern: &GlobPattern) -> io::Result<Option<GlobRoot>> {
let prefix = literal_dir_path(pattern);
match open_literal_prefix(Utf8Path::new(&prefix)) {
Ok(dir) => Ok(Some(GlobRoot {
dir,
prefix: Utf8PathBuf::from(prefix),
})),
Err(err) if prefix_is_unopenable(&err) => Ok(None),
Err(err) => Err(err),
}
}
fn open_literal_prefix(prefix: &Utf8Path) -> io::Result<Dir> {
let (base, remainder) = if prefix.is_absolute() {
let root = prefix.ancestors().last().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"an absolute glob prefix must have a filesystem root",
)
})?;
let remainder = prefix.strip_prefix(root).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"an absolute glob prefix must start with its filesystem root",
)
})?;
(root, remainder)
} else {
(Utf8Path::new("."), prefix)
};
let mut dir = Dir::open_ambient_dir(base, ambient_authority())?.into_std_file();
for component in remainder.components() {
dir = match component {
Utf8Component::CurDir => dir,
Utf8Component::ParentDir => open_parent_dir(&dir, ambient_authority())?,
Utf8Component::Normal(name) => {
if stat(&dir, name.as_ref(), FollowSymlinks::No)?.is_symlink() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"a glob literal prefix cannot traverse a symbolic link",
));
}
open_dir_nofollow(&dir, name.as_ref())?
}
Utf8Component::Prefix(_) | Utf8Component::RootDir => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a glob prefix remainder cannot contain a filesystem root",
));
}
};
}
Ok(Dir::from_std_file(dir))
}
fn literal_dir_path(pattern: &GlobPattern) -> String {
unescape_literal_escapes(literal_dir_prefix(pattern.normalized()))
}
fn prefix_is_unopenable(err: &io::Error) -> bool {
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) {
return true;
}
#[cfg(windows)]
{
const ERROR_DIRECTORY: i32 = 267;
return err.raw_os_error() == Some(ERROR_DIRECTORY);
}
#[cfg(not(windows))]
false
}
fn create_io_error(pattern: &GlobPattern, position: usize, detail: String) -> Error {
create_glob_error(
&GlobErrorContext {
pattern: pattern.raw().to_owned(),
error_char: '\0',
position,
error_type: GlobErrorType::IoError,
},
Some(detail),
)
}
pub(super) fn process_glob_entry(
entry: GlobEntryResult,
pattern: &GlobPattern,
root: &GlobRoot,
) -> std::result::Result<GlobEntry, Error> {
let path = entry.map_err(|e| create_io_error(pattern, 0, e.to_string()))?;
let utf_path = Utf8PathBuf::try_from(path).map_err(|_| {
create_io_error(
pattern,
pattern.raw().len(),
"glob matched a non-UTF-8 path".to_owned(),
)
})?;
names_a_file(root, &utf_path)
.map_err(|err| create_io_error(pattern, pattern.raw().len(), err.to_string()))
}
fn names_a_file(root: &GlobRoot, path: &Utf8Path) -> io::Result<GlobEntry> {
let relative = root.relativise(path)?;
let Some(metadata) = root.metadata_relative(relative)? else {
return Ok(GlobEntry::UnreachableSymlink(relative.to_path_buf()));
};
if metadata.is_file() {
return Ok(GlobEntry::Path(path.as_str().replace('\\', "/")));
}
Ok(GlobEntry::NotAFile)
}