string-analyze 0.1.0

Find key strings from cluttered text
Documentation
//string_analyze/src/tool.rs

#![allow(unused)]
use crate::State;
use regex::Regex;

pub trait MatchTarget<'a> {
    fn get_input(&self) -> &'a str;
    fn clear_ranges(&mut self);
    fn push_range(&mut self, start: usize, end: usize);
}

impl<'a> MatchTarget<'a> for &'a str {
    #[inline]
    fn get_input(&self) -> &'a str {
        *self
    }
    #[inline]
    fn clear_ranges(&mut self) {}
    #[inline]
    fn push_range(&mut self, _start: usize, _end: usize) {}
}
impl<'a, 'b> MatchTarget<'a> for &'b mut State<'a> {
    #[inline]
    fn get_input(&self) -> &'a str {
        self.input
    }
    #[inline]
    fn clear_ranges(&mut self) {
        self.ranges.clear();
    }
    #[inline]
    fn push_range(&mut self, start: usize, end: usize) {
        self.ranges.push((start, end));
    }
}

#[inline]
pub fn has_keyword<'a>(mut target: impl MatchTarget<'a>, keyword: &str, ignore_case: bool) -> bool {
    target.clear_ranges();
    let input = target.get_input();
    let k_len = keyword.len();
    if k_len == 0 || input.len() < k_len {
        return false;
    }

    let mut found = false;

    if !ignore_case {
        let mut start = 0;
        while let Some(idx) = input[start..].find(keyword) {
            let abs_idx = start + idx;
            target.push_range(abs_idx, abs_idx + k_len);
            found = true;
            start = abs_idx + k_len;
        }
    } else {
        let input_bytes = input.as_bytes();
        let keyword_bytes = keyword.as_bytes();
        let mut i = 0;
        while i <= input_bytes.len() - k_len {
            let window = &input_bytes[i..i + k_len];
            if window.eq_ignore_ascii_case(keyword_bytes) {
                if input.is_char_boundary(i) && input.is_char_boundary(i + k_len) {
                    target.push_range(i, i + k_len);
                    found = true;
                }
                i += k_len;
            } else {
                i += 1;
            }
        }
    }
    found
}

#[inline]
pub fn has_chars<'a>(
    mut target: impl MatchTarget<'a>,
    chars: &[char],
    any: bool,
    ignore_case: bool,
) -> bool {
    target.clear_ranges();
    let input = target.get_input();

    if chars.is_empty() || input.is_empty() {
        return false;
    }

    let mut target_map = [false; 256];
    let mut target_count = 0;

    // 1. 构建目标字符表
    for &c in chars {
        if c.is_ascii() {
            let b = if ignore_case {
                c.to_ascii_lowercase() as usize
            } else {
                c as usize
            };
            if !target_map[b] {
                target_map[b] = true;
                target_count += 1;
            }
        }
    }

    if target_count == 0 {
        return false;
    }

    let mut found_map = [false; 256];
    let mut distinct_found = 0;
    let mut any_found = false;

    // 2. 扫描输入字符串并标记字符位置
    for (i, b) in input.bytes().enumerate() {
        if !b.is_ascii() {
            continue;
        }
        let val = if ignore_case {
            b.to_ascii_lowercase() as usize
        } else {
            b as usize
        };

        if target_map[val] {
            // 标记当前 ASCII 字符的字节区间:(i, i + 1)
            target.push_range(i, i + 1);
            any_found = true;

            // 如果要求全部包含 (!any),记录独立找到的目标字符数
            if !any && !found_map[val] {
                found_map[val] = true;
                distinct_found += 1;
            }
        }
    }

    // 3. 校验匹配结果
    if any {
        any_found
    } else {
        let all_found = distinct_found == target_count;
        if !all_found {
            // 如果没有找齐所有指定的字符,清空区间并返回 false
            target.clear_ranges();
        }
        all_found
    }
}

#[inline]
pub fn has_regex<'a>(mut target: impl MatchTarget<'a>, re: &Regex) -> bool {
    target.clear_ranges();
    let input = target.get_input();
    let mut found = false;

    for mat in re.find_iter(input) {
        target.push_range(mat.start(), mat.end());
        found = true;
    }
    found
}

#[inline]
pub fn upper_prob(input: &str) -> f64 {
    let mut upper_count = 0;
    let mut letter_count = 0;

    for b in input.bytes() {
        if b.is_ascii_uppercase() {
            upper_count += 1;
            letter_count += 1;
        } else if b.is_ascii_lowercase() {
            letter_count += 1;
        }
    }

    if letter_count == 0 {
        return 0.0;
    }
    (upper_count as f64) / (letter_count as f64)
}

