Skip to main content

rakata_formats/
section_reader.rs

1//! Bounded, seekable windows over a shared source.
2//!
3//! Lazy archive indexes read entry bytes on demand rather than slurping a
4//! whole file, so several of them need to read from one open handle at
5//! independent positions. [`SectionReader`](crate::section_reader::SectionReader) is that primitive: a
6//! `(start, len)` window that presents itself as a standalone stream
7//! starting at zero.
8//!
9//! Two properties earn its place:
10//!
11//! - **Handles are shared through a `Mutex`, not `try_clone`.** Cloned file
12//!   descriptors share one OS seek cursor, so two readers walking the same
13//!   file would silently corrupt each other's positions. Each window keeps
14//!   its own logical position and seeks the shared handle immediately before
15//!   every read.
16//! - **Windows nest.** [`SectionReader::section`](crate::section_reader::SectionReader::section) carves a sub-window out of
17//!   an existing one against the same handle, which is what lets an archive
18//!   index open a nested archive stored at an offset inside its parent. A
19//!   save game is exactly this shape: per-module archives living at offsets
20//!   inside `SAVEGAME.sav`.
21
22use std::io::{self, Read, Seek, SeekFrom};
23use std::sync::{Arc, Mutex};
24
25/// A bounded window over a shared seekable source.
26///
27/// Offsets are window-relative: seeking to `0` lands on the window's first
28/// byte, and reads stop at its last one regardless of how much data follows
29/// in the underlying source. Cloning is cheap and yields an independent
30/// cursor over the same window.
31#[derive(Debug)]
32pub struct SectionReader<R> {
33    source: Arc<Mutex<R>>,
34    start: u64,
35    len: u64,
36    pos: u64,
37}
38
39impl<R> Clone for SectionReader<R> {
40    fn clone(&self) -> Self {
41        Self {
42            source: Arc::clone(&self.source),
43            start: self.start,
44            len: self.len,
45            pos: self.pos,
46        }
47    }
48}
49
50impl<R> SectionReader<R> {
51    /// Creates a window covering `len` bytes starting at `start` in `source`.
52    ///
53    /// The bounds are not validated against the source's actual length:
54    /// a window past the end simply reads short, matching how the underlying
55    /// reader behaves.
56    pub fn new(source: Arc<Mutex<R>>, start: u64, len: u64) -> Self {
57        Self {
58            source,
59            start,
60            len,
61            pos: 0,
62        }
63    }
64
65    /// Wraps an entire source in a window, given its total length.
66    pub fn whole(source: Arc<Mutex<R>>, len: u64) -> Self {
67        Self::new(source, 0, len)
68    }
69
70    /// Carves a sub-window out of this one against the same shared source.
71    ///
72    /// `offset` is relative to this window's start. Returns `None` when the
73    /// requested range would extend past this window's end, which for an
74    /// archive index means the entry table pointed outside its own container.
75    pub fn section(&self, offset: u64, len: u64) -> Option<Self> {
76        let end = offset.checked_add(len)?;
77        if end > self.len {
78            return None;
79        }
80        Some(Self {
81            source: Arc::clone(&self.source),
82            start: self.start.checked_add(offset)?,
83            len,
84            pos: 0,
85        })
86    }
87
88    /// Returns the window's length in bytes.
89    pub fn len(&self) -> u64 {
90        self.len
91    }
92
93    /// Returns whether the window covers zero bytes.
94    pub fn is_empty(&self) -> bool {
95        self.len == 0
96    }
97
98    /// Returns the current window-relative position.
99    pub fn position(&self) -> u64 {
100        self.pos
101    }
102}
103
104impl<R: Read + Seek> SectionReader<R> {
105    /// Reads the window's entire contents into a new buffer.
106    ///
107    /// Independent of the current position: this always reads the whole
108    /// window. Intended for the common archive case of pulling one complete
109    /// entry out in a single locked pass.
110    ///
111    /// # Errors
112    ///
113    /// [`io::ErrorKind::InvalidData`] when the window's length exceeds
114    /// addressable memory, and whatever the backing source reports otherwise.
115    /// A short read is [`io::ErrorKind::UnexpectedEof`], since the window's
116    /// length is what was asked for.
117    pub fn read_all(&self) -> io::Result<Vec<u8>> {
118        let capacity = usize::try_from(self.len).map_err(|_| {
119            io::Error::new(
120                io::ErrorKind::InvalidData,
121                "section length exceeds addressable memory",
122            )
123        })?;
124        let mut buf = vec![0u8; capacity];
125        // A fresh cursor rather than `self.clone()`: cloning preserves the
126        // current position, which would make this read short.
127        let mut cursor = Self {
128            source: Arc::clone(&self.source),
129            start: self.start,
130            len: self.len,
131            pos: 0,
132        };
133        cursor.read_exact(&mut buf)?;
134        Ok(buf)
135    }
136}
137
138impl<R: Read + Seek> Read for SectionReader<R> {
139    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
140        let remaining = self.len.saturating_sub(self.pos);
141        if remaining == 0 || buf.is_empty() {
142            return Ok(0);
143        }
144
145        // Clamp the request to what is left inside the window. usize::MAX is
146        // a safe saturation point: buf can never be longer than that anyway.
147        let cap = usize::try_from(remaining).unwrap_or(usize::MAX);
148        let take = buf.len().min(cap);
149
150        let offset = self
151            .start
152            .checked_add(self.pos)
153            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "section offset overflow"))?;
154
155        // A poisoned lock means another thread panicked while holding it. The
156        // handle itself is still usable here because every read seeks first,
157        // so a stale cursor left behind by that panic cannot affect this read.
158        let mut source = self
159            .source
160            .lock()
161            .unwrap_or_else(|poisoned| poisoned.into_inner());
162        source.seek(SeekFrom::Start(offset))?;
163        let read = source.read(&mut buf[..take])?;
164        drop(source);
165
166        self.pos = self.pos.saturating_add(
167            u64::try_from(read)
168                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "read length overflow"))?,
169        );
170        Ok(read)
171    }
172}
173
174impl<R: Read + Seek> Seek for SectionReader<R> {
175    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
176        let target = match pos {
177            SeekFrom::Start(offset) => i128::from(offset),
178            SeekFrom::End(offset) => i128::from(self.len) + i128::from(offset),
179            SeekFrom::Current(offset) => i128::from(self.pos) + i128::from(offset),
180        };
181
182        if target < 0 {
183            return Err(io::Error::new(
184                io::ErrorKind::InvalidInput,
185                "cannot seek before the start of a section",
186            ));
187        }
188
189        // Seeking past the end is legal and mirrors std's file behaviour:
190        // the position moves, and subsequent reads return zero bytes.
191        self.pos = u64::try_from(target)
192            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "seek position overflow"))?;
193        Ok(self.pos)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::io::Cursor;
201
202    fn source(bytes: &[u8]) -> Arc<Mutex<Cursor<Vec<u8>>>> {
203        Arc::new(Mutex::new(Cursor::new(bytes.to_vec())))
204    }
205
206    #[test]
207    fn reads_only_within_the_window() {
208        let src = source(b"0123456789");
209        let mut window = SectionReader::new(src, 3, 4);
210
211        let mut buf = Vec::new();
212        window.read_to_end(&mut buf).expect("read within window");
213
214        assert_eq!(buf, b"3456");
215    }
216
217    #[test]
218    fn read_stops_at_the_window_end_not_the_source_end() {
219        let src = source(b"0123456789");
220        let window = SectionReader::new(src, 0, 4);
221
222        let mut buf = [0u8; 10];
223        let mut cursor = window;
224        let read = cursor.read(&mut buf).expect("bounded read");
225
226        assert_eq!(read, 4);
227        assert_eq!(&buf[..4], b"0123");
228    }
229
230    #[test]
231    fn positions_are_window_relative() {
232        let src = source(b"0123456789");
233        let mut window = SectionReader::new(src, 5, 5);
234
235        assert_eq!(window.seek(SeekFrom::Start(0)).expect("seek start"), 0);
236        let mut byte = [0u8; 1];
237        window.read_exact(&mut byte).expect("read first byte");
238        assert_eq!(&byte, b"5");
239
240        assert_eq!(window.seek(SeekFrom::End(-1)).expect("seek end"), 4);
241        window.read_exact(&mut byte).expect("read last byte");
242        assert_eq!(&byte, b"9");
243    }
244
245    #[test]
246    fn seeking_before_the_window_start_is_rejected() {
247        let src = source(b"0123456789");
248        let mut window = SectionReader::new(src, 5, 5);
249
250        let err = window
251            .seek(SeekFrom::Current(-1))
252            .expect_err("negative seek");
253
254        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
255    }
256
257    #[test]
258    fn seeking_past_the_end_reads_nothing() {
259        let src = source(b"0123456789");
260        let mut window = SectionReader::new(src, 0, 4);
261
262        window.seek(SeekFrom::Start(99)).expect("seek past end");
263        let mut buf = [0u8; 4];
264
265        assert_eq!(window.read(&mut buf).expect("read past end"), 0);
266    }
267
268    #[test]
269    fn sections_nest_against_the_same_source() {
270        let src = source(b"0123456789");
271        let outer = SectionReader::new(src, 2, 6);
272
273        let inner = outer.section(1, 3).expect("nested window in bounds");
274
275        assert_eq!(inner.read_all().expect("read nested"), b"345");
276    }
277
278    #[test]
279    fn a_section_reaching_past_its_parent_is_rejected() {
280        let src = source(b"0123456789");
281        let outer = SectionReader::new(src, 2, 4);
282
283        assert!(outer.section(2, 3).is_none());
284        assert!(outer.section(0, 5).is_none());
285        assert!(outer.section(u64::MAX, 1).is_none());
286    }
287
288    #[test]
289    fn independent_cursors_share_one_handle_without_interfering() {
290        // The reason this type exists: File::try_clone would share one OS
291        // seek cursor and let these two reads corrupt each other.
292        let src = source(b"0123456789");
293        let mut first = SectionReader::new(Arc::clone(&src), 0, 5);
294        let mut second = SectionReader::new(src, 5, 5);
295
296        let mut a = [0u8; 2];
297        let mut b = [0u8; 2];
298        first.read_exact(&mut a).expect("first window read");
299        second.read_exact(&mut b).expect("second window read");
300        assert_eq!(&a, b"01");
301        assert_eq!(&b, b"56");
302
303        first.read_exact(&mut a).expect("first window resumes");
304        second.read_exact(&mut b).expect("second window resumes");
305        assert_eq!(&a, b"23");
306        assert_eq!(&b, b"78");
307    }
308
309    #[test]
310    fn read_all_ignores_the_current_position() {
311        let src = source(b"0123456789");
312        let mut window = SectionReader::new(src, 0, 4);
313        window.seek(SeekFrom::Start(2)).expect("advance cursor");
314
315        assert_eq!(window.read_all().expect("read all"), b"0123");
316    }
317
318    #[test]
319    fn an_empty_window_reads_nothing() {
320        let src = source(b"0123456789");
321        let window = SectionReader::new(src, 4, 0);
322
323        assert!(window.is_empty());
324        assert_eq!(window.read_all().expect("read empty"), b"");
325    }
326}