use serde::Serialize;
use crate::contract::schema::Contract;
use crate::protocol::facts::Facts;
use crate::protocol::plan::{PlanPhase, PlanTarget, ReleasePlan};
#[must_use]
pub fn build(contract: &Contract, facts: &Facts, head_sha: &str, version: &str) -> ReleasePlan {
let targets = resolve_targets(contract, facts);
let plan_id = seal(contract, &targets, head_sha, version);
ReleasePlan {
plan_id,
contract_schema_version: contract.schema_version,
head_sha: head_sha.to_string(),
version: version.to_string(),
targets,
phases: PlanPhase::SEQUENCE.to_vec(),
homebrew_tap: contract
.distribution
.as_ref()
.and_then(|d| d.homebrew_tap.clone()),
license: Some(contract.license.clone()),
}
}
#[must_use]
pub fn compute_plan_id(
contract: &Contract,
facts: &Facts,
head_sha: &str,
version: &str,
) -> String {
let targets = resolve_targets(contract, facts);
seal(contract, &targets, head_sha, version)
}
pub fn verify(
approved: &ReleasePlan,
contract: &Contract,
facts: &Facts,
head_sha: &str,
) -> Result<(), PlanDrift> {
let current_targets = resolve_targets(contract, facts);
let current_id = seal(contract, ¤t_targets, head_sha, &approved.version);
if current_id == approved.plan_id {
return Ok(());
}
let mut reasons = Vec::new();
if approved.head_sha != head_sha {
reasons.push(format!(
"HEAD moved from {} to {}",
short_sha(&approved.head_sha),
short_sha(head_sha)
));
}
if approved.contract_schema_version != contract.schema_version {
reasons.push(format!(
"contract schema_version changed from {} to {}",
approved.contract_schema_version, contract.schema_version
));
}
if approved.targets != current_targets {
reasons.push(
"the resolved target set changed (a target, package, registry, or adapter differs)"
.to_string(),
);
}
if reasons.is_empty() {
reasons.push("the normalized contract changed".to_string());
}
Err(PlanDrift {
approved_plan_id: approved.plan_id.clone(),
current_plan_id: current_id,
reasons,
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PlanDrift {
pub approved_plan_id: String,
pub current_plan_id: String,
pub reasons: Vec<String>,
}
fn resolve_targets(contract: &Contract, facts: &Facts) -> Vec<PlanTarget> {
contract
.targets
.iter()
.map(|t| {
let package = t
.package
.clone()
.or_else(|| resolve_package(facts, t.ecosystem));
PlanTarget {
ecosystem: t.ecosystem,
package,
registry: t.registry,
adapter: t.adapter,
}
})
.collect()
}
fn resolve_package(facts: &Facts, ecosystem: crate::contract::schema::Ecosystem) -> Option<String> {
let mut named = facts
.packages
.iter()
.filter(|p| p.ecosystem == ecosystem && p.package.is_some());
let first = named.next()?;
if named.next().is_some() {
return None;
}
first.package.clone()
}
const SEAL_DOMAIN: &str = "ossctl.release-plan";
const SEAL_VERSION: u32 = 2;
#[derive(Serialize)]
struct SealInput<'a> {
domain: &'static str,
seal_version: u32,
contract_schema_version: u32,
contract: &'a Contract,
head_sha: &'a str,
version: &'a str,
targets: &'a [PlanTarget],
phases: &'a [PlanPhase],
}
fn seal(contract: &Contract, targets: &[PlanTarget], head_sha: &str, version: &str) -> String {
let input = SealInput {
domain: SEAL_DOMAIN,
seal_version: SEAL_VERSION,
contract_schema_version: contract.schema_version,
contract,
head_sha,
version,
targets,
phases: &PlanPhase::SEQUENCE,
};
let bytes =
serde_json::to_vec(&input).expect("release-plan pre-image is infallible to serialize");
sha256::hex(&bytes)
}
fn short_sha(sha: &str) -> &str {
sha.get(..12).unwrap_or(sha)
}
mod sha256 {
#![allow(
clippy::unreadable_literal,
clippy::many_single_char_names,
clippy::needless_range_loop
)]
use std::fmt::Write as _;
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
const H0: [u32; 8] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
];
pub fn hex(data: &[u8]) -> String {
let mut h = H0;
let mut msg = data.to_vec();
let bit_len = (data.len() as u64)
.checked_mul(8)
.expect("SHA-256 input exceeds 2^64 bits");
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bit_len.to_be_bytes());
for chunk in msg.chunks_exact(64) {
let mut w = [0u32; 64];
for i in 0..16 {
w[i] = u32::from_be_bytes([
chunk[4 * i],
chunk[4 * i + 1],
chunk[4 * i + 2],
chunk[4 * i + 3],
]);
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16]
.wrapping_add(s0)
.wrapping_add(w[i - 7])
.wrapping_add(s1);
}
let mut a = h[0];
let mut b = h[1];
let mut c = h[2];
let mut d = h[3];
let mut e = h[4];
let mut f = h[5];
let mut g = h[6];
let mut hh = h[7];
for i in 0..64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let t1 = hh
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K[i])
.wrapping_add(w[i]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let t2 = s0.wrapping_add(maj);
hh = g;
g = f;
f = e;
e = d.wrapping_add(t1);
d = c;
c = b;
b = a;
a = t1.wrapping_add(t2);
}
h[0] = h[0].wrapping_add(a);
h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c);
h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
h[5] = h[5].wrapping_add(f);
h[6] = h[6].wrapping_add(g);
h[7] = h[7].wrapping_add(hh);
}
let mut out = String::with_capacity(64);
for v in h {
let _ = write!(out, "{v:08x}");
}
out
}
}
#[cfg(test)]
mod tests;