cksfv 0.1.3

A 10x faster cksfv reimplementation using Rust and the crc32fast crate.
Documentation
#[macro_use]
extern crate clap;
extern crate chrono;
extern crate crc32fast;

#[cfg(feature = "mmap")]
extern crate memmap;

use std::cmp::min;
use std::fmt::Debug;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Error as IoError;
use std::io::Read;
use std::io::Write;
use std::iter::IntoIterator;
use std::path::Path;

use chrono::DateTime;
use chrono::Datelike;
use chrono::Local;
use chrono::Timelike;
use crc32fast::Hasher;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;

/// Use a 64k buffer size for better performance.
const DEFAULT_BUFFER_SIZE: usize = 65536;

/// The final value of a CRC32 checksum round.
pub type Crc32 = u32;

// ---------------------------------------------------------------------------

/// Given a path to a file, attempt to compute its CRC32 hash.
fn compute_crc32(file: &Path) -> Result<Crc32, IoError> {
    // check the file is not a directory (File::open is fine opening
    // a directory and will just read it as an empty file, but we want
    // a hard error)
    if file.is_dir() {
        return Err(std::io::Error::from_raw_os_error(21));
    }

    // open the file and compute the hash
    File::open(&file).and_then(compute_crc32_inner)
}

/// Compute a CRC32 from a file content using `mmap`.
#[cfg(feature = "mmap")]
fn compute_crc32_inner(mut file: File) -> Result<Crc32, IoError> {
    let mut hasher = Hasher::new();
    let mmap = unsafe { memmap::MmapOptions::new().map(&file)? };
    hasher.update(&mmap[..]);
    Ok(hasher.finalize())
}

/// Compute a CRC32 from a file content without using `mmap`.
#[cfg(not(feature = "mmap"))]
fn compute_crc32_inner(mut file: File) -> Result<Crc32, IoError> {
    let mut hasher = Hasher::new();
    let mut buffer = [0; DEFAULT_BUFFER_SIZE];
    loop {
        let n = file.read(&mut buffer)?;
        if n == 0 {
            break;
        }
        hasher.update(&buffer[..n]);
    }
    Ok(hasher.finalize())
}

// ---------------------------------------------------------------------------

pub trait WriteDebug: Debug + Write {}

impl<F: Debug + Write> WriteDebug for F {}

#[derive(Debug)]
pub enum Output {
    Devnull,
    Stdout(std::io::Stdout),
    Stderr(std::io::Stderr),
}

impl Output {
    pub fn devnull() -> Self {
        Self::Devnull
    }

    pub fn stdout() -> Self {
        Output::Stdout(std::io::stdout())
    }

    pub fn stderr() -> Self {
        Output::Stderr(std::io::stderr())
    }
}

impl Clone for Output {
    fn clone(&self) -> Self {
        use self::Output::*;
        match self {
            Devnull => Self::devnull(),
            Stdout(_) => Self::stdout(),
            Stderr(_) => Self::stderr(),
        }
    }
}

impl Default for Output {
    fn default() -> Self {
        Self::stderr()
    }
}

impl Write for Output {
    fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
        use self::Output::*;
        match self {
            Devnull => Ok(buf.len()),
            Stdout(out) => out.write(buf),
            Stderr(err) => err.write(buf),
        }
    }

    fn flush(&mut self) -> Result<(), IoError> {
        use self::Output::*;
        match self {
            Devnull => Ok(()),
            Stdout(out) => out.flush(),
            Stderr(err) => err.flush(),
        }
    }
}

// ---------------------------------------------------------------------------

#[derive(Clone, Debug, Getters, MutGetters, Setters)]
pub struct Config {
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    stdout: Output,
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    stderr: Output,
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    quiet: bool,
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    print_basename: bool,
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    ignore_case: bool,
    #[get = "pub"]
    #[get_mut = "pub"]
    #[set = "pub"]
    force_slashes: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self::new()
    }
}

impl Config {
    /// Create a new `Cksfv` instance with default arguments.
    pub fn new() -> Self {
        Config {
            stdout: Output::stdout(),
            stderr: Output::stderr(),
            quiet: false,
            print_basename: false,
            ignore_case: false,
            force_slashes: false,
        }
    }

    pub fn with_stdout(mut self, stdout: Output) -> Self {
        self.stdout = stdout;
        self
    }

    pub fn with_stderr(mut self, stderr: Output) -> Self {
        self.stderr = stderr;
        self
    }

    pub fn with_print_basenamet(mut self, print_basename: bool) -> Self {
        self.print_basename = print_basename;
        self
    }

    /// Consume the configuration instance and get the `stdout` field.
    pub fn extract_stdout(self) -> Output {
        self.stdout
    }
}

// ---------------------------------------------------------------------------

