use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use crate::parallel::{Panicked, Threads};
const ALIGN: u64 = 4096;
const ALIGN_BYTES: usize = 4096;
const ALIGN_DOWN: u64 = !(ALIGN - 1);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Extent {
pub file: usize,
pub offset: u64,
pub len: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Options {
pub threads: Threads,
pub window: u64,
pub max_gap: u64,
}
impl Options {
pub fn new(threads: Threads) -> Options {
Options {
threads,
window: 16 << 20,
max_gap: 1 << 20,
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ExtentError {
#[error("{path}: {source}")]
Io {
path: PathBuf,
source: Arc<std::io::Error>,
},
#[error("{path}: {len} bytes at {offset:#x} run past the end of the file at {end:#x}")]
PastEnd {
path: PathBuf,
offset: u64,
len: usize,
end: u64,
},
#[error("file {file} of {files}")]
NoFile {
file: usize,
files: usize,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Scanned {
pub windows: u64,
pub bytes: u64,
}
#[derive(Debug)]
struct Window {
file: usize,
start: u64,
end: u64,
extents: Vec<usize>,
}
fn windows(files: usize, extents: &[Extent], options: &Options) -> (Vec<Window>, Vec<(usize, ExtentError)>) {
let mut bad = Vec::new();
let mut order: Vec<(usize, u64, u64, usize)> = Vec::with_capacity(extents.len());
for (i, e) in extents.iter().enumerate() {
if e.file >= files {
bad.push((i, ExtentError::NoFile { file: e.file, files }));
continue;
}
let end = u64::try_from(e.len)
.ok()
.and_then(|len| e.offset.checked_add(len));
let aligned = end.and_then(|end| end.checked_next_multiple_of(ALIGN));
match aligned {
Some(end) => order.push((e.file, e.offset & ALIGN_DOWN, end, i)),
None => bad.push((
i,
ExtentError::PastEnd {
path: PathBuf::new(),
offset: e.offset,
len: e.len,
end: u64::MAX,
},
)),
}
}
order.sort_unstable();
let mut out: Vec<Window> = Vec::new();
for (file, start, end, i) in order {
if let Some(w) = out.last_mut()
&& w.file == file
&& start <= w.end.saturating_add(options.max_gap)
&& end.max(w.end).saturating_sub(w.start) <= options.window
{
w.end = w.end.max(end);
w.extents.push(i);
continue;
}
out.push(Window {
file,
start,
end,
extents: vec![i],
});
}
let long = options.window.saturating_mul(4);
out.sort_by_key(|w| std::cmp::Reverse(w.end.saturating_sub(w.start) > long));
(out, bad)
}
#[derive(Debug, Default)]
struct Aligned(Vec<u8>);
impl Aligned {
fn get(&mut self, len: usize) -> Option<&mut [u8]> {
let total = len.checked_add(ALIGN_BYTES)?;
if self.0.len() < total {
self.0 = vec![0; total];
}
let off = self.0.as_ptr().align_offset(ALIGN_BYTES);
self.0.get_mut(off..off.checked_add(len)?)
}
}
#[derive(Debug)]
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
struct Open {
file: std::fs::File,
unbuffered: bool,
}
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
fn open(path: &Path, unbuffered: bool) -> std::io::Result<Open> {
let mut o = std::fs::OpenOptions::new();
o.read(true);
#[cfg(windows)]
if unbuffered {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_NO_BUFFERING: u32 = 0x2000_0000;
o.custom_flags(FILE_FLAG_NO_BUFFERING);
}
Ok(Open {
file: o.open(path)?,
unbuffered: cfg!(windows) && unbuffered,
})
}
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
fn read_at(file: &std::fs::File, buf: &mut [u8], at: u64) -> std::io::Result<usize> {
#[cfg(windows)]
{
std::os::windows::fs::FileExt::seek_read(file, buf, at)
}
#[cfg(unix)]
{
std::os::unix::fs::FileExt::read_at(file, buf, at)
}
}
fn fill(f: &Open, buf: &mut [u8], at: u64) -> std::io::Result<usize> {
let mut got = 0usize;
while let Some(rest) = buf.get_mut(got..) {
if rest.is_empty() {
break;
}
let pos = u64::try_from(got).ok().and_then(|g| at.checked_add(g));
let Some(pos) = pos else { break };
let n = read_at(&f.file, rest, pos)?;
if n == 0 {
break;
}
got = got.saturating_add(n);
if f.unbuffered && !(n as u64).is_multiple_of(ALIGN) {
break;
}
}
Ok(got)
}
pub fn scan(
files: &[PathBuf],
extents: &[Extent],
options: &Options,
visit: impl Fn(usize, Result<&mut [u8], ExtentError>) + Sync,
) -> Result<Scanned, Panicked> {
let (work, bad) = windows(files.len(), extents, options);
let call = |i: usize, r: Result<&mut [u8], ExtentError>| {
catch_unwind(AssertUnwindSafe(|| visit(i, r))).map_err(|p| crate::parallel::panicked(i, &*p))
};
for (i, e) in bad {
let e = match e {
ExtentError::PastEnd { offset, len, end, .. } => ExtentError::PastEnd {
path: extents
.get(i)
.and_then(|x| files.get(x.file))
.cloned()
.unwrap_or_default(),
offset,
len,
end,
},
e => e,
};
call(i, Err(e))?;
}
let next = AtomicUsize::new(0);
let failed = AtomicBool::new(false);
let (windows_read, bytes_read) = (AtomicU64::new(0), AtomicU64::new(0));
let worker = || -> Result<(), Panicked> {
let mut buf = Aligned::default();
let mut opened: Vec<Option<Result<Open, Arc<std::io::Error>>>> = Vec::new();
opened.resize_with(files.len(), || None);
while !failed.load(Ordering::Relaxed) {
let Some(w) = work.get(next.fetch_add(1, Ordering::Relaxed)) else {
break;
};
let path = files.get(w.file).map(PathBuf::as_path).unwrap_or(Path::new(""));
let slot = opened.get_mut(w.file);
let f = match slot {
Some(s) => s.get_or_insert_with(|| open(path, true).map_err(Arc::new)),
None => continue,
};
let span = usize::try_from(w.end.saturating_sub(w.start))
.ok()
.and_then(|n| buf.get(n));
let read = match (f.as_ref(), span) {
(Ok(f), Some(span)) => match fill(f, span, w.start) {
Err(e) if f.unbuffered && e.kind() == std::io::ErrorKind::InvalidInput => {
match open(path, false) {
Ok(b) => {
let got = fill(&b, span, w.start);
if let Some(s) = opened.get_mut(w.file) {
*s = Some(Ok(b));
}
got.map(|n| (n, span))
}
Err(e) => Err(e),
}
}
r => r.map(|n| (n, span)),
}
.map_err(Arc::new),
(Err(e), _) => Err(Arc::clone(e)),
(Ok(_), None) => Err(Arc::new(std::io::Error::other(
"a window larger than memory can address",
))),
};
match read {
Ok((got, span)) => {
windows_read.fetch_add(1, Ordering::Relaxed);
bytes_read.fetch_add(got as u64, Ordering::Relaxed);
let file_end = w.start.saturating_add(got as u64);
for &i in &w.extents {
let Some(e) = extents.get(i) else { continue };
let at = usize::try_from(e.offset.saturating_sub(w.start)).unwrap_or(usize::MAX);
let bytes = at
.checked_add(e.len)
.filter(|&end| end <= got)
.and_then(|end| span.get_mut(at..end));
let r = bytes.ok_or_else(|| ExtentError::PastEnd {
path: path.to_path_buf(),
offset: e.offset,
len: e.len,
end: file_end,
});
if let Err(p) = call(i, r) {
failed.store(true, Ordering::Relaxed);
return Err(p);
}
}
}
Err(source) => {
for &i in &w.extents {
let r = Err(ExtentError::Io {
path: path.to_path_buf(),
source: Arc::clone(&source),
});
if let Err(p) = call(i, r) {
failed.store(true, Ordering::Relaxed);
return Err(p);
}
}
}
}
}
Ok(())
};
let workers = options.threads.get().min(work.len()).max(1);
let results: Vec<Result<(), Panicked>> = if workers == 1 {
vec![worker()]
} else {
std::thread::scope(|s| {
let handles: Vec<_> = (0..workers).map(|_| s.spawn(worker)).collect();
handles.into_iter().map(|h| h.join().unwrap_or(Ok(()))).collect()
})
};
if let Some(p) = results.into_iter().filter_map(Result::err).min_by_key(|p| p.item) {
return Err(p);
}
Ok(Scanned {
windows: windows_read.load(Ordering::Relaxed),
bytes: bytes_read.load(Ordering::Relaxed),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(window: u64, max_gap: u64) -> Options {
Options {
threads: Threads::ONE,
window,
max_gap,
}
}
#[test]
fn neighbours_join_and_a_gap_splits() {
let e = |offset, len| Extent { file: 0, offset, len };
let extents = [e(5000, 100), e(0, 100), e(1 << 20, 10)];
let (w, bad) = windows(1, &extents, &opts(16 << 20, 8192));
assert!(bad.is_empty());
assert_eq!(w.len(), 2);
assert_eq!(
(w[0].start, w[0].end, w[0].extents.clone()),
(0, 8192, vec![1, 0])
);
assert_eq!((w[1].start, w[1].end), (1 << 20, (1 << 20) + 4096));
}
#[test]
fn a_window_never_exceeds_its_size_unless_one_extent_does() {
let e = |offset, len| Extent { file: 0, offset, len };
let extents = [e(0, 4096), e(4096, 4096), e(8192, 4096), e(12288, 40000)];
let (w, _) = windows(1, &extents, &opts(8192, 0));
let spans: Vec<_> = w.iter().map(|w| (w.start, w.end)).collect();
assert_eq!(spans, [(12288, 53248), (0, 8192), (8192, 12288)]);
}
#[test]
fn files_do_not_join_and_unknown_files_are_refused() {
let extents = [
Extent {
file: 1,
offset: 0,
len: 10,
},
Extent {
file: 0,
offset: 0,
len: 10,
},
Extent {
file: 2,
offset: 0,
len: 10,
},
Extent {
file: 0,
offset: u64::MAX - 1,
len: 10,
},
];
let (w, bad) = windows(2, &extents, &opts(16 << 20, 1 << 20));
assert_eq!(w.iter().map(|w| w.file).collect::<Vec<_>>(), [0, 1]);
assert!(matches!(bad[0], (2, ExtentError::NoFile { file: 2, files: 2 })));
assert!(matches!(bad[1], (3, ExtentError::PastEnd { .. })));
}
}