provenant/license_detection/
automaton.rs1use crate::license_detection::models::RuleId;
12use daachorse::DoubleArrayAhoCorasick;
13use rancor::Fallible;
14use rkyv::with::{ArchiveWith, DeserializeWith, SerializeWith};
15use rkyv::{Archive, Deserialize, Place, Serialize};
16
17pub struct AsBytes;
19
20impl ArchiveWith<Automaton> for AsBytes {
21 type Archived = <Vec<u8> as Archive>::Archived;
22 type Resolver = <Vec<u8> as Archive>::Resolver;
23
24 fn resolve_with(field: &Automaton, resolver: Self::Resolver, out: Place<Self::Archived>) {
25 field.serialize_bytes().resolve(resolver, out);
26 }
27}
28
29impl<S: Fallible + rkyv::ser::Writer + rkyv::ser::Allocator + ?Sized> SerializeWith<Automaton, S>
30 for AsBytes
31{
32 fn serialize_with(field: &Automaton, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
33 field.serialize_bytes().serialize(serializer)
34 }
35}
36
37impl<D: Fallible + ?Sized> DeserializeWith<<Vec<u8> as Archive>::Archived, Automaton, D> for AsBytes
38where
39 <Vec<u8> as Archive>::Archived: Deserialize<Vec<u8>, D>,
40 D::Error: rancor::Source,
41{
42 fn deserialize_with(
43 field: &<Vec<u8> as Archive>::Archived,
44 deserializer: &mut D,
45 ) -> Result<Automaton, D::Error> {
46 let bytes: Vec<u8> = field.deserialize(deserializer)?;
51 Automaton::deserialize(&bytes).map_err(|e| {
52 rancor::Source::new(std::io::Error::new(
55 std::io::ErrorKind::InvalidData,
56 format!("invalid serialized automaton: {e}"),
57 ))
58 })
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Match {
65 pub rule_id: RuleId,
67 pub start: usize,
69 pub end: usize,
71}
72
73pub struct Automaton {
78 inner: DoubleArrayAhoCorasick<u32>,
79}
80
81impl std::fmt::Debug for Automaton {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("Automaton")
84 .field("num_states", &self.inner.num_states())
85 .field("heap_bytes", &self.inner.heap_bytes())
86 .finish()
87 }
88}
89
90impl Clone for Automaton {
91 fn clone(&self) -> Self {
92 let bytes = self.inner.serialize();
93 Self::deserialize_unchecked(&bytes)
94 }
95}
96
97impl Automaton {
98 pub fn empty() -> Self {
103 let dummy_pattern: &[u8] = &[0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8];
106 let inner = DoubleArrayAhoCorasick::new([dummy_pattern])
107 .expect("Failed to create empty automaton with hardcoded dummy pattern");
108 Self { inner }
109 }
110
111 pub fn find_overlapping_iter(&self, haystack: &[u8]) -> FindOverlappingIter {
121 FindOverlappingIter::new(&self.inner, haystack)
122 }
123
124 pub fn deserialize(bytes: &[u8]) -> Result<Self, daachorse::errors::DaachorseError> {
131 let (ac, _) = DoubleArrayAhoCorasick::deserialize(bytes)?;
132 Ok(Self { inner: ac })
133 }
134
135 pub fn deserialize_unchecked(bytes: &[u8]) -> Self {
143 let (ac, _) = unsafe { DoubleArrayAhoCorasick::deserialize_unchecked(bytes) };
144 Self { inner: ac }
145 }
146
147 pub fn num_states(&self) -> usize {
149 self.inner.num_states()
150 }
151
152 pub fn heap_bytes(&self) -> usize {
154 self.inner.heap_bytes()
155 }
156
157 pub fn serialize_bytes(&self) -> Vec<u8> {
159 self.inner.serialize()
160 }
161}
162
163impl Default for Automaton {
164 fn default() -> Self {
165 Self::empty()
166 }
167}
168
169pub struct FindOverlappingIter {
178 inner: std::vec::IntoIter<daachorse::Match<u32>>,
179}
180
181impl FindOverlappingIter {
182 fn new(automaton: &DoubleArrayAhoCorasick<u32>, haystack: &[u8]) -> Self {
183 let matches: Vec<_> = automaton.find_overlapping_iter(haystack).collect();
184 Self {
185 inner: matches.into_iter(),
186 }
187 }
188}
189
190impl Iterator for FindOverlappingIter {
191 type Item = Match;
192
193 fn next(&mut self) -> Option<Self::Item> {
194 loop {
195 let m = self.inner.next()?;
196 if m.start() % 2 == 0 {
199 return Some(Match {
200 rule_id: RuleId::new(m.value() as usize),
201 start: m.start(),
202 end: m.end(),
203 });
204 }
205 }
207 }
208}
209
210pub struct AutomatonBuilder {
214 patterns: Vec<Vec<u8>>,
215 values: Vec<u32>,
216}
217
218impl AutomatonBuilder {
219 pub fn new() -> Self {
221 Self {
222 patterns: Vec::new(),
223 values: Vec::new(),
224 }
225 }
226
227 pub fn add_pattern_with_value(&mut self, pattern: &[u8], value: u32) {
232 if !pattern.is_empty() {
233 self.patterns.push(pattern.to_vec());
234 self.values.push(value);
235 }
236 }
237
238 pub fn add_pattern(&mut self, pattern: &[u8]) {
242 let value = self.patterns.len() as u32;
243 self.add_pattern_with_value(pattern, value);
244 }
245
246 pub fn build(self) -> Automaton {
251 if self.patterns.is_empty() {
252 return Automaton::empty();
253 }
254
255 let patvals: Vec<(&[u8], u32)> = self
256 .patterns
257 .iter()
258 .zip(self.values.iter())
259 .map(|(p, &v)| (p.as_slice(), v))
260 .collect();
261
262 match DoubleArrayAhoCorasick::with_values(patvals) {
263 Ok(ac) => Automaton { inner: ac },
264 Err(_) => Automaton::empty(),
265 }
266 }
267}
268
269impl Default for AutomatonBuilder {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_token_boundary_filtering() {
281 let pattern: &[u8] = &[31, 49];
282 let mut builder = AutomatonBuilder::new();
283 builder.add_pattern(pattern);
284 let ac = builder.build();
285
286 let haystack: &[u8] = &[109, 31, 49, 74];
289 let matches: Vec<_> = ac.find_overlapping_iter(haystack).collect();
290 assert!(
291 matches.is_empty(),
292 "Should not match across token boundaries"
293 );
294 }
295
296 #[test]
297 fn test_valid_token_match() {
298 let pattern: &[u8] = &[31, 49];
299 let mut builder = AutomatonBuilder::new();
300 builder.add_pattern(pattern);
301 let ac = builder.build();
302
303 let haystack: &[u8] = &[0, 0, 31, 49, 0, 0];
304 let matches: Vec<_> = ac.find_overlapping_iter(haystack).collect();
305 assert_eq!(matches.len(), 1);
306 assert_eq!(matches[0].start, 2);
307 assert_eq!(matches[0].end, 4);
308 }
309
310 #[test]
311 fn test_builder_skips_empty_patterns() {
312 let mut builder = AutomatonBuilder::new();
313 builder.add_pattern(b"");
314 builder.add_pattern(b"hello");
315 builder.add_pattern(b"");
316 let ac = builder.build();
317
318 let matches: Vec<_> = ac.find_overlapping_iter(b"hello").collect();
319 assert_eq!(matches.len(), 1);
320 }
321
322 #[test]
323 fn test_builder_with_values() {
324 let mut builder = AutomatonBuilder::new();
325 builder.add_pattern_with_value(b"hello", 42);
326 builder.add_pattern_with_value(b"world", 99);
327 let ac = builder.build();
328
329 let matches: Vec<_> = ac.find_overlapping_iter(b"hello world").collect();
330 assert_eq!(matches.len(), 2);
331 assert_eq!(matches[0].rule_id, RuleId::new(42));
332 assert_eq!(matches[1].rule_id, RuleId::new(99));
333 }
334
335 #[test]
336 fn test_builder_duplicate_patterns() {
337 let mut builder = AutomatonBuilder::new();
338 builder.add_pattern_with_value(b"hello", 10);
339 builder.add_pattern_with_value(b"hello", 20);
340 let ac = builder.build();
341
342 let matches: Vec<_> = ac.find_overlapping_iter(b"hello").collect();
343 assert_eq!(matches.len(), 2);
344 let mut values: Vec<RuleId> = matches.iter().map(|m| m.rule_id).collect();
345 values.sort();
346 assert_eq!(values, vec![RuleId::new(10), RuleId::new(20)]);
347 }
348}