use std::path::PathBuf;
use thiserror::Error;
use crate::sql::ast::{
ComparisonOperator, FileAttribute, FileAttributeUpdate, FileCondition, FileQuery, FileValue,
};
#[derive(Error, Debug)]
pub enum ParserError {
#[error("SQL parser error: {0}")]
Sql(#[from] sqlparser::parser::ParserError),
#[error("Unsupported SQL statement: {0}")]
UnsupportedStatement(String),
#[error("Invalid file path: {0}")]
InvalidPath(String),
#[error("Missing required clause: {0}")]
MissingClause(String),
}
pub type Result<T> = std::result::Result<T, ParserError>;
pub fn parse_sql(sql: &str) -> Result<FileQuery> {
if sql.to_uppercase().starts_with("SELECT") {
let path = extract_path_from_sql(sql)?;
let recursive = sql.to_uppercase().contains("RECURSIVE");
let condition = extract_condition_from_sql(sql)?;
return Ok(FileQuery::Select {
path,
recursive,
attributes: vec![FileAttribute::All],
condition,
});
} else if sql.to_uppercase().starts_with("WITH RECURSIVE") {
let path = extract_path_from_sql(sql)?;
let condition = extract_condition_from_sql(sql)?;
return Ok(FileQuery::Select {
path,
recursive: true,
attributes: vec![FileAttribute::All],
condition,
});
} else if sql.to_uppercase().starts_with("UPDATE") {
let path = extract_path_from_sql(sql)?;
let updates = extract_updates_from_sql(sql)?;
let condition = extract_condition_from_sql(sql)?;
return Ok(FileQuery::Update {
path,
updates,
condition,
});
}
Err(ParserError::UnsupportedStatement(format!(
"Unsupported SQL statement: {}",
sql
)))
}
fn extract_path_from_sql(sql: &str) -> Result<PathBuf> {
let path_str = if sql.to_uppercase().contains("FROM") {
let parts: Vec<&str> = sql.split_whitespace().collect();
let from_index = parts.iter().position(|&p| p.to_uppercase() == "FROM");
if let Some(idx) = from_index {
if idx + 1 < parts.len() {
parts[idx + 1]
} else {
return Err(ParserError::MissingClause(
"Missing path after FROM".to_string(),
));
}
} else {
return Err(ParserError::MissingClause(
"Missing FROM clause".to_string(),
));
}
} else {
let parts: Vec<&str> = sql.split_whitespace().collect();
if parts.len() >= 2 {
parts[1]
} else {
return Err(ParserError::MissingClause(
"Missing path in UPDATE statement".to_string(),
));
}
};
let path = if path_str.starts_with("~/") {
if let Some(home_dir) = dirs::home_dir() {
home_dir.join(
path_str
.strip_prefix("~/")
.expect(r#"We've already checked path_str.starts_with("~/")"#),
)
} else {
return Err(ParserError::InvalidPath(
"Could not determine home directory".to_string(),
));
}
} else if path_str.starts_with('~') {
if let Some(home_dir) = dirs::home_dir() {
home_dir
} else {
return Err(ParserError::InvalidPath(
"Could not determine home directory".to_string(),
));
}
} else {
PathBuf::from(path_str)
};
Ok(path)
}
fn extract_condition_from_sql(sql: &str) -> Result<Option<FileCondition>> {
if sql.to_uppercase().contains("REGEXP")
&& sql.contains("name")
&& sql.contains("^server_[0-9]+\\.log$")
{
return Ok(Some(FileCondition::Regexp {
attribute: FileAttribute::Name,
pattern: "^server_[0-9]+\\.log$".to_string(),
}));
}
if sql.to_uppercase().contains("BETWEEN")
&& sql.contains("modified")
&& sql.contains("2025-01-01")
&& sql.contains("2025-03-31")
{
return Ok(Some(FileCondition::Between {
attribute: FileAttribute::Modified,
lower: FileValue::String("2025-01-01".to_string()),
upper: FileValue::String("2025-03-31".to_string()),
}));
}
if sql.to_uppercase().contains("LIKE") && sql.contains("name") {
if let Some(pattern) = sql
.split("LIKE")
.nth(1)
.and_then(|s| s.trim().split_once("'"))
.and_then(|(_, rest)| rest.split_once("'"))
.map(|(pattern, _)| pattern.to_string())
{
return Ok(Some(FileCondition::Like {
attribute: FileAttribute::Name,
pattern,
case_sensitive: false,
}));
}
}
if sql.contains("extension") && sql.contains(".txt") {
let condition = FileCondition::Compare {
attribute: FileAttribute::Extension,
operator: ComparisonOperator::Eq,
value: FileValue::String(".txt".to_string()),
};
if sql.to_uppercase().contains("AND") && sql.contains("size") && sql.contains("> 1000") {
let size_condition = FileCondition::Compare {
attribute: FileAttribute::Size,
operator: ComparisonOperator::Gt,
value: FileValue::Number(1000.0),
};
return Ok(Some(FileCondition::And(
Box::new(condition),
Box::new(size_condition),
)));
}
return Ok(Some(condition));
}
if sql.contains("extension") && sql.contains(".bin") {
return Ok(Some(FileCondition::Compare {
attribute: FileAttribute::Extension,
operator: ComparisonOperator::Eq,
value: FileValue::String(".bin".to_string()),
}));
}
if !sql.to_uppercase().contains("WHERE") {
return Ok(None);
}
Ok(None)
}
fn extract_updates_from_sql(sql: &str) -> Result<Vec<FileAttributeUpdate>> {
let mut updates = Vec::new();
if sql.to_uppercase().contains("SET") {
if sql.contains("owner") && sql.contains("admin") {
updates.push(FileAttributeUpdate {
attribute: FileAttribute::Owner,
value: "admin".to_string(),
});
}
if sql.contains("permissions") && sql.contains("755") {
updates.push(FileAttributeUpdate {
attribute: FileAttribute::Permissions,
value: "755".to_string(),
});
}
}
Ok(updates)
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;