use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{BvError, Result};
pub type BinaryIndex = BTreeMap<String, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferenceDataPin {
pub id: String,
pub version: String,
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LockfileEntry {
pub tool_id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub declared_version_req: String,
pub version: String,
pub image_reference: String,
pub image_digest: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub manifest_sha256: String,
pub image_size_bytes: Option<u64>,
pub resolved_at: DateTime<Utc>,
#[serde(default)]
pub reference_data_pins: BTreeMap<String, ReferenceDataPin>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub binaries: Vec<String>,
}
impl LockfileEntry {
pub fn is_equivalent(&self, other: &Self) -> bool {
self.tool_id == other.tool_id
&& self.version == other.version
&& self.image_digest == other.image_digest
&& (self.manifest_sha256.is_empty()
|| other.manifest_sha256.is_empty()
|| self.manifest_sha256 == other.manifest_sha256)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockfileMetadata {
pub bv_version: String,
pub generated_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hardware_summary: Option<String>,
}
impl Default for LockfileMetadata {
fn default() -> Self {
Self {
bv_version: env!("CARGO_PKG_VERSION").to_string(),
generated_at: Utc::now(),
hardware_summary: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Lockfile {
pub version: u32,
#[serde(default)]
pub metadata: LockfileMetadata,
#[serde(default)]
pub tools: BTreeMap<String, LockfileEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub binary_index: BinaryIndex,
}
impl Lockfile {
pub fn new() -> Self {
Self {
version: 1,
metadata: LockfileMetadata::default(),
tools: BTreeMap::new(),
binary_index: BTreeMap::new(),
}
}
pub fn from_toml_str(s: &str) -> Result<Self> {
toml::from_str(s).map_err(|e| BvError::LockfileParse(e.to_string()))
}
pub fn to_toml_string(&self) -> Result<String> {
toml::to_string_pretty(self).map_err(|e| BvError::LockfileParse(e.to_string()))
}
pub fn rebuild_binary_index(
&mut self,
overrides: &BTreeMap<String, String>,
) -> std::result::Result<(), String> {
let mut index: BinaryIndex = BTreeMap::new();
let mut collisions: Vec<String> = Vec::new();
let mut sorted: Vec<_> = self.tools.iter().collect();
sorted.sort_by_key(|(id, _)| id.as_str());
for (tool_id, entry) in &sorted {
for binary in &entry.binaries {
if let Some(winner) = overrides.get(binary) {
index.insert(binary.clone(), winner.clone());
} else if let Some(existing) = index.insert(binary.clone(), tool_id.to_string())
&& existing != tool_id.as_str()
{
collisions.push(format!(
"'{binary}' exposed by both '{existing}' and '{tool_id}'"
));
index.insert(binary.clone(), existing);
}
}
}
if !collisions.is_empty() {
return Err(collisions.join(", "));
}
self.binary_index = index;
Ok(())
}
pub fn is_equivalent_to(&self, other: &Self) -> bool {
if self.tools.len() != other.tools.len() {
return false;
}
for (id, entry) in &self.tools {
match other.tools.get(id) {
Some(other_entry) => {
if !entry.is_equivalent(other_entry) {
return false;
}
}
None => return false,
}
}
true
}
}
impl Default for Lockfile {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: &str, version: &str, digest: &str) -> LockfileEntry {
LockfileEntry {
tool_id: id.to_string(),
declared_version_req: String::new(),
version: version.to_string(),
image_reference: format!("registry/{id}:{version}"),
image_digest: digest.to_string(),
manifest_sha256: format!("sha256:m-{id}"),
image_size_bytes: None,
resolved_at: chrono::DateTime::<chrono::Utc>::from_timestamp(1700000000, 0).unwrap(),
reference_data_pins: BTreeMap::new(),
binaries: vec![format!("{id}-bin")],
}
}
#[test]
fn to_toml_string_is_deterministic() {
let mut lock = Lockfile::new();
for id in ["zebra", "alpha", "mango", "beta", "tango"] {
lock.tools.insert(
id.to_string(),
entry(id, "1.0.0", &format!("sha256:d-{id}")),
);
lock.binary_index
.insert(format!("{id}-bin"), id.to_string());
}
let s1 = lock.to_toml_string().unwrap();
for _ in 0..32 {
assert_eq!(s1, lock.to_toml_string().unwrap(), "non-deterministic output");
}
let alpha = s1.find("\"alpha\"").unwrap();
let beta = s1.find("\"beta\"").unwrap();
let mango = s1.find("\"mango\"").unwrap();
let tango = s1.find("\"tango\"").unwrap();
let zebra = s1.find("\"zebra\"").unwrap();
assert!(alpha < beta && beta < mango && mango < tango && tango < zebra);
}
}