1use std::cell::RefCell;
2use std::fs::{self, File};
3use std::io::{self, BufRead, BufReader, Read, Write};
4use std::path::Path;
5
6#[cfg(target_os = "linux")]
7use std::sync::atomic::{AtomicBool, Ordering};
8
9use md5::Md5;
10use memmap2::MmapOptions;
11use sha2::{Digest, Sha256};
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> {
38 let mut hasher = D::new();
39 let mut buf = vec![0u8; 16 * 1024 * 1024]; loop {
41 let n = reader.read(&mut buf)?;
42 if n == 0 {
43 break;
44 }
45 hasher.update(&buf[..n]);
46 }
47 Ok(hex_encode(&hasher.finalize()))
48}
49
50pub fn hash_bytes(algo: HashAlgorithm, data: &[u8]) -> String {
54 match algo {
55 HashAlgorithm::Sha256 => hash_digest::<Sha256>(data),
56 HashAlgorithm::Md5 => hash_digest::<Md5>(data),
57 HashAlgorithm::Blake2b => {
58 let hash = blake2b_simd::blake2b(data);
59 hex_encode(hash.as_bytes())
60 }
61 }
62}
63
64pub fn hash_reader<R: Read>(algo: HashAlgorithm, reader: R) -> io::Result<String> {
66 match algo {
67 HashAlgorithm::Sha256 => hash_reader_impl::<Sha256>(reader),
68 HashAlgorithm::Md5 => hash_reader_impl::<Md5>(reader),
69 HashAlgorithm::Blake2b => blake2b_hash_reader(reader, 64),
70 }
71}
72
73const MMAP_THRESHOLD: u64 = 256 * 1024; thread_local! {
81 static READ_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(MMAP_THRESHOLD as usize));
82}
83
84#[cfg(target_os = "linux")]
87static NOATIME_SUPPORTED: AtomicBool = AtomicBool::new(true);
88
89#[cfg(target_os = "linux")]
92fn open_noatime(path: &Path) -> io::Result<File> {
93 use std::os::unix::fs::OpenOptionsExt;
94 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
95 match fs::OpenOptions::new()
96 .read(true)
97 .custom_flags(libc::O_NOATIME)
98 .open(path)
99 {
100 Ok(f) => return Ok(f),
101 Err(ref e) if e.raw_os_error() == Some(libc::EPERM) => {
102 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
104 }
105 Err(e) => return Err(e), }
107 }
108 File::open(path)
109}
110
111#[cfg(not(target_os = "linux"))]
112fn open_noatime(path: &Path) -> io::Result<File> {
113 File::open(path)
114}
115
116pub fn hash_file(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
119 let file = open_noatime(path)?;
121 let metadata = file.metadata()?; let len = metadata.len();
123 let is_regular = metadata.file_type().is_file();
124
125 if is_regular && len == 0 {
126 return Ok(hash_bytes(algo, &[]));
127 }
128
129 if is_regular && len > 0 {
130 if len < MMAP_THRESHOLD {
132 return READ_BUF.with(|cell| {
133 let mut buf = cell.borrow_mut();
134 buf.clear();
135 buf.reserve(len as usize);
137 Read::read_to_end(&mut &file, &mut buf)?;
138 Ok(hash_bytes(algo, &buf))
139 });
140 }
141
142 return mmap_and_hash(algo, &file);
144 }
145
146 let reader = BufReader::with_capacity(16 * 1024 * 1024, file);
148 hash_reader(algo, reader)
149}
150
151fn mmap_and_hash(algo: HashAlgorithm, file: &File) -> io::Result<String> {
153 match unsafe {
154 MmapOptions::new()
155 .populate() .map(file)
157 } {
158 Ok(mmap) => {
159 #[cfg(target_os = "linux")]
160 {
161 let _ = mmap.advise(memmap2::Advice::Sequential);
162 if mmap.len() >= 2 * 1024 * 1024 {
163 unsafe {
164 libc::madvise(
165 mmap.as_ptr() as *mut libc::c_void,
166 mmap.len(),
167 libc::MADV_HUGEPAGE,
168 );
169 }
170 }
171 }
172 Ok(hash_bytes(algo, &mmap))
173 }
174 Err(_) => {
175 let reader = BufReader::with_capacity(16 * 1024 * 1024, file);
177 hash_reader(algo, reader)
178 }
179 }
180}
181
182fn mmap_and_hash_blake2b(file: &File, output_bytes: usize) -> io::Result<String> {
184 match unsafe { MmapOptions::new().populate().map(file) } {
185 Ok(mmap) => {
186 #[cfg(target_os = "linux")]
187 {
188 let _ = mmap.advise(memmap2::Advice::Sequential);
189 if mmap.len() >= 2 * 1024 * 1024 {
190 unsafe {
191 libc::madvise(
192 mmap.as_ptr() as *mut libc::c_void,
193 mmap.len(),
194 libc::MADV_HUGEPAGE,
195 );
196 }
197 }
198 }
199 Ok(blake2b_hash_data(&mmap, output_bytes))
200 }
201 Err(_) => {
202 let reader = BufReader::with_capacity(16 * 1024 * 1024, file);
203 blake2b_hash_reader(reader, output_bytes)
204 }
205 }
206}
207
208pub fn hash_stdin(algo: HashAlgorithm) -> io::Result<String> {
210 #[cfg(unix)]
212 {
213 use std::os::unix::io::AsRawFd;
214 let stdin = io::stdin();
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 use std::os::unix::io::FromRawFd;
222 let file = unsafe { File::from_raw_fd(fd) };
223 let result = unsafe { MmapOptions::new().populate().map(&file) };
224 std::mem::forget(file); if let Ok(mmap) = result {
226 #[cfg(target_os = "linux")]
227 {
228 let _ = mmap.advise(memmap2::Advice::Sequential);
229 }
230 return Ok(hash_bytes(algo, &mmap));
231 }
232 }
233 }
234 let mut data = Vec::new();
236 io::stdin().lock().read_to_end(&mut data)?;
237 Ok(hash_bytes(algo, &data))
238}
239
240const PARALLEL_THRESHOLD: u64 = 8 * 1024 * 1024; pub fn estimate_total_size(paths: &[&Path]) -> u64 {
248 if paths.is_empty() {
249 return 0;
250 }
251 if let Ok(meta) = fs::metadata(paths[0]) {
253 meta.len().saturating_mul(paths.len() as u64)
254 } else {
255 0
256 }
257}
258
259pub fn should_use_parallel(paths: &[&Path]) -> bool {
261 if paths.len() < 2 {
262 return false;
263 }
264 estimate_total_size(paths) >= PARALLEL_THRESHOLD
265}
266
267#[cfg(target_os = "linux")]
270pub fn readahead_files(paths: &[&Path]) {
271 use std::os::unix::io::AsRawFd;
272 for path in paths {
273 if let Ok(file) = open_noatime(path) {
274 if let Ok(meta) = file.metadata() {
275 let len = meta.len();
276 if meta.file_type().is_file() && len > 0 {
277 unsafe {
278 libc::posix_fadvise(
279 file.as_raw_fd(),
280 0,
281 len as i64,
282 libc::POSIX_FADV_WILLNEED,
283 );
284 }
285 }
286 }
287 }
288 }
289}
290
291#[cfg(not(target_os = "linux"))]
292pub fn readahead_files(_paths: &[&Path]) {
293 }
295
296pub fn blake2b_hash_data(data: &[u8], output_bytes: usize) -> String {
301 let hash = blake2b_simd::Params::new()
302 .hash_length(output_bytes)
303 .hash(data);
304 hex_encode(hash.as_bytes())
305}
306
307pub fn blake2b_hash_reader<R: Read>(mut reader: R, output_bytes: usize) -> io::Result<String> {
309 let mut state = blake2b_simd::Params::new()
310 .hash_length(output_bytes)
311 .to_state();
312 let mut buf = vec![0u8; 16 * 1024 * 1024]; loop {
314 let n = reader.read(&mut buf)?;
315 if n == 0 {
316 break;
317 }
318 state.update(&buf[..n]);
319 }
320 Ok(hex_encode(state.finalize().as_bytes()))
321}
322
323pub fn blake2b_hash_file(path: &Path, output_bytes: usize) -> io::Result<String> {
326 let file = open_noatime(path)?;
328 let metadata = file.metadata()?;
329 let len = metadata.len();
330 let is_regular = metadata.file_type().is_file();
331
332 if is_regular && len == 0 {
333 return Ok(blake2b_hash_data(&[], output_bytes));
334 }
335
336 if is_regular && len > 0 {
337 if len < MMAP_THRESHOLD {
339 return READ_BUF.with(|cell| {
340 let mut buf = cell.borrow_mut();
341 buf.clear();
342 buf.reserve(len as usize);
343 Read::read_to_end(&mut &file, &mut buf)?;
344 Ok(blake2b_hash_data(&buf, output_bytes))
345 });
346 }
347
348 return mmap_and_hash_blake2b(&file, output_bytes);
350 }
351
352 let reader = BufReader::with_capacity(16 * 1024 * 1024, file);
354 blake2b_hash_reader(reader, output_bytes)
355}
356
357pub fn blake2b_hash_stdin(output_bytes: usize) -> io::Result<String> {
360 #[cfg(unix)]
362 {
363 use std::os::unix::io::AsRawFd;
364 let stdin = io::stdin();
365 let fd = stdin.as_raw_fd();
366 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
367 if unsafe { libc::fstat(fd, &mut stat) } == 0
368 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
369 && stat.st_size > 0
370 {
371 use std::os::unix::io::FromRawFd;
372 let file = unsafe { File::from_raw_fd(fd) };
373 let result = unsafe { MmapOptions::new().populate().map(&file) };
374 std::mem::forget(file); if let Ok(mmap) = result {
376 #[cfg(target_os = "linux")]
377 {
378 let _ = mmap.advise(memmap2::Advice::Sequential);
379 }
380 return Ok(blake2b_hash_data(&mmap, output_bytes));
381 }
382 }
383 }
384 let mut data = Vec::new();
386 io::stdin().lock().read_to_end(&mut data)?;
387 Ok(blake2b_hash_data(&data, output_bytes))
388}
389
390pub fn print_hash(
392 out: &mut impl Write,
393 hash: &str,
394 filename: &str,
395 binary: bool,
396) -> io::Result<()> {
397 let mode_char = if binary { '*' } else { ' ' };
398 writeln!(out, "{} {}{}", hash, mode_char, filename)
399}
400
401pub fn print_hash_zero(
403 out: &mut impl Write,
404 hash: &str,
405 filename: &str,
406 binary: bool,
407) -> io::Result<()> {
408 let mode_char = if binary { '*' } else { ' ' };
409 write!(out, "{} {}{}\0", hash, mode_char, filename)
410}
411
412pub fn print_hash_tag(
414 out: &mut impl Write,
415 algo: HashAlgorithm,
416 hash: &str,
417 filename: &str,
418) -> io::Result<()> {
419 writeln!(out, "{} ({}) = {}", algo.name(), filename, hash)
420}
421
422pub fn print_hash_tag_zero(
424 out: &mut impl Write,
425 algo: HashAlgorithm,
426 hash: &str,
427 filename: &str,
428) -> io::Result<()> {
429 write!(out, "{} ({}) = {}\0", algo.name(), filename, hash)
430}
431
432pub fn print_hash_tag_b2sum(
436 out: &mut impl Write,
437 hash: &str,
438 filename: &str,
439 bits: usize,
440) -> io::Result<()> {
441 if bits == 512 {
442 writeln!(out, "BLAKE2b ({}) = {}", filename, hash)
443 } else {
444 writeln!(out, "BLAKE2b-{} ({}) = {}", bits, filename, hash)
445 }
446}
447
448pub fn print_hash_tag_b2sum_zero(
450 out: &mut impl Write,
451 hash: &str,
452 filename: &str,
453 bits: usize,
454) -> io::Result<()> {
455 if bits == 512 {
456 write!(out, "BLAKE2b ({}) = {}\0", filename, hash)
457 } else {
458 write!(out, "BLAKE2b-{} ({}) = {}\0", bits, filename, hash)
459 }
460}
461
462pub struct CheckOptions {
464 pub quiet: bool,
465 pub status_only: bool,
466 pub strict: bool,
467 pub warn: bool,
468 pub ignore_missing: bool,
469 pub warn_prefix: String,
473}
474
475pub struct CheckResult {
477 pub ok: usize,
478 pub mismatches: usize,
479 pub format_errors: usize,
480 pub read_errors: usize,
481 pub ignored_missing: usize,
483}
484
485pub fn check_file<R: BufRead>(
488 algo: HashAlgorithm,
489 reader: R,
490 opts: &CheckOptions,
491 out: &mut impl Write,
492 err_out: &mut impl Write,
493) -> io::Result<CheckResult> {
494 let quiet = opts.quiet;
495 let status_only = opts.status_only;
496 let warn = opts.warn;
497 let ignore_missing = opts.ignore_missing;
498 let mut ok_count = 0;
499 let mut mismatch_count = 0;
500 let mut format_errors = 0;
501 let mut read_errors = 0;
502 let mut ignored_missing_count = 0;
503 let mut line_num = 0;
504
505 for line_result in reader.lines() {
506 line_num += 1;
507 let line = line_result?;
508 let line = line.trim_end();
509
510 if line.is_empty() {
511 continue;
512 }
513
514 let (expected_hash, filename) = match parse_check_line(line) {
516 Some(v) => v,
517 None => {
518 format_errors += 1;
519 if warn {
520 out.flush()?;
521 if opts.warn_prefix.is_empty() {
522 writeln!(
523 err_out,
524 "line {}: improperly formatted {} checksum line",
525 line_num,
526 algo.name()
527 )?;
528 } else {
529 writeln!(
530 err_out,
531 "{}: {}: improperly formatted {} checksum line",
532 opts.warn_prefix,
533 line_num,
534 algo.name()
535 )?;
536 }
537 }
538 continue;
539 }
540 };
541
542 let actual = match hash_file(algo, Path::new(filename)) {
544 Ok(h) => h,
545 Err(e) => {
546 if ignore_missing && e.kind() == io::ErrorKind::NotFound {
547 ignored_missing_count += 1;
548 continue;
549 }
550 read_errors += 1;
551 if !status_only {
552 out.flush()?;
553 writeln!(err_out, "{}: {}", filename, e)?;
554 writeln!(out, "{}: FAILED open or read", filename)?;
555 }
556 continue;
557 }
558 };
559
560 if actual.eq_ignore_ascii_case(expected_hash) {
561 ok_count += 1;
562 if !quiet && !status_only {
563 writeln!(out, "{}: OK", filename)?;
564 }
565 } else {
566 mismatch_count += 1;
567 if !status_only {
568 writeln!(out, "{}: FAILED", filename)?;
569 }
570 }
571 }
572
573 Ok(CheckResult {
574 ok: ok_count,
575 mismatches: mismatch_count,
576 format_errors,
577 read_errors,
578 ignored_missing: ignored_missing_count,
579 })
580}
581
582pub fn parse_check_line(line: &str) -> Option<(&str, &str)> {
584 let rest = line
586 .strip_prefix("MD5 (")
587 .or_else(|| line.strip_prefix("SHA256 ("))
588 .or_else(|| line.strip_prefix("BLAKE2b ("))
589 .or_else(|| {
590 if line.starts_with("BLAKE2b-") {
592 let after = &line["BLAKE2b-".len()..];
593 if let Some(sp) = after.find(" (") {
594 if after[..sp].bytes().all(|b| b.is_ascii_digit()) {
595 return Some(&after[sp + 2..]);
596 }
597 }
598 }
599 None
600 });
601 if let Some(rest) = rest {
602 if let Some(paren_idx) = rest.find(") = ") {
603 let filename = &rest[..paren_idx];
604 let hash = &rest[paren_idx + 4..];
605 return Some((hash, filename));
606 }
607 }
608
609 let line = line.strip_prefix('\\').unwrap_or(line);
611
612 if let Some(idx) = line.find(" ") {
614 let hash = &line[..idx];
615 let rest = &line[idx + 2..];
616 return Some((hash, rest));
617 }
618 if let Some(idx) = line.find(" *") {
620 let hash = &line[..idx];
621 let rest = &line[idx + 2..];
622 return Some((hash, rest));
623 }
624 None
625}
626
627pub fn parse_check_line_tag(line: &str) -> Option<(&str, &str, Option<usize>)> {
631 let paren_start = line.find(" (")?;
632 let algo_part = &line[..paren_start];
633 let rest = &line[paren_start + 2..];
634 let paren_end = rest.find(") = ")?;
635 let filename = &rest[..paren_end];
636 let hash = &rest[paren_end + 4..];
637
638 let bits = if let Some(dash_pos) = algo_part.rfind('-') {
640 algo_part[dash_pos + 1..].parse::<usize>().ok()
641 } else {
642 None
643 };
644
645 Some((hash, filename, bits))
646}
647
648const fn generate_hex_table() -> [[u8; 2]; 256] {
651 let hex = b"0123456789abcdef";
652 let mut table = [[0u8; 2]; 256];
653 let mut i = 0;
654 while i < 256 {
655 table[i] = [hex[i >> 4], hex[i & 0xf]];
656 i += 1;
657 }
658 table
659}
660
661const HEX_TABLE: [[u8; 2]; 256] = generate_hex_table();
662
663pub(crate) fn hex_encode(bytes: &[u8]) -> String {
666 let len = bytes.len() * 2;
667 let mut hex = String::with_capacity(len);
668 unsafe {
670 let buf = hex.as_mut_vec();
671 buf.set_len(len);
672 let ptr = buf.as_mut_ptr();
673 for (i, &b) in bytes.iter().enumerate() {
674 let pair = *HEX_TABLE.get_unchecked(b as usize);
675 *ptr.add(i * 2) = pair[0];
676 *ptr.add(i * 2 + 1) = pair[1];
677 }
678 }
679 hex
680}