1use std::{
2 collections::BTreeSet,
3 fs,
4 io::{BufWriter, Write},
5 path::Path,
6};
7
8use anyhow::{Context, Result};
9use serde::Serialize;
10use serde_json::{json, Value};
11
12use crate::{
13 model::{ArtifactDoc, EdgeDoc, ScanSummary, WarningDoc},
14 scan::ScanBundle,
15};
16
17pub fn write_scan_bundle(output_dir: &Path, bundle: &ScanBundle) -> Result<()> {
18 fs::create_dir_all(output_dir)
19 .with_context(|| format!("failed to create {}", output_dir.display()))?;
20 write_ndjson(output_dir.join("artifacts.ndjson"), &bundle.artifacts)?;
21 write_ndjson(output_dir.join("edges.ndjson"), &bundle.edges)?;
22 write_ndjson(output_dir.join("warnings.ndjson"), &bundle.warnings)?;
23 fs::write(
24 output_dir.join("summary.json"),
25 serde_json::to_vec_pretty(&bundle.summary)?,
26 )?;
27 fs::write(
28 output_dir.join("project-info.json"),
29 serde_json::to_vec_pretty(&bundle.project_info)?,
30 )?;
31 fs::write(
32 output_dir.join("meili-settings.json"),
33 serde_json::to_vec_pretty(&default_meili_settings())?,
34 )?;
35 Ok(())
36}
37
38pub fn write_ndjson<T: Serialize>(path: impl AsRef<Path>, docs: &[T]) -> Result<()> {
39 let file = fs::File::create(path.as_ref())
40 .with_context(|| format!("failed to write {}", path.as_ref().display()))?;
41 let mut writer = BufWriter::new(file);
42 for doc in docs {
43 serde_json::to_writer(&mut writer, doc)?;
44 writer.write_all(b"\n")?;
45 }
46 writer.flush()?;
47 Ok(())
48}
49
50pub fn build_summary(
51 repo: &str,
52 artifacts: &[ArtifactDoc],
53 edges: &[EdgeDoc],
54 warnings: &[WarningDoc],
55) -> ScanSummary {
56 let artifact_kinds = artifacts
57 .iter()
58 .map(|item| item.kind.clone())
59 .collect::<BTreeSet<_>>()
60 .into_iter()
61 .collect();
62 let warning_types = warnings
63 .iter()
64 .map(|item| item.warning_type.clone())
65 .collect::<BTreeSet<_>>()
66 .into_iter()
67 .collect();
68
69 ScanSummary {
70 repo: repo.to_owned(),
71 artifact_count: artifacts.len(),
72 edge_count: edges.len(),
73 warning_count: warnings.len(),
74 artifact_kinds,
75 warning_types,
76 scanned_at: chrono::Utc::now().to_rfc3339(),
77 }
78}
79
80pub fn default_meili_settings() -> Value {
81 json!({
82 "searchableAttributes": [
83 "name",
84 "invoke_key",
85 "command_name",
86 "plugin_name",
87 "plugin_export",
88 "hook_name",
89 "hook_kind",
90 "event_name",
91 "channel_name",
92 "rust_fqn",
93 "component",
94 "display_name",
95 "signature",
96 "source_path",
97 "bundle_path",
98 "nearest_symbol",
99 "permissions",
100 "effective_capabilities",
101 "target_rust_commands",
102 "called_by_frontend",
103 "related_symbols",
104 "related_php_symbols",
105 "related_tests",
106 "risk_reasons",
107 "tags",
108 "comments",
109 "package_name"
110 ],
111 "filterableAttributes": [
112 "repo",
113 "kind",
114 "side",
115 "language",
116 "source_path",
117 "package_name",
118 "risk_level",
119 "contains_phi",
120 "has_related_tests",
121 "command_name",
122 "invoke_key",
123 "plugin_name",
124 "plugin_export",
125 "hook_name",
126 "hook_kind",
127 "component",
128 "event_name",
129 "channel_name",
130 "window_label",
131 "webview_label",
132 "capability_id",
133 "permission_id",
134 "merged_capabilities",
135 "remote_capability",
136 "from_id",
137 "to_id",
138 "from_kind",
139 "to_kind",
140 "edge_type",
141 "warning_type",
142 "severity"
143 ],
144 "sortableAttributes": ["confidence", "updated_at"],
145 "rankingRules": [
146 "words",
147 "typo",
148 "proximity",
149 "attribute",
150 "sort",
151 "exactness"
152 ]
153 })
154}