Skip to main content

arcbox_docker_tools/
lockfile.rs

1//! Parser for the `[[tools]]` section of `assets.lock`.
2//!
3//! These tool entries pin both the **host-side CLI** binaries (installed to
4//! `~/.arcbox/runtime/bin/`) and the **guest-side daemon** binaries (exposed
5//! via VirtioFS to the VM). A single lockfile ensures the Docker CLI and guest
6//! dockerd are always at the same version — no version-skew concerns on upgrade.
7
8use std::collections::HashMap;
9
10use serde::Deserialize;
11
12/// Top-level lockfile structure (only the parts we care about).
13#[derive(Debug, Deserialize)]
14pub struct AssetsLock {
15    #[serde(default)]
16    pub tools: Vec<ToolEntry>,
17}
18
19/// Tool installation group.
20///
21/// New groups must be added here even if this crate does not consume them —
22/// `parse_tools()` deserializes the entire `[[tools]]` array before filtering,
23/// so an unknown group value would fail parsing for every caller. The
24/// `Desktop` variant is consumed by `arcbox-desktop` (host-only binaries
25/// embedded in the app bundle) and is inert for docker/kubernetes callers.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum ToolGroup {
29    Docker,
30    Kubernetes,
31    Desktop,
32}
33
34/// Per-architecture metadata for a tool.
35#[derive(Debug, Clone, Deserialize)]
36pub struct ArchEntry {
37    pub sha256: String,
38}
39
40/// A single `[[tools]]` entry.
41#[derive(Debug, Clone, Deserialize)]
42pub struct ToolEntry {
43    pub name: String,
44    pub group: ToolGroup,
45    pub version: String,
46    #[serde(default)]
47    pub arch: HashMap<String, ArchEntry>,
48}
49
50impl ToolEntry {
51    /// Returns the SHA-256 checksum for the given architecture, if present.
52    #[must_use]
53    pub fn sha256_for_arch(&self, arch: &str) -> Option<&str> {
54        // Accept common aliases.
55        let key = match arch {
56            "aarch64" => "arm64",
57            "amd64" => "x86_64",
58            _ => arch,
59        };
60        self.arch.get(key).map(|e| e.sha256.as_str())
61    }
62}
63
64/// Parse the `[[tools]]` entries from `assets.lock` TOML content.
65pub fn parse_tools(lock_toml: &str) -> Result<Vec<ToolEntry>, toml::de::Error> {
66    let lock: AssetsLock = toml::from_str(lock_toml)?;
67    Ok(lock.tools)
68}
69
70/// Parse the `[[tools]]` entries for a specific tool group.
71pub fn parse_tools_for_group(
72    lock_toml: &str,
73    group: ToolGroup,
74) -> Result<Vec<ToolEntry>, toml::de::Error> {
75    Ok(parse_tools(lock_toml)?
76        .into_iter()
77        .filter(|tool| tool.group == group)
78        .collect())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    const SAMPLE: &str = r#"
86[boot]
87version = "0.5.1"
88cdn = "https://boot.arcboxcdn.com"
89
90[[tools]]
91name = "docker"
92group = "docker"
93version = "29.3.1"
94arch.arm64.sha256 = "aaa"
95arch.x86_64.sha256 = "bbb"
96
97[[tools]]
98name = "docker-buildx"
99group = "docker"
100version = "0.21.1"
101arch.arm64.sha256 = "ccc"
102
103[[tools]]
104name = "kubectl"
105group = "kubernetes"
106version = "1.35.3"
107arch.arm64.sha256 = "ddd"
108
109[[tools]]
110name = "pstramp"
111group = "desktop"
112version = "0.2.0"
113arch.arm64.sha256 = "eee"
114"#;
115
116    #[test]
117    fn parse_tool_entries() {
118        let tools = parse_tools(SAMPLE).unwrap();
119        assert_eq!(tools.len(), 4);
120        assert_eq!(tools[0].name, "docker");
121        assert_eq!(tools[0].group, ToolGroup::Docker);
122        assert_eq!(tools[0].version, "29.3.1");
123        assert_eq!(tools[0].sha256_for_arch("arm64"), Some("aaa"));
124        assert_eq!(tools[0].sha256_for_arch("x86_64"), Some("bbb"));
125        assert_eq!(tools[1].sha256_for_arch("x86_64"), None);
126    }
127
128    #[test]
129    fn parse_tools_for_specific_group() {
130        let docker_tools = parse_tools_for_group(SAMPLE, ToolGroup::Docker).unwrap();
131        assert_eq!(docker_tools.len(), 2);
132        assert!(
133            docker_tools
134                .iter()
135                .all(|tool| tool.group == ToolGroup::Docker)
136        );
137
138        let kubernetes_tools = parse_tools_for_group(SAMPLE, ToolGroup::Kubernetes).unwrap();
139        assert_eq!(kubernetes_tools.len(), 1);
140        assert_eq!(kubernetes_tools[0].name, "kubectl");
141
142        // Desktop group should parse cleanly even though this crate does not
143        // consume it — docker/kubernetes flows must coexist with desktop tools.
144        let desktop_tools = parse_tools_for_group(SAMPLE, ToolGroup::Desktop).unwrap();
145        assert_eq!(desktop_tools.len(), 1);
146        assert_eq!(desktop_tools[0].name, "pstramp");
147    }
148
149    #[test]
150    fn arch_aliases() {
151        let tools = parse_tools(SAMPLE).unwrap();
152        // "aarch64" should resolve to "arm64"
153        assert_eq!(tools[0].sha256_for_arch("aarch64"), Some("aaa"));
154        // "amd64" should resolve to "x86_64"
155        assert_eq!(tools[0].sha256_for_arch("amd64"), Some("bbb"));
156    }
157}