Skip to main content

buf_read_ext/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::io::{self, BufRead};
6
7/// Extension trait for `std::io::BufRead`.
8pub trait BufReadExt: BufRead {
9    /// Returns a lending iterator over the lines of this reader.
10    ///
11    /// Unlike `std::io::BufRead::lines`, this iterator reuses a single `String` buffer
12    /// and yields lines as `&str` references bound to the lifetime of the iterator's `next` call.
13    /// This avoids allocating a new string for every line.
14    fn lending_lines(&mut self) -> LendingLines<'_, Self>
15    where
16        Self: Sized,
17    {
18        LendingLines { reader: self, buffer: String::new() }
19    }
20}
21
22impl<R: BufRead> BufReadExt for R {}
23
24/// A lending iterator over the lines of a `BufRead` reader.
25///
26/// See [`BufReadExt::lending_lines`] for more details.
27pub struct LendingLines<'a, R: BufRead> {
28    reader: &'a mut R,
29    buffer: String,
30}
31
32impl<'a, R: BufRead> LendingLines<'a, R> {
33    /// Returns the next line from the reader, or `None` if the end of the file has been reached.
34    /// The returned string slice is valid until the next call to `next`.
35    pub fn next(&mut self) -> Option<io::Result<&str>> {
36        self.buffer.clear();
37        match self.reader.read_line(&mut self.buffer) {
38            Ok(0) => None,
39            Ok(_) => {
40                let mut len = self.buffer.len();
41                if len > 0 && self.buffer.as_bytes()[len - 1] == b'\n' {
42                    len -= 1;
43                    if len > 0 && self.buffer.as_bytes()[len - 1] == b'\r' {
44                        len -= 1;
45                    }
46                }
47                Some(Ok(&self.buffer[..len]))
48            }
49            Err(e) => Some(Err(e)),
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use std::io::Cursor;
58
59    #[test]
60    fn test_lending_lines_empty() {
61        let data = "";
62        let mut cursor = Cursor::new(data);
63        let mut lines = cursor.lending_lines();
64
65        assert!(lines.next().is_none());
66    }
67
68    #[test]
69    fn test_lending_lines() {
70        let data = "line1\nline2\r\nline3";
71        let mut cursor = Cursor::new(data);
72        let mut lines = cursor.lending_lines();
73
74        assert_eq!(lines.next().unwrap().unwrap(), "line1");
75        assert_eq!(lines.next().unwrap().unwrap(), "line2");
76        assert_eq!(lines.next().unwrap().unwrap(), "line3");
77        assert!(lines.next().is_none());
78    }
79}