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
9#[cfg(not(target_os = "linux"))]
10use digest::Digest;
11#[cfg(not(target_os = "linux"))]
12use md5::Md5;
13
14#[derive(Debug, Clone, Copy)]
16pub enum HashAlgorithm {
17 Sha256,
18 Md5,
19 Blake2b,
20}
21
22impl HashAlgorithm {
23 pub fn name(self) -> &'static str {
24 match self {
25 HashAlgorithm::Sha256 => "SHA256",
26 HashAlgorithm::Md5 => "MD5",
27 HashAlgorithm::Blake2b => "BLAKE2b",
28 }
29 }
30}
31
32#[cfg(not(target_os = "linux"))]
36fn hash_digest<D: Digest>(data: &[u8]) -> String {
37 hex_encode(&D::digest(data))
38}
39
40#[cfg(not(target_os = "linux"))]
42fn hash_reader_impl<D: Digest>(mut reader: impl Read) -> io::Result<String> {
43 STREAM_BUF.with(|cell| {
44 let mut buf = cell.borrow_mut();
45 let mut hasher = D::new();
46 loop {
47 let n = read_full(&mut reader, &mut buf)?;
48 if n == 0 {
49 break;
50 }
51 hasher.update(&buf[..n]);
52 }
53 Ok(hex_encode(&hasher.finalize()))
54 })
55}
56
57const HASH_READ_BUF: usize = 8 * 1024 * 1024;
63
64thread_local! {
67 static STREAM_BUF: RefCell<Vec<u8>> = RefCell::new(vec![0u8; HASH_READ_BUF]);
68}
69
70#[cfg(target_os = "linux")]
75fn sha256_bytes(data: &[u8]) -> String {
76 let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data)
77 .expect("SHA256 hash failed");
78 hex_encode(&digest)
79}
80
81#[cfg(all(not(target_vendor = "apple"), not(target_os = "linux")))]
83fn sha256_bytes(data: &[u8]) -> String {
84 hex_encode(ring::digest::digest(&ring::digest::SHA256, data).as_ref())
85}
86
87#[cfg(target_vendor = "apple")]
89fn sha256_bytes(data: &[u8]) -> String {
90 hash_digest::<sha2::Sha256>(data)
91}
92
93#[cfg(target_os = "linux")]
96fn sha256_reader(mut reader: impl Read) -> io::Result<String> {
97 STREAM_BUF.with(|cell| {
98 let mut buf = cell.borrow_mut();
99 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
100 .map_err(|e| io::Error::other(e))?;
101 loop {
102 let n = read_full(&mut reader, &mut buf)?;
103 if n == 0 {
104 break;
105 }
106 hasher.update(&buf[..n]).map_err(|e| io::Error::other(e))?;
107 }
108 let digest = hasher.finish().map_err(|e| io::Error::other(e))?;
109 Ok(hex_encode(&digest))
110 })
111}
112
113#[cfg(all(not(target_vendor = "apple"), not(target_os = "linux")))]
115fn sha256_reader(mut reader: impl Read) -> io::Result<String> {
116 STREAM_BUF.with(|cell| {
117 let mut buf = cell.borrow_mut();
118 let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
119 loop {
120 let n = read_full(&mut reader, &mut buf)?;
121 if n == 0 {
122 break;
123 }
124 ctx.update(&buf[..n]);
125 }
126 Ok(hex_encode(ctx.finish().as_ref()))
127 })
128}
129
130#[cfg(target_vendor = "apple")]
132fn sha256_reader(reader: impl Read) -> io::Result<String> {
133 hash_reader_impl::<sha2::Sha256>(reader)
134}
135
136pub fn hash_bytes(algo: HashAlgorithm, data: &[u8]) -> String {
138 match algo {
139 HashAlgorithm::Sha256 => sha256_bytes(data),
140 HashAlgorithm::Md5 => md5_bytes(data),
141 HashAlgorithm::Blake2b => {
142 let hash = blake2b_simd::blake2b(data);
143 hex_encode(hash.as_bytes())
144 }
145 }
146}
147
148#[cfg(target_os = "linux")]
152fn md5_bytes(data: &[u8]) -> String {
153 let digest =
154 openssl::hash::hash(openssl::hash::MessageDigest::md5(), data).expect("MD5 hash failed");
155 hex_encode(&digest)
156}
157
158#[cfg(not(target_os = "linux"))]
160fn md5_bytes(data: &[u8]) -> String {
161 hash_digest::<Md5>(data)
162}
163
164pub fn hash_reader<R: Read>(algo: HashAlgorithm, reader: R) -> io::Result<String> {
166 match algo {
167 HashAlgorithm::Sha256 => sha256_reader(reader),
168 HashAlgorithm::Md5 => md5_reader(reader),
169 HashAlgorithm::Blake2b => blake2b_hash_reader(reader, 64),
170 }
171}
172
173#[cfg(target_os = "linux")]
175fn md5_reader(mut reader: impl Read) -> io::Result<String> {
176 STREAM_BUF.with(|cell| {
177 let mut buf = cell.borrow_mut();
178 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::md5())
179 .map_err(|e| io::Error::other(e))?;
180 loop {
181 let n = read_full(&mut reader, &mut buf)?;
182 if n == 0 {
183 break;
184 }
185 hasher.update(&buf[..n]).map_err(|e| io::Error::other(e))?;
186 }
187 let digest = hasher.finish().map_err(|e| io::Error::other(e))?;
188 Ok(hex_encode(&digest))
189 })
190}
191
192#[cfg(not(target_os = "linux"))]
194fn md5_reader(reader: impl Read) -> io::Result<String> {
195 hash_reader_impl::<Md5>(reader)
196}
197
198#[cfg(target_os = "linux")]
201static NOATIME_SUPPORTED: AtomicBool = AtomicBool::new(true);
202
203#[cfg(target_os = "linux")]
206fn open_noatime(path: &Path) -> io::Result<File> {
207 use std::os::unix::fs::OpenOptionsExt;
208 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
209 match std::fs::OpenOptions::new()
210 .read(true)
211 .custom_flags(libc::O_NOATIME)
212 .open(path)
213 {
214 Ok(f) => return Ok(f),
215 Err(ref e) if e.raw_os_error() == Some(libc::EPERM) => {
216 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
218 }
219 Err(e) => return Err(e), }
221 }
222 File::open(path)
223}
224
225#[cfg(not(target_os = "linux"))]
226fn open_noatime(path: &Path) -> io::Result<File> {
227 File::open(path)
228}
229
230#[cfg(target_os = "linux")]
233const FADVISE_MIN_SIZE: u64 = 1024 * 1024;
234
235const SMALL_FILE_LIMIT: u64 = 1024 * 1024;
239
240thread_local! {
243 static SMALL_FILE_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(64 * 1024));
244}
245
246pub fn hash_file(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
249 let file = open_noatime(path)?;
250 let metadata = file.metadata()?;
251 let file_size = metadata.len();
252
253 if metadata.file_type().is_file() && file_size == 0 {
254 return Ok(hash_bytes(algo, &[]));
255 }
256
257 if file_size > 0 && metadata.file_type().is_file() {
258 if file_size >= SMALL_FILE_LIMIT {
260 #[cfg(target_os = "linux")]
261 if file_size >= FADVISE_MIN_SIZE {
262 use std::os::unix::io::AsRawFd;
263 unsafe {
264 libc::posix_fadvise(
265 file.as_raw_fd(),
266 0,
267 file_size as i64,
268 libc::POSIX_FADV_SEQUENTIAL,
269 );
270 }
271 }
272 if let Ok(mmap) = unsafe { memmap2::MmapOptions::new().populate().map(&file) } {
273 #[cfg(target_os = "linux")]
274 {
275 let _ = mmap.advise(memmap2::Advice::Sequential);
276 if file_size >= 2 * 1024 * 1024 {
277 let _ = mmap.advise(memmap2::Advice::HugePage);
278 }
279 }
280 return Ok(hash_bytes(algo, &mmap));
281 }
282 }
283 if file_size < SMALL_FILE_LIMIT {
287 return hash_file_small(algo, file, file_size as usize);
288 }
289 }
290
291 #[cfg(target_os = "linux")]
293 if file_size >= FADVISE_MIN_SIZE {
294 use std::os::unix::io::AsRawFd;
295 unsafe {
296 libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
297 }
298 }
299 hash_reader(algo, file)
300}
301
302#[inline]
305fn hash_file_small(algo: HashAlgorithm, mut file: File, size: usize) -> io::Result<String> {
306 SMALL_FILE_BUF.with(|cell| {
307 let mut buf = cell.borrow_mut();
308 buf.clear();
309 let cap = buf.capacity();
311 if cap < size {
312 buf.reserve(size - cap);
313 }
314 file.read_to_end(&mut buf)?;
315 Ok(hash_bytes(algo, &buf))
316 })
317}
318
319pub fn hash_stdin(algo: HashAlgorithm) -> io::Result<String> {
321 let stdin = io::stdin();
322 #[cfg(target_os = "linux")]
324 {
325 use std::os::unix::io::AsRawFd;
326 let fd = stdin.as_raw_fd();
327 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
328 if unsafe { libc::fstat(fd, &mut stat) } == 0
329 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
330 && stat.st_size > 0
331 {
332 unsafe {
333 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
334 }
335 }
336 }
337 hash_reader(algo, stdin.lock())
339}
340
341pub fn should_use_parallel(paths: &[&Path]) -> bool {
346 paths.len() >= 2
347}
348
349#[cfg(target_os = "linux")]
354pub fn readahead_files(paths: &[&Path]) {
355 use std::os::unix::io::AsRawFd;
356 for path in paths {
357 if let Ok(file) = open_noatime(path) {
358 if let Ok(meta) = file.metadata() {
359 let len = meta.len();
360 if meta.file_type().is_file() && len >= FADVISE_MIN_SIZE {
361 unsafe {
362 libc::posix_fadvise(
363 file.as_raw_fd(),
364 0,
365 len as i64,
366 libc::POSIX_FADV_WILLNEED,
367 );
368 }
369 }
370 }
371 }
372 }
373}
374
375#[cfg(not(target_os = "linux"))]
376pub fn readahead_files(_paths: &[&Path]) {
377 }
379
380pub fn blake2b_hash_data(data: &[u8], output_bytes: usize) -> String {
385 let hash = blake2b_simd::Params::new()
386 .hash_length(output_bytes)
387 .hash(data);
388 hex_encode(hash.as_bytes())
389}
390
391pub fn blake2b_hash_reader<R: Read>(mut reader: R, output_bytes: usize) -> io::Result<String> {
394 STREAM_BUF.with(|cell| {
395 let mut buf = cell.borrow_mut();
396 let mut state = blake2b_simd::Params::new()
397 .hash_length(output_bytes)
398 .to_state();
399 loop {
400 let n = read_full(&mut reader, &mut buf)?;
401 if n == 0 {
402 break;
403 }
404 state.update(&buf[..n]);
405 }
406 Ok(hex_encode(state.finalize().as_bytes()))
407 })
408}
409
410pub fn blake2b_hash_file(path: &Path, output_bytes: usize) -> io::Result<String> {
414 let file = open_noatime(path)?;
415 let metadata = file.metadata()?;
416 let file_size = metadata.len();
417
418 if metadata.file_type().is_file() && file_size == 0 {
419 return Ok(blake2b_hash_data(&[], output_bytes));
420 }
421
422 if file_size > 0 && metadata.file_type().is_file() {
423 if file_size >= SMALL_FILE_LIMIT {
425 #[cfg(target_os = "linux")]
426 if file_size >= FADVISE_MIN_SIZE {
427 use std::os::unix::io::AsRawFd;
428 unsafe {
429 libc::posix_fadvise(
430 file.as_raw_fd(),
431 0,
432 file_size as i64,
433 libc::POSIX_FADV_SEQUENTIAL,
434 );
435 }
436 }
437 if let Ok(mmap) = unsafe { memmap2::MmapOptions::new().populate().map(&file) } {
438 #[cfg(target_os = "linux")]
439 {
440 let _ = mmap.advise(memmap2::Advice::Sequential);
441 if file_size >= 2 * 1024 * 1024 {
442 let _ = mmap.advise(memmap2::Advice::HugePage);
443 }
444 }
445 return Ok(blake2b_hash_data(&mmap, output_bytes));
446 }
447 }
448 if file_size < SMALL_FILE_LIMIT {
450 return blake2b_hash_file_small(file, file_size as usize, output_bytes);
451 }
452 }
453
454 #[cfg(target_os = "linux")]
456 if file_size >= FADVISE_MIN_SIZE {
457 use std::os::unix::io::AsRawFd;
458 unsafe {
459 libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
460 }
461 }
462 blake2b_hash_reader(file, output_bytes)
463}
464
465#[inline]
467fn blake2b_hash_file_small(mut file: File, size: usize, output_bytes: usize) -> io::Result<String> {
468 SMALL_FILE_BUF.with(|cell| {
469 let mut buf = cell.borrow_mut();
470 buf.clear();
471 let cap = buf.capacity();
472 if cap < size {
473 buf.reserve(size - cap);
474 }
475 file.read_to_end(&mut buf)?;
476 Ok(blake2b_hash_data(&buf, output_bytes))
477 })
478}
479
480pub fn blake2b_hash_stdin(output_bytes: usize) -> io::Result<String> {
483 let stdin = io::stdin();
484 #[cfg(target_os = "linux")]
485 {
486 use std::os::unix::io::AsRawFd;
487 let fd = stdin.as_raw_fd();
488 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
489 if unsafe { libc::fstat(fd, &mut stat) } == 0
490 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
491 && stat.st_size > 0
492 {
493 unsafe {
494 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
495 }
496 }
497 }
498 blake2b_hash_reader(stdin.lock(), output_bytes)
499}
500
501pub fn print_hash(
504 out: &mut impl Write,
505 hash: &str,
506 filename: &str,
507 binary: bool,
508) -> io::Result<()> {
509 let mode = if binary { b'*' } else { b' ' };
510 out.write_all(hash.as_bytes())?;
511 out.write_all(&[b' ', mode])?;
512 out.write_all(filename.as_bytes())?;
513 out.write_all(b"\n")
514}
515
516pub fn print_hash_zero(
518 out: &mut impl Write,
519 hash: &str,
520 filename: &str,
521 binary: bool,
522) -> io::Result<()> {
523 let mode = if binary { b'*' } else { b' ' };
524 out.write_all(hash.as_bytes())?;
525 out.write_all(&[b' ', mode])?;
526 out.write_all(filename.as_bytes())?;
527 out.write_all(b"\0")
528}
529
530pub fn print_hash_tag(
532 out: &mut impl Write,
533 algo: HashAlgorithm,
534 hash: &str,
535 filename: &str,
536) -> io::Result<()> {
537 out.write_all(algo.name().as_bytes())?;
538 out.write_all(b" (")?;
539 out.write_all(filename.as_bytes())?;
540 out.write_all(b") = ")?;
541 out.write_all(hash.as_bytes())?;
542 out.write_all(b"\n")
543}
544
545pub fn print_hash_tag_zero(
547 out: &mut impl Write,
548 algo: HashAlgorithm,
549 hash: &str,
550 filename: &str,
551) -> io::Result<()> {
552 out.write_all(algo.name().as_bytes())?;
553 out.write_all(b" (")?;
554 out.write_all(filename.as_bytes())?;
555 out.write_all(b") = ")?;
556 out.write_all(hash.as_bytes())?;
557 out.write_all(b"\0")
558}
559
560pub fn print_hash_tag_b2sum(
564 out: &mut impl Write,
565 hash: &str,
566 filename: &str,
567 bits: usize,
568) -> io::Result<()> {
569 if bits == 512 {
570 out.write_all(b"BLAKE2b (")?;
571 } else {
572 write!(out, "BLAKE2b-{} (", bits)?;
574 }
575 out.write_all(filename.as_bytes())?;
576 out.write_all(b") = ")?;
577 out.write_all(hash.as_bytes())?;
578 out.write_all(b"\n")
579}
580
581pub fn print_hash_tag_b2sum_zero(
583 out: &mut impl Write,
584 hash: &str,
585 filename: &str,
586 bits: usize,
587) -> io::Result<()> {
588 if bits == 512 {
589 out.write_all(b"BLAKE2b (")?;
590 } else {
591 write!(out, "BLAKE2b-{} (", bits)?;
592 }
593 out.write_all(filename.as_bytes())?;
594 out.write_all(b") = ")?;
595 out.write_all(hash.as_bytes())?;
596 out.write_all(b"\0")
597}
598
599pub struct CheckOptions {
601 pub quiet: bool,
602 pub status_only: bool,
603 pub strict: bool,
604 pub warn: bool,
605 pub ignore_missing: bool,
606 pub warn_prefix: String,
610}
611
612pub struct CheckResult {
614 pub ok: usize,
615 pub mismatches: usize,
616 pub format_errors: usize,
617 pub read_errors: usize,
618 pub ignored_missing: usize,
620}
621
622pub fn check_file<R: BufRead>(
625 algo: HashAlgorithm,
626 reader: R,
627 opts: &CheckOptions,
628 out: &mut impl Write,
629 err_out: &mut impl Write,
630) -> io::Result<CheckResult> {
631 let quiet = opts.quiet;
632 let status_only = opts.status_only;
633 let warn = opts.warn;
634 let ignore_missing = opts.ignore_missing;
635 let mut ok_count = 0;
636 let mut mismatch_count = 0;
637 let mut format_errors = 0;
638 let mut read_errors = 0;
639 let mut ignored_missing_count = 0;
640 let mut line_num = 0;
641
642 for line_result in reader.lines() {
643 line_num += 1;
644 let line = line_result?;
645 let line = line.trim_end();
646
647 if line.is_empty() {
648 continue;
649 }
650
651 let (expected_hash, filename) = match parse_check_line(line) {
653 Some(v) => v,
654 None => {
655 format_errors += 1;
656 if warn {
657 out.flush()?;
658 if opts.warn_prefix.is_empty() {
659 writeln!(
660 err_out,
661 "line {}: improperly formatted {} checksum line",
662 line_num,
663 algo.name()
664 )?;
665 } else {
666 writeln!(
667 err_out,
668 "{}: {}: improperly formatted {} checksum line",
669 opts.warn_prefix,
670 line_num,
671 algo.name()
672 )?;
673 }
674 }
675 continue;
676 }
677 };
678
679 let actual = match hash_file(algo, Path::new(filename)) {
681 Ok(h) => h,
682 Err(e) => {
683 if ignore_missing && e.kind() == io::ErrorKind::NotFound {
684 ignored_missing_count += 1;
685 continue;
686 }
687 read_errors += 1;
688 if !status_only {
689 out.flush()?;
690 writeln!(err_out, "{}: {}", filename, e)?;
691 writeln!(out, "{}: FAILED open or read", filename)?;
692 }
693 continue;
694 }
695 };
696
697 if actual.eq_ignore_ascii_case(expected_hash) {
698 ok_count += 1;
699 if !quiet && !status_only {
700 writeln!(out, "{}: OK", filename)?;
701 }
702 } else {
703 mismatch_count += 1;
704 if !status_only {
705 writeln!(out, "{}: FAILED", filename)?;
706 }
707 }
708 }
709
710 Ok(CheckResult {
711 ok: ok_count,
712 mismatches: mismatch_count,
713 format_errors,
714 read_errors,
715 ignored_missing: ignored_missing_count,
716 })
717}
718
719pub fn parse_check_line(line: &str) -> Option<(&str, &str)> {
721 let rest = line
723 .strip_prefix("MD5 (")
724 .or_else(|| line.strip_prefix("SHA256 ("))
725 .or_else(|| line.strip_prefix("BLAKE2b ("))
726 .or_else(|| {
727 if line.starts_with("BLAKE2b-") {
729 let after = &line["BLAKE2b-".len()..];
730 if let Some(sp) = after.find(" (") {
731 if after[..sp].bytes().all(|b| b.is_ascii_digit()) {
732 return Some(&after[sp + 2..]);
733 }
734 }
735 }
736 None
737 });
738 if let Some(rest) = rest {
739 if let Some(paren_idx) = rest.find(") = ") {
740 let filename = &rest[..paren_idx];
741 let hash = &rest[paren_idx + 4..];
742 return Some((hash, filename));
743 }
744 }
745
746 let line = line.strip_prefix('\\').unwrap_or(line);
748
749 if let Some(idx) = line.find(" ") {
751 let hash = &line[..idx];
752 let rest = &line[idx + 2..];
753 return Some((hash, rest));
754 }
755 if let Some(idx) = line.find(" *") {
757 let hash = &line[..idx];
758 let rest = &line[idx + 2..];
759 return Some((hash, rest));
760 }
761 None
762}
763
764pub fn parse_check_line_tag(line: &str) -> Option<(&str, &str, Option<usize>)> {
768 let paren_start = line.find(" (")?;
769 let algo_part = &line[..paren_start];
770 let rest = &line[paren_start + 2..];
771 let paren_end = rest.find(") = ")?;
772 let filename = &rest[..paren_end];
773 let hash = &rest[paren_end + 4..];
774
775 let bits = if let Some(dash_pos) = algo_part.rfind('-') {
777 algo_part[dash_pos + 1..].parse::<usize>().ok()
778 } else {
779 None
780 };
781
782 Some((hash, filename, bits))
783}
784
785#[inline]
789fn read_full(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<usize> {
790 let n = reader.read(buf)?;
792 if n == buf.len() || n == 0 {
793 return Ok(n);
794 }
795 let mut total = n;
797 while total < buf.len() {
798 match reader.read(&mut buf[total..]) {
799 Ok(0) => break,
800 Ok(n) => total += n,
801 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
802 Err(e) => return Err(e),
803 }
804 }
805 Ok(total)
806}
807
808const fn generate_hex_table() -> [[u8; 2]; 256] {
811 let hex = b"0123456789abcdef";
812 let mut table = [[0u8; 2]; 256];
813 let mut i = 0;
814 while i < 256 {
815 table[i] = [hex[i >> 4], hex[i & 0xf]];
816 i += 1;
817 }
818 table
819}
820
821const HEX_TABLE: [[u8; 2]; 256] = generate_hex_table();
822
823pub(crate) fn hex_encode(bytes: &[u8]) -> String {
826 let len = bytes.len() * 2;
827 let mut hex = String::with_capacity(len);
828 unsafe {
830 let buf = hex.as_mut_vec();
831 buf.set_len(len);
832 let ptr = buf.as_mut_ptr();
833 for (i, &b) in bytes.iter().enumerate() {
834 let pair = *HEX_TABLE.get_unchecked(b as usize);
835 *ptr.add(i * 2) = pair[0];
836 *ptr.add(i * 2 + 1) = pair[1];
837 }
838 }
839 hex
840}