keyhog 0.5.85

GPU-accelerated secret scanner for code, Git history, cloud, containers, browser assets, and live credential verification
use std::path::PathBuf;

/// Resolve the MatcherArtifact cache directory from explicit CLI/TOML config.
///
/// Resolution order:
///   1. explicit `--matcher-cache <DIR|off>` / `[system].matcher_cache`
///   2. `dirs::cache_dir()/keyhog-matcher-artifacts`
///
/// `off` / `0` / empty disables persistence. `--lockdown` also disables the
/// cache at the orchestrator layer (unsigned local detector/matcher graphs are
/// incompatible with lockdown's past-findings audit).
///
/// Default-on mirrors Hyperscan's persistent shard cache, which likewise
/// resolves under `dirs::cache_dir()/keyhog` when `--cache-dir` is unset
/// (`simd::backend::resolve_cache_dir`). An explicit path that fails validation
/// is a hard error. The automatic default automatically tightens loose permissions
/// (e.g. 0775 to 0700) and soft-fails to `None` (cache disabled) when the platform
/// cache root is missing or outside the allowlist.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ResolvedMatcherCacheConfig {
    Enabled(PathBuf),
    Disabled(keyhog_scanner::MatcherArtifactCacheDisableReason),
}

impl ResolvedMatcherCacheConfig {
    pub(crate) fn path(&self) -> Option<PathBuf> {
        match self {
            Self::Enabled(path) => Some(path.clone()),
            Self::Disabled(_) => None,
        }
    }

    pub(crate) fn disable_reason(
        &self,
    ) -> Option<keyhog_scanner::MatcherArtifactCacheDisableReason> {
        match self {
            Self::Enabled(_) => None,
            Self::Disabled(reason) => Some(*reason),
        }
    }
}

pub(crate) fn resolve_matcher_cache_path(
    raw: Option<&str>,
) -> Result<ResolvedMatcherCacheConfig, String> {
    resolve_matcher_cache_path_with_default(raw, dirs::cache_dir())
}

#[allow(dead_code)]
pub(crate) fn resolve_matcher_cache_path_with_default(
    raw: Option<&str>,
    default_cache_dir: Option<PathBuf>,
) -> Result<ResolvedMatcherCacheConfig, String> {
    if let Some(raw) = raw {
        let trimmed = raw.trim();
        if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("off") || trimmed == "0" {
            return Ok(ResolvedMatcherCacheConfig::Disabled(
                keyhog_scanner::MatcherArtifactCacheDisableReason::ConfiguredOff,
            ));
        }
        let path = PathBuf::from(trimmed);
        if !path.is_absolute() {
            return Err(format!(
                "matcher-artifact cache path must be an absolute directory, got `{trimmed}`. \
                 Configure with --matcher-cache <DIR|off> or [system].matcher_cache"
            ));
        }
        keyhog_scanner::validate_matcher_artifact_cache_dir(&path)?;
        return Ok(ResolvedMatcherCacheConfig::Enabled(path));
    }

    match keyhog_scanner::default_matcher_artifact_cache_dir_from_base(default_cache_dir) {
        Ok(path) => {
            match keyhog_scanner::validate_and_tighten_matcher_artifact_cache_dir(&path, true) {
                Ok(()) => Ok(ResolvedMatcherCacheConfig::Enabled(path)),
                Err(error) => {
                    tracing::debug!(
                        error = %error,
                        path = %path.display(),
                        "matcher-artifact cache unusable: default cache location is unusable"
                    );
                    Ok(ResolvedMatcherCacheConfig::Disabled(
                        keyhog_scanner::MatcherArtifactCacheDisableReason::UnusableLocation,
                    ))
                }
            }
        }
        Err(error) => {
            tracing::debug!(
                error = %error,
                "matcher-artifact cache soft-fail: no default cache location"
            );
            Ok(ResolvedMatcherCacheConfig::Disabled(
                keyhog_scanner::MatcherArtifactCacheDisableReason::UnusableLocation,
            ))
        }
    }
}