1#![doc = include_str!("../README.md")]
2
3pub mod entropy;
4pub mod rules;
5pub mod tool;
6
7use rayon::prelude::*;
8use std::borrow::Cow;
9use std::fmt;
10use std::fmt::Formatter;
11
12pub type UsizeRange = (usize, usize);
14
15#[derive(Debug)]
19pub struct Hit<'a, D> {
20 pub describe: Cow<'a, str>,
22 pub importance: u8,
24 pub data: D,
26}
27
28impl<'a> fmt::Display for Hit<'a, RuleResult<'a>> {
29 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
33 let (reset, yellow, grey) = if f.alternate() {
34 ("\x1b[0m", "\x1b[33m", "\x1b[90m")
35 } else {
36 ("", "", "")
37 };
38
39 writeln!(
41 f,
42 " {}↳{} [{}{}{}]",
43 grey, reset, yellow, self.describe, reset
44 )?;
45
46 for (i, extracted) in self.data.discovered.iter().enumerate() {
48 let cleaned_extracted = extracted.replace('\n', "\n ");
50 write!(f, " {}↳{} {}", grey, reset, cleaned_extracted)?;
51 if i < self.data.discovered.len() - 1 {
53 writeln!(f)?;
54 }
55 }
56
57 Ok(())
58 }
59}
60
61#[derive(Debug)]
66pub struct State<'a> {
67 pub input: &'a str,
69 pub ranges: Vec<UsizeRange>,
71}
72
73impl<'a> State<'a> {
74 pub fn new(input: &'a str) -> Self {
76 Self {
77 input,
78 ranges: Vec::new(),
79 }
80 }
81
82 pub fn retain<F>(&mut self, mut f: F)
86 where
87 F: FnMut(&'a str, UsizeRange) -> bool,
88 {
89 let input = self.input;
90 self.ranges.retain(|&r| f(input, r));
91 }
92}
93
94impl<'a> From<&'a str> for State<'a> {
95 fn from(s: &'a str) -> State<'a> {
96 State::new(s)
97 }
98}
99
100#[derive(Debug)]
105pub struct RuleResult<'a> {
106 pub complete_input: &'a str,
108 pub ranges: Vec<UsizeRange>,
110 pub discovered: Vec<&'a str>,
112}
113
114impl<'a> From<State<'a>> for RuleResult<'a> {
115 fn from(state: State<'a>) -> Self {
117 let discovered = state
118 .ranges
119 .iter()
120 .map(|&(s, e)| &state.input[s..e])
121 .collect();
122 Self {
123 complete_input: state.input,
124 ranges: state.ranges,
125 discovered,
126 }
127 }
128}
129
130pub struct AnyRule {
136 #[allow(clippy::type_complexity)]
138 pub flow: Vec<Box<dyn for<'a> Fn(&mut State<'a>) -> bool + Send + Sync>>,
139 #[allow(clippy::type_complexity)]
141 pub out: Box<dyn for<'a> Fn(State<'a>) -> Hit<'a, RuleResult<'a>> + Send + Sync>,
142}
143
144impl AnyRule {
145 pub fn new<O>(out: O) -> Self
147 where
148 O: for<'a> Fn(State<'a>) -> Hit<'a, RuleResult<'a>> + Send + Sync + 'static,
149 {
150 Self {
151 flow: Vec::new(),
152 out: Box::new(out),
153 }
154 }
155
156 pub fn add_flow<F>(mut self, flow: F) -> Self
160 where
161 F: for<'a> Fn(&mut State<'a>) -> bool + Send + Sync + 'static,
162 {
163 self.flow.push(Box::new(flow));
164 self
165 }
166
167 pub fn detect<'a>(&self, input: &'a str) -> Option<Hit<'a, RuleResult<'a>>> {
172 let mut state = State::new(input);
173
174 for flow in &self.flow {
175 if !flow(&mut state) {
176 return None;
177 }
178 }
179 Some((self.out)(state))
180 }
181}
182
183pub fn analyze_with<'a>(input: &'a str, rules: &[AnyRule]) -> AnalyzeResult<'a> {
194 AnalyzeResult {
195 input,
196 results: rules
197 .par_iter()
198 .filter_map(|rule| rule.detect(input))
199 .collect(),
200 }
201}
202
203pub struct AnalyzeResult<'a> {
207 pub input: &'a str,
209 pub results: Vec<Hit<'a, RuleResult<'a>>>,
211}
212
213impl<'a> AnalyzeResult<'a> {
214 pub fn new(input: &'a str, results: Vec<Hit<'a, RuleResult<'a>>>) -> Self {
216 Self { input, results }
217 }
218
219 pub fn score(&self) -> u8 {
226 if self.results.is_empty() {
227 return 0;
228 }
229 let mut fail_prob = 1.0f64;
230 for hit in &self.results {
231 let prob = (hit.importance as f64) / 100.0;
232 fail_prob *= 1.0 - prob;
233 }
234 ((1.0 - fail_prob) * 100.0).round() as u8
235 }
236}
237
238impl fmt::Display for AnalyzeResult<'_> {
239 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
247 if self.results.is_empty() {
248 return write!(f, "{}", self.input);
249 }
250
251 let ansi = f.alternate();
253 let (reset, red) = if ansi {
254 ("\x1b[0m", "\x1b[31m")
255 } else {
256 ("", "")
257 };
258
259 let mut all_ranges: Vec<UsizeRange> = self
261 .results
262 .iter()
263 .flat_map(|hit| hit.data.ranges.iter().copied())
264 .collect();
265
266 all_ranges.sort_unstable_by_key(|r| r.0);
268
269 let mut merged_ranges: Vec<UsizeRange> = Vec::with_capacity(all_ranges.len());
270 for r in all_ranges {
271 if let Some(last) = merged_ranges.last_mut() {
272 if r.0 <= last.1 {
274 last.1 = last.1.max(r.1);
275 continue;
276 }
277 }
278 merged_ranges.push(r);
279 }
280
281 let mut cursor = 0;
283 for &(start, end) in &merged_ranges {
284 if !self.input.is_char_boundary(start) || !self.input.is_char_boundary(end) {
286 continue;
287 }
288
289 if start > cursor {
290 write!(f, "{}", &self.input[cursor..start])?;
291 }
292 write!(f, "{}{}{}", red, &self.input[start..end], reset)?;
294
295 cursor = end;
296 }
297 if cursor < self.input.len() {
299 write!(f, "{}", &self.input[cursor..])?;
300 }
301
302 writeln!(f)?; for (i, hit) in self.results.iter().enumerate() {
306 if ansi {
307 write!(f, "{:#}", hit)?;
308 } else {
309 write!(f, "{}", hit)?;
310 }
311
312 if i < self.results.len() - 1 {
313 writeln!(f)?;
314 }
315 }
316
317 Ok(())
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::rules::ALL_RULES;
325
326 #[test]
327 fn test_comprehensive() {
328 let input = r#"
329Base64: mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI== 444 m7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==
330api_key =
331"12345678abcdefgh";
332 "#;
333
334 let result = analyze_with(input, ALL_RULES.as_slice());
335 let score = result.score();
336
337 println!("[综合测试评分: {score}]\n{:#}", result);
339 }
340}