#![allow(unused_assignments)]
use miette::{Diagnostic, SourceSpan};
use thiserror::Error;
use super::raw::Rule;
#[derive(Debug, Clone, Diagnostic, Error)]
pub enum DetectError {
#[error("Syntax error at line {line}, column {col}")]
#[diagnostic(code(detect::syntax))]
Syntax {
#[source_code]
src: String,
#[label("{expected_msg}")]
span: SourceSpan,
#[help]
help: Option<String>,
expected_msg: String,
line: usize,
col: usize,
},
#[error("Unknown selector: {selector}")]
#[diagnostic(code(detect::unknown_selector), help("Valid selectors: name, basename, ext, path, dir, size, type, depth, modified, created, accessed, content"))]
UnknownSelector {
selector: String,
#[label("unknown selector")]
span: SourceSpan,
#[source_code]
src: String,
},
#[error("Invalid {format} selector path: {path}")]
#[diagnostic(
code(detect::invalid_structured_path),
help("Structured selectors use format: {format}:.path.to.field")
)]
InvalidStructuredPath {
format: String,
path: String,
#[label("invalid path: {reason}")]
span: SourceSpan,
reason: String,
#[source_code]
src: String,
},
#[error("Unknown structured data format: '{format}'")]
#[diagnostic(code(detect::unknown_structured_format))]
UnknownStructuredFormat {
format: String,
#[label("unknown format")]
span: SourceSpan,
#[source_code]
src: String,
#[help]
suggestions: Option<String>,
},
#[error("Unknown operator: {operator}")]
#[diagnostic(
code(detect::unknown_operator),
help("Valid operators include: ==, !=, >, <, contains, matches, etc.")
)]
UnknownOperator {
operator: String,
#[label("unknown operator")]
span: SourceSpan,
#[source_code]
src: String,
},
#[error("Unknown alias: '{word}'")]
#[diagnostic(code(detect::unknown_alias))]
UnknownAlias {
word: String,
#[label("unknown alias")]
span: SourceSpan,
#[source_code]
src: String,
#[help]
suggestions: Option<String>,
},
#[error("Operator '{operator}' is not compatible with selector '{selector}'")]
#[diagnostic(
code(detect::incompatible_operator),
help("This selector requires a different type of operator")
)]
IncompatibleOperator {
selector: String,
operator: String,
#[label("incompatible operator")]
operator_span: SourceSpan,
#[label("for this selector")]
selector_span: SourceSpan,
#[source_code]
src: String,
},
#[error("Expected {expected} value, found: {found}")]
#[diagnostic(
code(detect::invalid_value),
help("Check the value type for this selector")
)]
InvalidValue {
expected: String,
found: String,
#[label("invalid value")]
span: SourceSpan,
#[source_code]
src: String,
},
#[error("Invalid escape sequence '\\{char}'")]
#[diagnostic(
code(detect::invalid_escape),
help("Valid escape sequences: \\n, \\t, \\\\, \\\", \\'")
)]
InvalidEscape {
char: char,
#[label("invalid escape")]
span: SourceSpan,
#[source_code]
src: String,
},
#[error("Unterminated escape sequence")]
#[diagnostic(code(detect::unterminated_escape))]
UnterminatedEscape {
#[label("escape sequence not completed")]
span: SourceSpan,
#[source_code]
src: String,
},
#[error("Unterminated string literal")]
#[diagnostic(code(detect::unterminated_string))]
UnterminatedString {
#[label("missing closing {quote} quote")]
span: SourceSpan,
quote: char,
#[source_code]
src: String,
},
#[error("Stray {quote} quote")]
#[diagnostic(
code(detect::stray_quote),
help("Remove the quote or add matching opening quote")
)]
StrayQuote {
#[label("unexpected quote")]
span: SourceSpan,
quote: char,
#[source_code]
src: String,
},
#[error("Directory not found: {path}")]
#[diagnostic(
code(detect::directory_not_found),
help("Check that the directory path exists and is accessible")
)]
DirectoryNotFound { path: String },
#[error("Path is not a directory: {path}")]
#[diagnostic(
code(detect::not_a_directory),
help("The path must be a directory, not a file")
)]
NotADirectory { path: String },
#[error("I/O error: {message}")]
#[diagnostic(code(detect::io_error))]
IoError { message: String },
#[error("Internal parser error: {message}")]
#[diagnostic(code(detect::internal))]
Internal {
message: String,
#[source_code]
src: String,
},
}
pub trait SpanExt {
fn to_location(&self) -> (usize, usize);
fn to_source_span(&self) -> SourceSpan;
}
impl SpanExt for pest::Span<'_> {
#[inline]
fn to_location(&self) -> (usize, usize) {
self.start_pos().line_col()
}
#[inline]
fn to_source_span(&self) -> SourceSpan {
(self.start(), self.end() - self.start()).into()
}
}
fn rule_to_friendly_name(rule: &Rule) -> &'static str {
match rule {
Rule::program => "program",
Rule::expr => "expression",
Rule::infix => "operator (AND/OR)",
Rule::and => "AND",
Rule::or => "OR",
Rule::prefix => "prefix operator (NOT)",
Rule::neg => "NOT",
Rule::primary => "predicate or expression",
Rule::predicate => "predicate",
Rule::selector => "selector",
Rule::operator => "operator",
Rule::value => "value",
Rule::value_content => "value",
Rule::raw_token => "value",
Rule::quoted_string => "quoted string",
Rule::unterminated_string => "unterminated string",
Rule::trailing_quote => "trailing quote",
Rule::single_word => "single-word alias",
Rule::set_contents => "set contents",
Rule::set_items => "set items",
Rule::set_item => "set item",
Rule::bare_set_item => "item",
Rule::inner_double => "string content",
Rule::inner_single => "string content",
Rule::escaped => "escape sequence",
Rule::raw_char => "character",
Rule::balanced_paren => "balanced parentheses",
Rule::balanced_bracket => "balanced brackets",
Rule::balanced_curly => "balanced braces",
Rule::WHITESPACE => "whitespace",
Rule::EOI => "end of input",
}
}
fn generate_help_text(positives: &[Rule], found_eoi: bool) -> Option<String> {
if positives.is_empty() {
return None;
}
if positives.contains(&Rule::value) {
if found_eoi {
return Some("Try adding a value after the operator, like: ext == rs".to_string());
}
return Some("Expected a value here (e.g., a string, number, or [set])".to_string());
}
if (positives.contains(&Rule::expr) || positives.contains(&Rule::predicate)) && found_eoi {
return Some("Expression is incomplete. Add a predicate after the operator.".to_string());
}
if positives.contains(&Rule::EOI) {
return Some("Unexpected input. Check for unbalanced parentheses or quotes.".to_string());
}
None
}
impl DetectError {
pub fn from_pest(pest_err: Box<pest::error::Error<Rule>>, src: String) -> Self {
use pest::error::{ErrorVariant, InputLocation};
let (span, _pos) = match pest_err.location {
InputLocation::Pos(pos) => {
if pos >= src.len() && pos > 0 {
((pos - 1, 1).into(), pos)
} else if pos < src.len() {
((pos, 1).into(), pos)
} else {
((0, 0).into(), pos)
}
}
InputLocation::Span((start, end)) => {
let width = end.saturating_sub(start).max(1); ((start, width).into(), start)
}
};
let (line, col) = match pest_err.line_col {
pest::error::LineColLocation::Pos((line, col)) => (line, col),
pest::error::LineColLocation::Span((line, col), _) => (line, col),
};
let (expected_msg, help) = match &pest_err.variant {
ErrorVariant::ParsingError {
positives,
negatives: _,
} => {
let found_eoi = match pest_err.location {
InputLocation::Pos(p) => p >= src.len(),
InputLocation::Span((_, end)) => end >= src.len(),
};
let expected_msg = if positives.is_empty() {
"Unexpected input".to_string()
} else if positives.len() == 1 {
format!("Expected {}", rule_to_friendly_name(&positives[0]))
} else {
let names: Vec<&str> = positives.iter().map(rule_to_friendly_name).collect();
if names.len() <= 3 {
format!("Expected one of: {}", names.join(", "))
} else {
format!("Expected one of: {}, ...", names[..3].join(", "))
}
};
let help = generate_help_text(positives, found_eoi);
(expected_msg, help)
}
ErrorVariant::CustomError { message } => (message.clone(), None),
};
DetectError::Syntax {
src,
span,
help,
expected_msg,
line,
col,
}
}
pub fn internal(msg: impl Into<String>) -> Self {
DetectError::Internal {
message: msg.into(),
src: String::new(),
}
}
pub fn with_source(mut self, src: String) -> Self {
match &mut self {
DetectError::Syntax { src: s, .. }
| DetectError::UnknownSelector { src: s, .. }
| DetectError::InvalidStructuredPath { src: s, .. }
| DetectError::UnknownStructuredFormat { src: s, .. }
| DetectError::UnknownOperator { src: s, .. }
| DetectError::UnknownAlias { src: s, .. }
| DetectError::IncompatibleOperator { src: s, .. }
| DetectError::InvalidValue { src: s, .. }
| DetectError::InvalidEscape { src: s, .. }
| DetectError::UnterminatedEscape { src: s, .. }
| DetectError::UnterminatedString { src: s, .. }
| DetectError::StrayQuote { src: s, .. }
| DetectError::Internal { src: s, .. } => {
*s = src;
}
DetectError::DirectoryNotFound { .. }
| DetectError::NotADirectory { .. }
| DetectError::IoError { .. } => {}
}
self
}
}