Skip to main content

wslplugins_rs/
wsl_version.rs

1use std::{
2    fmt::{self, Debug, Display},
3    hash::Hash,
4    ptr,
5    str::FromStr,
6};
7use strum::IntoEnumIterator;
8
9mod parse_error;
10pub use parse_error::WSLVersionParseError;
11
12mod capability;
13pub use capability::WSLVersionCapability;
14
15#[cfg(feature = "semver")]
16mod semver_impl;
17#[cfg(feature = "semver")]
18pub use semver_impl::SemverConversionError;
19
20#[cfg(feature = "serde")]
21mod serde_impl;
22
23/// Represents a WSL version number.
24///
25/// This struct wraps the `WSLVersion` from the WSL Plugin API and provides
26/// safe, idiomatic Rust access to its fields.
27/// # Example
28/// ```
29/// use wslplugins_rs::WSLVersion;
30/// let version = WSLVersion::new(2, 0, 0);
31/// assert_eq!(version.major(), 2);
32/// assert_eq!(version.minor(), 0);
33/// assert_eq!(version.revision(), 0);
34/// ```
35#[repr(transparent)]
36#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct WSLVersion(wslpluginapi_sys::WSLVersion);
38
39impl WSLVersion {
40    /// Creates a new `WSLVersion` instance.
41    /// # Parameters
42    /// - `major`: The major version number.
43    /// - `minor`: The minor version number.
44    /// - `revision`: The revision number.
45    /// # Returns
46    /// The new `WSLVersion` instance.
47    #[must_use]
48    #[inline]
49    pub const fn new(major: u32, minor: u32, revision: u32) -> Self {
50        Self(wslpluginapi_sys::WSLVersion {
51            Major: major,
52            Minor: minor,
53            Revision: revision,
54        })
55    }
56
57    /// Retrieves the major version number.
58    #[must_use]
59    #[inline]
60    pub const fn major(&self) -> u32 {
61        self.0.Major
62    }
63
64    /// Set the major version number.
65    #[inline]
66    pub const fn set_major(&mut self, major: u32) {
67        self.0.Major = major;
68    }
69
70    /// Retrieves the minor version number.
71    #[must_use]
72    #[inline]
73    pub const fn minor(&self) -> u32 {
74        self.0.Minor
75    }
76
77    /// Set the minor version number.
78    #[inline]
79    pub const fn set_minor(&mut self, minor: u32) {
80        self.0.Minor = minor;
81    }
82
83    /// Retrieves the revision version number.
84    #[must_use]
85    #[inline]
86    pub const fn revision(&self) -> u32 {
87        self.0.Revision
88    }
89
90    /// Set the revision version number.
91    #[inline]
92    pub const fn set_revision(&mut self, revision: u32) {
93        self.0.Revision = revision;
94    }
95
96    /// Returns `true` when this version is greater than or equal to `required_version`.
97    #[must_use]
98    #[inline]
99    pub const fn is_at_least(&self, required_version: Self) -> bool {
100        self.major() > required_version.major()
101            || (self.major() == required_version.major()
102                && (self.minor() > required_version.minor()
103                    || (self.minor() == required_version.minor()
104                        && self.revision() >= required_version.revision())))
105    }
106
107    /// Returns `true` when this version supports the requested capability.
108    #[must_use]
109    #[inline]
110    pub const fn supports(&self, capability: WSLVersionCapability) -> bool {
111        self.is_at_least(capability.required_version())
112    }
113
114    /// Iterates over every capability supported by this version, ordered by required version.
115    #[inline]
116    pub fn capabilities(&self) -> impl Iterator<Item = WSLVersionCapability> + '_ {
117        WSLVersionCapability::iter().filter(|capability| self.supports(*capability))
118    }
119}
120
121impl From<wslpluginapi_sys::WSLVersion> for WSLVersion {
122    #[inline]
123    fn from(value: wslpluginapi_sys::WSLVersion) -> Self {
124        Self(value)
125    }
126}
127
128impl From<WSLVersion> for wslpluginapi_sys::WSLVersion {
129    #[inline]
130    fn from(value: WSLVersion) -> Self {
131        value.0
132    }
133}
134
135impl AsRef<WSLVersion> for wslpluginapi_sys::WSLVersion {
136    #[inline]
137    fn as_ref(&self) -> &WSLVersion {
138        // SAFETY: Converting this reference is safe because `WSLVersion` is
139        // `#[repr(transparent)]` over `wslpluginapi_sys::WSLVersion`.
140        unsafe { &*ptr::from_ref(self).cast::<WSLVersion>() }
141    }
142}
143
144impl AsRef<wslpluginapi_sys::WSLVersion> for WSLVersion {
145    #[inline]
146    fn as_ref(&self) -> &wslpluginapi_sys::WSLVersion {
147        &self.0
148    }
149}
150
151impl Display for WSLVersion {
152    #[inline]
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}.{}.{}", self.major(), self.minor(), self.revision())
155    }
156}
157
158impl Debug for WSLVersion {
159    #[inline]
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.debug_struct(stringify!(WSLVersion))
162            .field("major", &self.major())
163            .field("minor", &self.minor())
164            .field("revision", &self.revision())
165            .finish()
166    }
167}
168
169impl FromStr for WSLVersion {
170    type Err = WSLVersionParseError;
171
172    #[inline]
173    #[expect(
174        clippy::indexing_slicing,
175        reason = "We check the length of `parts` before indexing it, so this is safe."
176    )]
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        let parts: Vec<&str> = s.split('.').collect();
179        if !matches!(parts.len(), 2 | 3) {
180            return Err(WSLVersionParseError::InvalidFormat {
181                input: s.to_owned(),
182            });
183        }
184
185        let major = parts[0]
186            .parse::<u32>()
187            .map_err(|_| WSLVersionParseError::InvalidMajor {
188                input: s.to_owned(),
189            })?;
190        let minor = parts[1]
191            .parse::<u32>()
192            .map_err(|_| WSLVersionParseError::InvalidMinor {
193                input: s.to_owned(),
194            })?;
195        let revision = parts
196            .get(2)
197            .map(|s| s.parse::<u32>())
198            .transpose()
199            .map_err(|_| WSLVersionParseError::InvalidRevision {
200                input: s.to_owned(),
201            })?
202            .unwrap_or(0);
203
204        Ok(Self::new(major, minor, revision))
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::utils::test_transparence;
212    use proptest::prelude::*;
213
214    fn arb_wsl_version() -> impl Strategy<Value = WSLVersion> {
215        (any::<u32>(), any::<u32>(), any::<u32>())
216            .prop_map(|(major, minor, revision)| WSLVersion::new(major, minor, revision))
217    }
218
219    #[test]
220    fn test_layouts() {
221        test_transparence::<wslpluginapi_sys::WSLVersion, WSLVersion>();
222    }
223
224    #[test]
225    fn test_from_str_rejects_invalid_format() {
226        let result = "2".parse::<WSLVersion>();
227
228        assert_eq!(
229            result,
230            Err(WSLVersionParseError::InvalidFormat {
231                input: "2".to_owned(),
232            })
233        );
234    }
235
236    #[test]
237    fn test_from_str_rejects_invalid_major() {
238        let result = "a.0.0".parse::<WSLVersion>();
239
240        assert_eq!(
241            result,
242            Err(WSLVersionParseError::InvalidMajor {
243                input: "a.0.0".to_owned(),
244            })
245        );
246    }
247
248    #[test]
249    fn test_from_str_rejects_invalid_minor() {
250        let result = "2.a.0".parse::<WSLVersion>();
251
252        assert_eq!(
253            result,
254            Err(WSLVersionParseError::InvalidMinor {
255                input: "2.a.0".to_owned(),
256            })
257        );
258    }
259
260    #[test]
261    fn test_from_str_rejects_invalid_revision() {
262        let result = "2.0.a".parse::<WSLVersion>();
263
264        assert_eq!(
265            result,
266            Err(WSLVersionParseError::InvalidRevision {
267                input: "2.0.a".to_owned(),
268            })
269        );
270    }
271
272    #[test]
273    fn supports_capability_when_version_is_high_enough() {
274        let version = WSLVersion::new(2, 1, 2);
275
276        assert!(version.supports(WSLVersionCapability::DistributionRegisteredHook));
277    }
278
279    #[test]
280    fn rejects_capability_when_version_is_too_low() {
281        let version = WSLVersion::new(2, 1, 1);
282
283        assert!(!version.supports(WSLVersionCapability::DistributionRegisteredHook));
284    }
285
286    #[test]
287    fn capabilities_iterates_supported_capabilities_by_required_version() {
288        let version = WSLVersion::new(2, 4, 4);
289        let capabilities = version.capabilities().collect::<Vec<_>>();
290
291        assert_eq!(
292            capabilities,
293            vec![
294                WSLVersionCapability::DistributionInitPid,
295                WSLVersionCapability::DistributionRegisteredHook,
296                WSLVersionCapability::DistributionUnregisteredHook,
297                WSLVersionCapability::ExecuteBinaryInDistribution,
298                WSLVersionCapability::DistributionFlavor,
299                WSLVersionCapability::DistributionVersion,
300            ]
301        );
302    }
303
304    #[test]
305    fn capabilities_excludes_capabilities_that_require_newer_versions() {
306        let version = WSLVersion::new(2, 1, 2);
307        let capabilities = version.capabilities().collect::<Vec<_>>();
308
309        assert_eq!(
310            capabilities,
311            vec![
312                WSLVersionCapability::DistributionInitPid,
313                WSLVersionCapability::DistributionRegisteredHook,
314                WSLVersionCapability::DistributionUnregisteredHook,
315                WSLVersionCapability::ExecuteBinaryInDistribution,
316            ]
317        );
318    }
319
320    proptest! {
321        #[test]
322        fn from_str_roundtrips_displayed_versions(version in arb_wsl_version()) {
323            prop_assert_eq!(version.to_string().parse::<WSLVersion>(), Ok(version));
324        }
325
326        #[test]
327        fn is_at_least_matches_derived_ordering(
328            current in arb_wsl_version(),
329            required in arb_wsl_version(),
330        ) {
331            prop_assert_eq!(current.is_at_least(required), current >= required);
332        }
333
334        #[test]
335        fn supports_matches_capability_required_version(version in arb_wsl_version()) {
336            for capability in WSLVersionCapability::iter() {
337                prop_assert_eq!(
338                    version.supports(capability),
339                    version.is_at_least(capability.required_version())
340                );
341            }
342        }
343
344        #[test]
345        fn capabilities_iterates_exactly_supported_capabilities(version in arb_wsl_version()) {
346            let expected = WSLVersionCapability::iter()
347                .filter(|capability| version.supports(*capability))
348                .collect::<Vec<_>>();
349
350            prop_assert_eq!(version.capabilities().collect::<Vec<_>>(), expected);
351        }
352    }
353}