1#[derive(Debug, thiserror::Error)]
5pub enum PolicyParseError {
6 #[error("YAML parse error: {0}")]
7 Yaml(#[from] serde_yaml::Error),
8
9 #[error("invalid rule '{rule}': {message}")]
10 InvalidRule { rule: String, message: String },
11
12 #[error("invalid effect '{0}'")]
13 InvalidEffect(String),
14
15 #[error("invalid tool '{0}'")]
16 InvalidTool(String),
17
18 #[error("invalid filter expression: {0}")]
19 InvalidFilter(String),
20
21 #[error("invalid profile expression: {0}")]
22 InvalidProfile(String),
23
24 #[error("unknown constraint or profile '{0}'")]
25 UnknownRef(String),
26
27 #[error("circular profile include: {cycle}")]
28 CircularInclude {
29 cycle: String,
31 path: Option<String>,
33 },
34
35 #[error("unknown profile '{name}' in include{}", .suggestion.as_ref().map(|s| format!("; did you mean '{}'?", s)).unwrap_or_default())]
36 UnknownInclude {
37 name: String,
38 suggestion: Option<String>,
40 },
41
42 #[error("invalid new-format rule key '{0}': {1}")]
43 InvalidNewRuleKey(String, String),
44
45 #[error("invalid cap-scoped fs key '{0}': {1}")]
46 InvalidCapScopedFs(String, String),
47
48 #[error("invalid args entry: {0}")]
49 InvalidArg(String),
50}
51
52impl PolicyParseError {
53 pub fn help(&self) -> Option<String> {
55 match self {
56 PolicyParseError::InvalidEffect(eff) => Some(format!(
57 "valid effects are: allow, deny, ask (got '{}')",
58 eff
59 )),
60 PolicyParseError::InvalidTool(tool) => Some(format!(
61 "any tool name is valid (bash, read, write, edit, task, glob, etc.) or * for wildcard (got '{}')",
62 tool
63 )),
64 PolicyParseError::InvalidRule { rule, .. } => Some(format!(
65 "expected format: 'effect entity tool pattern [: constraint]' (got '{}')",
66 rule
67 )),
68 PolicyParseError::CircularInclude { path, .. } => {
69 path.as_ref().map(|p| format!("include cycle: {}", p))
70 }
71 PolicyParseError::InvalidFilter(_) => Some(
72 "valid filter functions: subpath(path), literal(path), regex(pattern); \
73 combine with & (and), | (or), ! (not)"
74 .into(),
75 ),
76 PolicyParseError::InvalidProfile(_) => Some(
77 "profile expressions reference constraint or profile names; \
78 combine with & (and), | (or), ! (not)"
79 .into(),
80 ),
81 PolicyParseError::InvalidNewRuleKey(_, _) => {
82 Some("format: \"effect verb noun\" where effect=allow|deny|ask, verb=bash|read|write|edit|*, noun=command or path pattern. Example: \"allow bash git *\"".into())
83 }
84 _ => None,
85 }
86 }
87}
88
89#[derive(Debug, thiserror::Error)]
91pub enum CompileError {
92 #[error("invalid glob pattern '{pattern}': {source}")]
93 InvalidGlob {
94 pattern: String,
95 source: regex::Error,
96 },
97 #[error("invalid regex in filter '{pattern}': {source}")]
98 InvalidFilterRegex {
99 pattern: String,
100 source: regex::Error,
101 },
102 #[error("profile flattening error: {0}")]
103 ProfileError(String),
104}
105
106impl CompileError {
107 pub fn help(&self) -> Option<String> {
109 match self {
110 CompileError::InvalidGlob { pattern, .. } => Some(format!(
111 "check glob pattern '{}': use * for single segment, ** for recursive, ? for single char",
112 pattern
113 )),
114 CompileError::InvalidFilterRegex { pattern, .. } => Some(format!(
115 "check regex '{}': special characters like (, ), [, ] need escaping with \\",
116 pattern
117 )),
118 CompileError::ProfileError(_) => {
119 Some("check profile definitions for missing or circular includes".into())
120 }
121 }
122 }
123}
124
125#[derive(Debug, thiserror::Error)]
127pub enum PolicyError {
128 #[error(transparent)]
129 Parse(#[from] PolicyParseError),
130 #[error(transparent)]
131 Compile(#[from] CompileError),
132}
133
134pub fn levenshtein(a: &str, b: &str) -> usize {
136 let a_len = a.len();
137 let b_len = b.len();
138
139 if a_len == 0 {
140 return b_len;
141 }
142 if b_len == 0 {
143 return a_len;
144 }
145
146 let mut prev: Vec<usize> = (0..=b_len).collect();
147 let mut curr = vec![0; b_len + 1];
148
149 for (i, ca) in a.chars().enumerate() {
150 curr[0] = i + 1;
151 for (j, cb) in b.chars().enumerate() {
152 let cost = if ca == cb { 0 } else { 1 };
153 curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
154 }
155 std::mem::swap(&mut prev, &mut curr);
156 }
157
158 prev[b_len]
159}
160
161pub fn suggest_closest(name: &str, candidates: &[&str]) -> Option<String> {
164 candidates
165 .iter()
166 .map(|c| (c, levenshtein(name, c)))
167 .filter(|(_, dist)| *dist <= 3 && *dist > 0)
168 .min_by_key(|(_, dist)| *dist)
169 .map(|(c, _)| (*c).to_string())
170}