1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct Platform {
15 pub os: String,
17 pub architecture: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub variant: Option<String>,
22}
23
24impl Platform {
25 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 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 pub fn linux_amd64() -> Self {
49 Self::new("linux", "amd64")
50 }
51
52 pub fn linux_arm64() -> Self {
54 Self::new("linux", "arm64")
55 }
56
57 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 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 pub fn parse_list(s: &str) -> Result<Vec<Self>, String> {
90 s.split(',').map(|p| Self::parse(p.trim())).collect()
91 }
92
93 pub fn is_native(&self) -> bool {
95 *self == Self::host()
96 }
97
98 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
114fn 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 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}