use std::fmt;
use crate::Severity;
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanSummary {
scanned_sources: usize,
scanned_bytes: usize,
reports_with_findings: usize,
reports_without_findings: usize,
total_findings: usize,
total_candidates: usize,
reports_with_candidates: usize,
critical: usize,
high: usize,
medium: usize,
low: usize,
info: usize,
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub(crate) struct ScanSummaryStats {
pub(crate) scanned_sources: usize,
pub(crate) scanned_bytes: usize,
pub(crate) reports_with_findings: usize,
pub(crate) reports_without_findings: usize,
pub(crate) total_findings: usize,
pub(crate) total_candidates: usize,
pub(crate) reports_with_candidates: usize,
pub(crate) critical: usize,
pub(crate) high: usize,
pub(crate) medium: usize,
pub(crate) low: usize,
pub(crate) info: usize,
}
impl ScanSummary {
pub(crate) const fn from_stats(stats: ScanSummaryStats) -> Self {
Self {
scanned_sources: stats.scanned_sources,
scanned_bytes: stats.scanned_bytes,
reports_with_findings: stats.reports_with_findings,
reports_without_findings: stats.reports_without_findings,
total_findings: stats.total_findings,
total_candidates: stats.total_candidates,
reports_with_candidates: stats.reports_with_candidates,
critical: stats.critical,
high: stats.high,
medium: stats.medium,
low: stats.low,
info: stats.info,
}
}
#[must_use]
pub const fn scanned_sources(self) -> usize {
self.scanned_sources
}
#[must_use]
pub const fn scanned_bytes(self) -> usize {
self.scanned_bytes
}
#[must_use]
pub const fn reports_with_findings(self) -> usize {
self.reports_with_findings
}
#[must_use]
pub const fn reports_without_findings(self) -> usize {
self.reports_without_findings
}
#[must_use]
pub const fn total_findings(self) -> usize {
self.total_findings
}
#[must_use]
pub const fn total_candidates(self) -> usize {
self.total_candidates
}
#[must_use]
pub const fn reports_with_candidates(self) -> usize {
self.reports_with_candidates
}
#[must_use]
pub const fn has_candidates(self) -> bool {
self.total_candidates != 0
}
#[must_use]
pub const fn by_severity(self, severity: Severity) -> usize {
match severity {
Severity::Critical => self.critical,
Severity::High => self.high,
Severity::Medium => self.medium,
Severity::Low => self.low,
Severity::Info => self.info,
}
}
#[must_use]
pub const fn critical(self) -> usize {
self.critical
}
#[must_use]
pub const fn high(self) -> usize {
self.high
}
#[must_use]
pub const fn medium(self) -> usize {
self.medium
}
#[must_use]
pub const fn low(self) -> usize {
self.low
}
#[must_use]
pub const fn info(self) -> usize {
self.info
}
#[must_use]
pub const fn is_clean(self) -> bool {
self.total_findings == 0 && self.total_candidates == 0
}
#[must_use]
pub const fn has_critical(self) -> bool {
self.critical != 0
}
}
impl fmt::Display for ScanSummary {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
concat!(
"sources: {} (with findings: {}, without findings: {})\n",
"bytes: {}\n",
"findings: {}\n",
"ambiguous candidates: {} (reports: {})\n",
"critical: {}\n",
"high: {}\n",
"medium: {}\n",
"low: {}\n",
"info: {}"
),
self.scanned_sources,
self.reports_with_findings,
self.reports_without_findings,
self.scanned_bytes,
self.total_findings,
self.total_candidates,
self.reports_with_candidates,
self.critical,
self.high,
self.medium,
self.low,
self.info,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposes_counts_and_status_helpers() {
let summary = ScanSummary::from_stats(ScanSummaryStats {
scanned_sources: 3,
scanned_bytes: 128,
reports_with_findings: 2,
reports_without_findings: 1,
total_findings: 5,
total_candidates: 2,
reports_with_candidates: 1,
critical: 1,
high: 2,
medium: 1,
low: 1,
info: 0,
});
assert_eq!(summary.scanned_sources(), 3);
assert_eq!(summary.scanned_bytes(), 128);
assert_eq!(summary.reports_with_findings(), 2);
assert_eq!(summary.reports_without_findings(), 1);
assert_eq!(summary.total_findings(), 5);
assert_eq!(summary.total_candidates(), 2);
assert_eq!(summary.reports_with_candidates(), 1);
assert!(summary.has_candidates());
assert_eq!(summary.by_severity(Severity::Critical), 1);
assert_eq!(summary.by_severity(Severity::High), 2);
assert_eq!(summary.by_severity(Severity::Medium), 1);
assert_eq!(summary.by_severity(Severity::Low), 1);
assert_eq!(summary.by_severity(Severity::Info), 0);
assert!(summary.has_critical());
assert!(!summary.is_clean());
}
#[test]
fn formats_stable_plain_text_summary() {
let summary = ScanSummary::from_stats(ScanSummaryStats {
scanned_sources: 2,
scanned_bytes: 64,
reports_with_findings: 1,
reports_without_findings: 1,
total_findings: 1,
total_candidates: 2,
reports_with_candidates: 1,
critical: 1,
high: 0,
medium: 0,
low: 0,
info: 0,
});
assert_eq!(
summary.to_string(),
concat!(
"sources: 2 (with findings: 1, without findings: 1)\n",
"bytes: 64\n",
"findings: 1\n",
"ambiguous candidates: 2 (reports: 1)\n",
"critical: 1\n",
"high: 0\n",
"medium: 0\n",
"low: 0\n",
"info: 0"
),
);
}
}