1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum PlatformError {
10 Empty,
11 InvalidPart,
12 InvalidPlatform,
13}
14
15impl fmt::Display for PlatformError {
16 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 Self::Empty => formatter.write_str("OCI platform value cannot be empty"),
19 Self::InvalidPart => formatter.write_str("invalid OCI platform part"),
20 Self::InvalidPlatform => formatter.write_str("invalid OCI platform string"),
21 }
22 }
23}
24
25impl Error for PlatformError {}
26
27#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub enum OciOs {
30 Linux,
31 Windows,
32 Darwin,
33 FreeBsd,
34 Wasm,
35 Unknown,
36 Custom(String),
37}
38
39impl OciOs {
40 #[must_use]
42 pub fn as_str(&self) -> &str {
43 match self {
44 Self::Linux => "linux",
45 Self::Windows => "windows",
46 Self::Darwin => "darwin",
47 Self::FreeBsd => "freebsd",
48 Self::Wasm => "wasm",
49 Self::Unknown => "unknown",
50 Self::Custom(value) => value,
51 }
52 }
53}
54
55impl fmt::Display for OciOs {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter.write_str(self.as_str())
58 }
59}
60
61impl FromStr for OciOs {
62 type Err = PlatformError;
63
64 fn from_str(value: &str) -> Result<Self, Self::Err> {
65 let normalized = normalize_part(value)?;
66 match normalized.as_str() {
67 "linux" => Ok(Self::Linux),
68 "windows" => Ok(Self::Windows),
69 "darwin" | "macos" => Ok(Self::Darwin),
70 "freebsd" => Ok(Self::FreeBsd),
71 "wasm" | "wasi" => Ok(Self::Wasm),
72 "unknown" => Ok(Self::Unknown),
73 _ => Ok(Self::Custom(normalized)),
74 }
75 }
76}
77
78#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub enum OciArchitecture {
81 Amd64,
82 Arm64,
83 Arm,
84 I386,
85 Ppc64le,
86 Riscv64,
87 S390x,
88 Wasm,
89 Unknown,
90 Custom(String),
91}
92
93impl OciArchitecture {
94 #[must_use]
96 pub fn as_str(&self) -> &str {
97 match self {
98 Self::Amd64 => "amd64",
99 Self::Arm64 => "arm64",
100 Self::Arm => "arm",
101 Self::I386 => "386",
102 Self::Ppc64le => "ppc64le",
103 Self::Riscv64 => "riscv64",
104 Self::S390x => "s390x",
105 Self::Wasm => "wasm",
106 Self::Unknown => "unknown",
107 Self::Custom(value) => value,
108 }
109 }
110}
111
112impl fmt::Display for OciArchitecture {
113 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114 formatter.write_str(self.as_str())
115 }
116}
117
118impl FromStr for OciArchitecture {
119 type Err = PlatformError;
120
121 fn from_str(value: &str) -> Result<Self, Self::Err> {
122 let normalized = normalize_part(value)?;
123 match normalized.replace(['_', '-', ' '], "").as_str() {
124 "amd64" | "x8664" | "x64" => Ok(Self::Amd64),
125 "arm64" | "aarch64" => Ok(Self::Arm64),
126 "arm" => Ok(Self::Arm),
127 "386" | "i386" | "i686" => Ok(Self::I386),
128 "ppc64le" => Ok(Self::Ppc64le),
129 "riscv64" => Ok(Self::Riscv64),
130 "s390x" => Ok(Self::S390x),
131 "wasm" | "wasm32" | "wasm64" => Ok(Self::Wasm),
132 "unknown" => Ok(Self::Unknown),
133 _ => Ok(Self::Custom(normalized)),
134 }
135 }
136}
137
138macro_rules! text_part {
139 ($name:ident) => {
140 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
141 pub struct $name(String);
142
143 impl $name {
144 pub fn new(value: impl AsRef<str>) -> Result<Self, PlatformError> {
146 normalize_part(value.as_ref()).map(Self)
147 }
148
149 #[must_use]
151 pub fn as_str(&self) -> &str {
152 &self.0
153 }
154 }
155
156 impl AsRef<str> for $name {
157 fn as_ref(&self) -> &str {
158 self.as_str()
159 }
160 }
161
162 impl fmt::Display for $name {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 formatter.write_str(self.as_str())
165 }
166 }
167 };
168}
169
170text_part!(PlatformVariant);
171text_part!(OsVersion);
172text_part!(OsFeature);
173
174#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
176pub struct OciPlatform {
177 os: OciOs,
178 architecture: OciArchitecture,
179 variant: Option<PlatformVariant>,
180 os_version: Option<OsVersion>,
181 os_features: Vec<OsFeature>,
182}
183
184impl OciPlatform {
185 #[must_use]
187 pub fn new(os: OciOs, architecture: OciArchitecture) -> Self {
188 Self {
189 os,
190 architecture,
191 variant: None,
192 os_version: None,
193 os_features: Vec::new(),
194 }
195 }
196
197 pub fn with_variant(mut self, variant: impl AsRef<str>) -> Result<Self, PlatformError> {
199 self.variant = Some(PlatformVariant::new(variant)?);
200 Ok(self)
201 }
202
203 pub fn with_os_version(mut self, version: impl AsRef<str>) -> Result<Self, PlatformError> {
205 self.os_version = Some(OsVersion::new(version)?);
206 Ok(self)
207 }
208
209 pub fn with_os_feature(mut self, feature: impl AsRef<str>) -> Result<Self, PlatformError> {
211 self.os_features.push(OsFeature::new(feature)?);
212 Ok(self)
213 }
214
215 #[must_use]
217 pub const fn os(&self) -> &OciOs {
218 &self.os
219 }
220
221 #[must_use]
223 pub const fn architecture(&self) -> &OciArchitecture {
224 &self.architecture
225 }
226
227 #[must_use]
229 pub const fn variant(&self) -> Option<&PlatformVariant> {
230 self.variant.as_ref()
231 }
232
233 #[must_use]
235 pub fn os_features(&self) -> &[OsFeature] {
236 &self.os_features
237 }
238}
239
240impl fmt::Display for OciPlatform {
241 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(formatter, "{}/{}", self.os, self.architecture)?;
243 if let Some(variant) = &self.variant {
244 write!(formatter, "/{variant}")?;
245 }
246 Ok(())
247 }
248}
249
250impl FromStr for OciPlatform {
251 type Err = PlatformError;
252
253 fn from_str(value: &str) -> Result<Self, Self::Err> {
254 let parts = value.trim().split('/').collect::<Vec<_>>();
255 match parts.as_slice() {
256 [os, architecture] => Ok(Self::new(os.parse()?, architecture.parse()?)),
257 [os, architecture, variant] => {
258 Self::new(os.parse()?, architecture.parse()?).with_variant(variant)
259 },
260 _ => Err(PlatformError::InvalidPlatform),
261 }
262 }
263}
264
265fn normalize_part(value: &str) -> Result<String, PlatformError> {
266 let trimmed = value.trim();
267 if trimmed.is_empty() {
268 return Err(PlatformError::Empty);
269 }
270 if trimmed
271 .bytes()
272 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace() || byte == b'/')
273 {
274 return Err(PlatformError::InvalidPart);
275 }
276 Ok(trimmed.to_ascii_lowercase())
277}
278
279#[cfg(test)]
280mod tests {
281 use super::{OciArchitecture, OciOs, OciPlatform, PlatformError};
282
283 #[test]
284 fn parses_and_renders_platforms() -> Result<(), Box<dyn std::error::Error>> {
285 let platform: OciPlatform = "linux/arm64/v8".parse()?;
286
287 assert_eq!(platform.os(), &OciOs::Linux);
288 assert_eq!(platform.architecture(), &OciArchitecture::Arm64);
289 assert_eq!(platform.to_string(), "linux/arm64/v8");
290 assert_eq!(
291 "linux".parse::<OciPlatform>(),
292 Err(PlatformError::InvalidPlatform)
293 );
294 Ok(())
295 }
296
297 #[test]
298 fn accepts_common_architecture_aliases() -> Result<(), PlatformError> {
299 assert_eq!("x86_64".parse::<OciArchitecture>()?, OciArchitecture::Amd64);
300 assert_eq!(
301 "aarch64".parse::<OciArchitecture>()?,
302 OciArchitecture::Arm64
303 );
304 assert_eq!("i686".parse::<OciArchitecture>()?, OciArchitecture::I386);
305 Ok(())
306 }
307}