use chrono::{DateTime, Utc};
use regex::Regex;
use serde::Serialize;
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use thiserror::Error;
use walkdir::WalkDir;
use crate::sql::ast::{ComparisonOperator, FileAttribute, FileCondition, FileQuery, FileValue};
#[derive(Error, Debug)]
pub enum ExecutorError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Unsupported file attribute: {0}")]
UnsupportedAttribute(String),
#[error("Unsupported operation: {0}")]
UnsupportedOperation(String),
#[error("Invalid regular expression: {0}")]
InvalidRegex(#[from] regex::Error),
#[error("Type error: {0}")]
TypeError(String),
}
pub type Result<T> = std::result::Result<T, ExecutorError>;
#[derive(Debug, Clone, Serialize)]
pub struct FileResult {
pub path: PathBuf,
pub name: String,
pub size: u64,
pub is_directory: bool,
pub extension: Option<String>,
pub permissions: u32,
#[serde(with = "chrono::serde::ts_seconds")]
pub modified: DateTime<Utc>,
pub owner: Option<String>,
}
pub fn execute_query(query: &FileQuery) -> Result<Vec<FileResult>> {
match query {
FileQuery::Select {
path,
recursive,
attributes,
condition,
} => execute_select(path, *recursive, attributes, condition.as_ref()),
FileQuery::Update {
path,
updates,
condition,
} => execute_update(path, updates, condition.as_ref()),
}
}
fn execute_select(
path: &Path,
recursive: bool,
_attributes: &[FileAttribute],
condition: Option<&FileCondition>,
) -> Result<Vec<FileResult>> {
let files = list_files(path, recursive)?;
let filtered_files = if let Some(cond) = condition {
files
.into_iter()
.filter(|file| evaluate_condition(file, cond).unwrap_or(false))
.collect()
} else {
files
};
Ok(filtered_files)
}
fn execute_update(
path: &Path,
updates: &[crate::sql::ast::FileAttributeUpdate],
condition: Option<&FileCondition>,
) -> Result<Vec<FileResult>> {
let files = list_files(path, true)?;
let filtered_files = if let Some(cond) = condition {
files
.into_iter()
.filter(|file| evaluate_condition(file, cond).unwrap_or(false))
.collect()
} else {
files
};
let mut updated_files = Vec::new();
for file in filtered_files {
let mut file_updated = false;
for update in updates {
match update.attribute {
FileAttribute::Permissions => {
let perms = u32::from_str_radix(&update.value, 8).map_err(|_| {
ExecutorError::TypeError(format!(
"Invalid permissions value: {}",
update.value
))
})?;
fs::set_permissions(&file.path, Permissions::from_mode(perms))?;
file_updated = true;
}
FileAttribute::Owner => {
return Err(ExecutorError::UnsupportedOperation(
"Changing file ownership is not implemented".to_string(),
));
}
_ => {
return Err(ExecutorError::UnsupportedAttribute(format!(
"Cannot update attribute: {:?}",
update.attribute
)));
}
}
}
if file_updated {
let updated_file = create_file_result(&file.path)?;
updated_files.push(updated_file);
}
}
Ok(updated_files)
}
fn list_files(dir_path: &Path, recursive: bool) -> Result<Vec<FileResult>> {
let mut results = Vec::new();
let walker = if recursive {
WalkDir::new(dir_path).follow_links(false).into_iter()
} else {
WalkDir::new(dir_path)
.max_depth(1)
.follow_links(false)
.into_iter()
};
for entry in walker {
let entry = entry.map_err(|e| {
ExecutorError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read directory entry: {}", e),
))
})?;
let file_result = create_file_result(entry.path())?;
results.push(file_result);
}
Ok(results)
}
fn create_file_result(path: &Path) -> Result<FileResult> {
let metadata = fs::metadata(path)?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_string());
let modified = metadata
.modified()
.map(DateTime::<Utc>::from)
.unwrap_or_else(|_| Utc::now());
let permissions = metadata.permissions().mode();
let owner = None;
Ok(FileResult {
path: path.to_path_buf(),
name,
size: metadata.len(),
is_directory: metadata.is_dir(),
extension,
permissions,
modified,
owner,
})
}
fn evaluate_condition(file: &FileResult, condition: &FileCondition) -> Result<bool> {
match condition {
FileCondition::Compare {
attribute,
operator,
value,
} => {
let file_value = get_attribute_value(file, attribute)?;
compare_values(&file_value, operator, value)
}
FileCondition::And(left, right) => {
let left_result = evaluate_condition(file, left)?;
if !left_result {
return Ok(false);
}
evaluate_condition(file, right)
}
FileCondition::Or(left, right) => {
let left_result = evaluate_condition(file, left)?;
if left_result {
return Ok(true);
}
evaluate_condition(file, right)
}
FileCondition::Not(inner) => {
let inner_result = evaluate_condition(file, inner)?;
Ok(!inner_result)
}
FileCondition::Like {
attribute,
pattern,
case_sensitive,
} => {
let file_value = get_attribute_value(file, attribute)?;
match file_value {
FileValue::String(s) => {
let file_str = if *case_sensitive { s } else { s.to_lowercase() };
let pattern_str = if *case_sensitive {
pattern.clone()
} else {
pattern.to_lowercase()
};
let regex_pattern = pattern_str.replace('%', ".*").replace('_', ".");
let regex = Regex::new(&format!("^{}$", regex_pattern))?;
Ok(regex.is_match(&file_str))
}
_ => Err(ExecutorError::TypeError(format!(
"LIKE can only be used with string attributes, got {:?}",
file_value
))),
}
}
FileCondition::Between {
attribute,
lower,
upper,
} => {
let file_value = get_attribute_value(file, attribute)?;
let greater_than_lower = compare_values(&file_value, &ComparisonOperator::GtEq, lower)?;
let less_than_upper = compare_values(&file_value, &ComparisonOperator::LtEq, upper)?;
Ok(greater_than_lower && less_than_upper)
}
FileCondition::Regexp { attribute, pattern } => {
let file_value = get_attribute_value(file, attribute)?;
match file_value {
FileValue::String(s) => {
let regex = Regex::new(pattern)?;
Ok(regex.is_match(&s))
}
_ => Err(ExecutorError::TypeError(format!(
"REGEXP can only be used with string attributes, got {:?}",
file_value
))),
}
}
}
}
fn get_attribute_value(file: &FileResult, attribute: &FileAttribute) -> Result<FileValue> {
match attribute {
FileAttribute::Name => Ok(FileValue::String(file.name.clone())),
FileAttribute::Path => Ok(FileValue::String(file.path.to_string_lossy().to_string())),
FileAttribute::Size => Ok(FileValue::Number(file.size as f64)),
FileAttribute::Extension => Ok(FileValue::String(
file.extension.clone().unwrap_or_default(),
)),
FileAttribute::Modified => Ok(FileValue::DateTime(file.modified)),
FileAttribute::Permissions => Ok(FileValue::Number(file.permissions as f64)),
FileAttribute::IsDirectory => Ok(FileValue::Boolean(file.is_directory)),
FileAttribute::Owner => {
if let Some(owner) = &file.owner {
Ok(FileValue::String(owner.clone()))
} else {
Ok(FileValue::Null)
}
}
FileAttribute::IsExecutable => {
let is_executable = file.permissions & 0o100 != 0;
Ok(FileValue::Boolean(is_executable))
}
_ => Err(ExecutorError::UnsupportedAttribute(format!(
"Attribute not supported in conditions: {:?}",
attribute
))),
}
}
fn compare_values(
left: &FileValue,
operator: &ComparisonOperator,
right: &FileValue,
) -> Result<bool> {
match (left, right) {
(FileValue::String(l), FileValue::String(r)) => match operator {
ComparisonOperator::Eq => Ok(l == r),
ComparisonOperator::NotEq => Ok(l != r),
ComparisonOperator::Lt => Ok(l < r),
ComparisonOperator::LtEq => Ok(l <= r),
ComparisonOperator::Gt => Ok(l > r),
ComparisonOperator::GtEq => Ok(l >= r),
},
(FileValue::Number(l), FileValue::Number(r)) => match operator {
ComparisonOperator::Eq => Ok(l == r),
ComparisonOperator::NotEq => Ok(l != r),
ComparisonOperator::Lt => Ok(l < r),
ComparisonOperator::LtEq => Ok(l <= r),
ComparisonOperator::Gt => Ok(l > r),
ComparisonOperator::GtEq => Ok(l >= r),
},
(FileValue::DateTime(l), FileValue::DateTime(r)) => match operator {
ComparisonOperator::Eq => Ok(l == r),
ComparisonOperator::NotEq => Ok(l != r),
ComparisonOperator::Lt => Ok(l < r),
ComparisonOperator::LtEq => Ok(l <= r),
ComparisonOperator::Gt => Ok(l > r),
ComparisonOperator::GtEq => Ok(l >= r),
},
(FileValue::Boolean(l), FileValue::Boolean(r)) => match operator {
ComparisonOperator::Eq => Ok(l == r),
ComparisonOperator::NotEq => Ok(l != r),
_ => Err(ExecutorError::UnsupportedOperation(format!(
"Operator {:?} not supported for boolean values",
operator
))),
},
(FileValue::Null, FileValue::Null) => match operator {
ComparisonOperator::Eq => Ok(true),
ComparisonOperator::NotEq => Ok(false),
_ => Err(ExecutorError::UnsupportedOperation(
"Null values only support equality comparisons".to_string(),
)),
},
(_, FileValue::Null) | (FileValue::Null, _) => match operator {
ComparisonOperator::Eq => Ok(false),
ComparisonOperator::NotEq => Ok(true),
_ => Err(ExecutorError::UnsupportedOperation(
"Null values only support equality comparisons".to_string(),
)),
},
_ => Err(ExecutorError::TypeError(format!(
"Cannot compare values of different types: {:?} and {:?}",
left, right
))),
}
}
#[cfg(test)]
#[path = "executor_tests.rs"]
mod tests;