Skip to main content

security_rust/
pet.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! 项目宠物:**甲哨 Sentri**。
4//!
5//! 本模块不含逻辑,只有形象本身 —— 文档、示例、下游的 WAF 管理界面用同一份,
6//! 不必各自抄一遍。形象文件在 `docs/pet.svg`,终端里用 [`ASCII`]。
7//!
8//! ```text
9//!       (o)(o)
10//!    ⌕┬─────────┬!     甲哨 Sentri
11//!     │ · · · · │       只报告,不拦截
12//!     └──┬───┬──┘       32 detectors / 4 categories
13//!       /     \         deps = regex ×1
14//! ```
15//!
16//! 人设取自本库的设计:32 片甲就是 32 个检测器,四行甲片是四大类;左钳拿放大镜
17//! 负责**看**,右钳举告示牌负责**报**,但两只钳子都不替调用方做决定。
18//! 唯一的例外是 [`SessionGuard`](crate::session::SessionGuard) —— 它会真的 Block。
19
20/// 项目宠物名:中文名 + 英文名。
21pub const NAME: &str = "甲哨 Sentri";
22
23/// 一句话人设。
24pub const TAGLINE: &str = "只报告,不拦截";
25
26/// ASCII 版形象,给终端、日志、示例 banner 用。
27pub const ASCII: &str = r#"      (o)(o)
28   ⌕┬─────────┬!     甲哨 Sentri
29    │ · · · · │       只报告,不拦截
30    └──┬───┬──┘       32 detectors / 4 categories
31      /     \         deps = regex ×1"#;
32
33/// SVG 版形象(`docs/pet.svg`),给 README 与下游界面用。
34pub const SVG: &str = include_str!("../docs/pet.svg");
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::AttackCategory;
40    use regex::Regex;
41
42    #[test]
43    fn svg_is_bundled_whole() {
44        assert!(SVG.starts_with("<svg"), "svg 头部: {:?}", &SVG[..40]);
45        assert!(SVG.trim_end().ends_with("</svg>"));
46        assert!(SVG.contains(r#"viewBox="0 0 480 460""#), "viewBox 变了");
47    }
48
49    /// 形象与库的对账:四行甲片 = 四大类,甲片总数 = 检测器总数。
50    ///
51    /// 认账目不认画法:每行甲片带一句 `<!-- 类别: N detectors, M plates -->` 注记,
52    /// N 是该类**检测器**数(要对得上 [`AttackCategory`]),M 是该行声明画出的
53    /// 甲片数。两列分开写是因为它们本就不相等——注入 11 个检测器只画 8 片甲,
54    /// 甲片按类别配色,不按数量铺满;对账只认总数。
55    ///
56    /// 改半径、配色、坐标都不该让这里变红 —— 数 `r="5"` 是把画法当账目,加一个
57    /// 装饰圆就能骗过它。少画一片甲、注记与画面对不上、类别名不认账才该红。
58    /// crate 侧的 32 由 `scanner::tests` 自己看着。
59    #[test]
60    fn plates_account_for_every_detector() {
61        let row =
62            Regex::new(r"<!-- (\w+): (\d+) detectors, (\d+) plates -->\s*((?:<circle[^>]*/>\s*)+)")
63                .unwrap();
64        let rows: Vec<(String, usize, usize, usize)> = row
65            .captures_iter(SVG)
66            .map(|c| {
67                (
68                    c[1].to_string(),
69                    c[2].parse().unwrap(),           // 该类检测器数
70                    c[3].parse().unwrap(),           // 注记声明的甲片数
71                    c[4].matches("<circle").count(), // 实际画出的甲片数
72                )
73            })
74            .collect();
75
76        let names: Vec<&str> = rows.iter().map(|(name, ..)| name.as_str()).collect();
77        for category in [
78            AttackCategory::Injection,
79            AttackCategory::Protocol,
80            AttackCategory::Data,
81            AttackCategory::File,
82        ] {
83            assert!(
84                names.contains(&category.to_string().as_str()),
85                "形象里少了 `{category}` 那一行甲片(注记格式:`<!-- 类别: N detectors, M plates -->`)"
86            );
87        }
88        assert_eq!(names.len(), 4, "四行甲片 = 四大类,多出来的行: {names:?}");
89
90        for (name, _, declared_plates, drawn) in &rows {
91            assert_eq!(
92                declared_plates, drawn,
93                "`{name}` 那行:注记声明 {declared_plates} 片,实际画了 {drawn} 片"
94            );
95        }
96        let declared: usize = rows.iter().map(|(_, detectors, ..)| detectors).sum();
97        let drawn: usize = rows.iter().map(|(_, _, _, plates)| plates).sum();
98        assert_eq!(declared, 32, "注记里的检测器总数");
99        assert_eq!(drawn, 32, "画出来的甲片总数 —— 32 片甲 = 32 个检测器");
100    }
101
102    #[test]
103    fn ascii_and_name_are_not_empty() {
104        assert!(!NAME.is_empty() && !TAGLINE.is_empty());
105        assert!(ASCII.lines().count() >= 5);
106    }
107}