aft/bash_permissions/
mod.rs1pub mod arity;
8pub mod scan;
9
10use crate::context::AppContext;
11use serde::{Deserialize, Serialize};
12use std::path::{Component, Path, PathBuf};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PermissionAsk {
16 pub kind: PermissionKind,
17 pub patterns: Vec<String>,
18 pub always: Vec<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum PermissionKind {
23 #[serde(rename = "external_directory")]
24 ExternalDirectory,
25 #[serde(rename = "bash")]
26 Bash,
27}
28
29pub fn scan(command: &str, ctx: &AppContext) -> Vec<PermissionAsk> {
31 scan::scan(command, ctx)
32}
33
34pub(crate) fn is_system_temp_path(path: &Path) -> bool {
37 let resolved = resolve_with_existing_ancestors(path);
38 system_temp_roots().into_iter().any(|root| {
39 let root = normalize_path(&root);
40 path_is_under(&root, &resolved)
41 })
42}
43
44fn system_temp_roots() -> Vec<PathBuf> {
45 let mut roots = Vec::new();
46 roots.push(std::env::temp_dir());
47
48 #[cfg(unix)]
49 roots.extend([
50 PathBuf::from("/tmp"),
51 PathBuf::from("/var/tmp"),
52 PathBuf::from("/private/tmp"),
53 PathBuf::from("/private/var/tmp"),
54 ]);
55
56 #[cfg(target_os = "macos")]
57 roots.extend([
58 PathBuf::from("/var/folders"),
59 PathBuf::from("/private/var/folders"),
60 ]);
61
62 let mut expanded = Vec::with_capacity(roots.len() * 2);
63 for root in roots {
64 expanded.push(root.clone());
65 if let Ok(canonical) = std::fs::canonicalize(&root) {
66 expanded.push(canonical);
67 }
68 }
69 expanded
70}
71
72fn path_is_under(root: &Path, path: &Path) -> bool {
73 path == root || path.starts_with(root)
74}
75
76fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
77 if let Ok(canonical) = std::fs::canonicalize(path) {
78 return normalize_path(&canonical);
79 }
80
81 let normalized = normalize_path(path);
82 if !normalized.is_absolute() {
83 return normalized;
84 }
85
86 let mut missing: Vec<PathBuf> = Vec::new();
87 let mut current = normalized.clone();
88 loop {
89 if let Ok(real_parent) = std::fs::canonicalize(¤t) {
90 let mut resolved = real_parent;
91 for part in missing.iter().rev() {
92 resolved.push(part);
93 }
94 return normalize_path(&resolved);
95 }
96
97 let Some(parent) = current.parent() else {
98 return normalized;
99 };
100 if parent == current {
101 return normalized;
102 }
103 if let Some(name) = current.file_name() {
104 missing.push(PathBuf::from(name));
105 }
106 current = parent.to_path_buf();
107 }
108}
109
110fn normalize_path(path: &Path) -> PathBuf {
111 let mut result = PathBuf::new();
112 for component in path.components() {
113 match component {
114 Component::CurDir => {}
115 Component::ParentDir => {
116 if !result.pop() {
117 result.push(component.as_os_str());
118 }
119 }
120 other => result.push(other.as_os_str()),
121 }
122 }
123 result
124}
125
126#[cfg(test)]
127mod system_temp_path_tests {
128 use super::is_system_temp_path;
129 use std::path::Path;
130
131 #[cfg(unix)]
132 #[test]
133 fn unix_temp_roots_match_by_component() {
134 for path in [
135 "/tmp",
136 "/tmp/aft-file.txt",
137 "/var/tmp/aft-file.txt",
138 "/private/tmp/aft-file.txt",
139 "/private/var/tmp/aft-file.txt",
140 ] {
141 assert!(
142 is_system_temp_path(Path::new(path)),
143 "{path} should be temp"
144 );
145 }
146
147 for path in ["/tmpfoo/x", "/Users/x", "/home/user/tmp/x"] {
148 assert!(
149 !is_system_temp_path(Path::new(path)),
150 "{path} should not be temp"
151 );
152 }
153 }
154
155 #[cfg(target_os = "macos")]
156 #[test]
157 fn macos_var_folders_matches_tmpdir_tree() {
158 for path in [
159 "/var/folders/zz/aft/T/file.txt",
160 "/private/var/folders/zz/aft/T/file.txt",
161 ] {
162 assert!(
163 is_system_temp_path(Path::new(path)),
164 "{path} should be temp"
165 );
166 }
167 }
168
169 #[test]
170 fn process_temp_dir_and_children_are_exempt() {
171 let temp_dir = std::env::temp_dir();
172 assert!(is_system_temp_path(&temp_dir));
173 assert!(is_system_temp_path(
174 &temp_dir.join("aft-temp-exemption").join("file.txt")
175 ));
176 }
177
178 #[test]
179 fn relative_paths_are_not_temp_paths() {
180 for path in ["tmp/x", "./tmp/x", "project/tmp/x"] {
181 assert!(
182 !is_system_temp_path(Path::new(path)),
183 "{path} should not be temp"
184 );
185 }
186 }
187
188 #[cfg(windows)]
189 #[test]
190 fn windows_temp_dir_shape_is_exempt() {
191 let temp_dir = std::env::temp_dir();
192 assert!(is_system_temp_path(&temp_dir));
193 assert!(is_system_temp_path(
194 &temp_dir.join("aft-temp-exemption").join("file.txt")
195 ));
196 }
197}