source-map-tauri 0.3.0

Static Tauri app scanner that emits Meilisearch-ready NDJSON
Documentation
use std::{collections::BTreeSet, fs, path::Path};

use anyhow::{bail, Context, Result};

use crate::{
    ids::is_safe_document_id,
    model::{ArtifactDoc, EdgeDoc, WarningDoc},
};

fn read_ndjson<T: serde::de::DeserializeOwned>(path: &Path) -> Result<Vec<T>> {
    let text =
        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
    text.lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str(line).context("invalid ndjson"))
        .collect()
}

pub fn validate_output_dir(path: &Path) -> Result<()> {
    let artifacts: Vec<ArtifactDoc> = read_ndjson(&path.join("artifacts.ndjson"))?;
    let edges: Vec<EdgeDoc> = read_ndjson(&path.join("edges.ndjson"))?;
    let warnings: Vec<WarningDoc> = read_ndjson(&path.join("warnings.ndjson"))?;
    let ids = artifacts
        .iter()
        .map(|item| item.id.clone())
        .collect::<BTreeSet<_>>();

    for artifact in &artifacts {
        if !is_safe_document_id(&artifact.id) {
            bail!("invalid artifact id {}", artifact.id);
        }
        if artifact.repo.is_empty() || artifact.kind.is_empty() {
            bail!("artifact missing repo or kind");
        }
        if artifact.related_tests.is_empty() == artifact.has_related_tests {
            bail!("has_related_tests mismatch for {}", artifact.id);
        }
        if (artifact.risk_level == "high" || artifact.risk_level == "critical")
            && artifact.risk_reasons.is_empty()
        {
            bail!("high-risk artifact missing risk reasons: {}", artifact.id);
        }
    }

    for edge in &edges {
        if !ids.contains(&edge.from_id) || !ids.contains(&edge.to_id) {
            bail!("edge references missing endpoint: {}", edge.id);
        }
    }

    for warning in &warnings {
        if !is_safe_document_id(&warning.id) {
            bail!("invalid warning id {}", warning.id);
        }
    }

    for artifact in artifacts
        .iter()
        .filter(|item| item.kind == "tauri_command" || item.kind == "tauri_plugin_command")
    {
        let has_permission = artifacts.iter().any(|candidate| {
            candidate.kind == "tauri_permission"
                && candidate
                    .data
                    .get("commands_allow")
                    .and_then(serde_json::Value::as_array)
                    .map(|items| {
                        items
                            .iter()
                            .any(|item| item.as_str() == artifact.name.as_deref())
                    })
                    .unwrap_or(false)
        });
        let has_warning = warnings.iter().any(|candidate| {
            candidate.related_id.as_deref() == Some(&artifact.id)
                && (candidate.warning_type == "command_without_permission_evidence"
                    || candidate.warning_type == "plugin_command_without_permission_evidence")
        });
        if !has_permission && !has_warning && artifact.risk_level != "low" {
            bail!(
                "command missing permission evidence or warning: {}",
                artifact.id
            );
        }
    }

    Ok(())
}