use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use anyhow::{Context, Result};
use memmap2::Mmap;
pub(crate) struct Mapped {
map: Mmap,
len: u64,
}
impl Mapped {
pub(crate) fn get(&self, offset: u64, len: u64) -> Option<&[u8]> {
let end = offset.checked_add(len)?;
if end > self.len {
return None;
}
let (a, b) = (usize::try_from(offset).ok()?, usize::try_from(end).ok()?);
self.map.get(a..b)
}
pub(crate) fn len(&self) -> u64 {
self.len
}
}
pub(crate) struct ArchiveMap {
path: PathBuf,
current: RwLock<Option<Arc<Mapped>>>,
}
impl ArchiveMap {
pub(crate) fn new(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
current: RwLock::new(None),
}
}
pub(crate) fn snapshot(&self) -> Result<Arc<Mapped>> {
let on_disk = match std::fs::metadata(&self.path) {
Ok(m) => m.len(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0,
Err(e) => {
return Err(e).with_context(|| format!("stat {}", self.path.display()));
}
};
{
let cur = self
.current
.read()
.map_err(|_| anyhow::anyhow!("archive map poisoned"))?;
if let Some(m) = cur.as_ref()
&& m.len >= on_disk
{
return Ok(Arc::clone(m));
}
}
let mut cur = self
.current
.write()
.map_err(|_| anyhow::anyhow!("archive map poisoned"))?;
if let Some(m) = cur.as_ref()
&& m.len >= on_disk
{
return Ok(Arc::clone(m));
}
let mapped = if on_disk == 0 {
Arc::new(Mapped {
map: Self::empty_map()?,
len: 0,
})
} else {
let f = File::open(&self.path)
.with_context(|| format!("opening {} to map", self.path.display()))?;
let map = unsafe { Mmap::map(&f) }
.with_context(|| format!("mapping {}", self.path.display()))?;
let len = map.len() as u64;
Arc::new(Mapped { map, len })
};
*cur = Some(Arc::clone(&mapped));
Ok(mapped)
}
fn empty_map() -> Result<Mmap> {
memmap2::MmapOptions::new()
.len(1)
.map_anon()
.map(memmap2::MmapMut::make_read_only)
.context("mapping an empty archive")?
.context("freezing the empty archive mapping")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("archive-map-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d.join("objects.pack")
}
#[test]
fn an_extent_reads_back_as_the_bytes_that_were_written() {
let p = tmp("basic");
std::fs::write(&p, b"0123456789abcdef").unwrap();
let m = ArchiveMap::new(&p);
let snap = m.snapshot().unwrap();
assert_eq!(snap.get(0, 4).unwrap(), b"0123");
assert_eq!(snap.get(10, 6).unwrap(), b"abcdef");
assert_eq!(snap.len(), 16);
}
#[test]
fn an_extent_past_the_end_is_none_not_a_short_slice() {
let p = tmp("past");
std::fs::write(&p, b"0123456789").unwrap();
let snap = ArchiveMap::new(&p).snapshot().unwrap();
assert!(snap.get(8, 4).is_none(), "8..12 of a 10-byte file must refuse");
assert!(snap.get(10, 1).is_none());
assert!(snap.get(u64::MAX, 1).is_none(), "an overflowing extent must refuse");
assert_eq!(snap.get(6, 4).unwrap(), b"6789", "the exact tail is fine");
}
#[test]
fn growth_is_seen_by_the_next_snapshot_and_the_old_one_still_works() {
let p = tmp("grow");
std::fs::write(&p, b"AAAA").unwrap();
let m = ArchiveMap::new(&p);
let first = m.snapshot().unwrap();
assert_eq!(first.len(), 4);
let mut f = std::fs::OpenOptions::new().append(true).open(&p).unwrap();
f.write_all(b"BBBB").unwrap();
f.sync_all().unwrap();
let second = m.snapshot().unwrap();
assert_eq!(second.len(), 8, "the remap did not pick up the append");
assert_eq!(second.get(4, 4).unwrap(), b"BBBB");
assert_eq!(first.get(0, 4).unwrap(), b"AAAA");
assert!(first.get(4, 4).is_none(), "the old map must not see the growth");
}
#[test]
fn an_absent_or_empty_archive_maps_to_nothing_rather_than_failing() {
let p = tmp("empty");
let snap = ArchiveMap::new(&p).snapshot().unwrap();
assert_eq!(snap.len(), 0);
assert!(snap.get(0, 1).is_none());
std::fs::write(&p, b"").unwrap();
let snap = ArchiveMap::new(&p).snapshot().unwrap();
assert_eq!(snap.len(), 0);
assert!(snap.get(0, 0).is_some(), "a zero-length extent at 0 is legal");
}
#[test]
fn a_second_snapshot_without_growth_reuses_the_mapping() {
let p = tmp("reuse");
std::fs::write(&p, b"0123456789").unwrap();
let m = ArchiveMap::new(&p);
let a = m.snapshot().unwrap();
let b = m.snapshot().unwrap();
assert!(Arc::ptr_eq(&a, &b), "the mapping was rebuilt for no reason");
}
}