Skip to main content

unlab_gpu/
version.rs

1//
2// Copyright (c) 2025-2026 Ɓukasz Szpakowski
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7//
8//! A version module.
9use std::cmp::Ordering;
10use std::cmp::max;
11use std::fmt;
12use std::result;
13use crate::serde::de;
14use crate::serde::de::Visitor;
15use crate::serde::Deserialize;
16use crate::serde::Deserializer;
17use crate::serde::Serialize;
18use crate::serde::Serializer;
19use crate::error::*;
20
21/// A pre-release identifier.
22#[derive(Clone, Debug)]
23pub enum PreReleaseIdent
24{
25    /// A numeric identfier.
26    Numeric(u32),
27    /// An alphanumeric identifier.
28    Alphanumeric(String),
29}
30
31impl Eq for PreReleaseIdent
32{}
33
34impl PartialEq for PreReleaseIdent
35{
36    fn eq(&self, other: &Self) -> bool
37    { self.cmp(other) == Ordering::Equal }
38}
39
40impl Ord for PreReleaseIdent
41{
42    fn cmp(&self, other: &Self) -> Ordering 
43    {
44        match (self, other) {
45            (PreReleaseIdent::Numeric(n), PreReleaseIdent::Numeric(m)) => n.cmp(&m),
46            (PreReleaseIdent::Alphanumeric(_), PreReleaseIdent::Numeric(_)) => Ordering::Greater,
47            (PreReleaseIdent::Numeric(_), PreReleaseIdent::Alphanumeric(_)) => Ordering::Less,
48            (PreReleaseIdent::Alphanumeric(s), PreReleaseIdent::Alphanumeric(t)) => s.cmp(&t),
49        }
50    }
51}
52
53impl PartialOrd for PreReleaseIdent
54{
55    fn partial_cmp(&self, other: &Self) -> Option<Ordering>
56    { Some(self.cmp(other)) }
57}
58
59impl fmt::Display for PreReleaseIdent
60{
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
62    {
63        match self {
64            PreReleaseIdent::Numeric(n) => write!(f, "{}", n),
65            PreReleaseIdent::Alphanumeric(s) => write!(f, "{}", s),
66        }
67    }
68}
69
70/// A version structure.
71///
72/// The version structure has version field and fields compatible with
73/// [SemVer](https://semver.org). 
74#[derive(Clone, Debug)]
75pub struct Version
76{
77    version: String,
78    numeric_idents: Vec<u32>,
79    pre_release_idents: Option<Vec<PreReleaseIdent>>,
80    build_idents: Option<Vec<String>>,
81}
82
83impl Version
84{
85    /// Creates a version.
86    pub fn new(version: String, numeric_idents: Vec<u32>, pre_release_idents: Option<Vec<PreReleaseIdent>>, build_idents: Option<Vec<String>>) -> Self
87    { Version { version, numeric_idents, pre_release_idents, build_idents, } }
88    
89    /// Parses the string slice to a version.
90    pub fn parse(s: &str) -> Result<Self>
91    {
92        let (pair_s, build) = match s.split_once('+') {
93            Some(pair) => pair,
94            None => (s, ""),
95        };
96        let (version_core, pre_release) = match pair_s.split_once('-') {
97            Some(pair) => pair,
98            None => (pair_s, ""),
99        };
100        let mut numeric_idents: Vec<u32> = Vec::new();
101        for t in version_core.split('.') {
102            match t.parse::<u32>() {
103                Ok(n) => numeric_idents.push(n),
104                Err(_) => return Err(Error::InvalidVersion),
105            }
106        }
107        let pre_release_idents = if !pre_release.is_empty() {
108            let mut tmp_pre_release_idents: Vec<PreReleaseIdent> = Vec::new();
109            for t in pre_release.split('.') {
110                match t.parse::<u32>() {
111                    Ok(n) => tmp_pre_release_idents.push(PreReleaseIdent::Numeric(n)),
112                    Err(_) => {
113                        if t.is_empty() || t.contains('/') || t.contains('\\') {
114                            return Err(Error::InvalidVersion);
115                        }
116                        tmp_pre_release_idents.push(PreReleaseIdent::Alphanumeric(String::from(t)))
117                    },
118                }
119            }
120            Some(tmp_pre_release_idents)
121        } else {
122            None
123        };
124        let build_idents = if !build.is_empty() {
125            let mut tmp_build_idents: Vec<String> = Vec::new();
126            for t in build.split('.') {
127                if t.is_empty() || t.contains('/') || t.contains('\\') {
128                    return Err(Error::InvalidVersion);
129                }
130                tmp_build_idents.push(String::from(t));
131            }
132            Some(tmp_build_idents)
133        } else {
134            None
135        };
136        Ok(Self::new(String::from(s), numeric_idents, pre_release_idents, build_idents))
137    }
138    
139    /// Returns the version as the string slice.
140    pub fn version(&self) -> &str
141    { self.version.as_str() }
142    
143    /// Returns the numeric identifiers.
144    pub fn numeric_idents(&self) -> &[u32]
145    { self.numeric_idents.as_slice() }
146
147    /// Retursn the pre-release identifiers.
148    pub fn pre_release_idents(&self) -> Option<&[PreReleaseIdent]>
149    {
150        match &self.pre_release_idents {
151            Some(pre_release_idents) => Some(pre_release_idents.as_slice()),
152            None => None,
153        }
154    }
155
156    /// Returns the build indentifiers.
157    pub fn build_idents(&self) -> Option<&[String]>
158    {
159        match &self.build_idents {
160            Some(build_idents) => Some(build_idents.as_slice()),
161            None => None,
162        }
163    }
164
165    /// Returns `true` if the numeric identifiers of versions are equal for the specified number
166    /// of numeric identifiers, otherwise `false`.
167    pub fn eq_numeric_idents(&self, version: &Version, count: usize) -> bool
168    {
169        for i in 0..count {
170            let n = if i < self.numeric_idents.len() {
171                self.numeric_idents[i]
172            } else {
173                0
174            };
175            let m = if i < version.numeric_idents.len() {
176                version.numeric_idents[i]
177            } else {
178                0
179            };
180            if n != m {
181                return false;
182            }
183        }
184        true
185    }
186}
187
188impl Eq for Version
189{}
190
191impl PartialEq for Version
192{
193    fn eq(&self, other: &Self) -> bool
194    { self.cmp(other) == Ordering::Equal }
195}
196
197impl Ord for Version
198{
199    fn cmp(&self, other: &Self) -> Ordering 
200    {
201        let len = max(self.numeric_idents.len(), other.numeric_idents.len());
202        for i in 0..len {
203            let n = if i < self.numeric_idents.len() {
204                self.numeric_idents[i]
205            } else {
206                0
207            };
208            let m = if i < other.numeric_idents.len() {
209                other.numeric_idents[i]
210            } else {
211                0
212            };
213            match n.cmp(&m) {
214                Ordering::Equal => (),
215                ordering => return ordering,
216            }
217        }
218        match (&self.pre_release_idents, &other.pre_release_idents) {
219            (Some(pre_release_idents), Some(pre_release_idents2)) => pre_release_idents.cmp(&pre_release_idents2),
220            (Some(_), None) => Ordering::Less,
221            (None, Some(_)) => Ordering::Greater,
222            (None, None) => Ordering::Equal,
223        }
224    }
225}
226
227impl PartialOrd for Version
228{
229    fn partial_cmp(&self, other: &Self) -> Option<Ordering>
230    { Some(self.cmp(other)) }
231}
232
233impl fmt::Display for Version
234{
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
236    { write!(f, "{}", self.version) }
237}
238
239impl Serialize for Version
240{
241    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
242        where S: Serializer
243    { serializer.serialize_str(format!("{}", self).as_str()) }
244}
245
246struct VersionVisitor;
247
248impl<'de> Visitor<'de> for VersionVisitor
249{
250    type Value = Version;
251
252    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
253    { write!(formatter, "a version") }
254
255    fn visit_str<E>(self, v: &str) -> result::Result<Self::Value, E>
256        where E: de::Error
257    {
258        match Version::parse(v) {
259            Ok(version) => Ok(version),
260            Err(err) => Err(E::custom(format!("{}", err))),
261        }
262    }
263}
264
265impl<'de> Deserialize<'de> for Version
266{
267    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
268        where D: Deserializer<'de>
269    { deserializer.deserialize_str(VersionVisitor) }
270}
271
272/// An enumeration of version  operator.
273#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
274pub enum VersionOp
275{
276    /// Eqaul.
277    Eq,
278    /// Not equal.
279    Ne,
280    /// Less than.
281    Lt,
282    /// Greater than or equal to.
283    Ge,
284    /// Greater than.
285    Gt,
286    /// Less than or equal to.
287    Le,
288    /// A default operator.
289    Default,
290    /// A tylde operator.
291    Tilde,
292}
293
294impl fmt::Display for VersionOp
295{
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
297    {
298        match self {
299            VersionOp::Eq => write!(f, "="),
300            VersionOp::Ne => write!(f, "!="),
301            VersionOp::Lt => write!(f, "<"),
302            VersionOp::Ge => write!(f, ">="),
303            VersionOp::Gt => write!(f, ">"),
304            VersionOp::Le => write!(f, "<="),
305            VersionOp::Default => write!(f, "^"),
306            VersionOp::Tilde => write!(f, "~"),
307        }
308    }
309}
310
311/// An enumeration of single version requirement.
312///
313/// The single version requirement can have one version with a version operator or can be a
314/// wildcard. A version requirement uses many single version requirements which can be matched to
315/// a version while matching.
316#[derive(Clone, Debug)]
317pub enum SingleVersionReq
318{
319    /// A wildcard.
320    Wildcard,
321    /// A pair of version operator and version.
322    Pair(VersionOp, Version),
323}
324
325impl SingleVersionReq
326{
327    /// Parsers the string slice to a single version requirement.
328    pub fn parse(s: &str) -> Result<Self>
329    {
330        let trimmed_s = s.trim();
331        if trimmed_s != "*" {
332            let (op, t) = if trimmed_s.starts_with("=") {
333                (VersionOp::Eq, &trimmed_s[1..])
334            } else if trimmed_s.starts_with("!=") {
335                (VersionOp::Ne, &trimmed_s[2..])
336            } else if trimmed_s.starts_with("<=") {
337                (VersionOp::Le, &trimmed_s[2..])
338            } else if trimmed_s.starts_with("<") {
339                (VersionOp::Lt, &trimmed_s[1..])
340            } else if trimmed_s.starts_with(">=") {
341                (VersionOp::Ge, &trimmed_s[2..])
342            } else if trimmed_s.starts_with(">") {
343                (VersionOp::Gt, &trimmed_s[1..])
344            } else if trimmed_s.starts_with("^") {
345                (VersionOp::Default, &trimmed_s[1..])
346            } else if trimmed_s.starts_with("~") {
347                (VersionOp::Tilde, &trimmed_s[1..])
348            } else {
349                (VersionOp::Default, trimmed_s)
350            };
351            let trimmed_t = t.trim();
352            let version = Version::parse(trimmed_t)?;
353            Ok(SingleVersionReq::Pair(op, version))
354        } else {
355            Ok(SingleVersionReq::Wildcard)
356        }
357    }
358    
359    /// Matches the single version requirement to the version.
360    ///
361    /// This method returns `true` if the single version requirement is matched to the version,
362    /// otherwise `false`.
363    pub fn matches(&self, version: &Version) -> bool
364    {
365        match self {
366            SingleVersionReq::Wildcard => true,
367            SingleVersionReq::Pair(op, version2) => {
368                match op {
369                    VersionOp::Eq => version == version2,
370                    VersionOp::Ne => version != version2,
371                    VersionOp::Lt => version < version2,
372                    VersionOp::Ge => version >= version2,
373                    VersionOp::Gt => version > version2,
374                    VersionOp::Le => version <= version2,
375                    VersionOp::Default => {
376                        let mut count = 0usize;
377                        if !version2.numeric_idents.is_empty() {
378                            count += 1;
379                            for i in 0..version2.numeric_idents.len() {
380                                match version2.numeric_idents.get(i) {
381                                    Some(0) if version2.numeric_idents.len() >= i + 2 => count += 1,
382                                    _ => break,
383                                }
384                            }
385                        }
386                        version >= version2 && version.eq_numeric_idents(version2, count)
387                    },
388                    VersionOp::Tilde => {
389                        let count = if !version2.numeric_idents.is_empty() {
390                            if version2.numeric_idents.len() >= 2 {
391                                2
392                            } else {
393                                1
394                            }
395                        } else {
396                            0
397                        };
398                        version >= version2 && version.eq_numeric_idents(version2, count)
399                    },
400                }
401            },
402        }
403    }
404}
405
406impl fmt::Display for SingleVersionReq
407{
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
409    {
410        match self {
411            SingleVersionReq::Wildcard => write!(f, "*"),
412            SingleVersionReq::Pair(op, version) => write!(f, "{}{}", op, version),
413        }
414    }
415}
416
417/// A structure of version requirement.
418///
419/// The version requirement can be matched to the version and can contain many single version
420/// requirements which can be matched to version while matching. The version requirements are used
421/// to check versions by a package manager and a `reqver` built-in function.
422#[derive(Clone, Debug)]
423pub struct VersionReq
424{
425    single_reqs: Vec<SingleVersionReq>,
426}
427
428impl VersionReq
429{
430    /// Creates a version requirement.
431    pub fn new(single_reqs: Vec<SingleVersionReq>) -> Self
432    { VersionReq { single_reqs, } }
433    
434    /// Parsers the string slice to a version requirement.
435    pub fn parse(s: &str) -> Result<Self>
436    {
437        let mut single_reqs: Vec<SingleVersionReq> = Vec::new();
438        for t in s.split(',') {
439            single_reqs.push(SingleVersionReq::parse(t)?);
440        }
441        Ok(VersionReq::new(single_reqs))
442    }
443    
444    /// Returns the single version requirements.
445    pub fn single_reqs(&self) -> &[SingleVersionReq]
446    { self.single_reqs.as_slice() }
447
448    /// Matches the version requirement to the version.
449    ///
450    /// This method returns `true` if the version requirement is matched to the version, otherwise
451    /// `false`.
452    pub fn matches(&self, version: &Version) -> bool
453    { self.single_reqs.iter().all(|sr| sr.matches(version)) }
454}
455
456impl fmt::Display for VersionReq
457{
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
459    {
460        let mut is_first = true;
461        for single_req in &self.single_reqs {
462            if !is_first {
463                write!(f, ",")?;
464            }
465            write!(f, "{}", single_req)?;
466            is_first = false;
467        }
468        Ok(())
469    }
470}
471
472impl Serialize for VersionReq
473{
474    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
475        where S: Serializer
476    { serializer.serialize_str(format!("{}", self).as_str()) }
477}
478
479struct VersionReqVisitor;
480
481impl<'de> Visitor<'de> for VersionReqVisitor
482{
483    type Value = VersionReq;
484
485    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
486    { write!(formatter, "a version requirement") }
487
488    fn visit_str<E>(self, v: &str) -> result::Result<Self::Value, E>
489        where E: de::Error
490    {
491        match VersionReq::parse(v) {
492            Ok(req) => Ok(req),
493            Err(err) => Err(E::custom(format!("{}", err))),
494        }
495    }
496}
497
498impl<'de> Deserialize<'de> for VersionReq
499{
500    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
501        where D: Deserializer<'de>
502    { deserializer.deserialize_str(VersionReqVisitor) }
503}
504
505#[cfg(test)]
506mod tests;