Skip to content

Commit

Permalink
changes how read, write handle errors
Browse files Browse the repository at this point in the history
  • Loading branch information
David James committed Sep 12, 2024
1 parent 03f1328 commit 8dec74b
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 84 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "file-region"
version = "0.1.0"
version = "0.2.0"
edition = "2021"

description = "Encapsulates a contiguous byte range of a file"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ This crate provides a `FileRegion` type which encapsulates a particular region o
You can find this example at `examples/basic.rs` and run it with `cargo run --example basic`.

```rust
use file_region::FileRegion;
use file_region::{FileRegion, FileRegionError};
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::tempfile;

fn main() -> std::io::Result<()> {
fn main() -> Result<(), FileRegionError> {
let mut file = tempfile()?;
file.write_all(b"Hello, FileRegion.")?;

Expand Down
4 changes: 2 additions & 2 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use file_region::FileRegion;
use file_region::{FileRegion, FileRegionError};
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::tempfile;

fn main() -> std::io::Result<()> {
fn main() -> Result<(), FileRegionError> {
let mut file = tempfile()?;
file.write_all(b"Hello, FileRegion.")?;

Expand Down
133 changes: 87 additions & 46 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
use std::fs::{File, Metadata};
use std::io::Error as IoError;
use std::io::ErrorKind::InvalidInput;
use std::io::Result as IoResult;
use std::io::{Read, Seek, SeekFrom, Write};
use std::ops::Range;

use super::error::{FileRegionError, RegionError};

pub struct FileRegion<'a> {
file: &'a File,
range: Range<u64>,
}

impl<'a> FileRegion<'a> {
/// Creates a new `FileRegion`. Note that `range` is _not_ validated against
/// the `file`. Use `is_valid()` to check consistency.
/// the `file`. Use `is_valid()` or `validate()` to check consistency.
pub fn new(file: &File, range: Range<u64>) -> FileRegion {
FileRegion { file, range }
}

/// Creates a new `FileRegion`, validating the `range` against the `file`.
/// Returns `Ok(FileRegion)` if valid. Can return `Err(io::Error)` due to
/// invalid range or I/O errors during validation.
pub fn try_new(file: &'a File, range: Range<u64>) -> IoResult<FileRegion> {
/// Returns `Ok(FileRegion)` if valid. Otherwise, returns a
/// `FileRegionError` due to invalid range or I/O errors during
/// validation.
pub fn try_new(file: &'a File, range: Range<u64>) -> Result<FileRegion, FileRegionError> {
let region = FileRegion::new(file, range);
if region.is_valid()? {
Ok(region)
} else {
Err(IoError::new(InvalidInput, "Invalid file range"))
}
region.validate()?;
Ok(region)
}

/// Creates a new `FileRegion` spanning the entire `file`. Validity is
/// guaranteed.
pub fn from_file(file: &'a File) -> IoResult<FileRegion<'a>> {
pub fn from_file(file: &'a File) -> IoResult<Self> {
let range = 0..file.metadata()?.len();
Ok(FileRegion { file, range })
}
Expand Down Expand Up @@ -61,8 +59,19 @@ impl<'a> FileRegion<'a> {
/// Performs I/O to get the file's metadata.
pub fn is_valid(&self) -> IoResult<bool> {
let metadata = self.file.metadata()?;
let file_len = metadata.len();
Ok(self.range.start <= file_len && self.range.end <= file_len)
let len = metadata.len();
// Note the careful usage of `<` and `<=`.
Ok(self.range.start < len && self.range.end <= len)
}

/// Validates the `FileRegion` by checking if its range is within the bounds
/// of the underlying file. Returns `Ok(())` if valid, otherwise returns a
/// `FileRegionError` detailing the specific validation failure or I/O error
/// encountered.
pub fn validate(&self) -> Result<(), FileRegionError> {
let metadata = self.file.metadata().map_err(FileRegionError::Io)?;
let len = metadata.len();
validate_range(&self.range, len).map_err(FileRegionError::Region)
}

/// Performs a bounded read operation within the file region. Returns the
Expand All @@ -72,50 +81,82 @@ impl<'a> FileRegion<'a> {
/// from the offset. The actual number of bytes read may be less than the
/// buffer's capacity.
///
/// Returns an error if I/O errors occur during seeking or reading.
pub fn read(&mut self, offset: u64, buf: &mut [u8]) -> IoResult<usize> {
self.file.seek(SeekFrom::Start(self.range.start + offset))?;
let max = self.len().saturating_sub(offset);
(&mut self.file).take(max).read(buf)
/// If the read begin inside the region, no error is returned, even if the
/// read attempts to go past the end of the region.
///
/// On the other hand, if the read attempts to start beyond the region,
/// returns an error.
///
/// Returns an error if `offset: u64` is too large to fit in `usize`.
///
/// May return an I/O error from seeking or reading.
pub fn read(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, FileRegionError> {
let start = self
.range
.start
.checked_add(offset)
.ok_or(FileRegionError::Region(RegionError::StartOverflow))?;
if start >= self.range.end {
return Err(FileRegionError::Region(RegionError::StartOutOfBounds));
}
self.file
.seek(SeekFrom::Start(start))
.map_err(FileRegionError::Io)?;
let limit = self.len().saturating_sub(offset);
(&mut self.file)
.take(limit)
.read(buf)
.map_err(FileRegionError::Io)
}

/// Performs a bounded write operation within the file region. Returns the
/// number of bytes successfully written.
/// Attempts to perform a bounded write operation within the file region.
///
/// Writes data from the given buffer, limited by the region's size. If the
/// buffer exceeds the available space, this is not an error; it only writes
/// up to the region's end.
/// Returns the number of bytes successfully written.
///
/// Returns an error if:
/// - `offset: u64` is too large to fit in `usize`
/// - I/O errors occur during seeking or writing
pub fn write(&mut self, offset: u64, buf: &[u8]) -> IoResult<usize> {
self.file.seek(SeekFrom::Start(self.range.start + offset))?;
let x = self.len().saturating_sub(offset).try_into();
let max: usize = x.map_err(|_| IoError::new(InvalidInput, "offset too large"))?;
let buf_max = max.min(buf.len());
self.file.write(&buf[..buf_max])
/// If any part of the write are out-of-bounds, write nothing and return an
/// error. There are two out-of-bound cases:
/// - start the write in the region that is too long
/// - start the write beyond the region
///
/// Returns an error if `offset: u64` is too large to fit in `usize`.
///
/// May return an I/O error from seeking or writing.
pub fn write(&mut self, offset: u64, buf: &[u8]) -> Result<usize, FileRegionError> {
let range = subrange(&self.range, offset..offset + buf.len() as u64)
.map_err(FileRegionError::Region)?;
self.file
.seek(SeekFrom::Start(range.start))
.map_err(FileRegionError::Io)?;
self.file.write(buf).map_err(FileRegionError::Io)
}

/// Return a subregion. Checks for some inconsistencies but not all; use
/// `is_valid()` to check consistency against the underlying file.
pub fn subregion(self, range: Range<u64>) -> IoResult<FileRegion<'a>> {
let start = self.checked_offset(range.start, "start")?;
let end = self.checked_offset(range.end, "end")?;
if start > self.range.end {
return Err(IoError::new(InvalidInput, "subregion start exceeds parent"));
}
if end > self.range.end {
return Err(IoError::new(InvalidInput, "subregion end exceeds parent"));
}
pub fn subregion(self, range: Range<u64>) -> Result<FileRegion<'a>, RegionError> {
Ok(FileRegion {
file: self.file,
range: start..end,
range: subrange(&self.range, range)?,
})
}
}

fn subrange(parent: &Range<u64>, child: Range<u64>) -> Result<Range<u64>, RegionError> {
let add = |offset: u64| parent.start.checked_add(offset);
let start = add(child.start).ok_or(RegionError::StartOverflow)?;
let end = add(child.end).ok_or(RegionError::EndOverflow)?;
let range = start..end;
validate_range(&range, parent.end)?;
Ok(range)
}

fn checked_offset(&self, offset: u64, operation: &str) -> IoResult<u64> {
let n = self.range.start.checked_add(offset);
n.ok_or_else(|| IoError::new(InvalidInput, format!("subregion {} overflow", operation)))
/// Validates the range for a provided file length.
fn validate_range(range: &Range<u64>, len: u64) -> Result<(), RegionError> {
// Note the careful usage of `>=` and `>`.
if range.start >= len {
Err(RegionError::StartOutOfBounds)
} else if range.end > len {
Err(RegionError::EndOutOfBounds)
} else {
Ok(())
}
}
27 changes: 27 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::io::Error as IoError;

#[derive(Debug)]
pub enum FileRegionError {
Io(IoError),
Region(RegionError),
}

#[derive(Debug)]
pub enum RegionError {
StartOverflow,
EndOverflow,
StartOutOfBounds,
EndOutOfBounds,
}

impl From<IoError> for FileRegionError {
fn from(error: IoError) -> Self {
FileRegionError::Io(error)
}
}

impl From<RegionError> for FileRegionError {
fn from(error: RegionError) -> Self {
FileRegionError::Region(error)
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod core;
mod error;

pub use core::FileRegion;
pub use error::{FileRegionError, RegionError};

#[cfg(test)]
mod tests;
Loading

0 comments on commit 8dec74b

Please sign in to comment.