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