forked from 1b-agent/opencadstudio-upstream
feat(plugin): gate dependency mismatches
This commit is contained in:
commit
1e1eeea527
8 files changed
+438
-46
No files matched your search
+191
-5
@@ -30,6 +30,12 @@ pub struct RegistryEntry {
|
||||
pub struct ReleaseInfo {
|
||||
pub tag: String,
|
||||
pub api_version: u32,
|
||||
/// Full `acadrust` git source used by the release.
|
||||
pub acadrust_source: Option<String>,
|
||||
/// Whether `[opencad]` declares `acadrust_source`.
|
||||
pub acadrust_declared: bool,
|
||||
/// Whether the release matches this host.
|
||||
pub acadrust_compatible: bool,
|
||||
}
|
||||
|
||||
/// An add-on package found on disk (not necessarily loaded or compatible).
|
||||
@@ -44,6 +50,10 @@ pub struct ExternalPlugin {
|
||||
/// an older `plugin.toml` does not declare `repository`.
|
||||
pub repository: Option<String>,
|
||||
pub api_version: u32,
|
||||
/// Full `acadrust` git source used by the plugin.
|
||||
pub acadrust_source: Option<String>,
|
||||
/// Whether `[opencad]` declares `acadrust_source`.
|
||||
pub acadrust_declared: bool,
|
||||
pub ribbon_order: i32,
|
||||
pub command_prefixes: Vec<String>,
|
||||
/// The package directory under the plugins folder.
|
||||
@@ -58,11 +68,27 @@ impl ExternalPlugin {
|
||||
ocs_plugin_api::manifest::host_accepts_plugin_version(self.api_version)
|
||||
}
|
||||
|
||||
/// True when the package can be loaded today: compatible API *and* a native
|
||||
/// library present for this platform.
|
||||
/// Returns whether the package's dependency fingerprint matches the host.
|
||||
pub fn acadrust_compatible(&self) -> bool {
|
||||
if !ocs_plugin_api::version_info::uses_acadrust_gate(self.api_version) {
|
||||
return true;
|
||||
}
|
||||
if !self.acadrust_declared {
|
||||
return true;
|
||||
}
|
||||
match self.acadrust_source.as_deref() {
|
||||
None | Some("") => false,
|
||||
Some(source) => ocs_plugin_api::version_info::acadrust_sources_compatible(
|
||||
source,
|
||||
ocs_plugin_api::version_info::host_acadrust_source(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the package can be loaded.
|
||||
#[allow(dead_code)] // plugin-host surface (issue #100); not yet wired
|
||||
pub fn loadable(&self) -> bool {
|
||||
self.api_compatible() && self.lib_present
|
||||
self.api_compatible() && self.acadrust_compatible() && self.lib_present
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +202,21 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
let mut description = String::new();
|
||||
let mut repository = None;
|
||||
let mut api_version: u32 = 0;
|
||||
let mut acadrust_source: Option<String> = None;
|
||||
let mut acadrust_declared = false;
|
||||
let mut ribbon_order: i32 = 0;
|
||||
let mut command_prefixes: Vec<String> = Vec::new();
|
||||
let mut section = None;
|
||||
|
||||
for raw in text.lines() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some(header) = line.strip_prefix('[') {
|
||||
section = header
|
||||
.find(']')
|
||||
.map(|end| header[..end].trim());
|
||||
continue;
|
||||
}
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
@@ -196,6 +231,11 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
"description" => description = unquote(value),
|
||||
"repository" => repository = normalize_repository(&unquote(value)),
|
||||
"api_version" => api_version = value.parse().unwrap_or(0),
|
||||
"acadrust_source" if section == Some("opencad") => {
|
||||
acadrust_declared = true;
|
||||
let v = unquote(value);
|
||||
acadrust_source = if v.is_empty() { None } else { Some(v) };
|
||||
}
|
||||
"ribbon_order" => ribbon_order = value.parse().unwrap_or(0),
|
||||
"command_prefixes" => command_prefixes = parse_string_array(value),
|
||||
_ => {}
|
||||
@@ -209,6 +249,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
description,
|
||||
repository,
|
||||
api_version,
|
||||
acadrust_source,
|
||||
acadrust_declared,
|
||||
ribbon_order,
|
||||
command_prefixes,
|
||||
dir: PathBuf::new(),
|
||||
@@ -298,7 +340,36 @@ mod loader {
|
||||
manager.set_notification_handler(v4_support::notification_handler());
|
||||
let mut out = Vec::new();
|
||||
for d in &discovered {
|
||||
if !d.api_compatible() || !d.lib_present {
|
||||
if !d.api_compatible() {
|
||||
continue;
|
||||
}
|
||||
if !d.lib_present {
|
||||
continue;
|
||||
}
|
||||
if ocs_plugin_api::version_info::uses_acadrust_gate(d.api_version)
|
||||
&& d.acadrust_declared
|
||||
&& d.acadrust_source.is_none()
|
||||
{
|
||||
eprintln!(
|
||||
"[plugin] {} declares acadrust metadata but has no fingerprint; cannot verify compatibility",
|
||||
d.id
|
||||
);
|
||||
}
|
||||
if !d.acadrust_compatible() {
|
||||
let host_src = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let plugin_hash = d
|
||||
.acadrust_source
|
||||
.as_deref()
|
||||
.and_then(ocs_plugin_api::version_info::acadrust_source_hash)
|
||||
.unwrap_or("unknown");
|
||||
let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src)
|
||||
.unwrap_or("unknown");
|
||||
out.push((
|
||||
d.id.clone(),
|
||||
Err(format!(
|
||||
"Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}"
|
||||
)),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let Some(path) = lib_file(&d.dir) else {
|
||||
@@ -412,6 +483,121 @@ xdata_apps = ["MYPLUGIN_RECORD"]
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_acadrust_falls_back_to_api_gate() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(!p.acadrust_declared);
|
||||
assert!(p.acadrust_source.is_none());
|
||||
assert!(p.acadrust_compatible(), "undeclared acadrust is treated as compatible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_empty_acadrust_source_is_incompatible() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = ""
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(p.acadrust_source.is_none());
|
||||
assert!(!p.acadrust_compatible(), "declared but empty source is incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_source_outside_opencad_is_ignored() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
api_version = 4
|
||||
acadrust_source = "0123456789012345678901234567890123456789"
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(!p.acadrust_declared);
|
||||
assert!(p.acadrust_source.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_mismatch_detected() {
|
||||
let host = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let other = if host.contains("94df2c3") {
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
|
||||
} else {
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"
|
||||
};
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = "{other}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(!p.acadrust_compatible(), "mismatched acadrust fingerprint should be incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_match_detected() {
|
||||
let host = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = "{host}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(p.acadrust_compatible(), "matching acadrust fingerprint should be compatible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_gate_only_applies_to_api_v4_and_newer() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 2
|
||||
|
||||
[opencad]
|
||||
acadrust_source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
|
||||
"#;
|
||||
let mut p = parse_plugin_toml(toml).expect("parsed");
|
||||
p.lib_present = true;
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(
|
||||
p.acadrust_compatible(),
|
||||
"API v2 plugin should bypass acadrust gate"
|
||||
);
|
||||
assert!(p.loadable());
|
||||
}
|
||||
|
||||
/// Integration smoke test for the out-of-process plugin path.
|
||||
/// Set `OCS_TEST_PLUGIN` to the built cdylib path and make sure the
|
||||
/// `OpenCADStudio` binary is built; the test uses it as the runner host.
|
||||
|
||||
Reference in new issue
Block a user