use crate::declare_detector;
use crate::diagnostic::{Action, ActionPlan, Diagnostic, MaintscriptAction};
use crate::{FixerError, FixerPreferences};
use chrono::{DateTime, NaiveDate, Utc};
use debian_analyzer::maintscripts::{Entry, Maintscript};
use debian_workspace::Workspace;
use debversion::Version;
use distro_info::{DebianDistroInfo, DistroInfo};
use std::path::{Path, PathBuf};
use std::str::FromStr;
const DEFAULT_AGE_THRESHOLD_DAYS: i64 = 5 * 365;
fn find_maintscript_files(ws: &dyn Workspace) -> Result<Vec<String>, FixerError> {
let mut entries = match ws.list_dir(Path::new("debian"))? {
Some(e) => e,
None => return Ok(vec![]),
};
entries.sort();
Ok(entries
.into_iter()
.filter(|name| name == "maintscript" || name.ends_with(".maintscript"))
.collect())
}
fn get_date_threshold(upgrade_release: Option<&str>) -> Result<NaiveDate, FixerError> {
if let Some(release) = upgrade_release {
if let Ok(debian_info) = DebianDistroInfo::new() {
let all_releases = debian_info.all_at(Utc::now().naive_utc().date());
for series in all_releases {
if series.codename().eq_ignore_ascii_case(release) || series.series() == release {
if let Some(release_date) = series.release() {
return Ok(*release_date);
}
}
}
}
}
let now = Utc::now();
let threshold = now.date_naive() - chrono::Duration::days(DEFAULT_AGE_THRESHOLD_DAYS);
Ok(threshold)
}
fn parse_changelog_dates(ws: &dyn Workspace) -> Result<Vec<(Version, DateTime<Utc>)>, FixerError> {
let changelog = match ws.parsed_changelog() {
Ok(cl) => cl,
Err(debian_workspace::Error::NotFound) => return Ok(vec![]),
Err(e) => {
return Err(FixerError::Other(format!(
"Failed to parse changelog: {}",
e
)))
}
};
let mut dates = Vec::new();
for entry in changelog.iter() {
if let Some(version) = entry.version() {
match entry.datetime() {
Some(dt) => {
dates.push((version.clone(), dt.with_timezone(&Utc)));
}
None => {
if let Some(timestamp) = entry.timestamp() {
return Err(FixerError::Other(format!(
"Invalid date {:?} for {}",
timestamp, version
)));
}
}
}
}
}
Ok(dates)
}
fn is_well_past(
version: &Version,
cl_dates: &[(Version, DateTime<Utc>)],
date_threshold: &NaiveDate,
) -> bool {
for (cl_version, cl_dt) in cl_dates {
if cl_version <= version && cl_dt.date_naive() > *date_threshold {
return false;
}
}
true
}
struct RemovedEntry {
entry: Entry,
}
fn obsolete_maintscript_entries<F>(
ws: &dyn Workspace,
rel: &Path,
should_remove: F,
) -> Result<Vec<RemovedEntry>, FixerError>
where
F: Fn(Option<&str>, &Version) -> bool,
{
let bytes = match ws.read_file(rel)? {
Some(b) => b,
None => return Ok(Vec::new()),
};
let contents = std::str::from_utf8(&bytes)
.map_err(|e| FixerError::Other(format!("Failed to read maintscript: {}", e)))?;
let script = Maintscript::from_str(contents)
.map_err(|e| FixerError::Other(format!("Failed to parse maintscript: {}", e)))?;
let mut removed = Vec::new();
for entry in script.entries() {
let should = entry
.prior_version()
.map(|v| should_remove(entry.package().map(|s| s.as_str()), v))
.unwrap_or(false);
if should {
removed.push(RemovedEntry {
entry: entry.clone(),
});
}
}
Ok(removed)
}
fn find_version_date(
version: &Version,
cl_dates: &[(Version, DateTime<Utc>)],
) -> Option<DateTime<Utc>> {
cl_dates
.iter()
.find(|(v, _)| v == version)
.map(|(_, dt)| *dt)
}
fn format_removed_entry_detail(
entry: &RemovedEntry,
cl_dates: &[(Version, DateTime<Utc>)],
) -> String {
let version = entry.entry.prior_version().unwrap();
let date_info = find_version_date(version, cl_dates)
.map(|dt| format!(", uploaded on {}", dt.format("%Y-%m-%d")))
.unwrap_or_default();
format!("\"{}\" (version {}{})", entry.entry, version, date_info)
}
pub fn detect(
ws: &dyn Workspace,
preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let maintscripts = find_maintscript_files(ws)?;
if maintscripts.is_empty() {
return Ok(Vec::new());
}
let date_threshold = get_date_threshold(preferences.upgrade_release.as_deref())?;
let cl_dates = parse_changelog_dates(ws)?;
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for name in maintscripts {
let rel = PathBuf::from("debian").join(&name);
let removed = obsolete_maintscript_entries(ws, &rel, |_package, version| {
is_well_past(version, &cl_dates, &date_threshold)
})?;
for r in removed {
let detail = format_removed_entry_detail(&r, &cl_dates);
let label = format!("Remove obsolete maintscript entry {}.", detail);
diagnostics.push(Diagnostic::untagged(
detail,
label,
vec![Action::Maintscript(MaintscriptAction::DropEntry {
file: rel.clone(),
entry: r.entry.to_string(),
})],
));
}
}
Ok(diagnostics)
}
fn describe_aggregate(fixed: &[(Diagnostic, ActionPlan)], _actions: &[Action]) -> String {
let summary = if fixed.len() == 1 {
"Remove an obsolete maintscript entry.".to_string()
} else {
format!("Remove {} obsolete maintscript entries.", fixed.len())
};
let details: Vec<&str> = fixed.iter().map(|(d, _)| d.message.as_str()).collect();
format!("{}\n\n{}", summary, details.join("\n"))
}
declare_detector! {
name: "ancient-maintscript-entry",
tags: [],
triggers: [
debian_workspace::Trigger::File("debian/maintscript"),
debian_workspace::Trigger::Glob("debian/*.maintscript"),
debian_workspace::Trigger::Changelog(debian_workspace::ChangelogAspect::Version),
debian_workspace::Trigger::Changelog(debian_workspace::ChangelogAspect::Timestamp),
],
cost: crate::detector::DetectorCost::Filesystem,
detect: |ws, prefs| detect(ws, prefs),
describe: |fixed, actions| describe_aggregate(fixed, actions),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_well_past() {
use chrono::TimeZone;
let version = Version::from_str("0.1-1").unwrap();
let cl_dates = vec![
(
Version::from_str("0.1-2").unwrap(),
Utc.with_ymd_and_hms(2011, 3, 22, 16, 47, 42).unwrap(),
),
(
Version::from_str("0.1-1").unwrap(),
Utc.with_ymd_and_hms(2011, 3, 22, 16, 47, 31).unwrap(),
),
];
let threshold = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
assert!(is_well_past(&version, &cl_dates, &threshold));
}
#[test]
fn test_not_well_past() {
use chrono::TimeZone;
let version = Version::from_str("0.1-1").unwrap();
let cl_dates = vec![
(
Version::from_str("0.1-2").unwrap(),
Utc.with_ymd_and_hms(2021, 3, 22, 16, 47, 42).unwrap(),
),
(
Version::from_str("0.1-1").unwrap(),
Utc.with_ymd_and_hms(2021, 3, 22, 16, 47, 31).unwrap(),
),
];
let threshold = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
assert!(!is_well_past(&version, &cl_dates, &threshold));
}
}