#![cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::reader::RangeReader;
enum Src {
Bytes(Vec<u8>),
File(PathBuf),
}
struct Piece {
start: u64,
len: u64,
src: Src,
release: Option<u8>,
}
#[derive(Default)]
pub(crate) struct ImageBuilder {
pieces: Vec<Piece>,
at: u64,
}
impl ImageBuilder {
pub(crate) fn at(&self) -> u64 {
self.at
}
pub(crate) fn bytes(&mut self, b: &[u8]) {
if b.is_empty() {
return;
}
self.pieces.push(Piece {
start: self.at,
len: b.len() as u64,
src: Src::Bytes(b.to_vec()),
release: None,
});
self.at += b.len() as u64;
}
pub(crate) fn file(&mut self, path: &Path, len: u64, release: Option<u8>) {
if len == 0 {
return;
}
self.pieces.push(Piece {
start: self.at,
len,
src: Src::File(path.to_path_buf()),
release,
});
self.at += len;
}
pub(crate) fn patch_head(&mut self, head: Vec<u8>) {
if let Some(p) = self.pieces.first_mut() {
debug_assert_eq!(p.len, head.len() as u64);
p.src = Src::Bytes(head);
}
}
pub(crate) fn finish(self) -> SpillImage {
let n = self.pieces.len();
SpillImage {
len: self.at,
pieces: self.pieces,
open: Mutex::new(HashMap::new()),
track: Mutex::new(Track {
needed: vec![0; n],
copied: vec![0; n],
ready: vec![false; n],
gone: vec![false; n],
}),
released: std::sync::atomic::AtomicBool::new(false),
}
}
}
pub(crate) struct SpillImage {
len: u64,
pieces: Vec<Piece>,
open: Mutex<HashMap<usize, Arc<File>>>,
track: Mutex<Track>,
released: std::sync::atomic::AtomicBool,
}
struct Track {
needed: Vec<u64>,
copied: Vec<u64>,
ready: Vec<bool>,
gone: Vec<bool>,
}
fn read_exact_at(f: &File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
f.read_exact_at(buf, offset)
}
#[cfg(not(any(unix, windows)))]
{
let _ = (f, buf, offset);
Err(std::io::ErrorKind::Unsupported.into())
}
#[cfg(windows)]
{
use std::os::windows::fs::FileExt;
let mut done = 0usize;
while done < buf.len() {
let n = f.seek_read(&mut buf[done..], offset + done as u64)?;
if n == 0 {
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
done += n;
}
Ok(())
}
}
impl SpillImage {
fn handle(&self, i: usize, path: &Path) -> std::io::Result<Arc<File>> {
let mut open = self
.open
.lock()
.map_err(|_| std::io::Error::other("poisoned"))?;
if let Some(f) = open.get(&i) {
return Ok(f.clone());
}
let f = Arc::new(File::open(path)?);
open.insert(i, f.clone());
Ok(f)
}
pub(crate) fn any_released(&self) -> bool {
self.released.load(std::sync::atomic::Ordering::Relaxed)
}
fn remove(&self, i: usize, t: &mut Track) {
if t.gone[i] {
return;
}
t.gone[i] = true;
self.released
.store(true, std::sync::atomic::Ordering::Relaxed);
if let Ok(mut open) = self.open.lock() {
open.remove(&i);
}
if let Src::File(path) = &self.pieces[i].src {
let _ = std::fs::remove_file(path);
}
}
fn spans(&self, off: u64, len: u64) -> Vec<(usize, u64)> {
let end = off.saturating_add(len);
let mut i = self.pieces.partition_point(|p| p.start + p.len <= off);
let mut at = off;
let mut out = Vec::new();
while at < end && i < self.pieces.len() {
let p = &self.pieces[i];
let n = (p.start + p.len).min(end) - at;
out.push((i, n));
at += n;
i += 1;
}
out
}
pub(crate) fn need(&self, off: u64, len: u64) {
if let Ok(mut t) = self.track.lock() {
for (i, n) in self.spans(off, len) {
t.needed[i] += n;
}
}
}
pub(crate) fn consumed(&self, off: u64, len: u64) {
let Ok(mut t) = self.track.lock() else { return };
for (i, n) in self.spans(off, len) {
t.copied[i] += n;
if t.needed[i] > 0 && t.copied[i] >= t.needed[i] {
t.ready[i] = true;
}
}
}
pub(crate) fn release(&self, kind: u8) {
let Ok(mut t) = self.track.lock() else { return };
for i in 0..self.pieces.len() {
if self.pieces[i].release == Some(kind) {
t.ready[i] = true;
}
}
}
pub(crate) fn has_ready(&self) -> bool {
self.track
.lock()
.map(|t| (0..t.ready.len()).any(|i| t.ready[i] && !t.gone[i]))
.unwrap_or(false)
}
pub(crate) fn delete_ready(&self) {
let Ok(mut t) = self.track.lock() else { return };
for i in 0..self.pieces.len() {
if t.ready[i] {
self.remove(i, &mut t);
}
}
}
}
impl RangeReader for SpillImage {
fn len(&self) -> u64 {
self.len
}
fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
let end = offset
.checked_add(len)
.filter(|&e| e <= self.len)
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "past the image")
})?;
let mut out = vec![0u8; len as usize];
let mut i = self.pieces.partition_point(|p| p.start + p.len <= offset);
let mut at = offset;
while at < end {
let p = self
.pieces
.get(i)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "gap"))?;
let from = at - p.start;
let n = (p.len - from).min(end - at) as usize;
let dst = &mut out[(at - offset) as usize..(at - offset) as usize + n];
match &p.src {
Src::Bytes(b) => dst.copy_from_slice(&b[from as usize..from as usize + n]),
Src::File(path) => read_exact_at(&*self.handle(i, path)?, dst, from)?,
}
at += n as u64;
i += 1;
}
Ok(out)
}
}
#[derive(Clone)]
pub struct SharedImage(pub(crate) Arc<SpillImage>);
impl RangeReader for SharedImage {
fn len(&self) -> u64 {
self.0.len()
}
fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
self.0.read_at(offset, len)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_across_pieces_and_releases_files() {
let dir = std::env::temp_dir().join(format!("rete-spill-image-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("a.bin");
std::fs::write(&f, b"0123456789").unwrap();
let mut b = ImageBuilder::default();
b.bytes(b"abc");
b.file(&f, 10, Some(7));
b.bytes(b"XYZ");
let img = b.finish();
assert_eq!(img.len(), 16);
assert_eq!(img.read_at(0, 16).unwrap(), b"abc0123456789XYZ");
assert_eq!(img.read_at(2, 3).unwrap(), b"c01");
assert_eq!(img.read_at(11, 4).unwrap(), b"89XY");
assert!(img.read_at(10, 7).is_err());
img.release(6);
assert!(f.exists());
assert!(!img.any_released());
img.need(3, 4);
img.need(9, 4);
img.consumed(9, 4);
assert!(f.exists());
img.consumed(3, 4);
assert!(f.exists() && img.has_ready() && !img.any_released());
img.delete_ready();
assert!(!f.exists());
assert!(img.any_released() && !img.has_ready());
let _ = std::fs::remove_dir_all(&dir);
}
}