Skip to main content

lux_lib/package/
version.rs

1use std::{
2    cmp::{self, Ordering},
3    fmt::Display,
4    str::FromStr,
5};
6
7use html_escape::decode_html_entities;
8use itertools::Itertools;
9
10use miette::Diagnostic;
11use nonempty::NonEmpty;
12use semver::{Comparator, Error, Op, Prerelease, Version, VersionReq};
13use serde::{de, Deserialize, Deserializer, Serialize};
14use thiserror::Error;
15#[derive(Debug, Error, Diagnostic)]
16pub enum VersionReqToVersionError {
17    #[error("cannot parse version from non-exact version requirement '{0}'")]
18    NonExactVersionReq(VersionReq),
19    #[error("cannot parse version from version requirement '*' (any version)")]
20    Any,
21}
22
23#[derive(Clone, Eq, PartialEq, Hash, Debug)]
24pub enum PackageVersion {
25    /// **SemVer version** as defined by <https://semver.org>,
26    /// but a bit more lenient for compatibility with luarocks
27    SemVer(SemVer),
28    /// A known **Dev** version
29    DevVer(DevVer),
30    /// An arbitrary string version.
31    /// Yes, luarocks-site allows arbitrary string versions in the root manifest
32    /// ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
33    StringVer(StringVer),
34}
35
36impl HasModRev for PackageVersion {
37    fn to_modrev_string(&self) -> String {
38        match self {
39            Self::SemVer(ver) => ver.to_modrev_string(),
40            Self::DevVer(ver) => ver.to_modrev_string(),
41            Self::StringVer(ver) => ver.to_modrev_string(),
42        }
43    }
44}
45
46impl PackageVersion {
47    pub fn parse(text: &str) -> Result<Self, PackageVersionParseError> {
48        PackageVersion::from_str(text)
49    }
50    /// Note that this loses the specrev information.
51    pub fn into_version_req(&self) -> PackageVersionReq {
52        match self {
53            PackageVersion::DevVer(DevVer { modrev, .. }) => {
54                PackageVersionReq::DevVer(modrev.to_owned())
55            }
56            PackageVersion::StringVer(StringVer { modrev, .. }) => {
57                PackageVersionReq::StringVer(modrev.to_owned())
58            }
59            PackageVersion::SemVer(SemVer { version, .. }) => {
60                let version = version.to_owned();
61                PackageVersionReq::SemVer(VersionReq {
62                    comparators: vec![Comparator {
63                        op: Op::Exact,
64                        major: version.major,
65                        minor: Some(version.minor),
66                        patch: Some(version.patch),
67                        pre: version.pre,
68                    }],
69                })
70            }
71        }
72    }
73
74    pub(crate) fn is_semver(&self) -> bool {
75        matches!(self, PackageVersion::SemVer(_))
76    }
77
78    pub(crate) fn default_dev_version() -> Self {
79        Self::DevVer(DevVer::default())
80    }
81
82    pub(crate) fn default_dev_version_with_specrev(specrev: SpecRev) -> Self {
83        Self::DevVer(DevVer::default_with_specrev(specrev))
84    }
85}
86
87impl TryFrom<PackageVersionReq> for PackageVersion {
88    type Error = VersionReqToVersionError;
89
90    fn try_from(req: PackageVersionReq) -> Result<Self, Self::Error> {
91        match req {
92            PackageVersionReq::SemVer(version_req) => {
93                match NonEmpty::try_from(version_req.comparators.clone()) {
94                    Ok(comparators)
95                        if comparators
96                            .iter()
97                            .any(|comparator| comparator.op != semver::Op::Exact) =>
98                    {
99                        Err(VersionReqToVersionError::NonExactVersionReq(version_req))
100                    }
101                    Ok(comparators) => {
102                        let comparator = comparators.first();
103                        let version = semver::Version {
104                            major: comparator.major,
105                            minor: comparator.minor.unwrap_or(0),
106                            patch: comparator.patch.unwrap_or(0),
107                            pre: comparator.pre.clone(),
108                            build: semver::BuildMetadata::EMPTY,
109                        };
110                        let component_count = if comparator.patch.is_some() {
111                            3
112                        } else if comparator.minor.is_some() {
113                            2
114                        } else {
115                            1
116                        };
117                        Ok(PackageVersion::SemVer(SemVer {
118                            version,
119                            component_count,
120                            specrev: 1.into(),
121                        }))
122                    }
123                    Err(_) => Err(VersionReqToVersionError::NonExactVersionReq(version_req)),
124                }
125            }
126            PackageVersionReq::DevVer(modrev) => Ok(PackageVersion::DevVer(DevVer {
127                modrev,
128                specrev: 1.into(),
129            })),
130            PackageVersionReq::StringVer(modrev) => Ok(PackageVersion::StringVer(StringVer {
131                modrev,
132                specrev: 1.into(),
133            })),
134            PackageVersionReq::Any => Err(VersionReqToVersionError::Any),
135        }
136    }
137}
138
139#[derive(Error, Debug, Diagnostic)]
140#[non_exhaustive]
141pub enum PackageVersionParseError {
142    #[error(transparent)]
143    #[diagnostic(transparent)]
144    Specrev(#[from] SpecrevParseError),
145    #[error("failed to parse version")]
146    Version(#[from] Error),
147}
148
149impl Serialize for PackageVersion {
150    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
151    where
152        S: serde::Serializer,
153    {
154        match self {
155            PackageVersion::SemVer(version) => version.serialize(serializer),
156            PackageVersion::DevVer(version) => version.serialize(serializer),
157            PackageVersion::StringVer(version) => version.serialize(serializer),
158        }
159    }
160}
161
162impl<'de> Deserialize<'de> for PackageVersion {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let s = String::deserialize(deserializer)?;
168        Self::from_str(&s).map_err(de::Error::custom)
169    }
170}
171
172impl Display for PackageVersion {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            PackageVersion::SemVer(version) => version.fmt(f),
176            PackageVersion::DevVer(version) => version.fmt(f),
177            PackageVersion::StringVer(version) => version.fmt(f),
178        }
179    }
180}
181
182impl PartialOrd for PackageVersion {
183    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
184        Some(self.cmp(other))
185    }
186}
187
188impl Ord for PackageVersion {
189    fn cmp(&self, other: &Self) -> Ordering {
190        match (self, other) {
191            (PackageVersion::SemVer(a), PackageVersion::SemVer(b)) => a.cmp(b),
192            (PackageVersion::SemVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
193            (PackageVersion::SemVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
194            (PackageVersion::DevVer(..), PackageVersion::SemVer(..)) => Ordering::Greater,
195            (PackageVersion::DevVer(a), PackageVersion::DevVer(b)) => a.cmp(b),
196            (PackageVersion::DevVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
197            (PackageVersion::StringVer(a), PackageVersion::StringVer(b)) => a.cmp(b),
198            (PackageVersion::StringVer(..), PackageVersion::SemVer(..)) => Ordering::Less,
199            (PackageVersion::StringVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
200        }
201    }
202}
203
204impl FromStr for PackageVersion {
205    type Err = PackageVersionParseError;
206
207    fn from_str(text: &str) -> Result<Self, Self::Err> {
208        let (modrev, specrev) = split_specrev(text)?;
209        match modrev {
210            "scm" => Ok(PackageVersion::DevVer(DevVer {
211                modrev: DevVersion::Scm,
212                specrev,
213            })),
214            "dev" => Ok(PackageVersion::DevVer(DevVer {
215                modrev: DevVersion::Dev,
216                specrev,
217            })),
218            modrev => match parse_version(modrev) {
219                Ok(version) => Ok(PackageVersion::SemVer(SemVer {
220                    component_count: cmp::min(text.chars().filter(|c| *c == '.').count() + 1, 3),
221                    version,
222                    specrev,
223                })),
224                Err(_) => Ok(PackageVersion::StringVer(StringVer {
225                    modrev: modrev.into(),
226                    specrev,
227                })),
228            },
229        }
230    }
231}
232
233/// The revision of a rov
234#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
235pub struct SpecRev(u16);
236
237impl From<u16> for SpecRev {
238    fn from(value: u16) -> Self {
239        Self(value)
240    }
241}
242
243impl Display for SpecRev {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        self.0.fmt(f)
246    }
247}
248
249impl Default for SpecRev {
250    fn default() -> Self {
251        Self(1)
252    }
253}
254
255/// Iterates `SpecRev`s upwards, starting from `1`.
256pub(crate) struct SpecRevIterator {
257    current: u16,
258}
259
260impl SpecRevIterator {
261    pub fn new() -> Self {
262        SpecRevIterator {
263            current: SpecRev::default().0,
264        }
265    }
266}
267
268impl Iterator for SpecRevIterator {
269    type Item = SpecRev;
270
271    fn next(&mut self) -> Option<Self::Item> {
272        let current = self.current;
273        self.current += 1;
274        if self.current == 0 {
275            None // overflow
276        } else {
277            Some(SpecRev(current))
278        }
279    }
280}
281
282// TODO: Stop deriving Eq here
283#[derive(Clone, Eq, PartialEq, Hash, Debug)]
284pub struct SemVer {
285    version: Version,
286    component_count: usize,
287    specrev: SpecRev,
288}
289
290impl HasModRev for SemVer {
291    fn to_modrev_string(&self) -> String {
292        self.version.to_string()
293    }
294}
295
296impl Display for SemVer {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        let (version_str, remainder) = split_semver_version(&self.version.to_string());
299        let mut luarocks_version_str = version_str.split('.').take(self.component_count).join(".");
300        if let Some(remainder) = remainder {
301            // luarocks allows and arbitrary number of '.' separators
302            // We treat anything after the third '.' as a semver prerelease/build version,
303            // so we have to convert it back for luarocks.
304            luarocks_version_str.push_str(&format!(".{remainder}"));
305        }
306        let str = format!("{}-{}", luarocks_version_str, self.specrev);
307        str.fmt(f)
308    }
309}
310
311fn split_semver_version(version_str: &str) -> (String, Option<String>) {
312    if let Some(pos) = version_str.rfind('-') {
313        if let Some(pre_build_str) = version_str.get(pos + 1..) {
314            (version_str[..pos].into(), Some(pre_build_str.into()))
315        } else {
316            (version_str[..pos].into(), None)
317        }
318    } else {
319        (version_str.into(), None)
320    }
321}
322
323impl Serialize for SemVer {
324    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
325    where
326        S: serde::Serializer,
327    {
328        self.to_string().serialize(serializer)
329    }
330}
331
332impl Ord for SemVer {
333    fn cmp(&self, other: &Self) -> Ordering {
334        match cmp_versions(&self.version, &other.version) {
335            Ordering::Equal => self.specrev.cmp(&other.specrev),
336            ord => ord,
337        }
338    }
339}
340
341/// Compare two LuaRocks versions.
342/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits (e.g. `2.1.0.10`),
343/// which Lux treats as a SemVer prerelease.
344fn cmp_versions(a: &Version, b: &Version) -> Ordering {
345    match (a.major, a.minor, a.patch).cmp(&(b.major, b.minor, b.patch)) {
346        Ordering::Equal => cmp_prerelease(&a.pre, &b.pre),
347        ord => ord,
348    }
349}
350
351fn cmp_prerelease(a: &Prerelease, b: &Prerelease) -> Ordering {
352    match (a.is_empty(), b.is_empty()) {
353        (true, true) => Ordering::Equal,
354        (false, true) => Ordering::Greater,
355        (true, false) => Ordering::Less,
356        (false, false) => a.cmp(b),
357    }
358}
359
360/// Based on `semver`'s matching, but with the prerelease compared as an extension
361/// for compatibility with LuaRocks.
362/// The "prereleases only match prerelease comparators" rule is not applied.
363fn comparator_matches(comparator: &Comparator, version: &Version) -> bool {
364    match comparator.op {
365        Op::Exact | Op::Wildcard => matches_exact(comparator, version),
366        Op::Greater => matches_greater(comparator, version),
367        Op::GreaterEq => matches_greater(comparator, version) || matches_exact(comparator, version),
368        Op::Less => matches_less(comparator, version),
369        Op::LessEq => matches_less(comparator, version) || matches_exact(comparator, version),
370        Op::Tilde => matches_tilde(comparator, version),
371        Op::Caret => matches_caret(comparator, version),
372        _ => false,
373    }
374}
375
376fn matches_exact(comparator: &Comparator, version: &Version) -> bool {
377    if version.major != comparator.major {
378        return false;
379    }
380    if let Some(minor) = comparator.minor {
381        if version.minor != minor {
382            return false;
383        }
384    }
385    if let Some(patch) = comparator.patch {
386        if version.patch != patch {
387            return false;
388        }
389    }
390    version.pre == comparator.pre
391}
392
393fn matches_greater(comparator: &Comparator, version: &Version) -> bool {
394    if version.major != comparator.major {
395        return version.major > comparator.major;
396    }
397    match comparator.minor {
398        None => return false,
399        Some(minor) => {
400            if version.minor != minor {
401                return version.minor > minor;
402            }
403        }
404    }
405    match comparator.patch {
406        None => return false,
407        Some(patch) => {
408            if version.patch != patch {
409                return version.patch > patch;
410            }
411        }
412    }
413    cmp_prerelease(&version.pre, &comparator.pre) == Ordering::Greater
414}
415
416fn matches_less(comparator: &Comparator, version: &Version) -> bool {
417    if version.major != comparator.major {
418        return version.major < comparator.major;
419    }
420    match comparator.minor {
421        None => return false,
422        Some(minor) => {
423            if version.minor != minor {
424                return version.minor < minor;
425            }
426        }
427    }
428    match comparator.patch {
429        None => return false,
430        Some(patch) => {
431            if version.patch != patch {
432                return version.patch < patch;
433            }
434        }
435    }
436    cmp_prerelease(&version.pre, &comparator.pre) == Ordering::Less
437}
438
439fn matches_tilde(comparator: &Comparator, version: &Version) -> bool {
440    if version.major != comparator.major {
441        return false;
442    }
443    if let Some(minor) = comparator.minor {
444        if version.minor != minor {
445            return false;
446        }
447    }
448    if let Some(patch) = comparator.patch {
449        if version.patch != patch {
450            return version.patch > patch;
451        }
452    }
453    cmp_prerelease(&version.pre, &comparator.pre) != Ordering::Less
454}
455
456fn matches_caret(comparator: &Comparator, version: &Version) -> bool {
457    if version.major != comparator.major {
458        return false;
459    }
460    let minor = match comparator.minor {
461        None => return true,
462        Some(minor) => minor,
463    };
464    let patch = match comparator.patch {
465        None => {
466            if comparator.major > 0 {
467                return version.minor >= minor;
468            } else {
469                return version.minor == minor;
470            }
471        }
472        Some(patch) => patch,
473    };
474    if comparator.major > 0 {
475        if version.minor != minor {
476            return version.minor > minor;
477        } else if version.patch != patch {
478            return version.patch > patch;
479        }
480    } else if minor > 0 {
481        if version.minor != minor {
482            return false;
483        } else if version.patch != patch {
484            return version.patch > patch;
485        }
486    } else if version.minor != minor || version.patch != patch {
487        return false;
488    }
489    cmp_prerelease(&version.pre, &comparator.pre) != Ordering::Less
490}
491
492impl PartialOrd for SemVer {
493    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
494        Some(self.cmp(other))
495    }
496}
497
498#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default)]
499#[serde(rename_all = "lowercase")]
500pub enum DevVersion {
501    #[default]
502    Dev,
503    Scm,
504}
505
506impl Display for DevVersion {
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        match self {
509            Self::Dev => "dev".fmt(f),
510            Self::Scm => "scm".fmt(f),
511        }
512    }
513}
514
515#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
516pub struct DevVer {
517    modrev: DevVersion,
518    specrev: SpecRev,
519}
520
521impl DevVer {
522    fn default_with_specrev(specrev: SpecRev) -> Self {
523        Self {
524            modrev: DevVersion::default(),
525            specrev,
526        }
527    }
528}
529
530impl HasModRev for DevVer {
531    fn to_modrev_string(&self) -> String {
532        self.modrev.to_string().to_lowercase()
533    }
534}
535
536impl Display for DevVer {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        let str = format!("{}-{}", self.modrev, self.specrev);
539        str.fmt(f)
540    }
541}
542
543impl Serialize for DevVer {
544    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
545    where
546        S: serde::Serializer,
547    {
548        self.to_string().serialize(serializer)
549    }
550}
551
552impl Ord for DevVer {
553    fn cmp(&self, other: &Self) -> Ordering {
554        // NOTE: We compare specrevs first for dev versions
555        let result = self.specrev.cmp(&other.specrev);
556        if result == Ordering::Equal {
557            return self.modrev.cmp(&other.modrev);
558        }
559        result
560    }
561}
562
563impl PartialOrd for DevVer {
564    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
565        Some(self.cmp(other))
566    }
567}
568
569#[derive(Clone, Eq, PartialEq, Hash, Debug)]
570pub struct StringVer {
571    modrev: String,
572    specrev: SpecRev,
573}
574
575impl HasModRev for StringVer {
576    fn to_modrev_string(&self) -> String {
577        self.modrev.to_string()
578    }
579}
580
581impl Display for StringVer {
582    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
583        let str = format!("{}-{}", self.modrev, self.specrev);
584        str.fmt(f)
585    }
586}
587
588impl Serialize for StringVer {
589    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
590    where
591        S: serde::Serializer,
592    {
593        self.to_string().serialize(serializer)
594    }
595}
596
597impl Ord for StringVer {
598    fn cmp(&self, other: &Self) -> Ordering {
599        // NOTE: We compare specrevs first for dev versions
600        let result = self.specrev.cmp(&other.specrev);
601        if result == Ordering::Equal {
602            return self.modrev.cmp(&other.modrev);
603        }
604        result
605    }
606}
607
608impl PartialOrd for StringVer {
609    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
610        Some(self.cmp(other))
611    }
612}
613
614pub(crate) trait HasModRev {
615    /// If a version has a modrev (and possibly a specrev),
616    /// this is equivalent to `to_string()`, but includes only the modrev.
617    fn to_modrev_string(&self) -> String;
618}
619
620#[derive(Error, Debug, Diagnostic)]
621#[non_exhaustive]
622#[error(transparent)]
623pub struct PackageVersionReqError(#[from] Error);
624
625/// **SemVer version** requirement as defined by <https://semver.org>.
626/// or a **Dev** version requirement, which can be one of "dev", "scm", or "git"
627#[derive(Clone, Eq, PartialEq, Hash, Debug)]
628pub enum PackageVersionReq {
629    /// A PackageVersionReq that matches a SemVer version.
630    SemVer(VersionReq),
631    /// A PackageVersionReq that matches only known dev versions.
632    DevVer(DevVersion),
633    /// A PackageVersionReq that matches a arbitrary string version.
634    StringVer(String),
635    /// A PackageVersionReq that has no version constraint.
636    Any,
637}
638
639impl PackageVersionReq {
640    /// Returns a `PackageVersionReq` that matches any version.
641    pub fn any() -> Self {
642        PackageVersionReq::Any
643    }
644
645    pub fn parse(text: &str) -> Result<Self, PackageVersionReqError> {
646        PackageVersionReq::from_str(text)
647    }
648
649    pub fn matches(&self, version: &PackageVersion) -> bool {
650        match (self, version) {
651            (PackageVersionReq::SemVer(req), PackageVersion::SemVer(ver)) => req
652                .comparators
653                .iter()
654                .all(|comparator| comparator_matches(comparator, &ver.version)),
655            (PackageVersionReq::DevVer(req), PackageVersion::DevVer(ver)) => req == &ver.modrev,
656            (PackageVersionReq::StringVer(req), PackageVersion::StringVer(ver)) => {
657                req == &ver.modrev
658            }
659            (PackageVersionReq::Any, _) => true,
660            _ => false,
661        }
662    }
663
664    pub fn is_any(&self) -> bool {
665        matches!(self, PackageVersionReq::Any)
666    }
667}
668
669impl Display for PackageVersionReq {
670    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671        match self {
672            PackageVersionReq::SemVer(version_req) => {
673                let mut str = version_req.to_string();
674                if str.starts_with("=") {
675                    str = str.replacen("=", "==", 1);
676                } else if str.starts_with("^") {
677                    str = str.replacen("^", "~>", 1);
678                }
679                str.fmt(f)
680            }
681            PackageVersionReq::DevVer(name_req) => write!(f, "=={}", name_req),
682            PackageVersionReq::StringVer(name_req) => write!(f, "=={}", name_req),
683            PackageVersionReq::Any => f.write_str("any"),
684        }
685    }
686}
687
688impl<'de> Deserialize<'de> for PackageVersionReq {
689    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
690    where
691        D: Deserializer<'de>,
692    {
693        String::deserialize(deserializer)?
694            .parse()
695            .map_err(serde::de::Error::custom)
696    }
697}
698
699impl FromStr for PackageVersionReq {
700    type Err = PackageVersionReqError;
701
702    fn from_str(text: &str) -> Result<Self, Self::Err> {
703        let text = correct_version_req_str(text);
704
705        let trimmed = text.trim_start_matches('=').trim_start_matches('@').trim();
706
707        match parse_version_req(&text) {
708            Ok(_) => Ok(PackageVersionReq::SemVer(parse_version_req(&text)?)),
709            Err(_) => match trimmed {
710                "scm" => Ok(PackageVersionReq::DevVer(DevVersion::Scm)),
711                "dev" => Ok(PackageVersionReq::DevVer(DevVersion::Dev)),
712                ver => Ok(PackageVersionReq::StringVer(ver.to_string())),
713            },
714        }
715    }
716}
717
718fn correct_version_req_str(text: &str) -> String {
719    text.chars()
720        .chunk_by(|t| t.is_alphanumeric() || matches!(t, '-' | '_' | '.'))
721        .into_iter()
722        .map(|(is_version_str, chars)| (is_version_str, chars.collect::<String>()))
723        .map(|(is_version_str, chunk)| {
724            if is_version_str && !is_known_dev_version_str(&chunk) {
725                let version_str = trim_specrev(&chunk);
726                correct_prerelease_version_string(version_str)
727            } else {
728                chunk
729            }
730        })
731        .collect::<String>()
732}
733
734fn trim_specrev(version_str: &str) -> &str {
735    if let Some(pos) = version_str.rfind('-') {
736        &version_str[..pos]
737    } else {
738        version_str
739    }
740}
741
742#[derive(Error, Debug, Diagnostic)]
743#[non_exhaustive]
744pub enum SpecrevParseError {
745    #[error("specrev {specrev} in version {full_version} contains non-numeric characters")]
746    InvalidSpecrev {
747        specrev: String,
748        full_version: String,
749    },
750    #[error("could not parse specrev in version {0}")]
751    InvalidVersion(String),
752}
753
754fn split_specrev(version_str: &str) -> Result<(&str, SpecRev), SpecrevParseError> {
755    if let Some(pos) = version_str.rfind('-') {
756        if let Some(specrev_str) = version_str.get(pos + 1..) {
757            if specrev_str.chars().all(|c| c.is_ascii_digit()) {
758                let specrev =
759                    specrev_str
760                        .parse::<u16>()
761                        .map_err(|_| SpecrevParseError::InvalidSpecrev {
762                            specrev: specrev_str.into(),
763                            full_version: version_str.into(),
764                        })?;
765                Ok((&version_str[..pos], specrev.into()))
766            } else {
767                Err(SpecrevParseError::InvalidSpecrev {
768                    specrev: specrev_str.into(),
769                    full_version: version_str.into(),
770                })
771            }
772        } else {
773            Err(SpecrevParseError::InvalidVersion(version_str.into()))
774        }
775    } else {
776        // We assume a specrev of 1 if none can be found.
777        Ok((version_str, 1.into()))
778    }
779}
780
781fn is_known_dev_version_str(text: &str) -> bool {
782    matches!(text, "dev" | "scm")
783}
784
785/// Parses a Version from a string, automatically supplying any missing details (i.e. missing
786/// minor/patch sections).
787fn parse_version(s: &str) -> Result<Version, Error> {
788    let version_str = correct_version_string(s);
789    Version::parse(&version_str)
790}
791
792/// Transform LuaRocks constraints into constraints that can be parsed by the semver crate.
793fn parse_version_req(version_constraints: &str) -> Result<VersionReq, Error> {
794    let unescaped = decode_html_entities(version_constraints)
795        .to_string()
796        .as_str()
797        .to_owned();
798    let transformed = match unescaped {
799        s if s.starts_with("~>") => parse_pessimistic_version_constraint(s)?,
800        s if s.starts_with("@") => format!("={}", &s[1..]),
801        // The semver crate only understands "= version", unlike luarocks which understands "== version".
802        s if s.starts_with("==") => s[1..].to_string(),
803        s if s // semver parses no constraint prefix as ^ (equivalent to ~>)
804            .find(|c: char| c.is_alphanumeric())
805            .is_some_and(|idx| idx == 0) =>
806        {
807            format!("={}", s)
808        }
809        s => s,
810    };
811
812    let version_req = VersionReq::parse(&transformed)?;
813    Ok(version_req)
814}
815
816fn parse_pessimistic_version_constraint(version_constraint: String) -> Result<String, Error> {
817    // pessimistic operator
818    let min_version_str = &version_constraint[2..].trim();
819    let min_version = Version::parse(&correct_version_string(min_version_str))?;
820
821    let max_version = match min_version_str.matches('.').count() {
822        0 => Version {
823            major: &min_version.major + 1,
824            ..min_version.clone()
825        },
826        1 => Version {
827            minor: &min_version.minor + 1,
828            ..min_version.clone()
829        },
830        _ => Version {
831            patch: &min_version.patch + 1,
832            ..min_version.clone()
833        },
834    };
835
836    Ok(format!(">= {min_version}, < {max_version}"))
837}
838
839/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits
840/// This function attempts to correct a non-semver compliant version string,
841/// by swapping the third '.' out with a '-', converting the non-semver
842/// compliant digits to a pre-release identifier.
843fn correct_version_string(version: &str) -> String {
844    let version = append_minor_patch_if_missing(version);
845    correct_prerelease_version_string(&version)
846}
847
848fn correct_prerelease_version_string(version: &str) -> String {
849    let parts: Vec<&str> = version.split('.').collect();
850    if parts.len() > 3 {
851        let corrected_version = format!(
852            "{}.{}.{}-{}",
853            parts[0],
854            parts[1],
855            parts[2],
856            parts[3..].join(".")
857        );
858        corrected_version
859    } else {
860        version.to_string()
861    }
862}
863
864/// Recursively append .0 until the version string has a minor or patch version
865fn append_minor_patch_if_missing(version: &str) -> String {
866    if version.matches('.').count() < 2 {
867        append_minor_patch_if_missing(&format!("{version}.0"))
868    } else {
869        version.to_string()
870    }
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    #[tokio::test]
878    async fn parse_semver_version() {
879        assert_eq!(
880            PackageVersion::parse("1-1").unwrap(),
881            PackageVersion::SemVer(SemVer {
882                version: "1.0.0".parse().unwrap(),
883                component_count: 1,
884                specrev: 1.into(),
885            })
886        );
887        assert_eq!(
888            PackageVersion::parse("1.0-1").unwrap(),
889            PackageVersion::SemVer(SemVer {
890                version: "1.0.0".parse().unwrap(),
891                component_count: 2,
892                specrev: 1.into(),
893            })
894        );
895        assert_eq!(
896            PackageVersion::parse("1.0.0-1").unwrap(),
897            PackageVersion::SemVer(SemVer {
898                version: "1.0.0".parse().unwrap(),
899                component_count: 3,
900                specrev: 1.into()
901            })
902        );
903        assert_eq!(
904            PackageVersion::parse("1.0.0-1").unwrap(),
905            PackageVersion::SemVer(SemVer {
906                version: "1.0.0".parse().unwrap(),
907                component_count: 3,
908                specrev: 1.into()
909            })
910        );
911        assert_eq!(
912            PackageVersion::parse("1.0.0-10-1").unwrap(),
913            PackageVersion::SemVer(SemVer {
914                version: "1.0.0-10".parse().unwrap(),
915                component_count: 3,
916                specrev: 1.into()
917            })
918        );
919        assert_eq!(
920            PackageVersion::parse("1.0.0.10-1").unwrap(),
921            PackageVersion::SemVer(SemVer {
922                version: "1.0.0-10".parse().unwrap(),
923                component_count: 3,
924                specrev: 1.into()
925            })
926        );
927        assert_eq!(
928            PackageVersion::parse("1.0.0.10.0-1").unwrap(),
929            PackageVersion::SemVer(SemVer {
930                version: "1.0.0-10.0".parse().unwrap(),
931                component_count: 3,
932                specrev: 1.into()
933            })
934        );
935    }
936
937    #[test]
938    fn four_component_versions_sort_after_base_version() {
939        let base = PackageVersion::parse("2.1.0-1").unwrap();
940        let extended = PackageVersion::parse("2.1.0.10-1").unwrap();
941        assert!(extended > base);
942        assert!(base < extended);
943        assert_eq!(
944            PackageVersion::parse("2.1.0.10-1").unwrap(),
945            PackageVersion::parse("2.1.0.10-1").unwrap()
946        );
947    }
948
949    #[test]
950    fn four_component_version_satisfies_greater_eq_req() {
951        let req = PackageVersionReq::parse(">= 2.1.0").unwrap();
952        assert!(req.matches(&PackageVersion::parse("2.1.0.10-1").unwrap()));
953        assert!(req.matches(&PackageVersion::parse("2.1.0-1").unwrap()));
954        assert!(!req.matches(&PackageVersion::parse("2.0.9-1").unwrap()));
955    }
956
957    #[tokio::test]
958    async fn parse_dev_version() {
959        assert_eq!(
960            PackageVersion::parse("dev-1").unwrap(),
961            PackageVersion::DevVer(DevVer {
962                modrev: DevVersion::Dev,
963                specrev: 1.into()
964            })
965        );
966        assert_eq!(
967            PackageVersion::parse("scm-1").unwrap(),
968            PackageVersion::DevVer(DevVer {
969                modrev: DevVersion::Scm,
970                specrev: 1.into()
971            })
972        );
973        assert_eq!(
974            PackageVersion::parse("git-1").unwrap(),
975            PackageVersion::StringVer(StringVer {
976                modrev: "git".into(),
977                specrev: 1.into()
978            })
979        );
980        assert_eq!(
981            PackageVersion::parse("scm-1").unwrap(),
982            PackageVersion::DevVer(DevVer {
983                modrev: DevVersion::Scm,
984                specrev: 1.into()
985            })
986        );
987    }
988
989    #[tokio::test]
990    async fn parse_dev_version_req() {
991        assert_eq!(
992            PackageVersionReq::parse("dev").unwrap(),
993            PackageVersionReq::DevVer(DevVersion::Dev)
994        );
995        assert_eq!(
996            PackageVersionReq::parse("scm").unwrap(),
997            PackageVersionReq::DevVer(DevVersion::Scm)
998        );
999        assert_eq!(
1000            PackageVersionReq::parse("git").unwrap(),
1001            PackageVersionReq::StringVer("git".into())
1002        );
1003        assert_eq!(
1004            PackageVersionReq::parse("==dev").unwrap(),
1005            PackageVersionReq::DevVer(DevVersion::Dev)
1006        );
1007        assert_eq!(
1008            PackageVersionReq::parse("==git").unwrap(),
1009            PackageVersionReq::StringVer("git".into())
1010        );
1011        assert_eq!(
1012            PackageVersionReq::parse("== dev").unwrap(),
1013            PackageVersionReq::DevVer(DevVersion::Dev)
1014        );
1015        assert_eq!(
1016            PackageVersionReq::parse("== scm").unwrap(),
1017            PackageVersionReq::DevVer(DevVersion::Scm)
1018        );
1019        assert_eq!(
1020            PackageVersionReq::parse("@dev").unwrap(),
1021            PackageVersionReq::DevVer(DevVersion::Dev)
1022        );
1023        assert_eq!(
1024            PackageVersionReq::parse("@git").unwrap(),
1025            PackageVersionReq::StringVer("git".into())
1026        );
1027        assert_eq!(
1028            PackageVersionReq::parse("@ dev").unwrap(),
1029            PackageVersionReq::DevVer(DevVersion::Dev)
1030        );
1031        assert_eq!(
1032            PackageVersionReq::parse("@ scm").unwrap(),
1033            PackageVersionReq::DevVer(DevVersion::Scm)
1034        );
1035        assert_eq!(
1036            PackageVersionReq::parse(">1-1,<1.2-2").unwrap(),
1037            PackageVersionReq::SemVer(">1,<1.2".parse().unwrap())
1038        );
1039        assert_eq!(
1040            PackageVersionReq::parse("> 1-1, < 1.2-2").unwrap(),
1041            PackageVersionReq::SemVer("> 1, < 1.2".parse().unwrap())
1042        );
1043        assert_eq!(
1044            PackageVersionReq::parse("> 2.1.0.10, < 2.1.1").unwrap(),
1045            PackageVersionReq::SemVer("> 2.1.0-10, < 2.1.1".parse().unwrap())
1046        );
1047    }
1048
1049    #[tokio::test]
1050    async fn package_version_req_semver_roundtrips() {
1051        let req = PackageVersionReq::parse("==0.7.1").unwrap();
1052        assert_eq!(req.to_string(), "==0.7.1");
1053
1054        let req = PackageVersionReq::parse("0.7.1").unwrap();
1055        assert_eq!(req.to_string(), "==0.7.1");
1056
1057        let req = PackageVersionReq::parse(">=0.7.1").unwrap();
1058        assert_eq!(req.to_string(), ">=0.7.1");
1059
1060        let req = PackageVersionReq::parse(">0.7.1").unwrap();
1061        assert_eq!(req.to_string(), ">0.7.1");
1062
1063        let req = PackageVersionReq::parse("<0.7.1").unwrap();
1064        assert_eq!(req.to_string(), "<0.7.1");
1065
1066        let req = PackageVersionReq::parse("~> 0.7.1").unwrap();
1067        assert_eq!(req.to_string(), ">=0.7.1, <0.7.2");
1068    }
1069
1070    #[tokio::test]
1071    async fn package_version_req_devver_roundtrips() {
1072        let req = PackageVersionReq::parse("==scm").unwrap();
1073        assert_eq!(req.to_string(), "==scm");
1074
1075        let req = PackageVersionReq::parse("@scm").unwrap();
1076        assert_eq!(req.to_string(), "==scm");
1077
1078        let req = PackageVersionReq::parse("scm").unwrap();
1079        assert_eq!(req.to_string(), "==scm");
1080
1081        let req = PackageVersionReq::parse("==a144124839f027a2d0a95791936c478d047126fc").unwrap();
1082        assert_eq!(
1083            req.to_string(),
1084            "==a144124839f027a2d0a95791936c478d047126fc"
1085        );
1086    }
1087}