Skip to main content

aster/tools/file/
mod.rs

1//! File Tools Module
2//!
3//! This module provides file operation tools including:
4//! - ReadTool: Read text files, images, PDFs, and Jupyter notebooks
5//! - WriteTool: Write files with read-before-overwrite validation
6//! - EditTool: Smart string matching and batch edits
7//!
8//! Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10
9
10pub mod edit;
11pub mod read;
12pub mod write;
13
14use std::collections::HashMap;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::SystemTime;
18
19use serde::{Deserialize, Serialize};
20use std::sync::RwLock;
21
22// Re-export tools
23pub use edit::EditTool;
24pub use read::ReadTool;
25pub use write::WriteTool;
26
27/// Record of a file read operation
28///
29/// Tracks when a file was read and its content hash at that time.
30/// Used by EditTool and WriteTool to validate file state.
31///
32/// Requirements: 4.5
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct FileReadRecord {
35    /// Path to the file that was read
36    pub path: PathBuf,
37
38    /// Timestamp when the file was read
39    pub read_at: SystemTime,
40
41    /// Hash of the file content when read (for change detection)
42    pub content_hash: String,
43
44    /// File modification time when read
45    pub mtime: Option<SystemTime>,
46
47    /// File size when read
48    pub size: u64,
49
50    /// Number of lines in the file (for text files)
51    pub line_count: Option<usize>,
52}
53
54impl FileReadRecord {
55    /// Create a new FileReadRecord
56    pub fn new(path: PathBuf, content_hash: String, size: u64) -> Self {
57        Self {
58            path,
59            read_at: SystemTime::now(),
60            content_hash,
61            mtime: None,
62            size,
63            line_count: None,
64        }
65    }
66
67    /// Set the modification time
68    pub fn with_mtime(mut self, mtime: SystemTime) -> Self {
69        self.mtime = Some(mtime);
70        self
71    }
72
73    /// Set the line count
74    pub fn with_line_count(mut self, line_count: usize) -> Self {
75        self.line_count = Some(line_count);
76        self
77    }
78
79    /// Check if the file has been modified since it was read
80    pub fn is_modified(&self, current_mtime: SystemTime) -> bool {
81        match self.mtime {
82            Some(recorded_mtime) => current_mtime != recorded_mtime,
83            None => false, // Can't determine, assume not modified
84        }
85    }
86}
87
88/// File read history tracker
89///
90/// Maintains a history of file read operations for validation.
91/// Used by EditTool to ensure files are read before editing,
92/// and by WriteTool to ensure files are read before overwriting.
93///
94/// Requirements: 4.5
95#[derive(Debug, Default)]
96pub struct FileReadHistory {
97    /// Map of file paths to their read records
98    records: HashMap<PathBuf, FileReadRecord>,
99}
100
101impl FileReadHistory {
102    /// Create a new empty FileReadHistory
103    pub fn new() -> Self {
104        Self {
105            records: HashMap::new(),
106        }
107    }
108
109    /// Record a file read operation
110    pub fn record_read(&mut self, record: FileReadRecord) {
111        let path = record.path.clone();
112        self.records.insert(path, record);
113    }
114
115    /// Check if a file has been read
116    pub fn has_read(&self, path: &PathBuf) -> bool {
117        self.records.contains_key(path)
118    }
119
120    /// Get the read record for a file
121    pub fn get_record(&self, path: &PathBuf) -> Option<&FileReadRecord> {
122        self.records.get(path)
123    }
124
125    /// Remove a read record (e.g., after successful write)
126    pub fn remove_record(&mut self, path: &PathBuf) -> Option<FileReadRecord> {
127        self.records.remove(path)
128    }
129
130    /// Clear all read records
131    pub fn clear(&mut self) {
132        self.records.clear();
133    }
134
135    /// Get the number of tracked files
136    pub fn len(&self) -> usize {
137        self.records.len()
138    }
139
140    /// Check if the history is empty
141    pub fn is_empty(&self) -> bool {
142        self.records.is_empty()
143    }
144
145    /// Get all tracked file paths
146    pub fn tracked_files(&self) -> Vec<&PathBuf> {
147        self.records.keys().collect()
148    }
149
150    /// Check if a file has been modified since it was read
151    ///
152    /// Returns:
153    /// - Some(true) if the file has been modified
154    /// - Some(false) if the file has not been modified
155    /// - None if the file has not been read or mtime is not available
156    pub fn is_file_modified(&self, path: &PathBuf, current_mtime: SystemTime) -> Option<bool> {
157        self.records
158            .get(path)
159            .map(|record| record.is_modified(current_mtime))
160    }
161}
162
163/// Shared file read history for use across tools
164pub type SharedFileReadHistory = Arc<RwLock<FileReadHistory>>;
165
166/// Create a new shared file read history
167pub fn create_shared_history() -> SharedFileReadHistory {
168    Arc::new(RwLock::new(FileReadHistory::new()))
169}
170
171/// Compute a hash of file content for change detection
172pub fn compute_content_hash(content: &[u8]) -> String {
173    use std::collections::hash_map::DefaultHasher;
174    use std::hash::{Hash, Hasher};
175
176    let mut hasher = DefaultHasher::new();
177    content.hash(&mut hasher);
178    format!("{:016x}", hasher.finish())
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_file_read_record_new() {
187        let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100);
188
189        assert_eq!(record.path, PathBuf::from("/tmp/test.txt"));
190        assert_eq!(record.content_hash, "abc123");
191        assert_eq!(record.size, 100);
192        assert!(record.mtime.is_none());
193        assert!(record.line_count.is_none());
194    }
195
196    #[test]
197    fn test_file_read_record_with_mtime() {
198        let mtime = SystemTime::now();
199        let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
200            .with_mtime(mtime);
201
202        assert_eq!(record.mtime, Some(mtime));
203    }
204
205    #[test]
206    fn test_file_read_record_with_line_count() {
207        let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
208            .with_line_count(50);
209
210        assert_eq!(record.line_count, Some(50));
211    }
212
213    #[test]
214    fn test_file_read_record_is_modified() {
215        let mtime = SystemTime::now();
216        let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
217            .with_mtime(mtime);
218
219        // Same mtime - not modified
220        assert!(!record.is_modified(mtime));
221
222        // Different mtime - modified
223        let new_mtime = mtime + std::time::Duration::from_secs(1);
224        assert!(record.is_modified(new_mtime));
225    }
226
227    #[test]
228    fn test_file_read_history_new() {
229        let history = FileReadHistory::new();
230        assert!(history.is_empty());
231        assert_eq!(history.len(), 0);
232    }
233
234    #[test]
235    fn test_file_read_history_record_read() {
236        let mut history = FileReadHistory::new();
237        let path = PathBuf::from("/tmp/test.txt");
238        let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
239
240        history.record_read(record);
241
242        assert!(history.has_read(&path));
243        assert_eq!(history.len(), 1);
244    }
245
246    #[test]
247    fn test_file_read_history_get_record() {
248        let mut history = FileReadHistory::new();
249        let path = PathBuf::from("/tmp/test.txt");
250        let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
251
252        history.record_read(record);
253
254        let retrieved = history.get_record(&path);
255        assert!(retrieved.is_some());
256        assert_eq!(retrieved.unwrap().content_hash, "abc123");
257    }
258
259    #[test]
260    fn test_file_read_history_remove_record() {
261        let mut history = FileReadHistory::new();
262        let path = PathBuf::from("/tmp/test.txt");
263        let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
264
265        history.record_read(record);
266        assert!(history.has_read(&path));
267
268        let removed = history.remove_record(&path);
269        assert!(removed.is_some());
270        assert!(!history.has_read(&path));
271    }
272
273    #[test]
274    fn test_file_read_history_clear() {
275        let mut history = FileReadHistory::new();
276        history.record_read(FileReadRecord::new(
277            PathBuf::from("/tmp/test1.txt"),
278            "abc".to_string(),
279            100,
280        ));
281        history.record_read(FileReadRecord::new(
282            PathBuf::from("/tmp/test2.txt"),
283            "def".to_string(),
284            200,
285        ));
286
287        assert_eq!(history.len(), 2);
288
289        history.clear();
290        assert!(history.is_empty());
291    }
292
293    #[test]
294    fn test_file_read_history_tracked_files() {
295        let mut history = FileReadHistory::new();
296        let path1 = PathBuf::from("/tmp/test1.txt");
297        let path2 = PathBuf::from("/tmp/test2.txt");
298
299        history.record_read(FileReadRecord::new(path1.clone(), "abc".to_string(), 100));
300        history.record_read(FileReadRecord::new(path2.clone(), "def".to_string(), 200));
301
302        let tracked = history.tracked_files();
303        assert_eq!(tracked.len(), 2);
304        assert!(tracked.contains(&&path1));
305        assert!(tracked.contains(&&path2));
306    }
307
308    #[test]
309    fn test_file_read_history_is_file_modified() {
310        let mut history = FileReadHistory::new();
311        let path = PathBuf::from("/tmp/test.txt");
312        let mtime = SystemTime::now();
313        let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100).with_mtime(mtime);
314
315        history.record_read(record);
316
317        // Same mtime - not modified
318        assert_eq!(history.is_file_modified(&path, mtime), Some(false));
319
320        // Different mtime - modified
321        let new_mtime = mtime + std::time::Duration::from_secs(1);
322        assert_eq!(history.is_file_modified(&path, new_mtime), Some(true));
323
324        // Unknown file
325        let unknown_path = PathBuf::from("/tmp/unknown.txt");
326        assert_eq!(history.is_file_modified(&unknown_path, mtime), None);
327    }
328
329    #[test]
330    fn test_compute_content_hash() {
331        let content1 = b"Hello, World!";
332        let content2 = b"Hello, World!";
333        let content3 = b"Different content";
334
335        let hash1 = compute_content_hash(content1);
336        let hash2 = compute_content_hash(content2);
337        let hash3 = compute_content_hash(content3);
338
339        // Same content should produce same hash
340        assert_eq!(hash1, hash2);
341
342        // Different content should produce different hash
343        assert_ne!(hash1, hash3);
344
345        // Hash should be 16 hex characters
346        assert_eq!(hash1.len(), 16);
347    }
348
349    #[test]
350    fn test_create_shared_history() {
351        let history = create_shared_history();
352
353        // Should be able to write
354        {
355            let mut write_guard = history.write().unwrap();
356            write_guard.record_read(FileReadRecord::new(
357                PathBuf::from("/tmp/test.txt"),
358                "abc".to_string(),
359                100,
360            ));
361        }
362
363        // Should be able to read
364        {
365            let read_guard = history.read().unwrap();
366            assert!(read_guard.has_read(&PathBuf::from("/tmp/test.txt")));
367        }
368    }
369}