use std::path::{Component, Path, PathBuf};
use crate::{Error, Glob};
pub const FILE_PATH_SEPARATOR: char = '#';
#[derive(Debug, Clone, Default)]
pub struct FileKeyPaths {
entries: Vec<(FileGlobs, String)>,
}
impl FileKeyPaths {
pub fn new<S: AsRef<str>>(
entries: impl IntoIterator<Item = S>,
base: impl Into<PathBuf>,
) -> Result<Self, Error> {
let base = base.into();
let entries = entries
.into_iter()
.map(|entry| {
let entry = entry.as_ref();
match entry.split_once(FILE_PATH_SEPARATOR) {
Some((file, path)) if !file.trim().is_empty() && !path.trim().is_empty() => {
Ok((FileGlobs::new([file], [], base.clone()), path.trim().to_owned()))
}
_ => Err(Error::Config(format!(
"allow.file_paths entry {entry:?} is not FILE{FILE_PATH_SEPARATOR}KEY.PATH, \
such as \"config.yml{FILE_PATH_SEPARATOR}db.password\""
))),
}
})
.collect::<Result<_, _>>()?;
Ok(Self { entries })
}
pub fn for_file(&self, path: &Path) -> Vec<&str> {
self.entries
.iter()
.filter(|(files, _)| files.matches(path))
.map(|(_, key_path)| key_path.as_str())
.collect()
}
}
#[derive(Debug, Clone)]
pub struct FileGlobs {
base: PathBuf,
include: Vec<Glob>,
exclude: Vec<Glob>,
}
impl FileGlobs {
pub fn new<S: AsRef<str>>(
include: impl IntoIterator<Item = S>,
exclude: impl IntoIterator<Item = S>,
base: impl Into<PathBuf>,
) -> Self {
let base = base.into();
Self {
base: std::fs::canonicalize(&base).unwrap_or(base),
include: include.into_iter().map(|p| compile(p.as_ref())).collect(),
exclude: exclude.into_iter().map(|p| compile(p.as_ref())).collect(),
}
}
pub fn base(&self) -> &Path {
&self.base
}
pub fn is_empty(&self) -> bool {
self.include.is_empty()
}
pub fn matches(&self, path: &Path) -> bool {
if self.is_empty() {
return false;
}
let Ok(absolute) = std::path::absolute(path) else {
return false;
};
let lexical = normalize(&absolute);
let resolved = std::fs::canonicalize(&absolute).ok();
std::iter::once(lexical)
.chain(resolved)
.any(|candidate| self.matches_absolute(&candidate))
}
fn matches_absolute(&self, absolute: &Path) -> bool {
let Ok(relative) = absolute.strip_prefix(&self.base) else {
return false;
};
let relative: Vec<_> = relative
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect();
let relative = relative.join("/");
if relative.is_empty() {
return false;
}
let hit = |globs: &[Glob]| globs.iter().any(|glob| glob.is_match(&relative));
hit(&self.include) && !hit(&self.exclude)
}
}
fn compile(pattern: &str) -> Glob {
let pattern = pattern.trim();
let anchored = pattern.trim_end_matches('/').contains('/');
let mut pattern = pattern.trim_start_matches('/').to_owned();
if pattern.ends_with('/') {
pattern.push_str("**");
}
if anchored {
Glob::path(&pattern)
} else {
Glob::path(&format!("**/{pattern}"))
}
}
fn normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
out.pop();
}
other => out.push(other),
}
}
out
}