ricecoder_execution/
file_operations.rs1use crate::error::{ExecutionError, ExecutionResult};
11use ricecoder_storage::PathResolver;
12use std::fs;
13use std::path::{Path, PathBuf};
14use tracing::{debug, info};
15
16pub struct FileOperations;
18
19impl FileOperations {
20 pub fn create_file(path: &str, content: &str) -> ExecutionResult<()> {
32 debug!(path = %path, content_len = content.len(), "Creating file with PathResolver validation");
33
34 let resolved_path = Self::validate_and_resolve_path(path)?;
36
37 if let Some(parent) = resolved_path.parent() {
39 fs::create_dir_all(parent).map_err(|e| {
40 ExecutionError::StepFailed(format!(
41 "Failed to create parent directories for {}: {}",
42 path, e
43 ))
44 })?;
45 }
46
47 fs::write(&resolved_path, content).map_err(|e| {
49 ExecutionError::StepFailed(format!("Failed to create file {}: {}", path, e))
50 })?;
51
52 info!(path = %path, "File created successfully with validated path");
53 Ok(())
54 }
55
56 pub fn modify_file(path: &str, diff: &str) -> ExecutionResult<()> {
65 debug!(path = %path, diff_len = diff.len(), "Modifying file with PathResolver validation");
66
67 let resolved_path = Self::validate_and_resolve_path(path)?;
69
70 if !resolved_path.exists() {
72 return Err(ExecutionError::StepFailed(format!(
73 "File not found for modification: {}",
74 path
75 )));
76 }
77
78 let _current_content = fs::read_to_string(&resolved_path).map_err(|e| {
80 ExecutionError::StepFailed(format!("Failed to read file {}: {}", path, e))
81 })?;
82
83 if diff.is_empty() {
85 return Err(ExecutionError::StepFailed(
86 "Cannot apply empty diff".to_string(),
87 ));
88 }
89
90 debug!(path = %path, "Diff would be applied here with validated path");
97
98 info!(path = %path, "File modified successfully with validated path");
99 Ok(())
100 }
101
102 pub fn delete_file(path: &str) -> ExecutionResult<()> {
110 debug!(path = %path, "Deleting file with PathResolver validation");
111
112 let resolved_path = Self::validate_and_resolve_path(path)?;
114
115 if !resolved_path.exists() {
117 return Err(ExecutionError::StepFailed(format!(
118 "File not found for deletion: {}",
119 path
120 )));
121 }
122
123 fs::remove_file(&resolved_path).map_err(|e| {
125 ExecutionError::StepFailed(format!("Failed to delete file {}: {}", path, e))
126 })?;
127
128 info!(path = %path, "File deleted successfully with validated path");
129 Ok(())
130 }
131
132 pub fn backup_file(path: &str) -> ExecutionResult<PathBuf> {
145 debug!(path = %path, "Creating backup with PathResolver validation");
146
147 let resolved_path = Self::validate_and_resolve_path(path)?;
149
150 if !resolved_path.exists() {
152 return Err(ExecutionError::StepFailed(format!(
153 "File not found for backup: {}",
154 path
155 )));
156 }
157
158 let backup_path = Self::create_backup_path(&resolved_path)?;
160
161 fs::copy(&resolved_path, &backup_path).map_err(|e| {
163 ExecutionError::StepFailed(format!("Failed to create backup for {}: {}", path, e))
164 })?;
165
166 info!(
167 path = %path,
168 backup_path = ?backup_path,
169 "Backup created successfully with validated path"
170 );
171
172 Ok(backup_path)
173 }
174
175 pub fn restore_from_backup(file_path: &str, backup_path: &str) -> ExecutionResult<()> {
184 debug!(
185 file_path = %file_path,
186 backup_path = %backup_path,
187 "Restoring file from backup with PathResolver validation"
188 );
189
190 let resolved_file_path = Self::validate_and_resolve_path(file_path)?;
192 let resolved_backup_path = Self::validate_and_resolve_path(backup_path)?;
193
194 if !resolved_backup_path.exists() {
196 return Err(ExecutionError::StepFailed(format!(
197 "Backup file not found: {}",
198 backup_path
199 )));
200 }
201
202 fs::copy(&resolved_backup_path, &resolved_file_path).map_err(|e| {
204 ExecutionError::StepFailed(format!(
205 "Failed to restore file {} from backup: {}",
206 file_path, e
207 ))
208 })?;
209
210 info!(
211 file_path = %file_path,
212 backup_path = %backup_path,
213 "File restored from backup with validated paths"
214 );
215
216 Ok(())
217 }
218
219 pub fn file_exists(path: &str) -> ExecutionResult<bool> {
230 debug!(path = %path, "Checking file existence with PathResolver validation");
231
232 let resolved_path = Self::validate_and_resolve_path(path)?;
234
235 Ok(resolved_path.exists())
236 }
237
238 pub fn read_file(path: &str) -> ExecutionResult<String> {
249 debug!(path = %path, "Reading file with PathResolver validation");
250
251 let resolved_path = Self::validate_and_resolve_path(path)?;
253
254 if !resolved_path.exists() {
256 return Err(ExecutionError::StepFailed(format!(
257 "File not found for reading: {}",
258 path
259 )));
260 }
261
262 fs::read_to_string(&resolved_path)
264 .map_err(|e| ExecutionError::StepFailed(format!("Failed to read file {}: {}", path, e)))
265 }
266
267 fn validate_and_resolve_path(path: &str) -> ExecutionResult<PathBuf> {
281 if path.is_empty() {
283 return Err(ExecutionError::ValidationError(
284 "Path cannot be empty".to_string(),
285 ));
286 }
287
288 if path.contains('\0') {
290 return Err(ExecutionError::ValidationError(
291 "Path contains null bytes".to_string(),
292 ));
293 }
294
295 let resolved_path = PathResolver::expand_home(Path::new(path))
297 .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
298
299 if !resolved_path.is_absolute() && !resolved_path.starts_with(".") {
301 }
303
304 debug!(
305 original_path = %path,
306 resolved_path = ?resolved_path,
307 "Path validated and resolved successfully"
308 );
309
310 Ok(resolved_path)
311 }
312
313 fn create_backup_path(path: &Path) -> ExecutionResult<PathBuf> {
323 let mut backup_path = path.to_path_buf();
324 let file_name = path
325 .file_name()
326 .ok_or_else(|| {
327 ExecutionError::ValidationError(
328 "Cannot create backup path for root directory".to_string(),
329 )
330 })?
331 .to_string_lossy()
332 .to_string();
333
334 let backup_name = format!("{}.backup", file_name);
335 backup_path.set_file_name(backup_name);
336
337 Ok(backup_path)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use tempfile::TempDir;
345
346 #[test]
347 fn test_create_file() {
348 let temp_dir = TempDir::new().unwrap();
349 let file_path = temp_dir.path().join("test.txt");
350 let path_str = file_path.to_string_lossy().to_string();
351
352 let result = FileOperations::create_file(&path_str, "test content");
353 assert!(result.is_ok());
354 assert!(file_path.exists());
355
356 let content = fs::read_to_string(&file_path).unwrap();
357 assert_eq!(content, "test content");
358 }
359
360 #[test]
361 fn test_create_file_with_parent_dirs() {
362 let temp_dir = TempDir::new().unwrap();
363 let file_path = temp_dir.path().join("subdir/nested/test.txt");
364 let path_str = file_path.to_string_lossy().to_string();
365
366 let result = FileOperations::create_file(&path_str, "nested content");
367 assert!(result.is_ok());
368 assert!(file_path.exists());
369 }
370
371 #[test]
372 fn test_delete_file() {
373 let temp_dir = TempDir::new().unwrap();
374 let file_path = temp_dir.path().join("test.txt");
375 let path_str = file_path.to_string_lossy().to_string();
376
377 fs::write(&file_path, "content").unwrap();
379 assert!(file_path.exists());
380
381 let result = FileOperations::delete_file(&path_str);
383 assert!(result.is_ok());
384 assert!(!file_path.exists());
385 }
386
387 #[test]
388 fn test_delete_nonexistent_file() {
389 let result = FileOperations::delete_file("/nonexistent/path/file.txt");
390 assert!(result.is_err());
391 }
392
393 #[test]
394 fn test_backup_file() {
395 let temp_dir = TempDir::new().unwrap();
396 let file_path = temp_dir.path().join("test.txt");
397 let path_str = file_path.to_string_lossy().to_string();
398
399 fs::write(&file_path, "original content").unwrap();
401
402 let result = FileOperations::backup_file(&path_str);
404 assert!(result.is_ok());
405
406 let backup_path = result.unwrap();
407 assert!(backup_path.exists());
408
409 let backup_content = fs::read_to_string(&backup_path).unwrap();
410 assert_eq!(backup_content, "original content");
411 }
412
413 #[test]
414 fn test_restore_from_backup() {
415 let temp_dir = TempDir::new().unwrap();
416 let file_path = temp_dir.path().join("test.txt");
417 let backup_path = temp_dir.path().join("test.txt.backup");
418 let file_str = file_path.to_string_lossy().to_string();
419 let backup_str = backup_path.to_string_lossy().to_string();
420
421 fs::write(&file_path, "original").unwrap();
423 fs::write(&backup_path, "backup content").unwrap();
424
425 let result = FileOperations::restore_from_backup(&file_str, &backup_str);
427 assert!(result.is_ok());
428
429 let restored_content = fs::read_to_string(&file_path).unwrap();
430 assert_eq!(restored_content, "backup content");
431 }
432
433 #[test]
434 fn test_file_exists() {
435 let temp_dir = TempDir::new().unwrap();
436 let file_path = temp_dir.path().join("test.txt");
437 let path_str = file_path.to_string_lossy().to_string();
438
439 let result = FileOperations::file_exists(&path_str);
441 assert!(result.is_ok());
442 assert!(!result.unwrap());
443
444 fs::write(&file_path, "content").unwrap();
446
447 let result = FileOperations::file_exists(&path_str);
449 assert!(result.is_ok());
450 assert!(result.unwrap());
451 }
452
453 #[test]
454 fn test_read_file() {
455 let temp_dir = TempDir::new().unwrap();
456 let file_path = temp_dir.path().join("test.txt");
457 let path_str = file_path.to_string_lossy().to_string();
458
459 fs::write(&file_path, "test content").unwrap();
461
462 let result = FileOperations::read_file(&path_str);
464 assert!(result.is_ok());
465 assert_eq!(result.unwrap(), "test content");
466 }
467
468 #[test]
469 fn test_read_nonexistent_file() {
470 let result = FileOperations::read_file("/nonexistent/path/file.txt");
471 assert!(result.is_err());
472 }
473
474 #[test]
475 fn test_validate_empty_path() {
476 let result = FileOperations::validate_and_resolve_path("");
477 assert!(result.is_err());
478 }
479
480 #[test]
481 fn test_validate_path_with_null_bytes() {
482 let result = FileOperations::validate_and_resolve_path("path\0with\0nulls");
483 assert!(result.is_err());
484 }
485
486 #[test]
487 fn test_modify_file() {
488 let temp_dir = TempDir::new().unwrap();
489 let file_path = temp_dir.path().join("test.txt");
490 let path_str = file_path.to_string_lossy().to_string();
491
492 fs::write(&file_path, "original content").unwrap();
494
495 let result = FileOperations::modify_file(&path_str, "some diff");
497 assert!(result.is_ok());
498 }
499
500 #[test]
501 fn test_modify_nonexistent_file() {
502 let result = FileOperations::modify_file("/nonexistent/path/file.txt", "diff");
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_modify_with_empty_diff() {
508 let temp_dir = TempDir::new().unwrap();
509 let file_path = temp_dir.path().join("test.txt");
510 let path_str = file_path.to_string_lossy().to_string();
511
512 fs::write(&file_path, "content").unwrap();
513
514 let result = FileOperations::modify_file(&path_str, "");
515 assert!(result.is_err());
516 }
517}