const SCHEMA: &str = "boxology.classification-report@2";
pub fn render_text(report: &super::ClassificationReport) -> String {
let mut output = String::from("classification ");
output.push_str(report.verdict().canonical_name());
output.push('\n');
for finding in report.findings() {
output.push_str("finding ");
output.push_str(finding.code());
output.push_str(" path=\"");
push_escaped(&mut output, finding.path());
output.push_str("\" ");
output.push_str(finding.class().canonical_name());
output.push_str(" kind=\"");
push_escaped(&mut output, finding.kind());
output.push_str("\" base=");
push_text_excerpt(&mut output, finding.base_excerpt());
output.push_str(" submitted=");
push_text_excerpt(&mut output, finding.submitted_excerpt());
if let Some(condition) = finding.condition() {
output.push_str(" condition=\"");
push_escaped(&mut output, condition);
output.push('\"');
}
output.push('\n');
}
output
}
pub fn render_json(report: &super::ClassificationReport) -> String {
let mut output = String::from("{\n \"schema\": \"");
push_escaped(&mut output, SCHEMA);
output.push_str("\",\n \"verdict\": \"");
push_escaped(&mut output, report.verdict().canonical_name());
output.push_str("\",\n \"findings\": ");
if report.findings().is_empty() {
output.push_str("[]\n}\n");
return output;
}
output.push_str("[\n");
for (index, finding) in report.findings().iter().enumerate() {
if index != 0 {
output.push_str(",\n");
}
output.push_str(" {\n \"code\": \"");
push_escaped(&mut output, finding.code());
output.push_str("\",\n \"path\": \"");
push_escaped(&mut output, finding.path());
output.push_str("\",\n \"kind\": \"");
push_escaped(&mut output, finding.kind());
output.push_str("\",\n \"class\": \"");
push_escaped(&mut output, finding.class().canonical_name());
output.push_str("\",\n \"base\": ");
push_json_excerpt(&mut output, finding.base_excerpt());
output.push_str(",\n \"submitted\": ");
push_json_excerpt(&mut output, finding.submitted_excerpt());
if let Some(condition) = finding.condition() {
output.push_str(",\n \"condition\": \"");
push_escaped(&mut output, condition);
output.push('\"');
}
output.push_str("\n }");
}
output.push_str("\n ]\n}\n");
output
}
fn push_text_excerpt(output: &mut String, excerpt: Option<&str>) {
match excerpt {
None => output.push('-'),
Some(value) => {
output.push('\"');
push_escaped(output, value);
output.push('\"');
}
}
}
fn push_json_excerpt(output: &mut String, excerpt: Option<&str>) {
match excerpt {
None => output.push_str("null"),
Some(value) => {
output.push('\"');
push_escaped(output, value);
output.push('\"');
}
}
}
fn push_escaped(output: &mut String, value: &str) {
for character in value.chars() {
match character {
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\u{08}' => output.push_str("\\b"),
'\t' => output.push_str("\\t"),
'\n' => output.push_str("\\n"),
'\u{0c}' => output.push_str("\\f"),
'\r' => output.push_str("\\r"),
ch if must_escape(ch) => push_unicode_escape(output, ch),
ch => output.push(ch),
}
}
}
fn must_escape(character: char) -> bool {
character.is_control()
|| (character != ' ' && character.is_whitespace())
|| is_layout_or_spoofing_format(character)
}
fn is_layout_or_spoofing_format(character: char) -> bool {
character == '\u{00AD}'
|| ('\u{0600}'..='\u{0605}').contains(&character)
|| character == '\u{061C}'
|| character == '\u{06DD}'
|| character == '\u{070F}'
|| ('\u{0890}'..='\u{0891}').contains(&character)
|| character == '\u{08E2}'
|| character == '\u{180E}'
|| ('\u{200B}'..='\u{200F}').contains(&character)
|| ('\u{202A}'..='\u{202E}').contains(&character)
|| ('\u{2060}'..='\u{2064}').contains(&character)
|| ('\u{2066}'..='\u{206F}').contains(&character)
|| character == '\u{FEFF}'
|| ('\u{FFF9}'..='\u{FFFB}').contains(&character)
|| character == '\u{110BD}'
|| character == '\u{110CD}'
|| ('\u{13430}'..='\u{1343F}').contains(&character)
|| ('\u{1BCA0}'..='\u{1BCA3}').contains(&character)
|| ('\u{1D173}'..='\u{1D17A}').contains(&character)
|| character == '\u{E0001}'
|| ('\u{E0020}'..='\u{E007F}').contains(&character)
}
fn push_unicode_escape(output: &mut String, character: char) {
const HEX: &[u8; 16] = b"0123456789abcdef";
let push_unit = |output: &mut String, unit: u16| {
output.push_str("\\u");
output.push(HEX[((unit >> 12) & 0xf) as usize] as char);
output.push(HEX[((unit >> 8) & 0xf) as usize] as char);
output.push(HEX[((unit >> 4) & 0xf) as usize] as char);
output.push(HEX[(unit & 0xf) as usize] as char);
};
let code = u32::from(character);
if code < 0x1_0000 {
push_unit(output, code as u16);
} else {
let adjusted = code - 0x1_0000;
let high = 0xD800 + ((adjusted >> 10) as u16);
let low = 0xDC00 + ((adjusted & 0x3FF) as u16);
push_unit(output, high);
push_unit(output, low);
}
}