Skip to main content

zencodec_io/
file.rs

1//! File-backed random-access source.
2
3#[cfg(not(any(unix, windows)))]
4use std::io::{Read, Seek, SeekFrom};
5
6use crate::StdSourceError;
7use zencodec::source::{ReadAt, SourceError};
8
9/// [`ReadAt`] over a file, using positional reads on Unix and Windows and
10/// seek + read elsewhere. Unix reads leave a shared file cursor unchanged;
11/// Windows `seek_read` can update it. No implementation uses `unsafe`.
12///
13/// The caller must keep the opened file's bytes stable for the decoder operation.
14/// This adapter captures its length but neither locks the file nor detects every
15/// in-place replacement. An open handle and positional reads do not establish
16/// the coherent-representation contract of [`ReadAt`].
17pub struct FileReadAt {
18    file: std::fs::File,
19    len: u64,
20}
21
22impl FileReadAt {
23    /// Create a source over an already-open file.
24    pub fn new(file: std::fs::File) -> Result<Self, StdSourceError> {
25        let len = file.metadata()?.len();
26        Ok(Self { file, len })
27    }
28
29    /// Open `path` and create a source over it.
30    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        // The source's length snapshot cannot hide a later native read failure.
163        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        // Independent sources sharing the OS handle may read different offsets
196        // concurrently, with no seek/read pair to race.
197        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}