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!(
88 "Look at this file: @{}",
89 file_path.file_name().unwrap().to_string_lossy()
90 );
91
92 let result = include_file_content(&input, temp_dir.path()).await.unwrap();
93
94 assert!(result.contains("This is test file content"));
96 assert!(!result.contains('@')); }
98
99 #[tokio::test]
100 async fn test_include_file_content_regular_text() {
101 let temp_dir = TempDir::new().unwrap();
102 let input = "This is just regular text with @ symbol not followed by file";
103
104 let result = include_file_content(input, temp_dir.path()).await.unwrap();
105
106 assert_eq!(result, input);
108 }
109
110 #[tokio::test]
111 async fn test_include_file_content_invalid_file() {
112 let temp_dir = TempDir::new().unwrap();
113 let input = "Look at @nonexistent.txt which doesn't exist";
114
115 let result = include_file_content(input, temp_dir.path()).await.unwrap();
116
117 assert_eq!(result, input);
119 }
120
121 #[tokio::test]
122 async fn test_include_file_content_with_quoted_path() {
123 let temp_dir = TempDir::new().unwrap();
124 let file_path = temp_dir.path().join("file with spaces.txt");
125
126 let mut temp_file = std::io::BufWriter::new(std::fs::File::create(&file_path).unwrap());
128 writeln!(temp_file, "Content with spaces in filename").unwrap();
129 temp_file.flush().unwrap();
130
131 let input = format!(
132 "Look at this file: @\"{}\"",
133 file_path.file_name().unwrap().to_string_lossy()
134 );
135
136 let result = include_file_content(&input, temp_dir.path()).await.unwrap();
137
138 assert!(result.contains("Content with spaces in filename"));
140 }
141
142 #[test]
143 fn test_is_safe_relative_path() {
144 use vtcode_commons::paths::is_safe_relative_path;
145 assert!(!is_safe_relative_path("../../etc/passwd"));
146 assert!(!is_safe_relative_path("../file.txt"));
147 assert!(is_safe_relative_path("file.txt"));
148 assert!(is_safe_relative_path("./path/file.txt"));
149 assert!(is_safe_relative_path(" path with spaces .txt "));
150 }
151}