use anyhow::Result;
use std::path::Path;
use tokio::fs;
pub async fn include_file_content(input: &str, base_dir: &Path) -> Result<String> {
let matches = vtcode_commons::at_pattern::find_at_patterns(input);
if matches.is_empty() {
return Ok(input.to_string());
}
let mut result = String::new();
let mut last_end = 0;
for m in matches {
if m.start > last_end {
result.push_str(&input[last_end..m.start]);
}
if !vtcode_commons::paths::is_safe_relative_path(m.path) {
result.push_str(m.full_match);
last_end = m.end;
continue;
}
let file_path = base_dir.join(m.path.trim());
match fs::read_to_string(&file_path).await {
Ok(file_content) => {
result.push_str(&file_content);
}
Err(_) => {
result.push_str(m.full_match);
}
}
last_end = m.end;
}
if last_end < input.len() {
result.push_str(&input[last_end..]);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
#[tokio::test]
async fn test_include_file_content_with_text_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let mut temp_file = std::io::BufWriter::new(std::fs::File::create(&file_path).unwrap());
writeln!(temp_file, "This is test file content").unwrap();
temp_file.flush().unwrap();
let input = format!(
"Look at this file: @{}",
file_path.file_name().unwrap().to_string_lossy()
);
let result = include_file_content(&input, temp_dir.path()).await.unwrap();
assert!(result.contains("This is test file content"));
assert!(!result.contains('@')); }
#[tokio::test]
async fn test_include_file_content_regular_text() {
let temp_dir = TempDir::new().unwrap();
let input = "This is just regular text with @ symbol not followed by file";
let result = include_file_content(input, temp_dir.path()).await.unwrap();
assert_eq!(result, input);
}
#[tokio::test]
async fn test_include_file_content_invalid_file() {
let temp_dir = TempDir::new().unwrap();
let input = "Look at @nonexistent.txt which doesn't exist";
let result = include_file_content(input, temp_dir.path()).await.unwrap();
assert_eq!(result, input);
}
#[tokio::test]
async fn test_include_file_content_with_quoted_path() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("file with spaces.txt");
let mut temp_file = std::io::BufWriter::new(std::fs::File::create(&file_path).unwrap());
writeln!(temp_file, "Content with spaces in filename").unwrap();
temp_file.flush().unwrap();
let input = format!(
"Look at this file: @\"{}\"",
file_path.file_name().unwrap().to_string_lossy()
);
let result = include_file_content(&input, temp_dir.path()).await.unwrap();
assert!(result.contains("Content with spaces in filename"));
}
#[test]
fn test_is_safe_relative_path() {
use vtcode_commons::paths::is_safe_relative_path;
assert!(!is_safe_relative_path("../../etc/passwd"));
assert!(!is_safe_relative_path("../file.txt"));
assert!(is_safe_relative_path("file.txt"));
assert!(is_safe_relative_path("./path/file.txt"));
assert!(is_safe_relative_path(" path with spaces .txt "));
}
}