Bunch of changes, like a lot

This commit is contained in:
Elias Wendland committed 2026-07-17 18:58:52 +02:00
1 parent f07f54d4c1
commit 3297f60f00
25 files changed
+566 -285

No files matched your search

+7
View File
@@ -18,6 +18,13 @@ use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(about = "A modular file converter.", long_about = None)]
pub struct Args {
#[arg(
long,
help = "Print plugin API versions supported by this engine.",
conflicts_with_all = ["list_formats", "list_plugins"]
)]
pub plugin_api: bool,
#[arg(
long,
help = "List formats supported by installed plugins.",
+50 -4
View File
@@ -117,6 +117,15 @@ fn main() {
);
tracing::trace!(?args, "parsed command-line arguments");
if args.plugin_api {
println!("{}", convertis_plugin_api::API_VERSION);
tracing::debug!(
api = convertis_plugin_api::API_VERSION,
"reported supported plugin API"
);
return;
}
let registry = plugin::PluginRegistry::load(&args.plugin_dirs, !args.no_default_plugins);
tracing::trace!(
plugin_count = registry.plugins.len(),
@@ -128,8 +137,8 @@ fn main() {
}
if args.list_plugins {
tracing::debug!("listing official plugins");
println!("Official plugins:");
tracing::debug!("listing installed and official plugins");
println!("Plugins:");
for metadata in catalog::official_plugins() {
tracing::trace!(plugin = %metadata.id, package = %metadata.package, "checking official plugin state");
let installed = registry
@@ -137,8 +146,20 @@ fn main() {
.iter()
.find(|plugin| plugin.metadata().id == metadata.id);
let state = match installed.map(|plugin| plugin.availability()) {
Some(Ok(())) => "installed".to_owned(),
Some(Err(error)) => format!("unavailable: {error}"),
Some(Ok(())) => {
let info = &registry.info[&metadata.id];
format!(
"{} (API v{}, engine {})",
info.version, info.protocol, info.engine_version
)
}
Some(Err(error)) => {
let info = &registry.info[&metadata.id];
format!(
"{} (API v{}, engine {}, unavailable: {error})",
info.version, info.protocol, info.engine_version
)
}
None => "not installed".to_owned(),
};
tracing::trace!(plugin = %metadata.id, state, "resolved official plugin state");
@@ -147,6 +168,31 @@ fn main() {
metadata.package, state, metadata.description
);
}
let official: HashSet<_> = catalog::official_plugins()
.into_iter()
.map(|metadata| metadata.id)
.collect();
for plugin in &registry.plugins {
let metadata = plugin.metadata();
if official.contains(&metadata.id) {
continue;
}
let info = &registry.info[&metadata.id];
let state = match plugin.availability() {
Ok(()) => format!(
"{} (API v{}, engine {})",
info.version, info.protocol, info.engine_version
),
Err(error) => format!(
"{} (API v{}, engine {}, unavailable: {error})",
info.version, info.protocol, info.engine_version
),
};
println!(
" {:32} {:28} {}",
metadata.package, state, metadata.description
);
}
tracing::debug!("finished listing official plugins");
return;
}
+85 -137
View File
@@ -5,16 +5,14 @@
// the Free Software Foundation, version 3 exclusively.
use convertis_plugin_api::{
AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2,
LEGACY_FACTORY_SYMBOL_V1, LEGACY_MANIFEST_SYMBOL_V1, MANIFEST_SYMBOL, METADATA_SYMBOL_V2,
Plugin, PluginConvertCall, PluginFactory, PluginJsonCall, PluginManifest, PluginMetadata,
AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2, MANIFEST_SYMBOL,
METADATA_SYMBOL_V2, Plugin, PluginConvertCall, PluginJsonCall, PluginManifest, PluginMetadata,
PluginStringFree, WireResponse,
};
use libloading::Library;
use semver::{Version, VersionReq};
use serde::{Deserialize, de::DeserializeOwned};
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
ffi::{CStr, CString, c_char},
fs,
path::{Path, PathBuf},
@@ -26,18 +24,9 @@ const SUPPORTED_PROTOCOLS: &[u32] = &[2];
struct StableManifest {
manifest_version: u32,
protocol_versions: Vec<u32>,
engine_requirement: String,
plugin_id: String,
plugin_version: String,
target: String,
}
#[derive(Debug, Deserialize)]
struct LegacyManifestV1 {
api_version: u32,
engine_version: String,
plugin_id: String,
rustc_version: String,
target: String,
}
@@ -48,6 +37,20 @@ struct ProtocolV2Plugin {
free: PluginStringFree,
}
#[derive(Clone, Debug)]
pub struct PluginInfo {
pub version: String,
pub protocol: u32,
pub engine_version: String,
}
struct LoadedPlugin {
library: Library,
plugin: Box<dyn Plugin>,
id: String,
info: PluginInfo,
}
impl ProtocolV2Plugin {
unsafe fn call<T: DeserializeOwned>(
&self,
@@ -107,6 +110,7 @@ pub struct PluginRegistry {
pub plugins: Vec<Box<dyn Plugin>>,
libraries: Vec<Library>,
pub diagnostics: Vec<String>,
pub info: HashMap<String, PluginInfo>,
}
impl PluginRegistry {
@@ -151,6 +155,7 @@ impl PluginRegistry {
plugins: Vec::new(),
libraries: Vec::new(),
diagnostics: Vec::new(),
info: HashMap::new(),
};
let mut seen_paths = HashSet::new();
let mut seen_ids = HashSet::new();
@@ -198,11 +203,18 @@ impl PluginRegistry {
}
tracing::debug!(path = %path.display(), "loading plugin candidate");
match unsafe { Self::load_one(&path) } {
Ok((library, plugin, id, protocol)) => {
Ok(loaded) => {
let LoadedPlugin {
library,
plugin,
id,
info,
} = loaded;
if seen_ids.insert(id.clone()) {
tracing::info!(plugin = %id, protocol, path = %path.display(), "loaded plugin");
tracing::info!(plugin = %id, protocol = info.protocol, path = %path.display(), "loaded plugin");
registry.plugins.push(plugin);
registry.libraries.push(library);
registry.info.insert(id, info);
} else {
tracing::debug!(plugin = %id, path = %path.display(), "ignored duplicate plugin");
registry.diagnostics.push(format!(
@@ -228,98 +240,68 @@ impl PluginRegistry {
registry
}
unsafe fn load_one(path: &Path) -> Result<(Library, Box<dyn Plugin>, String, u32), String> {
unsafe fn load_one(path: &Path) -> Result<LoadedPlugin, String> {
tracing::trace!(path = %path.display(), "opening dynamic library");
let library = unsafe { Library::new(path) }.map_err(|error| error.to_string())?;
tracing::trace!(path = %path.display(), "dynamic library opened");
if let Ok(manifest_fn) = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) } {
tracing::trace!(path = %path.display(), "found stable plugin manifest entry point");
let manifest: StableManifest = unsafe { read_manifest(*manifest_fn) }?;
let protocol = validate_stable_manifest(&manifest, env!("CARGO_PKG_VERSION"))?;
tracing::debug!(
plugin = %manifest.plugin_id,
plugin_version = %manifest.plugin_version,
protocol,
engine_requirement = %manifest.engine_requirement,
"negotiated plugin protocol"
);
let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?;
let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) }
.map_err(|error| {
format!("protocol v2 availability entry point is missing: {error}")
})?;
let convert = *unsafe { library.get::<PluginConvertCall>(CONVERT_SYMBOL_V2) }.map_err(
|error| format!("protocol v2 conversion entry point is missing: {error}"),
)?;
let free =
*unsafe { library.get::<PluginStringFree>(FREE_SYMBOL_V2) }.map_err(|error| {
format!("protocol v2 string-free entry point is missing: {error}")
})?;
tracing::trace!(plugin = %manifest.plugin_id, "resolved all protocol v2 entry points");
let metadata: PluginMetadata = unsafe {
let pointer = metadata_call();
decode_response(pointer, free, "metadata")
}?;
if metadata.id != manifest.plugin_id {
return Err("manifest and plugin metadata IDs differ".to_owned());
}
let id = manifest.plugin_id;
let plugin = ProtocolV2Plugin {
metadata,
availability,
convert,
free,
};
return Ok((library, Box::new(plugin), id, protocol));
}
tracing::trace!(path = %path.display(), "stable manifest absent; trying legacy protocol v1 adapter");
let manifest_fn = unsafe { library.get::<PluginManifest>(LEGACY_MANIFEST_SYMBOL_V1) }
.map_err(|error| format!("missing stable or legacy ABI manifest: {error}"))?;
let manifest: LegacyManifestV1 = unsafe { read_manifest(*manifest_fn) }?;
validate_legacy_manifest(&manifest)?;
let factory = unsafe { library.get::<PluginFactory>(LEGACY_FACTORY_SYMBOL_V1) }
.map_err(|error| format!("missing legacy Rust plugin factory: {error}"))?;
let plugin = unsafe { factory() };
if plugin.metadata().id != manifest.plugin_id {
return Err("legacy manifest and plugin IDs differ".to_owned());
}
let manifest_fn = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) }
.map_err(|error| format!("missing protocol v2 manifest: {error}"))?;
tracing::trace!(path = %path.display(), "found plugin manifest entry point");
let manifest: StableManifest = unsafe { read_manifest(*manifest_fn) }?;
let protocol = validate_stable_manifest(&manifest)?;
tracing::debug!(
plugin = %manifest.plugin_id,
built_for_engine = %manifest.engine_version,
"loaded plugin through legacy protocol v1 adapter"
plugin_version = %manifest.plugin_version,
protocol,
"negotiated plugin protocol"
);
Ok((library, plugin, manifest.plugin_id, 1))
let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?;
let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 availability entry point is missing: {error}"))?;
let convert = *unsafe { library.get::<PluginConvertCall>(CONVERT_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 conversion entry point is missing: {error}"))?;
let free = *unsafe { library.get::<PluginStringFree>(FREE_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 string-free entry point is missing: {error}"))?;
tracing::trace!(plugin = %manifest.plugin_id, "resolved all protocol v2 entry points");
let metadata: PluginMetadata = unsafe {
let pointer = metadata_call();
decode_response(pointer, free, "metadata")
}?;
if metadata.id != manifest.plugin_id {
return Err("manifest and plugin metadata IDs differ".to_owned());
}
let id = manifest.plugin_id;
let version = manifest.plugin_version;
let engine_version = manifest.engine_version;
let plugin = ProtocolV2Plugin {
metadata,
availability,
convert,
free,
};
Ok(LoadedPlugin {
library,
plugin: Box::new(plugin),
id,
info: PluginInfo {
version,
protocol,
engine_version,
},
})
}
}
fn validate_stable_manifest(
manifest: &StableManifest,
engine_version: &str,
) -> Result<u32, String> {
tracing::trace!(
?manifest,
engine_version,
"validating stable plugin manifest"
);
fn validate_stable_manifest(manifest: &StableManifest) -> Result<u32, String> {
tracing::trace!(?manifest, "validating plugin manifest");
if manifest.manifest_version != 1 {
return Err(format!(
"manifest format {} is not supported",
manifest.manifest_version
));
}
let engine = Version::parse(engine_version)
.map_err(|error| format!("engine has an invalid version: {error}"))?;
let requirement = VersionReq::parse(&manifest.engine_requirement)
.map_err(|error| format!("plugin has an invalid engine requirement: {error}"))?;
if !requirement.matches(&engine) {
return Err(format!(
"plugin requires engine {}, but this engine is {}",
manifest.engine_requirement, engine_version
));
}
if manifest.target != env!("CONVERTIS_TARGET") {
return Err(format!(
"plugin targets {}, but the engine targets {}",
@@ -340,39 +322,6 @@ fn validate_stable_manifest(
})
}
fn validate_legacy_manifest(manifest: &LegacyManifestV1) -> Result<(), String> {
tracing::trace!(?manifest, "validating legacy plugin manifest");
if manifest.api_version != 1 {
return Err(format!(
"legacy plugin API {} is not supported",
manifest.api_version
));
}
if manifest.engine_version != convertis_plugin_api::ENGINE_VERSION {
tracing::debug!(
plugin = %manifest.plugin_id,
built_for_engine = %manifest.engine_version,
current_engine = convertis_plugin_api::ENGINE_VERSION,
"legacy engine release differs; continuing because protocol v1 is retained"
);
}
if manifest.rustc_version != env!("CONVERTIS_RUSTC_VERSION") {
return Err(format!(
"legacy plugin was built with {}, but the engine uses {}; protocol v1 uses the unstable Rust ABI",
manifest.rustc_version,
env!("CONVERTIS_RUSTC_VERSION")
));
}
if manifest.target != env!("CONVERTIS_TARGET") {
return Err(format!(
"legacy plugin targets {}, but the engine targets {}",
manifest.target,
env!("CONVERTIS_TARGET")
));
}
Ok(())
}
unsafe fn read_manifest<T: DeserializeOwned>(manifest: PluginManifest) -> Result<T, String> {
tracing::trace!("calling plugin manifest entry point");
let pointer = unsafe { manifest() };
@@ -430,33 +379,32 @@ unsafe fn decode_response<T: DeserializeOwned>(
mod tests {
use super::*;
fn manifest(requirement: &str, protocols: &[u32]) -> StableManifest {
fn manifest(protocols: &[u32]) -> StableManifest {
StableManifest {
manifest_version: 1,
protocol_versions: protocols.to_vec(),
engine_requirement: requirement.into(),
plugin_id: "example".into(),
plugin_version: "0.3.0".into(),
engine_version: "0.3.0".into(),
target: env!("CONVERTIS_TARGET").into(),
}
}
#[test]
fn current_plugin_requirement_accepts_future_engines() {
let manifest = manifest(">=0.3.0", &[2]);
assert_eq!(validate_stable_manifest(&manifest, "0.3.0"), Ok(2));
assert_eq!(validate_stable_manifest(&manifest, "99.0.0"), Ok(2));
}
#[test]
fn protocol_is_negotiated_independently_of_release_version() {
let manifest = manifest(">=0.1.0", &[1, 2, 3]);
assert_eq!(validate_stable_manifest(&manifest, "1.0.0"), Ok(2));
let manifest = manifest(&[2, 3]);
assert_eq!(validate_stable_manifest(&manifest), Ok(2));
}
#[test]
fn unsupported_protocol_is_rejected() {
let error = validate_stable_manifest(&manifest(">=0.3.0", &[7]), "0.3.0").unwrap_err();
let error = validate_stable_manifest(&manifest(&[7])).unwrap_err();
assert!(error.contains("not supported"));
}
#[test]
fn api_v1_is_permanently_rejected() {
let error = validate_stable_manifest(&manifest(&[1])).unwrap_err();
assert!(error.contains("not supported"));
}
}