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