1use std::cell::RefCell;
2use std::fs::File;
3use std::io::{self, BufRead, Read, Write};
4use std::path::Path;
5
6use std::sync::atomic::AtomicUsize;
7#[cfg(target_os = "linux")]
8use std::sync::atomic::{AtomicBool, Ordering};
9
10#[cfg(not(target_os = "linux"))]
11use digest::Digest;
12#[cfg(not(target_os = "linux"))]
13use md5::Md5;
14
15#[derive(Debug, Clone, Copy)]
17pub enum HashAlgorithm {
18 Sha1,
19 Sha224,
20 Sha256,
21 Sha384,
22 Sha512,
23 Md5,
24 Blake2b,
25}
26
27impl HashAlgorithm {
28 pub fn name(self) -> &'static str {
29 match self {
30 HashAlgorithm::Sha1 => "SHA1",
31 HashAlgorithm::Sha224 => "SHA224",
32 HashAlgorithm::Sha256 => "SHA256",
33 HashAlgorithm::Sha384 => "SHA384",
34 HashAlgorithm::Sha512 => "SHA512",
35 HashAlgorithm::Md5 => "MD5",
36 HashAlgorithm::Blake2b => "BLAKE2b",
37 }
38 }
39}
40
41#[cfg(not(target_os = "linux"))]
45fn hash_digest<D: Digest>(data: &[u8]) -> String {
46 hex_encode(&D::digest(data))
47}
48
49#[cfg(not(target_os = "linux"))]
51fn hash_reader_impl<D: Digest>(mut reader: impl Read) -> io::Result<String> {
52 STREAM_BUF.with(|cell| {
53 let mut buf = cell.borrow_mut();
54 ensure_stream_buf(&mut buf);
55 let mut hasher = D::new();
56 loop {
57 let n = read_full(&mut reader, &mut buf)?;
58 if n == 0 {
59 break;
60 }
61 hasher.update(&buf[..n]);
62 }
63 Ok(hex_encode(&hasher.finalize()))
64 })
65}
66
67const HASH_READ_BUF: usize = 131072;
74
75thread_local! {
79 static STREAM_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
80}
81
82#[inline]
85fn ensure_stream_buf(buf: &mut Vec<u8>) {
86 if buf.len() < HASH_READ_BUF {
87 buf.resize(HASH_READ_BUF, 0);
88 }
89}
90
91#[cfg(target_os = "linux")]
99#[inline]
100fn openssl_hash_bytes(md: openssl::hash::MessageDigest, data: &[u8]) -> io::Result<String> {
101 let digest = openssl::hash::hash(md, data).map_err(|e| io::Error::other(e.to_string()))?;
102 Ok(hex_encode(&digest))
103}
104
105#[cfg(target_os = "linux")]
107fn openssl_hash_reader(
108 md: openssl::hash::MessageDigest,
109 mut reader: impl Read,
110) -> io::Result<String> {
111 STREAM_BUF.with(|cell| {
112 let mut buf = cell.borrow_mut();
113 ensure_stream_buf(&mut buf);
114 let mut hasher =
115 openssl::hash::Hasher::new(md).map_err(|e| io::Error::other(e.to_string()))?;
116 loop {
117 let n = read_full(&mut reader, &mut buf)?;
118 if n == 0 {
119 break;
120 }
121 hasher
122 .update(&buf[..n])
123 .map_err(|e| io::Error::other(e.to_string()))?;
124 }
125 let digest = hasher
126 .finish()
127 .map_err(|e| io::Error::other(e.to_string()))?;
128 Ok(hex_encode(&digest))
129 })
130}
131
132#[cfg(target_os = "linux")]
135#[inline]
136fn openssl_hash_bytes_to_buf(
137 md: openssl::hash::MessageDigest,
138 data: &[u8],
139 out: &mut [u8],
140) -> io::Result<usize> {
141 let digest = openssl::hash::hash(md, data).map_err(|e| io::Error::other(e.to_string()))?;
142 hex_encode_to_slice(&digest, out);
143 Ok(digest.len() * 2)
144}
145
146#[cfg(all(not(target_vendor = "apple"), not(target_os = "linux")))]
151#[inline]
152fn ring_hash_bytes(algo: &'static ring::digest::Algorithm, data: &[u8]) -> io::Result<String> {
153 Ok(hex_encode(ring::digest::digest(algo, data).as_ref()))
154}
155
156#[cfg(all(not(target_vendor = "apple"), not(target_os = "linux")))]
158fn ring_hash_reader(
159 algo: &'static ring::digest::Algorithm,
160 mut reader: impl Read,
161) -> io::Result<String> {
162 STREAM_BUF.with(|cell| {
163 let mut buf = cell.borrow_mut();
164 ensure_stream_buf(&mut buf);
165 let mut ctx = ring::digest::Context::new(algo);
166 loop {
167 let n = read_full(&mut reader, &mut buf)?;
168 if n == 0 {
169 break;
170 }
171 ctx.update(&buf[..n]);
172 }
173 Ok(hex_encode(ctx.finish().as_ref()))
174 })
175}
176
177#[cfg(target_os = "linux")]
182fn algo_to_openssl_md(algo: HashAlgorithm) -> openssl::hash::MessageDigest {
183 match algo {
184 HashAlgorithm::Sha1 => openssl::hash::MessageDigest::sha1(),
185 HashAlgorithm::Sha224 => openssl::hash::MessageDigest::sha224(),
186 HashAlgorithm::Sha256 => openssl::hash::MessageDigest::sha256(),
187 HashAlgorithm::Sha384 => openssl::hash::MessageDigest::sha384(),
188 HashAlgorithm::Sha512 => openssl::hash::MessageDigest::sha512(),
189 HashAlgorithm::Md5 => openssl::hash::MessageDigest::md5(),
190 HashAlgorithm::Blake2b => unreachable!("Blake2b uses its own hasher"),
191 }
192}
193
194#[cfg(target_os = "linux")]
200fn sha256_bytes(data: &[u8]) -> io::Result<String> {
201 openssl_hash_bytes(openssl::hash::MessageDigest::sha256(), data)
202}
203
204#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
205fn sha256_bytes(data: &[u8]) -> io::Result<String> {
206 ring_hash_bytes(&ring::digest::SHA256, data)
207}
208
209#[cfg(target_vendor = "apple")]
210fn sha256_bytes(data: &[u8]) -> io::Result<String> {
211 Ok(hash_digest::<sha2::Sha256>(data))
212}
213
214#[cfg(target_os = "linux")]
215fn sha256_reader(reader: impl Read) -> io::Result<String> {
216 openssl_hash_reader(openssl::hash::MessageDigest::sha256(), reader)
217}
218
219#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
220fn sha256_reader(reader: impl Read) -> io::Result<String> {
221 ring_hash_reader(&ring::digest::SHA256, reader)
222}
223
224#[cfg(target_vendor = "apple")]
225fn sha256_reader(reader: impl Read) -> io::Result<String> {
226 hash_reader_impl::<sha2::Sha256>(reader)
227}
228
229#[cfg(target_os = "linux")]
232fn sha1_bytes(data: &[u8]) -> io::Result<String> {
233 openssl_hash_bytes(openssl::hash::MessageDigest::sha1(), data)
234}
235
236#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
237fn sha1_bytes(data: &[u8]) -> io::Result<String> {
238 ring_hash_bytes(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data)
239}
240
241#[cfg(target_vendor = "apple")]
242fn sha1_bytes(data: &[u8]) -> io::Result<String> {
243 Ok(hash_digest::<sha1::Sha1>(data))
244}
245
246#[cfg(target_os = "linux")]
247fn sha1_reader(reader: impl Read) -> io::Result<String> {
248 openssl_hash_reader(openssl::hash::MessageDigest::sha1(), reader)
249}
250
251#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
252fn sha1_reader(reader: impl Read) -> io::Result<String> {
253 ring_hash_reader(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, reader)
254}
255
256#[cfg(target_vendor = "apple")]
257fn sha1_reader(reader: impl Read) -> io::Result<String> {
258 hash_reader_impl::<sha1::Sha1>(reader)
259}
260
261#[cfg(target_os = "linux")]
265fn sha224_bytes(data: &[u8]) -> io::Result<String> {
266 openssl_hash_bytes(openssl::hash::MessageDigest::sha224(), data)
267}
268
269#[cfg(not(target_os = "linux"))]
270fn sha224_bytes(data: &[u8]) -> io::Result<String> {
271 Ok(hex_encode(&sha2::Sha224::digest(data)))
272}
273
274#[cfg(target_os = "linux")]
275fn sha224_reader(reader: impl Read) -> io::Result<String> {
276 openssl_hash_reader(openssl::hash::MessageDigest::sha224(), reader)
277}
278
279#[cfg(not(target_os = "linux"))]
280fn sha224_reader(reader: impl Read) -> io::Result<String> {
281 STREAM_BUF.with(|cell| {
282 let mut buf = cell.borrow_mut();
283 ensure_stream_buf(&mut buf);
284 let mut hasher = <sha2::Sha224 as digest::Digest>::new();
285 let mut reader = reader;
286 loop {
287 let n = read_full(&mut reader, &mut buf)?;
288 if n == 0 {
289 break;
290 }
291 digest::Digest::update(&mut hasher, &buf[..n]);
292 }
293 Ok(hex_encode(&digest::Digest::finalize(hasher)))
294 })
295}
296
297#[cfg(target_os = "linux")]
300fn sha384_bytes(data: &[u8]) -> io::Result<String> {
301 openssl_hash_bytes(openssl::hash::MessageDigest::sha384(), data)
302}
303
304#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
305fn sha384_bytes(data: &[u8]) -> io::Result<String> {
306 ring_hash_bytes(&ring::digest::SHA384, data)
307}
308
309#[cfg(target_vendor = "apple")]
310fn sha384_bytes(data: &[u8]) -> io::Result<String> {
311 Ok(hex_encode(&sha2::Sha384::digest(data)))
312}
313
314#[cfg(target_os = "linux")]
315fn sha384_reader(reader: impl Read) -> io::Result<String> {
316 openssl_hash_reader(openssl::hash::MessageDigest::sha384(), reader)
317}
318
319#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
320fn sha384_reader(reader: impl Read) -> io::Result<String> {
321 ring_hash_reader(&ring::digest::SHA384, reader)
322}
323
324#[cfg(target_vendor = "apple")]
325fn sha384_reader(reader: impl Read) -> io::Result<String> {
326 STREAM_BUF.with(|cell| {
327 let mut buf = cell.borrow_mut();
328 ensure_stream_buf(&mut buf);
329 let mut hasher = <sha2::Sha384 as digest::Digest>::new();
330 let mut reader = reader;
331 loop {
332 let n = read_full(&mut reader, &mut buf)?;
333 if n == 0 {
334 break;
335 }
336 digest::Digest::update(&mut hasher, &buf[..n]);
337 }
338 Ok(hex_encode(&digest::Digest::finalize(hasher)))
339 })
340}
341
342#[cfg(target_os = "linux")]
345fn sha512_bytes(data: &[u8]) -> io::Result<String> {
346 openssl_hash_bytes(openssl::hash::MessageDigest::sha512(), data)
347}
348
349#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
350fn sha512_bytes(data: &[u8]) -> io::Result<String> {
351 ring_hash_bytes(&ring::digest::SHA512, data)
352}
353
354#[cfg(target_vendor = "apple")]
355fn sha512_bytes(data: &[u8]) -> io::Result<String> {
356 Ok(hex_encode(&sha2::Sha512::digest(data)))
357}
358
359#[cfg(target_os = "linux")]
360fn sha512_reader(reader: impl Read) -> io::Result<String> {
361 openssl_hash_reader(openssl::hash::MessageDigest::sha512(), reader)
362}
363
364#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
365fn sha512_reader(reader: impl Read) -> io::Result<String> {
366 ring_hash_reader(&ring::digest::SHA512, reader)
367}
368
369#[cfg(target_vendor = "apple")]
370fn sha512_reader(reader: impl Read) -> io::Result<String> {
371 STREAM_BUF.with(|cell| {
372 let mut buf = cell.borrow_mut();
373 ensure_stream_buf(&mut buf);
374 let mut hasher = <sha2::Sha512 as digest::Digest>::new();
375 let mut reader = reader;
376 loop {
377 let n = read_full(&mut reader, &mut buf)?;
378 if n == 0 {
379 break;
380 }
381 digest::Digest::update(&mut hasher, &buf[..n]);
382 }
383 Ok(hex_encode(&digest::Digest::finalize(hasher)))
384 })
385}
386
387pub fn hash_bytes(algo: HashAlgorithm, data: &[u8]) -> io::Result<String> {
390 match algo {
391 HashAlgorithm::Sha1 => sha1_bytes(data),
392 HashAlgorithm::Sha224 => sha224_bytes(data),
393 HashAlgorithm::Sha256 => sha256_bytes(data),
394 HashAlgorithm::Sha384 => sha384_bytes(data),
395 HashAlgorithm::Sha512 => sha512_bytes(data),
396 HashAlgorithm::Md5 => md5_bytes(data),
397 HashAlgorithm::Blake2b => {
398 let hash = blake2b_simd::blake2b(data);
399 Ok(hex_encode(hash.as_bytes()))
400 }
401 }
402}
403
404#[cfg(target_os = "linux")]
409pub fn hash_bytes_to_buf(algo: HashAlgorithm, data: &[u8], out: &mut [u8]) -> io::Result<usize> {
410 match algo {
411 HashAlgorithm::Md5 => {
412 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::md5(), data, out)
413 }
414 HashAlgorithm::Sha1 => sha1_bytes_to_buf(data, out),
415 HashAlgorithm::Sha224 => sha224_bytes_to_buf(data, out),
416 HashAlgorithm::Sha256 => sha256_bytes_to_buf(data, out),
417 HashAlgorithm::Sha384 => sha384_bytes_to_buf(data, out),
418 HashAlgorithm::Sha512 => sha512_bytes_to_buf(data, out),
419 HashAlgorithm::Blake2b => {
420 let hash = blake2b_simd::blake2b(data);
421 let bytes = hash.as_bytes();
422 hex_encode_to_slice(bytes, out);
423 Ok(bytes.len() * 2)
424 }
425 }
426}
427
428#[cfg(target_os = "linux")]
429fn sha1_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
430 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::sha1(), data, out)
431}
432#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
433fn sha1_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
434 let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
435 hex_encode_to_slice(digest.as_ref(), out);
436 Ok(40)
437}
438#[cfg(target_vendor = "apple")]
439fn sha1_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
440 let digest = sha1::Sha1::digest(data);
441 hex_encode_to_slice(&digest, out);
442 Ok(40)
443}
444
445#[cfg(target_os = "linux")]
446fn sha224_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
447 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::sha224(), data, out)
448}
449#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
450fn sha224_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
451 let digest = <sha2::Sha224 as sha2::Digest>::digest(data);
452 hex_encode_to_slice(&digest, out);
453 Ok(56)
454}
455#[cfg(target_vendor = "apple")]
456fn sha224_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
457 let digest = <sha2::Sha224 as sha2::Digest>::digest(data);
458 hex_encode_to_slice(&digest, out);
459 Ok(56)
460}
461
462#[cfg(target_os = "linux")]
463fn sha256_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
464 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::sha256(), data, out)
465}
466#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
467fn sha256_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
468 let digest = ring::digest::digest(&ring::digest::SHA256, data);
469 hex_encode_to_slice(digest.as_ref(), out);
470 Ok(64)
471}
472#[cfg(target_vendor = "apple")]
473fn sha256_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
474 let digest = <sha2::Sha256 as sha2::Digest>::digest(data);
475 hex_encode_to_slice(&digest, out);
476 Ok(64)
477}
478
479#[cfg(target_os = "linux")]
480fn sha384_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
481 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::sha384(), data, out)
482}
483#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
484fn sha384_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
485 let digest = ring::digest::digest(&ring::digest::SHA384, data);
486 hex_encode_to_slice(digest.as_ref(), out);
487 Ok(96)
488}
489#[cfg(target_vendor = "apple")]
490fn sha384_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
491 let digest = <sha2::Sha384 as sha2::Digest>::digest(data);
492 hex_encode_to_slice(&digest, out);
493 Ok(96)
494}
495
496#[cfg(target_os = "linux")]
497fn sha512_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
498 openssl_hash_bytes_to_buf(openssl::hash::MessageDigest::sha512(), data, out)
499}
500#[cfg(all(not(target_os = "linux"), not(target_vendor = "apple")))]
501fn sha512_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
502 let digest = ring::digest::digest(&ring::digest::SHA512, data);
503 hex_encode_to_slice(digest.as_ref(), out);
504 Ok(128)
505}
506#[cfg(target_vendor = "apple")]
507fn sha512_bytes_to_buf(data: &[u8], out: &mut [u8]) -> io::Result<usize> {
508 let digest = <sha2::Sha512 as sha2::Digest>::digest(data);
509 hex_encode_to_slice(&digest, out);
510 Ok(128)
511}
512
513#[cfg(target_os = "linux")]
518pub fn hash_file_raw_to_buf(algo: HashAlgorithm, path: &Path, out: &mut [u8]) -> io::Result<usize> {
519 use std::os::unix::ffi::OsStrExt;
520
521 let path_bytes = path.as_os_str().as_bytes();
522 let c_path = std::ffi::CString::new(path_bytes)
523 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
524
525 let mut flags = libc::O_RDONLY | libc::O_CLOEXEC;
526 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
527 flags |= libc::O_NOATIME;
528 }
529
530 let fd = unsafe { libc::open(c_path.as_ptr(), flags) };
531 if fd < 0 {
532 let err = io::Error::last_os_error();
533 if err.raw_os_error() == Some(libc::EPERM) && flags & libc::O_NOATIME != 0 {
534 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
535 let fd2 = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
536 if fd2 < 0 {
537 return Err(io::Error::last_os_error());
538 }
539 return hash_from_raw_fd_to_buf(algo, fd2, out);
540 }
541 return Err(err);
542 }
543 hash_from_raw_fd_to_buf(algo, fd, out)
544}
545
546#[cfg(target_os = "linux")]
550fn hash_from_raw_fd_to_buf(algo: HashAlgorithm, fd: i32, out: &mut [u8]) -> io::Result<usize> {
551 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
552 if unsafe { libc::fstat(fd, &mut stat) } != 0 {
553 let err = io::Error::last_os_error();
554 unsafe {
555 libc::close(fd);
556 }
557 return Err(err);
558 }
559 let size = stat.st_size as u64;
560 let is_regular = (stat.st_mode & libc::S_IFMT) == libc::S_IFREG;
561
562 if is_regular && size == 0 {
564 unsafe {
565 libc::close(fd);
566 }
567 return hash_bytes_to_buf(algo, &[], out);
568 }
569
570 if is_regular && size < TINY_FILE_LIMIT {
572 let mut buf = [0u8; 8192];
573 let mut total = 0usize;
574 while total < size as usize {
575 let n = unsafe {
576 libc::read(
577 fd,
578 buf[total..].as_mut_ptr() as *mut libc::c_void,
579 (size as usize) - total,
580 )
581 };
582 if n < 0 {
583 let err = io::Error::last_os_error();
584 if err.kind() == io::ErrorKind::Interrupted {
585 continue;
586 }
587 unsafe {
588 libc::close(fd);
589 }
590 return Err(err);
591 }
592 if n == 0 {
593 break;
594 }
595 total += n as usize;
596 }
597 unsafe {
598 libc::close(fd);
599 }
600 return hash_bytes_to_buf(algo, &buf[..total], out);
601 }
602
603 use std::os::unix::io::FromRawFd;
606 let file = unsafe { File::from_raw_fd(fd) };
607 let hash_str = if is_regular && size > 0 {
608 hash_regular_file(algo, file, size)?
609 } else {
610 hash_reader(algo, file)?
611 };
612 let hex_bytes = hash_str.as_bytes();
613 out[..hex_bytes.len()].copy_from_slice(hex_bytes);
614 Ok(hex_bytes.len())
615}
616
617#[cfg(target_os = "linux")]
622fn md5_bytes(data: &[u8]) -> io::Result<String> {
623 openssl_hash_bytes(openssl::hash::MessageDigest::md5(), data)
624}
625
626#[cfg(not(target_os = "linux"))]
627fn md5_bytes(data: &[u8]) -> io::Result<String> {
628 Ok(hash_digest::<Md5>(data))
629}
630
631#[cfg(target_os = "linux")]
632fn md5_reader(reader: impl Read) -> io::Result<String> {
633 openssl_hash_reader(openssl::hash::MessageDigest::md5(), reader)
634}
635
636#[cfg(not(target_os = "linux"))]
637fn md5_reader(reader: impl Read) -> io::Result<String> {
638 hash_reader_impl::<Md5>(reader)
639}
640
641pub fn hash_reader<R: Read>(algo: HashAlgorithm, reader: R) -> io::Result<String> {
643 match algo {
644 HashAlgorithm::Sha1 => sha1_reader(reader),
645 HashAlgorithm::Sha224 => sha224_reader(reader),
646 HashAlgorithm::Sha256 => sha256_reader(reader),
647 HashAlgorithm::Sha384 => sha384_reader(reader),
648 HashAlgorithm::Sha512 => sha512_reader(reader),
649 HashAlgorithm::Md5 => md5_reader(reader),
650 HashAlgorithm::Blake2b => blake2b_hash_reader(reader, 64),
651 }
652}
653
654#[cfg(target_os = "linux")]
657static NOATIME_SUPPORTED: AtomicBool = AtomicBool::new(true);
658
659#[cfg(target_os = "linux")]
662fn open_noatime(path: &Path) -> io::Result<File> {
663 use std::os::unix::fs::OpenOptionsExt;
664 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
665 match std::fs::OpenOptions::new()
666 .read(true)
667 .custom_flags(libc::O_NOATIME)
668 .open(path)
669 {
670 Ok(f) => return Ok(f),
671 Err(ref e) if e.raw_os_error() == Some(libc::EPERM) => {
672 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
674 }
675 Err(e) => return Err(e), }
677 }
678 File::open(path)
679}
680
681#[cfg(not(target_os = "linux"))]
682fn open_noatime(path: &Path) -> io::Result<File> {
683 File::open(path)
684}
685
686#[cfg(target_os = "linux")]
689#[inline]
690fn open_and_stat(path: &Path) -> io::Result<(File, u64, bool)> {
691 let file = open_noatime(path)?;
692 let fd = {
693 use std::os::unix::io::AsRawFd;
694 file.as_raw_fd()
695 };
696 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
697 if unsafe { libc::fstat(fd, &mut stat) } != 0 {
698 return Err(io::Error::last_os_error());
699 }
700 let is_regular = (stat.st_mode & libc::S_IFMT) == libc::S_IFREG;
701 let size = stat.st_size as u64;
702 Ok((file, size, is_regular))
703}
704
705#[cfg(not(target_os = "linux"))]
706#[inline]
707fn open_and_stat(path: &Path) -> io::Result<(File, u64, bool)> {
708 let file = open_noatime(path)?;
709 let metadata = file.metadata()?;
710 Ok((file, metadata.len(), metadata.file_type().is_file()))
711}
712
713#[cfg(target_os = "linux")]
716const FADVISE_MIN_SIZE: u64 = 1024 * 1024;
717
718const SMALL_FILE_LIMIT: u64 = 16 * 1024 * 1024;
725
726const TINY_FILE_LIMIT: u64 = 8 * 1024;
730
731thread_local! {
735 static SMALL_FILE_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(64 * 1024));
736}
737
738#[cfg(target_os = "linux")]
744fn hash_file_pipelined(algo: HashAlgorithm, file: File, file_size: u64) -> io::Result<String> {
745 if file_size >= 64 * 1024 * 1024 {
748 hash_file_pipelined_read(algo, file, file_size)
749 } else {
750 hash_file_streaming(algo, file, file_size)
751 }
752}
753
754#[cfg(target_os = "linux")]
757fn hash_file_streaming(algo: HashAlgorithm, file: File, file_size: u64) -> io::Result<String> {
758 use std::os::unix::io::AsRawFd;
759
760 let _ = unsafe {
761 libc::posix_fadvise(
762 file.as_raw_fd(),
763 0,
764 file_size as i64,
765 libc::POSIX_FADV_SEQUENTIAL,
766 )
767 };
768
769 if matches!(algo, HashAlgorithm::Blake2b) {
771 blake2b_hash_reader(file, 64)
772 } else {
773 openssl_hash_reader(algo_to_openssl_md(algo), file)
774 }
775}
776
777#[cfg(target_os = "linux")]
781fn hash_file_pipelined_read(
782 algo: HashAlgorithm,
783 mut file: File,
784 file_size: u64,
785) -> io::Result<String> {
786 use std::os::unix::io::AsRawFd;
787
788 const PIPE_BUF_SIZE: usize = 4 * 1024 * 1024; let _ = unsafe {
791 libc::posix_fadvise(
792 file.as_raw_fd(),
793 0,
794 file_size as i64,
795 libc::POSIX_FADV_SEQUENTIAL,
796 )
797 };
798
799 let (tx, rx) = std::sync::mpsc::sync_channel::<(Vec<u8>, usize)>(1);
800 let (buf_tx, buf_rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(1);
801 let _ = buf_tx.send(vec![0u8; PIPE_BUF_SIZE]);
802
803 let reader_handle = std::thread::spawn(move || -> io::Result<()> {
804 while let Ok(mut buf) = buf_rx.recv() {
805 let mut total = 0;
806 while total < buf.len() {
807 match file.read(&mut buf[total..]) {
808 Ok(0) => break,
809 Ok(n) => total += n,
810 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
811 Err(e) => return Err(e),
812 }
813 }
814 if total == 0 {
815 break;
816 }
817 if tx.send((buf, total)).is_err() {
818 break;
819 }
820 }
821 Ok(())
822 });
823
824 macro_rules! hash_pipelined_openssl {
826 ($md:expr) => {{
827 let mut hasher =
828 openssl::hash::Hasher::new($md).map_err(|e| io::Error::other(e.to_string()))?;
829 while let Ok((buf, n)) = rx.recv() {
830 hasher
831 .update(&buf[..n])
832 .map_err(|e| io::Error::other(e.to_string()))?;
833 let _ = buf_tx.send(buf);
834 }
835 let digest = hasher
836 .finish()
837 .map_err(|e| io::Error::other(e.to_string()))?;
838 Ok(hex_encode(&digest))
839 }};
840 }
841
842 let hash_result: io::Result<String> = if matches!(algo, HashAlgorithm::Blake2b) {
843 let mut state = blake2b_simd::Params::new().to_state();
844 while let Ok((buf, n)) = rx.recv() {
845 state.update(&buf[..n]);
846 let _ = buf_tx.send(buf);
847 }
848 Ok(hex_encode(state.finalize().as_bytes()))
849 } else {
850 hash_pipelined_openssl!(algo_to_openssl_md(algo))
851 };
852
853 match reader_handle.join() {
854 Ok(Ok(())) => {}
855 Ok(Err(e)) => {
856 if hash_result.is_ok() {
857 return Err(e);
858 }
859 }
860 Err(payload) => {
861 let msg = if let Some(s) = payload.downcast_ref::<&str>() {
862 format!("reader thread panicked: {}", s)
863 } else if let Some(s) = payload.downcast_ref::<String>() {
864 format!("reader thread panicked: {}", s)
865 } else {
866 "reader thread panicked".to_string()
867 };
868 return Err(io::Error::other(msg));
869 }
870 }
871
872 hash_result
873}
874
875fn hash_regular_file(algo: HashAlgorithm, file: File, file_size: u64) -> io::Result<String> {
885 if file_size >= SMALL_FILE_LIMIT {
887 let mmap_result = unsafe { memmap2::MmapOptions::new().map(&file) };
888 if let Ok(mmap) = mmap_result {
889 #[cfg(target_os = "linux")]
890 {
891 if file_size >= 2 * 1024 * 1024 {
892 let _ = mmap.advise(memmap2::Advice::HugePage);
893 }
894 let _ = mmap.advise(memmap2::Advice::Sequential);
895 if mmap.advise(memmap2::Advice::PopulateRead).is_err() {
899 let _ = mmap.advise(memmap2::Advice::WillNeed);
900 }
901 }
902 return hash_bytes(algo, &mmap);
903 }
904 #[cfg(target_os = "linux")]
906 {
907 return hash_file_pipelined(algo, file, file_size);
908 }
909 #[cfg(not(target_os = "linux"))]
910 {
911 return hash_reader(algo, file);
912 }
913 }
914 hash_file_small(algo, file, file_size as usize)
920}
921
922pub fn hash_file(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
925 let (file, file_size, is_regular) = open_and_stat(path)?;
926
927 if is_regular && file_size == 0 {
928 return hash_bytes(algo, &[]);
929 }
930
931 if file_size > 0 && is_regular {
932 if file_size < TINY_FILE_LIMIT {
933 return hash_file_tiny(algo, file, file_size as usize);
934 }
935 return hash_regular_file(algo, file, file_size);
936 }
937
938 #[cfg(target_os = "linux")]
940 if file_size >= FADVISE_MIN_SIZE {
941 use std::os::unix::io::AsRawFd;
942 let _ = unsafe {
943 libc::posix_fadvise(
944 file.as_raw_fd(),
945 0,
946 file_size as i64,
947 libc::POSIX_FADV_SEQUENTIAL,
948 )
949 };
950 }
951 hash_reader(algo, file)
952}
953
954#[inline]
958fn hash_file_tiny(algo: HashAlgorithm, mut file: File, size: usize) -> io::Result<String> {
959 let mut buf = [0u8; 8192];
960 let mut total = 0;
961 while total < size {
963 match file.read(&mut buf[total..size]) {
964 Ok(0) => break,
965 Ok(n) => total += n,
966 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
967 Err(e) => return Err(e),
968 }
969 }
970 hash_bytes(algo, &buf[..total])
971}
972
973#[inline]
976fn hash_file_small(algo: HashAlgorithm, mut file: File, size: usize) -> io::Result<String> {
977 SMALL_FILE_BUF.with(|cell| {
978 let mut buf = cell.borrow_mut();
979 buf.clear();
981 buf.reserve(size);
982 unsafe {
985 buf.set_len(size);
986 }
987 let mut total = 0;
988 while total < size {
989 match file.read(&mut buf[total..size]) {
990 Ok(0) => break,
991 Ok(n) => total += n,
992 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
993 Err(e) => return Err(e),
994 }
995 }
996 hash_bytes(algo, &buf[..total])
997 })
998}
999
1000pub fn hash_stdin(algo: HashAlgorithm) -> io::Result<String> {
1002 let stdin = io::stdin();
1003 #[cfg(target_os = "linux")]
1005 {
1006 use std::os::unix::io::AsRawFd;
1007 let fd = stdin.as_raw_fd();
1008 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1009 if unsafe { libc::fstat(fd, &mut stat) } == 0
1010 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
1011 && stat.st_size > 0
1012 {
1013 unsafe {
1014 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
1015 }
1016 }
1017 }
1018 hash_reader(algo, stdin.lock())
1020}
1021
1022pub fn should_use_parallel(paths: &[&Path]) -> bool {
1027 paths.len() >= 2
1028}
1029
1030#[cfg(target_os = "linux")]
1035pub fn readahead_files(paths: &[&Path]) {
1036 use std::os::unix::io::AsRawFd;
1037 for path in paths {
1038 if let Ok(file) = open_noatime(path) {
1039 if let Ok(meta) = file.metadata() {
1040 let len = meta.len();
1041 if meta.file_type().is_file() && len >= FADVISE_MIN_SIZE {
1042 unsafe {
1043 libc::posix_fadvise(
1044 file.as_raw_fd(),
1045 0,
1046 len as i64,
1047 libc::POSIX_FADV_WILLNEED,
1048 );
1049 }
1050 }
1051 }
1052 }
1053 }
1054}
1055
1056#[cfg(not(target_os = "linux"))]
1057pub fn readahead_files(_paths: &[&Path]) {
1058 }
1060
1061pub fn blake2b_hash_data(data: &[u8], output_bytes: usize) -> String {
1066 let hash = blake2b_simd::Params::new()
1067 .hash_length(output_bytes)
1068 .hash(data);
1069 hex_encode(hash.as_bytes())
1070}
1071
1072pub fn blake2b_hash_reader<R: Read>(mut reader: R, output_bytes: usize) -> io::Result<String> {
1075 STREAM_BUF.with(|cell| {
1076 let mut buf = cell.borrow_mut();
1077 ensure_stream_buf(&mut buf);
1078 let mut state = blake2b_simd::Params::new()
1079 .hash_length(output_bytes)
1080 .to_state();
1081 loop {
1082 let n = read_full(&mut reader, &mut buf)?;
1083 if n == 0 {
1084 break;
1085 }
1086 state.update(&buf[..n]);
1087 }
1088 Ok(hex_encode(state.finalize().as_bytes()))
1089 })
1090}
1091
1092pub fn blake2b_hash_file(path: &Path, output_bytes: usize) -> io::Result<String> {
1096 let (file, file_size, is_regular) = open_and_stat(path)?;
1097
1098 if is_regular && file_size == 0 {
1099 return Ok(blake2b_hash_data(&[], output_bytes));
1100 }
1101
1102 if file_size > 0 && is_regular {
1103 if file_size < TINY_FILE_LIMIT {
1105 return blake2b_hash_file_tiny(file, file_size as usize, output_bytes);
1106 }
1107 if file_size >= SMALL_FILE_LIMIT {
1109 #[cfg(target_os = "linux")]
1110 {
1111 return blake2b_hash_file_pipelined(file, file_size, output_bytes);
1112 }
1113 #[cfg(not(target_os = "linux"))]
1114 {
1115 let mmap_result = unsafe { memmap2::MmapOptions::new().map(&file) };
1116 if let Ok(mmap) = mmap_result {
1117 return Ok(blake2b_hash_data(&mmap, output_bytes));
1118 }
1119 }
1120 }
1121 if file_size < SMALL_FILE_LIMIT {
1123 return blake2b_hash_file_small(file, file_size as usize, output_bytes);
1124 }
1125 }
1126
1127 #[cfg(target_os = "linux")]
1129 if file_size >= FADVISE_MIN_SIZE {
1130 use std::os::unix::io::AsRawFd;
1131 let _ = unsafe {
1132 libc::posix_fadvise(
1133 file.as_raw_fd(),
1134 0,
1135 file_size as i64,
1136 libc::POSIX_FADV_SEQUENTIAL,
1137 )
1138 };
1139 }
1140 blake2b_hash_reader(file, output_bytes)
1141}
1142
1143#[inline]
1145fn blake2b_hash_file_tiny(mut file: File, size: usize, output_bytes: usize) -> io::Result<String> {
1146 let mut buf = [0u8; 8192];
1147 let mut total = 0;
1148 while total < size {
1149 match file.read(&mut buf[total..size]) {
1150 Ok(0) => break,
1151 Ok(n) => total += n,
1152 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1153 Err(e) => return Err(e),
1154 }
1155 }
1156 Ok(blake2b_hash_data(&buf[..total], output_bytes))
1157}
1158
1159#[inline]
1161fn blake2b_hash_file_small(mut file: File, size: usize, output_bytes: usize) -> io::Result<String> {
1162 SMALL_FILE_BUF.with(|cell| {
1163 let mut buf = cell.borrow_mut();
1164 buf.clear();
1165 buf.reserve(size);
1166 unsafe {
1168 buf.set_len(size);
1169 }
1170 let mut total = 0;
1171 while total < size {
1172 match file.read(&mut buf[total..size]) {
1173 Ok(0) => break,
1174 Ok(n) => total += n,
1175 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1176 Err(e) => return Err(e),
1177 }
1178 }
1179 Ok(blake2b_hash_data(&buf[..total], output_bytes))
1180 })
1181}
1182
1183#[cfg(target_os = "linux")]
1188fn blake2b_hash_file_pipelined(
1189 file: File,
1190 file_size: u64,
1191 output_bytes: usize,
1192) -> io::Result<String> {
1193 match unsafe { memmap2::MmapOptions::new().map(&file) } {
1197 Ok(mmap) => {
1198 if file_size >= 2 * 1024 * 1024 {
1201 let _ = mmap.advise(memmap2::Advice::HugePage);
1202 }
1203 let _ = mmap.advise(memmap2::Advice::Sequential);
1204 if file_size >= 4 * 1024 * 1024 {
1207 if mmap.advise(memmap2::Advice::PopulateRead).is_err() {
1208 let _ = mmap.advise(memmap2::Advice::WillNeed);
1209 }
1210 } else {
1211 let _ = mmap.advise(memmap2::Advice::WillNeed);
1212 }
1213 Ok(blake2b_hash_data(&mmap, output_bytes))
1216 }
1217 Err(_) => {
1218 blake2b_hash_file_streamed(file, file_size, output_bytes)
1221 }
1222 }
1223}
1224
1225#[cfg(target_os = "linux")]
1229fn blake2b_hash_file_streamed(
1230 mut file: File,
1231 file_size: u64,
1232 output_bytes: usize,
1233) -> io::Result<String> {
1234 use std::os::unix::io::AsRawFd;
1235
1236 const PIPE_BUF_SIZE: usize = 8 * 1024 * 1024; unsafe {
1240 libc::posix_fadvise(
1241 file.as_raw_fd(),
1242 0,
1243 file_size as i64,
1244 libc::POSIX_FADV_SEQUENTIAL,
1245 );
1246 }
1247
1248 let (tx, rx) = std::sync::mpsc::sync_channel::<(Vec<u8>, usize)>(1);
1250 let (buf_tx, buf_rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(1);
1251 let _ = buf_tx.send(vec![0u8; PIPE_BUF_SIZE]);
1252
1253 let reader_handle = std::thread::spawn(move || -> io::Result<()> {
1254 while let Ok(mut buf) = buf_rx.recv() {
1256 let mut total = 0;
1257 while total < buf.len() {
1258 match file.read(&mut buf[total..]) {
1259 Ok(0) => break,
1260 Ok(n) => total += n,
1261 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1262 Err(e) => return Err(e),
1263 }
1264 }
1265 if total == 0 {
1266 break;
1267 }
1268 if tx.send((buf, total)).is_err() {
1269 break;
1270 }
1271 }
1272 Ok(())
1273 });
1274
1275 let mut state = blake2b_simd::Params::new()
1276 .hash_length(output_bytes)
1277 .to_state();
1278 while let Ok((buf, n)) = rx.recv() {
1279 state.update(&buf[..n]);
1280 let _ = buf_tx.send(buf);
1281 }
1282 let hash_result = Ok(hex_encode(state.finalize().as_bytes()));
1283
1284 match reader_handle.join() {
1285 Ok(Ok(())) => {}
1286 Ok(Err(e)) => {
1287 if hash_result.is_ok() {
1288 return Err(e);
1289 }
1290 }
1291 Err(payload) => {
1292 let msg = if let Some(s) = payload.downcast_ref::<&str>() {
1293 format!("reader thread panicked: {}", s)
1294 } else if let Some(s) = payload.downcast_ref::<String>() {
1295 format!("reader thread panicked: {}", s)
1296 } else {
1297 "reader thread panicked".to_string()
1298 };
1299 return Err(io::Error::other(msg));
1300 }
1301 }
1302
1303 hash_result
1304}
1305
1306pub fn blake2b_hash_stdin(output_bytes: usize) -> io::Result<String> {
1309 let stdin = io::stdin();
1310 #[cfg(target_os = "linux")]
1311 {
1312 use std::os::unix::io::AsRawFd;
1313 let fd = stdin.as_raw_fd();
1314 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1315 if unsafe { libc::fstat(fd, &mut stat) } == 0
1316 && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
1317 && stat.st_size > 0
1318 {
1319 unsafe {
1320 libc::posix_fadvise(fd, 0, stat.st_size, libc::POSIX_FADV_SEQUENTIAL);
1321 }
1322 }
1323 }
1324 blake2b_hash_reader(stdin.lock(), output_bytes)
1325}
1326
1327enum FileContent {
1330 Mmap(memmap2::Mmap),
1331 Buf(Vec<u8>),
1332}
1333
1334impl AsRef<[u8]> for FileContent {
1335 fn as_ref(&self) -> &[u8] {
1336 match self {
1337 FileContent::Mmap(m) => m,
1338 FileContent::Buf(v) => v,
1339 }
1340 }
1341}
1342
1343fn open_file_content(path: &Path) -> io::Result<FileContent> {
1347 let (file, size, is_regular) = open_and_stat(path)?;
1348 if is_regular && size == 0 {
1349 return Ok(FileContent::Buf(Vec::new()));
1350 }
1351 if is_regular && size > 0 {
1352 if size < TINY_FILE_LIMIT {
1356 let mut buf = vec![0u8; size as usize];
1357 let mut total = 0;
1358 let mut f = file;
1359 while total < size as usize {
1360 match f.read(&mut buf[total..]) {
1361 Ok(0) => break,
1362 Ok(n) => total += n,
1363 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1364 Err(e) => return Err(e),
1365 }
1366 }
1367 buf.truncate(total);
1368 return Ok(FileContent::Buf(buf));
1369 }
1370 let mmap_result = unsafe { memmap2::MmapOptions::new().map(&file) };
1372 if let Ok(mmap) = mmap_result {
1373 #[cfg(target_os = "linux")]
1374 {
1375 if size >= 2 * 1024 * 1024 {
1376 let _ = mmap.advise(memmap2::Advice::HugePage);
1377 }
1378 let _ = mmap.advise(memmap2::Advice::Sequential);
1379 if mmap.advise(memmap2::Advice::PopulateRead).is_err() {
1380 let _ = mmap.advise(memmap2::Advice::WillNeed);
1381 }
1382 }
1383 return Ok(FileContent::Mmap(mmap));
1384 }
1385 let mut buf = vec![0u8; size as usize];
1387 let mut total = 0;
1388 let mut f = file;
1389 while total < size as usize {
1390 match f.read(&mut buf[total..]) {
1391 Ok(0) => break,
1392 Ok(n) => total += n,
1393 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1394 Err(e) => return Err(e),
1395 }
1396 }
1397 buf.truncate(total);
1398 return Ok(FileContent::Buf(buf));
1399 }
1400 let mut buf = Vec::new();
1402 let mut f = file;
1403 f.read_to_end(&mut buf)?;
1404 Ok(FileContent::Buf(buf))
1405}
1406
1407fn read_remaining_to_vec(prefix: &[u8], mut file: File) -> io::Result<FileContent> {
1411 let mut buf = Vec::with_capacity(prefix.len() + 65536);
1412 buf.extend_from_slice(prefix);
1413 file.read_to_end(&mut buf)?;
1414 Ok(FileContent::Buf(buf))
1415}
1416
1417fn open_file_content_fast(path: &Path) -> io::Result<FileContent> {
1422 let mut file = open_noatime(path)?;
1423 let mut small_buf = [0u8; 4096];
1426 match file.read(&mut small_buf) {
1427 Ok(0) => return Ok(FileContent::Buf(Vec::new())),
1428 Ok(n) if n < small_buf.len() => {
1429 let mut vec = Vec::with_capacity(n);
1431 vec.extend_from_slice(&small_buf[..n]);
1432 return Ok(FileContent::Buf(vec));
1433 }
1434 Ok(n) => {
1435 let mut buf = vec![0u8; 65536];
1437 buf[..n].copy_from_slice(&small_buf[..n]);
1438 let mut total = n;
1439 loop {
1440 match file.read(&mut buf[total..]) {
1441 Ok(0) => {
1442 buf.truncate(total);
1443 return Ok(FileContent::Buf(buf));
1444 }
1445 Ok(n) => {
1446 total += n;
1447 if total >= buf.len() {
1448 return read_remaining_to_vec(&buf[..total], file);
1450 }
1451 }
1452 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1453 Err(e) => return Err(e),
1454 }
1455 }
1456 }
1457 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {
1458 let mut buf = vec![0u8; 65536];
1459 let mut total = 0;
1460 loop {
1461 match file.read(&mut buf[total..]) {
1462 Ok(0) => {
1463 buf.truncate(total);
1464 return Ok(FileContent::Buf(buf));
1465 }
1466 Ok(n) => {
1467 total += n;
1468 if total >= buf.len() {
1469 return read_remaining_to_vec(&buf[..total], file);
1471 }
1472 }
1473 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1474 Err(e) => return Err(e),
1475 }
1476 }
1477 }
1478 Err(e) => return Err(e),
1479 }
1480}
1481
1482pub fn blake2b_hash_files_many(paths: &[&Path], output_bytes: usize) -> Vec<io::Result<String>> {
1490 use blake2b_simd::many::{HashManyJob, hash_many};
1491
1492 let use_fast = paths.len() >= 20;
1497
1498 let file_data: Vec<io::Result<FileContent>> = if paths.len() <= 10 {
1499 paths.iter().map(|&path| open_file_content(path)).collect()
1501 } else {
1502 let num_threads = std::thread::available_parallelism()
1503 .map(|n| n.get())
1504 .unwrap_or(4)
1505 .min(paths.len());
1506 let chunk_size = (paths.len() + num_threads - 1) / num_threads;
1507
1508 std::thread::scope(|s| {
1509 let handles: Vec<_> = paths
1510 .chunks(chunk_size)
1511 .map(|chunk| {
1512 s.spawn(move || {
1513 chunk
1514 .iter()
1515 .map(|&path| {
1516 if use_fast {
1517 open_file_content_fast(path)
1518 } else {
1519 open_file_content(path)
1520 }
1521 })
1522 .collect::<Vec<_>>()
1523 })
1524 })
1525 .collect();
1526
1527 handles
1528 .into_iter()
1529 .flat_map(|h| h.join().unwrap())
1530 .collect()
1531 })
1532 };
1533
1534 let hash_results = {
1536 let mut params = blake2b_simd::Params::new();
1537 params.hash_length(output_bytes);
1538
1539 let ok_entries: Vec<(usize, &[u8])> = file_data
1540 .iter()
1541 .enumerate()
1542 .filter_map(|(i, r)| r.as_ref().ok().map(|c| (i, c.as_ref())))
1543 .collect();
1544
1545 let mut jobs: Vec<HashManyJob> = ok_entries
1546 .iter()
1547 .map(|(_, data)| HashManyJob::new(¶ms, data))
1548 .collect();
1549
1550 hash_many(jobs.iter_mut());
1552
1553 let mut hm: Vec<Option<String>> = vec![None; paths.len()];
1555 for (j, &(orig_i, _)) in ok_entries.iter().enumerate() {
1556 hm[orig_i] = Some(hex_encode(jobs[j].to_hash().as_bytes()));
1557 }
1558 hm
1559 }; hash_results
1563 .into_iter()
1564 .zip(file_data)
1565 .map(|(hash_opt, result)| match result {
1566 Ok(_) => Ok(hash_opt.unwrap()),
1567 Err(e) => Err(e),
1568 })
1569 .collect()
1570}
1571
1572pub fn blake2b_hash_files_parallel(
1580 paths: &[&Path],
1581 output_bytes: usize,
1582) -> Vec<io::Result<String>> {
1583 let n = paths.len();
1584
1585 let sample_count = n.min(5);
1589 let mut sample_max: u64 = 0;
1590 let mut sample_total: u64 = 0;
1591 for &p in paths.iter().take(sample_count) {
1592 let size = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
1593 sample_total += size;
1594 sample_max = sample_max.max(size);
1595 }
1596 let estimated_total = if sample_count > 0 {
1597 sample_total * (n as u64) / (sample_count as u64)
1598 } else {
1599 0
1600 };
1601
1602 if estimated_total < 1024 * 1024 && sample_max < SMALL_FILE_LIMIT {
1605 return blake2b_hash_files_many(paths, output_bytes);
1606 }
1607
1608 let mut indexed: Vec<(usize, &Path, u64)> = paths
1610 .iter()
1611 .enumerate()
1612 .map(|(i, &p)| {
1613 let size = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
1614 (i, p, size)
1615 })
1616 .collect();
1617
1618 indexed.sort_by(|a, b| b.2.cmp(&a.2));
1621
1622 #[cfg(target_os = "linux")]
1627 {
1628 use std::os::unix::io::AsRawFd;
1629 for &(_, path, size) in indexed.iter().take(20) {
1630 if size >= 1024 * 1024 {
1631 if let Ok(file) = open_noatime(path) {
1632 unsafe {
1633 libc::readahead(file.as_raw_fd(), 0, size as usize);
1634 }
1635 }
1636 }
1637 }
1638 }
1639
1640 let num_threads = std::thread::available_parallelism()
1641 .map(|n| n.get())
1642 .unwrap_or(4)
1643 .min(n);
1644
1645 let work_idx = AtomicUsize::new(0);
1647
1648 std::thread::scope(|s| {
1649 let work_idx = &work_idx;
1650 let indexed = &indexed;
1651
1652 let handles: Vec<_> = (0..num_threads)
1653 .map(|_| {
1654 s.spawn(move || {
1655 let mut local_results = Vec::new();
1656 loop {
1657 let idx = work_idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1658 if idx >= indexed.len() {
1659 break;
1660 }
1661 let (orig_idx, path, _size) = indexed[idx];
1662 let result = blake2b_hash_file(path, output_bytes);
1663 local_results.push((orig_idx, result));
1664 }
1665 local_results
1666 })
1667 })
1668 .collect();
1669
1670 let mut results: Vec<Option<io::Result<String>>> = (0..n).map(|_| None).collect();
1672 for handle in handles {
1673 for (orig_idx, result) in handle.join().unwrap() {
1674 results[orig_idx] = Some(result);
1675 }
1676 }
1677 results
1678 .into_iter()
1679 .map(|opt| opt.unwrap_or_else(|| Err(io::Error::other("missing result"))))
1680 .collect()
1681 })
1682}
1683
1684pub fn hash_files_auto(paths: &[&Path], algo: HashAlgorithm) -> Vec<io::Result<String>> {
1693 let n = paths.len();
1694 if n == 0 {
1695 return Vec::new();
1696 }
1697 if n == 1 {
1698 return vec![hash_file_nostat(algo, paths[0])];
1699 }
1700
1701 let sample_size = paths
1705 .iter()
1706 .take(3)
1707 .filter_map(|p| std::fs::metadata(p).ok())
1708 .map(|m| m.len())
1709 .max()
1710 .unwrap_or(0);
1711
1712 if sample_size < 65536 {
1713 #[cfg(target_os = "linux")]
1715 {
1716 let mut c_path_buf = Vec::with_capacity(256);
1718 paths
1719 .iter()
1720 .map(|&p| hash_file_raw_nostat(algo, p, &mut c_path_buf))
1721 .collect()
1722 }
1723 #[cfg(not(target_os = "linux"))]
1724 {
1725 paths.iter().map(|&p| hash_file_nostat(algo, p)).collect()
1726 }
1727 } else if n >= 20 {
1728 hash_files_batch(paths, algo)
1729 } else {
1730 hash_files_parallel_fast(paths, algo)
1731 }
1732}
1733
1734pub fn hash_files_parallel(paths: &[&Path], algo: HashAlgorithm) -> Vec<io::Result<String>> {
1740 let n = paths.len();
1741
1742 let mut indexed: Vec<(usize, &Path, u64)> = paths
1745 .iter()
1746 .enumerate()
1747 .map(|(i, &p)| {
1748 let size = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
1749 (i, p, size)
1750 })
1751 .collect();
1752
1753 indexed.sort_by(|a, b| b.2.cmp(&a.2));
1756
1757 #[cfg(target_os = "linux")]
1762 {
1763 use std::os::unix::io::AsRawFd;
1764 for &(_, path, size) in indexed.iter().take(20) {
1765 if size >= 1024 * 1024 {
1766 if let Ok(file) = open_noatime(path) {
1767 unsafe {
1768 libc::readahead(file.as_raw_fd(), 0, size as usize);
1769 }
1770 }
1771 }
1772 }
1773 }
1774
1775 let num_threads = std::thread::available_parallelism()
1776 .map(|n| n.get())
1777 .unwrap_or(4)
1778 .min(n);
1779
1780 let work_idx = AtomicUsize::new(0);
1782
1783 std::thread::scope(|s| {
1784 let work_idx = &work_idx;
1785 let indexed = &indexed;
1786
1787 let handles: Vec<_> = (0..num_threads)
1788 .map(|_| {
1789 s.spawn(move || {
1790 let mut local_results = Vec::new();
1791 loop {
1792 let idx = work_idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1793 if idx >= indexed.len() {
1794 break;
1795 }
1796 let (orig_idx, path, _size) = indexed[idx];
1797 let result = hash_file(algo, path);
1798 local_results.push((orig_idx, result));
1799 }
1800 local_results
1801 })
1802 })
1803 .collect();
1804
1805 let mut results: Vec<Option<io::Result<String>>> = (0..n).map(|_| None).collect();
1807 for handle in handles {
1808 for (orig_idx, result) in handle.join().unwrap() {
1809 results[orig_idx] = Some(result);
1810 }
1811 }
1812 results
1813 .into_iter()
1814 .map(|opt| opt.unwrap_or_else(|| Err(io::Error::other("missing result"))))
1815 .collect()
1816 })
1817}
1818
1819pub fn hash_files_parallel_fast(paths: &[&Path], algo: HashAlgorithm) -> Vec<io::Result<String>> {
1825 let n = paths.len();
1826 if n == 0 {
1827 return Vec::new();
1828 }
1829 if n == 1 {
1830 return vec![hash_file_nostat(algo, paths[0])];
1831 }
1832
1833 #[cfg(target_os = "linux")]
1836 readahead_files_all(paths);
1837
1838 let num_threads = std::thread::available_parallelism()
1839 .map(|n| n.get())
1840 .unwrap_or(4)
1841 .min(n);
1842
1843 let work_idx = AtomicUsize::new(0);
1844
1845 std::thread::scope(|s| {
1846 let work_idx = &work_idx;
1847
1848 let handles: Vec<_> = (0..num_threads)
1849 .map(|_| {
1850 s.spawn(move || {
1851 let mut local_results = Vec::new();
1852 loop {
1853 let idx = work_idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1854 if idx >= n {
1855 break;
1856 }
1857 let result = hash_file_nostat(algo, paths[idx]);
1858 local_results.push((idx, result));
1859 }
1860 local_results
1861 })
1862 })
1863 .collect();
1864
1865 let mut results: Vec<Option<io::Result<String>>> = (0..n).map(|_| None).collect();
1866 for handle in handles {
1867 for (idx, result) in handle.join().unwrap() {
1868 results[idx] = Some(result);
1869 }
1870 }
1871 results
1872 .into_iter()
1873 .map(|opt| opt.unwrap_or_else(|| Err(io::Error::other("missing result"))))
1874 .collect()
1875 })
1876}
1877
1878pub fn hash_files_batch(paths: &[&Path], algo: HashAlgorithm) -> Vec<io::Result<String>> {
1887 let n = paths.len();
1888 if n == 0 {
1889 return Vec::new();
1890 }
1891
1892 #[cfg(target_os = "linux")]
1894 readahead_files_all(paths);
1895
1896 let use_fast = n >= 20;
1899
1900 let file_data: Vec<io::Result<FileContent>> = if n <= 10 {
1901 paths
1903 .iter()
1904 .map(|&path| {
1905 if use_fast {
1906 open_file_content_fast(path)
1907 } else {
1908 open_file_content(path)
1909 }
1910 })
1911 .collect()
1912 } else {
1913 let num_threads = std::thread::available_parallelism()
1914 .map(|t| t.get())
1915 .unwrap_or(4)
1916 .min(n);
1917 let chunk_size = (n + num_threads - 1) / num_threads;
1918
1919 std::thread::scope(|s| {
1920 let handles: Vec<_> = paths
1921 .chunks(chunk_size)
1922 .map(|chunk| {
1923 s.spawn(move || {
1924 chunk
1925 .iter()
1926 .map(|&path| {
1927 if use_fast {
1928 open_file_content_fast(path)
1929 } else {
1930 open_file_content(path)
1931 }
1932 })
1933 .collect::<Vec<_>>()
1934 })
1935 })
1936 .collect();
1937
1938 handles
1939 .into_iter()
1940 .flat_map(|h| h.join().unwrap())
1941 .collect()
1942 })
1943 };
1944
1945 let num_hash_threads = std::thread::available_parallelism()
1948 .map(|t| t.get())
1949 .unwrap_or(4)
1950 .min(n);
1951 let work_idx = AtomicUsize::new(0);
1952
1953 std::thread::scope(|s| {
1954 let work_idx = &work_idx;
1955 let file_data = &file_data;
1956
1957 let handles: Vec<_> = (0..num_hash_threads)
1958 .map(|_| {
1959 s.spawn(move || {
1960 let mut local_results = Vec::new();
1961 loop {
1962 let idx = work_idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1963 if idx >= n {
1964 break;
1965 }
1966 let result = match &file_data[idx] {
1967 Ok(content) => hash_bytes(algo, content.as_ref()),
1968 Err(e) => Err(io::Error::new(e.kind(), e.to_string())),
1969 };
1970 local_results.push((idx, result));
1971 }
1972 local_results
1973 })
1974 })
1975 .collect();
1976
1977 let mut results: Vec<Option<io::Result<String>>> = (0..n).map(|_| None).collect();
1978 for handle in handles {
1979 for (idx, result) in handle.join().unwrap() {
1980 results[idx] = Some(result);
1981 }
1982 }
1983 results
1984 .into_iter()
1985 .map(|opt| opt.unwrap_or_else(|| Err(io::Error::other("missing result"))))
1986 .collect()
1987 })
1988}
1989
1990fn hash_stream_with_prefix(
1994 algo: HashAlgorithm,
1995 prefix: &[u8],
1996 mut file: File,
1997) -> io::Result<String> {
1998 if matches!(algo, HashAlgorithm::Blake2b) {
2000 let mut state = blake2b_simd::Params::new().to_state();
2001 state.update(prefix);
2002 return STREAM_BUF.with(|cell| {
2003 let mut buf = cell.borrow_mut();
2004 ensure_stream_buf(&mut buf);
2005 loop {
2006 let n = read_full(&mut file, &mut buf)?;
2007 if n == 0 {
2008 break;
2009 }
2010 state.update(&buf[..n]);
2011 }
2012 Ok(hex_encode(state.finalize().as_bytes()))
2013 });
2014 }
2015
2016 #[cfg(target_os = "linux")]
2017 {
2018 hash_stream_with_prefix_openssl(algo_to_openssl_md(algo), prefix, file)
2019 }
2020 #[cfg(not(target_os = "linux"))]
2021 {
2022 match algo {
2023 HashAlgorithm::Sha1 => hash_stream_with_prefix_digest::<sha1::Sha1>(prefix, file),
2024 HashAlgorithm::Sha224 => hash_stream_with_prefix_digest::<sha2::Sha224>(prefix, file),
2025 HashAlgorithm::Sha256 => hash_stream_with_prefix_digest::<sha2::Sha256>(prefix, file),
2026 HashAlgorithm::Sha384 => hash_stream_with_prefix_digest::<sha2::Sha384>(prefix, file),
2027 HashAlgorithm::Sha512 => hash_stream_with_prefix_digest::<sha2::Sha512>(prefix, file),
2028 HashAlgorithm::Md5 => hash_stream_with_prefix_digest::<md5::Md5>(prefix, file),
2029 HashAlgorithm::Blake2b => unreachable!(),
2030 }
2031 }
2032}
2033
2034#[cfg(not(target_os = "linux"))]
2036fn hash_stream_with_prefix_digest<D: digest::Digest>(
2037 prefix: &[u8],
2038 mut file: File,
2039) -> io::Result<String> {
2040 STREAM_BUF.with(|cell| {
2041 let mut buf = cell.borrow_mut();
2042 ensure_stream_buf(&mut buf);
2043 let mut hasher = D::new();
2044 hasher.update(prefix);
2045 loop {
2046 let n = read_full(&mut file, &mut buf)?;
2047 if n == 0 {
2048 break;
2049 }
2050 hasher.update(&buf[..n]);
2051 }
2052 Ok(hex_encode(&hasher.finalize()))
2053 })
2054}
2055
2056#[cfg(target_os = "linux")]
2058fn hash_stream_with_prefix_openssl(
2059 md: openssl::hash::MessageDigest,
2060 prefix: &[u8],
2061 mut file: File,
2062) -> io::Result<String> {
2063 STREAM_BUF.with(|cell| {
2064 let mut buf = cell.borrow_mut();
2065 ensure_stream_buf(&mut buf);
2066 let mut hasher =
2067 openssl::hash::Hasher::new(md).map_err(|e| io::Error::other(e.to_string()))?;
2068 hasher
2069 .update(prefix)
2070 .map_err(|e| io::Error::other(e.to_string()))?;
2071 loop {
2072 let n = read_full(&mut file, &mut buf)?;
2073 if n == 0 {
2074 break;
2075 }
2076 hasher
2077 .update(&buf[..n])
2078 .map_err(|e| io::Error::other(e.to_string()))?;
2079 }
2080 let digest = hasher
2081 .finish()
2082 .map_err(|e| io::Error::other(e.to_string()))?;
2083 Ok(hex_encode(&digest))
2084 })
2085}
2086
2087pub fn hash_file_nostat(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
2093 let mut file = open_noatime(path)?;
2094 let mut small_buf = [0u8; 4096];
2098 match file.read(&mut small_buf) {
2099 Ok(0) => return hash_bytes(algo, &[]),
2100 Ok(n) if n < small_buf.len() => {
2101 return hash_bytes(algo, &small_buf[..n]);
2103 }
2104 Ok(n) => {
2105 let mut buf = [0u8; 65536];
2107 buf[..n].copy_from_slice(&small_buf[..n]);
2108 let mut total = n;
2109 loop {
2110 match file.read(&mut buf[total..]) {
2111 Ok(0) => return hash_bytes(algo, &buf[..total]),
2112 Ok(n) => {
2113 total += n;
2114 if total >= buf.len() {
2115 return hash_stream_with_prefix(algo, &buf[..total], file);
2118 }
2119 }
2120 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
2121 Err(e) => return Err(e),
2122 }
2123 }
2124 }
2125 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {
2126 let mut buf = [0u8; 65536];
2128 let mut total = 0;
2129 loop {
2130 match file.read(&mut buf[total..]) {
2131 Ok(0) => return hash_bytes(algo, &buf[..total]),
2132 Ok(n) => {
2133 total += n;
2134 if total >= buf.len() {
2135 return hash_stream_with_prefix(algo, &buf[..total], file);
2137 }
2138 }
2139 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
2140 Err(e) => return Err(e),
2141 }
2142 }
2143 }
2144 Err(e) => return Err(e),
2145 }
2146}
2147
2148#[cfg(target_os = "linux")]
2154fn hash_file_raw_nostat(
2155 algo: HashAlgorithm,
2156 path: &Path,
2157 c_path_buf: &mut Vec<u8>,
2158) -> io::Result<String> {
2159 use std::os::unix::ffi::OsStrExt;
2160
2161 let path_bytes = path.as_os_str().as_bytes();
2162
2163 c_path_buf.clear();
2165 c_path_buf.reserve(path_bytes.len() + 1);
2166 c_path_buf.extend_from_slice(path_bytes);
2167 c_path_buf.push(0);
2168
2169 let mut flags = libc::O_RDONLY | libc::O_CLOEXEC;
2170 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
2171 flags |= libc::O_NOATIME;
2172 }
2173
2174 let fd = unsafe { libc::open(c_path_buf.as_ptr() as *const libc::c_char, flags) };
2175 if fd < 0 {
2176 let err = io::Error::last_os_error();
2177 if err.raw_os_error() == Some(libc::EPERM) && flags & libc::O_NOATIME != 0 {
2178 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
2179 let fd2 = unsafe {
2180 libc::open(
2181 c_path_buf.as_ptr() as *const libc::c_char,
2182 libc::O_RDONLY | libc::O_CLOEXEC,
2183 )
2184 };
2185 if fd2 < 0 {
2186 return Err(io::Error::last_os_error());
2187 }
2188 return hash_fd_small(algo, fd2);
2189 }
2190 return Err(err);
2191 }
2192 hash_fd_small(algo, fd)
2193}
2194
2195#[cfg(target_os = "linux")]
2197#[inline]
2198fn hash_fd_small(algo: HashAlgorithm, fd: i32) -> io::Result<String> {
2199 let mut buf = [0u8; 4096];
2200 let n = loop {
2201 let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
2202 if ret >= 0 {
2203 break ret;
2204 }
2205 let err = io::Error::last_os_error();
2206 if err.kind() == io::ErrorKind::Interrupted {
2207 continue;
2208 }
2209 unsafe {
2210 libc::close(fd);
2211 }
2212 return Err(err);
2213 };
2214 let n = n as usize;
2215 if n < buf.len() {
2216 unsafe {
2218 libc::close(fd);
2219 }
2220 return hash_bytes(algo, &buf[..n]);
2221 }
2222 use std::os::unix::io::FromRawFd;
2225 let mut file = unsafe { File::from_raw_fd(fd) };
2226 let mut big_buf = [0u8; 65536];
2227 big_buf[..n].copy_from_slice(&buf[..n]);
2228 let mut total = n;
2229 loop {
2230 match std::io::Read::read(&mut file, &mut big_buf[total..]) {
2231 Ok(0) => return hash_bytes(algo, &big_buf[..total]),
2232 Ok(n) => {
2233 total += n;
2234 if total >= big_buf.len() {
2235 return hash_stream_with_prefix(algo, &big_buf[..total], file);
2236 }
2237 }
2238 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
2239 Err(e) => return Err(e),
2240 }
2241 }
2242}
2243
2244#[cfg(target_os = "linux")]
2255pub fn hash_file_raw(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
2256 use std::os::unix::ffi::OsStrExt;
2257
2258 let path_bytes = path.as_os_str().as_bytes();
2259 let c_path = std::ffi::CString::new(path_bytes)
2260 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
2261
2262 let mut flags = libc::O_RDONLY | libc::O_CLOEXEC;
2264 if NOATIME_SUPPORTED.load(Ordering::Relaxed) {
2265 flags |= libc::O_NOATIME;
2266 }
2267
2268 let fd = unsafe { libc::open(c_path.as_ptr(), flags) };
2269 if fd < 0 {
2270 let err = io::Error::last_os_error();
2271 if err.raw_os_error() == Some(libc::EPERM) && flags & libc::O_NOATIME != 0 {
2272 NOATIME_SUPPORTED.store(false, Ordering::Relaxed);
2273 let fd2 = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
2274 if fd2 < 0 {
2275 return Err(io::Error::last_os_error());
2276 }
2277 return hash_from_raw_fd(algo, fd2);
2278 }
2279 return Err(err);
2280 }
2281 hash_from_raw_fd(algo, fd)
2282}
2283
2284#[cfg(target_os = "linux")]
2288fn hash_from_raw_fd(algo: HashAlgorithm, fd: i32) -> io::Result<String> {
2289 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2291 if unsafe { libc::fstat(fd, &mut stat) } != 0 {
2292 let err = io::Error::last_os_error();
2293 unsafe {
2294 libc::close(fd);
2295 }
2296 return Err(err);
2297 }
2298 let size = stat.st_size as u64;
2299 let is_regular = (stat.st_mode & libc::S_IFMT) == libc::S_IFREG;
2300
2301 if is_regular && size == 0 {
2303 unsafe {
2304 libc::close(fd);
2305 }
2306 return hash_bytes(algo, &[]);
2307 }
2308
2309 if is_regular && size < TINY_FILE_LIMIT {
2312 let mut buf = [0u8; 8192];
2313 let mut total = 0usize;
2314 while total < size as usize {
2315 let n = unsafe {
2316 libc::read(
2317 fd,
2318 buf[total..].as_mut_ptr() as *mut libc::c_void,
2319 (size as usize) - total,
2320 )
2321 };
2322 if n < 0 {
2323 let err = io::Error::last_os_error();
2324 if err.kind() == io::ErrorKind::Interrupted {
2325 continue;
2326 }
2327 unsafe {
2328 libc::close(fd);
2329 }
2330 return Err(err);
2331 }
2332 if n == 0 {
2333 break;
2334 }
2335 total += n as usize;
2336 }
2337 unsafe {
2338 libc::close(fd);
2339 }
2340 return hash_bytes(algo, &buf[..total]);
2341 }
2342
2343 use std::os::unix::io::FromRawFd;
2345 let file = unsafe { File::from_raw_fd(fd) };
2346
2347 if is_regular && size > 0 {
2348 return hash_regular_file(algo, file, size);
2349 }
2350
2351 hash_reader(algo, file)
2353}
2354
2355#[cfg(target_os = "linux")]
2358pub fn readahead_files_all(paths: &[&Path]) {
2359 use std::os::unix::io::AsRawFd;
2360 for path in paths {
2361 if let Ok(file) = open_noatime(path) {
2362 if let Ok(meta) = file.metadata() {
2363 if meta.file_type().is_file() {
2364 let len = meta.len();
2365 unsafe {
2366 libc::posix_fadvise(
2367 file.as_raw_fd(),
2368 0,
2369 len as i64,
2370 libc::POSIX_FADV_WILLNEED,
2371 );
2372 }
2373 }
2374 }
2375 }
2376 }
2377}
2378
2379#[cfg(not(target_os = "linux"))]
2380pub fn readahead_files_all(_paths: &[&Path]) {}
2381
2382pub fn print_hash(
2385 out: &mut impl Write,
2386 hash: &str,
2387 filename: &str,
2388 binary: bool,
2389) -> io::Result<()> {
2390 let mode = if binary { b'*' } else { b' ' };
2391 out.write_all(hash.as_bytes())?;
2392 out.write_all(&[b' ', mode])?;
2393 out.write_all(filename.as_bytes())?;
2394 out.write_all(b"\n")
2395}
2396
2397pub fn print_hash_zero(
2399 out: &mut impl Write,
2400 hash: &str,
2401 filename: &str,
2402 binary: bool,
2403) -> io::Result<()> {
2404 let mode = if binary { b'*' } else { b' ' };
2405 out.write_all(hash.as_bytes())?;
2406 out.write_all(&[b' ', mode])?;
2407 out.write_all(filename.as_bytes())?;
2408 out.write_all(b"\0")
2409}
2410
2411thread_local! {
2418 static LINE_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(256));
2419}
2420
2421#[inline]
2425pub fn write_hash_line(
2426 out: &mut impl Write,
2427 hash: &str,
2428 filename: &str,
2429 binary: bool,
2430 zero: bool,
2431 escaped: bool,
2432) -> io::Result<()> {
2433 LINE_BUF.with(|cell| {
2434 let mut buf = cell.borrow_mut();
2435 buf.clear();
2436 let mode = if binary { b'*' } else { b' ' };
2437 let term = if zero { b'\0' } else { b'\n' };
2438 if escaped {
2439 buf.push(b'\\');
2440 }
2441 buf.extend_from_slice(hash.as_bytes());
2442 buf.push(b' ');
2443 buf.push(mode);
2444 buf.extend_from_slice(filename.as_bytes());
2445 buf.push(term);
2446 out.write_all(&buf)
2447 })
2448}
2449
2450#[inline]
2453pub fn write_hash_tag_line(
2454 out: &mut impl Write,
2455 algo_name: &str,
2456 hash: &str,
2457 filename: &str,
2458 zero: bool,
2459) -> io::Result<()> {
2460 LINE_BUF.with(|cell| {
2461 let mut buf = cell.borrow_mut();
2462 buf.clear();
2463 let term = if zero { b'\0' } else { b'\n' };
2464 buf.extend_from_slice(algo_name.as_bytes());
2465 buf.extend_from_slice(b" (");
2466 buf.extend_from_slice(filename.as_bytes());
2467 buf.extend_from_slice(b") = ");
2468 buf.extend_from_slice(hash.as_bytes());
2469 buf.push(term);
2470 out.write_all(&buf)
2471 })
2472}
2473
2474pub fn print_hash_tag(
2476 out: &mut impl Write,
2477 algo: HashAlgorithm,
2478 hash: &str,
2479 filename: &str,
2480) -> io::Result<()> {
2481 out.write_all(algo.name().as_bytes())?;
2482 out.write_all(b" (")?;
2483 out.write_all(filename.as_bytes())?;
2484 out.write_all(b") = ")?;
2485 out.write_all(hash.as_bytes())?;
2486 out.write_all(b"\n")
2487}
2488
2489pub fn print_hash_tag_zero(
2491 out: &mut impl Write,
2492 algo: HashAlgorithm,
2493 hash: &str,
2494 filename: &str,
2495) -> io::Result<()> {
2496 out.write_all(algo.name().as_bytes())?;
2497 out.write_all(b" (")?;
2498 out.write_all(filename.as_bytes())?;
2499 out.write_all(b") = ")?;
2500 out.write_all(hash.as_bytes())?;
2501 out.write_all(b"\0")
2502}
2503
2504pub fn print_hash_tag_b2sum(
2508 out: &mut impl Write,
2509 hash: &str,
2510 filename: &str,
2511 bits: usize,
2512) -> io::Result<()> {
2513 if bits == 512 {
2514 out.write_all(b"BLAKE2b (")?;
2515 } else {
2516 write!(out, "BLAKE2b-{} (", bits)?;
2518 }
2519 out.write_all(filename.as_bytes())?;
2520 out.write_all(b") = ")?;
2521 out.write_all(hash.as_bytes())?;
2522 out.write_all(b"\n")
2523}
2524
2525pub fn print_hash_tag_b2sum_zero(
2527 out: &mut impl Write,
2528 hash: &str,
2529 filename: &str,
2530 bits: usize,
2531) -> io::Result<()> {
2532 if bits == 512 {
2533 out.write_all(b"BLAKE2b (")?;
2534 } else {
2535 write!(out, "BLAKE2b-{} (", bits)?;
2536 }
2537 out.write_all(filename.as_bytes())?;
2538 out.write_all(b") = ")?;
2539 out.write_all(hash.as_bytes())?;
2540 out.write_all(b"\0")
2541}
2542
2543pub struct CheckOptions {
2545 pub quiet: bool,
2546 pub status_only: bool,
2547 pub strict: bool,
2548 pub warn: bool,
2549 pub ignore_missing: bool,
2550 pub warn_prefix: String,
2554}
2555
2556pub struct CheckResult {
2558 pub ok: usize,
2559 pub mismatches: usize,
2560 pub format_errors: usize,
2561 pub read_errors: usize,
2562 pub ignored_missing: usize,
2564}
2565
2566pub fn check_file<R: BufRead>(
2569 algo: HashAlgorithm,
2570 reader: R,
2571 opts: &CheckOptions,
2572 out: &mut impl Write,
2573 err_out: &mut impl Write,
2574) -> io::Result<CheckResult> {
2575 let quiet = opts.quiet;
2576 let status_only = opts.status_only;
2577 let warn = opts.warn;
2578 let ignore_missing = opts.ignore_missing;
2579 let mut ok_count = 0;
2580 let mut mismatch_count = 0;
2581 let mut format_errors = 0;
2582 let mut read_errors = 0;
2583 let mut ignored_missing_count = 0;
2584 let mut line_num = 0;
2585
2586 for line_result in reader.lines() {
2587 line_num += 1;
2588 let line = line_result?;
2589 let line = line.trim_end();
2590
2591 if line.is_empty() {
2592 continue;
2593 }
2594
2595 let (expected_hash, filename) = match parse_check_line(line) {
2597 Some(v) => v,
2598 None => {
2599 format_errors += 1;
2600 if warn {
2601 out.flush()?;
2602 if opts.warn_prefix.is_empty() {
2603 writeln!(
2604 err_out,
2605 "line {}: improperly formatted {} checksum line",
2606 line_num,
2607 algo.name()
2608 )?;
2609 } else {
2610 writeln!(
2611 err_out,
2612 "{}: {}: improperly formatted {} checksum line",
2613 opts.warn_prefix,
2614 line_num,
2615 algo.name()
2616 )?;
2617 }
2618 }
2619 continue;
2620 }
2621 };
2622
2623 let actual = match hash_file(algo, Path::new(filename)) {
2625 Ok(h) => h,
2626 Err(e) => {
2627 if ignore_missing && e.kind() == io::ErrorKind::NotFound {
2628 ignored_missing_count += 1;
2629 continue;
2630 }
2631 read_errors += 1;
2632 if !status_only {
2633 out.flush()?;
2634 writeln!(err_out, "{}: {}", filename, e)?;
2635 writeln!(out, "{}: FAILED open or read", filename)?;
2636 }
2637 continue;
2638 }
2639 };
2640
2641 if actual.eq_ignore_ascii_case(expected_hash) {
2642 ok_count += 1;
2643 if !quiet && !status_only {
2644 writeln!(out, "{}: OK", filename)?;
2645 }
2646 } else {
2647 mismatch_count += 1;
2648 if !status_only {
2649 writeln!(out, "{}: FAILED", filename)?;
2650 }
2651 }
2652 }
2653
2654 Ok(CheckResult {
2655 ok: ok_count,
2656 mismatches: mismatch_count,
2657 format_errors,
2658 read_errors,
2659 ignored_missing: ignored_missing_count,
2660 })
2661}
2662
2663pub fn parse_check_line(line: &str) -> Option<(&str, &str)> {
2665 let rest = line
2667 .strip_prefix("MD5 (")
2668 .or_else(|| line.strip_prefix("SHA1 ("))
2669 .or_else(|| line.strip_prefix("SHA224 ("))
2670 .or_else(|| line.strip_prefix("SHA256 ("))
2671 .or_else(|| line.strip_prefix("SHA384 ("))
2672 .or_else(|| line.strip_prefix("SHA512 ("))
2673 .or_else(|| line.strip_prefix("BLAKE2b ("))
2674 .or_else(|| {
2675 if line.starts_with("BLAKE2b-") {
2677 let after = &line["BLAKE2b-".len()..];
2678 if let Some(sp) = after.find(" (") {
2679 if after[..sp].bytes().all(|b| b.is_ascii_digit()) {
2680 return Some(&after[sp + 2..]);
2681 }
2682 }
2683 }
2684 None
2685 });
2686 if let Some(rest) = rest {
2687 if let Some(paren_idx) = rest.find(") = ") {
2688 let filename = &rest[..paren_idx];
2689 let hash = &rest[paren_idx + 4..];
2690 return Some((hash, filename));
2691 }
2692 }
2693
2694 let line = line.strip_prefix('\\').unwrap_or(line);
2696
2697 if let Some(idx) = line.find(" ") {
2699 let hash = &line[..idx];
2700 let rest = &line[idx + 2..];
2701 return Some((hash, rest));
2702 }
2703 if let Some(idx) = line.find(" *") {
2705 let hash = &line[..idx];
2706 let rest = &line[idx + 2..];
2707 return Some((hash, rest));
2708 }
2709 None
2710}
2711
2712pub fn parse_check_line_tag(line: &str) -> Option<(&str, &str, Option<usize>)> {
2716 let paren_start = line.find(" (")?;
2717 let algo_part = &line[..paren_start];
2718 let rest = &line[paren_start + 2..];
2719 let paren_end = rest.find(") = ")?;
2720 let filename = &rest[..paren_end];
2721 let hash = &rest[paren_end + 4..];
2722
2723 let bits = if let Some(dash_pos) = algo_part.rfind('-') {
2725 algo_part[dash_pos + 1..].parse::<usize>().ok()
2726 } else {
2727 None
2728 };
2729
2730 Some((hash, filename, bits))
2731}
2732
2733#[inline]
2737fn read_full(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<usize> {
2738 let n = reader.read(buf)?;
2740 if n == buf.len() || n == 0 {
2741 return Ok(n);
2742 }
2743 let mut total = n;
2745 while total < buf.len() {
2746 match reader.read(&mut buf[total..]) {
2747 Ok(0) => break,
2748 Ok(n) => total += n,
2749 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
2750 Err(e) => return Err(e),
2751 }
2752 }
2753 Ok(total)
2754}
2755
2756const fn generate_hex_table() -> [[u8; 2]; 256] {
2759 let hex = b"0123456789abcdef";
2760 let mut table = [[0u8; 2]; 256];
2761 let mut i = 0;
2762 while i < 256 {
2763 table[i] = [hex[i >> 4], hex[i & 0xf]];
2764 i += 1;
2765 }
2766 table
2767}
2768
2769const HEX_TABLE: [[u8; 2]; 256] = generate_hex_table();
2770
2771pub(crate) fn hex_encode(bytes: &[u8]) -> String {
2774 let len = bytes.len() * 2;
2775 let mut hex = String::with_capacity(len);
2776 unsafe {
2778 let buf = hex.as_mut_vec();
2779 buf.set_len(len);
2780 hex_encode_to_slice(bytes, buf);
2781 }
2782 hex
2783}
2784
2785#[inline]
2788fn hex_encode_to_slice(bytes: &[u8], out: &mut [u8]) {
2789 unsafe {
2791 let ptr = out.as_mut_ptr();
2792 for (i, &b) in bytes.iter().enumerate() {
2793 let pair = *HEX_TABLE.get_unchecked(b as usize);
2794 *ptr.add(i * 2) = pair[0];
2795 *ptr.add(i * 2 + 1) = pair[1];
2796 }
2797 }
2798}