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