// SPDX-FileCopyrightText: nexB Inc. and others
// ScanCode is a trademark of nexB Inc.
// SPDX-FileCopyrightText: Provenant contributors
// SPDX-License-Identifier: Apache-2.0
// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
use std::collections::BTreeMap;
use std::io::{self, Write};
use minijinja::{Environment, context};
use crate::output_schema::{Output, OutputFileType};
use super::shared::{io_other, sorted_files};
pub(crate) fn write_html_report(output: &Output, writer: &mut dyn Write) -> io::Result<()> {
let mut license_copyright_rows = Vec::<BTreeMap<String, String>>::new();
let mut file_rows = Vec::<BTreeMap<String, String>>::new();
let mut holder_rows = Vec::<BTreeMap<String, String>>::new();
let mut author_rows = Vec::<BTreeMap<String, String>>::new();
let mut email_rows = Vec::<BTreeMap<String, String>>::new();
let mut url_rows = Vec::<BTreeMap<String, String>>::new();
let mut package_rows = Vec::<BTreeMap<String, String>>::new();
for file in sorted_files(&output.files) {
let mut file_row = BTreeMap::new();
file_row.insert("path".to_string(), file.path.clone());
file_row.insert(
"type".to_string(),
match file.file_type {
OutputFileType::File => "file".to_string(),
OutputFileType::Directory => "directory".to_string(),
},
);
file_row.insert("name".to_string(), file.name.clone());
file_row.insert("extension".to_string(), file.extension.clone());
file_row.insert("size".to_string(), file.size.to_string());
file_row.insert(
"sha1".to_string(),
file.sha1.clone().unwrap_or_else(|| "None".to_string()),
);
file_row.insert(
"md5".to_string(),
file.md5.clone().unwrap_or_else(|| "None".to_string()),
);
file_row.insert("files_count".to_string(), String::new());
file_row.insert("mime_type".to_string(), html_opt(file.mime_type.as_deref()));
file_row.insert(
"file_type".to_string(),
html_opt(file.file_type_label.as_deref()),
);
file_row.insert(
"programming_language".to_string(),
html_opt(file.programming_language.as_deref()),
);
file_row.insert(
"is_binary".to_string(),
html_bool(file.is_binary.unwrap_or(false)).to_string(),
);
file_row.insert(
"is_text".to_string(),
html_bool(file.is_text.unwrap_or(false)).to_string(),
);
file_row.insert(
"is_archive".to_string(),
html_bool(file.is_archive.unwrap_or(false)).to_string(),
);
file_row.insert(
"is_media".to_string(),
html_bool(file.is_media.unwrap_or(false)).to_string(),
);
file_row.insert(
"is_source".to_string(),
html_bool(file.is_source.unwrap_or(false)).to_string(),
);
file_row.insert(
"is_script".to_string(),
html_bool(file.is_script.unwrap_or(false)).to_string(),
);
file_rows.push(file_row);
for c in &file.copyrights {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("start".to_string(), c.start_line.to_string());
row.insert("end".to_string(), c.end_line.to_string());
row.insert("what".to_string(), "copyright".to_string());
row.insert("value".to_string(), c.copyright.clone());
license_copyright_rows.push(row);
}
for detection in &file.license_detections {
for m in &detection.matches {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("start".to_string(), m.start_line.to_string());
row.insert("end".to_string(), m.end_line.to_string());
row.insert("what".to_string(), "license".to_string());
row.insert("value".to_string(), detection.license_expression.clone());
license_copyright_rows.push(row);
}
}
for h in &file.holders {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("holder".to_string(), h.holder.clone());
row.insert("start".to_string(), h.start_line.to_string());
row.insert("end".to_string(), h.end_line.to_string());
holder_rows.push(row);
}
for a in &file.authors {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("author".to_string(), a.author.clone());
row.insert("start".to_string(), a.start_line.to_string());
row.insert("end".to_string(), a.end_line.to_string());
author_rows.push(row);
}
for e in &file.emails {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("email".to_string(), e.email.clone());
row.insert("start".to_string(), e.start_line.to_string());
row.insert("end".to_string(), e.end_line.to_string());
email_rows.push(row);
}
for u in &file.urls {
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
row.insert("url".to_string(), u.url.clone());
row.insert("start".to_string(), u.start_line.to_string());
row.insert("end".to_string(), u.end_line.to_string());
url_rows.push(row);
}
for package_data in &file.package_data {
if package_data.package_type.is_none()
&& package_data.name.is_none()
&& package_data.version.is_none()
&& package_data.primary_language.is_none()
{
continue;
}
let mut row = BTreeMap::new();
row.insert("path".to_string(), file.path.clone());
let package_type = package_data
.package_type
.as_ref()
.map(|pt| pt.as_str().to_string());
row.insert(
"type".to_string(),
package_type.unwrap_or_else(|| "None".to_string()),
);
row.insert(
"packaging".to_string(),
html_opt(package_data.namespace.as_deref()),
);
row.insert(
"primary_language".to_string(),
html_opt(package_data.primary_language.as_deref()),
);
package_rows.push(row);
}
}
license_copyright_rows.sort_by(|a, b| {
a["path"]
.cmp(&b["path"])
.then(a["start"].cmp(&b["start"]))
.then(a["what"].cmp(&b["what"]))
});
holder_rows.sort_by(|a, b| a["path"].cmp(&b["path"]).then(a["start"].cmp(&b["start"])));
author_rows.sort_by(|a, b| a["path"].cmp(&b["path"]).then(a["start"].cmp(&b["start"])));
email_rows.sort_by(|a, b| a["path"].cmp(&b["path"]).then(a["start"].cmp(&b["start"])));
url_rows.sort_by(|a, b| a["path"].cmp(&b["path"]).then(a["start"].cmp(&b["start"])));
package_rows.sort_by(|a, b| a["path"].cmp(&b["path"]).then(a["type"].cmp(&b["type"])));
let rendered = Environment::new()
.render_str(
HTML_REPORT_TEMPLATE,
context! {
license_copyright_rows,
file_rows,
holder_rows,
author_rows,
email_rows,
url_rows,
package_rows,
},
)
.map_err(io_other)?;
writer.write_all(rendered.as_bytes())
}
fn html_opt(value: Option<&str>) -> String {
value.unwrap_or("None").to_string()
}
fn html_bool(value: bool) -> &'static str {
if value { "True" } else { "False" }
}
const HTML_REPORT_TEMPLATE: &str = r##"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Provenant HTML Report</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Crect width='120' height='120' fill='%23171b24'/%3E%3Cg stroke='%23f1ece4' stroke-width='8' stroke-linecap='round' fill='none'%3E%3Cpath d='M60 86 V32'/%3E%3Cpath d='M60 58 L35 32'/%3E%3Cpath d='M60 58 L85 32'/%3E%3C/g%3E%3Ccircle cx='35' cy='32' r='8' fill='%23f1ece4'/%3E%3Ccircle cx='60' cy='32' r='8' fill='%23f1ece4'/%3E%3Ccircle cx='85' cy='32' r='8' fill='%23f1ece4'/%3E%3Ccircle cx='60' cy='86' r='10' fill='%23e08a3c'/%3E%3C/svg%3E" />
<style type="text/css">
table {
border-collapse: collapse;
border: 1px solid gray;
margin-bottom: 20px;
}
td {
padding: 5px 5px;
border-style: solid;
border-width: 1px;
overflow: hidden;
}
th {
padding: 10px 5px;
border-style: solid;
border-width: 1px;
overflow: hidden;
border-color: gray;
color: #fff;
background-color: #5e81b7;
}
tr:nth-child(even) {
background-color: #ffffff;
}
tr:nth-child(odd) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #eeeeee;
}
* {
font-family: Helvetica, Arial, sans-serif;
font-weight: normal;
font-size: 12px;
}
.report-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 22px;
}
.report-header .brand-mark {
width: 40px;
height: 40px;
flex: none;
}
.report-header .brand-name {
font-size: 20px;
font-weight: 600;
color: #171b24;
}
.report-header .brand-sub {
font-size: 12px;
color: #5b6472;
}
</style>
</head>
<body>
<header class="report-header">
<svg class="brand-mark" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect width="120" height="120" rx="26" fill="#171b24" />
<g stroke="#f1ece4" stroke-width="8" stroke-linecap="round" fill="none">
<path d="M60 86 V32" />
<path d="M60 58 L35 32" />
<path d="M60 58 L85 32" />
</g>
<circle cx="35" cy="32" r="8" fill="#f1ece4" />
<circle cx="60" cy="32" r="8" fill="#f1ece4" />
<circle cx="85" cy="32" r="8" fill="#f1ece4" />
<circle cx="60" cy="86" r="10" fill="#e08a3c" />
</svg>
<div class="brand-text">
<div class="brand-name">Provenant</div>
<div class="brand-sub">License & copyright scan report</div>
</div>
</header>
<table>
<caption>Copyrights and Licenses Information</caption>
<thead>
<tr>
<th>path</th>
<th>start</th>
<th>end</th>
<th>what</th>
<th>value</th>
</tr>
</thead>
<tbody>
{% for row in license_copyright_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
<td>{{ row.what }}</td>
<td>{{ row.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>File Information</caption>
<thead>
<tr>
<th>path</th>
<th>type</th>
<th>name</th>
<th>extension</th>
<th>size</th>
<th>sha1</th>
<th>md5</th>
<th>files_count</th>
<th>mime_type</th>
<th>file_type</th>
<th>programming_language</th>
<th>is_binary</th>
<th>is_text</th>
<th>is_archive</th>
<th>is_media</th>
<th>is_source</th>
<th>is_script</th>
</tr>
</thead>
<tbody>
{% for row in file_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.type }}</td>
<td>{{ row.name }}</td>
<td>{{ row.extension }}</td>
<td>{{ row.size }}</td>
<td>{{ row.sha1 }}</td>
<td>{{ row.md5 }}</td>
<td>{{ row.files_count }}</td>
<td>{{ row.mime_type }}</td>
<td>{{ row.file_type }}</td>
<td>{{ row.programming_language }}</td>
<td>{{ row.is_binary }}</td>
<td>{{ row.is_text }}</td>
<td>{{ row.is_archive }}</td>
<td>{{ row.is_media }}</td>
<td>{{ row.is_source }}</td>
<td>{{ row.is_script }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>Holders</caption>
<thead>
<tr>
<th>path</th>
<th>holder</th>
<th>start</th>
<th>end</th>
</tr>
</thead>
<tbody>
{% for row in holder_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.holder }}</td>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>Authors</caption>
<thead>
<tr>
<th>path</th>
<th>Author</th>
<th>start</th>
<th>end</th>
</tr>
</thead>
<tbody>
{% for row in author_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.author }}</td>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>Emails</caption>
<thead>
<tr>
<th>path</th>
<th>email</th>
<th>start</th>
<th>end</th>
</tr>
</thead>
<tbody>
{% for row in email_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.email }}</td>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>Urls</caption>
<thead>
<tr>
<th>path</th>
<th>url</th>
<th>start</th>
<th>end</th>
</tr>
</thead>
<tbody>
{% for row in url_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.url }}</td>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<caption>Package Information</caption>
<thead>
<tr>
<th>path</th>
<th>type</th>
<th>packaging</th>
<th>primary_language</th>
</tr>
</thead>
<tbody>
{% for row in package_rows %}
<tr>
<td>{{ row.path }}</td>
<td>{{ row.type }}</td>
<td>{{ row.packaging }}</td>
<td>{{ row.primary_language }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
<footer>
<p>
Generated with Provenant and provided on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. No content created from Provenant should be considered or
used as legal advice. Consult an attorney for legal advice. Provenant is a free software code
scanning tool. Visit
<a href="https://github.com/getprovenant/provenant/">https://github.com/getprovenant/provenant/</a>
for support and download.
</p>
</footer>
</html>
"##;