kingfisher-bin 2.8.0

MongoDB's blazingly fast and accurate secret scanning and validation tool
use anyhow::Result;
use tl::{HTMLTag, Node, Parser, ParserOptions};

use super::{Language, css, lexer};

pub(super) fn stream_context_candidates<F>(source: &[u8], sink: &mut F) -> Result<()>
where
    F: FnMut(&str) -> bool,
{
    let html = String::from_utf8_lossy(source);
    if html.trim().is_empty() {
        return Ok(());
    }

    let dom = match tl::parse(&html, ParserOptions::default()) {
        Ok(dom) => dom,
        Err(_) => return Ok(()),
    };
    let parser = dom.parser();

    for node in dom.nodes() {
        let Some(tag) = node.as_tag() else {
            continue;
        };
        let tag_name = tag.name().as_utf8_str().to_string();
        let normalized_tag_name = tag_name.to_ascii_lowercase();

        for (key, value) in tag.attributes().iter() {
            let Some(value) = value else {
                continue;
            };
            let candidate = format!("{key} = {value}");
            if !sink(&candidate) {
                return Ok(());
            }
        }

        match normalized_tag_name.as_str() {
            "script" => {
                let script_text = tag.inner_text(parser);
                let script_text = script_text.trim();
                if !script_text.is_empty() {
                    lexer::stream_context_candidates(
                        script_text.as_bytes(),
                        &Language::JavaScript,
                        sink,
                    )?;
                }
            }
            "style" => {
                let style_text = tag.inner_text(parser);
                let style_text = style_text.trim();
                if !style_text.is_empty() {
                    css::stream_context_candidates(style_text.as_bytes(), sink)?;
                }
            }
            _ => {
                let inner_text = text_without_embedded_code(tag, parser);
                if !inner_text.is_empty() && !sink(&format!("{tag_name} = {inner_text}")) {
                    return Ok(());
                }
            }
        }
    }

    Ok(())
}

fn text_without_embedded_code(tag: &HTMLTag<'_>, parser: &Parser<'_>) -> String {
    let mut text = String::new();
    collect_visible_text(tag, parser, &mut text);
    text.trim().to_string()
}

fn collect_visible_text(tag: &HTMLTag<'_>, parser: &Parser<'_>, out: &mut String) {
    for handle in tag.children().top().iter() {
        let Some(node) = handle.get(parser) else {
            continue;
        };

        match node {
            Node::Raw(raw) => out.push_str(raw.as_utf8_str().as_ref()),
            Node::Comment(_) => {}
            Node::Tag(child) => {
                let child_name = child.name().as_utf8_str();
                if child_name.eq_ignore_ascii_case("script")
                    || child_name.eq_ignore_ascii_case("style")
                {
                    continue;
                }
                collect_visible_text(child, parser, out);
            }
        }
    }
}