cksfv/
lib.rs

1#[macro_use]
2extern crate clap;
3extern crate chrono;
4extern crate crc32fast;
5
6#[cfg(feature = "mmap")]
7extern crate memmap;
8
9use std::cmp::min;
10use std::fmt::Debug;
11use std::fs::File;
12use std::io::BufRead;
13use std::io::BufReader;
14use std::io::Error as IoError;
15use std::io::Read;
16use std::io::Write;
17use std::iter::IntoIterator;
18use std::path::Path;
19
20use chrono::DateTime;
21use chrono::Datelike;
22use chrono::Local;
23use chrono::Timelike;
24use crc32fast::Hasher;
25use getset::Getters;
26use getset::MutGetters;
27use getset::Setters;
28
29/// Use a 64k buffer size for better performance.
30const DEFAULT_BUFFER_SIZE: usize = 65536;
31
32/// The final value of a CRC32 checksum round.
33pub type Crc32 = u32;
34
35// ---------------------------------------------------------------------------
36
37/// Given a path to a file, attempt to compute its CRC32 hash.
38fn compute_crc32(file: &Path) -> Result<Crc32, IoError> {
39    // check the file is not a directory (File::open is fine opening
40    // a directory and will just read it as an empty file, but we want
41    // a hard error)
42    if file.is_dir() {
43        return Err(std::io::Error::from_raw_os_error(21));
44    }
45
46    // open the file and compute the hash
47    File::open(&file).and_then(compute_crc32_inner)
48}
49
50/// Compute a CRC32 from a file content using `mmap`.
51#[cfg(feature = "mmap")]
52fn compute_crc32_inner(mut file: File) -> Result<Crc32, IoError> {
53    let mut hasher = Hasher::new();
54    let mmap = unsafe { memmap::MmapOptions::new().map(&file)? };
55    hasher.update(&mmap[..]);
56    Ok(hasher.finalize())
57}
58
59/// Compute a CRC32 from a file content without using `mmap`.
60#[cfg(not(feature = "mmap"))]
61fn compute_crc32_inner(mut file: File) -> Result<Crc32, IoError> {
62    let mut hasher = Hasher::new();
63    let mut buffer = [0; DEFAULT_BUFFER_SIZE];
64    loop {
65        let n = file.read(&mut buffer)?;
66        if n == 0 {
67            break;
68        }
69        hasher.update(&buffer[..n]);
70    }
71    Ok(hasher.finalize())
72}
73
74// ---------------------------------------------------------------------------
75
76pub trait WriteDebug: Debug + Write {}
77
78impl<F: Debug + Write> WriteDebug for F {}
79
80#[derive(Debug)]
81pub enum Output {
82    Devnull,
83    Stdout(std::io::Stdout),
84    Stderr(std::io::Stderr),
85}
86
87impl Output {
88    pub fn devnull() -> Self {
89        Self::Devnull
90    }
91
92    pub fn stdout() -> Self {
93        Output::Stdout(std::io::stdout())
94    }
95
96    pub fn stderr() -> Self {
97        Output::Stderr(std::io::stderr())
98    }
99}
100
101impl Clone for Output {
102    fn clone(&self) -> Self {
103        use self::Output::*;
104        match self {
105            Devnull => Self::devnull(),
106            Stdout(_) => Self::stdout(),
107            Stderr(_) => Self::stderr(),
108        }
109    }
110}
111
112impl Default for Output {
113    fn default() -> Self {
114        Self::stderr()
115    }
116}
117
118impl Write for Output {
119    fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
120        use self::Output::*;
121        match self {
122            Devnull => Ok(buf.len()),
123            Stdout(out) => out.write(buf),
124            Stderr(err) => err.write(buf),
125        }
126    }
127
128    fn flush(&mut self) -> Result<(), IoError> {
129        use self::Output::*;
130        match self {
131            Devnull => Ok(()),
132            Stdout(out) => out.flush(),
133            Stderr(err) => err.flush(),
134        }
135    }
136}
137
138// ---------------------------------------------------------------------------
139
140#[derive(Clone, Debug, Getters, MutGetters, Setters)]
141pub struct Config {
142    #[get = "pub"]
143    #[get_mut = "pub"]
144    #[set = "pub"]
145    stdout: Output,
146    #[get = "pub"]
147    #[get_mut = "pub"]
148    #[set = "pub"]
149    stderr: Output,
150    #[get = "pub"]
151    #[get_mut = "pub"]
152    #[set = "pub"]
153    quiet: bool,
154    #[get = "pub"]
155    #[get_mut = "pub"]
156    #[set = "pub"]
157    print_basename: bool,
158    #[get = "pub"]
159    #[get_mut = "pub"]
160    #[set = "pub"]
161    ignore_case: bool,
162    #[get = "pub"]
163    #[get_mut = "pub"]
164    #[set = "pub"]
165    force_slashes: bool,
166}
167
168impl Default for Config {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl Config {
175    /// Create a new `Cksfv` instance with default arguments.
176    pub fn new() -> Self {
177        Config {
178            stdout: Output::stdout(),
179            stderr: Output::stderr(),
180            quiet: false,
181            print_basename: false,
182            ignore_case: false,
183            force_slashes: false,
184        }
185    }
186
187    pub fn with_stdout(mut self, stdout: Output) -> Self {
188        self.stdout = stdout;
189        self
190    }
191
192    pub fn with_stderr(mut self, stderr: Output) -> Self {
193        self.stderr = stderr;
194        self
195    }
196
197    pub fn with_print_basenamet(mut self, print_basename: bool) -> Self {
198        self.print_basename = print_basename;
199        self
200    }
201
202    /// Consume the configuration instance and get the `stdout` field.
203    pub fn extract_stdout(self) -> Output {
204        self.stdout
205    }
206}
207
208// ---------------------------------------------------------------------------
209
210/// Generate a new SFV listing from a list of files.
211///
212/// This function always writes the result to `config.stdout`, which defaults
213/// to `std::io::Stdout` if no configuration is provided.
214pub fn newsfv<'a, F, C>(files: F, config: C) -> Result<bool, IoError>
215where
216    F: IntoIterator<Item = &'a Path>,
217    C: Into<Option<Config>>,
218{
219    // get a default config if none provided.
220    let mut cfg: Config = config.into().unwrap_or_default();
221
222    // collect the files
223    let files: Vec<&Path> = files.into_iter().collect();
224
225    // generate the headers from the files that where found
226    let now: DateTime<Local> = Local::now();
227    writeln!(
228        cfg.stdout,
229        "; Generated by cksfv.rs v{} on {:04}-{:02}-{:02} at {:02}:{:02}.{:02}",
230        crate_version!(),
231        now.year(),
232        now.month(),
233        now.day(),
234        now.hour(),
235        now.minute(),
236        now.second(),
237    )?;
238    writeln!(
239        cfg.stdout,
240        "; Project web site: {}",
241        env!("CARGO_PKG_REPOSITORY")
242    )?;
243    writeln!(cfg.stdout, ";")?;
244    for file in files.iter().filter(|p| p.is_file()) {
245        if let Ok(metadata) = std::fs::metadata(file) {
246            let mtime: DateTime<Local> = From::from(metadata.modified().unwrap());
247            writeln!(
248                cfg.stdout,
249                "; {:>12}  {:02}:{:02}.{:02} {:04}-{:02}-{:02} {}",
250                metadata.len(),
251                mtime.hour(),
252                mtime.minute(),
253                mtime.second(),
254                mtime.year(),
255                mtime.month(),
256                mtime.day(),
257                file.display()
258            )?;
259        }
260    }
261
262    // compute CRC32 of each file and generate the SFV listing
263    let mut success = true;
264    for file in &files {
265        match compute_crc32(file) {
266            Ok(crc32) if cfg.print_basename => {
267                let name = file.file_name().unwrap();
268                writeln!(
269                    cfg.stdout,
270                    "{} {:08X}",
271                    AsRef::<Path>::as_ref(&name).display(),
272                    crc32
273                )?
274            }
275            Ok(crc32) => writeln!(cfg.stdout, "{} {:08X}", file.display(), crc32)?,
276            Err(err) => {
277                success = false;
278                writeln!(cfg.stderr, "cksfv: {}: {}", file.display(), err)?
279            }
280        }
281    }
282
283    // return `true` if all CRC32 where successfully computed
284    Ok(success)
285}
286
287/// Check a SFV listing at the given location, optionally using `workdir`.
288///
289/// This function always writes some progress messages to `config.stderr`, and
290/// outputs a message line for each file it checks to `config.stdout`.
291pub fn cksfv<'a, F, C>(
292    sfv: &Path,
293    workdir: Option<&Path>,
294    config: C,
295    files: Option<F>,
296) -> Result<bool, IoError>
297where
298    F: IntoIterator<Item = &'a Path>,
299    C: Into<Option<Config>>,
300{
301    // get a default config if none provided.
302    let mut cfg: Config = config.into().unwrap_or_default();
303
304    // print the terminal "UI"
305    let workdir = workdir.unwrap_or_else(|| Path::new("."));
306    writeln!(
307        cfg.stderr,
308        "--( Verifying: {} ){}",
309        sfv.display(),
310        "-".repeat(63 - min(63, sfv.display().to_string().len()))
311    )?;
312
313    // open the SFV listing
314    let listing = match File::open(sfv) {
315        Ok(file) => BufReader::new(file),
316        Err(err) => {
317            writeln!(cfg.stderr, "cksfv: {}: {}", sfv.display(), err)?;
318            return Ok(false);
319        }
320    };
321
322    let mut success = true;
323    let mut lines = listing.lines();
324    if let Some(_files) = files {
325        // only check the files given as arguments
326        unimplemented!("TODO: checking with file arguments");
327    } else {
328        // check every line of the listing
329        while let Some(Ok(line)) = lines.next() {
330            if !line.starts_with(';') {
331                // extract filename and CRC from listing
332                let i = line.trim_end().rfind(' ').unwrap();
333                let filename = Path::new(&line[..i]);
334                let crc32_old = u32::from_str_radix(&line[i + 1..], 16).unwrap();
335                // check the current CRC32 and compare against recorded one
336                match compute_crc32(&workdir.join(filename)) {
337                    Ok(crc32_new) if crc32_new != crc32_old => {
338                        success = false;
339                        if cfg.quiet {
340                            writeln!(cfg.stdout, "{:<50}different CRC", filename.display())?;
341                        } else {
342                            writeln!(
343                                cfg.stdout,
344                                "cksfv: {}: Has a different CRC",
345                                filename.display()
346                            )?;
347                        }
348                    }
349                    Err(err) if cfg.quiet => {
350                        writeln!(cfg.stdout, "cksfv: {}: {}", filename.display(), err)?;
351                    }
352                    Err(err) => {
353                        writeln!(cfg.stdout, "{:<50}{:<30}", filename.display(), err)?;
354                        success = false
355                    }
356                    Ok(_) if !cfg.quiet => {
357                        writeln!(cfg.stdout, "{:<50}OK", filename.display())?;
358                    }
359                    Ok(_) => (),
360                }
361            }
362        }
363    }
364
365    // add result message
366    writeln!(cfg.stderr, "{}", "-".repeat(80))?;
367    if !cfg.quiet {
368        if success {
369            writeln!(cfg.stdout, "Everything OK")?;
370        } else {
371            writeln!(cfg.stdout, "Errors Occured")?;
372        }
373    }
374    Ok(success)
375}