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 let os = match std::env::consts::OS {
65 "macos" => "darwin",
66 other => other,
67 };
68 Self::new(os, arch)
69 }
70
71 pub fn parse(s: &str) -> Result<Self, String> {
73 let parts: Vec<&str> = s.split('/').collect();
74 match parts.len() {
75 2 => {
76 let arch = normalize_arch(parts[1]);
77 Ok(Self::new(parts[0], arch))
78 }
79 3 => {
80 let arch = normalize_arch(parts[1]);
81 Ok(Self::with_variant(parts[0], arch, parts[2]))
82 }
83 _ => Err(format!(
84 "Invalid platform '{}': expected 'os/arch' or 'os/arch/variant'",
85 s
86 )),
87 }
88 }
89
90 pub fn parse_list(s: &str) -> Result<Vec<Self>, String> {
94 s.split(',').map(|p| Self::parse(p.trim())).collect()
95 }
96
97 pub fn is_native(&self) -> bool {
99 *self == Self::host()
100 }
101
102 pub fn oci_arch(&self) -> &str {
104 &self.architecture
105 }
106}
107
108impl fmt::Display for Platform {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{}/{}", self.os, self.architecture)?;
111 if let Some(ref v) = self.variant {
112 write!(f, "/{}", v)?;
113 }
114 Ok(())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct PlatformCapabilities {
124 pub os: String,
126 pub architecture: String,
128 pub vm_backend: VmBackend,
130 pub host_guest_channel: HostGuestChannel,
132 pub unix_sockets: bool,
134 pub named_pipes: bool,
136 pub netproxy: bool,
138 pub bridge_network_backend: BridgeNetworkBackend,
140 pub bridge_outbound_nat: bool,
142 pub published_ports: bool,
144 pub tee_attestation: bool,
146 pub sealed_storage: bool,
148 pub interactive_pty: bool,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case")]
155pub enum VmBackend {
156 Krun,
158 Whpx,
160 Unsupported,
162}
163
164impl fmt::Display for VmBackend {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 Self::Krun => write!(f, "krun"),
168 Self::Whpx => write!(f, "whpx"),
169 Self::Unsupported => write!(f, "unsupported"),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "kebab-case")]
177pub enum HostGuestChannel {
178 UnixSocket,
180 NamedPipe,
182 Unsupported,
184}
185
186impl fmt::Display for HostGuestChannel {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::UnixSocket => write!(f, "unix-socket"),
190 Self::NamedPipe => write!(f, "named-pipe"),
191 Self::Unsupported => write!(f, "unsupported"),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(rename_all = "kebab-case")]
199pub enum BridgeNetworkBackend {
200 Passt,
202 Netproxy,
204 Unsupported,
206}
207
208impl fmt::Display for BridgeNetworkBackend {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 Self::Passt => write!(f, "passt"),
212 Self::Netproxy => write!(f, "netproxy"),
213 Self::Unsupported => write!(f, "unsupported"),
214 }
215 }
216}
217
218impl PlatformCapabilities {
219 pub fn current() -> Self {
221 let host = Platform::host();
222
223 Self {
224 os: std::env::consts::OS.to_string(),
225 architecture: host.architecture,
226 vm_backend: current_vm_backend(),
227 host_guest_channel: current_host_guest_channel(),
228 unix_sockets: cfg!(unix),
229 named_pipes: cfg!(windows),
230 netproxy: cfg!(target_os = "macos"),
231 bridge_network_backend: current_bridge_network_backend(),
232 bridge_outbound_nat: cfg!(target_os = "linux"),
236 published_ports: cfg!(unix) || cfg!(windows),
237 tee_attestation: cfg!(unix),
238 sealed_storage: cfg!(unix),
239 interactive_pty: cfg!(unix),
240 }
241 }
242
243 pub fn supports_native_vm(&self) -> bool {
245 matches!(self.vm_backend, VmBackend::Krun | VmBackend::Whpx)
246 }
247
248 pub fn supports_host_guest_channel(&self) -> bool {
250 self.host_guest_channel != HostGuestChannel::Unsupported
251 }
252
253 pub fn supports_bridge_networking(&self) -> bool {
255 self.bridge_network_backend != BridgeNetworkBackend::Unsupported
256 }
257
258 pub fn bridge_networking_summary(&self) -> String {
260 match self.bridge_network_backend {
261 BridgeNetworkBackend::Passt => {
262 "passt (peer networking and outbound NAT supported)".to_string()
263 }
264 BridgeNetworkBackend::Netproxy => {
265 "netproxy (peer networking and outbound TCP proxying supported)".to_string()
266 }
267 BridgeNetworkBackend::Unsupported => "unsupported".to_string(),
268 }
269 }
270}
271
272#[cfg(unix)]
273fn current_vm_backend() -> VmBackend {
274 VmBackend::Krun
275}
276
277#[cfg(windows)]
278fn current_vm_backend() -> VmBackend {
279 VmBackend::Whpx
280}
281
282#[cfg(not(any(unix, windows)))]
283fn current_vm_backend() -> VmBackend {
284 VmBackend::Unsupported
285}
286
287#[cfg(unix)]
288fn current_host_guest_channel() -> HostGuestChannel {
289 HostGuestChannel::UnixSocket
290}
291
292#[cfg(windows)]
293fn current_host_guest_channel() -> HostGuestChannel {
294 HostGuestChannel::NamedPipe
295}
296
297#[cfg(not(any(unix, windows)))]
298fn current_host_guest_channel() -> HostGuestChannel {
299 HostGuestChannel::Unsupported
300}
301
302#[cfg(target_os = "linux")]
303fn current_bridge_network_backend() -> BridgeNetworkBackend {
304 BridgeNetworkBackend::Passt
305}
306
307#[cfg(target_os = "macos")]
308fn current_bridge_network_backend() -> BridgeNetworkBackend {
309 BridgeNetworkBackend::Netproxy
310}
311
312#[cfg(not(any(target_os = "linux", target_os = "macos")))]
313fn current_bridge_network_backend() -> BridgeNetworkBackend {
314 BridgeNetworkBackend::Unsupported
315}
316
317fn normalize_arch(arch: &str) -> String {
319 match arch {
320 "x86_64" | "x86-64" => "amd64".to_string(),
321 "aarch64" | "arm64v8" => "arm64".to_string(),
322 "armhf" | "armv7l" => "arm".to_string(),
323 "i386" | "i686" | "x86" => "386".to_string(),
324 other => other.to_string(),
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_platform_new() {
334 let p = Platform::new("linux", "amd64");
335 assert_eq!(p.os, "linux");
336 assert_eq!(p.architecture, "amd64");
337 assert!(p.variant.is_none());
338 }
339
340 #[test]
341 fn test_platform_with_variant() {
342 let p = Platform::with_variant("linux", "arm", "v7");
343 assert_eq!(p.variant, Some("v7".to_string()));
344 }
345
346 #[test]
347 fn test_platform_display() {
348 assert_eq!(Platform::linux_amd64().to_string(), "linux/amd64");
349 assert_eq!(Platform::linux_arm64().to_string(), "linux/arm64");
350 assert_eq!(
351 Platform::with_variant("linux", "arm", "v7").to_string(),
352 "linux/arm/v7"
353 );
354 }
355
356 #[test]
357 fn test_platform_parse() {
358 let p = Platform::parse("linux/amd64").unwrap();
359 assert_eq!(p, Platform::linux_amd64());
360
361 let p = Platform::parse("linux/arm64").unwrap();
362 assert_eq!(p, Platform::linux_arm64());
363
364 let p = Platform::parse("linux/arm/v7").unwrap();
365 assert_eq!(p.architecture, "arm");
366 assert_eq!(p.variant, Some("v7".to_string()));
367 }
368
369 #[test]
370 fn test_platform_parse_normalizes() {
371 let p = Platform::parse("linux/x86_64").unwrap();
372 assert_eq!(p.architecture, "amd64");
373
374 let p = Platform::parse("linux/aarch64").unwrap();
375 assert_eq!(p.architecture, "arm64");
376 }
377
378 #[test]
379 fn test_platform_parse_invalid() {
380 assert!(Platform::parse("linux").is_err());
381 assert!(Platform::parse("a/b/c/d").is_err());
382 }
383
384 #[test]
385 fn test_platform_parse_list() {
386 let platforms = Platform::parse_list("linux/amd64,linux/arm64").unwrap();
387 assert_eq!(platforms.len(), 2);
388 assert_eq!(platforms[0], Platform::linux_amd64());
389 assert_eq!(platforms[1], Platform::linux_arm64());
390 }
391
392 #[test]
393 fn test_platform_parse_list_with_spaces() {
394 let platforms = Platform::parse_list("linux/amd64, linux/arm64").unwrap();
395 assert_eq!(platforms.len(), 2);
396 }
397
398 #[test]
399 fn test_platform_host() {
400 let host = Platform::host();
401 let expected_os = match std::env::consts::OS {
402 "macos" => "darwin",
403 other => other,
404 };
405 assert_eq!(host.os, expected_os);
406 assert!(host.architecture == "amd64" || host.architecture == "arm64");
407 }
408
409 #[test]
410 fn test_platform_is_native() {
411 let host = Platform::host();
412 assert!(host.is_native());
413 let other = if host.architecture == "amd64" {
415 Platform::linux_arm64()
416 } else {
417 Platform::linux_amd64()
418 };
419 assert!(!other.is_native());
420 }
421
422 #[test]
423 fn test_platform_serde_roundtrip() {
424 let p = Platform::linux_amd64();
425 let json = serde_json::to_string(&p).unwrap();
426 let parsed: Platform = serde_json::from_str(&json).unwrap();
427 assert_eq!(parsed, p);
428 }
429
430 #[test]
431 fn test_platform_serde_with_variant() {
432 let p = Platform::with_variant("linux", "arm", "v7");
433 let json = serde_json::to_string(&p).unwrap();
434 let parsed: Platform = serde_json::from_str(&json).unwrap();
435 assert_eq!(parsed, p);
436 assert_eq!(parsed.variant, Some("v7".to_string()));
437 }
438
439 #[test]
440 fn test_normalize_arch() {
441 assert_eq!(normalize_arch("x86_64"), "amd64");
442 assert_eq!(normalize_arch("aarch64"), "arm64");
443 assert_eq!(normalize_arch("armhf"), "arm");
444 assert_eq!(normalize_arch("i386"), "386");
445 assert_eq!(normalize_arch("riscv64"), "riscv64");
446 }
447
448 #[test]
449 fn test_platform_equality() {
450 assert_eq!(Platform::linux_amd64(), Platform::new("linux", "amd64"));
451 assert_ne!(Platform::linux_amd64(), Platform::linux_arm64());
452 }
453
454 #[test]
455 fn test_platform_capabilities_match_host() {
456 let capabilities = PlatformCapabilities::current();
457 assert_eq!(capabilities.os, std::env::consts::OS);
458 assert!(capabilities.supports_host_guest_channel());
459
460 #[cfg(unix)]
461 {
462 assert_eq!(capabilities.vm_backend, VmBackend::Krun);
463 assert_eq!(
464 capabilities.host_guest_channel,
465 HostGuestChannel::UnixSocket
466 );
467 assert!(capabilities.unix_sockets);
468 assert!(!capabilities.named_pipes);
469 assert!(capabilities.supports_native_vm());
470 assert!(capabilities.supports_bridge_networking());
471 }
472
473 #[cfg(windows)]
474 {
475 assert_eq!(capabilities.vm_backend, VmBackend::Whpx);
476 assert_eq!(capabilities.host_guest_channel, HostGuestChannel::NamedPipe);
477 assert!(!capabilities.unix_sockets);
478 assert!(capabilities.named_pipes);
479 assert!(capabilities.supports_native_vm());
480 assert!(!capabilities.interactive_pty);
481 }
482 }
483
484 #[test]
485 fn test_platform_capability_display_values() {
486 assert_eq!(VmBackend::Krun.to_string(), "krun");
487 assert_eq!(VmBackend::Whpx.to_string(), "whpx");
488 assert_eq!(HostGuestChannel::UnixSocket.to_string(), "unix-socket");
489 assert_eq!(HostGuestChannel::NamedPipe.to_string(), "named-pipe");
490 assert_eq!(BridgeNetworkBackend::Netproxy.to_string(), "netproxy");
491 }
492
493 #[test]
494 fn test_bridge_networking_summary_documents_outbound_support() {
495 let mut capabilities = PlatformCapabilities::current();
496 capabilities.bridge_network_backend = BridgeNetworkBackend::Netproxy;
497 capabilities.bridge_outbound_nat = false;
498
499 let summary = capabilities.bridge_networking_summary();
500
501 assert!(summary.contains("netproxy"));
502 assert!(summary.contains("outbound TCP proxying supported"));
503 assert!(!capabilities.bridge_outbound_nat);
504 }
505}