use crate::model::{DirNode, FileNode, Node, NodeName};
use anyhow::{Result, anyhow};
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use std::fmt;
use std::error::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCategory {
Missing,
WrongType,
NameMismatch,
Unexpected,
InvalidPattern,
IoError,
Other,
}
#[derive(Debug)]
pub struct ValidationError {
pub path: PathBuf,
pub message: String,
pub category: ErrorCategory,
pub children: Vec<ValidationError>,
}
impl ValidationError {
pub fn new(
path: impl AsRef<Path>,
message: impl Into<String>,
category: ErrorCategory
) -> Self {
ValidationError {
path: path.as_ref().to_path_buf(),
message: message.into(),
category,
children: Vec::new(),
}
}
pub fn with_children(
path: impl AsRef<Path>,
message: impl Into<String>,
category: ErrorCategory,
children: Vec<ValidationError>
) -> Self {
ValidationError {
path: path.as_ref().to_path_buf(),
message: message.into(),
category,
children,
}
}
pub fn add_child(&mut self, error: ValidationError) {
self.children.push(error);
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "At {}: {}", self.path.display(), self.message)?;
for child in &self.children {
write!(f, " ")?;
write!(f, "{}", child)?;
}
Ok(())
}
}
impl Error for ValidationError {}
pub type ValidationResult = Result<(), ValidationError>;
impl Node {
pub fn validate(&self, path: impl AsRef<Path>) -> ValidationResult {
let path = path.as_ref();
match self {
Node::File(file_rc) => {
let file = file_rc.borrow();
validate_file(&file, path)
}
Node::Dir(dir_rc) => {
let dir = dir_rc.borrow();
validate_dir(&dir, path)
}
}
}
}
fn validate_file(file: &FileNode, file_path: &Path) -> ValidationResult {
if file_path.exists() {
if !file_path.is_file() {
return Err(ValidationError::new(
file_path,
format!("Path exists but is not a file"),
ErrorCategory::WrongType,
));
}
let file_name = match file_path.file_name() {
Some(name) => name.to_string_lossy().to_string(),
None => {
return Err(ValidationError::new(
file_path,
format!("Invalid file path (no filename)"),
ErrorCategory::Other,
))
}
};
match &file.name {
NodeName::Literal(name) => {
if &file_name != name {
return Err(ValidationError::new(
file_path,
format!(
"File name '{}' doesn't match expected name '{}'",
file_name, name
),
ErrorCategory::NameMismatch,
));
}
Ok(())
}
NodeName::Pattern(pattern) => {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
return Err(ValidationError::new(
file_path,
format!("Invalid regex pattern '{}': {}", pattern, e),
ErrorCategory::InvalidPattern,
));
}
};
if re.is_match(&file_name) {
Ok(())
} else {
Err(ValidationError::new(
file_path,
format!(
"File name '{}' doesn't match expected pattern '{}'",
file_name, pattern
),
ErrorCategory::NameMismatch,
))
}
}
}
} else if file.required {
Err(ValidationError::new(
file_path,
format!("Missing required file"),
ErrorCategory::Missing,
))
} else {
Ok(())
}
}
fn validate_dir(dir: &DirNode, dir_path: &Path) -> ValidationResult {
if !dir_path.exists() {
return if dir.required {
Err(ValidationError::new(
dir_path,
format!("Missing required directory"),
ErrorCategory::Missing,
))
} else {
Ok(())
};
}
if !dir_path.is_dir() {
return Err(ValidationError::new(
dir_path,
format!("Path exists but is not a directory"),
ErrorCategory::WrongType,
));
}
let dir_name = match dir_path.file_name() {
Some(name) => name.to_string_lossy().to_string(),
None => {
return Err(ValidationError::new(
dir_path,
format!("Invalid directory path (no directory name)"),
ErrorCategory::Other,
));
}
};
let mut validation_errors = Vec::new();
match &dir.name {
NodeName::Literal(name) => {
if &dir_name != name {
validation_errors.push(ValidationError::new(
dir_path,
format!(
"Directory name '{}' doesn't match expected name '{}'",
dir_name, name
),
ErrorCategory::NameMismatch,
));
}
}
NodeName::Pattern(pattern) => {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
return Err(ValidationError::new(
dir_path,
format!("Invalid regex pattern '{}': {}", pattern, e),
ErrorCategory::InvalidPattern,
));
}
};
if !re.is_match(&dir_name) {
validation_errors.push(ValidationError::new(
dir_path,
format!(
"Directory name '{}' doesn't match expected pattern '{}'",
dir_name, pattern
),
ErrorCategory::NameMismatch,
));
}
}
}
if dir.allow_defined_only {
let expected_names: Vec<String> = dir
.children
.iter()
.map(|node| match node {
Node::File(f) => f.borrow().name_name_string(),
Node::Dir(d) => d.borrow().name_name_string(),
})
.collect();
let read_dir_result = match fs::read_dir(dir_path) {
Ok(entries) => entries,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory: {}", e),
ErrorCategory::IoError,
));
if !validation_errors.is_empty() {
return Err(ValidationError::with_children(
dir_path,
format!("Directory validation failed with {} errors", validation_errors.len()),
ErrorCategory::Other,
validation_errors,
));
}
return Ok(());
}
};
for entry_result in read_dir_result {
let entry = match entry_result {
Ok(e) => e,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory entry: {}", e),
ErrorCategory::IoError,
));
continue;
}
};
if dir
.excluded
.iter()
.any(|re| re.is_match(entry.file_name().to_string_lossy().to_string().as_str()))
{
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
if !expected_names.iter().any(|p| pattern_match(p, &name)) {
validation_errors.push(ValidationError::new(
entry.path(),
format!("Unexpected entry in directory"),
ErrorCategory::Unexpected,
));
}
}
}
let mut child_errors = Vec::new();
for child in &dir.children {
match child {
Node::File(f_rc) => {
let f = f_rc.borrow();
match &f.name {
NodeName::Literal(name) => {
let child_path = dir_path.join(name);
if let Err(err) = child.validate(&child_path) {
child_errors.push(err);
}
}
NodeName::Pattern(pattern) => {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Invalid regex pattern '{}': {}", pattern, e),
ErrorCategory::InvalidPattern,
));
continue;
}
};
let read_dir_result = match fs::read_dir(dir_path) {
Ok(entries) => entries,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory when matching pattern: {}", e),
ErrorCategory::IoError,
));
continue;
}
};
let mut matched_path = None;
for entry_result in read_dir_result {
let entry = match entry_result {
Ok(e) => e,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory entry: {}", e),
ErrorCategory::IoError,
));
continue;
}
};
if dir.excluded.iter().any(|re| {
re.is_match(
entry.file_name().to_string_lossy().to_string().as_str(),
)
}) {
continue;
}
if entry.path().is_file() {
let name = entry.file_name().to_string_lossy().to_string();
if re.is_match(&name) {
matched_path = Some(entry.path());
break;
}
}
}
match matched_path {
Some(path) => {
if let Err(err) = child.validate(&path) {
child_errors.push(err);
}
}
None => {
if f.required {
validation_errors.push(ValidationError::new(
dir_path,
format!(
"Missing required file matching pattern: {}",
pattern
),
ErrorCategory::Missing,
));
}
}
}
}
}
}
Node::Dir(d_rc) => {
let d = d_rc.borrow();
match &d.name {
NodeName::Literal(name) => {
let child_path = dir_path.join(name);
if let Err(err) = child.validate(&child_path) {
child_errors.push(err);
}
}
NodeName::Pattern(pattern) => {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Invalid regex pattern '{}': {}", pattern, e),
ErrorCategory::InvalidPattern,
));
continue;
}
};
let read_dir_result = match fs::read_dir(dir_path) {
Ok(entries) => entries,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory when matching pattern: {}", e),
ErrorCategory::IoError,
));
continue;
}
};
let mut matched_paths = vec![];
for entry_result in read_dir_result {
let entry = match entry_result {
Ok(e) => e,
Err(e) => {
validation_errors.push(ValidationError::new(
dir_path,
format!("Failed to read directory entry: {}", e),
ErrorCategory::IoError,
));
continue;
}
};
if dir.excluded.iter().any(|re| {
re.is_match(
entry.file_name().to_string_lossy().to_string().as_str(),
)
}) {
continue;
}
if entry.path().is_dir() {
let name = entry.file_name().to_string_lossy().to_string();
if re.is_match(&name) {
matched_paths.push(entry.path());
}
}
}
if matched_paths.is_empty() {
if d.required {
validation_errors.push(ValidationError::new(
dir_path,
format!(
"Missing required directory matching pattern: {}",
pattern
),
ErrorCategory::Missing,
));
}
} else {
for matched_path in matched_paths {
if let Err(err) = child.validate(&matched_path) {
child_errors.push(err);
}
}
}
}
}
}
};
}
validation_errors.extend(child_errors);
if validation_errors.is_empty() {
Ok(())
} else {
Err(ValidationError::with_children(
dir_path,
format!("Directory validation failed with {} errors", validation_errors.len()),
ErrorCategory::Other,
validation_errors,
))
}
}
fn pattern_match(pattern: &str, name: &str) -> bool {
if pattern.starts_with("PATTERN(") && pattern.ends_with(")") {
let inner = &pattern[8..pattern.len() - 1]; match Regex::new(inner) {
Ok(re) => re.is_match(name),
Err(e) => {
eprintln!("Error compiling regex pattern '{}': {}", inner, e);
false
}
}
} else {
pattern == name
}
}
pub fn to_anyhow_result(result: ValidationResult) -> Result<()> {
match result {
Ok(()) => Ok(()),
Err(validation_err) => Err(anyhow!(validation_err.to_string())),
}
}
trait NameString {
fn name_name_string(&self) -> String;
}
impl NameString for DirNode {
fn name_name_string(&self) -> String {
match &self.name {
NodeName::Literal(s) => s.clone(),
NodeName::Pattern(p) => format!("PATTERN({})", p),
}
}
}
impl NameString for FileNode {
fn name_name_string(&self) -> String {
match &self.name {
NodeName::Literal(s) => s.clone(),
NodeName::Pattern(p) => format!("PATTERN({})", p),
}
}
}