use std::collections::HashSet;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::types::PluginKind;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
pub plugin_dir: PathBuf,
pub config_dir: PathBuf,
pub enabled: bool,
pub default_timeout_ms: u64,
pub max_memory_mb: usize,
pub allow_network: bool,
pub disabled_plugins: HashSet<String>,
pub fs_read_roots: Vec<PathBuf>,
}
impl Default for PluginConfig {
fn default() -> Self {
let dirs = gravityfile_core::paths::Dirs::discover()
.unwrap_or_else(|| gravityfile_core::paths::Dirs::under(std::path::Path::new(".")));
Self {
plugin_dir: dirs.plugin_dir(),
config_dir: dirs.config,
enabled: true,
default_timeout_ms: 5000,
max_memory_mb: 256,
allow_network: false,
disabled_plugins: HashSet::new(),
fs_read_roots: Vec::new(),
}
}
}
impl PluginConfig {
pub fn with_plugin_dir(mut self, dir: PathBuf) -> Self {
self.plugin_dir = dir;
self
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.default_timeout_ms = timeout_ms;
self
}
pub fn disable_plugin(mut self, name: impl Into<String>) -> Self {
self.disabled_plugins.insert(name.into());
self
}
pub fn is_disabled(&self, name: &str) -> bool {
self.disabled_plugins.contains(name)
}
pub fn with_fs_read_roots(mut self, roots: Vec<PathBuf>) -> Self {
self.fs_read_roots = roots;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMetadata {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub author: String,
#[serde(default)]
pub url: String,
#[serde(default = "default_runtime")]
pub runtime: String,
#[serde(default)]
pub kind: PluginKind,
#[serde(default = "default_entry")]
pub entry: String,
#[serde(default)]
pub min_version: Option<String>,
#[serde(default)]
pub hooks: PluginHooks,
#[serde(default)]
pub permissions: PluginPermissions,
#[serde(default)]
pub dependencies: Vec<String>,
}
fn default_version() -> String {
"0.1.0".to_string()
}
fn default_runtime() -> String {
"lua".to_string()
}
fn default_entry() -> String {
"main.lua".to_string()
}
impl Default for PluginMetadata {
fn default() -> Self {
Self {
name: String::new(),
version: default_version(),
description: String::new(),
author: String::new(),
url: String::new(),
runtime: default_runtime(),
kind: PluginKind::Hook,
entry: default_entry(),
min_version: None,
hooks: PluginHooks::default(),
permissions: PluginPermissions::default(),
dependencies: vec![],
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginHooks {
#[serde(default)]
pub on_navigate: bool,
#[serde(default)]
pub on_drill_down: bool,
#[serde(default)]
pub on_back: bool,
#[serde(default)]
pub on_scan_start: bool,
#[serde(default)]
pub on_scan_progress: bool,
#[serde(default)]
pub on_scan_complete: bool,
#[serde(default)]
pub on_delete_start: bool,
#[serde(default)]
pub on_delete_complete: bool,
#[serde(default)]
pub on_copy_start: bool,
#[serde(default)]
pub on_copy_complete: bool,
#[serde(default)]
pub on_move_start: bool,
#[serde(default)]
pub on_move_complete: bool,
#[serde(default)]
pub on_render: bool,
#[serde(default)]
pub on_action: bool,
#[serde(default)]
pub on_mode_change: bool,
#[serde(default)]
pub on_startup: bool,
#[serde(default)]
pub on_shutdown: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginPermissions {
#[serde(default)]
pub filesystem: FilesystemPermission,
#[serde(default)]
pub network: bool,
#[serde(default)]
pub commands: Vec<String>,
#[serde(default)]
pub env: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FilesystemPermission {
#[default]
None,
Read,
Write,
Full,
}
impl PluginPermissions {
pub fn can_read(&self) -> bool {
!matches!(self.filesystem, FilesystemPermission::None)
}
pub fn can_write(&self) -> bool {
matches!(
self.filesystem,
FilesystemPermission::Write | FilesystemPermission::Full
)
}
pub fn can_run_command(&self, cmd: &str) -> bool {
self.commands.iter().any(|c| c == cmd || c == "*")
}
pub fn to_sandbox(
&self,
roots: &[PathBuf],
timeout_ms: u64,
max_memory: usize,
) -> crate::sandbox::SandboxConfig {
use crate::sandbox::{Permission, SandboxConfig};
let mut sandbox = SandboxConfig::minimal();
sandbox.timeout_ms = timeout_ms;
sandbox.max_memory = max_memory;
match self.filesystem {
FilesystemPermission::None => {}
FilesystemPermission::Read => {
sandbox.permissions.insert(Permission::Read);
sandbox.allowed_read_paths = roots.to_vec();
}
FilesystemPermission::Write => {
sandbox.permissions.insert(Permission::Read);
sandbox.permissions.insert(Permission::Write);
sandbox.allowed_read_paths = roots.to_vec();
sandbox.allowed_write_paths = roots.to_vec();
}
FilesystemPermission::Full => {
sandbox.permissions.insert(Permission::Read);
sandbox.permissions.insert(Permission::Write);
sandbox.allowed_read_paths = vec![PathBuf::from("/")];
sandbox.allowed_write_paths = vec![PathBuf::from("/")];
}
}
if self.network {
sandbox.allow_network = true;
sandbox.permissions.insert(Permission::Network);
}
if self.env {
sandbox.allow_env = true;
sandbox.permissions.insert(Permission::Environment);
}
if !self.commands.is_empty() {
sandbox.permissions.insert(Permission::Execute);
for cmd in &self.commands {
sandbox.allowed_commands.insert(cmd.clone());
}
}
sandbox
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::Permission;
#[test]
fn permissions_none_grants_no_filesystem() {
let perms = PluginPermissions::default();
let sb = perms.to_sandbox(&[PathBuf::from("/tmp")], 1000, 0);
assert!(!sb.has_permission(Permission::Read));
assert!(!sb.has_permission(Permission::Write));
assert!(sb.allowed_read_paths.is_empty());
}
#[test]
fn permissions_read_scopes_to_roots() {
let perms = PluginPermissions {
filesystem: FilesystemPermission::Read,
..Default::default()
};
let sb = perms.to_sandbox(&[PathBuf::from("/tmp")], 1000, 0);
assert!(sb.has_permission(Permission::Read));
assert!(!sb.has_permission(Permission::Write));
assert_eq!(sb.allowed_read_paths, vec![PathBuf::from("/tmp")]);
assert!(sb.allowed_write_paths.is_empty());
}
#[test]
fn permissions_write_grants_read_and_write_scoped() {
let perms = PluginPermissions {
filesystem: FilesystemPermission::Write,
env: true,
commands: vec!["git".to_string()],
..Default::default()
};
let sb = perms.to_sandbox(&[PathBuf::from("/data")], 1000, 0);
assert!(sb.has_permission(Permission::Read));
assert!(sb.has_permission(Permission::Write));
assert!(sb.has_permission(Permission::Environment));
assert!(sb.can_execute("git"));
assert!(!sb.can_execute("rm"));
}
}