use std::ffi::OsString;
use std::path::Path;
use crate_plugin_kit::{CratePluginKit, LoadedPlugin};
use pmpx_plugin::abi::{
self, PmpxCommand, PmpxPluginV1, PmpxStr, ABI_VERSION, PMPX_ERR_INTERNAL,
PMPX_ERR_INVALID_ARGS, PMPX_ERR_UNSUPPORTED_VERB, PMPX_OK,
};
use pmpx_plugin::{CommandSpec, Verb};
use crate::error::{PmpxError, Result};
use crate::plugins::InstalledPlugin;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BackendError {
UnsupportedVerb,
InvalidArgs(String),
Internal(String),
}
impl BackendError {
pub fn exit_code(&self) -> u8 {
match self {
BackendError::UnsupportedVerb | BackendError::InvalidArgs(_) => {
crate::error::EXIT_USAGE
}
BackendError::Internal(_) => crate::error::EXIT_INTERNAL,
}
}
}
impl std::fmt::Display for BackendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendError::UnsupportedVerb => f.write_str("this backend does not support that verb"),
BackendError::InvalidArgs(m) => write!(f, "the backend rejected the arguments: {m}"),
BackendError::Internal(m) => write!(f, "the backend failed internally: {m}"),
}
}
}
pub struct Backend {
loaded: LoadedPlugin<PmpxPluginV1>,
pub name: String,
}
impl Backend {
pub fn load(kit: &CratePluginKit<PmpxPluginV1>, plugin: &InstalledPlugin) -> Result<Self> {
let loaded = kit.load(&plugin.crate_name).map_err(|e| {
PmpxError::not_found(format!(
"failed to load plugin {}: {e}\n\
It is installed at {} -- try `pmpx plugin rm {}` and install it again.",
plugin.crate_name,
plugin.dir.display(),
plugin.name
))
})?;
let entry = unsafe { &*loaded.entry() };
if entry.abi_version != ABI_VERSION {
return Err(PmpxError::not_found(format!(
"plugin {} has ABI version {}, pmpx needs {}.\n\
They are two separately compiled worlds; when the layouts do not line up it \
cannot be loaded -- that is better than crashing.\n\
Use `pmpx plugin update {}` to upgrade the plugin, or upgrade pmpx to a \
matching version.",
plugin.crate_name, entry.abi_version, ABI_VERSION, plugin.name
)));
}
let self_reported = unsafe { read_plugin_str(entry.name, entry.free_str) };
if self_reported != plugin.name {
return Err(PmpxError::not_found(format!(
"the plugin calls itself \"{self_reported}\", but the manifest declares \
\"{}\" -- refusing to load.\n\
Delete {} and install it again.",
plugin.name,
plugin.dir.display()
)));
}
Ok(Self {
loaded,
name: plugin.name.clone(),
})
}
fn entry(&self) -> &PmpxPluginV1 {
unsafe { &*self.loaded.entry() }
}
pub fn family(&self) -> String {
let e = self.entry();
unsafe { read_plugin_str(e.family, e.free_str) }
}
pub fn rustc_version(&self) -> String {
unsafe { read_bytes(self.entry().rustc_version) }
}
pub fn target(&self) -> String {
unsafe { read_bytes(self.entry().target) }
}
pub fn command(
&self,
project_root: &Path,
matched: &[String],
verb: Verb,
args: &[OsString],
) -> Result<std::result::Result<CommandSpec, BackendError>> {
let entry = self.entry();
let root_bytes = abi::os_to_bytes(project_root.as_os_str());
let root = PmpxStr {
ptr: root_bytes.as_ptr(),
len: root_bytes.len(),
};
let matched_bytes: Vec<Vec<u8>> = matched.iter().map(|m| m.as_bytes().to_vec()).collect();
let matched_raw: Vec<PmpxStr> = matched_bytes
.iter()
.map(|b| PmpxStr {
ptr: b.as_ptr(),
len: b.len(),
})
.collect();
let arg_bytes: Vec<Vec<u8>> = args
.iter()
.map(|a| abi::os_to_bytes(a.as_os_str()))
.collect();
let args_raw: Vec<PmpxStr> = arg_bytes
.iter()
.map(|b| PmpxStr {
ptr: b.as_ptr(),
len: b.len(),
})
.collect();
let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
let code = {
let out_ptr = out.as_mut_ptr();
abi::guard(move || {
unsafe {
(entry.command)(
root,
matched_raw.as_ptr(),
matched_raw.len(),
verb.to_abi(),
args_raw.as_ptr(),
args_raw.len(),
out_ptr,
)
}
})
};
match code {
PMPX_OK => {}
PMPX_ERR_UNSUPPORTED_VERB => return Ok(Err(BackendError::UnsupportedVerb)),
PMPX_ERR_INVALID_ARGS => {
return Ok(Err(BackendError::InvalidArgs(format!(
"plugin {} thinks the arguments are invalid",
self.name
))))
}
PMPX_ERR_INTERNAL => {
return Ok(Err(BackendError::Internal(format!(
"plugin {} failed internally or panicked (details on its own stderr)",
self.name
))))
}
other => {
return Ok(Err(BackendError::Internal(format!(
"plugin {} returned unknown error code {other}",
self.name
))))
}
}
let mut cmd = unsafe { out.assume_init() };
let spec = unsafe {
let program = abi::read_os(cmd.program);
let cwd = if cmd.cwd.is_empty() {
None
} else {
Some(std::path::PathBuf::from(abi::read_os(cmd.cwd)))
};
let mut argv = Vec::with_capacity(cmd.args_len);
for i in 0..cmd.args_len {
argv.push(abi::read_os(*cmd.args.add(i)));
}
CommandSpec {
program,
args: argv,
cwd,
}
};
unsafe { (entry.free_command)(&mut cmd as *mut _) };
Ok(Ok(spec))
}
pub fn diagnostics(&self) -> BackendDiagnostics {
BackendDiagnostics {
name: self.name.clone(),
family: self.family(),
rustc_version: self.rustc_version(),
target: self.target(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendDiagnostics {
pub name: String,
pub family: String,
pub rustc_version: String,
pub target: String,
}
unsafe fn read_plugin_str(
f: unsafe extern "C" fn() -> PmpxStr,
free: unsafe extern "C" fn(PmpxStr),
) -> String {
let s = unsafe { f() };
let out = unsafe { read_bytes(s) };
unsafe { free(s) };
out
}
unsafe fn read_bytes(s: PmpxStr) -> String {
if s.ptr.is_null() || s.len == 0 {
return String::new();
}
let bytes = unsafe { std::slice::from_raw_parts(s.ptr, s.len) };
String::from_utf8_lossy(bytes).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_error_exit_codes_match_the_table() {
assert_eq!(
BackendError::UnsupportedVerb.exit_code(),
crate::error::EXIT_USAGE
);
assert_eq!(
BackendError::InvalidArgs("x".into()).exit_code(),
crate::error::EXIT_USAGE
);
assert_eq!(
BackendError::Internal("x".into()).exit_code(),
crate::error::EXIT_INTERNAL
);
}
#[test]
fn backend_error_messages_name_the_backend_situation() {
assert!(BackendError::UnsupportedVerb
.to_string()
.contains("does not support"));
assert!(BackendError::InvalidArgs("a".into())
.to_string()
.contains('a'));
assert!(BackendError::Internal("b".into()).to_string().contains('b'));
}
#[test]
fn reading_an_empty_string_is_safe() {
assert_eq!(unsafe { read_bytes(PmpxStr::EMPTY) }, "");
}
#[test]
fn reading_a_null_pointer_is_safe() {
let s = PmpxStr {
ptr: std::ptr::null(),
len: 99,
};
assert_eq!(
unsafe { read_bytes(s) },
"",
"a null pointer must not be dereferenced"
);
}
#[test]
fn reading_non_utf8_bytes_does_not_panic() {
let bytes = [0xff, 0xfe, 0xfd];
let s = PmpxStr {
ptr: bytes.as_ptr(),
len: bytes.len(),
};
assert!(!unsafe { read_bytes(s) }.is_empty());
}
}