#[inline]
pub fn has_upper(input: &str) -> bool {
    input.bytes().any(|b| b.is_ascii_uppercase())
}
#[inline]
pub fn has_lower(input: &str) -> bool {
    input.bytes().any(|b| b.is_ascii_lowercase())
}

pub fn is_base64(s: &str) -> bool {
    let len = s.len();

    // 1. 长度检查:至少4个字符
    if len <= 8 {
        return false;
    }

    // 2. 去除填充符
    let trimmed = s.trim_end_matches('=');
    let padding_count = len - trimmed.len();

    // 填充符只能是0, 1, 或2个
    if padding_count > 2 {
        return false;
    }

    // 3. Base64 不能以 / 或 + 结尾
    if trimmed.ends_with('/') || trimmed.ends_with('+') {
        return false;
    }
    if len % 4 != 0 {
        return false;
    }

    let has_digit = s.bytes().any(|b| b.is_ascii_digit());
    let has_special = s.contains('+') || s.contains('/') || s.contains('=');
    if !has_digit && !has_special {
        return false;
    }

    let prob = upper_prob(trimmed);
    if prob <= 0.2 || prob >= 0.8 {
        return false;
    }

    has_upper(s) && has_lower(s)
}

#[inline]
pub fn sort_ascii_counts(input: &str, ignore_case: bool) -> Vec<(char, usize)> {
    let mut counts = [0usize; 256];

    for b in input.bytes() {
        if b.is_ascii() {
            let val = if ignore_case {
                b.to_ascii_lowercase() as usize
            } else {
                b as usize
            };
            counts[val] += 1;
        }
    }

    let mut result: Vec<(char, usize)> = counts
        .iter()
        .enumerate()
        .filter(|&(_, &count)| count > 0)
        .map(|(b, &count)| (b as u8 as char, count))
        .collect();

    result.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));

    result
}

pub fn is_hex(input: &str) -> bool {
    let mut s = input.trim();
    if s.is_empty() || !s.is_ascii() || s.len() <= 10 {
        return false;
    }

    if (s.starts_with("0x") || s.starts_with("0X") || s.starts_with("\\x") || s.starts_with("\\X"))
        && s[2..].find(&s[0..2]).is_none()
    {
        s = &s[2..];
    }

    let prefix = if s.starts_with("0x") { "0x" }
    else if s.starts_with("0X") { "0X" }
    else if s.starts_with("\\x") { "\\x" }
    else if s.starts_with("\\X") { "\\X" }
    else if s.starts_with("%") { "%" }
    else { "" };

    let mut idx = prefix.len();
    let bytes = s.as_bytes();

    if idx + 2 > bytes.len() {
        return false;
    }

    let mut upper_hex = false;
    let mut lower_hex = false;

    #[inline]
    fn check_hex(b: u8, upper: &mut bool, lower: &mut bool) -> bool {
        if b.is_ascii_digit() { return true; }
        if (b'a'..=b'f').contains(&b) { *lower = true; return true; }
        if (b'A'..=b'F').contains(&b) { *upper = true; return true; }
        false
    }

    if !check_hex(bytes[idx], &mut upper_hex, &mut lower_hex) || !check_hex(bytes[idx+1], &mut upper_hex, &mut lower_hex) {
        return false;
    }
    idx += 2;

    let separator = if idx == bytes.len() {
        ""
    } else {
        if !prefix.is_empty() {
            if let Some(next_prefix_idx) = s[idx..].find(prefix) {
                &s[idx .. idx + next_prefix_idx]
            } else {
                return false;
            }
        } else {
            let mut sep_len = 0;
            while idx + sep_len < bytes.len() {
                let b = bytes[idx + sep_len];
                if b.is_ascii_hexdigit() {
                    break;
                }
                sep_len += 1;
            }
            &s[idx .. idx + sep_len]
        }
    };

    if !separator.is_empty() && separator.bytes().any(|b| b.is_ascii_alphanumeric()) {
        return false;
    }

    let mut curr_idx = 0;
    let mut count = 0;

    while curr_idx < bytes.len() {
        if !s[curr_idx..].starts_with(prefix) { return false; }
        curr_idx += prefix.len();

        if curr_idx + 2 > bytes.len() { return false; }

        if !check_hex(bytes[curr_idx], &mut upper_hex, &mut lower_hex) || !check_hex(bytes[curr_idx+1], &mut upper_hex, &mut lower_hex) {
            return false;
        }

        if upper_hex && lower_hex {
            return false;
        }

        curr_idx += 2;
        count += 1;

        if curr_idx == bytes.len() {
            break;
        }

        if !s[curr_idx..].starts_with(separator) {
            return false;
        }
        curr_idx += separator.len();

        if curr_idx == bytes.len() {
            return false;
        }
    }
    count > 0
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test() {
        has_keyword("","",true);
        has_keyword(&mut State::new(""),"",true);
    }

}