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 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
89fn 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 let last = s[end..].chars().next().map(|c| c.len_utf8()).unwrap_or(0);
101 &s[..end + last]
102}