Bunch of changes, like a lot
This commit is contained in:
1 parent
f07f54d4c1
commit
3297f60f00
25 files changed
+566
-285
No files matched your search
@@ -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
@@ -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 = ®istry.info[&metadata.id];
|
||||
format!(
|
||||
"{} (API v{}, engine {})",
|
||||
info.version, info.protocol, info.engine_version
|
||||
)
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
let info = ®istry.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 ®istry.plugins {
|
||||
let metadata = plugin.metadata();
|
||||
if official.contains(&metadata.id) {
|
||||
continue;
|
||||
}
|
||||
let info = ®istry.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
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user