feat(plugin): gate marketplace and load on acadrust source
- Add host_acadrust_source and source-hash comparison helpers to ocs_plugin_api. - Track acadrust_source and acadrust_declared in ExternalPlugin/ReleaseInfo. - Apply repo-level policy: if any release in a repo declares acadrust_source, undeclared v4+ releases from that repo are incompatible. - Preserve API v2/v3 backwards compatibility: legacy undeclared releases stay installable. - Update Plugin Manager UI to only offer compatible releases and show 'acadrust mismatch' status. - Drop acadrust_version from gate logic; only acadrust_source is required.
This commit is contained in:
parent
1fb770fb15
commit
3d35919656
4 changed files with 384 additions and 39 deletions
|
|
@ -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,60 @@ pub fn get_embedded_version_info_json() -> &'static str {
|
|||
EMBEDDED_VERSION_INFO_JSON
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct VersionInfo {
|
||||
acadrust_source: String,
|
||||
}
|
||||
|
||||
/// Extract the string value for a top-level JSON key from a compact JSON
|
||||
/// object. Only handles string values and does not unescape.
|
||||
fn json_string_value(json: &str, key: &str) -> Option<String> {
|
||||
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<VersionInfo> = OnceLock::new();
|
||||
INFO.get_or_init(|| VersionInfo {
|
||||
acadrust_source: json_string_value(EMBEDDED_VERSION_INFO_JSON, "acadrust_source")
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The full `acadrust` git source this host was built against, in Cargo source
|
||||
/// form (e.g. `git+https://github.com/...?rev=<short>#<full-sha>`).
|
||||
pub fn host_acadrust_source() -> &'static str {
|
||||
&parsed_version_info().acadrust_source
|
||||
}
|
||||
|
||||
/// Extract the canonical 40-character lowercase git commit hash from an
|
||||
/// `acadrust_source` string.
|
||||
///
|
||||
/// Cargo git sources end in `#<full-sha>` regardless of whether the `rev`
|
||||
/// query parameter was a short hash, tag, or branch. Returns `None` if no
|
||||
/// 40-character hex suffix is present.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// True when two `acadrust_source` strings refer to the same full commit.
|
||||
///
|
||||
/// Missing or unparseable sources are treated as incompatible (this helper is
|
||||
/// intended for callers that have already decided a fingerprint is present).
|
||||
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 +75,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 +110,50 @@ 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 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue