Skip to main content

source_map_tauri/
validate.rs

1use std::{collections::BTreeSet, fs, path::Path};
2
3use anyhow::{bail, Context, Result};
4
5use crate::{
6    ids::is_safe_document_id,
7    model::{ArtifactDoc, EdgeDoc, WarningDoc},
8};
9
10fn read_ndjson<T: serde::de::DeserializeOwned>(path: &Path) -> Result<Vec<T>> {
11    let text =
12        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
13    text.lines()
14        .filter(|line| !line.trim().is_empty())
15        .map(|line| serde_json::from_str(line).context("invalid ndjson"))
16        .collect()
17}
18
19pub fn validate_output_dir(path: &Path) -> Result<()> {
20    let artifacts: Vec<ArtifactDoc> = read_ndjson(&path.join("artifacts.ndjson"))?;
21    let edges: Vec<EdgeDoc> = read_ndjson(&path.join("edges.ndjson"))?;
22    let warnings: Vec<WarningDoc> = read_ndjson(&path.join("warnings.ndjson"))?;
23    let ids = artifacts
24        .iter()
25        .map(|item| item.id.clone())
26        .collect::<BTreeSet<_>>();
27
28    for artifact in &artifacts {
29        if !is_safe_document_id(&artifact.id) {
30            bail!("invalid artifact id {}", artifact.id);
31        }
32        if artifact.repo.is_empty() || artifact.kind.is_empty() {
33            bail!("artifact missing repo or kind");
34        }
35        if artifact.related_tests.is_empty() == artifact.has_related_tests {
36            bail!("has_related_tests mismatch for {}", artifact.id);
37        }
38        if (artifact.risk_level == "high" || artifact.risk_level == "critical")
39            && artifact.risk_reasons.is_empty()
40        {
41            bail!("high-risk artifact missing risk reasons: {}", artifact.id);
42        }
43    }
44
45    for edge in &edges {
46        if !ids.contains(&edge.from_id) || !ids.contains(&edge.to_id) {
47            bail!("edge references missing endpoint: {}", edge.id);
48        }
49    }
50
51    for warning in &warnings {
52        if !is_safe_document_id(&warning.id) {
53            bail!("invalid warning id {}", warning.id);
54        }
55    }
56
57    for artifact in artifacts
58        .iter()
59        .filter(|item| item.kind == "tauri_command" || item.kind == "tauri_plugin_command")
60    {
61        let has_permission = artifacts.iter().any(|candidate| {
62            candidate.kind == "tauri_permission"
63                && candidate
64                    .data
65                    .get("commands_allow")
66                    .and_then(serde_json::Value::as_array)
67                    .map(|items| {
68                        items
69                            .iter()
70                            .any(|item| item.as_str() == artifact.name.as_deref())
71                    })
72                    .unwrap_or(false)
73        });
74        let has_warning = warnings.iter().any(|candidate| {
75            candidate.related_id.as_deref() == Some(&artifact.id)
76                && (candidate.warning_type == "command_without_permission_evidence"
77                    || candidate.warning_type == "plugin_command_without_permission_evidence")
78        });
79        if !has_permission && !has_warning && artifact.risk_level != "low" {
80            bail!(
81                "command missing permission evidence or warning: {}",
82                artifact.id
83            );
84        }
85    }
86
87    Ok(())
88}