Skip to main content

file_region/
lib.rs

1use std::fs::{File, Metadata};
2use std::io::ErrorKind::InvalidInput;
3use std::io::{self, Error, Read, Seek, SeekFrom, Write};
4use std::ops::Range;
5
6pub struct FileRegion<'a> {
7    file: &'a File,
8    range: Range<u64>,
9}
10
11impl<'a> FileRegion<'a> {
12    pub fn file_metadata(&self) -> io::Result<Metadata> {
13        self.file.metadata()
14    }
15
16    pub fn range(self) -> Range<u64> {
17        self.range
18    }
19
20    pub fn len(&self) -> u64 {
21        self.range.end - self.range.start
22    }
23
24    pub fn is_empty(&self) -> bool {
25        self.range.is_empty()
26    }
27
28    pub fn new(file: &File, range: Range<u64>) -> FileRegion {
29        FileRegion { file, range }
30    }
31
32    pub fn is_valid(&self) -> io::Result<bool> {
33        let metadata = self.file.metadata()?;
34        let file_len = metadata.len();
35        Ok(self.range.start <= file_len && self.range.end <= file_len)
36    }
37
38    pub fn from_file(file: &'a File) -> io::Result<FileRegion<'a>> {
39        let range = 0..file.metadata()?.len();
40        Ok(FileRegion { file, range })
41    }
42
43    /// Return a subregion. Checks for some inconsistencies but not all; use
44    /// `is_valid()` to check consistency against the underlying file.
45    pub fn subregion(self, range: Range<u64>) -> io::Result<FileRegion<'a>> {
46        let start = {
47            let s = self.range.start.checked_add(range.start);
48            s.ok_or_else(|| Error::new(InvalidInput, "subregion start overflow"))?
49        };
50        let end = {
51            let e = self.range.start.checked_add(range.end);
52            e.ok_or_else(|| Error::new(InvalidInput, "subregion end overflow"))?
53        };
54        if start > self.range.end {
55            return Err(Error::new(InvalidInput, "subregion start exceeds parent"));
56        }
57        if end > self.range.end {
58            return Err(Error::new(InvalidInput, "subregion end exceeds parent"));
59        }
60        Ok(FileRegion {
61            file: self.file,
62            range: start..end,
63        })
64    }
65
66    pub fn read(&mut self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
67        self.file.seek(SeekFrom::Start(self.range.start + offset))?;
68        let max = self.len().saturating_sub(offset);
69        (&mut self.file).take(max).read(buf)
70    }
71
72    pub fn write(&mut self, offset: u64, buf: &[u8]) -> io::Result<usize> {
73        self.file.seek(SeekFrom::Start(self.range.start + offset))?;
74        let x = self.len().saturating_sub(offset).try_into();
75        let max: usize = x.map_err(|_| Error::new(InvalidInput, "offset too large"))?;
76        let buf_max = max.min(buf.len());
77        self.file.write(&buf[..buf_max])
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::io::{Read, Seek, SeekFrom, Write};
84    use tempfile::tempfile;
85
86    use super::FileRegion;
87
88    #[test]
89    fn test_from_file() {
90        let mut file = tempfile().unwrap();
91        file.write_all(b"Hello, World!").unwrap();
92        file.flush().unwrap();
93        let fr = FileRegion::from_file(&file).unwrap();
94        assert_eq!(fr.range(), 0..13);
95    }
96
97    #[test]
98    fn test_is_valid_true() {
99        let mut file = tempfile().unwrap();
100        file.write_all(b"0123456789").unwrap();
101        file.flush().unwrap();
102        let fr = FileRegion::from_file(&file).unwrap();
103        assert!(fr.is_valid().unwrap());
104    }
105
106    #[test]
107    fn test_is_valid_false_1() {
108        let file = tempfile().unwrap();
109        let fr = FileRegion::new(&file, 0..7);
110        assert!(!fr.is_valid().unwrap());
111    }
112
113    #[test]
114    fn test_is_valid_false_2() {
115        let mut file = tempfile().unwrap();
116        file.write_all(b"0123456789").unwrap();
117        file.flush().unwrap();
118        let fr = FileRegion::new(&file, 0..11);
119        assert!(!fr.is_valid().unwrap());
120    }
121
122    #[test]
123    fn test_subregion() {
124        let file = tempfile().unwrap();
125        let fr = FileRegion::new(&file, 100..2100);
126        let sub = fr.subregion(200..600).unwrap();
127        assert_eq!(sub.range(), 300..700);
128    }
129
130    #[test]
131    fn test_write_within_region() {
132        let mut file = tempfile().unwrap();
133        file.write_all(&[0; 200]).unwrap();
134
135        {
136            let mut fr = FileRegion::new(&file, 100..120);
137            let written = fr.write(0, b"enshittification").unwrap();
138            assert_eq!(written, 16);
139        }
140
141        file.seek(SeekFrom::Start(0)).unwrap();
142        let mut content = vec![0; 200];
143        file.read_exact(&mut content).unwrap();
144
145        assert_eq!(content[..100], [0; 100]);
146        assert_eq!(&content[100..116], b"enshittification");
147        assert_eq!(content[116..], [0; 84]);
148    }
149
150    #[test]
151    fn test_write_beyond_region() {
152        let mut file = tempfile().unwrap();
153        file.write_all(&[0; 200]).unwrap();
154
155        {
156            let mut fr = FileRegion::new(&file, 100..110);
157            let written = fr.write(0, b"enshittification").unwrap();
158            assert_eq!(written, 10);
159        }
160
161        file.seek(SeekFrom::Start(0)).unwrap();
162        let mut content = vec![0; 200];
163        file.read_exact(&mut content).unwrap();
164
165        assert_eq!(content[..100], [0; 100]);
166        assert_eq!(&content[100..110], b"enshittifi");
167        assert_eq!(content[110..], [0; 90]);
168    }
169
170    #[test]
171    fn test_example() {
172        let mut file = tempfile().unwrap();
173        file.write_all(b"Hello, FileRegion.").unwrap();
174
175        let mut region = FileRegion::new(&file, 7..16);
176        let mut buffer = [0; 9];
177        region.read(0, &mut buffer).unwrap();
178        assert_eq!(&buffer, b"FileRegio");
179
180        region.write(0, b"01234").unwrap();
181
182        let mut content = String::new();
183        file.seek(SeekFrom::Start(0)).unwrap();
184        file.read_to_string(&mut content).unwrap();
185        assert_eq!(content, "Hello, 01234egion.");
186    }
187}