use std::fs::File;
use std::io;
use std::path::Path;
use fs4::FileExt;
use memmap2::Mmap;
use crate::FileData;
#[must_use = "the mapped FileData must be retained to keep the advisory lock alive"]
pub fn map_file(path: impl AsRef<Path>) -> io::Result<FileData> {
let path = path.as_ref();
let file = File::open(path)?;
let metadata = file.metadata()?;
if metadata.len() == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("file is empty: {}", path.display()),
));
}
match FileExt::try_lock_shared(&file) {
Ok(()) => {} Err(fs4::TryLockError::WouldBlock) => {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
format!("file is locked by another process: {}", path.display()),
));
}
Err(fs4::TryLockError::Error(e)) => {
return Err(io::Error::new(
e.kind(),
format!("failed to acquire shared lock on {}: {e}", path.display()),
));
}
}
let mmap = unsafe { Mmap::map(&file)? };
Ok(FileData::Mapped(mmap, file))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
#[test]
fn map_file_reads_content() {
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
tmp.write_all(b"hello mmap").expect("failed to write");
tmp.flush().expect("failed to flush");
let data = map_file(tmp.path()).expect("map_file failed");
assert_eq!(&*data, b"hello mmap");
}
#[test]
fn map_file_rejects_empty() {
let tmp = NamedTempFile::new().expect("failed to create temp file");
let err = map_file(tmp.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(
err.to_string().contains("empty"),
"error should mention 'empty': {err}"
);
}
#[test]
fn map_file_rejects_nonexistent() {
let err = map_file("/tmp/mmap_guard_does_not_exist_12345").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn map_file_returns_mapped_variant() {
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
tmp.write_all(b"data").expect("failed to write");
tmp.flush().expect("failed to flush");
let data = map_file(tmp.path()).expect("map_file failed");
assert!(
matches!(data, FileData::Mapped(..)),
"expected Mapped variant"
);
}
#[cfg(unix)]
#[test]
fn map_file_rejects_permission_denied() {
use std::os::unix::fs::PermissionsExt;
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
tmp.write_all(b"secret").expect("failed to write");
tmp.flush().expect("failed to flush");
let perms = std::fs::Permissions::from_mode(0o000);
std::fs::set_permissions(tmp.path(), perms).expect("failed to chmod");
let err = map_file(tmp.path()).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::PermissionDenied,
"expected PermissionDenied, got: {err}"
);
let restore = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(tmp.path(), restore).expect("failed to restore permissions");
}
#[cfg(unix)]
#[test]
fn map_file_returns_would_block_when_exclusively_locked() {
use std::process::{Command, Stdio};
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
tmp.write_all(b"locked data").expect("failed to write");
tmp.flush().expect("failed to flush");
let path = tmp.path().to_owned();
let mut child = Command::new("python3")
.arg("-c")
.arg(
"import fcntl, os, sys; \
fd = os.open(sys.argv[1], os.O_RDONLY); \
fcntl.flock(fd, fcntl.LOCK_EX); \
sys.stdout.write('locked\\n'); sys.stdout.flush(); \
sys.stdin.readline()",
)
.arg(&path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn lock holder");
let stdout = child.stdout.as_mut().expect("missing stdout");
let mut buf = [0_u8; 7]; io::Read::read_exact(stdout, &mut buf).expect("child did not signal");
let err = map_file(&path).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::WouldBlock,
"expected WouldBlock, got: {err}"
);
assert!(
err.to_string().contains(&path.display().to_string()),
"error should mention the file path: {err}"
);
drop(child.stdin.take());
child.wait().expect("failed to reap child");
}
}