pub mod config;
use std::ffi::{CStr, CString, c_char, c_void};
use std::io::Write;
use std::path::Path;
use yosh_plugin_api::{HostApi, PluginDecl, YOSH_PLUGIN_API_VERSION};
use crate::env::ShellEnv;
use self::config::{PluginConfig, expand_tilde};
struct LoadedPlugin {
name: String,
#[allow(dead_code)]
library: libloading::Library,
commands: Vec<String>,
capabilities: u32,
has_pre_exec: bool,
has_post_exec: bool,
has_on_cd: bool,
has_pre_prompt: bool,
}
pub struct PluginManager {
plugins: Vec<LoadedPlugin>,
}
impl PluginManager {
pub fn new() -> Self {
PluginManager { plugins: Vec::new() }
}
pub fn load_from_config(&mut self, config_path: &Path, env: &mut ShellEnv) {
let config = match PluginConfig::load(config_path) {
Ok(c) => c,
Err(_) => return,
};
for entry in &config.plugin {
if !entry.enabled {
continue;
}
let path = expand_tilde(&entry.path);
let config_caps = entry
.capabilities
.as_ref()
.map(|strs| config::capabilities_from_strs(strs));
if let Err(e) = self.load_plugin_with_capabilities(&path, env, config_caps) {
eprintln!("yosh: plugin: {}", e);
}
}
}
pub fn load_plugin(&mut self, path: &Path, env: &mut ShellEnv) -> Result<(), String> {
self.load_plugin_with_capabilities(path, env, None)
}
pub fn load_plugin_with_capabilities(
&mut self,
path: &Path,
env: &mut ShellEnv,
config_capabilities: Option<u32>,
) -> Result<(), String> {
let library = unsafe { libloading::Library::new(path) }
.map_err(|e| format!("{}: {}", path.display(), e))?;
let (name, requested_capabilities) = unsafe {
let decl_fn: libloading::Symbol<extern "C" fn() -> *const PluginDecl> = library
.get(b"yosh_plugin_decl")
.map_err(|_| {
format!("{}: not a valid yosh plugin", path.display())
})?;
let decl = &*decl_fn();
if decl.api_version != YOSH_PLUGIN_API_VERSION {
return Err(format!(
"{}: API version mismatch (expected {}, got {})",
path.display(),
YOSH_PLUGIN_API_VERSION,
decl.api_version
));
}
let name = CStr::from_ptr(decl.name).to_string_lossy().into_owned();
(name, decl.required_capabilities)
};
let effective_capabilities = match config_capabilities {
None => requested_capabilities,
Some(config_caps) => {
let effective = requested_capabilities & config_caps;
let denied = requested_capabilities & !effective;
if denied != 0 {
Self::log_denied_capabilities(&name, denied);
}
effective
}
};
{
let mut ctx = HostContext::new(env, &name);
let mut api = build_host_api(effective_capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
let init_fn: libloading::Symbol<unsafe extern "C" fn(*const HostApi) -> i32> =
unsafe {
library.get(b"yosh_plugin_init").map_err(|_| {
format!("{}: missing yosh_plugin_init", path.display())
})?
};
let status = unsafe { init_fn(&api) };
if status != 0 {
return Err(format!("{}: initialization failed", name));
}
}
let commands: Vec<String> = unsafe {
let cmd_fn: Result<
libloading::Symbol<unsafe extern "C" fn(*mut u32) -> *const *const c_char>,
_,
> = library.get(b"yosh_plugin_commands");
match cmd_fn {
Ok(cmd_fn) => {
let mut count: u32 = 0;
let ptr = cmd_fn(&mut count);
(0..count)
.map(|i| {
CStr::from_ptr(*ptr.add(i as usize))
.to_string_lossy()
.into_owned()
})
.collect()
}
Err(_) => Vec::new(),
}
};
let has_pre_exec =
unsafe { library.get::<*const ()>(b"yosh_plugin_hook_pre_exec").is_ok() };
let has_post_exec =
unsafe { library.get::<*const ()>(b"yosh_plugin_hook_post_exec").is_ok() };
let has_on_cd =
unsafe { library.get::<*const ()>(b"yosh_plugin_hook_on_cd").is_ok() };
let has_pre_prompt =
unsafe { library.get::<*const ()>(b"yosh_plugin_hook_pre_prompt").is_ok() };
self.plugins.push(LoadedPlugin {
name,
library,
commands,
capabilities: effective_capabilities,
has_pre_exec,
has_post_exec,
has_on_cd,
has_pre_prompt,
});
Ok(())
}
fn log_denied_capabilities(plugin_name: &str, denied: u32) {
use yosh_plugin_api::*;
let caps = [
(CAP_VARIABLES_READ, "variables:read"),
(CAP_VARIABLES_WRITE, "variables:write"),
(CAP_FILESYSTEM, "filesystem"),
(CAP_IO, "io"),
(CAP_HOOK_PRE_EXEC, "hooks:pre_exec"),
(CAP_HOOK_POST_EXEC, "hooks:post_exec"),
(CAP_HOOK_ON_CD, "hooks:on_cd"),
(CAP_HOOK_PRE_PROMPT, "hooks:pre_prompt"),
];
for (flag, name) in caps {
if denied & flag != 0 {
eprintln!(
"yosh: plugin '{}': capability '{}' requested but not granted",
plugin_name, name
);
}
}
}
pub fn exec_command(
&self,
env: &mut ShellEnv,
name: &str,
args: &[String],
) -> Option<i32> {
let plugin = self.plugins.iter().find(|p| p.commands.iter().any(|c| c == name))?;
let mut ctx = HostContext::new(env, &plugin.name);
let mut api = build_host_api(plugin.capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
let c_name = CString::new(name).ok()?;
let c_args: Vec<CString> = args
.iter()
.filter_map(|a| CString::new(a.as_str()).ok())
.collect();
let c_arg_ptrs: Vec<*const c_char> =
c_args.iter().map(|s| s.as_ptr()).collect();
let status = unsafe {
let exec_fn: libloading::Symbol<
unsafe extern "C" fn(*const HostApi, *const c_char, i32, *const *const c_char) -> i32,
> = plugin.library.get(b"yosh_plugin_exec").ok()?;
exec_fn(
&api,
c_name.as_ptr(),
c_arg_ptrs.len() as i32,
c_arg_ptrs.as_ptr(),
)
};
Some(status)
}
pub fn call_pre_exec(&self, env: &mut ShellEnv, cmd: &str) {
let c_cmd = match CString::new(cmd) {
Ok(c) => c,
Err(_) => return,
};
for plugin in &self.plugins {
if !plugin.has_pre_exec {
continue;
}
if plugin.capabilities & yosh_plugin_api::CAP_HOOK_PRE_EXEC == 0 {
continue;
}
let mut ctx = HostContext::new(env, &plugin.name);
let mut api = build_host_api(plugin.capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
unsafe {
if let Ok(hook_fn) = plugin.library.get::<
unsafe extern "C" fn(*const HostApi, *const c_char),
>(b"yosh_plugin_hook_pre_exec")
{
hook_fn(&api, c_cmd.as_ptr());
}
}
}
}
pub fn call_post_exec(&self, env: &mut ShellEnv, cmd: &str, exit_code: i32) {
let c_cmd = match CString::new(cmd) {
Ok(c) => c,
Err(_) => return,
};
for plugin in &self.plugins {
if !plugin.has_post_exec {
continue;
}
if plugin.capabilities & yosh_plugin_api::CAP_HOOK_POST_EXEC == 0 {
continue;
}
let mut ctx = HostContext::new(env, &plugin.name);
let mut api = build_host_api(plugin.capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
unsafe {
if let Ok(hook_fn) = plugin.library.get::<
unsafe extern "C" fn(*const HostApi, *const c_char, i32),
>(b"yosh_plugin_hook_post_exec")
{
hook_fn(&api, c_cmd.as_ptr(), exit_code);
}
}
}
}
pub fn call_on_cd(&self, env: &mut ShellEnv, old_dir: &str, new_dir: &str) {
let c_old = match CString::new(old_dir) {
Ok(c) => c,
Err(_) => return,
};
let c_new = match CString::new(new_dir) {
Ok(c) => c,
Err(_) => return,
};
for plugin in &self.plugins {
if !plugin.has_on_cd {
continue;
}
if plugin.capabilities & yosh_plugin_api::CAP_HOOK_ON_CD == 0 {
continue;
}
let mut ctx = HostContext::new(env, &plugin.name);
let mut api = build_host_api(plugin.capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
unsafe {
if let Ok(hook_fn) = plugin.library.get::<
unsafe extern "C" fn(*const HostApi, *const c_char, *const c_char),
>(b"yosh_plugin_hook_on_cd")
{
hook_fn(&api, c_old.as_ptr(), c_new.as_ptr());
}
}
}
}
pub fn call_pre_prompt(&self, env: &mut ShellEnv) {
for plugin in &self.plugins {
if !plugin.has_pre_prompt {
continue;
}
if plugin.capabilities & yosh_plugin_api::CAP_HOOK_PRE_PROMPT == 0 {
continue;
}
let mut ctx = HostContext::new(env, &plugin.name);
let mut api = build_host_api(plugin.capabilities);
api.ctx = &mut ctx as *mut HostContext as *mut c_void;
unsafe {
if let Ok(hook_fn) = plugin.library.get::<
unsafe extern "C" fn(*const HostApi),
>(b"yosh_plugin_hook_pre_prompt")
{
hook_fn(&api);
}
}
}
}
pub fn unload_all(&mut self) {
for plugin in &self.plugins {
unsafe {
if let Ok(destroy_fn) =
plugin.library.get::<unsafe extern "C" fn()>(b"yosh_plugin_destroy")
{
destroy_fn();
}
}
}
self.plugins.clear();
}
pub fn has_command(&self, name: &str) -> bool {
self.plugins.iter().any(|p| p.commands.iter().any(|c| c == name))
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for PluginManager {
fn drop(&mut self) {
self.unload_all();
}
}
struct HostContext<'a> {
env: &'a mut ShellEnv,
plugin_name: String,
return_buf: CString,
}
impl<'a> HostContext<'a> {
fn new(env: &'a mut ShellEnv, plugin_name: &str) -> Self {
HostContext {
env,
plugin_name: plugin_name.to_string(),
return_buf: CString::default(),
}
}
}
unsafe extern "C" fn host_get_var(ctx: *mut c_void, name: *const c_char) -> *const c_char {
unsafe {
let host = &mut *(ctx as *mut HostContext);
let name = match CStr::from_ptr(name).to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null(),
};
match host.env.vars.get(name) {
Some(val) => {
host.return_buf = CString::new(val).unwrap_or_default();
host.return_buf.as_ptr()
}
None => std::ptr::null(),
}
}
}
unsafe extern "C" fn host_set_var(
ctx: *mut c_void,
name: *const c_char,
value: *const c_char,
) -> i32 {
unsafe {
let host = &mut *(ctx as *mut HostContext);
let name = match CStr::from_ptr(name).to_str() {
Ok(s) => s,
Err(_) => return 1,
};
let value = match CStr::from_ptr(value).to_str() {
Ok(s) => s,
Err(_) => return 1,
};
match host.env.vars.set(name, value) {
Ok(()) => 0,
Err(_) => 1,
}
}
}
unsafe extern "C" fn host_export_var(
ctx: *mut c_void,
name: *const c_char,
value: *const c_char,
) -> i32 {
unsafe {
let host = &mut *(ctx as *mut HostContext);
let name = match CStr::from_ptr(name).to_str() {
Ok(s) => s,
Err(_) => return 1,
};
let value = match CStr::from_ptr(value).to_str() {
Ok(s) => s,
Err(_) => return 1,
};
match host.env.vars.set(name, value) {
Ok(()) => {
host.env.vars.export(name);
0
}
Err(_) => 1,
}
}
}
unsafe extern "C" fn host_get_cwd(ctx: *mut c_void) -> *const c_char {
unsafe {
let host = &mut *(ctx as *mut HostContext);
match std::env::current_dir() {
Ok(cwd) => {
host.return_buf = CString::new(cwd.to_string_lossy().as_ref()).unwrap_or_default();
host.return_buf.as_ptr()
}
Err(_) => std::ptr::null(),
}
}
}
unsafe extern "C" fn host_set_cwd(_ctx: *mut c_void, path: *const c_char) -> i32 {
unsafe {
let path = match CStr::from_ptr(path).to_str() {
Ok(s) => s,
Err(_) => return 1,
};
match std::env::set_current_dir(path) {
Ok(()) => 0,
Err(_) => 1,
}
}
}
unsafe extern "C" fn host_write_stdout(
_ctx: *mut c_void,
data: *const c_char,
len: usize,
) -> i32 {
unsafe {
let slice = std::slice::from_raw_parts(data as *const u8, len);
match std::io::stdout().write_all(slice) {
Ok(()) => 0,
Err(_) => 1,
}
}
}
unsafe extern "C" fn host_write_stderr(
_ctx: *mut c_void,
data: *const c_char,
len: usize,
) -> i32 {
unsafe {
let slice = std::slice::from_raw_parts(data as *const u8, len);
match std::io::stderr().write_all(slice) {
Ok(()) => 0,
Err(_) => 1,
}
}
}
unsafe extern "C" fn deny_get_var(ctx: *mut c_void, _name: *const c_char) -> *const c_char {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': get_var denied (missing 'variables:read' capability)",
host.plugin_name
);
}
std::ptr::null()
}
unsafe extern "C" fn deny_set_var(
ctx: *mut c_void,
_name: *const c_char,
_value: *const c_char,
) -> i32 {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': set_var denied (missing 'variables:write' capability)",
host.plugin_name
);
}
-1
}
unsafe extern "C" fn deny_export_var(
ctx: *mut c_void,
_name: *const c_char,
_value: *const c_char,
) -> i32 {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': export_var denied (missing 'variables:write' capability)",
host.plugin_name
);
}
-1
}
unsafe extern "C" fn deny_get_cwd(ctx: *mut c_void) -> *const c_char {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': get_cwd denied (missing 'filesystem' capability)",
host.plugin_name
);
}
std::ptr::null()
}
unsafe extern "C" fn deny_set_cwd(ctx: *mut c_void, _path: *const c_char) -> i32 {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': set_cwd denied (missing 'filesystem' capability)",
host.plugin_name
);
}
-1
}
unsafe extern "C" fn deny_write_stdout(
ctx: *mut c_void,
_data: *const c_char,
_len: usize,
) -> i32 {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': write_stdout denied (missing 'io' capability)",
host.plugin_name
);
}
-1
}
unsafe extern "C" fn deny_write_stderr(
ctx: *mut c_void,
_data: *const c_char,
_len: usize,
) -> i32 {
unsafe {
let host = &*(ctx as *mut HostContext);
eprintln!(
"yosh: plugin '{}': write_stderr denied (missing 'io' capability)",
host.plugin_name
);
}
-1
}
fn build_host_api(capabilities: u32) -> HostApi {
use yosh_plugin_api::*;
let has = |cap: u32| capabilities & cap != 0;
HostApi {
ctx: std::ptr::null_mut(),
get_var: if has(CAP_VARIABLES_READ) { host_get_var } else { deny_get_var },
set_var: if has(CAP_VARIABLES_WRITE) { host_set_var } else { deny_set_var },
export_var: if has(CAP_VARIABLES_WRITE) { host_export_var } else { deny_export_var },
get_cwd: if has(CAP_FILESYSTEM) { host_get_cwd } else { deny_get_cwd },
set_cwd: if has(CAP_FILESYSTEM) { host_set_cwd } else { deny_set_cwd },
write_stdout: if has(CAP_IO) { host_write_stdout } else { deny_write_stdout },
write_stderr: if has(CAP_IO) { host_write_stderr } else { deny_write_stderr },
}
}