blazehash 0.2.5

Forensic file hasher — hashdeep for the modern era, BLAKE3 by default
Documentation
use anyhow::Result;
use std::io::Write;
use std::path::Path;

pub struct FilterOpts<'a> {
    pub include: Option<&'a [String]>,
    pub algo: Option<&'a str>,
}

pub fn filter_manifest<W: Write>(
    manifest_path: &Path,
    opts: &FilterOpts<'_>,
    out: &mut W,
) -> Result<()> {
    let content = std::fs::read_to_string(manifest_path)?;

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('%') {
            writeln!(out, "{line}")?;
            continue;
        }
        let parts: Vec<&str> = trimmed.splitn(3, "  ").collect();
        if parts.len() != 3 {
            continue;
        }
        let algo = parts[0].trim();
        let path = parts[2].trim();

        if let Some(required_algo) = opts.algo {
            if !algo.eq_ignore_ascii_case(required_algo) {
                continue;
            }
        }
        if let Some(patterns) = opts.include {
            let matched = patterns.iter().any(|pat| glob_match(pat, path));
            if !matched {
                continue;
            }
        }
        writeln!(out, "{line}")?;
    }
    Ok(())
}

fn glob_match(pattern: &str, path: &str) -> bool {
    let path = path.replace('\\', "/");
    let pattern = pattern.replace('\\', "/");
    glob_match_inner(&pattern, &path)
}

fn glob_match_inner(pattern: &str, text: &str) -> bool {
    if pattern.is_empty() {
        return text.is_empty();
    }
    if pattern == "**" {
        return true;
    }
    if let Some(rest) = pattern.strip_prefix("**/") {
        if glob_match_inner(rest, text) {
            return true;
        }
        if let Some(slash) = text.find('/') {
            return glob_match_inner(pattern, &text[slash + 1..]);
        }
        return false;
    }
    let (pat_seg, pat_rest) = match pattern.find('/') {
        Some(i) => (&pattern[..i], Some(&pattern[i + 1..])),
        None => (pattern, None),
    };
    let (txt_seg, txt_rest) = match text.find('/') {
        Some(i) => (&text[..i], Some(&text[i + 1..])),
        None => (text, None),
    };
    if !segment_match(pat_seg, txt_seg) {
        return false;
    }
    match (pat_rest, txt_rest) {
        (None, None) => true,
        (Some(p), Some(t)) => glob_match_inner(p, t),
        _ => false,
    }
}

fn segment_match(pattern: &str, text: &str) -> bool {
    if pattern == "*" {
        return true;
    }
    if !pattern.contains('*') {
        return pattern == text;
    }
    let parts: Vec<&str> = pattern.split('*').collect();
    let mut pos = 0usize;
    for (i, part) in parts.iter().enumerate() {
        if part.is_empty() {
            continue;
        }
        if i == 0 {
            if !text.starts_with(part) {
                return false;
            }
            pos = part.len();
        } else if i == parts.len() - 1 {
            return text[pos..].ends_with(part);
        } else if let Some(found) = text[pos..].find(part) {
            pos += found + part.len();
        } else {
            return false;
        }
    }
    true
}