1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::{BvError, Result};
7
8pub type BinaryIndex = HashMap<String, String>;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ReferenceDataPin {
13 pub id: String,
14 pub version: String,
15 pub sha256: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct LockfileEntry {
25 pub tool_id: String,
26 #[serde(default, skip_serializing_if = "String::is_empty")]
28 pub declared_version_req: String,
29 pub version: String,
31 pub image_reference: String,
33 pub image_digest: String,
35 #[serde(default, skip_serializing_if = "String::is_empty")]
37 pub manifest_sha256: String,
38 pub image_size_bytes: Option<u64>,
39 pub resolved_at: DateTime<Utc>,
40 #[serde(default)]
41 pub reference_data_pins: HashMap<String, ReferenceDataPin>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub binaries: Vec<String>,
45}
46
47impl LockfileEntry {
48 pub fn is_equivalent(&self, other: &Self) -> bool {
51 self.tool_id == other.tool_id
52 && self.version == other.version
53 && self.image_digest == other.image_digest
54 && (self.manifest_sha256.is_empty()
55 || other.manifest_sha256.is_empty()
56 || self.manifest_sha256 == other.manifest_sha256)
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct LockfileMetadata {
63 pub bv_version: String,
64 pub generated_at: DateTime<Utc>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub hardware_summary: Option<String>,
67}
68
69impl Default for LockfileMetadata {
70 fn default() -> Self {
71 Self {
72 bv_version: env!("CARGO_PKG_VERSION").to_string(),
73 generated_at: Utc::now(),
74 hardware_summary: None,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Lockfile {
85 pub version: u32,
87 #[serde(default)]
88 pub metadata: LockfileMetadata,
89 #[serde(default)]
90 pub tools: HashMap<String, LockfileEntry>,
91 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
94 pub binary_index: BinaryIndex,
95}
96
97impl Lockfile {
98 pub fn new() -> Self {
99 Self {
100 version: 1,
101 metadata: LockfileMetadata::default(),
102 tools: HashMap::new(),
103 binary_index: HashMap::new(),
104 }
105 }
106
107 pub fn from_toml_str(s: &str) -> Result<Self> {
108 toml::from_str(s).map_err(|e| BvError::LockfileParse(e.to_string()))
109 }
110
111 pub fn to_toml_string(&self) -> Result<String> {
112 toml::to_string_pretty(self).map_err(|e| BvError::LockfileParse(e.to_string()))
113 }
114
115 pub fn rebuild_binary_index(
120 &mut self,
121 overrides: &HashMap<String, String>,
122 ) -> std::result::Result<(), String> {
123 let mut index: BinaryIndex = HashMap::new();
124 let mut collisions: Vec<String> = Vec::new();
125
126 let mut sorted: Vec<_> = self.tools.iter().collect();
127 sorted.sort_by_key(|(id, _)| id.as_str());
128
129 for (tool_id, entry) in &sorted {
130 for binary in &entry.binaries {
131 if let Some(winner) = overrides.get(binary) {
132 index.insert(binary.clone(), winner.clone());
133 } else if let Some(existing) = index.insert(binary.clone(), tool_id.to_string())
134 && existing != tool_id.as_str()
135 {
136 collisions.push(format!(
137 "'{binary}' exposed by both '{existing}' and '{tool_id}'"
138 ));
139 index.insert(binary.clone(), existing);
140 }
141 }
142 }
143
144 if !collisions.is_empty() {
145 return Err(collisions.join(", "));
146 }
147 self.binary_index = index;
148 Ok(())
149 }
150
151 pub fn is_equivalent_to(&self, other: &Self) -> bool {
154 if self.tools.len() != other.tools.len() {
155 return false;
156 }
157 for (id, entry) in &self.tools {
158 match other.tools.get(id) {
159 Some(other_entry) => {
160 if !entry.is_equivalent(other_entry) {
161 return false;
162 }
163 }
164 None => return false,
165 }
166 }
167 true
168 }
169}
170
171impl Default for Lockfile {
172 fn default() -> Self {
173 Self::new()
174 }
175}