uira_security/permissions/
pattern.rs1use globset::{GlobBuilder, GlobMatcher};
7use std::path::PathBuf;
8
9#[derive(Debug, thiserror::Error)]
11pub enum PatternError {
12 #[error("invalid glob pattern: {0}")]
13 InvalidPattern(#[from] globset::Error),
14
15 #[error("path expansion failed: {0}")]
16 ExpansionFailed(String),
17}
18
19#[derive(Debug, Clone)]
21pub struct Pattern {
22 original: String,
24 expanded: String,
26 matcher: GlobMatcher,
28}
29
30impl Pattern {
31 pub fn new(pattern: &str) -> Result<Self, PatternError> {
38 let expanded = expand_path(pattern)?;
39 let glob = GlobBuilder::new(&expanded)
40 .literal_separator(true)
41 .build()?;
42 let matcher = glob.compile_matcher();
43
44 Ok(Self {
45 original: pattern.to_string(),
46 expanded,
47 matcher,
48 })
49 }
50
51 pub fn matches(&self, path: &str) -> bool {
53 self.matcher.is_match(path)
54 }
55
56 pub fn matches_expanded(&self, path: &str) -> bool {
58 if let Ok(expanded_path) = expand_path(path) {
59 self.matcher.is_match(&expanded_path)
60 } else {
61 self.matcher.is_match(path)
62 }
63 }
64
65 pub fn original(&self) -> &str {
67 &self.original
68 }
69
70 pub fn expanded(&self) -> &str {
72 &self.expanded
73 }
74}
75
76pub fn expand_path(path: &str) -> Result<String, PatternError> {
83 let mut result = path.to_string();
84
85 if result.starts_with("~/") {
87 let home = dirs::home_dir()
88 .ok_or_else(|| PatternError::ExpansionFailed("could not find home directory".into()))?;
89 result = format!("{}{}", home.display(), &result[1..]);
90 }
91
92 if result.contains("$HOME") {
94 let home = dirs::home_dir()
95 .ok_or_else(|| PatternError::ExpansionFailed("could not find home directory".into()))?;
96 result = result.replace("$HOME", &home.display().to_string());
97 }
98
99 if result.contains("$CWD") {
101 let cwd = std::env::current_dir()
102 .map_err(|e| PatternError::ExpansionFailed(format!("could not get cwd: {}", e)))?;
103 result = result.replace("$CWD", &cwd.display().to_string());
104 }
105
106 Ok(result)
107}
108
109pub fn normalize_path(path: &str) -> String {
113 let mut result = path.replace('\\', "/");
114 while result.ends_with('/') && result.len() > 1 {
115 result.pop();
116 }
117 result
118}
119
120pub fn matches_any(path: &str, patterns: &[Pattern]) -> bool {
122 let normalized = normalize_path(path);
123 patterns.iter().any(|p| p.matches(&normalized))
124}
125
126pub fn directory_pattern(dir: &str) -> Result<Pattern, PatternError> {
128 let normalized = normalize_path(dir);
129 Pattern::new(&format!("{}/**", normalized))
130}
131
132pub fn home_dir() -> Option<PathBuf> {
134 dirs::home_dir()
135}
136
137pub fn current_dir() -> std::io::Result<PathBuf> {
139 std::env::current_dir()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_simple_pattern() {
148 let pattern = Pattern::new("src/**/*.rs").unwrap();
149 assert!(pattern.matches("src/main.rs"));
150 assert!(pattern.matches("src/lib/mod.rs"));
151 assert!(!pattern.matches("tests/test.rs"));
152 }
153
154 #[test]
155 fn test_exact_pattern() {
156 let pattern = Pattern::new("Cargo.toml").unwrap();
157 assert!(pattern.matches("Cargo.toml"));
158 assert!(!pattern.matches("cargo.toml"));
159 assert!(!pattern.matches("src/Cargo.toml"));
160 }
161
162 #[test]
163 fn test_wildcard_pattern() {
164 let pattern = Pattern::new("*.rs").unwrap();
165 assert!(pattern.matches("main.rs"));
166 assert!(pattern.matches("lib.rs"));
167 }
170
171 #[test]
172 fn test_double_wildcard() {
173 let pattern = Pattern::new("**/*.rs").unwrap();
174 assert!(pattern.matches("main.rs"));
175 assert!(pattern.matches("src/main.rs"));
176 assert!(pattern.matches("src/deep/nested/file.rs"));
177 }
178
179 #[test]
180 fn test_expand_home() {
181 let result = expand_path("~/test").unwrap();
182 assert!(!result.starts_with("~/"));
183 assert!(result.contains("test"));
184 }
185
186 #[test]
187 fn test_expand_cwd() {
188 let result = expand_path("$CWD/test").unwrap();
189 assert!(!result.contains("$CWD"));
190 assert!(result.contains("test"));
191 }
192
193 #[test]
194 fn test_normalize_path() {
195 assert_eq!(normalize_path("src/"), "src");
196 assert_eq!(normalize_path("src//"), "src");
197 assert_eq!(normalize_path("src\\lib"), "src/lib");
198 assert_eq!(normalize_path("/"), "/");
199 }
200
201 #[test]
202 fn test_directory_pattern() {
203 let pattern = directory_pattern("src").unwrap();
204 assert!(pattern.matches("src/main.rs"));
205 assert!(pattern.matches("src/lib/mod.rs"));
206 assert!(!pattern.matches("tests/test.rs"));
207 }
208
209 #[test]
210 fn test_matches_any() {
211 let patterns = vec![
212 Pattern::new("src/**/*.rs").unwrap(),
213 Pattern::new("tests/**/*.rs").unwrap(),
214 ];
215 assert!(matches_any("src/main.rs", &patterns));
216 assert!(matches_any("tests/test.rs", &patterns));
217 assert!(!matches_any("docs/readme.md", &patterns));
218 }
219
220 #[test]
221 fn test_invalid_pattern() {
222 let result = Pattern::new("[invalid");
223 assert!(result.is_err());
224 }
225}