Skip to main content

readcon_core/
trajectory.rs

1//! Random access to the frames of one CON file.
2
3use std::path::Path;
4
5use crate::compression::{self, Compression, FileContents};
6use crate::error::ParseError;
7use crate::frame_index::{self, FrameIndex};
8use crate::iterators::ConFrameIterator;
9use crate::types::ConFrame;
10
11/// The contents of one file and its checked [`FrameIndex`], kept for
12/// repeated reads by frame number.
13///
14/// Uncompressed input reuses `path.rcidx` when it matches the file and
15/// scans otherwise; `write_index` writes a fresh scan back. gzip and zstd
16/// input is decompressed once and indexed in memory only, with no sidecar.
17pub struct ConTrajectory {
18    contents: FileContents,
19    index: FrameIndex,
20}
21
22impl ConTrajectory {
23    /// Read `path` once, check it as UTF-8 and load or scan its index.
24    pub fn open(path: &Path, write_index: bool) -> Result<Self, Box<dyn std::error::Error>> {
25        let (contents, compression) = compression::read_file_contents_detected(path)?;
26        let text = contents.as_str_sliced()?;
27        let index = if compression == Compression::None {
28            frame_index::load_or_build(path, text, write_index)?
29        } else {
30            FrameIndex::scan(text)?
31        };
32        Ok(Self { contents, index })
33    }
34
35    pub fn len(&self) -> usize {
36        self.index.len()
37    }
38
39    pub fn is_empty(&self) -> bool {
40        self.index.is_empty()
41    }
42
43    /// Parse frame `i`. An index past the end is
44    /// [`ParseError::IndexOutOfBounds`].
45    pub fn frame(&self, i: usize) -> Result<ConFrame, ParseError> {
46        let out_of_range = ParseError::IndexOutOfBounds {
47            index: i,
48            len: self.len(),
49        };
50        let range = self.index.range(i).ok_or(out_of_range)?;
51        let text = self.text().get(range).ok_or(ParseError::IncompleteFrame)?;
52        ConFrameIterator::new(text)
53            .next()
54            .unwrap_or(Err(ParseError::IncompleteFrame))
55    }
56
57    fn text(&self) -> &str {
58        // SAFETY: `open` checked every byte with `as_str_sliced` and the
59        // contents are never written. A mapped file must not change while
60        // mapped, as for every mapped reader in this crate.
61        unsafe { std::str::from_utf8_unchecked(self.contents.as_bytes()) }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn offset_inside_a_character_is_an_error() {
71        let text = "é\n".repeat(4);
72        let traj = ConTrajectory {
73            contents: FileContents::Owned(text.clone()),
74            index: FrameIndex {
75                offsets: vec![0, 1],
76                data_len: text.len() as u64,
77            },
78        };
79        assert!(matches!(traj.frame(0), Err(ParseError::IncompleteFrame)));
80        assert!(matches!(traj.frame(1), Err(ParseError::IncompleteFrame)));
81    }
82}