use std::{
fs::File,
io::{self, BufRead, Read, Result as IoResult, Seek, SeekFrom, Write},
os::unix::fs::MetadataExt,
path::Path,
};
pub trait Backend: Read + Write + Seek {}
impl<T: Read + Write + Seek> Backend for T {}
pub struct BlockReader<T: Backend> {
inner: T,
block: Vec<u8>,
idx: usize,
dirty: bool,
rw: bool,
}
impl BlockReader<File> {
pub fn open(path: &Path, rw: bool) -> IoResult<Self> {
let file = File::options().read(true).write(rw).open(path)?;
let bs = file.metadata()?.blksize() as usize;
Ok(BlockReader::new(file, bs, rw))
}
}
impl<T: Backend> BlockReader<T> {
pub fn new(inner: T, bs: usize, rw: bool) -> Self {
let block = vec![0u8; bs];
Self {
inner,
block,
idx: bs,
dirty: false,
rw,
}
}
pub fn write_enabled(&self) -> bool {
self.rw
}
fn refill(&mut self) -> IoResult<()> {
if self.dirty {
panic!("Cannot refill dirty BlockReader");
}
self.block.fill(0u8);
let mut num = 0;
while num < self.block.len() {
match self.inner.read(&mut self.block[num..])? {
0 => break,
n => num += n,
}
}
if num < self.block.len() {
log::error!("BlockReader::refill(): num={num}, eof?");
}
self.idx = 0;
Ok(())
}
fn buffered(&self) -> usize {
self.block.len() - self.idx
}
fn refill_if_empty(&mut self) -> IoResult<()> {
if self.buffered() == 0 {
self.refill()?;
}
Ok(())
}
pub fn blksize(&self) -> usize {
self.block.len()
}
}
impl<T: Backend> Read for BlockReader<T> {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
self.refill_if_empty()?;
let num = buf.len().min(self.buffered());
let buf = &mut buf[0..num];
buf.copy_from_slice(&self.block[self.idx..(self.idx + num)]);
self.idx += num;
Ok(num)
}
}
impl<T: Backend> Write for BlockReader<T> {
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
if !self.rw {
panic!(
"BUG: BlockReader::write() should never be called when the medium is not writable"
);
}
self.refill_if_empty()?;
let num = buf.len().min(self.buffered());
self.block[self.idx..(self.idx + num)].copy_from_slice(&buf[0..num]);
self.idx += num;
self.dirty = true;
self.flush()?;
Ok(num)
}
fn flush(&mut self) -> IoResult<()> {
if !self.dirty {
return Ok(());
}
self.inner
.seek(SeekFrom::Current(-(self.block.len() as i64)))?;
let mut num = 0;
while num < self.block.len() {
match self.inner.write(&self.block[num..])? {
0 => break,
n => num += n,
}
}
if num < self.block.len() {
let pos = self.inner.stream_position()?;
log::error!(
"short write: pos={pos}, num={num}, len={}",
self.block.len()
);
}
self.dirty = false;
Ok(())
}
}
impl<T: Backend> BufRead for BlockReader<T> {
fn fill_buf(&mut self) -> IoResult<&[u8]> {
self.refill_if_empty()?;
Ok(&self.block[self.idx..])
}
fn consume(&mut self, amt: usize) {
assert!(amt <= self.buffered());
self.idx += amt;
}
}
impl<T: Backend> Seek for BlockReader<T> {
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
let bs = self.blksize() as u64;
match pos {
SeekFrom::Start(pos) => {
self.flush()?;
let real = self.inner.seek(SeekFrom::Start(pos / bs * bs))?;
let rem = pos - real;
assert!(rem < bs);
self.refill()?;
self.idx = rem as usize;
Ok(real + rem)
}
SeekFrom::Current(offset) => {
let real = self.inner.stream_position()?;
let cur = real - self.block.len() as u64 + self.idx as u64;
let newidx = offset + self.idx as i64;
if newidx >= 0 && newidx < self.blksize() as i64 {
self.idx = newidx as usize;
Ok(real - self.block.len() as u64 + newidx as u64)
} else if cur as i64 + offset < 0 {
Err(io::Error::from_raw_os_error(libc::EINVAL))
} else {
self.seek(SeekFrom::Start((cur as i64 + offset) as u64))
}
}
SeekFrom::End(_) => todo!("SeekFrom::End()"),
}
}
}
#[cfg(test)]
mod t {
use super::*;
const FSIZE: u64 = 1 << 20;
fn harness(rw: bool) -> BlockReader<File> {
let f = tempfile::NamedTempFile::new().unwrap();
f.as_file().set_len(FSIZE).unwrap();
let br = BlockReader::open(f.path(), rw).unwrap();
let bs = br.blksize();
assert!(FSIZE > 2 * bs as u64);
br
}
mod write {
use super::*;
#[test]
fn simple_write() {
let mut br = harness(true);
let bs = br.blksize();
let pos = bs + (bs >> 2);
let mut buf = vec![0x55u8; bs];
br.seek(SeekFrom::Start(pos as u64)).unwrap();
br.write_all(&buf).unwrap();
buf.fill(0);
br.seek(SeekFrom::Start(pos as u64)).unwrap();
br.read_exact(&mut buf).unwrap();
assert_eq!(buf, vec![0x55u8; bs]);
}
}
mod seek {
use super::*;
#[test]
#[allow(clippy::seek_from_current)] fn current_0() {
let mut br = harness(false);
let bs = br.blksize();
let pos = bs + (bs >> 2);
br.seek(SeekFrom::Start(pos as u64)).unwrap();
let idx = br.idx;
let real_pos = br.inner.stream_position().unwrap();
br.seek(SeekFrom::Current(0)).unwrap();
assert_eq!(real_pos, br.inner.stream_position().unwrap());
assert_eq!(idx, br.idx);
}
#[test]
fn current_neg() {
let mut br = harness(false);
let bs = br.blksize();
let initial = bs + (bs >> 2);
br.seek(SeekFrom::Start(initial as u64)).unwrap();
let idx = br.idx as u64;
let real_pos = br.inner.stream_position().unwrap();
br.seek(SeekFrom::Current(-1)).unwrap();
assert_eq!(
real_pos + idx - 1,
br.inner.stream_position().unwrap() + br.idx as u64
);
}
#[test]
fn current_neg_neg() {
let mut br = harness(false);
let bs = br.blksize();
let initial = bs + (bs >> 2);
br.seek(SeekFrom::Start(initial as u64)).unwrap();
let e = br.seek(SeekFrom::Current(-2 * initial as i64)).unwrap_err();
assert_eq!(libc::EINVAL, e.raw_os_error().unwrap());
}
#[test]
fn current_pos_incr() {
let mut br = harness(false);
let bs = br.blksize();
let initial = bs + (bs >> 2);
br.seek(SeekFrom::Start(initial as u64)).unwrap();
let idx = br.idx as u64;
let real_pos = br.inner.stream_position().unwrap();
br.seek(SeekFrom::Current(1)).unwrap();
assert_eq!(
real_pos + idx + 1,
br.inner.stream_position().unwrap() + br.idx as u64
);
}
#[test]
fn current_pos_large() {
let mut br = harness(false);
let bs = br.blksize();
let initial = bs + (bs >> 2);
br.seek(SeekFrom::Start(initial as u64)).unwrap();
let idx = br.idx as u64;
let real_pos = br.inner.stream_position().unwrap();
br.seek(SeekFrom::Current(bs as i64)).unwrap();
assert_eq!(
real_pos + idx + bs as u64,
br.inner.stream_position().unwrap() + br.idx as u64
);
}
}
}