diff --git a/Cargo.lock b/Cargo.lock index 160bc72d..808da012 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=1fa0a5e#1fa0a5e22f53dd45c6b0b9d8c63f7fc7730a3a8f" +source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b2ae66#5b2ae66d0bd7b0da2d13392d3c3332f8a39caa1f" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 1b3573a5..dc49bfb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1fa0a5e", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" @@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "1fa0a5e" } +acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 6f6feee5..fb3a0f1a 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_plugin_api/src/version_info.rs b/crates/ocs_plugin_api/src/version_info.rs index 1d690c07..1df0bc30 100644 --- a/crates/ocs_plugin_api/src/version_info.rs +++ b/crates/ocs_plugin_api/src/version_info.rs @@ -1,5 +1,7 @@ //! Embedded version metadata generated at build time. +use std::sync::OnceLock; + /// The embedded version info as a JSON string. pub const EMBEDDED_VERSION_INFO_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/version_info.json")); @@ -9,6 +11,57 @@ pub fn get_embedded_version_info_json() -> &'static str { EMBEDDED_VERSION_INFO_JSON } +#[derive(Debug, Clone)] +struct VersionInfo { + acadrust_source: String, +} + +pub const ACADRUST_GATE_API_VERSION: u32 = 4; + +/// Returns whether this API version uses the dependency gate. +pub fn uses_acadrust_gate(api_version: u32) -> bool { + api_version >= ACADRUST_GATE_API_VERSION +} + +/// Extracts a string from the generated compact JSON. +fn json_string_value(json: &str, key: &str) -> Option { + let pattern = format!(r#""{key}":""#); + let start = json.find(&pattern)? + pattern.len(); + let end = json[start..].find('"')?; + Some(json[start..start + end].to_string()) +} + +fn parsed_version_info() -> &'static VersionInfo { + static INFO: OnceLock = OnceLock::new(); + INFO.get_or_init(|| VersionInfo { + acadrust_source: json_string_value(EMBEDDED_VERSION_INFO_JSON, "acadrust_source") + .unwrap_or_default(), + }) +} + +/// Returns the host's full `acadrust` Cargo source. +pub fn host_acadrust_source() -> &'static str { + &parsed_version_info().acadrust_source +} + +/// Extracts the full git commit hash from a Cargo source. +pub fn acadrust_source_hash(source: &str) -> Option<&str> { + let hash = source.rsplit('#').next()?; + if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) { + Some(hash) + } else { + None + } +} + +/// Returns whether two Cargo sources use the same commit. +pub fn acadrust_sources_compatible(a: &str, b: &str) -> bool { + match (acadrust_source_hash(a), acadrust_source_hash(b)) { + (Some(ha), Some(hb)) => ha.eq_ignore_ascii_case(hb), + _ => false, + } +} + #[cfg(test)] mod tests { use super::*; @@ -19,7 +72,7 @@ mod tests { assert!(!json.is_empty()); let value: serde_json::Value = serde_json::from_str(json).expect("valid JSON"); assert!(value.get("ocs_version").is_some()); - assert!(value.get("acadrust_version").is_some()); + assert!(value.get("acadrust_source").is_some()); } #[test] @@ -54,4 +107,57 @@ mod tests { let acadrust = value["acadrust_version"].as_str().expect("acadrust_version string"); assert_eq!(acadrust.split('.').count(), 3); } + + #[test] + fn host_acadrust_source_is_non_empty() { + assert!(!host_acadrust_source().is_empty()); + } + + #[test] + fn dependency_gate_starts_at_api_v4() { + assert!(!uses_acadrust_gate(3)); + assert!(uses_acadrust_gate(4)); + assert!(uses_acadrust_gate(5)); + } + + #[test] + fn extracts_full_hash_from_cargo_source() { + let src = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"; + assert_eq!( + acadrust_source_hash(src), + Some("94df2c3f87fa051b16ffc3923f80e9247c85c5fd") + ); + } + + #[test] + fn rejects_malformed_or_missing_hash() { + assert!(acadrust_source_hash("").is_none()); + assert!(acadrust_source_hash("registry+https://crates.io").is_none()); + assert!(acadrust_source_hash("git+https://github.com/foo/bar.git#short").is_none()); + assert!( + acadrust_source_hash("git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg") + .is_none() + ); + } + + #[test] + fn source_comparison_matches_full_hashes() { + let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"; + let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"; + assert!(acadrust_sources_compatible(a, b)); + } + + #[test] + fn source_comparison_detects_mismatch() { + let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"; + let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"; + assert!(!acadrust_sources_compatible(a, b)); + } + + #[test] + fn source_comparison_case_insensitive() { + let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"; + let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94DF2C3F87FA051B16FFC3923F80E9247C85C5FD"; + assert!(acadrust_sources_compatible(a, b)); + } } diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index b3a052b0..202b5164 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/src/plugin/external.rs b/src/plugin/external.rs index d7df8c48..c395b1e5 100644 --- a/src/plugin/external.rs +++ b/src/plugin/external.rs @@ -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, + /// 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, pub api_version: u32, + /// Full `acadrust` git source used by the plugin. + pub acadrust_source: Option, + /// Whether `[opencad]` declares `acadrust_source`. + pub acadrust_declared: bool, pub ribbon_order: i32, pub command_prefixes: Vec, /// 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 { let mut description = String::new(); let mut repository = None; let mut api_version: u32 = 0; + let mut acadrust_source: Option = None; + let mut acadrust_declared = false; let mut ribbon_order: i32 = 0; let mut command_prefixes: Vec = 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 { "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 { 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. diff --git a/src/plugin/marketplace.rs b/src/plugin/marketplace.rs index 797580c4..f3f6c5f3 100644 --- a/src/plugin/marketplace.rs +++ b/src/plugin/marketplace.rs @@ -219,9 +219,29 @@ pub fn fetch_release_info(repo: &str) -> Result, String> { let manifest_text = download_string(&manifest_asset.url)?; let manifest = external::parse_plugin_toml(&manifest_text) .ok_or_else(|| format!("release {} plugin.toml is missing an id", release.tag))?; + let acadrust_source = manifest.acadrust_source.clone(); + let acadrust_declared = manifest.acadrust_declared; + let acadrust_compatible = if !ocs_plugin_api::version_info::uses_acadrust_gate( + manifest.api_version, + ) { + true + } else if !acadrust_declared { + true + } else { + match 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(), + ), + } + }; Ok::<_, String>(ReleaseInfo { tag: release.tag, api_version: manifest.api_version, + acadrust_source, + acadrust_declared, + acadrust_compatible, }) })(); match result { @@ -229,6 +249,19 @@ pub fn fetch_release_info(repo: &str) -> Result, String> { Err(error) => last_error = Some(error), } } + + // Once a repo declares a fingerprint, require it on later API versions. + let any_declared = info.iter().any(|r| r.acadrust_declared); + if any_declared { + for r in &mut info { + if !r.acadrust_declared + && ocs_plugin_api::version_info::uses_acadrust_gate(r.api_version) + { + r.acadrust_compatible = false; + } + } + } + if info.is_empty() { Err(last_error.unwrap_or_else(|| "no installable releases found".to_string())) } else { @@ -283,8 +316,8 @@ fn download_bytes(url: &str) -> Result, String> { } /// Download and install a release's package into the plugins folder. Verifies -/// the API version from the package's `plugin.toml` first. Returns the plugin -/// id on success. +/// the API version and, when present, the `acadrust` fingerprint from the +/// package's `plugin.toml` first. Returns the plugin id on success. pub fn install(release: &Release, repository: &str) -> Result { let lib = release.lib_asset().ok_or("no library for this platform")?; let toml = release.toml_asset().ok_or("release has no plugin.toml")?; @@ -300,6 +333,34 @@ pub fn install(release: &Release, repository: &str) -> Result { )); } + if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version) + && manifest.acadrust_declared + { + let Some(source) = manifest.acadrust_source.as_deref() else { + return Err( + "Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(), + ); + }; + if source.is_empty() { + return Err( + "Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(), + ); + } + if !ocs_plugin_api::version_info::acadrust_sources_compatible( + source, + ocs_plugin_api::version_info::host_acadrust_source(), + ) { + let host_src = ocs_plugin_api::version_info::host_acadrust_source(); + let plugin_hash = ocs_plugin_api::version_info::acadrust_source_hash(source) + .unwrap_or("unknown"); + let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src) + .unwrap_or("unknown"); + return Err(format!( + "Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}" + )); + } + } + let dir = external::plugins_dir() .ok_or("cannot locate the plugins folder")? .join(&manifest.id); diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs index 6a212cc2..1b11c735 100644 --- a/src/ui/window/plugin_manager.rs +++ b/src/ui/window/plugin_manager.rs @@ -196,17 +196,22 @@ fn trim_version_prefix(value: &str) -> &str { value.trim_start_matches(|c| c == 'v' || c == 'V') } -fn newest_update(installed: &str, tags: &[String]) -> Option { +fn newest_update(installed: &str, releases: &[ReleaseInfo]) -> Option { let installed = semver::Version::parse(trim_version_prefix(installed)).ok()?; - tags.iter() - .filter_map(|tag| { - semver::Version::parse(trim_version_prefix(tag)) + releases + .iter() + .filter(|release| { + ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version) + && release.acadrust_compatible + }) + .filter_map(|release| { + semver::Version::parse(trim_version_prefix(&release.tag)) .ok() - .map(|version| (version, tag)) + .map(|version| (version, release.tag.clone())) }) .filter(|(version, _)| version > &installed) .max_by(|(left, _), (right, _)| left.cmp(right)) - .map(|(_, tag)| tag.clone()) + .map(|(_, tag)| tag) } fn external_card<'a>( @@ -219,12 +224,15 @@ fn external_card<'a>( selected: bool, ) -> Element<'a, Message> { let failed_old_api = load_error.is_some() && !p.api_compatible(); + let acadrust_mismatch = p.acadrust_declared && !p.acadrust_compatible(); let (status, kind) = if loaded && disabled { (t!("Disabled"), StatusKind::Muted) } else if loaded { (t!("Loaded"), StatusKind::Success) } else if !p.api_compatible() || failed_old_api { (t!("API incompatible"), StatusKind::Danger) + } else if acadrust_mismatch { + (t!("Incompatible"), StatusKind::Danger) } else if load_error.is_some() { (t!("Load failed"), StatusKind::Danger) } else if !p.lib_present { @@ -352,11 +360,8 @@ fn install_controls<'a>( .iter() .map(|release| release.tag.clone()) .collect::>(); - let selected_api = selected.as_ref().and_then(|selected| { - releases - .iter() - .find(|release| release.tag == *selected) - .map(|release| release.api_version) + let selected_release = selected.as_ref().and_then(|selected| { + releases.iter().find(|release| release.tag == *selected) }); let picker: Element<'_, Message> = if tags.is_empty() { text(t!("no releases")).size(11).style(muted_style).into() @@ -367,9 +372,10 @@ fn install_controls<'a>( .text_size(12) .into() }; - let action = match selected_api { - Some(api_version) - if ocs_plugin_api::manifest::host_accepts_plugin_version(api_version) => + let action = match selected_release { + Some(release) + if ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version) + && release.acadrust_compatible => { pill_button( t!("Install"), @@ -842,18 +848,7 @@ pub fn view_window<'a>( let update_tag = repository .as_ref() .and_then(|repo| market.release_tags.get(repo)) - .and_then(|releases| { - let compatible_tags = releases - .iter() - .filter(|release| { - ocs_plugin_api::manifest::host_accepts_plugin_version( - release.api_version, - ) - }) - .map(|release| release.tag.clone()) - .collect::>(); - newest_update(&p.version, &compatible_tags) - }); + .and_then(|releases| newest_update(&p.version, releases)); list = list.push(external_card( p, repository, @@ -984,16 +979,60 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> { #[cfg(test)] mod tests { use super::{newest_update, registry_error_message, repository_display_name}; + use crate::plugin::external::ReleaseInfo; #[test] fn newest_update_uses_semver_not_release_order() { - let tags = vec![ - "v1.4.0".to_string(), - "v2.0.0".to_string(), - "v1.9.9".to_string(), + let releases = vec![ + ReleaseInfo { + tag: "v1.4.0".to_string(), + api_version: 4, + acadrust_source: None, + acadrust_declared: false, + acadrust_compatible: true, + }, + ReleaseInfo { + tag: "v2.0.0".to_string(), + api_version: 4, + acadrust_source: None, + acadrust_declared: false, + acadrust_compatible: true, + }, + ReleaseInfo { + tag: "v1.9.9".to_string(), + api_version: 4, + acadrust_source: None, + acadrust_declared: false, + acadrust_compatible: true, + }, ]; - assert_eq!(newest_update("1.3.0", &tags).as_deref(), Some("v2.0.0")); - assert_eq!(newest_update("2.0.0", &tags), None); + assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v2.0.0")); + assert_eq!(newest_update("2.0.0", &releases), None); + } + + #[test] + fn newest_update_ignores_incompatible_releases() { + let releases = vec![ + ReleaseInfo { + tag: "v1.4.0".to_string(), + api_version: 4, + acadrust_source: Some( + "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(), + ), + acadrust_declared: true, + acadrust_compatible: true, + }, + ReleaseInfo { + tag: "v2.0.0".to_string(), + api_version: 4, + acadrust_source: Some( + "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(), + ), + acadrust_declared: true, + acadrust_compatible: false, + }, + ]; + assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v1.4.0")); } #[test]