Skip to main content

reading_liner/stream/
stream.rs

1use crate::{
2    location::{Offset, line_column},
3    stream::alias::{Guard, IndexRef, MutGuard},
4};
5use std::io;
6
7/// A stream which can be used to convert between offsets and line-column locations.
8///
9/// The stream records line offsets into an external [crate::Index] while reading.
10/// This allows `Stream` to support incremental location lookups without consuming
11/// the index.
12///
13/// `Stream` accepts an [`IndexRef`], so it can operate in either of two modes:
14///
15/// - exclusive ownership via `IndexRef::Direct(&mut Index)`;
16/// - aliased single-threaded sharing via `IndexRef::Shared(Rc<RefCell<Index>>)`,
17///   which is useful when multiple callers need to query the same index.
18///
19/// The shared mode is intentionally single-threaded: it relies on [`RefCell`]
20/// runtime borrow checking and does not provide `Sync`/`Send` guarantees.
21#[derive(Debug)]
22pub struct Stream<'index, Reader> {
23    reader: Reader,
24    index: IndexRef<'index>,
25
26    next_offset: Offset,
27    current_line: usize,
28    base: usize, // For future use
29}
30
31impl<'index, R> Stream<'index, R> {
32    pub fn new(reader: R, index: IndexRef<'index>) -> Self {
33        Self {
34            reader,
35            base: 0,
36            index,
37            next_offset: 0.into(),
38            current_line: 0,
39        }
40    }
41
42    pub fn get_ref(&self) -> &R {
43        &self.reader
44    }
45
46    #[inline]
47    pub fn base(&self) -> usize {
48        self.base
49    }
50
51    #[inline]
52    pub fn get_index(&self) -> Guard<'_> {
53        self.index.get()
54    }
55
56    #[inline]
57    pub fn get_index_mut(&mut self) -> MutGuard<'_> {
58        self.index.get_mut()
59    }
60}
61
62impl<'index, R: io::Read> Stream<'index, R> {
63    /// Read length
64    #[inline]
65    pub fn read_len(&self) -> usize {
66        self.next_offset.raw()
67    }
68
69    /// Try to get more bytes and update states
70    fn forward(&mut self, buf: &mut [u8]) -> io::Result<usize> {
71        let n = self.reader.read(buf)?;
72
73        for (offset, b) in buf.iter().take(n).enumerate() {
74            if *b == b'\n' {
75                self.current_line += 1;
76                let next_offset = self.next_offset;
77                self.get_index_mut().add_next_line(next_offset + offset + 1); // next line begin
78
79                continue;
80            }
81        }
82
83        // reached EoF, try to add fake ending
84        if !buf.is_empty() && n == 0 {
85            // TODO
86            let end = self.get_index().end();
87            let next_offset = self.next_offset;
88
89            match end {
90                Some(end) if end != next_offset => {
91                    self.get_index_mut().add_next_line(next_offset);
92                }
93                None => self.get_index_mut().add_next_line(next_offset),
94                _ => {}
95            }
96        }
97
98        self.next_offset += n;
99        Ok(n)
100    }
101
102    /// Locate the (line, column) position for a given byte `offset`.  
103    ///
104    /// NOTE: this method may cause extra reading when the offset input cannot find a location.
105    ///
106    /// This method first resolves the line index via [`locate_line`], then
107    /// computes the column by subtracting the starting offset of that line.
108    ///
109    /// # Parameters
110    /// - `offset`: The target byte offset.
111    /// - `buf`: A temporary buffer used for incremental reading.
112    ///
113    /// # Returns
114    /// - `Ok(ZeroBased(line, column))` if the offset is within bounds.
115    /// - `Err` if the offset exceeds EOF (propagated from [`locate_line`]).
116    ///
117    /// # Invariants
118    /// - The internal index always contains a valid starting offset for every line.
119    /// - Therefore, `line_offset(line)` must succeed for any valid `line`.
120    ///
121    /// # Notes
122    /// - Both line and column are zero-based.
123    /// - Column is computed in **bytes**, not characters (UTF-8 aware handling is not performed here).
124    pub fn locate(&mut self, offset: Offset, buf: &mut [u8]) -> io::Result<line_column::ZeroBased> {
125        let line = self.locate_line(offset, buf)?;
126        let line_offset = self.get_index().query().line_offset(line).unwrap();
127        let col = offset - line_offset;
128        Ok((line, col.raw()).into())
129    }
130
131    /// Locate the line index for a given byte `offset`.
132    ///
133    /// This method performs an incremental lookup:
134    /// it first queries the existing line index, and if the offset
135    /// is not covered, it reads more data and extends the index.
136    /// This method may cause extra reading when the offset input cannot find a location.
137    ///
138    /// # Invariants
139    /// - The internal index is non-empty and ends with a sentinel EOF offset.
140    ///
141    /// # Errors
142    /// Returns an error if `offset` exceeds EOF.
143    pub fn locate_line(&mut self, offset: Offset, buf: &mut [u8]) -> io::Result<usize> {
144        let mut begin = 0;
145        loop {
146            // Invariant: index is non-empty and ends with EOF.
147            // Therefore, begin <= query.count() always holds, and range_from(begin..)
148            // is guaranteed to be a valid slice (possibly containing only EOF).
149            if let Some(i) = self
150                .get_index()
151                .query()
152                .range_from(begin..)
153                .locate_line(offset)
154            {
155                break Ok(i); // look here the returned `i` is already `begin` based, there's no need to add an extra begin
156            }
157            begin = self.get_index().count();
158
159            if self.forward(buf)? == 0 {
160                break Err(io_error("Invalid offset, exceed EOF"));
161            }
162        }
163    }
164
165    /// Encode a (line, column) location into a byte `Offset`.
166    ///
167    /// This method may incrementally extend the internal line index by reading
168    /// additional data if the requested line is not yet available.
169    ///
170    /// # Behavior
171    /// - If the line is already indexed, the offset is computed directly.
172    /// - Otherwise, more data is read and the index is extended until the line
173    ///   becomes available or EOF is reached.
174    ///
175    /// # Returns
176    /// - `Ok(offset)` if the position can be resolved.
177    /// - `Err` if the line index exceeds EOF.
178    ///
179    /// # Notes
180    /// - Column is interpreted as a **byte offset** relative to the start of the line.
181    /// - This method does **not** validate whether the column lies within the bounds
182    ///   of the line.
183    pub fn encode(
184        &mut self,
185        line_index: line_column::ZeroBased,
186        buf: &mut [u8],
187    ) -> io::Result<Offset> {
188        let (line, col) = line_index.raw();
189        loop {
190            if let Some(offset) = self.get_index().query().line_offset(line) {
191                break Ok(offset + col);
192            }
193
194            if self.forward(buf)? == 0 {
195                break Err(io_error(format!("Invalid line index: ({}, {})", line, col)));
196            }
197        }
198    }
199
200    /// Drain the reader, consume the reader
201    pub fn drain(&mut self, buf: &mut [u8]) -> io::Result<()> {
202        loop {
203            let n = self.forward(buf)?;
204            if n == 0 {
205                return Ok(());
206            }
207        }
208    }
209}
210
211/// You can use [Stream] as a normal [io::Read] and recording index at the same time.
212impl<'index, R: io::Read> io::Read for Stream<'index, R> {
213    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
214        self.forward(buf)
215    }
216}
217
218#[inline]
219fn io_error<S: ToString>(msg: S) -> io::Error {
220    io::Error::new(io::ErrorKind::Other, msg.to_string())
221}
222
223#[cfg(test)]
224mod test {
225    #![allow(unused_must_use)]
226    use crate::Index;
227
228    use super::*;
229    use std::{
230        cell::RefCell,
231        io::{BufReader, Read},
232        rc::Rc,
233    };
234
235    static SRC: &'static str = "\nThis is s sim\nple test that\n I have to verify stream reader!";
236
237    #[test]
238    fn test_stream_str_buf() {
239        let mut index = Index::new();
240        let stream = Stream::new(SRC.as_bytes(), IndexRef::Direct(&mut index));
241        let mut reader = BufReader::new(stream);
242        let mut buf = String::new();
243        reader.read_to_string(&mut buf).unwrap();
244
245        let ans = reader.get_ref().get_index().query().locate(Offset(20));
246        assert!(ans.is_some());
247        assert_eq!(ans.unwrap(), (2, 5).into());
248    }
249
250    #[test]
251    fn test_stream_str_drain() {
252        let mut index = Index::new();
253        let mut stream = Stream::new(SRC.as_bytes(), IndexRef::Direct(&mut index));
254        let mut buf = vec![b'\0'; 10];
255        stream.drain(&mut buf);
256
257        let ans = stream.get_index().query().locate(Offset(20));
258        assert!(ans.is_some());
259        assert_eq!(ans.unwrap(), (2, 5).into());
260    }
261
262    #[test]
263    fn test_stream_str_incremental() {
264        let mut index = Index::new();
265        let mut stream = Stream::new(SRC.as_bytes(), IndexRef::Direct(&mut index));
266        let mut buf = vec![b'\0'; 10];
267
268        let ans = stream.locate(Offset(20), &mut buf);
269        assert!(ans.is_ok());
270        assert_eq!(ans.unwrap(), (2, 5).into());
271    }
272
273    #[test]
274    fn test_stream_str_incremental_rc() {
275        let index = Index::new();
276        let index = Rc::new(RefCell::new(index));
277
278        let mut stream = Stream::new(SRC.as_bytes(), IndexRef::Shared(index.clone()));
279        let mut buf = vec![b'\0'; 10];
280
281        let ans = stream.locate(Offset(20), &mut buf);
282        assert!(ans.is_ok());
283        assert_eq!(ans.unwrap(), (2, 5).into());
284
285        let ans = index.borrow().query().locate(Offset(20));
286        assert!(ans.is_some());
287        assert_eq!(ans.unwrap(), (2, 5).into());
288    }
289}