blob: f90282279c9b010144e0ec86c2feb97a5d8dc282 [file] [edit]
//! Helper functions for efficient IO.
#[cfg(feature = "mmap")]
use std::fs::File;
use std::io;
#[cfg(feature = "mmap")]
const MINIMUM_MMAP_SIZE: u64 = 16 * 1024; // 16 KiB
pub(crate) fn copy_wide(mut reader: impl io::Read, hasher: &mut crate::Hasher) -> io::Result<u64> {
let mut buffer = [0; 65536];
let mut total = 0;
loop {
match reader.read(&mut buffer) {
Ok(0) => return Ok(total),
Ok(n) => {
hasher.update(&buffer[..n]);
total += n as u64;
}
// see test_update_reader_interrupted
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
// Try to mmap a file, if it looks like a good idea. Return None if mmap fails, or if the file is
// short enough that it's not worth it.
//
// SAFETY: Mmaps are fundamentally unsafe, because you can call invariant-checking functions like
// str::from_utf8 on them and then have them change out from under you. Letting a safe caller get
// their hands on an mmap, or even a &[u8] that's backed by an mmap, is unsound. However, because
// this function is crate-private, we can guarantee that all can ever happen in the event of a race
// condition is that we either hash nonsense bytes or crash with SIGBUS or similar, neither of
// which should risk memory corruption in a safe caller.
//
// PARANOIA: But a data race...is a data race...is a data race...right? Even if we know that no
// platform in the "real world" is ever going to do anything other than compute the "wrong answer"
// if we race on this mmap while we hash it, aren't we still supposed to feel bad about doing this?
// Well, maybe. This is IO, and IO gets special carve-outs in the memory model. Consider a
// memory-mapped register that returns random 32-bit words. (This is actually realistic if you have
// a hardware RNG.) It's probably sound to construct a *const i32 pointing to that register and do
// some raw pointer reads from it. Those reads should be volatile if you don't want the compiler to
// coalesce them, but either way the compiler isn't allowed to just _go nuts_ and insert
// should-never-happen branches to wipe your hard drive if two adjacent reads happen to give
// different values. As far as I'm aware, there's no such thing as a read that's allowed if it's
// volatile but prohibited if it's not (unlike atomics). As mentioned above, it's not ok to
// construct a safe &i32 to the register if you're going to leak that reference to unknown callers.
// But if you "know what you're doing," I don't think *const i32 and &i32 are fundamentally
// different here. Feedback needed.
#[cfg(feature = "mmap")]
pub(crate) fn maybe_mmap_file(file: &File) -> io::Result<Option<memmap2::Mmap>> {
let metadata = file.metadata()?;
let file_size = metadata.len();
if !metadata.is_file() {
// Not a real file.
Ok(None)
} else if file_size < MINIMUM_MMAP_SIZE {
// Mapping small files is not worth it, and some special files that can't be mapped report
// a size of zero.
Ok(None)
} else {
// If the mmap itself fails (as opposed to opening the File previously, or reading its
// metadata above), swallow the error and return Ok(None).
Ok(unsafe { memmap2::Mmap::map(file) }.ok())
}
}
#[cfg(all(test, feature = "mmap"))]
mod test {
use super::*;
use std::io;
use std::io::prelude::*;
#[test]
fn test_maybe_mmap_current_exe() -> io::Result<()> {
// The current executable should always be a regular file larger than 16 KiB, so mmap
// should ~always succeed. (A filesystem might not support mmap at all, but we don't test
// any of those in CI.)
let exe_file = File::open(std::env::current_exe()?)?;
assert!(exe_file.metadata()?.len() > MINIMUM_MMAP_SIZE);
let mmap = maybe_mmap_file(&exe_file)?.expect("maybe_mmap_file should return Some");
// Mainly we're testing that we got `Some` above, but go ahead and read the mmap just to
// make sure it doesn't bus fault or anything like that.
assert_eq!(
crate::hash(&mmap),
crate::Hasher::new().update_reader(&exe_file)?.finalize(),
);
Ok(())
}
#[test]
fn test_maybe_mmap_small_file() -> io::Result<()> {
// Create a file smaller than 16 KiB. `maybe_mmap_file` returns `None` because of its size.
let mut f = tempfile::NamedTempFile::new()?;
f.write_all(b"hello world")?;
f.flush()?;
assert!(maybe_mmap_file(&File::open(f.path())?)?.is_none());
Ok(())
}
#[cfg(target_os = "linux")]
#[test]
fn test_unmappable_linux() -> io::Result<()> {
// I'm not aware of any similarly unmappable paths on macOS or Windows, so this test is
// Linux-only for now.
let unmappable_path = "/sys/kernel/btf/vmlinux";
let mut unmappable_file = File::open(unmappable_path)?;
// The file is large enough to attempt mmapping.
assert!(unmappable_file.metadata()?.len() > MINIMUM_MMAP_SIZE);
// We're allowed to read the file.
assert_eq!(unmappable_file.read(&mut [0])?, 1);
// But mmapping the file fails.
unsafe { memmap2::Mmap::map(&unmappable_file) }.unwrap_err();
// `maybe_mmap_file` swallows that error and returns `None`.
assert!(maybe_mmap_file(&unmappable_file)?.is_none());
Ok(())
}
}