Skip to main content

harness_read/
lines.rs

1use std::path::Path;
2use tokio::fs::File;
3use tokio::io::{AsyncBufReadExt, BufReader};
4
5use crate::constants::{max_line_suffix, MAX_BYTES, MAX_LINE_LENGTH};
6
7#[derive(Debug, Clone)]
8pub struct StreamLinesOptions {
9    pub offset: usize,
10    pub limit: usize,
11    pub max_bytes: Option<usize>,
12    pub max_line_length: Option<usize>,
13}
14
15#[derive(Debug, Clone)]
16pub struct StreamLinesResult {
17    pub lines: Vec<String>,
18    pub total_lines: usize,
19    pub offset: usize,
20    pub more: bool,
21    pub byte_cap: bool,
22}
23
24pub async fn stream_lines(path: &Path, opts: StreamLinesOptions) -> std::io::Result<StreamLinesResult> {
25    let max_bytes = opts.max_bytes.unwrap_or(MAX_BYTES);
26    let max_line_len = opts.max_line_length.unwrap_or(MAX_LINE_LENGTH);
27    let start = opts.offset.saturating_sub(1);
28
29    let f = File::open(path).await?;
30    let mut reader = BufReader::new(f);
31    let mut buf = String::new();
32    let mut out: Vec<String> = Vec::new();
33    let mut bytes = 0usize;
34    let mut total_lines = 0usize;
35    let mut more = false;
36    let mut byte_cap = false;
37    let suffix = max_line_suffix();
38
39    loop {
40        buf.clear();
41        let n = reader.read_line(&mut buf).await?;
42        if n == 0 {
43            break;
44        }
45        total_lines += 1;
46        // Strip the trailing newline (but preserve content on missing newline).
47        if buf.ends_with('\n') {
48            buf.pop();
49            if buf.ends_with('\r') {
50                buf.pop();
51            }
52        }
53        if total_lines <= start {
54            continue;
55        }
56        if out.len() >= opts.limit {
57            more = true;
58            continue;
59        }
60
61        let truncated = if buf.len() > max_line_len {
62            let mut t = String::with_capacity(max_line_len + suffix.len());
63            t.push_str(safe_slice(&buf, max_line_len));
64            t.push_str(&suffix);
65            t
66        } else {
67            buf.clone()
68        };
69
70        let this_bytes = truncated.as_bytes().len() + if !out.is_empty() { 1 } else { 0 };
71        if bytes + this_bytes > max_bytes {
72            byte_cap = true;
73            more = true;
74            break;
75        }
76        out.push(truncated);
77        bytes += this_bytes;
78    }
79
80    Ok(StreamLinesResult {
81        lines: out,
82        total_lines,
83        offset: opts.offset,
84        more,
85        byte_cap,
86    })
87}
88
89/// Take the first `max` chars (UTF-8 code points) without breaking a
90/// multi-byte sequence. Falls back to byte slice when all ASCII.
91fn safe_slice(s: &str, max: usize) -> &str {
92    if s.is_ascii() {
93        return &s[..max.min(s.len())];
94    }
95    let mut end = 0usize;
96    for (i, _) in s.char_indices().take(max) {
97        end = i;
98    }
99    // Include the last character fully.
100    let last = s[end..].chars().next().map(|c| c.len_utf8()).unwrap_or(0);
101    &s[..end + last]
102}