Skip to main content

arcbox_boot/
upstream.rs

1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4
5/// Root structure of `upstream.toml`.
6#[derive(Debug, Clone, Deserialize)]
7pub struct UpstreamConfig {
8    pub binaries: Vec<UpstreamBinary>,
9}
10
11/// A single upstream binary declaration.
12#[derive(Debug, Clone, Deserialize)]
13pub struct UpstreamBinary {
14    pub name: String,
15    pub version: String,
16    /// Per-architecture source definitions.
17    pub source: BTreeMap<String, UpstreamSource>,
18    /// Subdirectory under the VirtioFS share root (e.g. "kernel" → /arcbox/kernel/).
19    /// Defaults to "bin" when absent.
20    pub install_dir: Option<String>,
21}
22
23/// Where to download a binary for a specific architecture.
24#[derive(Debug, Clone, Deserialize)]
25pub struct UpstreamSource {
26    /// Source format. Defaults to `tgz` for backward compatibility.
27    #[serde(default)]
28    pub format: UpstreamSourceFormat,
29    /// Download URL (typically a .tar.gz or .tgz).
30    pub url: String,
31    /// Path inside the archive to extract (e.g. "docker/dockerd").
32    ///
33    /// Required for `tgz`, omitted for `binary`.
34    #[serde(default)]
35    pub extract: Option<String>,
36}
37
38/// Supported upstream artifact formats.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum UpstreamSourceFormat {
42    /// A `.tar.gz` / `.tgz` archive where a single file is extracted.
43    #[default]
44    Tgz,
45    /// A direct binary download.
46    Binary,
47}
48
49impl UpstreamConfig {
50    pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
51        let content = std::fs::read_to_string(path)
52            .map_err(|e| format!("failed to read {}: {e}", path.display()))?;
53        let config: Self = toml::from_str(&content)
54            .map_err(|e| format!("failed to parse {}: {e}", path.display()))?;
55        config.validate(path)?;
56        Ok(config)
57    }
58}
59
60impl UpstreamConfig {
61    fn validate(&self, path: &std::path::Path) -> Result<(), String> {
62        for binary in &self.binaries {
63            for (arch, source) in &binary.source {
64                match source.format {
65                    UpstreamSourceFormat::Tgz => {
66                        if source.extract.as_deref().is_none_or(str::is_empty) {
67                            return Err(format!(
68                                "invalid {}: binary '{}' arch '{}' requires 'extract' for format=tgz",
69                                path.display(),
70                                binary.name,
71                                arch
72                            ));
73                        }
74                    }
75                    UpstreamSourceFormat::Binary => {
76                        if source
77                            .extract
78                            .as_deref()
79                            .is_some_and(|value| !value.is_empty())
80                        {
81                            return Err(format!(
82                                "invalid {}: binary '{}' arch '{}' must not set 'extract' for format=binary",
83                                path.display(),
84                                binary.name,
85                                arch
86                            ));
87                        }
88                    }
89                }
90            }
91        }
92
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::{UpstreamConfig, UpstreamSourceFormat};
100
101    #[test]
102    fn parse_defaults_to_tgz_format() {
103        let config: UpstreamConfig = toml::from_str(
104            r#"
105[[binaries]]
106name = "dockerd"
107version = "27.5.1"
108
109[binaries.source.arm64]
110url = "https://example.invalid/docker.tgz"
111extract = "docker/dockerd"
112"#,
113        )
114        .unwrap();
115
116        let source = &config.binaries[0].source["arm64"];
117        assert_eq!(source.format, UpstreamSourceFormat::Tgz);
118        assert_eq!(source.extract.as_deref(), Some("docker/dockerd"));
119    }
120
121    #[test]
122    fn parse_binary_source_without_extract() {
123        let config: UpstreamConfig = toml::from_str(
124            r#"
125[[binaries]]
126name = "k3s"
127version = "v1.34.3+k3s1"
128
129[binaries.source.arm64]
130format = "binary"
131url = "https://example.invalid/k3s-arm64"
132"#,
133        )
134        .unwrap();
135
136        let source = &config.binaries[0].source["arm64"];
137        assert_eq!(source.format, UpstreamSourceFormat::Binary);
138        assert_eq!(source.extract, None);
139    }
140}