libredefender 0.6.0

Light-weight antivirus scanner for Linux
#![allow(
    clippy::wildcard_imports,
    clippy::non_ascii_literal,
    clippy::missing_errors_doc
)]

use chrono::{DateTime, Local, Utc};
use chrono_humanize::HumanTime;
use colored::{Color, ColoredString, Colorize};
use env_logger::Env;
use libredefender::args::{Args, SubCommand};
use libredefender::config;
use libredefender::db::Database;
use libredefender::errors::*;
use libredefender::nice;
use libredefender::notify;
use libredefender::scan;
use libredefender::schedule;
use libredefender::utils;
use num_format::{Locale, ToFormattedString};
use std::borrow::Cow;
use std::path::Path;
use structopt::StructOpt;

fn format_num(num: usize, zero_is_bad: bool) -> ColoredString {
    let color = if zero_is_bad ^ (num != 0) {
        Color::Red
    } else {
        Color::Green
    };
    num.to_formatted_string(&Locale::en).color(color).bold()
}

fn format_datetime(dt: &Option<DateTime<Utc>>) -> Cow<'_, str> {
    if let Some(dt) = dt {
        let elapsed_since = dt.signed_duration_since(Utc::now());
        Cow::Owned(format!(
            "{} {}",
            dt.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S %Z"),
            format!("({})", HumanTime::from(elapsed_since)).bold()
        ))
    } else {
        Cow::Borrowed("-")
    }
}

fn print_line(line: &str, good: bool) {
    if good {
        println!("{}", line);
    } else {
        println!("{}", line);
    }
}

fn main() -> Result<()> {
    let args = Args::from_args();

    let logging = match (args.quiet, args.verbose) {
        (true, _) => "warn",
        (false, 0) => "info",
        (false, 1) => "info,libredefender=debug",
        (false, 2) => "debug",
        (false, _) => "debug,libredefender=trace",
    };
    env_logger::init_from_env(Env::default().default_filter_or(logging));

    if args.colors {
        colored::control::set_override(true);
    }

    match args.subcommand {
        None => {
            let db = Database::load().context("Failed to load database")?;
            let data = db.data();

            print_line(
                &format!(
                    "Last scan                 {}",
                    format_datetime(&data.last_scan)
                ),
                data.last_scan.is_some(),
            );
            print_line(
                &format!(
                    "Threats present           {}",
                    format_num(data.threats.len(), false)
                ),
                data.threats.is_empty(),
            );

            print_line(
                &format!(
                    "Signatures                {}",
                    format_num(data.signature_count, true)
                ),
                data.signature_count > 0,
            );
            print_line(
                &format!(
                    "Signatures updated        {}",
                    format_datetime(&data.signatures_age)
                ),
                data.signatures_age.is_some(),
            );

            println!();
            println!(
                "{}",
                "Start a scan with `libredefender scan` or run `libredefender help`".green()
            );
        }
        Some(SubCommand::Scan(args)) => {
            nice::setup()?;
            scan::init()?;
            scan::run(args)?;
        }
        Some(SubCommand::Scheduler(args)) => {
            nice::setup()?;
            scan::init()?;
            schedule::run(&args)?;
        }
        Some(SubCommand::Infections(args)) => {
            let mut db = Database::load().context("Failed to load database")?;
            let data = db.data_mut();

            let mut deleted = Vec::new();

            for (path, names) in &data.threats {
                if args.delete || args.delete_all {
                    let should_delete = if args.delete_all {
                        true
                    } else {
                        utils::ask_confirmation(&format!("Delete {:?} at {:?}", names, path))?
                    };

                    if should_delete {
                        info!("Deleting {:?} at {:?}", names, path);
                        if let Err(err) = utils::ensure_deleted(path) {
                            error!("Failed to delete {:?}: {:#}", path, err);
                        } else {
                            deleted.push(path.clone());
                        }
                    }
                } else {
                    for name in names {
                        println!(
                            "{} => {}",
                            name.red().bold(),
                            format!("{:?}", path).yellow(),
                        );
                    }
                }
            }

            if !deleted.is_empty() {
                for path in deleted {
                    data.threats.remove(&path);
                }
                db.store().context("Failed to write database")?;
            }
        }
        Some(SubCommand::TestNotify) => notify::show(Path::new("/just/a/test"), "just/testing")?,
        Some(SubCommand::DumpConfig) => {
            let config = config::load(None).context("Failed to load config")?;

            serde_json::to_writer_pretty(std::io::stdout(), &config)?;
            println!();
        }
        Some(SubCommand::Completions(args)) => args.gen_completions()?,
    }

    Ok(())
}