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("这个后端不支持该动词"),
BackendError::InvalidArgs(m) => write!(f, "后端拒绝了入参:{m}"),
BackendError::Internal(m) => write!(f, "后端内部错误:{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!(
"加载插件 {} 失败:{e}\n\
插件装在 {} —— 用 `pmpx plugin rm {}` 删掉后重装试试。",
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!(
"插件 {} 的 ABI 版本是 {},pmpx 需要 {}。\n\
它们是两个独立编译的世界,布局对不上就不能加载 —— 这比崩溃好。\n\
用 `pmpx plugin update {}` 升级插件,或把 pmpx 升到匹配的版本。",
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!(
"插件自报名是 \"{self_reported}\",但 manifest 声明的是 \"{}\" —— 拒绝加载。\n\
请删掉 {} 后重新安装。",
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!(
"插件 {} 认为参数不合法",
self.name
))))
}
PMPX_ERR_INTERNAL => {
return Ok(Err(BackendError::Internal(format!(
"插件 {} 内部出错或 panic 了(细节在它自己的 stderr 上)",
self.name
))))
}
other => {
return Ok(Err(BackendError::Internal(format!(
"插件 {} 返回了未知错误码 {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("不支持"));
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) }, "", "空指针不该被解引用");
}
#[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());
}
}