use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
pub const MAX_SEEK_POSITION: u64 = i64::MAX as u64;
#[derive(Debug)]
pub struct GuardedSeek<T> {
inner: T,
}
impl<T> GuardedSeek<T> {
pub fn new(inner: T) -> Self {
Self { inner }
}
}
impl<T: Read> Read for GuardedSeek<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<T: Seek> Seek for GuardedSeek<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
if let SeekFrom::Start(target) = pos
&& target > MAX_SEEK_POSITION
{
return Err(io::Error::new(
ErrorKind::InvalidData,
format!(
"a seek to absolute position {target} is past {MAX_SEEK_POSITION}, the \
largest position any file can hold; the input declares a location that \
cannot exist"
),
));
}
self.inner.seek(pos).map_err(|e| {
if e.kind() == ErrorKind::InvalidInput {
io::Error::new(
ErrorKind::InvalidData,
format!(
"the platform refused a seek to {pos:?} as an invalid position ({e}); \
the input declares a location that cannot exist"
),
)
} else {
e
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use std::io::Write;
fn temp_file(bytes: &[u8]) -> std::fs::File {
let mut f = tempfile::tempfile().unwrap();
f.write_all(bytes).unwrap();
f.seek(SeekFrom::Start(0)).unwrap();
f
}
#[test]
fn a_start_past_the_addressable_range_is_malformed_input_not_an_os_failure() {
let mut g = GuardedSeek::new(temp_file(b"0123456789"));
let err = g.seek(SeekFrom::Start(MAX_SEEK_POSITION + 1)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData, "{err}");
assert!(
err.get_ref().is_some(),
"the refusal must carry its own message, not a bare errno: {err:?}"
);
let classified = Error::from_decode_io(err);
assert!(matches!(classified, Error::Corrupt(_)), "{classified:?}");
assert_eq!(classified.exit_code(), 5);
}
#[test]
fn the_in_memory_backing_refuses_what_a_file_refuses() {
let mut bare = io::Cursor::new(b"0123456789".to_vec());
assert!(
bare.seek(SeekFrom::Start(u64::MAX)).is_ok(),
"premise: an unguarded Cursor accepts a position no file can hold"
);
let mut g = GuardedSeek::new(io::Cursor::new(b"0123456789".to_vec()));
let err = g.seek(SeekFrom::Start(u64::MAX)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData, "{err}");
}
#[test]
fn a_negative_resulting_position_is_folded_too() {
let mut g = GuardedSeek::new(temp_file(b"0123456789"));
let err = g.seek(SeekFrom::End(-1000)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData, "{err}");
assert_eq!(Error::from_decode_io(err).exit_code(), 5);
}
#[test]
fn an_ordinary_seek_and_read_are_untouched() {
let mut g = GuardedSeek::new(temp_file(b"0123456789"));
assert_eq!(g.seek(SeekFrom::Start(4)).unwrap(), 4);
let mut buf = [0u8; 3];
g.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"456");
assert_eq!(g.seek(SeekFrom::End(0)).unwrap(), 10);
assert_eq!(g.seek(SeekFrom::Current(-2)).unwrap(), 8);
}
#[test]
fn a_medium_failure_keeps_its_kind_in_both_directions() {
struct Failing;
impl Read for Failing {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(ErrorKind::PermissionDenied, "disk said no"))
}
}
impl Seek for Failing {
fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
Err(io::Error::from_raw_os_error(5)) }
}
let mut g = GuardedSeek::new(Failing);
let read_err = g.read(&mut [0u8; 4]).unwrap_err();
assert_eq!(read_err.kind(), ErrorKind::PermissionDenied, "{read_err}");
assert!(matches!(Error::from_decode_io(read_err), Error::Io(_)));
let seek_err = g.seek(SeekFrom::Start(0)).unwrap_err();
assert_eq!(seek_err.raw_os_error(), Some(5), "{seek_err:?}");
assert!(matches!(Error::from_decode_io(seek_err), Error::Io(_)));
}
}