guard_core_engine/patterns/
binary.rs1#[must_use]
19pub fn build_binary_prefix(text: &str) -> Vec<u32> {
20 let mut counts = Vec::with_capacity(text.chars().count() + 1);
21 counts.push(0u32);
22 let mut count = 0u32;
23 for c in text.chars() {
24 if is_binary_artifact(c) {
25 count += 1;
26 }
27 counts.push(count);
28 }
29 counts
30}
31
32const BINARY_DENSITY_RADIUS: usize = 64;
33const BINARY_DENSITY_LIMIT: u32 = 4;
34
35#[must_use]
36pub fn match_is_binary_density(
37 binary_prefix: &[u32],
38 match_start: usize,
39 match_end: usize,
40) -> bool {
41 if binary_prefix.is_empty() {
42 return false;
43 }
44 let high = (match_end + BINARY_DENSITY_RADIUS).min(binary_prefix.len() - 1);
45 let low = match_start.saturating_sub(BINARY_DENSITY_RADIUS);
46 binary_prefix[high].saturating_sub(binary_prefix[low]) >= BINARY_DENSITY_LIMIT
47}
48
49#[must_use]
50pub(crate) fn is_binary_artifact(c: char) -> bool {
51 let cp = c as u32;
52 if (cp < 0x20 && !matches!(cp, 0x09 | 0x0A | 0x0D)) || cp == 0x7F {
54 return true;
55 }
56 if cp == 0xFFFD {
58 return true;
59 }
60 #[cfg(not(coverage))] if (0xDC80..=0xDCFF).contains(&cp) {
63 return true;
64 }
65 if (0x80..=0x024F).contains(&cp) {
67 return !is_text_allowlist(c);
68 }
69 false
70}
71
72#[must_use]
77fn is_text_allowlist(c: char) -> bool {
78 let cp = c as u32;
79 if (0x00C0..=0x00FF).contains(&cp) {
80 return true;
81 }
82 if matches!(
83 cp,
84 0x00A3 | 0x00A5 | 0x00AA | 0x00B0 | 0x00B1 | 0x00B2 | 0x00B3 | 0x00B5 | 0x00B9 | 0x00BA
85 ) {
86 return true;
87 }
88 (0x0100..=0x017F).contains(&cp)
89}
90
91const BINARY_CONTENT_RATIO_THRESHOLD: f64 = 0.2;
92
93#[must_use]
95pub fn looks_like_binary_content(content: &str) -> bool {
96 if content.is_empty() {
97 return false;
98 }
99 let mut non_text = 0usize;
100 let mut len = 0usize;
101 for c in content.chars() {
102 len += 1;
103 let cp = c as u32;
104 let is_ws = matches!(c, '\t' | '\r' | '\n');
105 let printable = !c.is_control() && !matches!(cp, 0xFFFD);
106 if !is_ws && (!printable || cp == 0xFFFD) {
107 non_text += 1;
108 }
109 }
110 let ratio = non_text as f64 / len as f64;
111 ratio >= BINARY_CONTENT_RATIO_THRESHOLD
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn pure_text_has_zero_artifacts() {
120 let prefix = build_binary_prefix("hello <script> world\t\n");
121 assert_eq!(prefix.last(), Some(&0));
122 }
123
124 #[test]
125 fn control_bytes_count() {
126 let prefix = build_binary_prefix("a\u{1}b\u{2}c\u{3}d\u{4}");
127 assert!(match_is_binary_density(&prefix, 0, 8));
128 }
129
130 #[test]
131 fn accented_text_is_allowed() {
132 let prefix = build_binary_prefix("caf\u{00e9} cr\u{00e8}me br\u{00fb}l\u{00e9}");
133 assert_eq!(prefix.last(), Some(&0));
134 }
135
136 #[test]
137 fn artifact_classes_match_reference_exactly() {
138 const ARTIFACT_RANGES: &[(u32, u32)] = &[
142 (0x00, 0x08),
143 (0x0B, 0x0C),
144 (0x0E, 0x1F),
145 (0x7F, 0xA2),
146 (0xA4, 0xA4),
147 (0xA6, 0xA9),
148 (0xAB, 0xAF),
149 (0xB4, 0xB4),
150 (0xB6, 0xB8),
151 (0xBB, 0xBF),
152 (0x0180, 0x024F),
153 ];
154 let is_ref_artifact = |cp: u32| {
155 ARTIFACT_RANGES
156 .iter()
157 .any(|(lo, hi)| (*lo..=*hi).contains(&cp))
158 };
159 for cp in 0u32..=0x3000 {
160 let c = char::from_u32(cp).expect("BMP scalar");
161 assert_eq!(
162 is_binary_artifact(c),
163 is_ref_artifact(cp),
164 "artifact mismatch at U+{cp:04X}"
165 );
166 }
167 }
168
169 #[test]
170 fn latin1_punctuation_signs_are_text() {
171 let prefix = build_binary_prefix("3 \u{00d7} 4 \u{00f7} 2");
174 assert_eq!(prefix.last(), Some(&0));
175 }
176}
177
178#[cfg(test)]
179mod coverage_tests {
180 use super::*;
181
182 #[test]
183 fn density_gate_needs_a_prefix_and_enough_high_bytes() {
184 assert!(!match_is_binary_density(&[], 0, 1));
186 let prefix = vec![0_u32, 0, 0, 0, 3, 3];
188 assert!(!match_is_binary_density(&prefix, 2, 3));
189 let dense = vec![0_u32, 9, 9, 9, 9, 9];
191 assert!(match_is_binary_density(&dense, 2, 3));
192 }
193
194 #[test]
195 fn artifact_chars_cover_control_replacement_surrogate_and_latin() {
196 assert!(is_binary_artifact('\u{1}'));
197 assert!(is_binary_artifact('\u{7f}'));
198 assert!(is_binary_artifact('\u{fffd}'));
199 assert!(!is_binary_artifact(
200 char::from_u32(0x0301).expect("in range")
201 ));
202 assert!(is_binary_artifact('\u{80}'));
203 assert!(!is_binary_artifact('\t'));
204 assert!(!is_binary_artifact('a'));
205 }
206}