Skip to main content

gentoo_core/
variant.rs

1//! Gentoo release media variants.
2//!
3//! This module provides [`Variant`], a type representing the `{arch}-{tag}`
4//! format used for Gentoo release media (stage3 tarballs, ISO images, and
5//! profile selections).
6//!
7//! # Format
8//!
9//! Variants combine an architecture with a tag string:
10//!
11//! ```text
12//! {arch}-{tag}
13//! ```
14//!
15//! The tag typically encodes the init system and profile variant:
16//!
17//! - `"openrc"` — OpenRC init system
18//! - `"systemd"` — systemd init system
19//! - `"musl"` — musl libc
20//! - `"musl-hardened-openrc"` — combined musl and hardened profile
21//!
22//! # Examples
23//!
24//! ```
25//! use gentoo_core::Variant;
26//!
27//! let variant: Variant = "amd64-openrc".parse().unwrap();
28//! assert_eq!(variant.keyword(), "amd64");
29//! assert_eq!(variant.flavor(), "openrc");
30//! ```
31
32use crate::arch::Arch;
33use crate::error::Error;
34use gentoo_interner::{DefaultInterner, Interned, Interner};
35use std::fmt;
36use std::str::FromStr;
37
38/// A Gentoo release media variant.
39///
40/// Represents the `{arch}-{tag}` format used for stage3 tarballs, ISO images,
41/// and profile selections. The tag encodes the init system and profile variant
42/// (e.g., `"openrc"`, `"systemd"`, `"musl-hardened-openrc"`).
43///
44/// # Memory Efficiency
45///
46/// With the default `interner` feature, `Variant<GlobalInterner>` is `Copy`
47/// (8 bytes) and identical strings share a single allocation. This is useful
48/// when processing many variant references.
49///
50/// # Examples
51///
52/// ```
53/// use gentoo_core::{Variant, Arch, KnownArch};
54///
55/// let variant: Variant = "arm64-openrc".parse().unwrap();
56/// assert!(matches!(variant.arch, Arch::Known(KnownArch::AArch64)));
57/// assert_eq!(variant.flavor(), "openrc");
58/// ```
59#[derive(Debug, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(bound = ""))]
62pub struct Variant<I = DefaultInterner>
63where
64    I: Interner,
65{
66    /// Variant architecture.
67    pub arch: Arch<I>,
68    /// Interned flavor/profile string (e.g. `"openrc"`, `"systemd"`).
69    flavor: Interned<I>,
70}
71
72impl<I: Interner> Clone for Variant<I> {
73    fn clone(&self) -> Self {
74        Self {
75            arch: self.arch.clone(),
76            flavor: self.flavor.clone(),
77        }
78    }
79}
80
81impl<I: Interner> Copy for Variant<I> where Interned<I>: Copy {}
82
83impl<I: Interner> Variant<I> {
84    /// Create a variant from an arch and a flavor string using interner `I`.
85    pub(crate) fn new(arch: Arch<I>, flavor: &str) -> Self {
86        Self {
87            arch,
88            flavor: Interned::intern(flavor),
89        }
90    }
91
92    /// Parse arch + flavor strings using the interner `I`.
93    pub fn parse(arch: &str, flavor: &str) -> Result<Self, Error> {
94        let arch = Arch::intern(arch);
95        Ok(Self::new(arch, flavor))
96    }
97
98    /// Resolve the flavor string using the interner `I`.
99    pub fn flavor(&self) -> &str {
100        self.flavor.resolve()
101    }
102
103    /// The Gentoo keyword for this variant's architecture.
104    pub fn keyword(&self) -> &str {
105        self.arch.as_str()
106    }
107}
108
109impl<I: Interner> fmt::Display for Variant<I> {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}-{}", self.arch.as_str(), self.flavor())
112    }
113}
114
115impl<I: Interner> FromStr for Variant<I> {
116    type Err = Error;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        let (arch_str, flavor_str) = s.split_once('-').ok_or_else(|| {
120            Error::ParseError(format!(
121                "Invalid variant format: expected arch-flavor, got '{s}'"
122            ))
123        })?;
124        Self::parse(arch_str, flavor_str)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::arch::KnownArch;
132
133    #[test]
134    fn test_variant_creation() {
135        let variant: Variant = Variant::new(Arch::Known(KnownArch::X86_64), "systemd");
136        assert_eq!(variant.arch, Arch::Known(KnownArch::X86_64));
137        assert_eq!(variant.flavor(), "systemd");
138    }
139
140    #[test]
141    fn test_variant_keyword() {
142        assert_eq!(
143            Variant::new(<Arch>::Known(KnownArch::AArch64), "systemd").keyword(),
144            "arm64"
145        );
146        assert_eq!(
147            Variant::new(<Arch>::Known(KnownArch::X86), "openrc").keyword(),
148            "x86"
149        );
150    }
151
152    #[test]
153    fn test_variant_parsing() {
154        let variant: Variant = Variant::parse("amd64", "systemd").unwrap();
155        assert_eq!(variant.arch, Arch::Known(KnownArch::X86_64));
156
157        let variant: Variant = Variant::parse("arm", "openrc").unwrap();
158        assert_eq!(variant.arch, Arch::Known(KnownArch::Arm));
159    }
160
161    #[test]
162    fn test_from_str() {
163        let variant = "arm64-openrc".parse::<Variant>().unwrap();
164        assert!(matches!(variant.arch, Arch::Known(KnownArch::AArch64)));
165
166        let variant = "amd64-musl-hardened-openrc".parse::<Variant>().unwrap();
167        assert_eq!(variant.arch, Arch::Known(KnownArch::X86_64));
168        assert_eq!(variant.flavor(), "musl-hardened-openrc");
169
170        assert!("arm64".parse::<Variant>().is_err());
171    }
172
173    #[test]
174    fn test_display() {
175        assert_eq!(
176            Variant::new(<Arch>::Known(KnownArch::AArch64), "openrc").to_string(),
177            "arm64-openrc"
178        );
179        assert_eq!(
180            Variant::new(<Arch>::Known(KnownArch::X86_64), "musl-hardened-openrc").to_string(),
181            "amd64-musl-hardened-openrc"
182        );
183    }
184
185    // ── Serde roundtrip ──────────────────────────────────────────────────────
186
187    #[cfg(feature = "serde")]
188    mod serde {
189        use super::*;
190
191        #[test]
192        fn variant_known_arch_serializes_as_strings() {
193            let variant: Variant = "amd64-systemd".parse().unwrap();
194            let json = serde_json::to_string(&variant).unwrap();
195            assert_eq!(json, r#"{"arch":"amd64","flavor":"systemd"}"#);
196        }
197
198        #[test]
199        fn variant_known_arch_roundtrip() {
200            let original: Variant = "amd64-systemd".parse().unwrap();
201            let json = serde_json::to_string(&original).unwrap();
202            let restored: Variant = serde_json::from_str(&json).unwrap();
203            assert_eq!(original, restored);
204            assert_eq!(restored.flavor(), "systemd");
205        }
206
207        #[test]
208        fn variant_exotic_arch_roundtrip() {
209            let original: Variant = "mymachine-openrc".parse().unwrap();
210            let json = serde_json::to_string(&original).unwrap();
211            assert_eq!(json, r#"{"arch":"mymachine","flavor":"openrc"}"#);
212            let restored: Variant = serde_json::from_str(&json).unwrap();
213            assert_eq!(original, restored);
214        }
215
216        #[test]
217        fn variant_complex_flavor_roundtrip() {
218            let original: Variant = "amd64-musl-hardened-openrc".parse().unwrap();
219            let json = serde_json::to_string(&original).unwrap();
220            assert_eq!(json, r#"{"arch":"amd64","flavor":"musl-hardened-openrc"}"#);
221            let restored: Variant = serde_json::from_str(&json).unwrap();
222            assert_eq!(original, restored);
223        }
224    }
225}