agentic_navigation_guide/
verifier.rs

1//! Filesystem verification for navigation guides
2
3use crate::errors::{Result, SemanticError};
4use crate::types::{FilesystemItem, NavigationGuide, NavigationGuideLine};
5use std::path::{Path, PathBuf};
6
7/// Verifier for navigation guides against filesystem
8pub struct Verifier {
9    /// Root path for verification
10    root_path: PathBuf,
11}
12
13impl Verifier {
14    /// Create a new verifier with the given root path
15    pub fn new(root_path: &Path) -> Self {
16        Self {
17            root_path: root_path.to_path_buf(),
18        }
19    }
20
21    /// Verify a navigation guide against the filesystem
22    pub fn verify(&self, guide: &NavigationGuide) -> Result<()> {
23        // First validate syntax (should already be done, but double-check)
24        crate::validator::Validator::new().validate_syntax(guide)?;
25
26        // Then verify each item against the filesystem
27        for item in &guide.items {
28            self.verify_item(item, &self.root_path)?;
29        }
30
31        Ok(())
32    }
33
34    /// Verify a single item against the filesystem
35    fn verify_item(&self, item: &NavigationGuideLine, parent_path: &Path) -> Result<()> {
36        let item_path = parent_path.join(item.path());
37
38        // Check if the item exists
39        if !item_path.exists() {
40            return Err(SemanticError::ItemNotFound {
41                line: item.line_number,
42                item_type: self.get_item_type_string(item),
43                path: item.path().to_string(),
44                full_path: item_path,
45            }
46            .into());
47        }
48
49        // Check if the item type matches
50        match &item.item {
51            FilesystemItem::Directory { children, .. } => {
52                if !item_path.is_dir() {
53                    return Err(SemanticError::TypeMismatch {
54                        line: item.line_number,
55                        expected: "directory".to_string(),
56                        found: if item_path.is_file() {
57                            "file".to_string()
58                        } else {
59                            "symlink".to_string()
60                        },
61                        path: item.path().to_string(),
62                    }
63                    .into());
64                }
65
66                // Verify children recursively
67                for child in children {
68                    self.verify_item(child, &item_path)?;
69                }
70            }
71            FilesystemItem::File { .. } => {
72                if !item_path.is_file() {
73                    return Err(SemanticError::TypeMismatch {
74                        line: item.line_number,
75                        expected: "file".to_string(),
76                        found: if item_path.is_dir() {
77                            "directory".to_string()
78                        } else {
79                            "symlink".to_string()
80                        },
81                        path: item.path().to_string(),
82                    }
83                    .into());
84                }
85            }
86            FilesystemItem::Symlink { target, .. } => {
87                let metadata = match std::fs::symlink_metadata(&item_path) {
88                    Ok(m) => m,
89                    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
90                        return Err(SemanticError::PermissionDenied {
91                            line: item.line_number,
92                            path: item.path().to_string(),
93                        }
94                        .into());
95                    }
96                    Err(e) => return Err(e.into()),
97                };
98
99                if !metadata.is_symlink() {
100                    return Err(SemanticError::TypeMismatch {
101                        line: item.line_number,
102                        expected: "symlink".to_string(),
103                        found: if item_path.is_dir() {
104                            "directory".to_string()
105                        } else {
106                            "file".to_string()
107                        },
108                        path: item.path().to_string(),
109                    }
110                    .into());
111                }
112
113                // Verify symlink target if specified
114                if let Some(expected_target) = target {
115                    if let Ok(actual_target) = std::fs::read_link(&item_path) {
116                        if actual_target.to_string_lossy() != *expected_target {
117                            return Err(SemanticError::SymlinkTargetMismatch {
118                                line: item.line_number,
119                                path: item.path().to_string(),
120                                expected: expected_target.clone(),
121                                actual: actual_target.to_string_lossy().to_string(),
122                            }
123                            .into());
124                        }
125                    }
126                }
127            }
128        }
129
130        Ok(())
131    }
132
133    /// Get a human-readable string for the item type
134    fn get_item_type_string(&self, item: &NavigationGuideLine) -> String {
135        match &item.item {
136            FilesystemItem::Directory { .. } => "directory".to_string(),
137            FilesystemItem::File { .. } => "file".to_string(),
138            FilesystemItem::Symlink { .. } => "symlink".to_string(),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use tempfile::TempDir;
147
148    #[test]
149    fn test_verify_missing_file() {
150        let temp_dir = TempDir::new().unwrap();
151        let verifier = Verifier::new(temp_dir.path());
152
153        let guide = NavigationGuide {
154            items: vec![NavigationGuideLine {
155                line_number: 1,
156                indent_level: 0,
157                item: FilesystemItem::File {
158                    path: "missing.txt".to_string(),
159                    comment: None,
160                },
161            }],
162            prologue: None,
163            epilogue: None,
164        };
165
166        let result = verifier.verify(&guide);
167        assert!(matches!(
168            result,
169            Err(crate::errors::AppError::Semantic(
170                SemanticError::ItemNotFound { .. }
171            ))
172        ));
173    }
174}