1#[cfg(not(any(unix, windows)))]
4use std::io::{Read, Seek, SeekFrom};
5
6use crate::StdSourceError;
7use zencodec::source::{ReadAt, SourceError};
8
9pub struct FileReadAt {
18 file: std::fs::File,
19 len: u64,
20}
21
22impl FileReadAt {
23 pub fn new(file: std::fs::File) -> Result<Self, StdSourceError> {
25 let len = file.metadata()?.len();
26 Ok(Self { file, len })
27 }
28
29 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, StdSourceError> {
31 Self::new(std::fs::File::open(path)?)
32 }
33}
34
35impl ReadAt for FileReadAt {
36 type Error = StdSourceError;
37
38 fn len(&mut self) -> Result<u64, Self::Error> {
39 Ok(self.len)
40 }
41
42 fn len_hint(&self) -> Option<u64> {
43 Some(self.len)
44 }
45
46 fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), Self::Error> {
47 if offset
48 .checked_add(out.len() as u64)
49 .is_none_or(|end| end > self.len)
50 {
51 return Err(SourceError::UnexpectedEof {
52 offset,
53 len: out.len() as u64,
54 total: Some(self.len),
55 }
56 .into());
57 }
58 #[cfg(unix)]
59 {
60 use std::os::unix::fs::FileExt;
61 self.file.read_exact_at(out, offset).map_err(Into::into)
62 }
63 #[cfg(windows)]
64 {
65 use std::os::windows::fs::FileExt;
66 let mut filled = 0;
67 while filled < out.len() {
68 match self
69 .file
70 .seek_read(&mut out[filled..], offset + filled as u64)
71 {
72 Ok(0) => {
73 return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof).into());
74 }
75 Ok(n) => filled += n,
76 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
77 Err(e) => return Err(e.into()),
78 }
79 }
80 Ok(())
81 }
82 #[cfg(not(any(unix, windows)))]
83 {
84 self.file.seek(SeekFrom::Start(offset))?;
85 self.file.read_exact(out).map_err(Into::into)
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::io::Write;
94 #[cfg(unix)]
95 use std::io::{Seek, SeekFrom};
96 use std::{format, vec::Vec};
97
98 struct Fixture(std::path::PathBuf);
99 impl Fixture {
100 fn new(name: &str) -> (Self, std::fs::File) {
101 let dir = std::env::var_os("TMPDIR")
102 .map(std::path::PathBuf::from)
103 .unwrap_or_else(|| {
104 std::path::PathBuf::from(
105 std::env::var_os("HOME")
106 .or_else(|| std::env::var_os("USERPROFILE"))
107 .expect("home directory"),
108 )
109 .join("tmp")
110 });
111 std::fs::create_dir_all(&dir).unwrap();
112 let path = dir.join(format!("zencodec-file-{name}-{}", std::process::id()));
113 let file = std::fs::OpenOptions::new()
114 .create_new(true)
115 .read(true)
116 .write(true)
117 .open(&path)
118 .unwrap();
119 (Self(path), file)
120 }
121 }
122 impl Drop for Fixture {
123 fn drop(&mut self) {
124 std::fs::remove_file(&self.0).unwrap();
125 }
126 }
127
128 #[test]
129 fn file_ranges_eof_overflow_and_truncation() {
130 let (_fixture, mut file) = Fixture::new("bounds");
131 let bytes: Vec<u8> = (0..=255).collect();
132 file.write_all(&bytes).unwrap();
133 let mut source = FileReadAt::new(file.try_clone().unwrap()).unwrap();
134 let mut out = [0; 7];
135 for at in [200, 4, 249, 0, 100] {
136 source.read_exact_at(at, &mut out).unwrap();
137 assert_eq!(out, bytes[at as usize..at as usize + 7]);
138 }
139 assert_eq!(source.read_at(253, &mut out).unwrap(), 3);
140 assert_eq!(&out[..3], &[253, 254, 255]);
141 assert_eq!(source.read_at(256, &mut out).unwrap(), 0);
142 for at in [250, u64::MAX] {
143 assert!(matches!(
144 source.read_exact_at(at, &mut out),
145 Err(StdSourceError::Source(SourceError::UnexpectedEof { .. }))
146 ));
147 }
148 file.set_len(2).unwrap();
149 assert!(source.read_exact_at(0, &mut out).is_err());
150 }
151
152 #[test]
153 fn native_file_errors_retain_kind_and_os_code() {
154 let (fixture, file) = Fixture::new("errors");
155 let missing = fixture.0.with_extension("missing");
156 assert!(!missing.exists());
157 let error = FileReadAt::open(&missing).err().expect("missing file");
158 let native = zencodec::find_cause::<std::io::Error>(&error).unwrap();
159 assert_eq!(native.kind(), std::io::ErrorKind::NotFound);
160 assert!(native.raw_os_error().is_some());
161
162 file.set_len(8).unwrap();
164 let mut source = FileReadAt::new(file.try_clone().unwrap()).unwrap();
165 file.set_len(0).unwrap();
166 let error = source.read_exact_at(0, &mut [0; 8]).unwrap_err();
167 assert_eq!(
168 zencodec::find_cause::<std::io::Error>(&error)
169 .unwrap()
170 .kind(),
171 std::io::ErrorKind::UnexpectedEof
172 );
173 }
174
175 #[cfg(unix)]
176 #[test]
177 fn positional_reads_preserve_a_shared_cursor_and_large_offsets() {
178 let (_fixture, mut file) = Fixture::new("cursor");
179 let offset = (1u64 << 32) + 123;
180 file.seek(SeekFrom::Start(offset)).unwrap();
181 file.write_all(b"large-offset").unwrap();
182 file.seek(SeekFrom::Start(17)).unwrap();
183 let mut source = FileReadAt::new(file.try_clone().unwrap()).unwrap();
184 assert_eq!(
185 file.stream_position().unwrap(),
186 17,
187 "construction moved the shared cursor"
188 );
189 let mut out = [0; 12];
190 source.read_exact_at(offset, &mut out).unwrap();
191 assert_eq!(&out, b"large-offset");
192 source.read_exact_at(0, &mut out).unwrap();
193 assert_eq!(out, [0; 12]);
194 assert_eq!(file.stream_position().unwrap(), 17);
195 std::thread::scope(|scope| {
198 for at in [0, offset] {
199 let mut source = FileReadAt::new(file.try_clone().unwrap()).unwrap();
200 scope.spawn(move || {
201 for _ in 0..1000 {
202 let mut out = [0; 12];
203 source.read_exact_at(at, &mut out).unwrap();
204 assert_eq!(&out, if at == 0 { &[0; 12] } else { b"large-offset" });
205 }
206 });
207 }
208 });
209 assert_eq!(file.stream_position().unwrap(), 17);
210 }
211}