// fif - a command-line tool for detecting and optionally correcting files with incorrect extensions. // Copyright (C) 2021 Lynnesbian // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #![forbid(unsafe_code)] #![warn(trivial_casts, unused_lifetimes, unused_qualifications)] use std::io::{stdout, BufWriter, Write}; use std::process::exit; use cfg_if::cfg_if; use clap::Clap; use fif::files::{scan_directory, scan_from_walkdir}; use fif::formats::Format; use fif::parameters::OutputFormat; use fif::utils::{os_name, CLAP_LONG_VERSION}; use fif::{formats, parameters}; use log::{debug, error, info, trace, warn, Level}; #[cfg(test)] mod tests; #[doc(hidden)] #[allow(clippy::cognitive_complexity)] fn main() { let args: parameters::Parameters = parameters::Parameters::parse(); let mut builder = env_logger::Builder::new(); builder .filter_level(args.default_verbosity()) // set default log level .parse_default_env() // set log level from RUST_LOG .parse_env("FIF_LOG") // set log level from FIF_LOG .format(|buf, r| { let mut style = buf.default_level_style(r.level()); // use bold for warnings and errors style.set_bold(r.level() <= Level::Warn); // only use the first character of the log level name let abbreviation = style.value(r.level().to_string().chars().next().unwrap()); // e.g. [D] Debug message writeln!(buf, "[{}] {}", abbreviation, r.args()) }) .init(); trace!( "fif {}, running on {} {}", CLAP_LONG_VERSION.as_str(), std::env::consts::ARCH, os_name() ); debug!("Iterating directory: {:?}", args.dir); let extensions = args.extensions(); let excludes = args.excluded_extensions(); if let Some(extensions) = &extensions { debug!("Checking files with extensions: {:?}", extensions); } else if let Some(excludes) = &excludes { debug!("Skipping files with extensions: {:?}", excludes); } else { debug!("Checking files regardless of extensions"); } let entries = match scan_directory(&args.dir, extensions.as_ref(), excludes.as_ref(), &args.get_scan_opts()) { // no need to log anything for fatal errors - fif will already have printed something obvious like // "[ERROR] /fake/path: No such file or directory (os error 2)". we can assume that if this has happened, the dir // given as input doesn't exist or is otherwise unreadable. None => exit(exitcode::NOINPUT), Some(e) => e, }; if entries.is_empty() { warn!("No files matching requested options found."); exit(exitcode::OK); } trace!("Found {} items to check", entries.len()); cfg_if! { if #[cfg(feature = "multi-threaded")] { let use_threads = args.jobs != 1; if use_threads { // 0 is a special case - it should be understood to mean "all available host CPUs" let jobs = if args.jobs == 0 { num_cpus::get() } else { args.jobs }; // set up the global thread pool with the requested number of threads rayon::ThreadPoolBuilder::new().num_threads(jobs).build_global().unwrap(); trace!("Multithreading enabled, using {} threads", jobs); } else { trace!("Multithreading disabled at runtime"); } } else { // `multi-threading` feature disabled let use_threads = false; trace!("Multithreading disabled at compile time"); } } let results: Vec<_> = scan_from_walkdir(&entries, args.canonical_paths, use_threads) .into_iter() .filter( |result| result.is_err() || !result.as_ref().unwrap().valid, // TODO: find a way to trace! the valid files without doing ↓ // || if result.as_ref().unwrap().valid { trace!("{:?} ok", result.as_ref().unwrap().file); false } else { true } ) .collect(); trace!("Scanning complete"); for result in &results { match result { Ok(r) => { debug!( "{:?} is of type {}, should have extension \"{}\"", r.file, r.mime, r.recommended_extension().unwrap_or_else(|| "???".into()) ); } Err(f) => warn!("{}", f), } } if results.is_empty() { info!("All files have valid extensions!"); exit(exitcode::OK); } let mut buffered_stdout = BufWriter::new(stdout()); let result = match args.output_format { OutputFormat::Sh => formats::Shell.write_all(&mut buffered_stdout, &results), OutputFormat::PowerShell => formats::PowerShell.write_all(&mut buffered_stdout, &results), #[cfg(feature = "json")] OutputFormat::Json => formats::Json.write_all(&mut buffered_stdout, &results), OutputFormat::Text => formats::Text.write_all(&mut buffered_stdout, &results), }; if result.is_err() { error!("Failed to write to stdout."); exit(exitcode::IOERR); } if buffered_stdout.flush().is_err() { error!("Failed to flush stdout."); exit(exitcode::IOERR); } debug!("Done"); }