1use std::path::Path;
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6use super::path::unrestricted_path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ReadArgs {
10 pub path: String,
11 #[serde(default)]
12 pub offset: usize,
13 pub limit: Option<usize>,
14}
15
16pub async fn read_text_file(root: &Path, args: &ReadArgs, max_lines: usize) -> Result<String> {
17 read_text_file_bounded(root, args, max_lines, usize::MAX).await
18}
19
20pub async fn read_text_file_bounded(
21 root: &Path,
22 args: &ReadArgs,
23 max_lines: usize,
24 max_bytes: usize,
25) -> Result<String> {
26 let path = unrestricted_path(root, &args.path, true)?;
27 let bytes = tokio::fs::read(&path)
28 .await
29 .with_context(|| format!("read {}", path.display()))?;
30 if bytes.contains(&0) {
31 anyhow::bail!("binary file cannot be read as text: {}", args.path);
32 }
33 let source = String::from_utf8(bytes)
34 .with_context(|| format!("file is not valid UTF-8: {}", args.path))?;
35 let lines = source.lines().collect::<Vec<_>>();
36 let limit = args.limit.unwrap_or(max_lines).min(max_lines);
37 let selected = lines.iter().skip(args.offset).take(limit);
38 let mut output = selected
39 .enumerate()
40 .map(|(index, line)| format!("{}: {line}", args.offset + index + 1))
41 .collect::<Vec<_>>()
42 .join("\n");
43 let consumed = args.offset.saturating_add(limit).min(lines.len());
44 let remaining = lines.len().saturating_sub(consumed);
45 if remaining > 0 {
46 if !output.is_empty() {
47 output.push('\n');
48 }
49 output.push_str(&format!(
50 "[truncated; {remaining} more line{}]",
51 if remaining == 1 { "" } else { "s" }
52 ));
53 }
54 if output.len() > max_bytes {
55 let mut end = max_bytes.min(output.len());
56 while end > 0 && !output.is_char_boundary(end) {
57 end -= 1;
58 }
59 output.truncate(end);
60 output.push_str("\n[truncated by output byte limit]");
61 }
62 Ok(output)
63}