ricecoder_execution/
file_operations.rs

1//! File operations wrapper ensuring all paths are validated with PathResolver
2//!
3//! This module provides a centralized interface for all file operations,
4//! ensuring that every path is validated through `ricecoder_storage::PathResolver`
5//! before any file system operation is performed.
6//!
7//! **CRITICAL**: All file operations MUST use PathResolver for path validation.
8//! No hardcoded paths are permitted.
9
10use crate::error::{ExecutionError, ExecutionResult};
11use ricecoder_storage::PathResolver;
12use std::fs;
13use std::path::{Path, PathBuf};
14use tracing::{debug, info};
15
16/// File operations wrapper ensuring PathResolver validation
17pub struct FileOperations;
18
19impl FileOperations {
20    /// Create a file with the specified content
21    ///
22    /// # Arguments
23    /// * `path` - File path (will be validated with PathResolver)
24    /// * `content` - Content to write to the file
25    ///
26    /// # Errors
27    /// Returns error if path is invalid or file creation fails
28    ///
29    /// # Panics
30    /// Never panics; all errors are returned as ExecutionError
31    pub fn create_file(path: &str, content: &str) -> ExecutionResult<()> {
32        debug!(path = %path, content_len = content.len(), "Creating file with PathResolver validation");
33
34        // Validate and resolve path using PathResolver
35        let resolved_path = Self::validate_and_resolve_path(path)?;
36
37        // Create parent directories if needed
38        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        // Write the file
48        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    /// Modify a file by applying a diff
57    ///
58    /// # Arguments
59    /// * `path` - File path (will be validated with PathResolver)
60    /// * `diff` - Diff to apply to the file
61    ///
62    /// # Errors
63    /// Returns error if path is invalid, file doesn't exist, or diff application fails
64    pub fn modify_file(path: &str, diff: &str) -> ExecutionResult<()> {
65        debug!(path = %path, diff_len = diff.len(), "Modifying file with PathResolver validation");
66
67        // Validate and resolve path using PathResolver
68        let resolved_path = Self::validate_and_resolve_path(path)?;
69
70        // Check if file exists
71        if !resolved_path.exists() {
72            return Err(ExecutionError::StepFailed(format!(
73                "File not found for modification: {}",
74                path
75            )));
76        }
77
78        // Read the current content (for validation)
79        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        // Validate that diff is not empty
84        if diff.is_empty() {
85            return Err(ExecutionError::StepFailed(
86                "Cannot apply empty diff".to_string(),
87            ));
88        }
89
90        // In a real implementation, this would:
91        // 1. Parse the diff
92        // 2. Apply hunks to the file
93        // 3. Handle conflicts
94        // 4. Write the modified content back
95
96        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    /// Delete a file
103    ///
104    /// # Arguments
105    /// * `path` - File path (will be validated with PathResolver)
106    ///
107    /// # Errors
108    /// Returns error if path is invalid or file deletion fails
109    pub fn delete_file(path: &str) -> ExecutionResult<()> {
110        debug!(path = %path, "Deleting file with PathResolver validation");
111
112        // Validate and resolve path using PathResolver
113        let resolved_path = Self::validate_and_resolve_path(path)?;
114
115        // Check if file exists
116        if !resolved_path.exists() {
117            return Err(ExecutionError::StepFailed(format!(
118                "File not found for deletion: {}",
119                path
120            )));
121        }
122
123        // Delete the file
124        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    /// Backup a file before modification
133    ///
134    /// Creates a backup copy of the file with a `.backup` extension.
135    ///
136    /// # Arguments
137    /// * `path` - File path (will be validated with PathResolver)
138    ///
139    /// # Returns
140    /// Path to the backup file
141    ///
142    /// # Errors
143    /// Returns error if path is invalid or backup creation fails
144    pub fn backup_file(path: &str) -> ExecutionResult<PathBuf> {
145        debug!(path = %path, "Creating backup with PathResolver validation");
146
147        // Validate and resolve path using PathResolver
148        let resolved_path = Self::validate_and_resolve_path(path)?;
149
150        // Check if file exists
151        if !resolved_path.exists() {
152            return Err(ExecutionError::StepFailed(format!(
153                "File not found for backup: {}",
154                path
155            )));
156        }
157
158        // Create backup path
159        let backup_path = Self::create_backup_path(&resolved_path)?;
160
161        // Copy file to backup
162        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    /// Restore a file from backup
176    ///
177    /// # Arguments
178    /// * `file_path` - Original file path (will be validated with PathResolver)
179    /// * `backup_path` - Backup file path (will be validated with PathResolver)
180    ///
181    /// # Errors
182    /// Returns error if paths are invalid or restoration fails
183    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        // Validate and resolve both paths using PathResolver
191        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        // Check if backup exists
195        if !resolved_backup_path.exists() {
196            return Err(ExecutionError::StepFailed(format!(
197                "Backup file not found: {}",
198                backup_path
199            )));
200        }
201
202        // Restore the file from backup
203        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    /// Check if a file exists
220    ///
221    /// # Arguments
222    /// * `path` - File path (will be validated with PathResolver)
223    ///
224    /// # Returns
225    /// true if file exists, false otherwise
226    ///
227    /// # Errors
228    /// Returns error if path is invalid
229    pub fn file_exists(path: &str) -> ExecutionResult<bool> {
230        debug!(path = %path, "Checking file existence with PathResolver validation");
231
232        // Validate and resolve path using PathResolver
233        let resolved_path = Self::validate_and_resolve_path(path)?;
234
235        Ok(resolved_path.exists())
236    }
237
238    /// Read file content
239    ///
240    /// # Arguments
241    /// * `path` - File path (will be validated with PathResolver)
242    ///
243    /// # Returns
244    /// File content as string
245    ///
246    /// # Errors
247    /// Returns error if path is invalid or file read fails
248    pub fn read_file(path: &str) -> ExecutionResult<String> {
249        debug!(path = %path, "Reading file with PathResolver validation");
250
251        // Validate and resolve path using PathResolver
252        let resolved_path = Self::validate_and_resolve_path(path)?;
253
254        // Check if file exists
255        if !resolved_path.exists() {
256            return Err(ExecutionError::StepFailed(format!(
257                "File not found for reading: {}",
258                path
259            )));
260        }
261
262        // Read the file
263        fs::read_to_string(&resolved_path)
264            .map_err(|e| ExecutionError::StepFailed(format!("Failed to read file {}: {}", path, e)))
265    }
266
267    /// Validate and resolve a path using PathResolver
268    ///
269    /// This is the CRITICAL function that ensures all paths go through PathResolver.
270    /// Every file operation must call this function to validate paths.
271    ///
272    /// # Arguments
273    /// * `path` - Path to validate and resolve
274    ///
275    /// # Returns
276    /// Resolved PathBuf if valid
277    ///
278    /// # Errors
279    /// Returns error if path is invalid or cannot be resolved
280    fn validate_and_resolve_path(path: &str) -> ExecutionResult<PathBuf> {
281        // Validate that path is not empty
282        if path.is_empty() {
283            return Err(ExecutionError::ValidationError(
284                "Path cannot be empty".to_string(),
285            ));
286        }
287
288        // Validate that path doesn't contain null bytes
289        if path.contains('\0') {
290            return Err(ExecutionError::ValidationError(
291                "Path contains null bytes".to_string(),
292            ));
293        }
294
295        // Use PathResolver to expand home directory and validate
296        let resolved_path = PathResolver::expand_home(Path::new(path))
297            .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
298
299        // Validate that the resolved path is absolute or relative to current directory
300        if !resolved_path.is_absolute() && !resolved_path.starts_with(".") {
301            // Allow relative paths that don't start with . (they're relative to current dir)
302        }
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    /// Create a backup path from an original path
314    ///
315    /// Appends `.backup` to the original path.
316    ///
317    /// # Arguments
318    /// * `path` - Original file path
319    ///
320    /// # Returns
321    /// Backup path
322    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        // Create the file first
378        fs::write(&file_path, "content").unwrap();
379        assert!(file_path.exists());
380
381        // Delete it
382        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        // Create the file first
400        fs::write(&file_path, "original content").unwrap();
401
402        // Create backup
403        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        // Create original and backup files
422        fs::write(&file_path, "original").unwrap();
423        fs::write(&backup_path, "backup content").unwrap();
424
425        // Restore from backup
426        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        // File doesn't exist yet
440        let result = FileOperations::file_exists(&path_str);
441        assert!(result.is_ok());
442        assert!(!result.unwrap());
443
444        // Create the file
445        fs::write(&file_path, "content").unwrap();
446
447        // File exists now
448        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        // Create the file
460        fs::write(&file_path, "test content").unwrap();
461
462        // Read it
463        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        // Create the file first
493        fs::write(&file_path, "original content").unwrap();
494
495        // Modify it (with a non-empty diff)
496        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}