Skip to main content

string_analyze/
tool.rs

1//! 字符串匹配与启发式特征分析工具模块。
2//!
3//! 本模块提供了底层的高性能字符串搜索算法(子串、字符集、正则),
4//! 以及针对特定编码(如 Base64、Hex)的启发式(Heuristics)校验函数。
5//! 这些工具被广泛应用于扫描规则的执行流中。
6
7#![allow(unused)]
8use crate::State;
9use regex::Regex;
10
11/// 匹配目标抽象特征。
12///
13/// 允许底层匹配函数(如 `has_keyword`)既可以作为一个单纯的布尔检查器(传入 `&str`),
14/// 也可以作为一个范围捕获器(传入 `&mut State`),从而避免重复编写匹配逻辑。
15pub trait MatchTarget<'a> {
16    /// 获取当前正在处理的完整字符串。
17    fn get_input(&self) -> &'a str;
18    /// 清空之前记录的所有匹配区间。
19    fn clear_ranges(&mut self);
20    /// 记录一个新的命中区间 `[start, end)`。
21    fn push_range(&mut self, start: usize, end: usize);
22}
23
24// 针对只读字符串的空实现(仅用于布尔判断,丢弃捕获的区间)
25impl<'a> MatchTarget<'a> for &'a str {
26    #[inline]
27    fn get_input(&self) -> &'a str {
28        self
29    }
30    #[inline]
31    fn clear_ranges(&mut self) {}
32    #[inline]
33    fn push_range(&mut self, _start: usize, _end: usize) {}
34}
35
36// 针对规则状态的实现(会将匹配到的区间真实记录到 State 中)
37impl<'a> MatchTarget<'a> for &mut State<'a> {
38    #[inline]
39    fn get_input(&self) -> &'a str {
40        self.input
41    }
42    #[inline]
43    fn clear_ranges(&mut self) {
44        self.ranges.clear();
45    }
46    #[inline]
47    fn push_range(&mut self, start: usize, end: usize) {
48        self.ranges.push((start, end));
49    }
50}
51
52/// 扫描输入文本是否包含指定的子串关键词。
53///
54/// 匹配到的所有位置会被写入 `target`。
55///
56/// # 参数
57/// - `target`: 匹配目标载体(`&str` 或 `&mut State`)。
58/// - `keyword`: 要查找的子串。
59/// - `ignore_case`: 是否忽略大小写(仅支持 ASCII 忽略大小写,以保证性能)。
60#[inline]
61pub fn has_keyword<'a>(mut target: impl MatchTarget<'a>, keyword: &str, ignore_case: bool) -> bool {
62    target.clear_ranges();
63    let input = target.get_input();
64    let k_len = keyword.len();
65    if k_len == 0 || input.len() < k_len {
66        return false;
67    }
68
69    let mut found = false;
70
71    if !ignore_case {
72        let mut start = 0;
73        while let Some(idx) = input[start..].find(keyword) {
74            let abs_idx = start + idx;
75            target.push_range(abs_idx, abs_idx + k_len);
76            found = true;
77            start = abs_idx + k_len;
78        }
79    } else {
80        // 忽略大小写模式,采用 ASCII 窗口滑动匹配
81        let input_bytes = input.as_bytes();
82        let keyword_bytes = keyword.as_bytes();
83        let mut i = 0;
84        while i <= input_bytes.len() - k_len {
85            let window = &input_bytes[i..i + k_len];
86            if window.eq_ignore_ascii_case(keyword_bytes) {
87                // 确保匹配边界在有效的 UTF-8 字符上
88                if input.is_char_boundary(i) && input.is_char_boundary(i + k_len) {
89                    target.push_range(i, i + k_len);
90                    found = true;
91                }
92                i += k_len;
93            } else {
94                i += 1;
95            }
96        }
97    }
98    found
99}
100
101/// 扫描输入文本是否包含指定的字符集合。
102///
103/// # 参数
104/// - `chars`: 需要查找的字符数组。
105/// - `any`: 如果为 `true`,只要出现 `chars` 中的任意一个字符即算匹配成功;
106///   如果为 `false`,则必须包含 `chars` 中的**所有**不同字符才算成功。
107/// - `ignore_case`: 是否忽略 ASCII 大小写。
108#[inline]
109pub fn has_chars<'a>(
110    mut target: impl MatchTarget<'a>,
111    chars: &[char],
112    any: bool,
113    ignore_case: bool,
114) -> bool {
115    target.clear_ranges();
116    let input = target.get_input();
117
118    if chars.is_empty() || input.is_empty() {
119        return false;
120    }
121
122    let mut target_map = [false; 256];
123    let mut target_count = 0;
124
125    // 1. 构建目标字符掩码表 (仅支持 ASCII)
126    for &c in chars {
127        if c.is_ascii() {
128            let b = if ignore_case {
129                c.to_ascii_lowercase() as usize
130            } else {
131                c as usize
132            };
133            if !target_map[b] {
134                target_map[b] = true;
135                target_count += 1; // 统计有多少种独立的目标字符
136            }
137        }
138    }
139
140    if target_count == 0 {
141        return false;
142    }
143
144    let mut found_map = [false; 256];
145    let mut distinct_found = 0;
146    let mut any_found = false;
147
148    // 2. 扫描输入字符串并记录命中的单字节位置
149    for (i, b) in input.bytes().enumerate() {
150        if !b.is_ascii() {
151            continue;
152        }
153        let val = if ignore_case {
154            b.to_ascii_lowercase() as usize
155        } else {
156            b as usize
157        };
158
159        if target_map[val] {
160            // 标记当前命中字符的区间
161            target.push_range(i, i + 1);
162            any_found = true;
163
164            // 如果要求包含所有指定字符 (!any),需记录当前找到了几种独立字符
165            if !any && !found_map[val] {
166                found_map[val] = true;
167                distinct_found += 1;
168            }
169        }
170    }
171
172    // 3. 校验最终结果
173    if any {
174        any_found
175    } else {
176        let all_found = distinct_found == target_count;
177        if !all_found {
178            // 如果没有找齐所有指定的字符,视为失败,清空捕获区间
179            target.clear_ranges();
180        }
181        all_found
182    }
183}
184
185/// 执行正则表达式扫描,将所有命中区间记录进目标载体。
186#[inline]
187pub fn has_regex<'a>(mut target: impl MatchTarget<'a>, re: &Regex) -> bool {
188    target.clear_ranges();
189    let input = target.get_input();
190    let mut found = false;
191
192    for mat in re.find_iter(input) {
193        target.push_range(mat.start(), mat.end());
194        found = true;
195    }
196    found
197}
198
199/// 计算字符串中大写字母占所有字母(大小写)的比例。
200///
201/// 忽略非字母字符。如果字符串没有英文字母,返回 `0.0`。
202#[inline]
203pub fn upper_prob(input: &str) -> f64 {
204    let mut upper_count = 0;
205    let mut letter_count = 0;
206
207    for b in input.bytes() {
208        if b.is_ascii_uppercase() {
209            upper_count += 1;
210            letter_count += 1;
211        } else if b.is_ascii_lowercase() {
212            letter_count += 1;
213        }
214    }
215
216    if letter_count == 0 {
217        return 0.0;
218    }
219    (upper_count as f64) / (letter_count as f64)
220}
221
222/// 快速检查字符串是否包含 ASCII 大写字母。
223#[inline]
224pub fn has_upper(input: &str) -> bool {
225    input.bytes().any(|b| b.is_ascii_uppercase())
226}
227
228/// 快速检查字符串是否包含 ASCII 小写字母。
229#[inline]
230pub fn has_lower(input: &str) -> bool {
231    input.bytes().any(|b| b.is_ascii_lowercase())
232}
233
234/// 启发式判断字符串是否可能是一段真实的 Base64 编码数据 (支持 URL-Safe & 无填充格式)
235///
236/// # 过滤规则 (Heuristics)
237/// 1. **长度限制**: 必须大于 8 个字符。
238/// 2. **字节对齐**: 真实 Base64 剔除 '=' 后,长度对 4 取模绝对不能为 1。
239/// 3. **填充符校验**: `=` 最多出现 2 次。
240/// 4. **非法结尾**: 有效载荷不能以 `+`, `/`, `-`, `_` 结尾(基于编码填充位的数学特性)。
241/// 5. **混入度校验**: 必须包含数字或特殊字符(`+`, `/`, `-`, `_`, `=`),防止将纯字母当成 Base64。
242/// 6. **连续性检查**: 不能有连续 8 个大写或小写字母,防止英文长单词或全大写常量误报。
243/// 7. **大小写比例**: 大写字母在总字母中的比例必须介于 `0.25` 到 `0.75` 之间。
244pub fn is_base64(s: &str) -> bool {
245    let len = s.len();
246
247    // 1. 基础长度限制,降噪
248    if len <= 8 {
249        return false;
250    }
251
252    // 3. 去除填充符并检查
253    let trimmed = s.trim_end_matches('=');
254    let padding_count = len - trimmed.len();
255
256    if padding_count > 2 {
257        return false;
258    }
259
260    // 2. 字节对齐检查 (核心修改)
261    // 无论是标准还是 URL-Safe,无 '=' 状态下的有效载荷长度 % 4 只能是 0, 2, 3。
262    if trimmed.len() % 4 == 1 {
263        return false;
264    }
265
266    // 4. Base64 有效载荷通常不能以特殊符号结尾
267    // (结尾字符的低位必须是 0 作为隐式填充,因此对应字典表中的字符不可能是这几个)
268    if trimmed.ends_with('/')
269        || trimmed.ends_with('+')
270        || trimmed.ends_with('-')
271        || trimmed.ends_with('_')
272    {
273        return false;
274    }
275
276    let mut upper_count = 0;
277    let mut letter_count = 0;
278
279    let mut has_digit = false;
280    let mut has_special = padding_count > 0;
281
282    let mut consecutive_upper = 0;
283    let mut consecutive_lower = 0;
284
285    // 单次遍历优化 (O(N))
286    for b in trimmed.bytes() {
287        if b.is_ascii_uppercase() {
288            upper_count += 1;
289            letter_count += 1;
290            consecutive_upper += 1;
291            consecutive_lower = 0;
292
293            if consecutive_upper >= 8 {
294                return false;
295            }
296        } else if b.is_ascii_lowercase() {
297            letter_count += 1;
298            consecutive_lower += 1;
299            consecutive_upper = 0;
300
301            if consecutive_lower >= 8 {
302                return false;
303            }
304        } else {
305            // 打断连续性
306            consecutive_upper = 0;
307            consecutive_lower = 0;
308
309            if b.is_ascii_digit() {
310                has_digit = true;
311            } else if b == b'+' || b == b'/' || b == b'-' || b == b'_' {
312                // 将 - 和 _ 纳入特殊字符检测
313                has_special = true;
314            }
315        }
316    }
317
318    // 5. 必须包含至少一个数字或特殊符号
319    if !has_digit && !has_special {
320        return false;
321    }
322
323    // 避免除零异常
324    if letter_count == 0 {
325        return false;
326    }
327
328    // 7. 校验大小写字母的分布比例
329    let prob = (upper_count as f64) / (letter_count as f64);
330    if prob <= 0.25 || prob >= 0.75 {
331        return false;
332    }
333
334    true
335}
336
337/// 统计字符串中各 ASCII 字符的出现频率,并按降序排序。
338///
339/// # 返回值
340/// 返回一个向量,元素为 `(字符, 出现次数)`,按次数降序排列,次数相同时按字符升序排列。
341#[inline]
342pub fn sort_ascii_counts(input: &str, ignore_case: bool) -> Vec<(char, usize)> {
343    let mut counts = [0usize; 256];
344
345    for b in input.bytes() {
346        if b.is_ascii() {
347            let val = if ignore_case {
348                b.to_ascii_lowercase() as usize
349            } else {
350                b as usize
351            };
352            counts[val] += 1;
353        }
354    }
355
356    let mut result: Vec<(char, usize)> = counts
357        .iter()
358        .enumerate()
359        .filter(|&(_, &count)| count > 0)
360        .map(|(b, &count)| (b as u8 as char, count))
361        .collect();
362
363    // 优先按次数降序,次数相同则按字符 ASCII 码升序,保证稳定性
364    result.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
365
366    result
367}
368
369/// 启发式判断字符串是否为一段连续或格式化的十六进制 (Hex) 数据。
370///
371/// 旨在精准提取代码或内存转储中的 Hex 序列,如 `\xAA\xBB`, `0x1a, 0x2b`, `AABBCC` 等。
372///
373/// # 过滤规则 (Heuristics)
374/// 1. 过滤短文本,总长度需大于 10。
375/// 2. 支持并提取常见前缀 (`0x`, `0X`, `\x`, `\X`, `%`)。
376/// 3. **一致性检查**: 一个 Hex 串中的字母**不允许大小写混杂** (要么全是 `a-f`,要么全是 `A-F`)。
377/// 4. **分隔符推导**: 如果字节之间有间隔,会自动推导分隔符(如空格、逗号等),并要求整个序列遵循该分隔符。
378/// 5. 分隔符本身不能包含字母或数字,以防止错误截断。
379pub fn is_hex(input: &str) -> bool {
380    let mut s = input.trim();
381    if s.is_empty() || !s.is_ascii() || s.len() <= 10 {
382        return false;
383    }
384
385    // 处理全局仅带有一次前缀的情况 (例如 0xAABBCC),剥离前缀当做纯 Hex 处理
386    if (s.starts_with("0x") || s.starts_with("0X") || s.starts_with("\\x") || s.starts_with("\\X"))
387        && s[2..].find(&s[0..2]).is_none()
388    {
389        s = &s[2..];
390    }
391
392    // 提取重复性前缀
393    let prefix = if s.starts_with("0x") {
394        "0x"
395    } else if s.starts_with("0X") {
396        "0X"
397    } else if s.starts_with("\\x") {
398        "\\x"
399    } else if s.starts_with("\\X") {
400        "\\X"
401    } else if s.starts_with("%") {
402        "%"
403    } else {
404        ""
405    };
406
407    let mut idx = prefix.len();
408    let bytes = s.as_bytes();
409
410    if idx + 2 > bytes.len() {
411        return false;
412    }
413
414    let mut upper_hex = false;
415    let mut lower_hex = false;
416
417    // 内部校验是否为有效的 Hex 字符,并记录大小写状态
418    #[inline]
419    fn check_hex(b: u8, upper: &mut bool, lower: &mut bool) -> bool {
420        if b.is_ascii_digit() {
421            return true;
422        }
423        if (b'a'..=b'f').contains(&b) {
424            *lower = true;
425            return true;
426        }
427        if (b'A'..=b'F').contains(&b) {
428            *upper = true;
429            return true;
430        }
431        false
432    }
433
434    if !check_hex(bytes[idx], &mut upper_hex, &mut lower_hex)
435        || !check_hex(bytes[idx + 1], &mut upper_hex, &mut lower_hex)
436    {
437        return false;
438    }
439    idx += 2;
440
441    // 推导字节之间的分隔符(如: 0xAA[分隔符]0xBB)
442    let separator = if idx == bytes.len() {
443        ""
444    } else {
445        if !prefix.is_empty() {
446            if let Some(next_prefix_idx) = s[idx..].find(prefix) {
447                &s[idx..idx + next_prefix_idx]
448            } else {
449                return false;
450            }
451        } else {
452            let mut sep_len = 0;
453            while idx + sep_len < bytes.len() {
454                let b = bytes[idx + sep_len];
455                if b.is_ascii_hexdigit() {
456                    break;
457                }
458                sep_len += 1;
459            }
460            &s[idx..idx + sep_len]
461        }
462    };
463
464    // 严禁分隔符包含字母和数字,避免解析错乱
465    if !separator.is_empty() && separator.bytes().any(|b| b.is_ascii_alphanumeric()) {
466        return false;
467    }
468
469    let mut curr_idx = 0;
470    let mut count = 0;
471
472    // 遍历循环验证整条数据链
473    while curr_idx < bytes.len() {
474        if !s[curr_idx..].starts_with(prefix) {
475            return false;
476        }
477        curr_idx += prefix.len();
478
479        if curr_idx + 2 > bytes.len() {
480            return false;
481        }
482
483        if !check_hex(bytes[curr_idx], &mut upper_hex, &mut lower_hex)
484            || !check_hex(bytes[curr_idx + 1], &mut upper_hex, &mut lower_hex)
485        {
486            return false;
487        }
488
489        // 核心规则:十六进制字符串中的字母大小写必须保持一致
490        if upper_hex && lower_hex {
491            return false;
492        }
493
494        curr_idx += 2;
495        count += 1;
496
497        if curr_idx == bytes.len() {
498            break;
499        }
500
501        if !s[curr_idx..].starts_with(separator) {
502            return false;
503        }
504        curr_idx += separator.len();
505
506        if curr_idx == bytes.len() {
507            return false; // 结尾不能带着悬空的分隔符
508        }
509    }
510    count > 0
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn test() {
519        has_keyword("", "", true);
520        has_keyword(&mut State::new(""), "", true);
521    }
522}