#![allow(clippy::manual_string_new)]
use std::path::Path;
use crate::cm_tools::workspace::path::WorkspacePathError;
use super::path::{
path_for_tool_display, resolve_for_read_open, tool_user_error_from_workspace_path,
};
use super::read_file_pipeline::{
ReadFileBodyError, ReadFileLinesDispatch, ReadFileLinesResult, ReadFileLinesSpec,
dispatch_read_file_lines, format_encoding_header, guard_read_file_count_total_size,
maybe_count_total_lines_for_read, resolve_read_end_line, sniff_opened_file_encoding,
};
#[path = "read_tool_parse.rs"]
mod read_tool_parse;
use read_tool_parse::{ReadFileParsedArgs, parse_read_file_args};
const READ_FILE_NOT_FOUND_LAYOUT_HINT: &str = "\n\n提示:若路径不存在,常见于误猜 Rust 布局——模块多为 `…/模块名/mod.rs` 而非同级 `…/模块名.rs`;workspace 子 crate 入口可能在包根 `lib.rs`(或 `Cargo.toml` 的 `[lib] path`),未必有 `src/lib.rs`。请先用 read_dir / glob_files / list_tree 确认真实路径后再 read_file。";
fn read_file_missing_path_hint_eligible(e: &WorkspacePathError) -> bool {
match e {
WorkspacePathError::PathResolveFailed(io)
| WorkspacePathError::WorkspacePathInvalid(io)
| WorkspacePathError::NormalizationFailed(io) => io.kind() == std::io::ErrorKind::NotFound,
WorkspacePathError::NoExistingAncestor => true,
_ => false,
}
}
fn read_file_workspace_tool_error(e: WorkspacePathError) -> crate::cm_tools::tool_result::ToolError {
let code: &'static str = match e.kind() {
"empty_path" => "read_file_workspace_empty_path",
"absolute_path_not_allowed" => "read_file_workspace_absolute_path_not_allowed",
"workspace_set_path_empty" => "read_file_workspace_set_path_empty",
"current_dir_unavailable" => "read_file_workspace_current_dir_unavailable",
"workspace_path_invalid" => "read_file_workspace_path_invalid",
"path_resolve_failed" => "read_file_workspace_path_resolve_failed",
"workspace_resolve_failed" => "read_file_workspace_resolve_failed",
"web_effective_workspace_unset" => "read_file_workspace_web_unset",
"not_a_directory" => "read_file_workspace_not_a_directory",
"sensitive_path_denied" => "read_file_workspace_sensitive_path_denied",
"effective_root_sensitive" => "read_file_workspace_effective_root_sensitive",
"outside_allowed_roots" => "read_file_workspace_outside_allowed_roots",
"effective_root_outside_allowed" => "read_file_workspace_effective_root_outside_allowed",
"outside_workspace_root" => "read_file_workspace_outside_workspace_root",
"path_normalize_failed" => "read_file_workspace_path_normalize_failed",
"no_existing_ancestor" => "read_file_workspace_no_existing_ancestor",
_ => "read_file_workspace_other",
};
crate::cm_tools::tool_result::ToolError::external_code(code, tool_user_error_from_workspace_path(e))
}
fn read_file_workspace_tool_error_maybe_hint(
e: WorkspacePathError,
) -> crate::cm_tools::tool_result::ToolError {
let attach = read_file_missing_path_hint_eligible(&e);
let mut err = read_file_workspace_tool_error(e);
if attach {
err.message.push_str(READ_FILE_NOT_FOUND_LAYOUT_HINT);
}
err
}
fn prepend_read_file_output_header(body: &str, meta: &ReadFileOutputMeta<'_>) -> String {
crate::cm_tools::tool_result::prepend_crabmate_tool_output(
"read_file",
crate::cm_tools::tool_result::ReadFileOutputFields {
path: path_for_tool_display(meta.working_dir, meta.target, Some(meta.user_path)),
start_line: meta.start_line,
end_line_shown: meta.end_line_shown,
line_count_returned: meta.line_count_returned,
total_lines: meta.total_lines,
truncated_by_max_lines: meta.truncated_by_max_lines,
has_more: meta.has_more,
file_empty: meta.file_empty,
},
body,
)
}
struct ReadFileOutputMeta<'a> {
working_dir: &'a Path,
target: &'a Path,
user_path: &'a str,
start_line: usize,
end_line_shown: usize,
line_count_returned: usize,
total_lines: Option<usize>,
truncated_by_max_lines: bool,
has_more: bool,
file_empty: bool,
}
fn read_file_logical_cache_key(canonical: &std::path::Path, v: &serde_json::Value) -> String {
let mut start_line = v.get("start_line").and_then(|n| n.as_u64()).unwrap_or(1);
let mut end_line_opt = v.get("end_line").and_then(|n| n.as_u64());
if let Some(e) = end_line_opt.as_mut()
&& *e < start_line
{
std::mem::swap(&mut start_line, e);
}
let end_line = end_line_opt
.map(|n| n.to_string())
.unwrap_or_else(|| "none".to_string());
let max_lines = v.get("max_lines").and_then(|n| n.as_u64()).unwrap_or(500);
let count_total = v
.get("count_total_lines")
.and_then(|b| b.as_bool())
.unwrap_or(false);
let enc = v
.get("encoding")
.and_then(|x| x.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "utf-8".to_string());
format!(
"{}|sl={}|el={}|ml={}|ct={}|enc={}",
canonical.display(),
start_line,
end_line,
max_lines,
count_total,
enc
)
}
fn read_file_build_empty_response(
working_dir: &Path,
target: &Path,
path: &str,
start_line: usize,
) -> String {
let body = format!(
"文件为空: {}",
path_for_tool_display(working_dir, target, Some(path))
);
prepend_read_file_output_header(
&body,
&ReadFileOutputMeta {
working_dir,
target,
user_path: path,
start_line,
end_line_shown: 0,
line_count_returned: 0,
total_lines: Some(0),
truncated_by_max_lines: false,
has_more: false,
file_empty: true,
},
)
}
#[allow(clippy::result_large_err)]
fn read_file_require_regular_file(
meta: &std::fs::Metadata,
) -> Result<(), crate::cm_tools::tool_result::ToolError> {
if meta.is_file() {
return Ok(());
}
let msg = if meta.is_dir() {
"错误:路径指向目录而非文件,read_file 无法读取目录;请对该路径使用 read_dir,或读取目录内的具体文件(例如某个 .rs 文件或常见入口 mod.rs)。"
.to_string()
} else {
"错误:路径不是文件或不存在,无法读取".to_string()
};
Err(crate::cm_tools::tool_result::ToolError::external_code(
"read_file_not_file",
msg,
))
}
fn read_file_turn_cache_get(
ctx: &super::super::ToolContext<'_>,
cache_key: &str,
meta: &std::fs::Metadata,
) -> Option<String> {
let cache = ctx.read_file_turn_cache?;
let modified = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
cache.try_get(cache_key, modified, meta.len())
}
fn read_file_turn_cache_put(
ctx: &super::super::ToolContext<'_>,
cache_key: String,
meta: &std::fs::Metadata,
out: &str,
) {
let Some(cache) = ctx.read_file_turn_cache else {
return;
};
let modified = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
cache.insert(cache_key, modified, meta.len(), out.to_string());
}
#[allow(clippy::result_large_err)]
fn read_file_pipeline_body(
working_dir: &Path,
target: &Path,
path: &str,
parsed: &ReadFileParsedArgs,
file: std::fs::File,
) -> Result<String, crate::cm_tools::tool_result::ToolError> {
let ReadFileParsedArgs {
enc_name,
start_line,
end_line_opt,
max_lines,
count_total,
..
} = parsed;
let (resolved, decode_note, file, head) = sniff_opened_file_encoding(file, *enc_name)?;
let total_lines = maybe_count_total_lines_for_read(*count_total, target, *enc_name)?;
let (end_line, truncated_by_max) =
resolve_read_end_line(*start_line, *end_line_opt, *max_lines);
let enc_header = format_encoding_header(&decode_note);
let line_spec = ReadFileLinesSpec {
start_line: *start_line,
end_line,
max_lines: *max_lines,
total_lines: total_lines.as_ref(),
truncated_by_max,
};
let lines_result = dispatch_read_file_lines(
resolved,
ReadFileLinesDispatch {
working_dir,
target,
path,
enc_name: *enc_name,
line_spec: &line_spec,
enc_header: enc_header.as_str(),
},
file,
head,
)
.map_err(ReadFileBodyError::into_tool_error)?;
let ReadFileLinesResult {
raw_body,
end_line_shown,
line_count_returned,
has_more,
} = lines_result;
Ok(prepend_read_file_output_header(
&raw_body,
&ReadFileOutputMeta {
working_dir,
target,
user_path: path,
start_line: *start_line,
end_line_shown,
line_count_returned,
total_lines,
truncated_by_max_lines: truncated_by_max,
has_more,
file_empty: false,
},
))
}
#[allow(clippy::result_large_err)]
pub fn read_file_try(
args_json: &str,
working_dir: &Path,
ctx: &super::super::ToolContext<'_>,
) -> Result<String, crate::cm_tools::tool_result::ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json)
.map_err(crate::cm_tools::tool_result::ToolError::invalid_args)?;
let parsed = parse_read_file_args(&v)?;
let path = parsed.path.as_str();
let opened = resolve_for_read_open(working_dir, path)
.map_err(read_file_workspace_tool_error_maybe_hint)?;
read_file_require_regular_file(&opened.metadata)?;
let target = opened.resolved_path;
let meta = opened.metadata;
let cache_key = read_file_logical_cache_key(&target, &v);
if let Some(hit) = read_file_turn_cache_get(ctx, &cache_key, &meta) {
return Ok(hit);
}
if meta.len() == 0 {
let out = read_file_build_empty_response(working_dir, &target, path, parsed.start_line);
read_file_turn_cache_put(ctx, cache_key, &meta, &out);
return Ok(out);
}
guard_read_file_count_total_size(meta.len(), parsed.count_total)?;
let out = read_file_pipeline_body(working_dir, &target, path, &parsed, opened.file)?;
read_file_turn_cache_put(ctx, cache_key, &meta, &out);
Ok(out)
}
pub fn read_file(
args_json: &str,
working_dir: &Path,
ctx: &super::super::ToolContext<'_>,
) -> String {
match read_file_try(args_json, working_dir, ctx) {
Ok(s) => s,
Err(e) => e.message,
}
}
#[cfg(test)]
pub fn strip_read_file_output_header_for_tests(s: &str) -> &str {
if s.starts_with("{\"kind\":\"crabmate_tool_output\",\"tool\":\"read_file\"")
&& let Some(idx) = s.find('\n')
{
return &s[idx + 1..];
}
s
}