1use std::cell::RefCell;
2use std::fs::File;
3use std::io::{self, BufRead, Read, Write};
4use std::path::Path;
5
6#[cfg(target_os = "linux")]
7use std::sync::atomic::{AtomicBool, Ordering};
8
9use digest::Digest;
10use md5::Md5;
11use memmap2::MmapOptions;
12
13#[derive(Debug, Clone, Copy)]
15pub enum HashAlgorithm {
16 Sha256,
17 Md5,
18 Blake2b,
19}
20
21impl HashAlgorithm {
22 pub fn name(self) -> &'static str {
23 match self {
24 HashAlgorithm::Sha256 => "SHA256",
25 HashAlgorithm::Md5 => "MD5",
26 HashAlgorithm::Blake2b => "BLAKE2b",
27 }
28 }
29}
30
31fn hash_digest<D: Digest>(data: &[u8]) -> String {
34 hex_encode(&D::digest(data))
35}
36
37fn hash_reader_impl<D: Digest>(mut reader: impl Read) -> io::Result<String> {
42 STREAM_BUF.with(|cell| {
43 let mut buf = cell.borrow_mut();
44 let mut hasher = D::new();
45 loop {
46 let n = read_full(&mut reader, &mut buf)?;
47 if n == 0 {
48 break;
49 }
50 hasher.update(&buf[..n]);
51 }
52 Ok(hex_encode(&hasher.finalize()))
53 })
54}
55
56const HASH_READ_BUF: usize = 4 * 1024 * 1024;
63
64thread_local! {
67 static STREAM_BUF: RefCell<Vec<u8>> = RefCell::new(vec![0u8; HASH_READ_BUF]);
68}
69
70#[cfg(not(target_vendor = "apple"))]
74fn sha256_bytes(data: &[u8]) -> String {
75 hex_encode(ring::digest::digest(&ring::digest::SHA256, data).as_ref())
76}
77
78#[cfg(target_vendor = "apple")]
80fn sha256_bytes(data: &[u8]) -> String {
81 hash_digest::<sha2::Sha256>(data)
82}
83
84#[cfg(not(target_vendor = "apple"))]
86fn sha256_reader(mut reader: impl Read) -> io::Result<String> {
87 STREAM_BUF.with(|cell| {
88 let mut buf = cell.borrow_mut();
89 let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
90 loop {
91 let n = read_full(&mut reader, &mut buf)?;
92 if n == 0 {
93 break;
94 }
95 ctx.update(&buf[..n]);
96 }
97 Ok(hex_encode(ctx.finish().as_ref()))
98 })
99}
100
101#[cfg(target_vendor = "apple")]
103fn sha256_reader(reader: impl Read) -> io::Result<String> {
104 hash_reader_impl::<sha2::Sha256>(reader)
105}
106
107pub fn hash_bytes(algo: HashAlgorithm, data: &[u8]) -> String {
109 match algo {
110 HashAlgorithm::Sha256 => sha256_bytes(data),
111 HashAlgorithm::Md5 => hash_digest::<Md5>(data),
112 HashAlgorithm::Blake2b => {
113 let hash = blake2b_simd::blake2b(data);
114 hex_encode(hash.as_bytes())
115 }
116 }
117}
118
119pub fn hash_reader<R: Read>(algo: HashAlgorithm, reader: R) -> io::Result<String> {
121 match algo {
122 HashAlgorithm::Sha256 => sha256_reader(reader),
123 HashAlgorithm::Md5 => hash_reader_impl::<Md5>(reader),
124 HashAlgorithm::Blake2b => blake2b_hash_reader(reader, 64),
125 }
126}
127
128#[cfg(target_os = "linux")]
131static NOATIME_SUPPORTED: AtomicBool = AtomicBool::new(true);
132
133#[cfg(target_os = "linux")]
136fn open_noatime(path: &Path) -> io::Result<File> {
137 use std::os::unix::fs::OpenOptionsExt;
138 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
139 match std::fs::OpenOptions::new()
140 .read(true)
141 .custom_flags(libc::O_NOATIME)
142 .open(path)
143 {
144 Ok(f) => return Ok(f),
145 Err(ref e) if e.raw_os_error() == Some(libc::EPERM) => {
146 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
148 }
149 Err(e) => return Err(e), }
151 }
152 File::open(path)
153}
154
155#[cfg(not(target_os = "linux"))]
156fn open_noatime(path: &Path) -> io::Result<File> {
157 File::open(path)
158}
159
160#[cfg(target_os = "linux")]
165#[inline]
166fn mmap_advise(mmap: &memmap2::Mmap) {
167 unsafe {
168 let ptr = mmap.as_ptr() as *mut libc::c_void;
169 let len = mmap.len();
170 libc::madvise(ptr, len, libc::MADV_SEQUENTIAL);
171 libc::madvise(ptr, len, libc::MADV_HUGEPAGE);
172 }
173}
174
175#[cfg(not(target_os = "linux"))]
176#[inline]
177fn mmap_advise(_mmap: &memmap2::Mmap) {}
178
179pub fn hash_file(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
185 let file = open_noatime(path)?;
187 let metadata = file.metadata()?; let len = metadata.len();
189 let is_regular = metadata.file_type().is_file();
190
191 if is_regular && len == 0 {
192 return Ok(hash_bytes(algo, &[]));
193 }
194
195 if is_regular && len > 0 {
196 let mmap = unsafe { MmapOptions::new().populate().map(&file)? };
200 mmap_advise(&mmap);
201 return Ok(hash_bytes(algo, &mmap));
202 }
203
204 hash_reader(algo, file)
206}
207
208pub fn hash_stdin(algo: HashAlgorithm) -> io::Result<String> {
210 let stdin = io::stdin();
211 #[cfg(target_os = "linux")]
213 {
214 use std::os::unix::io::AsRawFd;
215 let fd = stdin.as_raw_fd();
216 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
217 if unsafe { libc::fstat(fd, &mut stat) } == 0
218 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
219 && stat.st_size > 0
220 {
221 unsafe {
222 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
223 }
224 }
225 }
226 hash_reader(algo, stdin.lock())
228}
229
230pub fn should_use_parallel(paths: &[&Path]) -> bool {
236 paths.len() >= 2
237}
238
239#[cfg(target_os = "linux")]
242pub fn readahead_files(paths: &[&Path]) {
243 use std::os::unix::io::AsRawFd;
244 for path in paths {
245 if let Ok(file) = open_noatime(path) {
246 if let Ok(meta) = file.metadata() {
247 let len = meta.len();
248 if meta.file_type().is_file() && len > 0 {
249 unsafe {
250 libc::posix_fadvise(
251 file.as_raw_fd(),
252 0,
253 len as i64,
254 libc::POSIX_FADV_WILLNEED,
255 );
256 }
257 }
258 }
259 }
260 }
261}
262
263#[cfg(not(target_os = "linux"))]
264pub fn readahead_files(_paths: &[&Path]) {
265 }
267
268pub fn blake2b_hash_data(data: &[u8], output_bytes: usize) -> String {
273 let hash = blake2b_simd::Params::new()
274 .hash_length(output_bytes)
275 .hash(data);
276 hex_encode(hash.as_bytes())
277}
278
279pub fn blake2b_hash_reader<R: Read>(mut reader: R, output_bytes: usize) -> io::Result<String> {
282 STREAM_BUF.with(|cell| {
283 let mut buf = cell.borrow_mut();
284 let mut state = blake2b_simd::Params::new()
285 .hash_length(output_bytes)
286 .to_state();
287 loop {
288 let n = read_full(&mut reader, &mut buf)?;
289 if n == 0 {
290 break;
291 }
292 state.update(&buf[..n]);
293 }
294 Ok(hex_encode(state.finalize().as_bytes()))
295 })
296}
297
298pub fn blake2b_hash_file(path: &Path, output_bytes: usize) -> io::Result<String> {
301 let file = open_noatime(path)?;
303 let metadata = file.metadata()?;
304 let len = metadata.len();
305 let is_regular = metadata.file_type().is_file();
306
307 if is_regular && len == 0 {
308 return Ok(blake2b_hash_data(&[], output_bytes));
309 }
310
311 if is_regular && len > 0 {
312 let mmap = unsafe { MmapOptions::new().populate().map(&file)? };
314 mmap_advise(&mmap);
315 return Ok(blake2b_hash_data(&mmap, output_bytes));
316 }
317
318 blake2b_hash_reader(file, output_bytes)
320}
321
322pub fn blake2b_hash_stdin(output_bytes: usize) -> io::Result<String> {
325 let stdin = io::stdin();
326 #[cfg(target_os = "linux")]
327 {
328 use std::os::unix::io::AsRawFd;
329 let fd = stdin.as_raw_fd();
330 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
331 if unsafe { libc::fstat(fd, &mut stat) } == 0
332 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
333 && stat.st_size > 0
334 {
335 unsafe {
336 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
337 }
338 }
339 }
340 blake2b_hash_reader(stdin.lock(), output_bytes)
341}
342
343pub fn print_hash(
345 out: &mut impl Write,
346 hash: &str,
347 filename: &str,
348 binary: bool,
349) -> io::Result<()> {
350 let mode_char = if binary { '*' } else { ' ' };
351 writeln!(out, "{} {}{}", hash, mode_char, filename)
352}
353
354pub fn print_hash_zero(
356 out: &mut impl Write,
357 hash: &str,
358 filename: &str,
359 binary: bool,
360) -> io::Result<()> {
361 let mode_char = if binary { '*' } else { ' ' };
362 write!(out, "{} {}{}\0", hash, mode_char, filename)
363}
364
365pub fn print_hash_tag(
367 out: &mut impl Write,
368 algo: HashAlgorithm,
369 hash: &str,
370 filename: &str,
371) -> io::Result<()> {
372 writeln!(out, "{} ({}) = {}", algo.name(), filename, hash)
373}
374
375pub fn print_hash_tag_zero(
377 out: &mut impl Write,
378 algo: HashAlgorithm,
379 hash: &str,
380 filename: &str,
381) -> io::Result<()> {
382 write!(out, "{} ({}) = {}\0", algo.name(), filename, hash)
383}
384
385pub fn print_hash_tag_b2sum(
389 out: &mut impl Write,
390 hash: &str,
391 filename: &str,
392 bits: usize,
393) -> io::Result<()> {
394 if bits == 512 {
395 writeln!(out, "BLAKE2b ({}) = {}", filename, hash)
396 } else {
397 writeln!(out, "BLAKE2b-{} ({}) = {}", bits, filename, hash)
398 }
399}
400
401pub fn print_hash_tag_b2sum_zero(
403 out: &mut impl Write,
404 hash: &str,
405 filename: &str,
406 bits: usize,
407) -> io::Result<()> {
408 if bits == 512 {
409 write!(out, "BLAKE2b ({}) = {}\0", filename, hash)
410 } else {
411 write!(out, "BLAKE2b-{} ({}) = {}\0", bits, filename, hash)
412 }
413}
414
415pub struct CheckOptions {
417 pub quiet: bool,
418 pub status_only: bool,
419 pub strict: bool,
420 pub warn: bool,
421 pub ignore_missing: bool,
422 pub warn_prefix: String,
426}
427
428pub struct CheckResult {
430 pub ok: usize,
431 pub mismatches: usize,
432 pub format_errors: usize,
433 pub read_errors: usize,
434 pub ignored_missing: usize,
436}
437
438pub fn check_file<R: BufRead>(
441 algo: HashAlgorithm,
442 reader: R,
443 opts: &CheckOptions,
444 out: &mut impl Write,
445 err_out: &mut impl Write,
446) -> io::Result<CheckResult> {
447 let quiet = opts.quiet;
448 let status_only = opts.status_only;
449 let warn = opts.warn;
450 let ignore_missing = opts.ignore_missing;
451 let mut ok_count = 0;
452 let mut mismatch_count = 0;
453 let mut format_errors = 0;
454 let mut read_errors = 0;
455 let mut ignored_missing_count = 0;
456 let mut line_num = 0;
457
458 for line_result in reader.lines() {
459 line_num += 1;
460 let line = line_result?;
461 let line = line.trim_end();
462
463 if line.is_empty() {
464 continue;
465 }
466
467 let (expected_hash, filename) = match parse_check_line(line) {
469 Some(v) => v,
470 None => {
471 format_errors += 1;
472 if warn {
473 out.flush()?;
474 if opts.warn_prefix.is_empty() {
475 writeln!(
476 err_out,
477 "line {}: improperly formatted {} checksum line",
478 line_num,
479 algo.name()
480 )?;
481 } else {
482 writeln!(
483 err_out,
484 "{}: {}: improperly formatted {} checksum line",
485 opts.warn_prefix,
486 line_num,
487 algo.name()
488 )?;
489 }
490 }
491 continue;
492 }
493 };
494
495 let actual = match hash_file(algo, Path::new(filename)) {
497 Ok(h) => h,
498 Err(e) => {
499 if ignore_missing && e.kind() == io::ErrorKind::NotFound {
500 ignored_missing_count += 1;
501 continue;
502 }
503 read_errors += 1;
504 if !status_only {
505 out.flush()?;
506 writeln!(err_out, "{}: {}", filename, e)?;
507 writeln!(out, "{}: FAILED open or read", filename)?;
508 }
509 continue;
510 }
511 };
512
513 if actual.eq_ignore_ascii_case(expected_hash) {
514 ok_count += 1;
515 if !quiet && !status_only {
516 writeln!(out, "{}: OK", filename)?;
517 }
518 } else {
519 mismatch_count += 1;
520 if !status_only {
521 writeln!(out, "{}: FAILED", filename)?;
522 }
523 }
524 }
525
526 Ok(CheckResult {
527 ok: ok_count,
528 mismatches: mismatch_count,
529 format_errors,
530 read_errors,
531 ignored_missing: ignored_missing_count,
532 })
533}
534
535pub fn parse_check_line(line: &str) -> Option<(&str, &str)> {
537 let rest = line
539 .strip_prefix("MD5 (")
540 .or_else(|| line.strip_prefix("SHA256 ("))
541 .or_else(|| line.strip_prefix("BLAKE2b ("))
542 .or_else(|| {
543 if line.starts_with("BLAKE2b-") {
545 let after = &line["BLAKE2b-".len()..];
546 if let Some(sp) = after.find(" (") {
547 if after[..sp].bytes().all(|b| b.is_ascii_digit()) {
548 return Some(&after[sp + 2..]);
549 }
550 }
551 }
552 None
553 });
554 if let Some(rest) = rest {
555 if let Some(paren_idx) = rest.find(") = ") {
556 let filename = &rest[..paren_idx];
557 let hash = &rest[paren_idx + 4..];
558 return Some((hash, filename));
559 }
560 }
561
562 let line = line.strip_prefix('\\').unwrap_or(line);
564
565 if let Some(idx) = line.find(" ") {
567 let hash = &line[..idx];
568 let rest = &line[idx + 2..];
569 return Some((hash, rest));
570 }
571 if let Some(idx) = line.find(" *") {
573 let hash = &line[..idx];
574 let rest = &line[idx + 2..];
575 return Some((hash, rest));
576 }
577 None
578}
579
580pub fn parse_check_line_tag(line: &str) -> Option<(&str, &str, Option<usize>)> {
584 let paren_start = line.find(" (")?;
585 let algo_part = &line[..paren_start];
586 let rest = &line[paren_start + 2..];
587 let paren_end = rest.find(") = ")?;
588 let filename = &rest[..paren_end];
589 let hash = &rest[paren_end + 4..];
590
591 let bits = if let Some(dash_pos) = algo_part.rfind('-') {
593 algo_part[dash_pos + 1..].parse::<usize>().ok()
594 } else {
595 None
596 };
597
598 Some((hash, filename, bits))
599}
600
601#[inline]
604fn read_full(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<usize> {
605 let mut total = 0;
606 while total < buf.len() {
607 match reader.read(&mut buf[total..]) {
608 Ok(0) => break,
609 Ok(n) => total += n,
610 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
611 Err(e) => return Err(e),
612 }
613 }
614 Ok(total)
615}
616
617const fn generate_hex_table() -> [[u8; 2]; 256] {
620 let hex = b"0123456789abcdef";
621 let mut table = [[0u8; 2]; 256];
622 let mut i = 0;
623 while i < 256 {
624 table[i] = [hex[i >> 4], hex[i & 0xf]];
625 i += 1;
626 }
627 table
628}
629
630const HEX_TABLE: [[u8; 2]; 256] = generate_hex_table();
631
632pub(crate) fn hex_encode(bytes: &[u8]) -> String {
635 let len = bytes.len() * 2;
636 let mut hex = String::with_capacity(len);
637 unsafe {
639 let buf = hex.as_mut_vec();
640 buf.set_len(len);
641 let ptr = buf.as_mut_ptr();
642 for (i, &b) in bytes.iter().enumerate() {
643 let pair = *HEX_TABLE.get_unchecked(b as usize);
644 *ptr.add(i * 2) = pair[0];
645 *ptr.add(i * 2 + 1) = pair[1];
646 }
647 }
648 hex
649}