use super::RuleMatch;
pub(crate) fn is_message_bearing(message: &str) -> bool {
let trimmed = message.trim_matches(|c: char| c.is_whitespace() || c == '\u{8}');
let stripped = crate::evaluator::strip_no_separator_marker(trimmed).unwrap_or(trimmed);
!stripped
.trim_matches(|c: char| c.is_whitespace() || c == '\u{8}')
.is_empty()
}
pub(crate) fn has_message_bearing_match(matches: &[RuleMatch], from: usize) -> bool {
matches
.get(from..)
.is_some_and(|tail| tail.iter().any(|m| is_message_bearing(&m.message)))
}
fn first_rendering_match(matches: &mut [RuleMatch]) -> Option<&mut RuleMatch> {
matches.iter_mut().find(|m| is_message_bearing(&m.message))
}
fn mark_no_separator(target: &mut RuleMatch) {
if crate::evaluator::strip_no_separator_marker(&target.message).is_none() {
target.message = format!("\\b{}", target.message);
}
}
pub(crate) fn attach_no_separator_to_first(mut matches: Vec<RuleMatch>) -> Vec<RuleMatch> {
if let Some(target) = first_rendering_match(&mut matches) {
mark_no_separator(target);
}
matches
}
pub(crate) fn attach_no_separator_if_top_level(mut matches: Vec<RuleMatch>) -> Vec<RuleMatch> {
if let Some(target) = first_rendering_match(&mut matches).filter(|m| m.level == 0) {
mark_no_separator(target);
}
matches
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ast::{TypeKind, Value};
fn match_with(message: &str) -> RuleMatch {
RuleMatch::new(
message.to_string(),
0,
0,
Value::Uint(0),
TypeKind::Byte { signed: false },
1.0,
)
}
fn messages(matches: &[RuleMatch]) -> Vec<&str> {
matches.iter().map(|m| m.message.as_str()).collect()
}
#[test]
fn attach_no_separator_marks_first_message_bearing_match() {
let out = attach_no_separator_to_first(vec![match_with("MachO"), match_with("x86_64")]);
assert_eq!(
messages(&out),
vec!["\\bMachO", "x86_64"],
"only the first message-bearing match takes the marker"
);
}
#[test]
fn attach_no_separator_skips_message_less_leading_matches() {
let out = attach_no_separator_to_first(vec![
match_with(""),
match_with(" "),
match_with("\\b"),
match_with("MachO"),
]);
assert_eq!(
messages(&out),
vec!["", " ", "\\b", "\\bMachO"],
"the marker must land on the first match that actually renders text"
);
}
#[test]
fn attach_no_separator_does_not_double_mark() {
for already in ["\\b, contains ", "\u{0008}already"] {
let out = attach_no_separator_to_first(vec![match_with(already)]);
assert_eq!(
messages(&out),
vec![already],
"a match already carrying a marker must be left untouched"
);
}
}
#[test]
fn attach_no_separator_is_a_noop_without_a_message_bearing_match() {
for input in [vec![], vec![match_with("")], vec![match_with("\\b")]] {
let expected = messages(&input)
.iter()
.map(|s| (*s).to_string())
.collect::<Vec<_>>();
let out = attach_no_separator_to_first(input);
assert_eq!(
messages(&out),
expected,
"nothing to mark leaves the vector unchanged"
);
}
}
fn match_at_level(message: &str, level: u32) -> RuleMatch {
RuleMatch {
level,
..match_with(message)
}
}
#[test]
fn attach_no_separator_if_top_level_is_gated_on_the_first_rendering_match() {
let cases: &[(&str, Vec<RuleMatch>, Vec<&str>)] = &[
(
"level-0 first fragment takes the marker",
vec![match_at_level("MachO", 0), match_at_level("x86_64", 1)],
vec!["\\bMachO", "x86_64"],
),
(
"continuation first fragment stays spaced",
vec![match_at_level("MPEG ADTS", 1)],
vec!["MPEG ADTS"],
),
(
"message-less level-0 match defers to the level-1 fragment",
vec![match_at_level("", 0), match_at_level("MPEG ADTS", 1)],
vec!["", "MPEG ADTS"],
),
(
"level-0 fragment already marked is not doubled",
vec![match_at_level("\\b[TIFF", 0)],
vec!["\\b[TIFF"],
),
("nothing renders", vec![match_at_level("", 0)], vec![""]),
];
for (name, input, expected) in cases {
let out = attach_no_separator_if_top_level(input.clone());
assert_eq!(messages(&out), *expected, "case: {name}");
}
}
}