1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize)]
7pub struct UpstreamConfig {
8 pub binaries: Vec<UpstreamBinary>,
9}
10
11#[derive(Debug, Clone, Deserialize)]
13pub struct UpstreamBinary {
14 pub name: String,
15 pub version: String,
16 pub source: BTreeMap<String, UpstreamSource>,
18 pub install_dir: Option<String>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
25pub struct UpstreamSource {
26 #[serde(default)]
28 pub format: UpstreamSourceFormat,
29 pub url: String,
31 #[serde(default)]
35 pub extract: Option<String>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum UpstreamSourceFormat {
42 #[default]
44 Tgz,
45 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}