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        if self
299            .version
300            .pre
301            .as_str()
302            .chars()
303            .any(|c| c.is_ascii_alphabetic())
304        {
305            // pre-release instead of specrev
306            return self.version.fmt(f);
307        }
308        let (version_str, remainder) = split_semver_version(&self.version.to_string());
309        let mut luarocks_version_str = version_str.split('.').take(self.component_count).join(".");
310        if let Some(remainder) = remainder {
311            // luarocks allows and arbitrary number of '.' separators
312            // We treat anything after the third '.' as a semver prerelease/build version,
313            // so we have to convert it back for luarocks.
314            luarocks_version_str.push_str(&format!(".{remainder}"));
315        }
316        let str = format!("{}-{}", luarocks_version_str, self.specrev);
317        str.fmt(f)
318    }
319}
320
321fn split_semver_version(version_str: &str) -> (String, Option<String>) {
322    if let Some(pos) = version_str.rfind('-') {
323        if let Some(pre_build_str) = version_str.get(pos + 1..) {
324            (version_str[..pos].into(), Some(pre_build_str.into()))
325        } else {
326            (version_str[..pos].into(), None)
327        }
328    } else {
329        (version_str.into(), None)
330    }
331}
332
333impl Serialize for SemVer {
334    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
335    where
336        S: serde::Serializer,
337    {
338        self.to_string().serialize(serializer)
339    }
340}
341
342impl Ord for SemVer {
343    fn cmp(&self, other: &Self) -> Ordering {
344        match cmp_versions(&self.version, &other.version) {
345            Ordering::Equal => self.specrev.cmp(&other.specrev),
346            ord => ord,
347        }
348    }
349}
350
351/// Compare two LuaRocks versions.
352/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits (e.g. `2.1.0.10`),
353/// which Lux treats as a SemVer prerelease.
354fn cmp_versions(a: &Version, b: &Version) -> Ordering {
355    match (a.major, a.minor, a.patch).cmp(&(b.major, b.minor, b.patch)) {
356        Ordering::Equal => cmp_prerelease(&a.pre, &b.pre),
357        ord => ord,
358    }
359}
360
361fn cmp_prerelease(a: &Prerelease, b: &Prerelease) -> Ordering {
362    match (a.is_empty(), b.is_empty()) {
363        (true, true) => Ordering::Equal,
364        (false, true) => Ordering::Greater,
365        (true, false) => Ordering::Less,
366        (false, false) => a.cmp(b),
367    }
368}
369
370/// Based on `semver`'s matching, but with the prerelease compared as an extension
371/// for compatibility with LuaRocks.
372/// The "prereleases only match prerelease comparators" rule is not applied.
373fn comparator_matches(comparator: &Comparator, version: &Version) -> bool {
374    match comparator.op {
375        Op::Exact | Op::Wildcard => matches_exact(comparator, version),
376        Op::Greater => matches_greater(comparator, version),
377        Op::GreaterEq => matches_greater(comparator, version) || matches_exact(comparator, version),
378        Op::Less => matches_less(comparator, version),
379        Op::LessEq => matches_less(comparator, version) || matches_exact(comparator, version),
380        Op::Tilde => matches_tilde(comparator, version),
381        Op::Caret => matches_caret(comparator, version),
382        _ => false,
383    }
384}
385
386fn matches_exact(comparator: &Comparator, version: &Version) -> bool {
387    if version.major != comparator.major {
388        return false;
389    }
390    if let Some(minor) = comparator.minor {
391        if version.minor != minor {
392            return false;
393        }
394    }
395    if let Some(patch) = comparator.patch {
396        if version.patch != patch {
397            return false;
398        }
399    }
400    version.pre == comparator.pre
401}
402
403fn matches_greater(comparator: &Comparator, version: &Version) -> bool {
404    if version.major != comparator.major {
405        return version.major > comparator.major;
406    }
407    match comparator.minor {
408        None => return false,
409        Some(minor) => {
410            if version.minor != minor {
411                return version.minor > minor;
412            }
413        }
414    }
415    match comparator.patch {
416        None => return false,
417        Some(patch) => {
418            if version.patch != patch {
419                return version.patch > patch;
420            }
421        }
422    }
423    cmp_prerelease(&version.pre, &comparator.pre) == Ordering::Greater
424}
425
426fn matches_less(comparator: &Comparator, version: &Version) -> bool {
427    if version.major != comparator.major {
428        return version.major < comparator.major;
429    }
430    match comparator.minor {
431        None => return false,
432        Some(minor) => {
433            if version.minor != minor {
434                return version.minor < minor;
435            }
436        }
437    }
438    match comparator.patch {
439        None => return false,
440        Some(patch) => {
441            if version.patch != patch {
442                return version.patch < patch;
443            }
444        }
445    }
446    cmp_prerelease(&version.pre, &comparator.pre) == Ordering::Less
447}
448
449fn matches_tilde(comparator: &Comparator, version: &Version) -> bool {
450    if version.major != comparator.major {
451        return false;
452    }
453    if let Some(minor) = comparator.minor {
454        if version.minor != minor {
455            return false;
456        }
457    }
458    if let Some(patch) = comparator.patch {
459        if version.patch != patch {
460            return version.patch > patch;
461        }
462    }
463    cmp_prerelease(&version.pre, &comparator.pre) != Ordering::Less
464}
465
466fn matches_caret(comparator: &Comparator, version: &Version) -> bool {
467    if version.major != comparator.major {
468        return false;
469    }
470    let minor = match comparator.minor {
471        None => return true,
472        Some(minor) => minor,
473    };
474    let patch = match comparator.patch {
475        None => {
476            if comparator.major > 0 {
477                return version.minor >= minor;
478            } else {
479                return version.minor == minor;
480            }
481        }
482        Some(patch) => patch,
483    };
484    if comparator.major > 0 {
485        if version.minor != minor {
486            return version.minor > minor;
487        } else if version.patch != patch {
488            return version.patch > patch;
489        }
490    } else if minor > 0 {
491        if version.minor != minor {
492            return false;
493        } else if version.patch != patch {
494            return version.patch > patch;
495        }
496    } else if version.minor != minor || version.patch != patch {
497        return false;
498    }
499    cmp_prerelease(&version.pre, &comparator.pre) != Ordering::Less
500}
501
502impl PartialOrd for SemVer {
503    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
504        Some(self.cmp(other))
505    }
506}
507
508#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default)]
509#[serde(rename_all = "lowercase")]
510pub enum DevVersion {
511    #[default]
512    Dev,
513    Scm,
514}
515
516impl Display for DevVersion {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        match self {
519            Self::Dev => "dev".fmt(f),
520            Self::Scm => "scm".fmt(f),
521        }
522    }
523}
524
525#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
526pub struct DevVer {
527    modrev: DevVersion,
528    specrev: SpecRev,
529}
530
531impl DevVer {
532    fn default_with_specrev(specrev: SpecRev) -> Self {
533        Self {
534            modrev: DevVersion::default(),
535            specrev,
536        }
537    }
538}
539
540impl HasModRev for DevVer {
541    fn to_modrev_string(&self) -> String {
542        self.modrev.to_string().to_lowercase()
543    }
544}
545
546impl Display for DevVer {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        let str = format!("{}-{}", self.modrev, self.specrev);
549        str.fmt(f)
550    }
551}
552
553impl Serialize for DevVer {
554    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
555    where
556        S: serde::Serializer,
557    {
558        self.to_string().serialize(serializer)
559    }
560}
561
562impl Ord for DevVer {
563    fn cmp(&self, other: &Self) -> Ordering {
564        // NOTE: We compare specrevs first for dev versions
565        let result = self.specrev.cmp(&other.specrev);
566        if result == Ordering::Equal {
567            return self.modrev.cmp(&other.modrev);
568        }
569        result
570    }
571}
572
573impl PartialOrd for DevVer {
574    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
575        Some(self.cmp(other))
576    }
577}
578
579#[derive(Clone, Eq, PartialEq, Hash, Debug)]
580pub struct StringVer {
581    modrev: String,
582    specrev: SpecRev,
583}
584
585impl HasModRev for StringVer {
586    fn to_modrev_string(&self) -> String {
587        self.modrev.to_string()
588    }
589}
590
591impl Display for StringVer {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        let str = format!("{}-{}", self.modrev, self.specrev);
594        str.fmt(f)
595    }
596}
597
598impl Serialize for StringVer {
599    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
600    where
601        S: serde::Serializer,
602    {
603        self.to_string().serialize(serializer)
604    }
605}
606
607impl Ord for StringVer {
608    fn cmp(&self, other: &Self) -> Ordering {
609        // NOTE: We compare specrevs first for dev versions
610        let result = self.specrev.cmp(&other.specrev);
611        if result == Ordering::Equal {
612            return self.modrev.cmp(&other.modrev);
613        }
614        result
615    }
616}
617
618impl PartialOrd for StringVer {
619    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
620        Some(self.cmp(other))
621    }
622}
623
624pub(crate) trait HasModRev {
625    /// If a version has a modrev (and possibly a specrev),
626    /// this is equivalent to `to_string()`, but includes only the modrev.
627    fn to_modrev_string(&self) -> String;
628}
629
630#[derive(Error, Debug, Diagnostic)]
631#[non_exhaustive]
632#[error(transparent)]
633pub struct PackageVersionReqError(#[from] Error);
634
635/// **SemVer version** requirement as defined by <https://semver.org>.
636/// or a **Dev** version requirement, which can be one of "dev", "scm", or "git"
637#[derive(Clone, Eq, PartialEq, Hash, Debug)]
638pub enum PackageVersionReq {
639    /// A PackageVersionReq that matches a SemVer version.
640    SemVer(VersionReq),
641    /// A PackageVersionReq that matches only known dev versions.
642    DevVer(DevVersion),
643    /// A PackageVersionReq that matches a arbitrary string version.
644    StringVer(String),
645    /// A PackageVersionReq that has no version constraint.
646    Any,
647}
648
649impl PackageVersionReq {
650    /// Returns a `PackageVersionReq` that matches any version.
651    pub fn any() -> Self {
652        PackageVersionReq::Any
653    }
654
655    pub fn parse(text: &str) -> Result<Self, PackageVersionReqError> {
656        PackageVersionReq::from_str(text)
657    }
658
659    pub fn matches(&self, version: &PackageVersion) -> bool {
660        match (self, version) {
661            (PackageVersionReq::SemVer(req), PackageVersion::SemVer(ver)) => req
662                .comparators
663                .iter()
664                .all(|comparator| comparator_matches(comparator, &ver.version)),
665            (PackageVersionReq::DevVer(req), PackageVersion::DevVer(ver)) => req == &ver.modrev,
666            (PackageVersionReq::StringVer(req), PackageVersion::StringVer(ver)) => {
667                req == &ver.modrev
668            }
669            (PackageVersionReq::Any, _) => true,
670            _ => false,
671        }
672    }
673
674    pub fn is_any(&self) -> bool {
675        matches!(self, PackageVersionReq::Any)
676    }
677}
678
679impl Display for PackageVersionReq {
680    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681        match self {
682            PackageVersionReq::SemVer(version_req) => {
683                let mut str = version_req.to_string();
684                if str.starts_with("=") {
685                    str = str.replacen("=", "==", 1);
686                } else if str.starts_with("^") {
687                    str = str.replacen("^", "~>", 1);
688                }
689                str.fmt(f)
690            }
691            PackageVersionReq::DevVer(name_req) => write!(f, "=={}", name_req),
692            PackageVersionReq::StringVer(name_req) => write!(f, "=={}", name_req),
693            PackageVersionReq::Any => f.write_str("any"),
694        }
695    }
696}
697
698impl<'de> Deserialize<'de> for PackageVersionReq {
699    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
700    where
701        D: Deserializer<'de>,
702    {
703        String::deserialize(deserializer)?
704            .parse()
705            .map_err(serde::de::Error::custom)
706    }
707}
708
709impl FromStr for PackageVersionReq {
710    type Err = PackageVersionReqError;
711
712    fn from_str(text: &str) -> Result<Self, Self::Err> {
713        let text = correct_version_req_str(text);
714
715        let trimmed = text.trim_start_matches('=').trim_start_matches('@').trim();
716
717        match parse_version_req(&text) {
718            Ok(_) => Ok(PackageVersionReq::SemVer(parse_version_req(&text)?)),
719            Err(_) => match trimmed {
720                "scm" => Ok(PackageVersionReq::DevVer(DevVersion::Scm)),
721                "dev" => Ok(PackageVersionReq::DevVer(DevVersion::Dev)),
722                ver => Ok(PackageVersionReq::StringVer(ver.to_string())),
723            },
724        }
725    }
726}
727
728fn correct_version_req_str(text: &str) -> String {
729    text.chars()
730        .chunk_by(|t| t.is_alphanumeric() || matches!(t, '-' | '_' | '.'))
731        .into_iter()
732        .map(|(is_version_str, chars)| (is_version_str, chars.collect::<String>()))
733        .map(|(is_version_str, chunk)| {
734            if is_version_str && !is_known_dev_version_str(&chunk) {
735                let version_str = trim_specrev(&chunk);
736                correct_prerelease_version_string(version_str)
737            } else {
738                chunk
739            }
740        })
741        .collect::<String>()
742}
743
744fn trim_specrev(version_str: &str) -> &str {
745    if let Some(pos) = version_str.rfind('-') {
746        let suffix = &version_str[pos + 1..];
747        if suffix.chars().all(|c| c.is_ascii_digit()) {
748            &version_str[..pos]
749        } else {
750            version_str
751        }
752    } else {
753        version_str
754    }
755}
756
757#[derive(Error, Debug, Diagnostic)]
758#[non_exhaustive]
759pub enum SpecrevParseError {
760    #[error("specrev {specrev} in version {full_version} contains non-numeric characters")]
761    InvalidSpecrev {
762        specrev: String,
763        full_version: String,
764    },
765    #[error("could not parse specrev in version {0}")]
766    InvalidVersion(String),
767}
768
769fn split_specrev(version_str: &str) -> Result<(&str, SpecRev), SpecrevParseError> {
770    if let Some(pos) = version_str.rfind('-') {
771        if let Some(specrev_str) = version_str.get(pos + 1..) {
772            if specrev_str.chars().all(|c| c.is_ascii_digit()) {
773                let specrev =
774                    specrev_str
775                        .parse::<u16>()
776                        .map_err(|_| SpecrevParseError::InvalidSpecrev {
777                            specrev: specrev_str.into(),
778                            full_version: version_str.into(),
779                        })?;
780                Ok((&version_str[..pos], specrev.into()))
781            } else {
782                // A non-numeric suffix is a semver prerelease, not a specrev.
783                Ok((version_str, 1.into()))
784            }
785        } else {
786            Err(SpecrevParseError::InvalidVersion(version_str.into()))
787        }
788    } else {
789        // We assume a specrev of 1 if none can be found.
790        Ok((version_str, 1.into()))
791    }
792}
793
794fn is_known_dev_version_str(text: &str) -> bool {
795    matches!(text, "dev" | "scm")
796}
797
798/// Parses a Version from a string, automatically supplying any missing details (i.e. missing
799/// minor/patch sections).
800fn parse_version(s: &str) -> Result<Version, Error> {
801    let version_str = correct_version_string(s);
802    Version::parse(&version_str)
803}
804
805/// Transform LuaRocks constraints into constraints that can be parsed by the semver crate.
806fn parse_version_req(version_constraints: &str) -> Result<VersionReq, Error> {
807    let unescaped = decode_html_entities(version_constraints)
808        .to_string()
809        .as_str()
810        .to_owned();
811    let transformed = match unescaped {
812        s if s.starts_with("~>") => parse_pessimistic_version_constraint(s)?,
813        s if s.starts_with("@") => format!("={}", &s[1..]),
814        // The semver crate only understands "= version", unlike luarocks which understands "== version".
815        s if s.starts_with("==") => s[1..].to_string(),
816        s if s // semver parses no constraint prefix as ^ (equivalent to ~>)
817            .find(|c: char| c.is_alphanumeric())
818            .is_some_and(|idx| idx == 0) =>
819        {
820            format!("={}", s)
821        }
822        s => s,
823    };
824
825    let version_req = VersionReq::parse(&transformed)?;
826    Ok(version_req)
827}
828
829fn parse_pessimistic_version_constraint(version_constraint: String) -> Result<String, Error> {
830    // pessimistic operator
831    let min_version_str = &version_constraint[2..].trim();
832    let min_version = Version::parse(&correct_version_string(min_version_str))?;
833
834    let max_version = match min_version_str.matches('.').count() {
835        0 => Version {
836            major: &min_version.major + 1,
837            ..min_version.clone()
838        },
839        1 => Version {
840            minor: &min_version.minor + 1,
841            ..min_version.clone()
842        },
843        _ => Version {
844            patch: &min_version.patch + 1,
845            ..min_version.clone()
846        },
847    };
848
849    Ok(format!(">= {min_version}, < {max_version}"))
850}
851
852/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits
853/// This function attempts to correct a non-semver compliant version string,
854/// by swapping the third '.' out with a '-', converting the non-semver
855/// compliant digits to a pre-release identifier.
856fn correct_version_string(version: &str) -> String {
857    let version = append_minor_patch_if_missing(version);
858    correct_prerelease_version_string(&version)
859}
860
861fn correct_prerelease_version_string(version: &str) -> String {
862    if version.contains('-') {
863        return version.to_string();
864    }
865    let parts: Vec<&str> = version.split('.').collect();
866    if parts.len() > 3 {
867        format!(
868            "{}.{}.{}-{}",
869            parts[0],
870            parts[1],
871            parts[2],
872            parts[3..].join(".")
873        )
874    } else {
875        version.to_string()
876    }
877}
878
879/// Recursively append .0 until the version string has a minor or patch version
880fn append_minor_patch_if_missing(version: &str) -> String {
881    if version.matches('.').count() < 2 {
882        append_minor_patch_if_missing(&format!("{version}.0"))
883    } else {
884        version.to_string()
885    }
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    #[tokio::test]
893    async fn parse_semver_version() {
894        assert_eq!(
895            PackageVersion::parse("1-1").unwrap(),
896            PackageVersion::SemVer(SemVer {
897                version: "1.0.0".parse().unwrap(),
898                component_count: 1,
899                specrev: 1.into(),
900            })
901        );
902        assert_eq!(
903            PackageVersion::parse("1.0-1").unwrap(),
904            PackageVersion::SemVer(SemVer {
905                version: "1.0.0".parse().unwrap(),
906                component_count: 2,
907                specrev: 1.into(),
908            })
909        );
910        assert_eq!(
911            PackageVersion::parse("1.0.0-1").unwrap(),
912            PackageVersion::SemVer(SemVer {
913                version: "1.0.0".parse().unwrap(),
914                component_count: 3,
915                specrev: 1.into()
916            })
917        );
918        assert_eq!(
919            PackageVersion::parse("1.0.0-1").unwrap(),
920            PackageVersion::SemVer(SemVer {
921                version: "1.0.0".parse().unwrap(),
922                component_count: 3,
923                specrev: 1.into()
924            })
925        );
926        assert_eq!(
927            PackageVersion::parse("1.0.0-10-1").unwrap(),
928            PackageVersion::SemVer(SemVer {
929                version: "1.0.0-10".parse().unwrap(),
930                component_count: 3,
931                specrev: 1.into()
932            })
933        );
934        assert_eq!(
935            PackageVersion::parse("1.0.0.10-1").unwrap(),
936            PackageVersion::SemVer(SemVer {
937                version: "1.0.0-10".parse().unwrap(),
938                component_count: 3,
939                specrev: 1.into()
940            })
941        );
942        assert_eq!(
943            PackageVersion::parse("1.0.0.10.0-1").unwrap(),
944            PackageVersion::SemVer(SemVer {
945                version: "1.0.0-10.0".parse().unwrap(),
946                component_count: 3,
947                specrev: 1.into()
948            })
949        );
950    }
951
952    #[test]
953    fn four_component_versions_sort_after_base_version() {
954        let base = PackageVersion::parse("2.1.0-1").unwrap();
955        let extended = PackageVersion::parse("2.1.0.10-1").unwrap();
956        assert!(extended > base);
957        assert!(base < extended);
958        assert_eq!(
959            PackageVersion::parse("2.1.0.10-1").unwrap(),
960            PackageVersion::parse("2.1.0.10-1").unwrap()
961        );
962    }
963
964    #[test]
965    fn four_component_version_satisfies_greater_eq_req() {
966        let req = PackageVersionReq::parse(">= 2.1.0").unwrap();
967        assert!(req.matches(&PackageVersion::parse("2.1.0.10-1").unwrap()));
968        assert!(req.matches(&PackageVersion::parse("2.1.0-1").unwrap()));
969        assert!(!req.matches(&PackageVersion::parse("2.0.9-1").unwrap()));
970    }
971
972    #[test]
973    fn parse_semver_prerelease() {
974        let prerelease = PackageVersion::parse("1.2.3-alpha.1").unwrap();
975        assert_eq!(
976            prerelease,
977            PackageVersion::SemVer(SemVer {
978                version: "1.2.3-alpha.1".parse().unwrap(),
979                component_count: 3,
980                specrev: 1.into()
981            })
982        );
983        assert_eq!(prerelease.to_string(), "1.2.3-alpha.1");
984        assert_eq!(
985            PackageVersion::parse("1.2.3-1").unwrap().to_string(),
986            "1.2.3-1"
987        );
988        assert_eq!(
989            PackageVersion::parse(&prerelease.to_string()).unwrap(),
990            prerelease
991        );
992    }
993
994    #[test]
995    fn prerelease_req_matches() {
996        let req = PackageVersionReq::parse("=1.2.3-alpha.1").unwrap();
997        assert!(req.matches(&PackageVersion::parse("1.2.3-alpha.1").unwrap()));
998        assert!(!req.matches(&PackageVersion::parse("1.2.3-alpha.2").unwrap()));
999    }
1000
1001    #[tokio::test]
1002    async fn parse_dev_version() {
1003        assert_eq!(
1004            PackageVersion::parse("dev-1").unwrap(),
1005            PackageVersion::DevVer(DevVer {
1006                modrev: DevVersion::Dev,
1007                specrev: 1.into()
1008            })
1009        );
1010        assert_eq!(
1011            PackageVersion::parse("scm-1").unwrap(),
1012            PackageVersion::DevVer(DevVer {
1013                modrev: DevVersion::Scm,
1014                specrev: 1.into()
1015            })
1016        );
1017        assert_eq!(
1018            PackageVersion::parse("git-1").unwrap(),
1019            PackageVersion::StringVer(StringVer {
1020                modrev: "git".into(),
1021                specrev: 1.into()
1022            })
1023        );
1024        assert_eq!(
1025            PackageVersion::parse("scm-1").unwrap(),
1026            PackageVersion::DevVer(DevVer {
1027                modrev: DevVersion::Scm,
1028                specrev: 1.into()
1029            })
1030        );
1031    }
1032
1033    #[tokio::test]
1034    async fn parse_dev_version_req() {
1035        assert_eq!(
1036            PackageVersionReq::parse("dev").unwrap(),
1037            PackageVersionReq::DevVer(DevVersion::Dev)
1038        );
1039        assert_eq!(
1040            PackageVersionReq::parse("scm").unwrap(),
1041            PackageVersionReq::DevVer(DevVersion::Scm)
1042        );
1043        assert_eq!(
1044            PackageVersionReq::parse("git").unwrap(),
1045            PackageVersionReq::StringVer("git".into())
1046        );
1047        assert_eq!(
1048            PackageVersionReq::parse("==dev").unwrap(),
1049            PackageVersionReq::DevVer(DevVersion::Dev)
1050        );
1051        assert_eq!(
1052            PackageVersionReq::parse("==git").unwrap(),
1053            PackageVersionReq::StringVer("git".into())
1054        );
1055        assert_eq!(
1056            PackageVersionReq::parse("== dev").unwrap(),
1057            PackageVersionReq::DevVer(DevVersion::Dev)
1058        );
1059        assert_eq!(
1060            PackageVersionReq::parse("== scm").unwrap(),
1061            PackageVersionReq::DevVer(DevVersion::Scm)
1062        );
1063        assert_eq!(
1064            PackageVersionReq::parse("@dev").unwrap(),
1065            PackageVersionReq::DevVer(DevVersion::Dev)
1066        );
1067        assert_eq!(
1068            PackageVersionReq::parse("@git").unwrap(),
1069            PackageVersionReq::StringVer("git".into())
1070        );
1071        assert_eq!(
1072            PackageVersionReq::parse("@ dev").unwrap(),
1073            PackageVersionReq::DevVer(DevVersion::Dev)
1074        );
1075        assert_eq!(
1076            PackageVersionReq::parse("@ scm").unwrap(),
1077            PackageVersionReq::DevVer(DevVersion::Scm)
1078        );
1079        assert_eq!(
1080            PackageVersionReq::parse(">1-1,<1.2-2").unwrap(),
1081            PackageVersionReq::SemVer(">1,<1.2".parse().unwrap())
1082        );
1083        assert_eq!(
1084            PackageVersionReq::parse("> 1-1, < 1.2-2").unwrap(),
1085            PackageVersionReq::SemVer("> 1, < 1.2".parse().unwrap())
1086        );
1087        assert_eq!(
1088            PackageVersionReq::parse("> 2.1.0.10, < 2.1.1").unwrap(),
1089            PackageVersionReq::SemVer("> 2.1.0-10, < 2.1.1".parse().unwrap())
1090        );
1091    }
1092
1093    #[tokio::test]
1094    async fn package_version_req_semver_roundtrips() {
1095        let req = PackageVersionReq::parse("==0.7.1").unwrap();
1096        assert_eq!(req.to_string(), "==0.7.1");
1097
1098        let req = PackageVersionReq::parse("0.7.1").unwrap();
1099        assert_eq!(req.to_string(), "==0.7.1");
1100
1101        let req = PackageVersionReq::parse(">=0.7.1").unwrap();
1102        assert_eq!(req.to_string(), ">=0.7.1");
1103
1104        let req = PackageVersionReq::parse(">0.7.1").unwrap();
1105        assert_eq!(req.to_string(), ">0.7.1");
1106
1107        let req = PackageVersionReq::parse("<0.7.1").unwrap();
1108        assert_eq!(req.to_string(), "<0.7.1");
1109
1110        let req = PackageVersionReq::parse("~> 0.7.1").unwrap();
1111        assert_eq!(req.to_string(), ">=0.7.1, <0.7.2");
1112    }
1113
1114    #[tokio::test]
1115    async fn package_version_req_devver_roundtrips() {
1116        let req = PackageVersionReq::parse("==scm").unwrap();
1117        assert_eq!(req.to_string(), "==scm");
1118
1119        let req = PackageVersionReq::parse("@scm").unwrap();
1120        assert_eq!(req.to_string(), "==scm");
1121
1122        let req = PackageVersionReq::parse("scm").unwrap();
1123        assert_eq!(req.to_string(), "==scm");
1124
1125        let req = PackageVersionReq::parse("==a144124839f027a2d0a95791936c478d047126fc").unwrap();
1126        assert_eq!(
1127            req.to_string(),
1128            "==a144124839f027a2d0a95791936c478d047126fc"
1129        );
1130    }
1131}