opendev_repl/file_injector/
processors.rs1use base64::Engine as _;
4use std::fs;
5use std::path::Path;
6
7use super::constants::*;
8use super::{FileContentInjector, ImageBlock};
9
10impl FileContentInjector {
11 pub(super) fn process_ref(
13 &self,
14 ref_str: &str,
15 path: &Path,
16 ) -> Result<(String, Option<ImageBlock>), String> {
17 if !path.exists() {
18 return Err("File not found".to_string());
19 }
20
21 if path.is_dir() {
22 return Ok((self.process_directory(path, ref_str), None));
23 }
24
25 let ext = ext_lower(path);
26
27 if ext == ".pdf" {
28 return Ok((Self::process_pdf(path, ref_str), None));
29 }
30
31 if IMAGE_EXTENSIONS.contains(&ext.as_str()) {
32 let (tag, block) = Self::process_image(path, ref_str);
33 return Ok((tag, block));
34 }
35
36 if Self::is_text_file(path) {
37 return Ok((Self::process_text_file(path, ref_str)?, None));
38 }
39
40 Err("Unsupported file type".to_string())
41 }
42
43 pub fn process_text_file(path: &Path, ref_str: &str) -> Result<String, String> {
45 let content = fs::read_to_string(path).map_err(|e| format!("Read error: {}", e))?;
46 let lines: Vec<&str> = content.lines().collect();
47 let line_count = lines.len();
48 let size = content.len() as u64;
49
50 if size > MAX_FILE_SIZE || line_count > MAX_LINES {
51 return Ok(Self::process_large_file(path, ref_str, &content, &lines));
52 }
53
54 let language = Self::get_language(path);
55 let lang_attr = if language.is_empty() {
56 String::new()
57 } else {
58 format!(" language=\"{}\"", language)
59 };
60
61 let abs_path = path.to_string_lossy();
62
63 Ok(format!(
64 "<file_content path=\"{}\" absolute_path=\"{}\" exists=\"true\"{}>\n{}\n</file_content>",
65 ref_str, abs_path, lang_attr, content
66 ))
67 }
68
69 pub fn process_large_file(
71 path: &Path,
72 ref_str: &str,
73 _content: &str,
74 lines: &[&str],
75 ) -> String {
76 let total_lines = lines.len();
77 let head: Vec<&str> = lines.iter().take(HEAD_LINES).copied().collect();
78 let tail_start = total_lines.saturating_sub(TAIL_LINES);
79 let tail: Vec<&str> = lines.iter().skip(tail_start).copied().collect();
80 let omitted = if total_lines > HEAD_LINES + TAIL_LINES {
81 total_lines - HEAD_LINES - TAIL_LINES
82 } else {
83 0
84 };
85
86 let language = Self::get_language(path);
87 let lang_attr = if language.is_empty() {
88 String::new()
89 } else {
90 format!(" language=\"{}\"", language)
91 };
92
93 let abs_path = path.to_string_lossy();
94 let head_content = head.join("\n");
95 let tail_content = tail.join("\n");
96
97 format!(
98 "<file_truncated path=\"{}\" absolute_path=\"{}\" exists=\"true\" total_lines=\"{}\"{}>\n\
99 === HEAD (lines 1-{}) ===\n\
100 {}\n\n\
101 === TRUNCATED ({} lines omitted) ===\n\n\
102 === TAIL (lines {}-{}) ===\n\
103 {}\n\
104 </file_truncated>",
105 ref_str,
106 abs_path,
107 total_lines,
108 lang_attr,
109 HEAD_LINES,
110 head_content,
111 omitted,
112 total_lines - TAIL_LINES + 1,
113 total_lines,
114 tail_content,
115 )
116 }
117
118 pub fn process_directory(&self, path: &Path, ref_str: &str) -> String {
120 let tree = self.build_tree(path, "", 0);
121 let item_count = tree.iter().filter(|l| !l.ends_with("...")).count();
122 let dir_name = path
123 .file_name()
124 .map(|n| n.to_string_lossy().to_string())
125 .unwrap_or_else(|| path.to_string_lossy().to_string());
126
127 format!(
128 "<directory_listing path=\"{}\" count=\"{}\">\n{}/\n{}\n</directory_listing>",
129 ref_str,
130 item_count,
131 dir_name,
132 tree.join("\n"),
133 )
134 }
135
136 pub(super) fn build_tree(&self, dir_path: &Path, prefix: &str, depth: usize) -> Vec<String> {
138 if depth > MAX_DIR_DEPTH {
139 return vec![format!("{}...", prefix)]; }
141
142 let entries = match fs::read_dir(dir_path) {
143 Ok(rd) => rd,
144 Err(_) => return vec![format!("{}[permission denied]", prefix)],
145 };
146
147 let mut items: Vec<std::path::PathBuf> = entries
148 .filter_map(|e| {
149 e.ok()
150 .map(|e| e.path().canonicalize().unwrap_or_else(|_| e.path()))
151 })
152 .collect();
153
154 items.sort_by(|a, b| {
156 let a_dir = a.is_dir();
157 let b_dir = b.is_dir();
158 match (a_dir, b_dir) {
159 (true, false) => std::cmp::Ordering::Less,
160 (false, true) => std::cmp::Ordering::Greater,
161 _ => {
162 let a_name = a
163 .file_name()
164 .unwrap_or_default()
165 .to_string_lossy()
166 .to_lowercase();
167 let b_name = b
168 .file_name()
169 .unwrap_or_default()
170 .to_string_lossy()
171 .to_lowercase();
172 a_name.cmp(&b_name)
173 }
174 }
175 });
176
177 let items: Vec<std::path::PathBuf> = items
179 .into_iter()
180 .filter(|p| !self.gitignore.is_ignored(p))
181 .take(MAX_DIR_ITEMS)
182 .collect();
183
184 let mut lines: Vec<String> = Vec::new();
185 let count = items.len();
186
187 for (i, item) in items.iter().enumerate() {
188 let is_last = i == count - 1;
189 let connector = if is_last {
190 "\u{2514}\u{2500}\u{2500} "
191 } else {
192 "\u{251C}\u{2500}\u{2500} "
193 };
194 let new_prefix = if is_last {
195 format!("{} ", prefix)
196 } else {
197 format!("{}\u{2502} ", prefix)
198 };
199
200 let name = item
201 .file_name()
202 .unwrap_or_default()
203 .to_string_lossy()
204 .to_string();
205
206 if item.is_dir() {
207 lines.push(format!("{}{}{}/", prefix, connector, name));
208 lines.extend(self.build_tree(item, &new_prefix, depth + 1));
209 } else {
210 let size_str = item
211 .metadata()
212 .map(|m| format!(" ({})", Self::format_size(m.len())))
213 .unwrap_or_default();
214 lines.push(format!("{}{}{}{}", prefix, connector, name, size_str));
215 }
216 }
217
218 lines
219 }
220
221 pub fn process_pdf(path: &Path, ref_str: &str) -> String {
223 let abs_path = path.to_string_lossy();
226 format!(
227 "<pdf_content path=\"{}\" absolute_path=\"{}\" pages=\"?\">\n\
228 [PDF text extraction not yet implemented. Add a PDF crate for full support.]\n\
229 </pdf_content>",
230 ref_str, abs_path,
231 )
232 }
233
234 pub fn process_image(path: &Path, ref_str: &str) -> (String, Option<ImageBlock>) {
236 let data = match fs::read(path) {
237 Ok(d) => d,
238 Err(e) => {
239 return (
240 format!(
241 "<file_error path=\"{}\" reason=\"Failed to read image file: {}\" />",
242 ref_str, e
243 ),
244 None,
245 );
246 }
247 };
248
249 let ext = ext_lower(path);
250 let mime_type = match ext.as_str() {
251 ".png" => "image/png",
252 ".jpg" | ".jpeg" => "image/jpeg",
253 ".gif" => "image/gif",
254 ".webp" => "image/webp",
255 ".bmp" => "image/bmp",
256 _ => "image/png",
257 };
258
259 let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
260
261 let tag = format!(
262 "<image path=\"{}\" type=\"{}\">\n[Image attached as multimodal content]\n</image>",
263 ref_str, mime_type,
264 );
265
266 let block = ImageBlock {
267 media_type: mime_type.to_string(),
268 data: b64,
269 };
270
271 (tag, Some(block))
272 }
273}