string-analyze 0.1.1

Find key strings from cluttered text
Documentation
//! 文本与字节序列的熵值 (Entropy) 计算模块。
//!
//! 本模块提供基于信息论的多种熵计算方法(基础香农熵、差分熵、二元语法熵),
//! 并提供了一个融合这三种特征的复合熵评分系统,
//! 专门用于评估文本的随机程度,以辅助过滤无效匹配并精准定位高随机性的密钥、Token 或混淆代码。

use std::cell::RefCell;

/// 计算给定频率分布的香农熵 (Shannon Entropy) 核心算法。
///
/// 内部公式基于 `H = -Σ (p_i * log2(p_i))`,其中 `p_i = count / total`。
/// 为了优化计算性能,对公式进行了代数变形:
/// `H = log2(total) - (Σ (count * log2(count)) / total)`
/// 这样可以避免在循环内部进行除法运算。
#[inline(always)]
fn calc_entropy<'a, I>(counts: I, total_grams: f64) -> f64
where
    I: Iterator<Item = &'a u32>,
{
    let sum_c_log2_c: f64 = counts
        .filter(|&&c| c > 0)
        .map(|&c| {
            let c_f64 = c as f64;
            c_f64 * c_f64.log2()
        })
        .sum();
    total_grams.log2() - (sum_c_log2_c / total_grams)
}

/// 计算标准的单字节香农熵 (1-gram Entropy)。
///
/// 评估输入序列中各个字节(0-255)出现的均匀程度。
/// 返回值区间通常为 `[0.0, 8.0]` (因为 2^8 = 256)。
///
/// # 参数
/// - `bytes`: 待计算的字节切片。
pub fn entropy(bytes: &[u8]) -> f64 {
    if bytes.is_empty() {
        return 0.0;
    }

    let mut counts = [0u32; 256];
    for &b in bytes {
        counts[b as usize] += 1;
    }
    calc_entropy(counts.iter(), bytes.len() as f64)
}

/// 计算差分熵 (Delta Entropy)。
///
/// 评估相邻字节之间**差值**的分布均匀度。
///
/// # 作用
/// 标准熵无法识破规律递增或递减的序列(如 `"12345678"` 或 `"abcdefgh"`),
/// 这些序列在单字节熵中表现为高熵,但在差分熵计算中,由于差值总是固定值(如 `1`),
/// 其差分熵会极其接近 `0.0`,从而有效过滤掉这批伪随机字符串。
pub fn delta_entropy(bytes: &[u8]) -> f64 {
    let len = bytes.len();

    if len < 2 {
        return 0.0;
    }

    let mut counts = [0u32; 256];
    for window in bytes.windows(2) {
        let delta = window[1].wrapping_sub(window[0]);
        counts[delta as usize] += 1;
    }

    calc_entropy(counts.iter(), (len - 1) as f64)
}

/// 计算二元语法熵 (2-gram / Bigram Entropy)。
///
/// 评估相邻两个字节组合(0x0000 - 0xFFFF,共 65536 种可能)的分布情况。
/// 理论最大值为 `16.0` (2^16)。用于评估序列中字符组合的复杂度和随机性。
///
/// # 性能优化
/// 因为需要 `65536` 大小的数组,频繁的堆内存分配 (`Vec::new`) 会严重拖慢性能。
/// 此处使用了 `thread_local!` 缓存复用的方式,使得并发扫描时零内存分配。
pub fn gram_entropy2(bytes: &[u8]) -> f64 {
    thread_local! {
        // 使用 (counts, visited_indices) 的组合来实现 O(K) 的快速清零 (K 为独立 2-gram 数量)
        static GRAM_BUFFER: RefCell<(Vec<u32>, Vec<u16>)> = RefCell::new((vec![0; 65536], Vec::with_capacity(4096)));
    }
    let len = bytes.len();

    if len < 2 {
        return 0.0;
    }

    GRAM_BUFFER.with(|buf| {
        let (counts, visited) = &mut *buf.borrow_mut();
        let total_grams = (len - 1) as f64;

        for w in bytes.windows(2) {
            let idx = ((w[0] as usize) << 8) | (w[1] as usize);
            if counts[idx] == 0 {
                visited.push(idx as u16);
            }
            counts[idx] += 1;
        }

        let mut sum_c_log2_c = 0.0;
        for &idx in visited.iter() {
            let u_idx = idx as usize;
            let c = counts[u_idx];
            counts[u_idx] = 0; // 顺手清理数组,为下一次调用做准备

            let c_f64 = c as f64;
            sum_c_log2_c += c_f64 * c_f64.log2();
        }

        visited.clear();

        total_grams.log2() - (sum_c_log2_c / total_grams)
    })
}

