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    pub fn read_all(&self) -> io::Result<Vec<u8>> {
111        let capacity = usize::try_from(self.len).map_err(|_| {
112            io::Error::new(
113                io::ErrorKind::InvalidData,
114                "section length exceeds addressable memory",
115            )
116        })?;
117        let mut buf = vec![0u8; capacity];
118        // A fresh cursor rather than `self.clone()`: cloning preserves the
119        // current position, which would make this read short.
120        let mut cursor = Self {
121            source: Arc::clone(&self.source),
122            start: self.start,
123            len: self.len,
124            pos: 0,
125        };
126        cursor.read_exact(&mut buf)?;
127        Ok(buf)
128    }
129}
130
131impl<R: Read + Seek> Read for SectionReader<R> {
132    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
133        let remaining = self.len.saturating_sub(self.pos);
134        if remaining == 0 || buf.is_empty() {
135            return Ok(0);
136        }
137
138        // Clamp the request to what is left inside the window. usize::MAX is
139        // a safe saturation point: buf can never be longer than that anyway.
140        let cap = usize::try_from(remaining).unwrap_or(usize::MAX);
141        let take = buf.len().min(cap);
142
143        let offset = self
144            .start
145            .checked_add(self.pos)
146            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "section offset overflow"))?;
147
148        // A poisoned lock means another thread panicked while holding it. The
149        // handle itself is still usable here because every read seeks first,
150        // so a stale cursor left behind by that panic cannot affect us.
151        let mut source = self
152            .source
153            .lock()
154            .unwrap_or_else(|poisoned| poisoned.into_inner());
155        source.seek(SeekFrom::Start(offset))?;
156        let read = source.read(&mut buf[..take])?;
157        drop(source);
158
159        self.pos = self.pos.saturating_add(
160            u64::try_from(read)
161                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "read length overflow"))?,
162        );
163        Ok(read)
164    }
165}
166
167impl<R: Read + Seek> Seek for SectionReader<R> {
168    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
169        let target = match pos {
170            SeekFrom::Start(offset) => i128::from(offset),
171            SeekFrom::End(offset) => i128::from(self.len) + i128::from(offset),
172            SeekFrom::Current(offset) => i128::from(self.pos) + i128::from(offset),
173        };
174
175        if target < 0 {
176            return Err(io::Error::new(
177                io::ErrorKind::InvalidInput,
178                "cannot seek before the start of a section",
179            ));
180        }
181
182        // Seeking past the end is legal and mirrors std's file behaviour:
183        // the position moves, and subsequent reads return zero bytes.
184        self.pos = u64::try_from(target)
185            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "seek position overflow"))?;
186        Ok(self.pos)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::io::Cursor;
194
195    fn source(bytes: &[u8]) -> Arc<Mutex<Cursor<Vec<u8>>>> {
196        Arc::new(Mutex::new(Cursor::new(bytes.to_vec())))
197    }
198
199    #[test]
200    fn reads_only_within_the_window() {
201        let src = source(b"0123456789");
202        let mut window = SectionReader::new(src, 3, 4);
203
204        let mut buf = Vec::new();
205        window.read_to_end(&mut buf).expect("read within window");
206
207        assert_eq!(buf, b"3456");
208    }
209
210    #[test]
211    fn read_stops_at_the_window_end_not_the_source_end() {
212        let src = source(b"0123456789");
213        let window = SectionReader::new(src, 0, 4);
214
215        let mut buf = [0u8; 10];
216        let mut cursor = window;
217        let read = cursor.read(&mut buf).expect("bounded read");
218
219        assert_eq!(read, 4);
220        assert_eq!(&buf[..4], b"0123");
221    }
222
223    #[test]
224    fn positions_are_window_relative() {
225        let src = source(b"0123456789");
226        let mut window = SectionReader::new(src, 5, 5);
227
228        assert_eq!(window.seek(SeekFrom::Start(0)).expect("seek start"), 0);
229        let mut byte = [0u8; 1];
230        window.read_exact(&mut byte).expect("read first byte");
231        assert_eq!(&byte, b"5");
232
233        assert_eq!(window.seek(SeekFrom::End(-1)).expect("seek end"), 4);
234        window.read_exact(&mut byte).expect("read last byte");
235        assert_eq!(&byte, b"9");
236    }
237
238    #[test]
239    fn seeking_before_the_window_start_is_rejected() {
240        let src = source(b"0123456789");
241        let mut window = SectionReader::new(src, 5, 5);
242
243        let err = window
244            .seek(SeekFrom::Current(-1))
245            .expect_err("negative seek");
246
247        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
248    }
249
250    #[test]
251    fn seeking_past_the_end_reads_nothing() {
252        let src = source(b"0123456789");
253        let mut window = SectionReader::new(src, 0, 4);
254
255        window.seek(SeekFrom::Start(99)).expect("seek past end");
256        let mut buf = [0u8; 4];
257
258        assert_eq!(window.read(&mut buf).expect("read past end"), 0);
259    }
260
261    #[test]
262    fn sections_nest_against_the_same_source() {
263        let src = source(b"0123456789");
264        let outer = SectionReader::new(src, 2, 6);
265
266        let inner = outer.section(1, 3).expect("nested window in bounds");
267
268        assert_eq!(inner.read_all().expect("read nested"), b"345");
269    }
270
271    #[test]
272    fn a_section_reaching_past_its_parent_is_rejected() {
273        let src = source(b"0123456789");
274        let outer = SectionReader::new(src, 2, 4);
275
276        assert!(outer.section(2, 3).is_none());
277        assert!(outer.section(0, 5).is_none());
278        assert!(outer.section(u64::MAX, 1).is_none());
279    }
280
281    #[test]
282    fn independent_cursors_share_one_handle_without_interfering() {
283        // The reason this type exists: File::try_clone would share one OS
284        // seek cursor and let these two reads corrupt each other.
285        let src = source(b"0123456789");
286        let mut first = SectionReader::new(Arc::clone(&src), 0, 5);
287        let mut second = SectionReader::new(src, 5, 5);
288
289        let mut a = [0u8; 2];
290        let mut b = [0u8; 2];
291        first.read_exact(&mut a).expect("first window read");
292        second.read_exact(&mut b).expect("second window read");
293        assert_eq!(&a, b"01");
294        assert_eq!(&b, b"56");
295
296        first.read_exact(&mut a).expect("first window resumes");
297        second.read_exact(&mut b).expect("second window resumes");
298        assert_eq!(&a, b"23");
299        assert_eq!(&b, b"78");
300    }
301
302    #[test]
303    fn read_all_ignores_the_current_position() {
304        let src = source(b"0123456789");
305        let mut window = SectionReader::new(src, 0, 4);
306        window.seek(SeekFrom::Start(2)).expect("advance cursor");
307
308        assert_eq!(window.read_all().expect("read all"), b"0123");
309    }
310
311    #[test]
312    fn an_empty_window_reads_nothing() {
313        let src = source(b"0123456789");
314        let window = SectionReader::new(src, 4, 0);
315
316        assert!(window.is_empty());
317        assert_eq!(window.read_all().expect("read empty"), b"");
318    }
319}