swallow mmap errors
diff --git a/src/io.rs b/src/io.rs
index 7e8e154..f902822 100644
--- a/src/io.rs
+++ b/src/io.rs
@@ -1,10 +1,13 @@
 //! Helper functions for efficient IO.
 
-#[cfg(feature = "std")]
-pub(crate) fn copy_wide(
-    mut reader: impl std::io::Read,
-    hasher: &mut crate::Hasher,
-) -> std::io::Result<u64> {
+#[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 {
@@ -15,15 +18,14 @@
                 total += n as u64;
             }
             // see test_update_reader_interrupted
-            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
+            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
             Err(e) => return Err(e),
         }
     }
 }
 
-// Mmap a file, if it looks like a good idea. Return None in cases where we know mmap will fail, or
-// if the file is short enough that mmapping isn't worth it. However, if we do try to mmap and it
-// fails, return the error.
+// 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
@@ -47,18 +49,71 @@
 // 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: &std::fs::File) -> std::io::Result<Option<memmap2::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 < 16 * 1024 {
+    } 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 {
-        let map = unsafe { memmap2::Mmap::map(file)? };
-        Ok(Some(map))
+        // 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(())
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 67d4ef4..6484cfb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -135,6 +135,7 @@
 #[cfg(feature = "traits-preview")]
 pub mod traits;
 
+#[cfg(feature = "std")]
 mod io;
 mod join;