use std::fmt;
use std::fs::File;
use std::io;
use std::path::Path;
use crate::sys;
pub const DEFAULT_SPAN: usize = 4 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WindowError {
#[error("{operation} failed: {source}")]
Os {
operation: &'static str,
#[source]
source: io::Error,
},
#[error("bytes {at}..{end} were asked for and the file is {len} bytes long")]
OutOfBounds {
at: u64,
end: u64,
len: u64,
},
#[error("covering that range needs {wanted} bytes in one view and this window reserved {span}")]
TooLarge {
wanted: usize,
span: usize,
},
}
type Result<T> = std::result::Result<T, WindowError>;
fn os(operation: &'static str) -> impl FnOnce(io::Error) -> WindowError {
move |source| WindowError::Os { operation, source }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct View {
at: u64,
len: u64,
}
impl View {
fn covers(self, at: u64, len: u64) -> bool {
let Some(want_end) = at.checked_add(len) else {
return false;
};
let Some(have_end) = self.at.checked_add(self.len) else {
return false;
};
at >= self.at && want_end <= have_end
}
}
pub struct Window {
reservation: sys::Reservation,
backing: Option<sys::Backing>,
file: File,
len: u64,
view: Option<View>,
slides: u64,
}
impl Window {
pub fn open(path: &Path) -> Result<Self> {
let file = File::open(path).map_err(os("opening the file"))?;
Self::with_span(file, DEFAULT_SPAN)
}
pub fn with_span(file: File, span: usize) -> Result<Self> {
let len = file.metadata().map_err(os("measuring the file"))?.len();
let unit = sys::granularity();
let span = round_up(span.max(1), unit).ok_or(WindowError::TooLarge {
wanted: span,
span: usize::MAX,
})?;
let reservation = sys::Reservation::new(span).map_err(os("reserving address space"))?;
let backing = if len == 0 {
None
} else {
Some(sys::Backing::new(&file).map_err(os("preparing the file for mapping"))?)
};
Ok(Self {
reservation,
backing,
file,
len,
view: None,
slides: 0,
})
}
#[must_use]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn file(&self) -> &File {
&self.file
}
#[must_use]
pub fn span(&self) -> usize {
self.reservation.span()
}
#[must_use]
pub fn slides(&self) -> u64 {
self.slides
}
pub fn range(&mut self, at: u64, len: usize) -> Result<&[u8]> {
let wide = len as u64;
let end = at.saturating_add(wide);
if end > self.len {
return Err(WindowError::OutOfBounds {
at,
end,
len: self.len,
});
}
let unit = sys::granularity() as u64;
let skew = usize::try_from(at % unit).unwrap_or(usize::MAX);
let needed = skew.saturating_add(len);
if needed > self.span() {
return Err(WindowError::TooLarge {
wanted: needed,
span: self.span(),
});
}
if !self.view.is_some_and(|view| view.covers(at, wide)) {
self.slide_to(at)?;
}
let Some(view) = self.view else {
debug_assert_eq!(len, 0);
return Ok(&[]);
};
let offset = usize::try_from(at - view.at).unwrap_or(0);
let bytes = unsafe {
std::slice::from_raw_parts(self.reservation.base().as_ptr().add(offset), len)
};
Ok(bytes)
}
fn slide_to(&mut self, at: u64) -> Result<()> {
let Some(backing) = self.backing.as_ref() else {
return Ok(());
};
let unit = sys::granularity() as u64;
let start = at - (at % unit);
let remaining = self.len - start;
let span = self.reservation.span();
let available = usize::try_from(remaining).unwrap_or(usize::MAX);
let view_len = round_up(available.min(span), sys::page())
.unwrap_or(span)
.min(span);
if view_len == 0 {
self.view = None;
return self
.reservation
.unmap()
.map_err(os("unmapping the previous view"));
}
self.view = None;
self.reservation
.unmap()
.map_err(os("unmapping the previous view"))?;
self.reservation
.map(backing, start, view_len)
.map_err(os("mapping the file into the reservation"))?;
self.view = Some(View {
at: start,
len: view_len as u64,
});
self.slides += 1;
Ok(())
}
#[must_use]
pub fn address(&self) -> *const u8 {
self.reservation.base().as_ptr()
}
#[must_use]
pub fn mapped(&self) -> Option<(u64, usize)> {
self.view
.map(|view| (view.at, usize::try_from(view.len).unwrap_or(usize::MAX)))
}
}
impl fmt::Debug for Window {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Window")
.field("len", &self.len)
.field("span", &self.span())
.field("view", &self.view)
.field("slides", &self.slides)
.finish_non_exhaustive()
}
}
fn round_up(value: usize, unit: usize) -> Option<usize> {
debug_assert!(
unit != 0,
"an alignment of zero is not something a platform reports"
);
let over = value % unit;
if over == 0 {
return Some(value);
}
value.checked_add(unit - over)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounding_up_leaves_exact_multiples_alone() {
assert_eq!(round_up(0, 4096), Some(0));
assert_eq!(round_up(4096, 4096), Some(4096));
assert_eq!(round_up(1, 4096), Some(4096));
assert_eq!(round_up(4097, 4096), Some(8192));
assert_eq!(round_up(usize::MAX, 4096), None);
}
#[test]
fn a_view_covers_a_range_inside_it_and_nothing_else() {
let view = View { at: 100, len: 50 };
assert!(view.covers(100, 50));
assert!(view.covers(120, 10));
assert!(view.covers(150, 0));
assert!(!view.covers(99, 1));
assert!(!view.covers(150, 1));
assert!(!view.covers(140, 11));
assert!(!view.covers(u64::MAX, 2));
}
}