use crate::v2::{Error, Result};
use semver::Version;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct LockedComponent {
pub name: String,
pub version: Version,
pub sha256: String,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Lockfile {
pub version: u32,
components: HashMap<String, LockedComponent>,
pub checksum: Option<String>,
}
impl Lockfile {
pub fn new() -> Self {
Self {
version: 1,
components: HashMap::new(),
checksum: None,
}
}
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::parse(&content)
}
pub fn parse(content: &str) -> Result<Self> {
let mut lockfile = Self::new();
let mut current_component: Option<LockedComponent> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("[[component]]") {
if let Some(comp) = current_component.take() {
lockfile.components.insert(comp.name.clone(), comp);
}
current_component = Some(LockedComponent {
name: String::new(),
version: Version::new(0, 0, 0),
sha256: String::new(),
dependencies: Vec::new(),
});
} else if let Some(ref mut comp) = current_component {
if let Some(name) = line.strip_prefix("name = ") {
comp.name = name.trim_matches('"').to_string();
} else if let Some(version) = line.strip_prefix("version = ") {
comp.version = Version::parse(version.trim_matches('"'))
.map_err(|e| Error::other(format!("Invalid version: {}", e)))?;
} else if let Some(hash) = line.strip_prefix("sha256 = ") {
comp.sha256 = hash.trim_matches('"').to_string();
} else if let Some(deps) = line.strip_prefix("dependencies = ") {
let deps = deps.trim_start_matches('[').trim_end_matches(']');
comp.dependencies = deps
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect();
}
} else {
if let Some(version_str) = line.strip_prefix("version = ") {
lockfile.version = version_str.parse().unwrap_or(1);
} else if let Some(checksum_str) = line.strip_prefix("checksum = ") {
lockfile.checksum = Some(checksum_str.trim_matches('"').to_string());
}
}
}
if let Some(comp) = current_component {
if !comp.name.is_empty() {
lockfile.components.insert(comp.name.clone(), comp);
}
}
Ok(lockfile)
}
pub fn save(&self, path: &Path) -> Result<()> {
let content = self.serialize();
std::fs::write(path, content)?;
Ok(())
}
pub fn serialize(&self) -> String {
let mut output = String::new();
output.push_str("# This file is auto-generated by Run. Do not edit manually.\n");
output.push_str(&format!("version = {}\n\n", self.version));
let mut sorted: Vec<_> = self.components.values().collect();
sorted.sort_by(|a, b| a.name.cmp(&b.name));
for comp in sorted {
output.push_str("[[component]]\n");
output.push_str(&format!("name = \"{}\"\n", comp.name));
output.push_str(&format!("version = \"{}\"\n", comp.version));
output.push_str(&format!("sha256 = \"{}\"\n", comp.sha256));
if !comp.dependencies.is_empty() {
let deps: Vec<String> = comp
.dependencies
.iter()
.map(|d| format!("\"{}\"", d))
.collect();
output.push_str(&format!("dependencies = [{}]\n", deps.join(", ")));
}
output.push('\n');
}
let checksum = compute_checksum(&output);
output.push_str(&format!("checksum = \"{}\"\n", checksum));
output
}
pub fn add(&mut self, component: LockedComponent) {
self.components.insert(component.name.clone(), component);
}
pub fn remove(&mut self, name: &str) -> Option<LockedComponent> {
self.components.remove(name)
}
pub fn get(&self, name: &str) -> Option<&LockedComponent> {
self.components.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.components.contains_key(name)
}
pub fn components(&self) -> impl Iterator<Item = &LockedComponent> {
self.components.values()
}
pub fn len(&self) -> usize {
self.components.len()
}
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn verify(&self) -> bool {
let content = self.serialize_without_checksum();
let expected = compute_checksum(&content);
self.checksum
.as_ref()
.map(|c| c == &expected)
.unwrap_or(true)
}
fn serialize_without_checksum(&self) -> String {
let mut output = String::new();
output.push_str(&format!("version = {}\n\n", self.version));
let mut sorted: Vec<_> = self.components.values().collect();
sorted.sort_by(|a, b| a.name.cmp(&b.name));
for comp in sorted {
output.push_str("[[component]]\n");
output.push_str(&format!("name = \"{}\"\n", comp.name));
output.push_str(&format!("version = \"{}\"\n", comp.version));
output.push_str(&format!("sha256 = \"{}\"\n", comp.sha256));
if !comp.dependencies.is_empty() {
let deps: Vec<String> = comp
.dependencies
.iter()
.map(|d| format!("\"{}\"", d))
.collect();
output.push_str(&format!("dependencies = [{}]\n", deps.join(", ")));
}
output.push('\n');
}
output
}
pub fn diff(&self, other: &Lockfile) -> LockfileDiff {
let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();
for (name, comp) in &other.components {
match self.components.get(name) {
Some(old_comp) => {
if old_comp.version != comp.version {
changed.push((
name.clone(),
old_comp.version.clone(),
comp.version.clone(),
));
}
}
None => {
added.push((name.clone(), comp.version.clone()));
}
}
}
for name in self.components.keys() {
if !other.components.contains_key(name) {
let comp = &self.components[name];
removed.push((name.clone(), comp.version.clone()));
}
}
LockfileDiff {
added,
removed,
changed,
}
}
}
impl Default for Lockfile {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct LockfileDiff {
pub added: Vec<(String, Version)>,
pub removed: Vec<(String, Version)>,
pub changed: Vec<(String, Version, Version)>,
}
impl LockfileDiff {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
}
pub fn summary(&self) -> String {
let mut parts = Vec::new();
if !self.added.is_empty() {
parts.push(format!("{} added", self.added.len()));
}
if !self.removed.is_empty() {
parts.push(format!("{} removed", self.removed.len()));
}
if !self.changed.is_empty() {
parts.push(format!("{} changed", self.changed.len()));
}
if parts.is_empty() {
"No changes".to_string()
} else {
parts.join(", ")
}
}
}
fn compute_checksum(content: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
hex::encode(hasher.finalize())
}
pub fn compute_sha256(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lockfile_new() {
let lockfile = Lockfile::new();
assert_eq!(lockfile.version, 1);
assert!(lockfile.is_empty());
}
#[test]
fn test_lockfile_add_remove() {
let mut lockfile = Lockfile::new();
lockfile.add(LockedComponent {
name: "test".to_string(),
version: Version::new(1, 0, 0),
sha256: "abc123".to_string(),
dependencies: vec![],
});
assert!(lockfile.contains("test"));
assert_eq!(lockfile.len(), 1);
lockfile.remove("test");
assert!(!lockfile.contains("test"));
}
#[test]
fn test_lockfile_serialize_parse() {
let mut lockfile = Lockfile::new();
lockfile.add(LockedComponent {
name: "wasi:http".to_string(),
version: Version::new(0, 2, 0),
sha256: "abc123def456".to_string(),
dependencies: vec!["wasi:io".to_string()],
});
let serialized = lockfile.serialize();
let parsed = Lockfile::parse(&serialized).unwrap();
assert_eq!(parsed.len(), 1);
let comp = parsed.get("wasi:http").unwrap();
assert_eq!(comp.version, Version::new(0, 2, 0));
}
}