source_map_tauri/
validate.rs1use 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}