wslplugins_rs/
wsl_version.rs1use 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#[repr(transparent)]
36#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct WSLVersion(wslpluginapi_sys::WSLVersion);
38
39impl WSLVersion {
40 #[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 #[must_use]
59 #[inline]
60 pub const fn major(&self) -> u32 {
61 self.0.Major
62 }
63
64 #[inline]
66 pub const fn set_major(&mut self, major: u32) {
67 self.0.Major = major;
68 }
69
70 #[must_use]
72 #[inline]
73 pub const fn minor(&self) -> u32 {
74 self.0.Minor
75 }
76
77 #[inline]
79 pub const fn set_minor(&mut self, minor: u32) {
80 self.0.Minor = minor;
81 }
82
83 #[must_use]
85 #[inline]
86 pub const fn revision(&self) -> u32 {
87 self.0.Revision
88 }
89
90 #[inline]
92 pub const fn set_revision(&mut self, revision: u32) {
93 self.0.Revision = revision;
94 }
95
96 #[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 #[must_use]
109 #[inline]
110 pub const fn supports(&self, capability: WSLVersionCapability) -> bool {
111 self.is_at_least(capability.required_version())
112 }
113
114 #[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 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}