/// 计算综合复合熵 (Composite Entropy) 评分。
///
/// 融合了标准熵、差分熵和二元语法熵,输出一个归一化到 `[0.0, 10.0]` 的综合评分。
/// 分数越高,表示序列越趋近于真正的强随机(如高质量哈希、密码、加密密钥)。
///
/// # 算法逻辑
/// 1. 提取三种熵并归一化到 `[0.0, 1.0]` 的区间。
/// 2. 对 2-gram 熵 (`n3`) 应用一个平滑多项式 `y = -4x^5 + 15x^4 - 20x^3 + 10x^2`,
///    这是一种基于 Smoothstep 思想的映射,旨在压制处于中低水平的 2-gram 影响。
/// 3. 采用**带有负指数 (`P = -1.6`) 的加权幂平均法** (Weighted Power Mean) 融合三者。
///    负指数的特性是具有**“木桶效应 / 惩罚低分”**:只要三种熵中有任意一个明显偏低(例如具有规律性递增),
///    整体的分数就会被大幅拉低。
pub fn composite_entropy(bytes: &[u8]) -> f64 {
    let len = bytes.len();
    if len < 4 {
        return 0.0;
    }

    let e1 = entropy(bytes);
    let e2 = delta_entropy(bytes);
    let e3 = gram_entropy2(bytes);

    const EPS: f64 = 1e-6;
    let n1 = (e1 / 8.0).clamp(EPS, 1.0);
    let n2 = (e2 / 8.0).clamp(EPS, 1.0);

    let n3_raw = (e3 / 16.0).clamp(EPS, 1.0);

    // 对 2-gram 应用多项式非线性映射 y = -4x^5 + 15x^4 - 20x^3 + 10x^2
    let n3 = {
        let x = n3_raw;
        (x * x * (10.0 + x * (-20.0 + x * (15.0 - 4.0 * x)))).clamp(EPS, 1.0)
    };

    // 权重配置
    const W1: f64 = 0.6; // 基础熵占主导
    const W2: f64 = 0.3; // 差分熵
    const W3: f64 = 0.1; // 2-gram 熵
    const P: f64 = -1.6; // 幂指数,负值强调惩罚偏低的特征值

    // 计算加权幂平均
    let mean_pow = W1 * n1.powf(P) + W2 * n2.powf(P) + W3 * n3.powf(P);
    let combined = mean_pow.powf(1.0 / P);

    (combined * 10.0).clamp(0.0, 10.0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use rand::{RngExt, random, rng};

    #[test]
    fn test_entropy() {
        let input = &[
            "unittests src/lib.rs (target/debug/deps/string_analyze-551ebe6d3e6cc1ac)",
            "0123456789ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef",
            "96ef6443c6effd748c7202c04f0577f92761b821db19fdfea2a5c2364b439209",
            "horizontal-scroll-mode",
            "112233445566778899aabbccddeeffxxyyzz112233445566778899aabbccddeeffxxyyzz",
            "aGVsbG8=",
            "MDEyMzQ1Njc4OUFNREV5TXpRMU5qYzRPVUZDUTBSRlJqQXhNak09QkNERUYwMTIz",
            "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==",
            "12~3",
            "1[82~3",
            "Bitcoin/IPFS",
            " ",
            "~呜嗷~~嗷嗷呜~嗷呜喵呜喵呜喵~嗷~~呜~喵喵呜嗷喵嗷呜嗷呜嗷~喵~呜呜呜喵嗷呜喵喵喵呜喵喵",
            "~呜嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵嗷喵呜~呜~嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜呜嗷呜嗷呜呜喵呜喵嗷喵",
            "~~~~~~~~~~~!!!!!!!!!!!!!!###############",
            "ccVoejdMVQnfrzpgIunpQchW6WXcCnuX7leuRhzY3Ripc26KwOtCqxBRcjmHbN9PiaesCgHTi9iF",
            r#"M_2cL`Eqq:;|z_hiRTvhW@RLyq&d+UPXf6$ZarHI+9AiM[ZcQ"8egZ4m3Ur>J*AJE>kitQ@5Exo!C3$z.c[E#{*Rq5D"#,
            "豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛斗之墟;人傑地靈,徐孺下陳蕃之榻。雄州霧列,俊彩星馳。臺隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿範,襜帷暫駐。十旬休假,勝友如雲。千里逢迎,高朋滿座。騰蛟起鳳,孟學士之詞宗;紫電青霜,王將軍之武庫。家君作宰,路出名區。童子何知?躬逢勝餞。",
        ];

        for s in input {
            println!(
                "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            );
            println!(
                "[entropy          ] [{:<5.3}]\t{s:.128}",
                entropy(s.as_bytes())
            );
            println!(
                "[delta_entropy    ] [{:<5.3}]\t{s:.128}",
                delta_entropy(s.as_bytes())
            );
            println!(
                "[gram_entropy2    ] [{:<5.3}]\t{s:.128}",
                gram_entropy2(s.as_bytes())
            );
            println!(
                "[composite_entropy] [{:<5.3}]\t{s:.128}",
                composite_entropy(s.as_bytes())
            );
        }
        let input = &mut vec![
            b"\xd6\x23\x15\x9e\xbd\xe2\x90\xf9\xaa\xa0\x2e\xa0\x80\x9a\xa6\xf3\xcd\x31\xaa\x0d\x04\x6f\x51\x9c\xf1\x34\xcd\xef\x41\x29\xa4\x28\x44\x0c\xf7\x2d\xbb\x3d\x69\xf2\x03\xff\x9d\x54\x95\x25\x2d\x83\xd2\x80\x8d\x44\xef\xef\xf5\x6a\xf8\xc3\x61\x99\x9c\xe9\x2c\xec\x23\x3b\xea\xf3\x43\x1f\x57\xbd\x45\xce\x0e\x96\x4c\xb7\xbf\x28\x08\x16\x77\x53\x83\x59\x16\xd3\xac".as_slice()
        ];
        let x256 = random::<[u8; 256]>();
        input.push(x256.as_slice());
        let x512 = random::<[u8; 512]>();
        input.push(x512.as_slice());

        let x1024 = random::<[u8; 1024]>();
        input.push(x1024.as_slice());

        let mut x4096 = vec![0u8; 4096];
        rng().fill(&mut x4096);
        input.push(x4096.as_slice());

        for b in input {
            println!(
                "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            );
            println!(
                "[entropy          ] [{:<5.3}]\t{:.128}",
                entropy(b),
                format!("{b:?}")
            );
            println!(
                "[delta_entropy    ] [{:<5.3}]\t{:.128}",
                delta_entropy(b),
                format!("{b:?}")
            );
            println!(
                "[gram_entropy2    ] [{:<5.3}]\t{:.128}",
                gram_entropy2(b),
                format!("{b:?}")
            );
            println!(
                "[composite_entropy] [{:<5.3}]\t{:.128}",
                composite_entropy(b),
                format!("{b:?}")
            );
        }
    }
}