use crate::matcher::CpuMatcher;
use crate::rules::{builtin_rules, ServiceMatch, ServiceRule};
pub struct BannerClassifier {
matcher: CpuMatcher,
}
impl BannerClassifier {
#[must_use]
pub fn new() -> Self {
Self {
matcher: CpuMatcher::new(builtin_rules()),
}
}
#[must_use]
pub fn with_rules(rules: Vec<ServiceRule>) -> Self {
Self {
matcher: CpuMatcher::new(rules),
}
}
#[must_use]
pub fn classify(&self, banner: &str) -> Vec<ServiceMatch> {
self.matcher.match_banner(banner)
}
#[must_use]
pub fn classify_batch(&self, banners: &[&str]) -> Vec<Vec<ServiceMatch>> {
self.matcher.match_batch(banners)
}
#[must_use]
pub fn classify_top(&self, banner: &str) -> Option<ServiceMatch> {
self.matcher.match_banner(banner).into_iter().next()
}
}
impl Default for BannerClassifier {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifier_loads_builtin_rules_without_panic() {
let c = BannerClassifier::new();
let _ = c.classify("Server: nginx/1.25.3\r\n");
}
#[test]
fn classify_top_returns_none_for_garbage() {
let c = BannerClassifier::new();
assert!(c.classify_top("\x00\x00\x00\x00").is_none());
}
#[test]
fn classify_batch_preserves_ordering() {
let c = BannerClassifier::new();
let banners = ["Server: nginx", "SSH-2.0-OpenSSH_8.9", "garbage"];
let out = c.classify_batch(&banners);
assert_eq!(out.len(), banners.len(), "one result vec per input banner");
}
#[test]
fn with_rules_uses_caller_rule_set() {
let c = BannerClassifier::with_rules(vec![]);
assert!(c.classify("Server: nginx/1.25.3").is_empty());
assert!(c.classify_top("anything").is_none());
}
}