1use allow_core::{MatchOutcome, json_escape};
2
3use crate::{
4 ARTIFACT_STATUS_FAILED, ARTIFACT_STATUS_PASSED, ArtifactContract, CLAIM_BOUNDARY,
5 InventoryContext, ReportContext, SCANNER_LIMITATIONS,
6};
7
8pub(crate) fn push_json_artifact_header(
9 out: &mut String,
10 contract: ArtifactContract,
11 command: &str,
12) {
13 if let Some(fixed_command) = contract.fixed_command {
14 debug_assert_eq!(fixed_command, command);
15 }
16 let schema_version = contract.schema_version;
17 out.push_str(&format!(" \"schema_version\": {schema_version},\n"));
18 out.push_str(&format!(
19 " \"schema_id\": \"{}\",\n",
20 json_escape(contract.schema_id)
21 ));
22 out.push_str(" \"tool\": \"cargo-allow\",\n");
23 out.push_str(&format!(" \"command\": \"{}\",\n", json_escape(command)));
24}
25
26pub(crate) fn push_json_artifact_preamble(
27 out: &mut String,
28 contract: ArtifactContract,
29 command: &str,
30 inventory: InventoryContext<'_>,
31) {
32 push_json_artifact_header(out, contract, command);
33 push_json_artifact_source_context(out, inventory);
34}
35
36pub(crate) fn push_json_fixed_artifact_preamble(
37 out: &mut String,
38 contract: ArtifactContract,
39 inventory: InventoryContext<'_>,
40) {
41 let Some(command) = contract.fixed_command else {
42 std::panic::panic_any("fixed artifact preamble requires a fixed-command artifact contract");
43 };
44 push_json_artifact_preamble(out, contract, command, inventory);
45}
46
47pub(crate) fn push_json_artifact_source_context(out: &mut String, inventory: InventoryContext<'_>) {
48 out.push_str(&format!(
49 " \"claim_boundary\": {},\n",
50 render_claim_boundary_json()
51 ));
52 out.push_str(&format!(
53 " \"scanner_limitations\": {},\n",
54 render_scanner_limitations_json()
55 ));
56 out.push_str(" \"inventory\": ");
57 out.push_str(&render_inventory_json(inventory, " "));
58 out.push_str(",\n");
59}
60
61pub(crate) fn push_json_status_fields(out: &mut String, failed: bool) {
62 push_json_status_fields_with_status(
63 out,
64 if failed {
65 ARTIFACT_STATUS_FAILED
66 } else {
67 ARTIFACT_STATUS_PASSED
68 },
69 failed,
70 );
71}
72
73pub(crate) fn push_json_status_fields_with_status(out: &mut String, status: &str, failed: bool) {
74 out.push_str(&format!(" \"status\": \"{}\",\n", json_escape(status)));
75 out.push_str(&format!(" \"failed\": {},\n", bool_json(failed)));
76}
77
78pub(crate) fn push_json_receipt_run_metadata(out: &mut String, context: ReportContext<'_>) {
79 if let Some(mode) = context.mode {
80 out.push_str(&format!(" \"mode\": \"{}\",\n", json_escape(mode)));
81 }
82 if let Some(enforcement) = context.enforcement {
83 out.push_str(&format!(
84 " \"enforcement\": \"{}\",\n",
85 json_escape(enforcement)
86 ));
87 }
88 if let Some(policy_config) = context.policy_config {
89 out.push_str(&format!(
90 " \"policy_config\": \"{}\",\n",
91 json_escape(policy_config)
92 ));
93 }
94 if let Some(tool_version) = context.tool_version {
95 out.push_str(&format!(
96 " \"tool_version\": \"{}\",\n",
97 json_escape(tool_version)
98 ));
99 }
100}
101
102pub(crate) fn push_json_source_context_properties(
103 out: &mut String,
104 inventory: InventoryContext<'_>,
105 indent: &str,
106) {
107 out.push_str(&format!("{indent}\"inventory\": "));
108 out.push_str(&render_inventory_json(inventory, indent));
109 out.push_str(",\n");
110 out.push_str(&format!(
111 "{indent}\"claim_boundary\": {},\n",
112 render_claim_boundary_json()
113 ));
114 out.push_str(&format!(
115 "{indent}\"scanner_limitations\": {}\n",
116 render_scanner_limitations_json()
117 ));
118}
119
120pub(crate) fn option_json(value: Option<&str>) -> String {
121 value
122 .map(|v| format!("\"{}\"", json_escape(v)))
123 .unwrap_or_else(|| "null".to_string())
124}
125
126pub(crate) fn bool_json(value: bool) -> &'static str {
127 if value { "true" } else { "false" }
128}
129
130pub(crate) fn option_u32_json(value: Option<u32>) -> String {
131 value
132 .map(|value| value.to_string())
133 .unwrap_or_else(|| "null".to_string())
134}
135
136pub(crate) fn option_usize_json(value: Option<usize>) -> String {
137 value
138 .map(|value| value.to_string())
139 .unwrap_or_else(|| "null".to_string())
140}
141
142pub(crate) fn render_match_outcome_json(outcome: &MatchOutcome, indent: &str) -> String {
143 let fields = MatchOutcomeJsonFields::new(outcome);
144 format!(
145 "{indent} {{\n{indent} \"status\": \"{}\",\n{indent} \"allow_id\": {},\n{indent} \"finding_index\": {},\n{indent} \"score\": {},\n{indent} \"message\": \"{}\"\n{indent} }}",
146 fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
147 )
148}
149
150pub(crate) fn render_match_outcome_json_compact(outcome: &MatchOutcome) -> String {
151 let fields = MatchOutcomeJsonFields::new(outcome);
152 format!(
153 "{{\"status\": \"{}\", \"allow_id\": {}, \"finding_index\": {}, \"score\": {}, \"message\": \"{}\"}}",
154 fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
155 )
156}
157
158struct MatchOutcomeJsonFields {
159 status: &'static str,
160 allow_id: String,
161 finding_index: String,
162 score: u32,
163 message: String,
164}
165
166impl MatchOutcomeJsonFields {
167 fn new(outcome: &MatchOutcome) -> Self {
168 Self {
169 status: outcome.status.as_str(),
170 allow_id: option_json(outcome.allow_id.as_deref()),
171 finding_index: option_usize_json(outcome.finding_index),
172 score: outcome.score,
173 message: json_escape(&outcome.message),
174 }
175 }
176}
177
178pub(crate) fn json_string_array<T: AsRef<str>>(values: &[T]) -> String {
179 format!(
180 "[{}]",
181 values
182 .iter()
183 .map(|value| format!("\"{}\"", json_escape(value.as_ref())))
184 .collect::<Vec<_>>()
185 .join(", ")
186 )
187}
188
189pub fn render_claim_boundary_json() -> String {
190 json_string_array(CLAIM_BOUNDARY)
191}
192
193pub fn render_scanner_limitations_json() -> String {
194 json_string_array(SCANNER_LIMITATIONS)
195}
196
197pub fn render_inventory_json(context: InventoryContext<'_>, indent: &str) -> String {
198 let mut out = String::new();
199 out.push_str("{\n");
200 out.push_str(&format!(
201 "{indent} \"scope\": \"{}\",\n",
202 json_escape(context.scope)
203 ));
204 out.push_str(&format!(
205 "{indent} \"scanner\": \"{}\",\n",
206 json_escape(context.scanner)
207 ));
208 out.push_str(&format!(
209 "{indent} \"source\": \"{}\"",
210 json_escape(context.source)
211 ));
212 if let Some(root) = context.root {
213 out.push_str(",\n");
214 out.push_str(&format!("{indent} \"root\": \"{}\"", json_escape(root)));
215 }
216 if let Some(files) = context.files_scanned {
217 out.push_str(",\n");
218 out.push_str(&format!("{indent} \"files_scanned\": {files}"));
219 }
220 out.push('\n');
221 out.push_str(&format!("{indent}}}"));
222 out
223}