use crate::commands::now;
use crate::output;
use crate::when;
use anyhow::{Result, bail};
use sefy_core::{Due, Place, Report, Vault};
struct Row<'a> {
group: String,
place: &'a Place,
detail: String,
}
pub fn run(vault: &Vault) -> Result<()> {
let now = now();
let report = sefy_core::audit(vault, now)?;
print(&report, now);
let findings = report.findings();
if findings > 0 {
bail!("{}", output::count(findings, "finding"));
}
Ok(())
}
fn print(report: &Report, now: i64) {
println!(
"looked at {} and {} in {}",
output::count(report.passwords, "password"),
output::count(report.dated, "date"),
output::count(report.items, "item")
);
if report.unknown > 0 {
println!(
"{} of a kind this sefy does not know {} not looked into",
output::count(report.unknown, "item"),
if report.unknown == 1 { "was" } else { "were" }
);
}
if report.findings() == 0 {
println!("nothing to look at");
return;
}
let mut sections: Vec<(&str, String, Vec<Row<'_>>)> = Vec::new();
if !report.weak.is_empty() {
sections.push((
"weak",
format!(
"{} easy to guess",
are(report.weak.len(), "password", "is", "are")
),
report
.weak
.iter()
.map(|weak| Row {
group: String::new(),
place: &weak.place,
detail: format!("strength {}/{}", weak.score, sefy_core::Strength::MAX_SCORE),
})
.collect(),
));
}
if !report.reused.is_empty() {
sections.push((
"reused",
format!(
"{} shared by more than one item",
are(report.reused.len(), "password", "is", "are each")
),
grouped(&report.reused),
));
}
if !report.duplicated.is_empty() {
sections.push((
"duplicated",
format!(
"{} {} in more than one item, likely a leftover copy",
report.duplicated.len(),
if report.duplicated.len() == 1 {
"card, token or key is"
} else {
"cards, tokens or keys are each"
}
),
grouped(&report.duplicated),
));
}
if !report.old.is_empty() {
sections.push((
"old",
format!(
"{} unchanged for over a year",
are(report.old.len(), "password", "has been", "have been")
),
report
.old
.iter()
.map(|old| Row {
group: String::new(),
place: &old.place,
detail: format!(
"since {} ({} days)",
when::date(old.since),
(now - old.since).div_euclid(86_400)
),
})
.collect(),
));
}
if !report.expired.is_empty() {
sections.push((
"expired",
format!(
"{} past the last day it works",
are(report.expired.len(), "record", "is", "are")
),
dues(&report.expired, now),
));
}
if !report.expiring.is_empty() {
sections.push((
"expiring",
format!(
"{} within {} days",
are(
report.expiring.len(),
"record",
"stops working",
"stop working"
),
sefy_core::expiry::SOON_DAYS
),
dues(&report.expiring, now),
));
}
if !report.unreadable.is_empty() {
sections.push((
"unreadable",
format!(
"{} not one sefy can read; fix with: sefy edit ID --set {}=2027-01-31",
are(report.unreadable.len(), "date", "is", "are"),
sefy_core::expiry::FIELD
),
report
.unreadable
.iter()
.map(|odd| Row {
group: String::new(),
place: &odd.place,
detail: format!("{:?}", odd.value),
})
.collect(),
));
}
let rows = || sections.iter().flat_map(|(_, _, rows)| rows.iter());
let label = sections
.iter()
.map(|(label, _, _)| label.len())
.max()
.unwrap_or(0)
+ 2;
let id = rows()
.map(|row| row.place.id.to_string().len())
.max()
.unwrap_or(1);
let title = rows()
.map(|row| row.place.title.chars().count())
.max()
.unwrap_or(0)
.min(30);
let kind = rows()
.map(|row| row.place.kind.as_str().len())
.max()
.unwrap_or(0);
let field = rows()
.map(|row| row.place.field.chars().count())
.max()
.unwrap_or(0);
for (name, headline, rows) in §ions {
println!();
println!("{name:<label$}{headline}");
for row in rows {
let line = format!(
"{:>marker$} {:>id$} {:<title$} {:<kind$} {:<field$} {}",
row.group,
row.place.id,
output::truncate(&row.place.title, title),
row.place.kind.as_str(),
row.place.field,
row.detail,
marker = label - 1
);
println!("{}", line.trim_end());
}
}
println!();
}
fn are(n: usize, noun: &str, one: &str, many: &str) -> String {
format!(
"{} {}",
output::count(n, noun),
if n == 1 { one } else { many }
)
}
fn grouped(groups: &[Vec<Place>]) -> Vec<Row<'_>> {
groups
.iter()
.enumerate()
.flat_map(|(index, places)| {
places.iter().enumerate().map(move |(position, place)| Row {
group: if position == 0 {
format!("{})", index + 1)
} else {
String::new()
},
place,
detail: String::new(),
})
})
.collect()
}
fn dues(dues: &[Due], now: i64) -> Vec<Row<'_>> {
dues.iter()
.map(|due| Row {
group: String::new(),
place: &due.place,
detail: format!(
"{} ({})",
due.expiry,
when::days_away(due.expiry.days_left(now))
),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_count_agrees_with_its_verb() {
assert_eq!(are(1, "password", "is", "are"), "1 password is");
assert_eq!(are(2, "password", "is", "are"), "2 passwords are");
}
}