boxology_classifier/
report.rs1const SCHEMA: &str = "boxology.classification-report@2";
2
3pub fn render_text(report: &super::ClassificationReport) -> String {
12 let mut output = String::from("classification ");
13 output.push_str(report.verdict().canonical_name());
14 output.push('\n');
15 for finding in report.findings() {
16 output.push_str("finding ");
17 output.push_str(finding.code());
18 output.push_str(" path=\"");
19 push_escaped(&mut output, finding.path());
20 output.push_str("\" ");
21 output.push_str(finding.class().canonical_name());
22 output.push_str(" kind=\"");
23 push_escaped(&mut output, finding.kind());
24 output.push_str("\" base=");
25 push_text_excerpt(&mut output, finding.base_excerpt());
26 output.push_str(" submitted=");
27 push_text_excerpt(&mut output, finding.submitted_excerpt());
28 if let Some(condition) = finding.condition() {
29 output.push_str(" condition=\"");
30 push_escaped(&mut output, condition);
31 output.push('\"');
32 }
33 output.push('\n');
34 }
35 output
36}
37
38pub fn render_json(report: &super::ClassificationReport) -> String {
48 let mut output = String::from("{\n \"schema\": \"");
49 push_escaped(&mut output, SCHEMA);
50 output.push_str("\",\n \"verdict\": \"");
51 push_escaped(&mut output, report.verdict().canonical_name());
52 output.push_str("\",\n \"findings\": ");
53 if report.findings().is_empty() {
54 output.push_str("[]\n}\n");
55 return output;
56 }
57 output.push_str("[\n");
58 for (index, finding) in report.findings().iter().enumerate() {
59 if index != 0 {
60 output.push_str(",\n");
61 }
62 output.push_str(" {\n \"code\": \"");
63 push_escaped(&mut output, finding.code());
64 output.push_str("\",\n \"path\": \"");
65 push_escaped(&mut output, finding.path());
66 output.push_str("\",\n \"kind\": \"");
67 push_escaped(&mut output, finding.kind());
68 output.push_str("\",\n \"class\": \"");
69 push_escaped(&mut output, finding.class().canonical_name());
70 output.push_str("\",\n \"base\": ");
71 push_json_excerpt(&mut output, finding.base_excerpt());
72 output.push_str(",\n \"submitted\": ");
73 push_json_excerpt(&mut output, finding.submitted_excerpt());
74 if let Some(condition) = finding.condition() {
75 output.push_str(",\n \"condition\": \"");
76 push_escaped(&mut output, condition);
77 output.push('\"');
78 }
79 output.push_str("\n }");
80 }
81 output.push_str("\n ]\n}\n");
82 output
83}
84
85fn push_text_excerpt(output: &mut String, excerpt: Option<&str>) {
86 match excerpt {
87 None => output.push('-'),
88 Some(value) => {
89 output.push('\"');
90 push_escaped(output, value);
91 output.push('\"');
92 }
93 }
94}
95
96fn push_json_excerpt(output: &mut String, excerpt: Option<&str>) {
97 match excerpt {
98 None => output.push_str("null"),
99 Some(value) => {
100 output.push('\"');
101 push_escaped(output, value);
102 output.push('\"');
103 }
104 }
105}
106
107fn push_escaped(output: &mut String, value: &str) {
118 for character in value.chars() {
119 match character {
120 '"' => output.push_str("\\\""),
121 '\\' => output.push_str("\\\\"),
122 '\u{08}' => output.push_str("\\b"),
123 '\t' => output.push_str("\\t"),
124 '\n' => output.push_str("\\n"),
125 '\u{0c}' => output.push_str("\\f"),
126 '\r' => output.push_str("\\r"),
127 ch if must_escape(ch) => push_unicode_escape(output, ch),
128 ch => output.push(ch),
129 }
130 }
131}
132
133fn must_escape(character: char) -> bool {
134 character.is_control()
135 || (character != ' ' && character.is_whitespace())
136 || is_layout_or_spoofing_format(character)
137}
138
139fn is_layout_or_spoofing_format(character: char) -> bool {
142 character == '\u{00AD}'
143 || ('\u{0600}'..='\u{0605}').contains(&character)
144 || character == '\u{061C}'
145 || character == '\u{06DD}'
146 || character == '\u{070F}'
147 || ('\u{0890}'..='\u{0891}').contains(&character)
148 || character == '\u{08E2}'
149 || character == '\u{180E}'
150 || ('\u{200B}'..='\u{200F}').contains(&character)
151 || ('\u{202A}'..='\u{202E}').contains(&character)
152 || ('\u{2060}'..='\u{2064}').contains(&character)
153 || ('\u{2066}'..='\u{206F}').contains(&character)
154 || character == '\u{FEFF}'
155 || ('\u{FFF9}'..='\u{FFFB}').contains(&character)
156 || character == '\u{110BD}'
157 || character == '\u{110CD}'
158 || ('\u{13430}'..='\u{1343F}').contains(&character)
159 || ('\u{1BCA0}'..='\u{1BCA3}').contains(&character)
160 || ('\u{1D173}'..='\u{1D17A}').contains(&character)
161 || character == '\u{E0001}'
162 || ('\u{E0020}'..='\u{E007F}').contains(&character)
163}
164
165fn push_unicode_escape(output: &mut String, character: char) {
166 const HEX: &[u8; 16] = b"0123456789abcdef";
167 let push_unit = |output: &mut String, unit: u16| {
168 output.push_str("\\u");
169 output.push(HEX[((unit >> 12) & 0xf) as usize] as char);
170 output.push(HEX[((unit >> 8) & 0xf) as usize] as char);
171 output.push(HEX[((unit >> 4) & 0xf) as usize] as char);
172 output.push(HEX[(unit & 0xf) as usize] as char);
173 };
174 let code = u32::from(character);
175 if code < 0x1_0000 {
176 push_unit(output, code as u16);
177 } else {
178 let adjusted = code - 0x1_0000;
179 let high = 0xD800 + ((adjusted >> 10) as u16);
180 let low = 0xDC00 + ((adjusted & 0x3FF) as u16);
181 push_unit(output, high);
182 push_unit(output, low);
183 }
184}