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