csaf_walker/report/
render.rs

1use crate::report::{DocumentKey, ReportResult};
2use std::{
3    fmt::{Display, Formatter},
4    path::Path,
5};
6use url::Url;
7use walker_common::{locale::Formatted, report, report::Summary};
8
9#[derive(Clone, Debug)]
10pub struct ReportRenderOption<'a> {
11    pub output: &'a Path,
12
13    pub base_url: &'a Option<Url>,
14    pub source_url: &'a Option<Url>,
15}
16
17pub fn render_to_html<W: std::io::Write>(
18    out: &mut W,
19    report: &ReportResult,
20    options: ReportRenderOption,
21) -> anyhow::Result<()> {
22    report::render(
23        out,
24        "CSAF Report",
25        HtmlReport {
26            result: report,
27            base_url: options.base_url,
28            source_url: options.source_url,
29        },
30        &Default::default(),
31    )?;
32
33    Ok(())
34}
35
36#[derive(Copy, Clone, PartialEq, Eq, Debug)]
37pub enum Title {
38    Duplicates,
39    Warnings,
40    Errors,
41}
42
43impl Display for Title {
44    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::Duplicates => f.write_str("Duplicates"),
47            Self::Warnings => f.write_str("Warnings"),
48            Self::Errors => f.write_str("Errors"),
49        }
50    }
51}
52
53struct HtmlReport<'r> {
54    result: &'r ReportResult<'r>,
55    /// The base of the source, used to generate a relative URL
56    base_url: &'r Option<Url>,
57    /// Override source URL
58    source_url: &'r Option<Url>,
59}
60
61impl HtmlReport<'_> {
62    fn render_duplicates(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63        let count = self.result.duplicates.duplicates.len();
64        let data = |f: &mut Formatter<'_>| {
65            for (k, v) in &self.result.duplicates.duplicates {
66                let (_url, label) = self.link_document(k);
67                writeln!(
68                    f,
69                    r#"
70            <tr>
71                <td><code>{label}<code></td>
72                <td class="text-right">{v}</td>
73            </tr>
74            "#,
75                    label = html_escape::encode_text(&label),
76                )?;
77            }
78            Ok(())
79        };
80
81        if !self.result.duplicates.duplicates.is_empty() {
82            let total: usize = self.result.duplicates.duplicates.values().sum();
83
84            Self::render_table(
85                f,
86                [count],
87                Title::Duplicates,
88                format!(
89                    "{count} duplicates URLs found, totaling {total} redundant entries",
90                    count = Formatted(count),
91                    total = Formatted(total),
92                )
93                .as_str(),
94                data,
95            )?;
96        }
97        Ok(())
98    }
99
100    fn render_errors(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101        let count = self.result.errors.len();
102
103        let data = |f: &mut Formatter<'_>| {
104            for (k, v) in self.result.errors {
105                let (url, label) = self.link_document(k);
106
107                let id = format!("error-{url}");
108                let id = html_escape::encode_quoted_attribute(&id);
109
110                writeln!(
111                    f,
112                    r##"
113            <tr>
114                <td id="{id}"><a href="{url}" target="_blank" style="white-space: nowrap;">{label}</a> <a class="link-secondary" href="#{id}">§</a></td>
115                <td><code>{v}</code></td>
116            </tr>
117            "##,
118                    url = html_escape::encode_quoted_attribute(&url),
119                    label = html_escape::encode_text(&label),
120                    v = html_escape::encode_text(&v),
121                )?;
122            }
123            Ok(())
124        };
125        if count > 0 {
126            Self::render_table(
127                f,
128                [count],
129                Title::Errors,
130                &format!("{count} file(s) with errors", count = Formatted(count),),
131                data,
132            )?;
133        }
134        Ok(())
135    }
136
137    fn render_table<F>(
138        f: &mut Formatter<'_>,
139        count: impl IntoIterator<Item = usize>,
140        title: Title,
141        sub_title: &str,
142        data: F,
143    ) -> std::fmt::Result
144    where
145        F: Fn(&mut Formatter<'_>) -> std::fmt::Result,
146    {
147        Self::title(f, title, count)?;
148        writeln!(f, "<p>{sub_title}</p>")?;
149
150        writeln!(
151            f,
152            r#"
153    <table class="table">
154        <thead>
155            <tr>
156                <th scope="col">File</th>
157                <th scope="col">{title}</th>
158            </tr>
159        </thead>
160
161        <tbody>
162"#
163        )?;
164        data(f)?;
165        writeln!(f, "</tbody></table>")?;
166
167        Ok(())
168    }
169
170    fn render_warnings(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
171        let file_count = self.result.warnings.len();
172        let total_count = self.result.warnings.values().map(|w| w.len()).sum();
173
174        let data = |f: &mut Formatter<'_>| {
175            for (k, v) in self.result.warnings {
176                let (url, label) = self.link_document(k);
177
178                let id = format!("warning-{url}");
179                let id = html_escape::encode_quoted_attribute(&id);
180
181                writeln!(
182                    f,
183                    r##"
184            <tr>
185                <td id="{id}"><a href="{url}" target="_blank" style="white-space: nowrap;">{label}</a> <a class="link-secondary" href="#{id}">§</a></td>
186                <td><ul>
187"##,
188                    url = html_escape::encode_quoted_attribute(&url),
189                    label = html_escape::encode_text(&label),
190                )?;
191
192                for text in v {
193                    writeln!(
194                        f,
195                        r#"
196            <li>
197                <code>{v}</code>
198            </li>
199            "#,
200                        v = html_escape::encode_text(&text),
201                    )?;
202                }
203
204                writeln!(
205                    f,
206                    r#"
207                    </ul>
208                </td>
209            </tr>
210"#
211                )?;
212            }
213
214            Ok(())
215        };
216        if total_count > 0 {
217            Self::render_table(
218                f,
219                [file_count, total_count],
220                Title::Warnings,
221                &format!(
222                    "{total_count} warning(s) in {file_count} file(s) detected",
223                    total_count = Formatted(total_count),
224                    file_count = Formatted(file_count),
225                ),
226                data,
227            )?;
228        }
229        Ok(())
230    }
231
232    fn gen_link(&self, key: &DocumentKey) -> Option<(String, String)> {
233        let label = key.url.clone();
234
235        // the full URL of the document
236        let url = key.distribution_url.join(&key.url).ok()?;
237
238        let url = match &self.base_url {
239            Some(base_url) => base_url
240                .make_relative(&url)
241                .unwrap_or_else(|| url.to_string()),
242            None => url.to_string(),
243        };
244
245        Some((url, label))
246    }
247
248    /// create a link towards a document, returning url and label
249    fn link_document(&self, key: &DocumentKey) -> (String, String) {
250        self.gen_link(key)
251            .unwrap_or_else(|| (key.url.clone(), key.url.clone()))
252    }
253
254    fn title(
255        f: &mut Formatter<'_>,
256        title: Title,
257        count: impl IntoIterator<Item = usize>,
258    ) -> std::fmt::Result {
259        write!(f, "<h2>{title}")?;
260
261        for count in count {
262            let (class, text) = if count > 0 {
263                (
264                    match title {
265                        Title::Warnings => "text-bg-warning",
266                        _ => "text-bg-danger",
267                    },
268                    Formatted(count).to_string(),
269                )
270            } else {
271                ("text-bg-light", "None".to_string())
272            };
273
274            write!(
275                f,
276                r#" <span class="badge {class} rounded-pill">{text}</span>"#,
277            )?;
278        }
279
280        writeln!(f, "</h2>")?;
281
282        Ok(())
283    }
284
285    fn render_total(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
286        let mut summary = Vec::new();
287
288        summary.push(("Total", Formatted(self.result.total).to_string()));
289        if let Some(source) = self.source_url.as_ref().or(self.base_url.as_ref()) {
290            summary.push(("Source", source.to_string()));
291        }
292
293        Summary(summary).fmt(f)
294    }
295}
296
297impl Display for HtmlReport<'_> {
298    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
299        self.render_total(f)?;
300        self.render_duplicates(f)?;
301        self.render_errors(f)?;
302        self.render_warnings(f)?;
303        Ok(())
304    }
305}
306
307#[cfg(test)]
308mod test {
309    use super::*;
310    use reqwest::Url;
311    use std::path::PathBuf;
312
313    #[test]
314    fn test_link() {
315        let details = ReportResult {
316            total: 0,
317            duplicates: &Default::default(),
318            errors: &Default::default(),
319            warnings: &Default::default(),
320        };
321        let _output = PathBuf::default();
322        let base_url = Some(Url::parse("file:///foo/bar/").expect("example value must parse"));
323        let report = HtmlReport {
324            result: &details,
325            base_url: &base_url,
326            source_url: &None,
327        };
328
329        let (url, _label) = report.link_document(&DocumentKey {
330            distribution_url: Url::parse("file:///foo/bar/distribution/")
331                .expect("example value must parse"),
332            url: "2023/cve.json".to_string(),
333        });
334
335        assert_eq!(url, "distribution/2023/cve.json");
336    }
337}