1use std::collections::HashSet;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8use crate::types::PluginKind;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PluginConfig {
13 pub plugin_dir: PathBuf,
15
16 pub config_dir: PathBuf,
18
19 pub enabled: bool,
21
22 pub default_timeout_ms: u64,
24
25 pub max_memory_mb: usize,
27
28 pub allow_network: bool,
30
31 pub disabled_plugins: HashSet<String>,
33
34 pub fs_read_roots: Vec<PathBuf>,
37}
38
39impl Default for PluginConfig {
40 fn default() -> Self {
41 let dirs = gravityfile_core::paths::Dirs::discover()
42 .unwrap_or_else(|| gravityfile_core::paths::Dirs::under(std::path::Path::new(".")));
43
44 Self {
45 plugin_dir: dirs.plugin_dir(),
46 config_dir: dirs.config,
47 enabled: true,
48 default_timeout_ms: 5000,
49 max_memory_mb: 256,
50 allow_network: false,
51 disabled_plugins: HashSet::new(),
52 fs_read_roots: Vec::new(),
53 }
54 }
55}
56
57impl PluginConfig {
58 pub fn with_plugin_dir(mut self, dir: PathBuf) -> Self {
60 self.plugin_dir = dir;
61 self
62 }
63
64 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
66 self.default_timeout_ms = timeout_ms;
67 self
68 }
69
70 pub fn disable_plugin(mut self, name: impl Into<String>) -> Self {
72 self.disabled_plugins.insert(name.into());
73 self
74 }
75
76 pub fn is_disabled(&self, name: &str) -> bool {
78 self.disabled_plugins.contains(name)
79 }
80
81 pub fn with_fs_read_roots(mut self, roots: Vec<PathBuf>) -> Self {
83 self.fs_read_roots = roots;
84 self
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PluginMetadata {
91 pub name: String,
93
94 #[serde(default = "default_version")]
96 pub version: String,
97
98 #[serde(default)]
100 pub description: String,
101
102 #[serde(default)]
104 pub author: String,
105
106 #[serde(default)]
108 pub url: String,
109
110 #[serde(default = "default_runtime")]
112 pub runtime: String,
113
114 #[serde(default)]
116 pub kind: PluginKind,
117
118 #[serde(default = "default_entry")]
120 pub entry: String,
121
122 #[serde(default)]
124 pub min_version: Option<String>,
125
126 #[serde(default)]
128 pub hooks: PluginHooks,
129
130 #[serde(default)]
132 pub permissions: PluginPermissions,
133
134 #[serde(default)]
136 pub dependencies: Vec<String>,
137}
138
139fn default_version() -> String {
140 "0.1.0".to_string()
141}
142
143fn default_runtime() -> String {
144 "lua".to_string()
145}
146
147fn default_entry() -> String {
148 "main.lua".to_string()
149}
150
151impl Default for PluginMetadata {
152 fn default() -> Self {
153 Self {
154 name: String::new(),
155 version: default_version(),
156 description: String::new(),
157 author: String::new(),
158 url: String::new(),
159 runtime: default_runtime(),
160 kind: PluginKind::Hook,
161 entry: default_entry(),
162 min_version: None,
163 hooks: PluginHooks::default(),
164 permissions: PluginPermissions::default(),
165 dependencies: vec![],
166 }
167 }
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172pub struct PluginHooks {
173 #[serde(default)]
175 pub on_navigate: bool,
176 #[serde(default)]
177 pub on_drill_down: bool,
178 #[serde(default)]
179 pub on_back: bool,
180
181 #[serde(default)]
183 pub on_scan_start: bool,
184 #[serde(default)]
185 pub on_scan_progress: bool,
186 #[serde(default)]
187 pub on_scan_complete: bool,
188
189 #[serde(default)]
191 pub on_delete_start: bool,
192 #[serde(default)]
193 pub on_delete_complete: bool,
194 #[serde(default)]
195 pub on_copy_start: bool,
196 #[serde(default)]
197 pub on_copy_complete: bool,
198 #[serde(default)]
199 pub on_move_start: bool,
200 #[serde(default)]
201 pub on_move_complete: bool,
202
203 #[serde(default)]
205 pub on_render: bool,
206 #[serde(default)]
207 pub on_action: bool,
208 #[serde(default)]
209 pub on_mode_change: bool,
210
211 #[serde(default)]
213 pub on_startup: bool,
214 #[serde(default)]
215 pub on_shutdown: bool,
216}
217
218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
220pub struct PluginPermissions {
221 #[serde(default)]
223 pub filesystem: FilesystemPermission,
224
225 #[serde(default)]
227 pub network: bool,
228
229 #[serde(default)]
231 pub commands: Vec<String>,
232
233 #[serde(default)]
235 pub env: bool,
236}
237
238#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "lowercase")]
241pub enum FilesystemPermission {
242 #[default]
244 None,
245
246 Read,
248
249 Write,
251
252 Full,
254}
255
256impl PluginPermissions {
257 pub fn can_read(&self) -> bool {
259 !matches!(self.filesystem, FilesystemPermission::None)
260 }
261
262 pub fn can_write(&self) -> bool {
264 matches!(
265 self.filesystem,
266 FilesystemPermission::Write | FilesystemPermission::Full
267 )
268 }
269
270 pub fn can_run_command(&self, cmd: &str) -> bool {
272 self.commands.iter().any(|c| c == cmd || c == "*")
273 }
274
275 pub fn to_sandbox(
280 &self,
281 roots: &[PathBuf],
282 timeout_ms: u64,
283 max_memory: usize,
284 ) -> crate::sandbox::SandboxConfig {
285 use crate::sandbox::{Permission, SandboxConfig};
286
287 let mut sandbox = SandboxConfig::minimal();
288 sandbox.timeout_ms = timeout_ms;
289 sandbox.max_memory = max_memory;
290
291 match self.filesystem {
292 FilesystemPermission::None => {}
293 FilesystemPermission::Read => {
294 sandbox.permissions.insert(Permission::Read);
295 sandbox.allowed_read_paths = roots.to_vec();
296 }
297 FilesystemPermission::Write => {
298 sandbox.permissions.insert(Permission::Read);
299 sandbox.permissions.insert(Permission::Write);
300 sandbox.allowed_read_paths = roots.to_vec();
301 sandbox.allowed_write_paths = roots.to_vec();
302 }
303 FilesystemPermission::Full => {
304 sandbox.permissions.insert(Permission::Read);
305 sandbox.permissions.insert(Permission::Write);
306 sandbox.allowed_read_paths = vec![PathBuf::from("/")];
307 sandbox.allowed_write_paths = vec![PathBuf::from("/")];
308 }
309 }
310
311 if self.network {
312 sandbox.allow_network = true;
313 sandbox.permissions.insert(Permission::Network);
314 }
315 if self.env {
316 sandbox.allow_env = true;
317 sandbox.permissions.insert(Permission::Environment);
318 }
319 if !self.commands.is_empty() {
320 sandbox.permissions.insert(Permission::Execute);
321 for cmd in &self.commands {
322 sandbox.allowed_commands.insert(cmd.clone());
323 }
324 }
325
326 sandbox
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::sandbox::Permission;
334
335 #[test]
336 fn permissions_none_grants_no_filesystem() {
337 let perms = PluginPermissions::default();
338 let sb = perms.to_sandbox(&[PathBuf::from("/tmp")], 1000, 0);
339 assert!(!sb.has_permission(Permission::Read));
340 assert!(!sb.has_permission(Permission::Write));
341 assert!(sb.allowed_read_paths.is_empty());
342 }
343
344 #[test]
345 fn permissions_read_scopes_to_roots() {
346 let perms = PluginPermissions {
347 filesystem: FilesystemPermission::Read,
348 ..Default::default()
349 };
350 let sb = perms.to_sandbox(&[PathBuf::from("/tmp")], 1000, 0);
351 assert!(sb.has_permission(Permission::Read));
352 assert!(!sb.has_permission(Permission::Write));
353 assert_eq!(sb.allowed_read_paths, vec![PathBuf::from("/tmp")]);
354 assert!(sb.allowed_write_paths.is_empty());
355 }
356
357 #[test]
358 fn permissions_write_grants_read_and_write_scoped() {
359 let perms = PluginPermissions {
360 filesystem: FilesystemPermission::Write,
361 env: true,
362 commands: vec!["git".to_string()],
363 ..Default::default()
364 };
365 let sb = perms.to_sandbox(&[PathBuf::from("/data")], 1000, 0);
366 assert!(sb.has_permission(Permission::Read));
367 assert!(sb.has_permission(Permission::Write));
368 assert!(sb.has_permission(Permission::Environment));
369 assert!(sb.can_execute("git"));
370 assert!(!sb.can_execute("rm"));
371 }
372}