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
29const DEFAULT_BUFFER_SIZE: usize = 65536;
31
32pub type Crc32 = u32;
34
35fn compute_crc32(file: &Path) -> Result<Crc32, IoError> {
39 if file.is_dir() {
43 return Err(std::io::Error::from_raw_os_error(21));
44 }
45
46 File::open(&file).and_then(compute_crc32_inner)
48}
49
50#[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#[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
74pub 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#[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 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 pub fn extract_stdout(self) -> Output {
204 self.stdout
205 }
206}
207
208pub 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 let mut cfg: Config = config.into().unwrap_or_default();
221
222 let files: Vec<&Path> = files.into_iter().collect();
224
225 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 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 Ok(success)
285}
286
287pub 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 let mut cfg: Config = config.into().unwrap_or_default();
303
304 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 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 unimplemented!("TODO: checking with file arguments");
327 } else {
328 while let Some(Ok(line)) = lines.next() {
330 if !line.starts_with(';') {
331 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 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 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}