use std::{error::Error as StdError, fmt};
macro_rules! module_enum {
(
$(#[$enum_meta:meta])*
pub enum $name:ident {
$( $(#[$vmeta:meta])* $variant:ident => $slug:literal ),+ $(,)?
}
) => {
$(#[$enum_meta])*
pub enum $name {
$( $(#[$vmeta])* $variant, )+
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
$( $name::$variant => $slug, )+
};
f.write_str(s)
}
}
};
}
module_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Module {
Http => "http",
Tls => "tls",
Dns => "dns",
Icmp => "icmp",
Pcap => "pcap",
Reassembler => "reassembler",
Tracker => "tracker",
Layers => "layers",
Driver => "driver",
Emit => "emit",
Detect => "detect",
Aggregate => "aggregate",
Correlate => "correlate",
Arp => "arp",
Ndp => "ndp",
Lldp => "lldp",
Cdp => "cdp",
Dhcp => "dhcp",
Ssdp => "ssdp",
NetbiosNs => "netbios-ns",
Stun => "stun",
Ssh => "ssh",
Ntp => "ntp",
Tftp => "tftp",
Wireguard => "wireguard",
Modbus => "modbus",
Rdp => "rdp",
Snmp => "snmp",
Radius => "radius",
Dnp3 => "dnp3",
Smb => "smb",
Ldap => "ldap",
Kerberos => "kerberos",
Quic => "quic",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
Parse,
BufferOverflow,
Io,
Unsupported,
Truncated,
Eof,
Other,
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ErrorCode::Parse => "parse",
ErrorCode::BufferOverflow => "buffer-overflow",
ErrorCode::Io => "io",
ErrorCode::Unsupported => "unsupported",
ErrorCode::Truncated => "truncated",
ErrorCode::Eof => "eof",
ErrorCode::Other => "other",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ErrorKind {
pub module: Module,
pub code: ErrorCode,
pub message: String,
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}: {}", self.module, self.code, self.message)
}
}
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
}
impl Error {
#[cfg(any(feature = "http", feature = "tls", feature = "dns"))]
pub(crate) fn parse(module: Module, message: impl Into<String>) -> Self {
Self {
kind: ErrorKind {
module,
code: ErrorCode::Parse,
message: message.into(),
},
source: None,
}
}
#[cfg(any(feature = "extractors", feature = "pcap", feature = "http"))]
pub(crate) fn parse_with<E>(module: Module, message: impl Into<String>, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
kind: ErrorKind {
module,
code: ErrorCode::Parse,
message: message.into(),
},
source: Some(Box::new(source)),
}
}
#[cfg(any(feature = "http", feature = "tls"))]
pub(crate) fn buffer_overflow(module: Module, cap: usize) -> Self {
Self {
kind: ErrorKind {
module,
code: ErrorCode::BufferOverflow,
message: format!("buffer exceeded cap={cap}"),
},
source: None,
}
}
#[cfg(feature = "pcap")]
pub(crate) fn io(module: Module, source: std::io::Error) -> Self {
Self {
kind: ErrorKind {
module,
code: ErrorCode::Io,
message: source.to_string(),
},
source: Some(Box::new(source)),
}
}
#[cfg(any(
feature = "arp",
feature = "ndp",
feature = "lldp",
feature = "cdp",
feature = "dhcp",
feature = "ssdp",
feature = "netbios-ns",
feature = "stun",
feature = "ssh",
feature = "ntp",
feature = "tftp",
feature = "wireguard",
feature = "modbus",
feature = "rdp",
feature = "snmp",
feature = "radius",
feature = "dnp3",
feature = "smb",
feature = "ldap",
feature = "kerberos",
feature = "quic",
))]
pub(crate) fn with_code(module: Module, code: ErrorCode, message: impl Into<String>) -> Self {
Self {
kind: ErrorKind {
module,
code,
message: message.into(),
},
source: None,
}
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
pub fn module(&self) -> Module {
self.kind.module
}
pub fn code(&self) -> ErrorCode {
self.kind.code
}
pub fn is_recoverable(&self) -> bool {
matches!(
self.kind.code,
ErrorCode::Parse
| ErrorCode::BufferOverflow
| ErrorCode::Truncated
| ErrorCode::Unsupported
)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.kind, f)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_deref()
.map(|s| s as &(dyn StdError + 'static))
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(feature = "http", feature = "tls", feature = "dns"))]
#[test]
fn error_carries_module_and_code() {
let e = Error::parse(Module::Http, "bad request line");
assert_eq!(e.module(), Module::Http);
assert_eq!(e.code(), ErrorCode::Parse);
assert!(e.is_recoverable());
assert!(e.source().is_none());
}
#[cfg(feature = "pcap")]
#[test]
fn error_wraps_source() {
let io = std::io::Error::other("disk gone");
let e = Error::io(Module::Pcap, io);
assert_eq!(e.module(), Module::Pcap);
assert_eq!(e.code(), ErrorCode::Io);
assert!(!e.is_recoverable());
assert!(e.source().is_some());
}
#[cfg(any(feature = "http", feature = "tls", feature = "dns"))]
#[test]
fn display_format() {
let e = Error::parse(Module::Tls, "alert received");
let s = format!("{e}");
assert!(s.contains("tls"));
assert!(s.contains("parse"));
assert!(s.contains("alert received"));
}
#[test]
fn send_sync_static() {
fn assert_traits<T: Send + Sync + 'static>() {}
assert_traits::<Error>();
}
#[cfg(feature = "pcap")]
#[test]
fn source_chain_walks() {
use std::error::Error as _;
let io = std::io::Error::other("disk gone");
let e = Error::io(Module::Pcap, io);
let s = e.source().unwrap();
assert!(format!("{s}").contains("disk gone"));
}
#[cfg(any(feature = "http", feature = "tls"))]
#[test]
fn buffer_overflow_includes_cap() {
let e = Error::buffer_overflow(Module::Http, 8192);
assert!(e.to_string().contains("8192"));
}
#[test]
fn module_display_covers_all_variants() {
assert_eq!(Module::Driver.to_string(), "driver");
assert_eq!(Module::Emit.to_string(), "emit");
assert_eq!(Module::Detect.to_string(), "detect");
assert_eq!(Module::Aggregate.to_string(), "aggregate");
assert_eq!(Module::Correlate.to_string(), "correlate");
assert_eq!(Module::Http.to_string(), "http");
assert_eq!(Module::Layers.to_string(), "layers");
}
}