use std::{
cell::RefCell,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum PathTraversalError {
OutOfScope {
requested: String,
base: String,
},
NotFound {
path: String,
kind: DirectoryEntryKind,
},
Io {
path: Option<String>,
source: std::io::Error,
},
PermissionDenied {
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum DirectoryEntryKind {
File,
Directory,
}
impl std::fmt::Display for DirectoryEntryKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DirectoryEntryKind::File => {
write!(f, "File")
}
DirectoryEntryKind::Directory => {
write!(f, "Directory")
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DirectoryEntry {
pub path: String,
pub kind: DirectoryEntryKind,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DirectoryListing {
pub entries: Vec<DirectoryEntry>,
pub total_entries: usize,
pub truncated: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Location {
pub path: PathBuf,
pub line: usize,
}
#[derive(Clone, Debug)]
pub struct PathTraversal {
observe_hidden: bool,
observe_ignore: bool,
package_matcher: RefCell<Option<ignore::IncrementalIgnore>>,
workspace_matcher: RefCell<ignore::IncrementalIgnore>,
}
impl PathTraversal {
pub fn new(root: &Path, observe_hidden: bool, observe_ignore: bool) -> Self {
let workspace_matcher = Self::make_matcher(root, observe_hidden, observe_ignore);
Self {
observe_hidden,
observe_ignore,
package_matcher: RefCell::new(None),
workspace_matcher: RefCell::new(workspace_matcher),
}
}
pub fn absolute(
&self,
path: &Path,
package_root: Option<&Path>,
) -> Result<PathBuf, PathTraversalError> {
let path = self.cleaned_path(path);
let matcher = self.get_matcher(package_root);
let mut start_path = matcher.root().to_path_buf();
start_path.push(path);
let relative = matcher
.normalize(&start_path)
.ok_or(PathTraversalError::OutOfScope {
requested: start_path.to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})?;
let mut path = PathBuf::new();
path.push(matcher.root());
path.push(relative);
Ok(path)
}
pub fn list_dir(
&self,
path: &Path,
recursive: bool,
package_root: Option<&Path>,
) -> Result<DirectoryListing, PathTraversalError> {
let mut result = DirectoryListing {
entries: Vec::new(),
total_entries: 0,
truncated: false,
};
let path = self.cleaned_path(path);
let mut maybe_matcher = self.package_matcher.borrow_mut();
let matcher = if let Some(pckg_root) = package_root {
if let Some(ref mut pckgm) = *maybe_matcher
&& pckgm.root() == pckg_root
{
pckgm
} else {
*maybe_matcher = Some(Self::make_matcher(
pckg_root,
self.observe_hidden,
self.observe_ignore,
));
maybe_matcher
.as_mut()
.expect("Package matcher should exist right after creation.")
}
} else {
&mut self.workspace_matcher.borrow_mut()
};
let mut start_path = matcher.root().to_path_buf();
start_path.push(&path);
start_path = matcher
.normalize(&start_path)
.ok_or(PathTraversalError::OutOfScope {
requested: start_path.to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})?;
if !matcher.root().join(&start_path).is_dir() {
return Result::Err(PathTraversalError::NotFound {
path: start_path.to_string_lossy().to_string(),
kind: DirectoryEntryKind::Directory,
});
}
let mut backlog = vec![start_path];
while let Some(dir) = backlog.pop() {
let absolute_path = matcher.root().join(&dir);
let paths = match std::fs::read_dir(absolute_path) {
Ok(paths) => paths,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
continue;
}
Err(err) => return Err(io_error_to_fsa_error(err, &path)),
};
for entry in paths.flatten() {
let is_dir = entry.path().is_dir();
let entry =
matcher
.normalize(entry.path())
.ok_or(PathTraversalError::OutOfScope {
requested: entry.path().to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})?;
if matcher.matched(&entry, is_dir).is_ignore() {
continue;
}
if is_dir {
if recursive {
backlog.push(entry.clone());
}
result.entries.push(DirectoryEntry {
path: entry.to_string_lossy().to_string(),
kind: DirectoryEntryKind::Directory,
});
} else {
result.entries.push(DirectoryEntry {
path: entry.to_string_lossy().to_string(),
kind: DirectoryEntryKind::File,
});
}
}
}
result.total_entries = result.entries.len();
Ok(result)
}
pub fn observe_ignore(&self) -> bool {
self.observe_ignore
}
pub fn observe_hidden(&self) -> bool {
self.observe_hidden
}
fn cleaned_path(&self, path: &Path) -> PathBuf {
if path == "\"\"" || path == "." {
PathBuf::new()
} else {
PathBuf::from(path)
}
}
fn make_matcher(
root: &Path,
observe_hidden: bool,
observe_ignore: bool,
) -> ignore::IncrementalIgnore {
ignore::WalkBuilder::new(root)
.require_git(false)
.parents(true)
.hidden(observe_hidden)
.ignore(observe_ignore)
.git_ignore(observe_ignore)
.git_exclude(observe_ignore)
.git_global(observe_ignore)
.build_matchers()
.first()
.expect("We should get exactly one matcher")
.to_owned()
}
fn get_matcher(&self, package_root: Option<&Path>) -> ignore::IncrementalIgnore {
let mut maybe_matcher = self.package_matcher.borrow_mut();
if let Some(pckg_root) = package_root {
if let Some(pckgm) = (*maybe_matcher).as_ref()
&& pckgm.root() == pckg_root
{
pckgm.clone()
} else {
*maybe_matcher = Some(Self::make_matcher(
pckg_root,
self.observe_hidden,
self.observe_ignore,
));
maybe_matcher
.as_ref()
.expect("Package matcher should exist right after creation.")
.clone()
}
} else {
self.workspace_matcher.borrow().clone()
}
}
}
pub fn io_error_to_fsa_error(err: std::io::Error, path: &Path) -> PathTraversalError {
let path_string = path.to_string_lossy().to_string();
match err.kind() {
std::io::ErrorKind::NotFound => PathTraversalError::NotFound {
path: path_string,
kind: DirectoryEntryKind::File,
},
std::io::ErrorKind::PermissionDenied => {
PathTraversalError::PermissionDenied { path: path_string }
}
_ => PathTraversalError::Io {
path: Some(path_string),
source: err,
},
}
}