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() {
if let Ok(abs) = std::fs::canonicalize(value) {
return format!("{{ path = \"{}\" }}", abs.display());
}
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/colliery-io/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>,
extension: 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)?;
if let Some(ext) = extension {
let fidius_toml = format!("extension = \"{ext}\"\n");
std::fs::write(crate_dir.join("fidius.toml"), fidius_toml)?;
}
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 fidius_dep = resolve_dep("fidius", 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 = {fidius_dep}
"#
);
let struct_name = format!("My{trait_name}");
let lib_rs = format!(
r#"use {interface_mod}::{{plugin_impl, {trait_name}, PluginError, __fidius_{trait_name}}};
pub struct {struct_name};
#[plugin_impl({trait_name})]
impl {trait_name} for {struct_name} {{
fn process(&self, input: String) -> String {{
format!("processed: {{}}", input)
}}
}}
fidius::fidius_plugin_registry!();
"#
);
std::fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
std::fs::write(src_dir.join("lib.rs"), lib_rs)?;
let interface_path = Path::new(interface);
let extension = if interface_path.is_dir() {
let fidius_toml_path = interface_path.join("fidius.toml");
if fidius_toml_path.exists() {
let content = std::fs::read_to_string(&fidius_toml_path)?;
let table: toml::Table = content.parse().unwrap_or_default();
table
.get("extension")
.and_then(|v| v.as_str())
.map(String::from)
} else {
None
}
} else {
None
};
let ext_line = match &extension {
Some(ext) => format!("\nextension = \"{ext}\""),
None => String::new(),
};
let package_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
interface = "{interface_crate}"
interface_version = 1{ext_line}
[metadata]
"#
);
std::fs::write(crate_dir.join("package.toml"), package_toml)?;
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())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&secret_path, std::fs::Permissions::from_mode(0o600))?;
}
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(_) => Err(format!("Signature INVALID: {}", dylib_path.display()).into()),
}
}
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(())
}
pub fn package_validate(dir: &Path) -> Result {
let manifest = fidius_core::package::load_manifest_untyped(dir)?;
let pkg = &manifest.package;
println!("Package: {} v{}", pkg.name, pkg.version);
println!(
" Interface: {} (version {})",
pkg.interface, pkg.interface_version
);
println!(
" Metadata: {} field(s)",
manifest.metadata.as_table().map_or(0, |t| t.len())
);
println!("\nManifest valid.");
Ok(())
}
pub fn package_build(dir: &Path, release: bool) -> Result {
let manifest = fidius_core::package::load_manifest_untyped(dir)?;
let cargo_toml = dir.join("Cargo.toml");
if !cargo_toml.exists() {
return Err(format!("Cargo.toml not found in {}", dir.display()).into());
}
println!(
"Building package: {} v{}",
manifest.package.name, manifest.package.version
);
let mut cmd = std::process::Command::new("cargo");
cmd.arg("build").arg("--manifest-path").arg(&cargo_toml);
if release {
cmd.arg("--release");
}
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("build failed:\n{}", stderr).into());
}
let profile = if release { "release" } else { "debug" };
println!(
"Build successful. Output in {}/target/{}/",
dir.display(),
profile
);
Ok(())
}
pub fn package_inspect(dir: &Path) -> Result {
let manifest = fidius_core::package::load_manifest_untyped(dir)?;
let pkg = &manifest.package;
println!("Package: {}", dir.display());
println!(" Name: {}", pkg.name);
println!(" Version: {}", pkg.version);
println!(" Interface: {}", pkg.interface);
println!(" Interface version: {}", pkg.interface_version);
if let Some(table) = manifest.metadata.as_table() {
println!(" Metadata:");
for (key, value) in table {
println!(" {} = {}", key, value);
}
}
Ok(())
}
pub fn package_sign(key_path: &Path, dir: &Path) -> Result {
if !dir.join("package.toml").exists() {
return Err(format!("package.toml not found in {}", dir.display()).into());
}
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 digest = fidius_core::package::package_digest(dir)?;
let signature = signing_key.sign(&digest);
let sig_path = dir.join("package.sig");
std::fs::write(&sig_path, signature.to_bytes())?;
println!(
"Signed package: {} -> {}",
dir.display(),
sig_path.display()
);
Ok(())
}
pub fn package_verify(key_path: &Path, dir: &Path) -> Result {
if !dir.join("package.toml").exists() {
return Err(format!("package.toml not found in {}", dir.display()).into());
}
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 sig_path = dir.join("package.sig");
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);
let digest = fidius_core::package::package_digest(dir)?;
match verifying_key.verify(&digest, &signature) {
Ok(()) => {
println!("Package signature valid: {}", dir.display());
Ok(())
}
Err(_) => Err(format!("Package signature INVALID: {}", dir.display()).into()),
}
}
pub fn package_pack(dir: &Path, output: Option<&Path>) -> Result {
let result = fidius_core::package::pack_package(dir, output)?;
if result.unsigned {
eprintln!("warning: package is unsigned (no package.sig found)");
}
let size = std::fs::metadata(&result.path)?.len();
let human_size = if size >= 1024 * 1024 {
format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
} else if size >= 1024 {
format!("{:.1} KB", size as f64 / 1024.0)
} else {
format!("{size} B")
};
println!("Packed: {} ({human_size})", result.path.display());
Ok(())
}
pub fn package_unpack(archive: &Path, dest: Option<&Path>) -> Result {
let dest = dest.unwrap_or_else(|| Path::new("."));
let pkg_dir = fidius_core::package::unpack_package(archive, dest)?;
println!("Unpacked: {}", pkg_dir.display());
Ok(())
}