/// Generate a new SFV listing from a list of files.
///
/// This function always writes the result to `config.stdout`, which defaults
/// to `std::io::Stdout` if no configuration is provided.
pub fn newsfv<'a, F, C>(files: F, config: C) -> Result<bool, IoError>
where
    F: IntoIterator<Item = &'a Path>,
    C: Into<Option<Config>>,
{
    // get a default config if none provided.
    let mut cfg: Config = config.into().unwrap_or_default();

    // collect the files
    let files: Vec<&Path> = files.into_iter().collect();

    // generate the headers from the files that where found
    let now: DateTime<Local> = Local::now();
    writeln!(
        cfg.stdout,
        "; Generated by cksfv.rs v{} on {:04}-{:02}-{:02} at {:02}:{:02}.{:02}",
        crate_version!(),
        now.year(),
        now.month(),
        now.day(),
        now.hour(),
        now.minute(),
        now.second(),
    )?;
    writeln!(
        cfg.stdout,
        "; Project web site: {}",
        env!("CARGO_PKG_REPOSITORY")
    )?;
    writeln!(cfg.stdout, ";")?;
    for file in files.iter().filter(|p| p.is_file()) {
        if let Ok(metadata) = std::fs::metadata(file) {
            let mtime: DateTime<Local> = From::from(metadata.modified().unwrap());
            writeln!(
                cfg.stdout,
                "; {:>12}  {:02}:{:02}.{:02} {:04}-{:02}-{:02} {}",
                metadata.len(),
                mtime.hour(),
                mtime.minute(),
                mtime.second(),
                mtime.year(),
                mtime.month(),
                mtime.day(),
                file.display()
            )?;
        }
    }

    // compute CRC32 of each file and generate the SFV listing
    let mut success = true;
    for file in &files {
        match compute_crc32(file) {
            Ok(crc32) if cfg.print_basename => {
                let name = file.file_name().unwrap();
                writeln!(
                    cfg.stdout,
                    "{} {:08X}",
                    AsRef::<Path>::as_ref(&name).display(),
                    crc32
                )?
            }
            Ok(crc32) => writeln!(cfg.stdout, "{} {:08X}", file.display(), crc32)?,
            Err(err) => {
                success = false;
                writeln!(cfg.stderr, "cksfv: {}: {}", file.display(), err)?
            }
        }
    }

    // return `true` if all CRC32 where successfully computed
    Ok(success)
}

/// Check a SFV listing at the given location, optionally using `workdir`.
///
/// This function always writes some progress messages to `config.stderr`, and
/// outputs a message line for each file it checks to `config.stdout`.
pub fn cksfv<'a, F, C>(
    sfv: &Path,
    workdir: Option<&Path>,
    config: C,
    files: Option<F>,
) -> Result<bool, IoError>
where
    F: IntoIterator<Item = &'a Path>,
    C: Into<Option<Config>>,
{
    // get a default config if none provided.
    let mut cfg: Config = config.into().unwrap_or_default();

    // print the terminal "UI"
    let workdir = workdir.unwrap_or_else(|| Path::new("."));
    writeln!(
        cfg.stderr,
        "--( Verifying: {} ){}",
        sfv.display(),
        "-".repeat(63 - min(63, sfv.display().to_string().len()))
    )?;

    // open the SFV listing
    let listing = match File::open(sfv) {
        Ok(file) => BufReader::new(file),
        Err(err) => {
            writeln!(cfg.stderr, "cksfv: {}: {}", sfv.display(), err)?;
            return Ok(false);
        }
    };

    let mut success = true;
    let mut lines = listing.lines();
    if let Some(_files) = files {
        // only check the files given as arguments
        unimplemented!("TODO: checking with file arguments");
    } else {
        // check every line of the listing
        while let Some(Ok(line)) = lines.next() {
            if !line.starts_with(';') {
                // extract filename and CRC from listing
                let i = line.trim_end().rfind(' ').unwrap();
                let filename = Path::new(&line[..i]);
                let crc32_old = u32::from_str_radix(&line[i + 1..], 16).unwrap();
                // check the current CRC32 and compare against recorded one
                match compute_crc32(&workdir.join(filename)) {
                    Ok(crc32_new) if crc32_new != crc32_old => {
                        success = false;
                        if cfg.quiet {
                            writeln!(cfg.stdout, "{:<50}different CRC", filename.display())?;
                        } else {
                            writeln!(
                                cfg.stdout,
                                "cksfv: {}: Has a different CRC",
                                filename.display()
                            )?;
                        }
                    }
                    Err(err) if cfg.quiet => {
                        writeln!(cfg.stdout, "cksfv: {}: {}", filename.display(), err)?;
                    }
                    Err(err) => {
                        writeln!(cfg.stdout, "{:<50}{:<30}", filename.display(), err)?;
                        success = false
                    }
                    Ok(_) if !cfg.quiet => {
                        writeln!(cfg.stdout, "{:<50}OK", filename.display())?;
                    }
                    Ok(_) => (),
                }
            }
        }
    }

    // add result message
    writeln!(cfg.stderr, "{}", "-".repeat(80))?;
    if !cfg.quiet {
        if success {
            writeln!(cfg.stdout, "Everything OK")?;
        } else {
            writeln!(cfg.stdout, "Errors Occured")?;
        }
    }
    Ok(success)
}