Skip to main content

a3s_box_core/
platform.rs

1//! Platform types for multi-architecture image builds.
2//!
3//! Represents target OS/architecture pairs used by Buildx-style
4//! multi-platform builds and OCI Image Index manifests.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// A target platform (OS + architecture).
10///
11/// Used for multi-platform builds and OCI Image Index entries.
12/// Compatible with Docker/OCI platform specification.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct Platform {
15    /// Operating system (e.g., "linux").
16    pub os: String,
17    /// CPU architecture (e.g., "amd64", "arm64").
18    pub architecture: String,
19    /// Optional variant (e.g., "v7" for armv7).
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub variant: Option<String>,
22}
23
24impl Platform {
25    /// Create a new platform.
26    pub fn new(os: impl Into<String>, architecture: impl Into<String>) -> Self {
27        Self {
28            os: os.into(),
29            architecture: architecture.into(),
30            variant: None,
31        }
32    }
33
34    /// Create a platform with a variant.
35    pub fn with_variant(
36        os: impl Into<String>,
37        architecture: impl Into<String>,
38        variant: impl Into<String>,
39    ) -> Self {
40        Self {
41            os: os.into(),
42            architecture: architecture.into(),
43            variant: Some(variant.into()),
44        }
45    }
46
47    /// linux/amd64
48    pub fn linux_amd64() -> Self {
49        Self::new("linux", "amd64")
50    }
51
52    /// linux/arm64
53    pub fn linux_arm64() -> Self {
54        Self::new("linux", "arm64")
55    }
56
57    /// Detect the current host platform.
58    pub fn host() -> Self {
59        let arch = match std::env::consts::ARCH {
60            "x86_64" => "amd64",
61            "aarch64" => "arm64",
62            other => other,
63        };
64        Self::new("linux", arch)
65    }
66
67    /// Parse a platform string like "linux/amd64" or "linux/arm/v7".
68    pub fn parse(s: &str) -> Result<Self, String> {
69        let parts: Vec<&str> = s.split('/').collect();
70        match parts.len() {
71            2 => {
72                let arch = normalize_arch(parts[1]);
73                Ok(Self::new(parts[0], arch))
74            }
75            3 => {
76                let arch = normalize_arch(parts[1]);
77                Ok(Self::with_variant(parts[0], arch, parts[2]))
78            }
79            _ => Err(format!(
80                "Invalid platform '{}': expected 'os/arch' or 'os/arch/variant'",
81                s
82            )),
83        }
84    }
85
86    /// Parse a comma-separated list of platforms.
87    ///
88    /// Example: "linux/amd64,linux/arm64"
89    pub fn parse_list(s: &str) -> Result<Vec<Self>, String> {
90        s.split(',').map(|p| Self::parse(p.trim())).collect()
91    }
92
93    /// Check if this platform matches the host architecture.
94    pub fn is_native(&self) -> bool {
95        *self == Self::host()
96    }
97
98    /// Get the OCI architecture string.
99    pub fn oci_arch(&self) -> &str {
100        &self.architecture
101    }
102}
103
104impl fmt::Display for Platform {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(f, "{}/{}", self.os, self.architecture)?;
107        if let Some(ref v) = self.variant {
108            write!(f, "/{}", v)?;
109        }
110        Ok(())
111    }
112}
113
114/// Normalize architecture names to OCI conventions.
115fn normalize_arch(arch: &str) -> String {
116    match arch {
117        "x86_64" | "x86-64" => "amd64".to_string(),
118        "aarch64" | "arm64v8" => "arm64".to_string(),
119        "armhf" | "armv7l" => "arm".to_string(),
120        "i386" | "i686" | "x86" => "386".to_string(),
121        other => other.to_string(),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_platform_new() {
131        let p = Platform::new("linux", "amd64");
132        assert_eq!(p.os, "linux");
133        assert_eq!(p.architecture, "amd64");
134        assert!(p.variant.is_none());
135    }
136
137    #[test]
138    fn test_platform_with_variant() {
139        let p = Platform::with_variant("linux", "arm", "v7");
140        assert_eq!(p.variant, Some("v7".to_string()));
141    }
142
143    #[test]
144    fn test_platform_display() {
145        assert_eq!(Platform::linux_amd64().to_string(), "linux/amd64");
146        assert_eq!(Platform::linux_arm64().to_string(), "linux/arm64");
147        assert_eq!(
148            Platform::with_variant("linux", "arm", "v7").to_string(),
149            "linux/arm/v7"
150        );
151    }
152
153    #[test]
154    fn test_platform_parse() {
155        let p = Platform::parse("linux/amd64").unwrap();
156        assert_eq!(p, Platform::linux_amd64());
157
158        let p = Platform::parse("linux/arm64").unwrap();
159        assert_eq!(p, Platform::linux_arm64());
160
161        let p = Platform::parse("linux/arm/v7").unwrap();
162        assert_eq!(p.architecture, "arm");
163        assert_eq!(p.variant, Some("v7".to_string()));
164    }
165
166    #[test]
167    fn test_platform_parse_normalizes() {
168        let p = Platform::parse("linux/x86_64").unwrap();
169        assert_eq!(p.architecture, "amd64");
170
171        let p = Platform::parse("linux/aarch64").unwrap();
172        assert_eq!(p.architecture, "arm64");
173    }
174
175    #[test]
176    fn test_platform_parse_invalid() {
177        assert!(Platform::parse("linux").is_err());
178        assert!(Platform::parse("a/b/c/d").is_err());
179    }
180
181    #[test]
182    fn test_platform_parse_list() {
183        let platforms = Platform::parse_list("linux/amd64,linux/arm64").unwrap();
184        assert_eq!(platforms.len(), 2);
185        assert_eq!(platforms[0], Platform::linux_amd64());
186        assert_eq!(platforms[1], Platform::linux_arm64());
187    }
188
189    #[test]
190    fn test_platform_parse_list_with_spaces() {
191        let platforms = Platform::parse_list("linux/amd64, linux/arm64").unwrap();
192        assert_eq!(platforms.len(), 2);
193    }
194
195    #[test]
196    fn test_platform_host() {
197        let host = Platform::host();
198        assert_eq!(host.os, "linux");
199        assert!(host.architecture == "amd64" || host.architecture == "arm64");
200    }
201
202    #[test]
203    fn test_platform_is_native() {
204        let host = Platform::host();
205        assert!(host.is_native());
206        // The opposite arch should not be native
207        let other = if host.architecture == "amd64" {
208            Platform::linux_arm64()
209        } else {
210            Platform::linux_amd64()
211        };
212        assert!(!other.is_native());
213    }
214
215    #[test]
216    fn test_platform_serde_roundtrip() {
217        let p = Platform::linux_amd64();
218        let json = serde_json::to_string(&p).unwrap();
219        let parsed: Platform = serde_json::from_str(&json).unwrap();
220        assert_eq!(parsed, p);
221    }
222
223    #[test]
224    fn test_platform_serde_with_variant() {
225        let p = Platform::with_variant("linux", "arm", "v7");
226        let json = serde_json::to_string(&p).unwrap();
227        let parsed: Platform = serde_json::from_str(&json).unwrap();
228        assert_eq!(parsed, p);
229        assert_eq!(parsed.variant, Some("v7".to_string()));
230    }
231
232    #[test]
233    fn test_normalize_arch() {
234        assert_eq!(normalize_arch("x86_64"), "amd64");
235        assert_eq!(normalize_arch("aarch64"), "arm64");
236        assert_eq!(normalize_arch("armhf"), "arm");
237        assert_eq!(normalize_arch("i386"), "386");
238        assert_eq!(normalize_arch("riscv64"), "riscv64");
239    }
240
241    #[test]
242    fn test_platform_equality() {
243        assert_eq!(Platform::linux_amd64(), Platform::new("linux", "amd64"));
244        assert_ne!(Platform::linux_amd64(), Platform::linux_arm64());
245    }
246}