use semver::Version;
use serde::Deserialize;
use crate::{Diagnostic, DiagnosticCode, PodmanLensResult};
const CATALOGUE_JSON: &str = include_str!("../catalogue/v1/podman-capabilities.json");
const LEGACY_INPUT_ANCHORS: [(&str, &str, &str, &str, &str, &str); 5] = [
(
"3.0.1",
"3.0.2",
"3.0.0",
"3.0.0",
"c640670e85c4aaaff92741691d6a854a90229d8d",
"version/version.go",
),
(
"3.4.4",
"3.4.5",
"3.1.0",
"3.4.4",
"f6526ada1025c2e3f88745ba83b8b461ca659933",
"version/version.go",
),
(
"4.3.1",
"4.3.2",
"4.0.0",
"4.3.1",
"814b7b003cc630bf6ab188274706c383f9fb9915",
"version/version.go",
),
(
"4.9.3",
"4.9.4",
"4.0.0",
"4.9.3",
"8d2b55ddde1bc81f43d018dfc1ac027c06b26a7f",
"version/rawversion/rawversion.go",
),
(
"4.9.4",
"4.9.5",
"4.0.0",
"4.9.4",
"3aceae8ace3c7e3c5591900db32d188cf60be535",
"version/rawversion/rawversion.go",
),
];
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EvidenceReference {
source: String,
revision: String,
release_tag: String,
}
impl EvidenceReference {
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
#[must_use]
pub fn revision(&self) -> &str {
&self.revision
}
#[must_use]
pub fn release_tag(&self) -> &str {
&self.release_tag
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct CapabilityCatalogueEntry {
podman_minor_line: String,
minimum_podman_version: String,
maximum_exclusive_podman_version: String,
minimum_libpod_api_version: String,
observed_podman_version: String,
observed_libpod_api_version: String,
#[serde(default = "default_output_supported")]
output_supported: bool,
evidence: EvidenceReference,
}
impl CapabilityCatalogueEntry {
#[must_use]
pub fn podman_minor_line(&self) -> &str {
&self.podman_minor_line
}
#[must_use]
pub fn minimum_podman_version(&self) -> &str {
&self.minimum_podman_version
}
#[must_use]
pub fn maximum_exclusive_podman_version(&self) -> &str {
&self.maximum_exclusive_podman_version
}
#[must_use]
pub fn minimum_libpod_api_version(&self) -> &str {
&self.minimum_libpod_api_version
}
#[must_use]
pub fn observed_podman_version(&self) -> &str {
&self.observed_podman_version
}
#[must_use]
pub fn observed_libpod_api_version(&self) -> &str {
&self.observed_libpod_api_version
}
#[must_use]
pub const fn output_supported(&self) -> bool {
self.output_supported
}
#[must_use]
pub fn evidence(&self) -> &EvidenceReference {
&self.evidence
}
}
const fn default_output_supported() -> bool {
true
}
#[derive(Deserialize)]
struct Catalogue {
schema_version: u8,
reviewed_lines: Vec<CapabilityCatalogueEntry>,
}
pub fn capability_catalogue() -> PodmanLensResult<Vec<CapabilityCatalogueEntry>> {
parse_catalogue(CATALOGUE_JSON)
}
fn parse_catalogue(source: &str) -> PodmanLensResult<Vec<CapabilityCatalogueEntry>> {
let catalogue: Catalogue =
serde_json::from_str(source).map_err(|_| Diagnostic::new(DiagnosticCode::CatalogueUnavailable))?;
if catalogue.schema_version != 1 || !valid_catalogue(&catalogue.reviewed_lines) {
return Err(Diagnostic::new(DiagnosticCode::CatalogueUnavailable));
}
Ok(catalogue.reviewed_lines)
}
fn valid_catalogue(entries: &[CapabilityCatalogueEntry]) -> bool {
if !valid_legacy_catalogue(entries) {
return false;
}
valid_output_catalogue(entries)
}
fn valid_legacy_catalogue(entries: &[CapabilityCatalogueEntry]) -> bool {
let legacy = entries
.iter()
.filter(|entry| !entry.output_supported)
.collect::<Vec<_>>();
if legacy.len() != LEGACY_INPUT_ANCHORS.len() {
return false;
}
for (entry, (minimum, maximum, minimum_api, expected_api, revision, source_path)) in
legacy.into_iter().zip(LEGACY_INPUT_ANCHORS)
{
let Some((parsed_minimum, parsed_maximum, _parsed_minimum_api, observed_engine, observed_api)) =
parsed_versions(entry)
else {
return false;
};
let Ok(expected_observed_api) = Version::parse(expected_api) else {
return false;
};
let expected_source = format!("https://github.com/containers/podman/blob/{revision}/{source_path}");
if entry.minimum_podman_version != minimum
|| entry.maximum_exclusive_podman_version != maximum
|| entry.minimum_libpod_api_version != minimum_api
|| entry.observed_podman_version != minimum
|| entry.observed_libpod_api_version != expected_api
|| entry.evidence.revision != revision
|| entry.evidence.source != expected_source
|| parsed_minimum != observed_engine
|| observed_api != expected_observed_api
|| parsed_maximum <= parsed_minimum
|| entry.podman_minor_line != format!("{}.{}", parsed_minimum.major, parsed_minimum.minor)
|| entry.evidence.release_tag != format!("v{observed_engine}")
|| !is_lowercase_sha40(&entry.evidence.revision)
{
return false;
}
}
true
}
fn valid_output_catalogue(entries: &[CapabilityCatalogueEntry]) -> bool {
let mut expected_minimum = Version::new(5, 4, 0);
for entry in entries.iter().filter(|entry| entry.output_supported) {
let Some((minimum, maximum_exclusive, minimum_api, observed_engine, observed_api)) = parsed_versions(entry)
else {
return false;
};
let expected_label = format!("{}.{}", minimum.major, minimum.minor);
let expected_source = format!(
"https://github.com/containers/podman/blob/{}/version/rawversion/rawversion.go",
entry.evidence.revision
);
let expected_tag = format!("v{observed_engine}");
if minimum != expected_minimum
|| maximum_exclusive <= minimum
|| entry.podman_minor_line != expected_label
|| minimum_api != Version::new(4, 0, 0)
|| observed_engine < minimum
|| observed_engine >= maximum_exclusive
|| observed_api < minimum_api
|| observed_api > observed_engine
|| observed_engine != observed_api
|| entry.evidence.release_tag != expected_tag
|| entry.evidence.source != expected_source
|| !is_lowercase_sha40(&entry.evidence.revision)
{
return false;
}
expected_minimum = maximum_exclusive;
}
expected_minimum == Version::new(6, 2, 0)
}
fn parsed_versions(entry: &CapabilityCatalogueEntry) -> Option<(Version, Version, Version, Version, Version)> {
let minimum = Version::parse(&entry.minimum_podman_version).ok()?;
let maximum_exclusive = Version::parse(&entry.maximum_exclusive_podman_version).ok()?;
let minimum_api = Version::parse(&entry.minimum_libpod_api_version).ok()?;
let observed_engine = Version::parse(&entry.observed_podman_version).ok()?;
let observed_api = Version::parse(&entry.observed_libpod_api_version).ok()?;
[
&minimum,
&maximum_exclusive,
&minimum_api,
&observed_engine,
&observed_api,
]
.iter()
.all(|version| version.pre.is_empty() && version.build.is_empty())
.then_some((minimum, maximum_exclusive, minimum_api, observed_engine, observed_api))
}
fn is_lowercase_sha40(value: &str) -> bool {
value.len() == 40
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
}
#[cfg(test)]
mod tests {
use super::{CATALOGUE_JSON, Catalogue, parse_catalogue, valid_catalogue};
fn decoded_catalogue() -> Result<Catalogue, Box<dyn std::error::Error>> {
Ok(serde_json::from_str(CATALOGUE_JSON)?)
}
#[test]
fn embedded_catalogue_obeys_the_complete_policy() -> Result<(), Box<dyn std::error::Error>> {
let catalogue = decoded_catalogue()?;
assert!(valid_catalogue(&catalogue.reviewed_lines));
Ok(())
}
#[test]
fn malformed_catalogue_policy_is_rejected() {
let cases = [
(
"\"maximum_exclusive_podman_version\": \"5.5.0\"",
"\"maximum_exclusive_podman_version\": \"5.4.9\"",
),
(
"\"minimum_podman_version\": \"5.5.0\"",
"\"minimum_podman_version\": \"5.4.0\"",
),
(
"f9f7d48b24b1ca4403f189caaeab1cb8ff4a9aa2",
"F9f7d48b24b1ca4403f189caaeab1cb8ff4a9aa2",
),
("\"release_tag\": \"v5.4.0\"", "\"release_tag\": \"v5.4.1\""),
(
"\"observed_libpod_api_version\": \"5.4.0\"",
"\"observed_libpod_api_version\": \"3.9.9\"",
),
];
for (from, to) in cases {
assert!(parse_catalogue(&CATALOGUE_JSON.replacen(from, to, 1)).is_err());
}
}
#[test]
fn reordered_catalogue_policy_is_rejected() -> Result<(), Box<dyn std::error::Error>> {
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines.swap(0, 1);
assert!(!valid_catalogue(&catalogue.reviewed_lines));
Ok(())
}
#[test]
fn individual_evidence_policy_violations_are_rejected() -> Result<(), Box<dyn std::error::Error>> {
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines[5].evidence.revision.replace_range(0..1, "F");
assert!(!valid_catalogue(&catalogue.reviewed_lines));
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines[5].evidence.source = "https://example.invalid/mutable".to_owned();
assert!(!valid_catalogue(&catalogue.reviewed_lines));
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines[5].observed_podman_version = "5.4.1".to_owned();
assert!(!valid_catalogue(&catalogue.reviewed_lines));
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines[0].evidence.source = "https://example.invalid/mutable".to_owned();
assert!(!valid_catalogue(&catalogue.reviewed_lines));
let mut catalogue = decoded_catalogue()?;
catalogue.reviewed_lines[1].observed_libpod_api_version = "3.1.0".to_owned();
assert!(!valid_catalogue(&catalogue.reviewed_lines));
Ok(())
}
}