vtcode_core/utils/
file_workflow.rs1use anyhow::Result;
7use std::path::Path;
8use tokio::fs;
9
10pub async fn include_file_content(input: &str, base_dir: &Path) -> Result<String> {
24 let matches = vtcode_commons::at_pattern::find_at_patterns(input);
25 if matches.is_empty() {
26 return Ok(input.to_string());
27 }
28
29 let mut result = String::new();
30 let mut last_end = 0;
31
32 for m in matches {
33 if m.start > last_end {
35 result.push_str(&input[last_end..m.start]);
36 }
37
38 if !vtcode_commons::paths::is_safe_relative_path(m.path) {
40 result.push_str(m.full_match);
42 last_end = m.end;
43 continue;
44 }
45
46 let file_path = base_dir.join(m.path.trim());
48
49 match fs::read_to_string(&file_path).await {
50 Ok(file_content) => {
51 result.push_str(&file_content);
53 }
54 Err(_) => {
55 result.push_str(m.full_match);
57 }
58 }
59
60 last_end = m.end;
61 }
62
63 if last_end < input.len() {
65 result.push_str(&input[last_end..]);
66 }
67
68 Ok(result)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::io::Write;
75 use tempfile::TempDir;
76
77 #[tokio::test]
78 async fn test_include_file_content_with_text_file() {
79 let temp_dir = TempDir::new().unwrap();
80 let file_path = temp_dir.path().join("test.txt");
81
82 let mut temp_file = std::io::BufWriter::new(std::fs::File::create(&file_path).unwrap());
84 writeln!(temp_file, "This is test file content").unwrap();
85 temp_file.flush().unwrap();
86
87 let input = format!("Look at this file: @{}", file_path.file_name().unwrap().to_string_lossy());
88
89 let result = include_file_content(&input, temp_dir.path()).await.unwrap();
90
91 assert!(result.contains("This is test file content"));
93 assert!(!result.contains('@')); }
95
96 #[tokio::test]
97 async fn test_include_file_content_regular_text() {
98 let temp_dir = TempDir::new().unwrap();
99 let input = "This is just regular text with @ symbol not followed by file";
100
101 let result = include_file_content(input, temp_dir.path()).await.unwrap();
102
103 assert_eq!(result, input);
105 }
106
107 #[tokio::test]
108 async fn test_include_file_content_invalid_file() {
109 let temp_dir = TempDir::new().unwrap();
110 let input = "Look at @nonexistent.txt which doesn't exist";
111
112 let result = include_file_content(input, temp_dir.path()).await.unwrap();
113
114 assert_eq!(result, input);
116 }
117
118 #[tokio::test]
119 async fn test_include_file_content_with_quoted_path() {
120 let temp_dir = TempDir::new().unwrap();
121 let file_path = temp_dir.path().join("file with spaces.txt");
122
123 let mut temp_file = std::io::BufWriter::new(std::fs::File::create(&file_path).unwrap());
125 writeln!(temp_file, "Content with spaces in filename").unwrap();
126 temp_file.flush().unwrap();
127
128 let input = format!("Look at this file: @\"{}\"", file_path.file_name().unwrap().to_string_lossy());
129
130 let result = include_file_content(&input, temp_dir.path()).await.unwrap();
131
132 assert!(result.contains("Content with spaces in filename"));
134 }
135
136 #[test]
137 fn test_is_safe_relative_path() {
138 use vtcode_commons::paths::is_safe_relative_path;
139 assert!(!is_safe_relative_path("../../etc/passwd"));
140 assert!(!is_safe_relative_path("../file.txt"));
141 assert!(is_safe_relative_path("file.txt"));
142 assert!(is_safe_relative_path("./path/file.txt"));
143 assert!(is_safe_relative_path(" path with spaces .txt "));
144 }
145}