run-kit 0.7.1

Universal multi-language runner and smart REPL
Documentation
//! Lockfile Management
//!
//! The lockfile (run.lock) ensures deterministic installs.

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));
    }
}