Skip to main content

string_analyze/
lib.rs

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
12/// 字符串索引范围别名,表示一段文本的 `(start, end)` 字节位置。
13pub type UsizeRange = (usize, usize);
14
15/// 规则命中的结果包装器。
16///
17/// 包含了命中的描述信息、重要程度(用于风险评分)以及附加数据。
18#[derive(Debug)]
19pub struct Hit<'a, D> {
20    /// 规则的名称或描述信息,支持借用或拥有所有权的字符串。
21    pub describe: Cow<'a, str>,
22    /// 该规则的重要程度(0-100),用于后续计算综合风险评分。
23    pub importance: u8,
24    /// 命中的具体数据内容(通常是 `RuleResult`)。
25    pub data: D,
26}
27
28impl<'a> fmt::Display for Hit<'a, RuleResult<'a>> {
29    /// 格式化命中结果,用于控制台输出。
30    ///
31    /// 如果使用了替代格式化标志 `{:#}`,则会输出带有 ANSI 颜色的文本。
32    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        // 打印 Hit 头信息,例如: ↳ [Base64]
40        writeln!(
41            f,
42            "    {}↳{} [{}{}{}]",
43            grey, reset, yellow, self.describe, reset
44        )?;
45
46        // 打印 Hit 捕获的具体内容
47        for (i, extracted) in self.data.discovered.iter().enumerate() {
48            // 对多行文本进行缩进处理,保持输出美观
49            let cleaned_extracted = extracted.replace('\n', "\n          ");
50            write!(f, "        {}↳{} {}", grey, reset, cleaned_extracted)?;
51            // 如果不是最后一条数据,换行
52            if i < self.data.discovered.len() - 1 {
53                writeln!(f)?;
54            }
55        }
56
57        Ok(())
58    }
59}
60
61/// 规则执行过程中的中间状态。
62///
63/// 随着规则流(`flow`)的执行,`ranges` 可能会被修改、过滤或新增,
64/// 最终通过的所有 `ranges` 将作为命中的结果。
65#[derive(Debug)]
66pub struct State<'a> {
67    /// 待分析的原始输入文本。
68    pub input: &'a str,
69    /// 当前阶段捕获到的文本范围集合。
70    pub ranges: Vec<UsizeRange>,
71}
72
73impl<'a> State<'a> {
74    /// 创建一个新的空状态,准备开始规则匹配。
75    pub fn new(input: &'a str) -> Self {
76        Self {
77            input,
78            ranges: Vec::new(),
79        }
80    }
81
82    /// 根据给定的闭包条件,保留符合要求的范围,移除不符合的范围。
83    ///
84    /// 此方法常用于在规则的后续步骤中对初步匹配结果进行二次过滤。
85    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/// 规则执行完成后的最终提取结果。
101///
102/// 该结构体由 `State` 转换而来,除了保留索引范围外,
103/// 还会实际切片提取出对应的字符串片段。
104#[derive(Debug)]
105pub struct RuleResult<'a> {
106    /// 原始的完整输入文本。
107    pub complete_input: &'a str,
108    /// 最终命中的所有文本范围。
109    pub ranges: Vec<UsizeRange>,
110    /// 根据 `ranges` 从 `complete_input` 中切片提取出的实际文本内容。
111    pub discovered: Vec<&'a str>,
112}
113
114impl<'a> From<State<'a>> for RuleResult<'a> {
115    /// 将中间状态 `State` 转换为最终的 `RuleResult`。
116    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
130/// 灵活的规则定义器。
131///
132/// 一个规则由一系列的条件流(`flow`)和一个输出生成器(`out`)组成。
133/// 只有当 `flow` 中的所有条件都返回 `true` 时,规则才算命中,
134/// 并调用 `out` 生成最终的 `Hit` 报告。
135pub struct AnyRule {
136    /// 规则流:一系列依次执行的闭包,可修改 `State` 并返回是否继续执行。
137    #[allow(clippy::type_complexity)]
138    pub flow: Vec<Box<dyn for<'a> Fn(&mut State<'a>) -> bool + Send + Sync>>,
139    /// 结果生成器:当所有 flow 都通过时,将 `State` 转换为 `Hit`。
140    #[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    /// 创建一个新的规则,指定命中时的结果生成器。
146    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    /// 向规则流中追加一个处理步骤。
157    ///
158    /// 可以链式调用。
159    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    /// 针对输入文本执行当前规则。
168    ///
169    /// 如果中途有任何一个 `flow` 返回 `false`,则返回 `None`。
170    /// 否则返回包含提取数据的 `Hit`。
171    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
183/// 使用指定的规则集对文本进行并行分析。
184///
185/// 借助 `rayon`,所有的规则将并发执行以提升扫描效率。
186///
187/// # 参数
188/// - `input`: 待分析的完整文本。
189/// - `rules`: 需要应用的规则切片。
190///
191/// # 返回值
192/// 返回包含所有命中结果的 `AnalyzeResult`。
193pub 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
203/// 综合分析报告。
204///
205/// 包含了原始文本以及所有成功命中的规则结果。
206pub struct AnalyzeResult<'a> {
207    /// 原始被分析的文本。
208    pub input: &'a str,
209    /// 成功命中的规则及结果列表。
210    pub results: Vec<Hit<'a, RuleResult<'a>>>,
211}
212
213impl<'a> AnalyzeResult<'a> {
214    /// 创建一个新的分析报告实例。
215    pub fn new(input: &'a str, results: Vec<Hit<'a, RuleResult<'a>>>) -> Self {
216        Self { input, results }
217    }
218
219    /// 计算概率融合后的综合风险得分 (0 - 100)。
220    ///
221    /// # 算法说明
222    /// 采用独立事件的概率联合公式:
223    /// `P(联合) = 1 - ( (1 - P1) * (1 - P2) * ... )`
224    /// 这意味着多条规则同时命中时,综合得分会向 100 逼近,但不会超过 100。
225    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    /// 格式化输出完整的分析报告。
240    ///
241    /// 该实现会完成两项工作:
242    /// 1. 打印原始文本,并将所有命中规则的文本片段进行高亮(支持重叠区间的合并合并)。
243    /// 2. 依次列出所有命中规则的详细信息(名称、提取内容等)。
244    ///
245    /// 支持使用 `{:#}` 开启 ANSI 颜色输出。
246    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
247        if self.results.is_empty() {
248            return write!(f, "{}", self.input);
249        }
250
251        // 借助 f.alternate() (即使用 {:#}) 来判断是否应用 ANSI 颜色
252        let ansi = f.alternate();
253        let (reset, red) = if ansi {
254            ("\x1b[0m", "\x1b[31m")
255        } else {
256            ("", "")
257        };
258
259        // 提取所有的命中区间并进行合并处理,防止高亮输出时发生错乱
260        let mut all_ranges: Vec<UsizeRange> = self
261            .results
262            .iter()
263            .flat_map(|hit| hit.data.ranges.iter().copied())
264            .collect();
265
266        // 按起点排序
267        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                // 如果当前区间与上一个区间有重叠,则合并区间
273                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        // 边遍历边向 formatter 写入文本和高亮代码,免去巨大的 String 内存分配开销
282        let mut cursor = 0;
283        for &(start, end) in &merged_ranges {
284            // 安全检查:确保切片边界落在合法的 UTF-8 字符边界上
285            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            // 写入高亮部分
293            write!(f, "{}{}{}", red, &self.input[start..end], reset)?;
294
295            cursor = end;
296        }
297        // 补全末尾未高亮的文本
298        if cursor < self.input.len() {
299            write!(f, "{}", &self.input[cursor..])?;
300        }
301
302        writeln!(f)?; // 在原文本和分析报告之间加一个换行
303
304        // 借助 Hit 的 fmt::Display 实现生成 Hits 列表报告
305        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        // 使用 {:#} 触发带颜色的控制台输出
338        println!("[综合测试评分: {score}]\n{:#}", result);
339    }
340}