Skip to main content

string_analyze/
lib.rs

1pub mod rules;
2mod entropy;
3mod tool;
4
5use rayon::prelude::*;
6use std::borrow::Cow;
7use std::fmt;
8use std::fmt::Formatter;
9
10pub type UsizeRange = (usize, usize);
11
12#[derive(Debug)]
13pub struct Hit<'a, D> {
14    pub describe: Cow<'a, str>,
15    pub importance: u8,
16    pub data: D,
17}
18
19impl<'a> fmt::Display for Hit<'a, RuleResult<'a>> {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        let (reset, yellow, grey) = if f.alternate() {
22            ("\x1b[0m", "\x1b[33m", "\x1b[90m")
23        } else {
24            ("", "", "")
25        };
26
27        // 打印 Hit 头信息
28        writeln!(
29            f,
30            "    {}↳{} [{}{}{}]",
31            grey, reset, yellow, self.describe, reset
32        )?;
33
34        // 打印 Hit 捕获的具体内容
35        for (i, extracted) in self.data.discovered.iter().enumerate() {
36            let cleaned_extracted = extracted.replace('\n', "\n          ");
37            write!(f, "        {}↳{} {}", grey, reset, cleaned_extracted)?;
38            // 如果不是最后一条数据,换行
39            if i < self.data.discovered.len() - 1 {
40                writeln!(f)?;
41            }
42        }
43
44        Ok(())
45    }
46}
47
48#[derive(Debug)]
49pub struct State<'a> {
50    pub input: &'a str,
51    pub ranges: Vec<UsizeRange>,
52}
53
54impl<'a> State<'a> {
55    pub fn new(input: &'a str) -> Self {
56        Self {
57            input,
58            ranges: Vec::new(),
59        }
60    }
61
62    pub fn retain<F>(&mut self, mut f: F)
63    where
64        F: FnMut(&'a str, UsizeRange) -> bool,
65    {
66        let input = self.input;
67        self.ranges.retain(|&r| f(input, r));
68    }
69}
70impl<'a> From<&'a str> for State<'a> {
71    fn from(s: &'a str) -> State<'a> {
72        State::new(s)
73    }
74}
75
76#[derive(Debug)]
77pub struct RuleResult<'a> {
78    pub complete_input: &'a str,
79    pub ranges: Vec<UsizeRange>,
80    pub discovered: Vec<&'a str>,
81}
82
83impl<'a> From<State<'a>> for RuleResult<'a> {
84    fn from(state: State<'a>) -> Self {
85        let discovered = state
86            .ranges
87            .iter()
88            .map(|&(s, e)| &state.input[s..e])
89            .collect();
90        Self {
91            complete_input: state.input,
92            ranges: state.ranges,
93            discovered,
94        }
95    }
96}
97
98pub struct AnyRule {
99    pub flow: Vec<Box<dyn for<'a> Fn(&mut State<'a>) -> bool + Send + Sync>>,
100    pub out: Box<dyn for<'a> Fn(State<'a>) -> Hit<'a, RuleResult<'a>> + Send + Sync>,
101}
102
103impl AnyRule {
104    pub fn new<O>(out: O) -> Self
105    where
106        O: for<'a> Fn(State<'a>) -> Hit<'a, RuleResult<'a>> + Send + Sync + 'static,
107    {
108        Self {
109            flow: Vec::new(),
110            out: Box::new(out),
111        }
112    }
113
114    pub fn add_flow<F>(mut self, flow: F) -> Self
115    where
116        F: for<'a> Fn(&mut State<'a>) -> bool + Send + Sync + 'static,
117    {
118        self.flow.push(Box::new(flow));
119        self
120    }
121
122    pub fn detect<'a>(&self, input: &'a str) -> Option<Hit<'a, RuleResult<'a>>> {
123        let mut state = State::new(input);
124
125        for flow in &self.flow {
126            if !flow(&mut state) {
127                return None;
128            }
129        }
130        Some((self.out)(state))
131    }
132}
133
134pub fn analyze_with<'a>(input: &'a str, rules: &[AnyRule]) -> AnalyzeResult<'a> {
135    AnalyzeResult {
136        input,
137        results: rules
138            .par_iter()
139            .filter_map(|rule| rule.detect(input))
140            .collect(),
141    }
142}
143
144pub struct AnalyzeResult<'a> {
145    pub input: &'a str,
146    pub results: Vec<Hit<'a, RuleResult<'a>>>,
147}
148
149impl<'a> AnalyzeResult<'a> {
150    pub fn new(input: &'a str, results: Vec<Hit<'a, RuleResult<'a>>>) -> Self {
151        Self { input, results }
152    }
153
154    /// 计算概率融合后的风险得分
155    pub fn score(&self) -> u8 {
156        if self.results.is_empty() {
157            return 0;
158        }
159        let mut fail_prob = 1.0f64;
160        for hit in &self.results {
161            let prob = (hit.importance as f64) / 100.0;
162            fail_prob *= 1.0 - prob;
163        }
164        ((1.0 - fail_prob) * 100.0).round() as u8
165    }
166}
167
168impl fmt::Display for AnalyzeResult<'_> {
169    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
170        if self.results.is_empty() {
171            return write!(f, "{}", self.input);
172        }
173
174        // 借助 f.alternate() (即使用 {:#}) 来判断是否应用 ANSI 颜色
175        let ansi = f.alternate();
176        let (reset, red) = if ansi {
177            ("\x1b[0m", "\x1b[31m")
178        } else {
179            ("", "")
180        };
181
182        // 提取并区间合并
183        let mut all_ranges: Vec<UsizeRange> = self
184            .results
185            .iter()
186            .flat_map(|hit| hit.data.ranges.iter().copied())
187            .collect();
188
189        all_ranges.sort_unstable_by_key(|r| r.0);
190
191        let mut merged_ranges: Vec<UsizeRange> = Vec::with_capacity(all_ranges.len());
192        for r in all_ranges {
193            if let Some(last) = merged_ranges.last_mut() {
194                if r.0 <= last.1 {
195                    last.1 = last.1.max(r.1);
196                    continue;
197                }
198            }
199            merged_ranges.push(r);
200        }
201
202        // 直接向 formatter 写入,免去巨大的 String 内存分配开销
203        let mut cursor = 0;
204        for &(start, end) in &merged_ranges {
205            if !self.input.is_char_boundary(start) || !self.input.is_char_boundary(end) {
206                continue;
207            }
208
209            if start > cursor {
210                write!(f, "{}", &self.input[cursor..start])?;
211            }
212            write!(f, "{}{}{}", red, &self.input[start..end], reset)?;
213
214            cursor = end;
215        }
216        if cursor < self.input.len() {
217            write!(f, "{}", &self.input[cursor..])?;
218        }
219
220        writeln!(f)?; // 在原文本和分析报告之间加一个换行
221
222        // 借助 Hit 的 fmt::Display 实现生成 Hits 列表报告
223        for (i, hit) in self.results.iter().enumerate() {
224            if ansi {
225                write!(f, "{:#}", hit)?;
226            } else {
227                write!(f, "{}", hit)?;
228            }
229
230            if i < self.results.len() - 1 {
231                writeln!(f)?;
232            }
233        }
234
235        Ok(())
236    }
237}
238
239
240#[cfg(test)]
241mod tests {
242    use crate::rules::ALL_RULES;
243    use super::*;
244
245    #[test]
246    fn test_comprehensive() {
247        let input = r#"
248Base64: mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI== 444 m7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==
249api_key =
250"12345678abcdefgh";
251        "#;
252
253        let result = analyze_with(input, ALL_RULES.as_slice());
254        let score = result.score();
255
256        println!("[综合测试评分: {score}]\n{:#}", result);
257    }
258}