use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use crate::error::{IoError, OxiGeoError, Result};
use crate::io::traits::{dst_too_small, range_len_usize};
use crate::io::{ByteRange, DataSource};
const MAX_SEEK_OFFSET: u64 = i64::MAX as u64;
#[cfg(any(unix, windows))]
#[derive(Debug)]
struct FileHandle(File);
#[cfg(not(any(unix, windows)))]
#[derive(Debug)]
struct FileHandle(std::sync::Mutex<File>);
#[cfg(unix)]
impl FileHandle {
fn new(file: File) -> Self {
Self(file)
}
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<()> {
use std::os::unix::fs::FileExt;
self.0
.read_exact_at(buf, offset)
.map_err(|e| read_err(buf.len(), offset, &e))
}
}
#[cfg(windows)]
impl FileHandle {
fn new(file: File) -> Self {
Self(file)
}
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<()> {
use std::os::windows::fs::FileExt;
let total = buf.len();
let mut remaining = buf;
let mut position = offset;
while !remaining.is_empty() {
match self.0.seek_read(remaining, position) {
Ok(0) => break,
Ok(n) => {
let rest = remaining;
remaining = &mut rest[n..];
position = position.saturating_add(n as u64);
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(read_err(total, offset, &e)),
}
}
if remaining.is_empty() {
Ok(())
} else {
Err(read_err(
total,
offset,
&io::Error::new(io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"),
))
}
}
}
#[cfg(not(any(unix, windows)))]
impl FileHandle {
fn new(file: File) -> Self {
Self(std::sync::Mutex::new(file))
}
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<()> {
use std::io::{Read, Seek, SeekFrom};
let mut file = self.0.lock().map_err(|e| OxiGeoError::Internal {
message: format!("Failed to lock file mutex: {e}"),
})?;
file.seek(SeekFrom::Start(offset))
.map_err(|_| OxiGeoError::Io(IoError::Seek { position: offset }))?;
file.read_exact(buf)
.map_err(|e| read_err(buf.len(), offset, &e))
}
}
fn read_err(len: usize, offset: u64, e: &io::Error) -> OxiGeoError {
OxiGeoError::Io(IoError::Read {
message: format!("Failed to read {len} bytes at offset {offset}: {e}"),
})
}
pub struct FileDataSource {
path: PathBuf,
handle: FileHandle,
size: u64,
}
impl FileDataSource {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let file = File::open(&path).map_err(|e| {
OxiGeoError::Io(IoError::Read {
message: format!("Failed to open file '{}': {}", path.display(), e),
})
})?;
let metadata = file.metadata().map_err(|e| {
OxiGeoError::Io(IoError::Read {
message: format!("Failed to get file metadata: {e}"),
})
})?;
Ok(Self {
path,
handle: FileHandle::new(file),
size: metadata.len(),
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl DataSource for FileDataSource {
fn size(&self) -> Result<u64> {
Ok(self.size)
}
fn read_range(&self, range: ByteRange) -> Result<Vec<u8>> {
let len = range_len_usize(range)?;
let mut buffer = vec![0u8; len];
self.read_range_into(range, &mut buffer)?;
Ok(buffer)
}
fn read_range_into(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize> {
let len = range_len_usize(range)?;
if dst.len() < len {
return Err(dst_too_small(len, dst.len()));
}
if range.start > MAX_SEEK_OFFSET {
return Err(OxiGeoError::Io(IoError::Seek {
position: range.start,
}));
}
self.handle.read_exact_at(&mut dst[..len], range.start)?;
Ok(len)
}
}
impl std::fmt::Debug for FileDataSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileDataSource")
.field("path", &self.path)
.field("size", &self.size)
.finish()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use std::env::temp_dir;
use std::fs;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
struct TempPath(PathBuf);
impl TempPath {
fn new(name: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
Self(temp_dir().join(format!(
"oxigeo_core_file_{}_{seq}_{name}",
std::process::id()
)))
}
}
impl std::ops::Deref for TempPath {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl AsRef<Path> for TempPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl Drop for TempPath {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
fn write_temp_file(name: &str, data: &[u8]) -> TempPath {
let path = TempPath::new(name);
let mut f = fs::File::create(&path).expect("create temp file");
f.write_all(data).expect("write temp data");
f.flush().expect("flush temp file");
path
}
#[test]
fn test_file_read_range_and_into_agree() {
let data: Vec<u8> = (0u8..=255u8).collect();
let path = write_temp_file("ds_agree.bin", &data);
let src = FileDataSource::open(&path).expect("open");
assert_eq!(src.size().expect("size"), 256);
let range = ByteRange::new(10, 60);
let owned = src.read_range(range).expect("read_range");
let mut buf = vec![0u8; 50];
let n = src
.read_range_into(range, &mut buf)
.expect("read_range_into");
assert_eq!(n, 50);
assert_eq!(owned, buf);
assert_eq!(buf, data[10..60]);
}
#[test]
fn test_file_read_range_into_empty_range() {
let path = write_temp_file("ds_empty.bin", &[1, 2, 3, 4]);
let src = FileDataSource::open(&path).expect("open");
let mut empty: [u8; 0] = [];
let n = src
.read_range_into(ByteRange::new(2, 2), &mut empty)
.expect("empty range");
assert_eq!(n, 0);
}
#[test]
fn test_file_read_range_into_rejects_short_dst() {
let path = write_temp_file("ds_short.bin", &[1, 2, 3, 4, 5, 6, 7, 8]);
let src = FileDataSource::open(&path).expect("open");
let mut buf = [0xAAu8; 3];
let err = src
.read_range_into(ByteRange::new(0, 4), &mut buf)
.expect_err("dst too short must be rejected");
assert!(matches!(err, OxiGeoError::InvalidParameter { .. }));
assert_eq!(buf, [0xAAu8; 3]);
}
#[test]
fn test_file_read_range_into_past_eof_errors() {
let path = write_temp_file("ds_eof.bin", &[1, 2, 3, 4]);
let src = FileDataSource::open(&path).expect("open");
let range = ByteRange::new(2, 16);
let owned_err = src.read_range(range).expect_err("past EOF");
let mut buf = vec![0u8; 14];
let into_err = src.read_range_into(range, &mut buf).expect_err("past EOF");
assert_eq!(format!("{owned_err}"), format!("{into_err}"));
}
}