use std::fs::File;
use std::io::{self, BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use crate::descriptor::ExtentEntry;
pub(crate) struct MultiExtentReader {
extents: Vec<FlatExtent>,
pos: u64,
total_bytes: u64,
}
struct FlatExtent {
byte_start: u64,
byte_end: u64,
file_offset: u64,
file: Option<BufReader<File>>,
}
impl MultiExtentReader {
pub(crate) fn open(base_dir: &Path, extents: &[ExtentEntry]) -> io::Result<Self> {
let mut flat = Vec::with_capacity(extents.len());
let mut virt = 0u64;
for ext in extents {
let size_bytes = ext.size_sectors * 512;
let file = if ext.is_zero {
None
} else {
let path = crate::descriptor::resolve_extent_path(base_dir, ext.filename.as_ref())?;
Some(BufReader::new(File::open(&path).map_err(|e| {
io::Error::new(e.kind(), format!("{}: {e}", path.display()))
})?))
};
flat.push(FlatExtent {
byte_start: virt,
byte_end: virt + size_bytes,
file_offset: ext.file_byte_offset,
file,
});
virt += size_bytes;
}
Ok(MultiExtentReader {
extents: flat,
pos: 0,
total_bytes: virt,
})
}
}
impl Read for MultiExtentReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.pos >= self.total_bytes || buf.is_empty() {
return Ok(0);
}
let ext = self
.extents
.iter_mut()
.find(|e| e.byte_start <= self.pos && self.pos < e.byte_end)
.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "offset beyond extents"))?;
let offset_in_extent = self.pos - ext.byte_start;
let remaining_in_extent = (ext.byte_end - self.pos) as usize;
let remaining_total = (self.total_bytes - self.pos) as usize;
let to_read = buf.len().min(remaining_in_extent).min(remaining_total);
let n = if let Some(file) = &mut ext.file {
file.seek(SeekFrom::Start(ext.file_offset + offset_in_extent))?;
file.read(&mut buf[..to_read])?
} else {
buf[..to_read].fill(0);
to_read
};
self.pos += n as u64;
Ok(n)
}
}
impl Seek for MultiExtentReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::Current(n) => self.pos as i64 + n,
SeekFrom::End(n) => self.total_bytes as i64 + n,
};
if new_pos < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"seek before start",
));
}
self.pos = new_pos as u64;
Ok(self.pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ExtentEntry;
use std::io::Write as _;
fn two_sector_reader(dir: &std::path::Path) -> MultiExtentReader {
std::fs::File::create(dir.join("e.vmdk"))
.unwrap()
.write_all(&[7u8; 1024])
.unwrap();
let ext = ExtentEntry {
size_sectors: 2,
filename: Box::from("e.vmdk"),
file_byte_offset: 0,
is_zero: false,
};
MultiExtentReader::open(dir, &[ext]).unwrap()
}
#[test]
fn seek_current_end_and_read() {
let dir = tempfile::tempdir().unwrap();
let mut r = two_sector_reader(dir.path());
assert_eq!(r.seek(SeekFrom::Start(512)).unwrap(), 512);
assert_eq!(r.seek(SeekFrom::Current(-256)).unwrap(), 256);
assert_eq!(r.seek(SeekFrom::End(-100)).unwrap(), 1024 - 100);
let mut b = [0u8; 4];
r.read_exact(&mut b).unwrap();
assert_eq!(b, [7, 7, 7, 7]);
}
#[test]
fn seek_before_start_errors() {
let dir = tempfile::tempdir().unwrap();
let mut r = two_sector_reader(dir.path());
assert!(r.seek(SeekFrom::End(-99999)).is_err());
}
#[test]
fn read_past_end_returns_zero() {
let dir = tempfile::tempdir().unwrap();
let mut r = two_sector_reader(dir.path());
r.seek(SeekFrom::Start(1024)).unwrap();
let mut b = [0u8; 4];
assert_eq!(r.read(&mut b).unwrap(), 0);
r.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(r.read(&mut []).unwrap(), 0);
}
#[test]
fn missing_extent_file_errors() {
let dir = tempfile::tempdir().unwrap();
let ext = ExtentEntry {
size_sectors: 2,
filename: Box::from("nope.vmdk"),
file_byte_offset: 0,
is_zero: false,
};
assert!(MultiExtentReader::open(dir.path(), &[ext]).is_err());
}
#[test]
fn zero_extent_reads_zeros() {
let dir = tempfile::tempdir().unwrap();
let ext = ExtentEntry {
size_sectors: 2,
filename: Box::from(""),
file_byte_offset: 0,
is_zero: true,
};
let mut r = MultiExtentReader::open(dir.path(), &[ext]).unwrap();
let mut b = [0xFFu8; 8];
r.read_exact(&mut b).unwrap();
assert_eq!(b, [0u8; 8]);
}
}