use std::cell::RefCell;
use super::ui;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Outcome {
Selected,
TraceLimited,
Unverified,
OptionalOmission,
Removed,
Unresolved,
RequiredFailure,
Unchecked,
}
impl Outcome {
fn label(self) -> &'static str {
match self {
Outcome::Selected => "selected",
Outcome::TraceLimited => "trace-limited",
Outcome::Unverified => "unverified",
Outcome::OptionalOmission => "optional omission",
Outcome::Removed => "removed",
Outcome::Unresolved => "unresolved",
Outcome::RequiredFailure => "required failure",
Outcome::Unchecked => "unchecked",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Finding {
pub entrypoint: Option<String>,
pub framework: Option<String>,
pub object: String,
pub outcome: Outcome,
pub reason: String,
pub activated_by: Option<String>,
}
impl Finding {
pub fn new(object: impl Into<String>, outcome: Outcome, reason: impl Into<String>) -> Self {
Finding {
entrypoint: None,
framework: None,
object: object.into(),
outcome,
reason: reason.into(),
activated_by: None,
}
}
pub fn of_rule(mut self, rule: &str) -> Self {
self.framework = Some(rule.to_string());
self
}
pub fn text(&self) -> String {
let mut text = String::new();
if let Some(entrypoint) = &self.entrypoint {
text.push_str(&format!("entrypoint '{entrypoint}': "));
}
if let Some(framework) = &self.framework {
text.push_str(framework);
text.push_str(": ");
}
text.push_str(&format!(
"{}: {}: {}",
self.object,
self.outcome.label(),
self.reason
));
if let Some(soname) = &self.activated_by {
text.push_str(&format!(" (activated by {soname})"));
}
text
}
}
pub fn render(findings: &[Finding]) -> Vec<String> {
let mut sorted: Vec<&Finding> = findings.iter().collect();
sorted.sort();
sorted.dedup();
if sorted.is_empty() {
return Vec::new();
}
let mut lines = vec![format!("Completeness findings ({}):", sorted.len())];
lines.extend(sorted.into_iter().map(|f| ui::finding_line(&f.text())));
lines
}
thread_local! {
static PENDING: RefCell<Vec<Finding>> = const { RefCell::new(Vec::new()) };
static REPORTED: RefCell<Vec<Finding>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn record(finding: Finding) {
PENDING.with(|p| p.borrow_mut().push(finding));
}
pub(crate) fn settle(object: Option<&str>, marker: &str, replacement: &str) {
PENDING.with(|p| {
for finding in p.borrow_mut().iter_mut() {
if object.is_none_or(|object| finding.object == object) {
finding.reason = finding.reason.replace(marker, replacement);
}
}
});
}
pub(crate) fn report(activations: &[super::RuleActivation]) {
let mut findings = PENDING.with(|p| std::mem::take(&mut *p.borrow_mut()));
for finding in &mut findings {
if finding.activated_by.is_none()
&& let Some(rule) = &finding.framework
{
finding.activated_by = activations
.iter()
.find(|a| &a.rule == rule)
.map(|a| a.reason.clone());
}
}
findings.sort();
findings.dedup();
let lines = render(&findings);
if !lines.is_empty() {
eprintln!("\n{}", lines.join("\n"));
}
REPORTED.with(|r| *r.borrow_mut() = findings);
}
pub fn take_reported() -> Vec<Finding> {
REPORTED.with(|r| std::mem::take(&mut *r.borrow_mut()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_block_is_sorted_and_attributed_whatever_the_walk_order() {
let walk = vec![
Finding::new(
"share/gst/registry",
Outcome::RequiredFailure,
"no declared location in the selected source",
)
.of_rule("gstreamer"),
Finding::new("libabsent.so.7", Outcome::Unresolved, "needed by bin/app"),
Finding::new("lib/libz.so.1", Outcome::Selected, "host scan /usr/lib"),
Finding {
entrypoint: Some("gui".to_string()),
..Finding::new(
"set gtk",
Outcome::Unverified,
"not cached; a dry run does not fetch it",
)
},
Finding::new(
"lib/gstreamer-1.0",
Outcome::OptionalOmission,
"no usable plugin group",
)
.of_rule("gstreamer"),
Finding::new("libabsent.so.7", Outcome::Unresolved, "needed by bin/app"),
Finding::new(
"lib/libx.so",
Outcome::Unchecked,
"reads /usr/share/x; carry share/x and the runtime sets X_DIR",
),
];
let activations = vec![super::super::RuleActivation {
rule: "gstreamer".to_string(),
reason: "libgstreamer-1.0.so.0".to_string(),
groups: Vec::new(),
}];
for order in [walk.clone(), walk.iter().rev().cloned().collect()] {
for finding in order {
record(finding);
}
report(&activations);
let lines = render(&take_reported());
assert_eq!(
lines,
vec![
"Completeness findings (6):",
" - lib/libx.so: unchecked: reads /usr/share/x; carry share/x and the \
runtime sets X_DIR",
" - lib/libz.so.1: selected: host scan /usr/lib",
" - libabsent.so.7: unresolved: needed by bin/app",
" - gstreamer: lib/gstreamer-1.0: optional omission: no usable plugin \
group (activated by libgstreamer-1.0.so.0)",
" - gstreamer: share/gst/registry: required failure: no declared location \
in the selected source (activated by libgstreamer-1.0.so.0)",
" - entrypoint 'gui': set gtk: unverified: not cached; a dry run does not \
fetch it",
]
);
}
assert!(render(&[]).is_empty());
}
}