use std::path::Path;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
fn resolve_dep(value: &str, version_override: Option<&str>) -> String {
if Path::new(value).exists() {
return format!("{{ path = \"{}\" }}", value);
}
if let Some(ver) = version_override {
return format!("\"{}\"", ver);
}
if let Some(ver) = check_crates_io(value) {
return format!("\"{}\"", ver);
}
eprintln!(
"warning: could not find '{}' as a local path or on crates.io, using path dep",
value
);
format!("{{ path = \"{}\" }}", value)
}
fn check_crates_io(name: &str) -> Option<String> {
let url = format!("https://crates.io/api/v1/crates/{}", name);
let mut response = ureq::get(&url)
.header("User-Agent", "fidius-cli (https://github.com/fidius-rs/fidius)")
.call()
.ok()?;
let body_str = response.body_mut().read_to_string().ok()?;
let body: serde_json::Value = serde_json::from_str(&body_str).ok()?;
body["crate"]["max_stable_version"]
.as_str()
.map(String::from)
}
pub fn init_interface(
name: &str,
trait_name: &str,
path: Option<&Path>,
version: Option<&str>,
) -> Result {
let base = path.unwrap_or_else(|| Path::new("."));
let crate_dir = base.join(name);
if crate_dir.exists() {
return Err(format!("directory '{}' already exists", crate_dir.display()).into());
}
let src_dir = crate_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
let fidius_dep = resolve_dep("fidius", version);
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
[dependencies]
fidius = {fidius_dep}
"#
);
let lib_rs = format!(
r#"pub use fidius::{{plugin_impl, PluginError}};
#[fidius::plugin_interface(version = 1, buffer = PluginAllocated)]
pub trait {trait_name}: Send + Sync {{
fn process(&self, input: String) -> String;
}}
"#
);
std::fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
std::fs::write(src_dir.join("lib.rs"), lib_rs)?;
println!("Created interface crate: {}", crate_dir.display());
Ok(())
}
pub fn init_plugin(
name: &str,
interface: &str,
trait_name: &str,
path: Option<&Path>,
version: Option<&str>,
) -> Result {
let base = path.unwrap_or_else(|| Path::new("."));
let crate_dir = base.join(name);
if crate_dir.exists() {
return Err(format!("directory '{}' already exists", crate_dir.display()).into());
}
let src_dir = crate_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
let interface_dep = resolve_dep(interface, version);
let interface_crate = Path::new(interface)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(interface);
let interface_mod = interface_crate.replace('-', "_");
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
{interface_crate} = {interface_dep}
fidius-core = {{ version = "0.1" }}
"#
);
let struct_name = format!("My{trait_name}");
let lib_rs = format!(
r#"use {interface_mod}::{{plugin_impl, {trait_name}, PluginError}};
pub struct {struct_name};
#[plugin_impl({trait_name})]
impl {trait_name} for {struct_name} {{
fn process(&self, input: String) -> String {{
todo!("implement {trait_name}")
}}
}}
fidius_core::fidius_plugin_registry!();
"#
);
std::fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
std::fs::write(src_dir.join("lib.rs"), lib_rs)?;
println!("Created plugin crate: {}", crate_dir.display());
Ok(())
}
pub fn keygen(out: &str) -> Result {
use rand::rngs::OsRng;
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let secret_path = format!("{}.secret", out);
let public_path = format!("{}.public", out);
std::fs::write(&secret_path, signing_key.to_bytes())?;
std::fs::write(&public_path, verifying_key.to_bytes())?;
println!("Generated keypair:");
println!(" Secret: {}", secret_path);
println!(" Public: {}", public_path);
Ok(())
}
pub fn sign(key_path: &Path, dylib_path: &Path) -> Result {
let key_bytes: [u8; 32] = std::fs::read(key_path)?
.try_into()
.map_err(|_| "secret key must be exactly 32 bytes")?;
let signing_key = SigningKey::from_bytes(&key_bytes);
let dylib_bytes = std::fs::read(dylib_path)?;
let signature = signing_key.sign(&dylib_bytes);
let sig_path = dylib_path.with_extension(format!(
"{}.sig",
dylib_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
));
std::fs::write(&sig_path, signature.to_bytes())?;
println!("Signed: {} -> {}", dylib_path.display(), sig_path.display());
Ok(())
}
pub fn verify(key_path: &Path, dylib_path: &Path) -> Result {
let key_bytes: [u8; 32] = std::fs::read(key_path)?
.try_into()
.map_err(|_| "public key must be exactly 32 bytes")?;
let verifying_key =
VerifyingKey::from_bytes(&key_bytes).map_err(|e| format!("invalid public key: {e}"))?;
let dylib_bytes = std::fs::read(dylib_path)?;
let sig_path = dylib_path.with_extension(format!(
"{}.sig",
dylib_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
));
let sig_bytes: [u8; 64] = std::fs::read(&sig_path)
.map_err(|_| format!("signature file not found: {}", sig_path.display()))?
.try_into()
.map_err(|_| "signature must be exactly 64 bytes")?;
let signature = Signature::from_bytes(&sig_bytes);
match verifying_key.verify(&dylib_bytes, &signature) {
Ok(()) => {
println!("Signature valid: {}", dylib_path.display());
Ok(())
}
Err(_) => {
eprintln!("Signature INVALID: {}", dylib_path.display());
std::process::exit(1);
}
}
}
pub fn inspect(dylib_path: &Path) -> Result {
let loaded = fidius_host::loader::load_library(dylib_path)
.map_err(|e| format!("failed to load {}: {e}", dylib_path.display()))?;
println!("Plugin Registry: {}", dylib_path.display());
println!(" Plugins: {}", loaded.plugins.len());
println!();
for (i, plugin) in loaded.plugins.iter().enumerate() {
let info = &plugin.info;
println!(" [{}] {}", i, info.name);
println!(" Interface: {}", info.interface_name);
println!(" Interface hash: {:#018x}", info.interface_hash);
println!(" Interface version: {}", info.interface_version);
println!(
" Buffer strategy: {:?}",
info.buffer_strategy
);
println!(" Wire format: {:?}", info.wire_format);
println!(" Capabilities: {:#018x}", info.capabilities);
}
Ok(())
}