use fast_flirt::{FlirtSet, FlirtSetBuilder};
use std::path::Path;
use crate::{Error, Result};
pub(crate) const FLIRT_LOOKAHEAD_BYTES: u32 = 256;
pub struct FlirtMatcher {
set: FlirtSet,
sig_count: usize,
source_count: usize,
}
impl FlirtMatcher {
pub fn from_directory(path: &Path, logger: &(dyn Fn(&str) + Sync + Send)) -> Result<Self> {
if !path.is_dir() {
return Err(Error::InvalidRuleFile(format!(
"flirt: signatures path is not a directory: {}",
path.display()
)));
}
let mut builder = FlirtSetBuilder::new();
let mut source_count = 0usize;
let mut gz_skipped = 0usize;
for entry in walkdir::WalkDir::new(path)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.file_type().is_symlink() {
continue;
}
let p = entry.path();
if !p.is_file() {
continue;
}
let name = match p.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
let lower = name.to_ascii_lowercase();
let parsed_ok = if lower.ends_with(".sig") {
match std::fs::read(p) {
Ok(bytes) => match builder.add_sig(&bytes) {
Ok(_) => true,
Err(e) => {
logger(&format!("flirt: failed to parse .sig {}: {}", name, e));
false
}
},
Err(e) => {
logger(&format!("flirt: failed to read .sig {}: {}", name, e));
false
}
}
} else if lower.ends_with(".pat.gz") {
gz_skipped += 1;
false
} else if lower.ends_with(".pat") {
match std::fs::read_to_string(p) {
Ok(text) => match builder.add_pat(&text) {
Ok(_) => true,
Err(e) => {
logger(&format!("flirt: failed to parse .pat {}: {}", name, e));
false
}
},
Err(e) => {
logger(&format!("flirt: failed to read .pat {}: {}", name, e));
false
}
}
} else {
false
};
if parsed_ok {
source_count += 1;
}
}
let set = builder
.build()
.map_err(|e| Error::InvalidRuleFile(format!("flirt: build failed: {}", e)))?;
let sig_count = set.len();
if sig_count == 0 {
return Err(Error::InvalidRuleFile(format!(
"flirt: no signatures loaded from {} ({} sources attempted, {} .pat.gz skipped)",
path.display(),
source_count,
gz_skipped
)));
}
if gz_skipped > 0 {
logger(&format!(
"flirt: loaded {} signatures from {} files in {} ({} .pat.gz skipped — gunzip to enable)",
sig_count,
source_count,
path.display(),
gz_skipped
));
} else {
logger(&format!(
"flirt: loaded {} signatures from {} files in {}",
sig_count,
source_count,
path.display()
));
}
Ok(Self {
set,
sig_count,
source_count,
})
}
pub fn signature_count(&self) -> usize {
self.sig_count
}
pub fn source_file_count(&self) -> usize {
self.source_count
}
pub fn match_function<'a>(&'a self, function_bytes: &[u8]) -> Option<&'a str> {
self.set.match_public_name(function_bytes)
}
pub fn match_function_at<'a>(
&'a self,
function_addr: u64,
extractor: &dyn crate::extractor::Extractor,
) -> Option<&'a str> {
let bytes = extractor.function_bytes(function_addr, FLIRT_LOOKAHEAD_BYTES)?;
if bytes.is_empty() {
return None;
}
self.match_function(bytes)
}
}
impl std::fmt::Debug for FlirtMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlirtMatcher")
.field("signatures", &self.sig_count)
.field("source_files", &self.source_count)
.finish_non_exhaustive()
}
}