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;
9use mlua::{ExternalResult, FromLua, IntoLua};
10use nonempty::NonEmpty;
11use semver::{Comparator, Error, Op, Version, VersionReq};
12use serde::{de, Deserialize, Deserializer, Serialize};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum VersionReqToVersionError {
17    #[error("cannot parse version from non-exact version requirement '{0}'")]
18    NonExactVersionReq(VersionReq),
19    #[error("cannot parse version from version requirement '*' (any version)")]
20    Any,
21}
22
23#[derive(Clone, Eq, PartialEq, Hash, Debug)]
24pub enum PackageVersion {
25    /// **SemVer version** as defined by <https://semver.org>,
26    /// but a bit more lenient for compatibility with luarocks
27    SemVer(SemVer),
28    /// A known **Dev** version
29    DevVer(DevVer),
30    /// An arbitrary string version.
31    /// Yes, luarocks-site allows arbitrary string versions in the root manifest
32    /// ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
33    StringVer(StringVer),
34}
35
36impl HasModRev for PackageVersion {
37    fn to_modrev_string(&self) -> String {
38        match self {
39            Self::SemVer(ver) => ver.to_modrev_string(),
40            Self::DevVer(ver) => ver.to_modrev_string(),
41            Self::StringVer(ver) => ver.to_modrev_string(),
42        }
43    }
44}
45
46impl PackageVersion {
47    pub fn parse(text: &str) -> Result<Self, PackageVersionParseError> {
48        PackageVersion::from_str(text)
49    }
50    /// Note that this loses the specrev information.
51    pub fn into_version_req(&self) -> PackageVersionReq {
52        match self {
53            PackageVersion::DevVer(DevVer { modrev, .. }) => {
54                PackageVersionReq::DevVer(modrev.to_owned())
55            }
56            PackageVersion::StringVer(StringVer { modrev, .. }) => {
57                PackageVersionReq::StringVer(modrev.to_owned())
58            }
59            PackageVersion::SemVer(SemVer { version, .. }) => {
60                let version = version.to_owned();
61                PackageVersionReq::SemVer(VersionReq {
62                    comparators: vec![Comparator {
63                        op: Op::Exact,
64                        major: version.major,
65                        minor: Some(version.minor),
66                        patch: Some(version.patch),
67                        pre: version.pre,
68                    }],
69                })
70            }
71        }
72    }
73
74    pub(crate) fn is_semver(&self) -> bool {
75        matches!(self, PackageVersion::SemVer(_))
76    }
77
78    pub(crate) fn default_dev_version() -> Self {
79        Self::DevVer(DevVer::default())
80    }
81
82    pub(crate) fn default_dev_version_with_specrev(specrev: SpecRev) -> Self {
83        Self::DevVer(DevVer::default_with_specrev(specrev))
84    }
85}
86
87impl TryFrom<PackageVersionReq> for PackageVersion {
88    type Error = VersionReqToVersionError;
89
90    fn try_from(req: PackageVersionReq) -> Result<Self, Self::Error> {
91        match req {
92            PackageVersionReq::SemVer(version_req) => {
93                match NonEmpty::try_from(version_req.comparators.clone()) {
94                    Ok(comparators)
95                        if comparators
96                            .iter()
97                            .any(|comparator| comparator.op != semver::Op::Exact) =>
98                    {
99                        Err(VersionReqToVersionError::NonExactVersionReq(version_req))
100                    }
101                    Ok(comparators) => {
102                        let comparator = comparators.first();
103                        let version = semver::Version {
104                            major: comparator.major,
105                            minor: comparator.minor.unwrap_or(0),
106                            patch: comparator.patch.unwrap_or(0),
107                            pre: comparator.pre.clone(),
108                            build: semver::BuildMetadata::EMPTY,
109                        };
110                        let component_count = if comparator.patch.is_some() {
111                            3
112                        } else if comparator.minor.is_some() {
113                            2
114                        } else {
115                            1
116                        };
117                        Ok(PackageVersion::SemVer(SemVer {
118                            version,
119                            component_count,
120                            specrev: 1.into(),
121                        }))
122                    }
123                    Err(_) => Err(VersionReqToVersionError::NonExactVersionReq(version_req)),
124                }
125            }
126            PackageVersionReq::DevVer(modrev) => Ok(PackageVersion::DevVer(DevVer {
127                modrev,
128                specrev: 1.into(),
129            })),
130            PackageVersionReq::StringVer(modrev) => Ok(PackageVersion::StringVer(StringVer {
131                modrev,
132                specrev: 1.into(),
133            })),
134            PackageVersionReq::Any => Err(VersionReqToVersionError::Any),
135        }
136    }
137}
138
139impl IntoLua for PackageVersion {
140    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
141        self.to_string().into_lua(lua)
142    }
143}
144
145#[derive(Error, Debug)]
146pub enum PackageVersionParseError {
147    #[error(transparent)]
148    Specrev(#[from] SpecrevParseError),
149    #[error("failed to parse version: {0}")]
150    Version(#[from] Error),
151}
152
153impl Serialize for PackageVersion {
154    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
155    where
156        S: serde::Serializer,
157    {
158        match self {
159            PackageVersion::SemVer(version) => version.serialize(serializer),
160            PackageVersion::DevVer(version) => version.serialize(serializer),
161            PackageVersion::StringVer(version) => version.serialize(serializer),
162        }
163    }
164}
165
166impl<'de> Deserialize<'de> for PackageVersion {
167    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
168    where
169        D: Deserializer<'de>,
170    {
171        let s = String::deserialize(deserializer)?;
172        Self::from_str(&s).map_err(de::Error::custom)
173    }
174}
175
176impl FromLua for PackageVersion {
177    fn from_lua(
178        value: mlua::prelude::LuaValue,
179        lua: &mlua::prelude::Lua,
180    ) -> mlua::prelude::LuaResult<Self> {
181        let s = String::from_lua(value, lua)?;
182        Self::from_str(&s).map_err(|err| mlua::Error::DeserializeError(err.to_string()))
183    }
184}
185
186impl Display for PackageVersion {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            PackageVersion::SemVer(version) => version.fmt(f),
190            PackageVersion::DevVer(version) => version.fmt(f),
191            PackageVersion::StringVer(version) => version.fmt(f),
192        }
193    }
194}
195
196impl PartialOrd for PackageVersion {
197    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
198        Some(self.cmp(other))
199    }
200}
201
202impl Ord for PackageVersion {
203    fn cmp(&self, other: &Self) -> Ordering {
204        match (self, other) {
205            (PackageVersion::SemVer(a), PackageVersion::SemVer(b)) => a.cmp(b),
206            (PackageVersion::SemVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
207            (PackageVersion::SemVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
208            (PackageVersion::DevVer(..), PackageVersion::SemVer(..)) => Ordering::Greater,
209            (PackageVersion::DevVer(a), PackageVersion::DevVer(b)) => a.cmp(b),
210            (PackageVersion::DevVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
211            (PackageVersion::StringVer(a), PackageVersion::StringVer(b)) => a.cmp(b),
212            (PackageVersion::StringVer(..), PackageVersion::SemVer(..)) => Ordering::Less,
213            (PackageVersion::StringVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
214        }
215    }
216}
217
218impl FromStr for PackageVersion {
219    type Err = PackageVersionParseError;
220
221    fn from_str(text: &str) -> Result<Self, Self::Err> {
222        let (modrev, specrev) = split_specrev(text)?;
223        match modrev {
224            "scm" => Ok(PackageVersion::DevVer(DevVer {
225                modrev: DevVersion::Scm,
226                specrev,
227            })),
228            "dev" => Ok(PackageVersion::DevVer(DevVer {
229                modrev: DevVersion::Dev,
230                specrev,
231            })),
232            modrev => match parse_version(modrev) {
233                Ok(version) => Ok(PackageVersion::SemVer(SemVer {
234                    component_count: cmp::min(text.chars().filter(|c| *c == '.').count() + 1, 3),
235                    version,
236                    specrev,
237                })),
238                Err(_) => Ok(PackageVersion::StringVer(StringVer {
239                    modrev: modrev.into(),
240                    specrev,
241                })),
242            },
243        }
244    }
245}
246
247/// The revision of a rov
248#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
249pub struct SpecRev(u16);
250
251impl From<u16> for SpecRev {
252    fn from(value: u16) -> Self {
253        Self(value)
254    }
255}
256
257impl Display for SpecRev {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        self.0.fmt(f)
260    }
261}
262
263impl Default for SpecRev {
264    fn default() -> Self {
265        Self(1)
266    }
267}
268
269impl FromLua for SpecRev {
270    fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
271        match value {
272            mlua::Value::Integer(value) => {
273                let value = u16::try_from(value).map_err(|err| {
274                    mlua::Error::DeserializeError(format!(
275                        "Error deserializing specrev {value}:\n{err}"
276                    ))
277                })?;
278                Ok(Self(value))
279            }
280            value => Err(mlua::Error::DeserializeError(format!(
281                "Expected specrev to be an integer, but got {}",
282                value.type_name()
283            ))),
284        }
285    }
286}
287
288/// Iterates `SpecRev`s upwards, starting from `1`.
289pub(crate) struct SpecRevIterator {
290    current: u16,
291}
292
293impl SpecRevIterator {
294    pub fn new() -> Self {
295        SpecRevIterator {
296            current: SpecRev::default().0,
297        }
298    }
299}
300
301impl Iterator for SpecRevIterator {
302    type Item = SpecRev;
303
304    fn next(&mut self) -> Option<Self::Item> {
305        self.current += 1;
306        if self.current == 0 {
307            None // overflow
308        } else {
309            Some(SpecRev(self.current))
310        }
311    }
312}
313
314// TODO: Stop deriving Eq here
315#[derive(Clone, Eq, PartialEq, Hash, Debug)]
316pub struct SemVer {
317    version: Version,
318    component_count: usize,
319    specrev: SpecRev,
320}
321
322impl HasModRev for SemVer {
323    fn to_modrev_string(&self) -> String {
324        self.version.to_string()
325    }
326}
327
328impl Display for SemVer {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        let (version_str, remainder) = split_semver_version(&self.version.to_string());
331        let mut luarocks_version_str = version_str.split('.').take(self.component_count).join(".");
332        if let Some(remainder) = remainder {
333            // luarocks allows and arbitrary number of '.' separators
334            // We treat anything after the third '.' as a semver prerelease/build version,
335            // so we have to convert it back for luarocks.
336            luarocks_version_str.push_str(&format!(".{remainder}"));
337        }
338        let str = format!("{}-{}", luarocks_version_str, self.specrev);
339        str.fmt(f)
340    }
341}
342
343fn split_semver_version(version_str: &str) -> (String, Option<String>) {
344    if let Some(pos) = version_str.rfind('-') {
345        if let Some(pre_build_str) = version_str.get(pos + 1..) {
346            (version_str[..pos].into(), Some(pre_build_str.into()))
347        } else {
348            (version_str[..pos].into(), None)
349        }
350    } else {
351        (version_str.into(), None)
352    }
353}
354
355impl Serialize for SemVer {
356    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
357    where
358        S: serde::Serializer,
359    {
360        self.to_string().serialize(serializer)
361    }
362}
363
364impl Ord for SemVer {
365    fn cmp(&self, other: &Self) -> Ordering {
366        let result = self.version.cmp(&other.version);
367        if result == Ordering::Equal {
368            return self.specrev.cmp(&other.specrev);
369        }
370        result
371    }
372}
373
374impl PartialOrd for SemVer {
375    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
376        Some(self.cmp(other))
377    }
378}
379
380#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default)]
381#[serde(rename_all = "lowercase")]
382pub enum DevVersion {
383    #[default]
384    Dev,
385    Scm,
386}
387
388impl Display for DevVersion {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        match self {
391            Self::Dev => "dev".fmt(f),
392            Self::Scm => "scm".fmt(f),
393        }
394    }
395}
396
397impl IntoLua for DevVersion {
398    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
399        self.to_string().into_lua(lua)
400    }
401}
402
403#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
404pub struct DevVer {
405    modrev: DevVersion,
406    specrev: SpecRev,
407}
408
409impl DevVer {
410    fn default_with_specrev(specrev: SpecRev) -> Self {
411        Self {
412            modrev: DevVersion::default(),
413            specrev,
414        }
415    }
416}
417
418impl HasModRev for DevVer {
419    fn to_modrev_string(&self) -> String {
420        self.modrev.to_string().to_lowercase()
421    }
422}
423
424impl Display for DevVer {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        let str = format!("{}-{}", self.modrev, self.specrev);
427        str.fmt(f)
428    }
429}
430
431impl Serialize for DevVer {
432    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
433    where
434        S: serde::Serializer,
435    {
436        self.to_string().serialize(serializer)
437    }
438}
439
440impl Ord for DevVer {
441    fn cmp(&self, other: &Self) -> Ordering {
442        // NOTE: We compare specrevs first for dev versions
443        let result = self.specrev.cmp(&other.specrev);
444        if result == Ordering::Equal {
445            return self.modrev.cmp(&other.modrev);
446        }
447        result
448    }
449}
450
451impl PartialOrd for DevVer {
452    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
453        Some(self.cmp(other))
454    }
455}
456
457#[derive(Clone, Eq, PartialEq, Hash, Debug)]
458pub struct StringVer {
459    modrev: String,
460    specrev: SpecRev,
461}
462
463impl HasModRev for StringVer {
464    fn to_modrev_string(&self) -> String {
465        self.modrev.to_string()
466    }
467}
468
469impl Display for StringVer {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        let str = format!("{}-{}", self.modrev, self.specrev);
472        str.fmt(f)
473    }
474}
475
476impl Serialize for StringVer {
477    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
478    where
479        S: serde::Serializer,
480    {
481        self.to_string().serialize(serializer)
482    }
483}
484
485impl Ord for StringVer {
486    fn cmp(&self, other: &Self) -> Ordering {
487        // NOTE: We compare specrevs first for dev versions
488        let result = self.specrev.cmp(&other.specrev);
489        if result == Ordering::Equal {
490            return self.modrev.cmp(&other.modrev);
491        }
492        result
493    }
494}
495
496impl PartialOrd for StringVer {
497    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
498        Some(self.cmp(other))
499    }
500}
501
502pub(crate) trait HasModRev {
503    /// If a version has a modrev (and possibly a specrev),
504    /// this is equivalent to `to_string()`, but includes only the modrev.
505    fn to_modrev_string(&self) -> String;
506}
507
508#[derive(Error, Debug)]
509#[error(transparent)]
510pub struct PackageVersionReqError(#[from] Error);
511
512/// **SemVer version** requirement as defined by <https://semver.org>.
513/// or a **Dev** version requirement, which can be one of "dev", "scm", or "git"
514#[derive(Clone, Eq, PartialEq, Hash, Debug)]
515pub enum PackageVersionReq {
516    /// A PackageVersionReq that matches a SemVer version.
517    SemVer(VersionReq),
518    /// A PackageVersionReq that matches only known dev versions.
519    DevVer(DevVersion),
520    /// A PackageVersionReq that matches a arbitrary string version.
521    StringVer(String),
522    /// A PackageVersionReq that has no version constraint.
523    Any,
524}
525
526impl FromLua for PackageVersionReq {
527    fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
528        PackageVersionReq::parse(&String::from_lua(value, lua)?).into_lua_err()
529    }
530}
531
532impl IntoLua for PackageVersionReq {
533    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
534        let table = lua.create_table()?;
535
536        match self {
537            PackageVersionReq::SemVer(version_req) => {
538                table.set("semver", version_req.to_string())?
539            }
540            PackageVersionReq::DevVer(dev) => table.set("dev", dev)?,
541            PackageVersionReq::StringVer(dev) => table.set("stringver", dev)?,
542            PackageVersionReq::Any => table.set("any", true)?,
543        }
544
545        Ok(mlua::Value::Table(table))
546    }
547}
548
549impl PackageVersionReq {
550    /// Returns a `PackageVersionReq` that matches any version.
551    pub fn any() -> Self {
552        PackageVersionReq::Any
553    }
554
555    pub fn parse(text: &str) -> Result<Self, PackageVersionReqError> {
556        PackageVersionReq::from_str(text)
557    }
558
559    pub fn matches(&self, version: &PackageVersion) -> bool {
560        match (self, version) {
561            (PackageVersionReq::SemVer(req), PackageVersion::SemVer(ver)) => {
562                req.matches(&ver.version)
563            }
564            (PackageVersionReq::DevVer(req), PackageVersion::DevVer(ver)) => req == &ver.modrev,
565            (PackageVersionReq::StringVer(req), PackageVersion::StringVer(ver)) => {
566                req == &ver.modrev
567            }
568            (PackageVersionReq::Any, _) => true,
569            _ => false,
570        }
571    }
572
573    pub fn is_any(&self) -> bool {
574        matches!(self, PackageVersionReq::Any)
575    }
576}
577
578impl Display for PackageVersionReq {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        match self {
581            PackageVersionReq::SemVer(version_req) => {
582                let mut str = version_req.to_string();
583                if str.starts_with("=") {
584                    str = str.replacen("=", "==", 1);
585                } else if str.starts_with("^") {
586                    str = str.replacen("^", "~>", 1);
587                }
588                str.fmt(f)
589            }
590            PackageVersionReq::DevVer(name_req) => write!(f, "=={}", &name_req),
591            PackageVersionReq::StringVer(name_req) => write!(f, "=={}", &name_req),
592            PackageVersionReq::Any => f.write_str("any"),
593        }
594    }
595}
596
597impl<'de> Deserialize<'de> for PackageVersionReq {
598    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
599    where
600        D: Deserializer<'de>,
601    {
602        String::deserialize(deserializer)?
603            .parse()
604            .map_err(serde::de::Error::custom)
605    }
606}
607
608impl FromStr for PackageVersionReq {
609    type Err = PackageVersionReqError;
610
611    fn from_str(text: &str) -> Result<Self, Self::Err> {
612        let text = correct_version_req_str(text);
613
614        let trimmed = text.trim_start_matches('=').trim_start_matches('@').trim();
615
616        match parse_version_req(&text) {
617            Ok(_) => Ok(PackageVersionReq::SemVer(parse_version_req(&text)?)),
618            Err(_) => match trimmed {
619                "scm" => Ok(PackageVersionReq::DevVer(DevVersion::Scm)),
620                "dev" => Ok(PackageVersionReq::DevVer(DevVersion::Dev)),
621                ver => Ok(PackageVersionReq::StringVer(ver.to_string())),
622            },
623        }
624    }
625}
626
627fn correct_version_req_str(text: &str) -> String {
628    text.chars()
629        .chunk_by(|t| t.is_alphanumeric() || matches!(t, '-' | '_' | '.'))
630        .into_iter()
631        .map(|(is_version_str, chars)| (is_version_str, chars.collect::<String>()))
632        .map(|(is_version_str, chunk)| {
633            if is_version_str && !is_known_dev_version_str(&chunk) {
634                let version_str = trim_specrev(&chunk);
635                correct_prerelease_version_string(version_str)
636            } else {
637                chunk
638            }
639        })
640        .collect::<String>()
641}
642
643fn trim_specrev(version_str: &str) -> &str {
644    if let Some(pos) = version_str.rfind('-') {
645        &version_str[..pos]
646    } else {
647        version_str
648    }
649}
650
651#[derive(Error, Debug)]
652pub enum SpecrevParseError {
653    #[error("specrev {specrev} in version {full_version} contains non-numeric characters")]
654    InvalidSpecrev {
655        specrev: String,
656        full_version: String,
657    },
658    #[error("could not parse specrev in version {0}")]
659    InvalidVersion(String),
660}
661
662fn split_specrev(version_str: &str) -> Result<(&str, SpecRev), SpecrevParseError> {
663    if let Some(pos) = version_str.rfind('-') {
664        if let Some(specrev_str) = version_str.get(pos + 1..) {
665            if specrev_str.chars().all(|c| c.is_ascii_digit()) {
666                let specrev =
667                    specrev_str
668                        .parse::<u16>()
669                        .map_err(|_| SpecrevParseError::InvalidSpecrev {
670                            specrev: specrev_str.into(),
671                            full_version: version_str.into(),
672                        })?;
673                Ok((&version_str[..pos], specrev.into()))
674            } else {
675                Err(SpecrevParseError::InvalidSpecrev {
676                    specrev: specrev_str.into(),
677                    full_version: version_str.into(),
678                })
679            }
680        } else {
681            Err(SpecrevParseError::InvalidVersion(version_str.into()))
682        }
683    } else {
684        // We assume a specrev of 1 if none can be found.
685        Ok((version_str, 1.into()))
686    }
687}
688
689fn is_known_dev_version_str(text: &str) -> bool {
690    matches!(text, "dev" | "scm")
691}
692
693/// Parses a Version from a string, automatically supplying any missing details (i.e. missing
694/// minor/patch sections).
695fn parse_version(s: &str) -> Result<Version, Error> {
696    let version_str = correct_version_string(s);
697    Version::parse(&version_str)
698}
699
700/// Transform LuaRocks constraints into constraints that can be parsed by the semver crate.
701fn parse_version_req(version_constraints: &str) -> Result<VersionReq, Error> {
702    let unescaped = decode_html_entities(version_constraints)
703        .to_string()
704        .as_str()
705        .to_owned();
706    let transformed = match unescaped {
707        s if s.starts_with("~>") => parse_pessimistic_version_constraint(s)?,
708        s if s.starts_with("@") => format!("={}", &s[1..]),
709        // The semver crate only understands "= version", unlike luarocks which understands "== version".
710        s if s.starts_with("==") => s[1..].to_string(),
711        s if s // semver parses no constraint prefix as ^ (equivalent to ~>)
712            .find(|c: char| c.is_alphanumeric())
713            .is_some_and(|idx| idx == 0) =>
714        {
715            format!("={}", &s)
716        }
717        s => s,
718    };
719
720    let version_req = VersionReq::parse(&transformed)?;
721    Ok(version_req)
722}
723
724fn parse_pessimistic_version_constraint(version_constraint: String) -> Result<String, Error> {
725    // pessimistic operator
726    let min_version_str = &version_constraint[2..].trim();
727    let min_version = Version::parse(&correct_version_string(min_version_str))?;
728
729    let max_version = match min_version_str.matches('.').count() {
730        0 => Version {
731            major: &min_version.major + 1,
732            ..min_version.clone()
733        },
734        1 => Version {
735            minor: &min_version.minor + 1,
736            ..min_version.clone()
737        },
738        _ => Version {
739            patch: &min_version.patch + 1,
740            ..min_version.clone()
741        },
742    };
743
744    Ok(format!(">= {min_version}, < {max_version}"))
745}
746
747/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits
748/// This function attempts to correct a non-semver compliant version string,
749/// by swapping the third '.' out with a '-', converting the non-semver
750/// compliant digits to a pre-release identifier.
751fn correct_version_string(version: &str) -> String {
752    let version = append_minor_patch_if_missing(version);
753    correct_prerelease_version_string(&version)
754}
755
756fn correct_prerelease_version_string(version: &str) -> String {
757    let parts: Vec<&str> = version.split('.').collect();
758    if parts.len() > 3 {
759        let corrected_version = format!(
760            "{}.{}.{}-{}",
761            parts[0],
762            parts[1],
763            parts[2],
764            parts[3..].join(".")
765        );
766        corrected_version
767    } else {
768        version.to_string()
769    }
770}
771
772/// Recursively append .0 until the version string has a minor or patch version
773fn append_minor_patch_if_missing(version: &str) -> String {
774    if version.matches('.').count() < 2 {
775        append_minor_patch_if_missing(&format!("{version}.0"))
776    } else {
777        version.to_string()
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784
785    #[tokio::test]
786    async fn parse_semver_version() {
787        assert_eq!(
788            PackageVersion::parse("1-1").unwrap(),
789            PackageVersion::SemVer(SemVer {
790                version: "1.0.0".parse().unwrap(),
791                component_count: 1,
792                specrev: 1.into(),
793            })
794        );
795        assert_eq!(
796            PackageVersion::parse("1.0-1").unwrap(),
797            PackageVersion::SemVer(SemVer {
798                version: "1.0.0".parse().unwrap(),
799                component_count: 2,
800                specrev: 1.into(),
801            })
802        );
803        assert_eq!(
804            PackageVersion::parse("1.0.0-1").unwrap(),
805            PackageVersion::SemVer(SemVer {
806                version: "1.0.0".parse().unwrap(),
807                component_count: 3,
808                specrev: 1.into()
809            })
810        );
811        assert_eq!(
812            PackageVersion::parse("1.0.0-1").unwrap(),
813            PackageVersion::SemVer(SemVer {
814                version: "1.0.0".parse().unwrap(),
815                component_count: 3,
816                specrev: 1.into()
817            })
818        );
819        assert_eq!(
820            PackageVersion::parse("1.0.0-10-1").unwrap(),
821            PackageVersion::SemVer(SemVer {
822                version: "1.0.0-10".parse().unwrap(),
823                component_count: 3,
824                specrev: 1.into()
825            })
826        );
827        assert_eq!(
828            PackageVersion::parse("1.0.0.10-1").unwrap(),
829            PackageVersion::SemVer(SemVer {
830                version: "1.0.0-10".parse().unwrap(),
831                component_count: 3,
832                specrev: 1.into()
833            })
834        );
835        assert_eq!(
836            PackageVersion::parse("1.0.0.10.0-1").unwrap(),
837            PackageVersion::SemVer(SemVer {
838                version: "1.0.0-10.0".parse().unwrap(),
839                component_count: 3,
840                specrev: 1.into()
841            })
842        );
843    }
844
845    #[tokio::test]
846    async fn parse_dev_version() {
847        assert_eq!(
848            PackageVersion::parse("dev-1").unwrap(),
849            PackageVersion::DevVer(DevVer {
850                modrev: DevVersion::Dev,
851                specrev: 1.into()
852            })
853        );
854        assert_eq!(
855            PackageVersion::parse("scm-1").unwrap(),
856            PackageVersion::DevVer(DevVer {
857                modrev: DevVersion::Scm,
858                specrev: 1.into()
859            })
860        );
861        assert_eq!(
862            PackageVersion::parse("git-1").unwrap(),
863            PackageVersion::StringVer(StringVer {
864                modrev: "git".into(),
865                specrev: 1.into()
866            })
867        );
868        assert_eq!(
869            PackageVersion::parse("scm-1").unwrap(),
870            PackageVersion::DevVer(DevVer {
871                modrev: DevVersion::Scm,
872                specrev: 1.into()
873            })
874        );
875    }
876
877    #[tokio::test]
878    async fn parse_dev_version_req() {
879        assert_eq!(
880            PackageVersionReq::parse("dev").unwrap(),
881            PackageVersionReq::DevVer(DevVersion::Dev)
882        );
883        assert_eq!(
884            PackageVersionReq::parse("scm").unwrap(),
885            PackageVersionReq::DevVer(DevVersion::Scm)
886        );
887        assert_eq!(
888            PackageVersionReq::parse("git").unwrap(),
889            PackageVersionReq::StringVer("git".into())
890        );
891        assert_eq!(
892            PackageVersionReq::parse("==dev").unwrap(),
893            PackageVersionReq::DevVer(DevVersion::Dev)
894        );
895        assert_eq!(
896            PackageVersionReq::parse("==git").unwrap(),
897            PackageVersionReq::StringVer("git".into())
898        );
899        assert_eq!(
900            PackageVersionReq::parse("== dev").unwrap(),
901            PackageVersionReq::DevVer(DevVersion::Dev)
902        );
903        assert_eq!(
904            PackageVersionReq::parse("== scm").unwrap(),
905            PackageVersionReq::DevVer(DevVersion::Scm)
906        );
907        assert_eq!(
908            PackageVersionReq::parse("@dev").unwrap(),
909            PackageVersionReq::DevVer(DevVersion::Dev)
910        );
911        assert_eq!(
912            PackageVersionReq::parse("@git").unwrap(),
913            PackageVersionReq::StringVer("git".into())
914        );
915        assert_eq!(
916            PackageVersionReq::parse("@ dev").unwrap(),
917            PackageVersionReq::DevVer(DevVersion::Dev)
918        );
919        assert_eq!(
920            PackageVersionReq::parse("@ scm").unwrap(),
921            PackageVersionReq::DevVer(DevVersion::Scm)
922        );
923        assert_eq!(
924            PackageVersionReq::parse(">1-1,<1.2-2").unwrap(),
925            PackageVersionReq::SemVer(">1,<1.2".parse().unwrap())
926        );
927        assert_eq!(
928            PackageVersionReq::parse("> 1-1, < 1.2-2").unwrap(),
929            PackageVersionReq::SemVer("> 1, < 1.2".parse().unwrap())
930        );
931        assert_eq!(
932            PackageVersionReq::parse("> 2.1.0.10, < 2.1.1").unwrap(),
933            PackageVersionReq::SemVer("> 2.1.0-10, < 2.1.1".parse().unwrap())
934        );
935    }
936
937    #[tokio::test]
938    async fn package_version_req_semver_roundtrips() {
939        let req = PackageVersionReq::parse("==0.7.1").unwrap();
940        assert_eq!(req.to_string(), "==0.7.1");
941
942        let req = PackageVersionReq::parse("0.7.1").unwrap();
943        assert_eq!(req.to_string(), "==0.7.1");
944
945        let req = PackageVersionReq::parse(">=0.7.1").unwrap();
946        assert_eq!(req.to_string(), ">=0.7.1");
947
948        let req = PackageVersionReq::parse(">0.7.1").unwrap();
949        assert_eq!(req.to_string(), ">0.7.1");
950
951        let req = PackageVersionReq::parse("<0.7.1").unwrap();
952        assert_eq!(req.to_string(), "<0.7.1");
953
954        let req = PackageVersionReq::parse("~> 0.7.1").unwrap();
955        assert_eq!(req.to_string(), ">=0.7.1, <0.7.2");
956    }
957
958    #[tokio::test]
959    async fn package_version_req_devver_roundtrips() {
960        let req = PackageVersionReq::parse("==scm").unwrap();
961        assert_eq!(req.to_string(), "==scm");
962
963        let req = PackageVersionReq::parse("@scm").unwrap();
964        assert_eq!(req.to_string(), "==scm");
965
966        let req = PackageVersionReq::parse("scm").unwrap();
967        assert_eq!(req.to_string(), "==scm");
968
969        let req = PackageVersionReq::parse("==a144124839f027a2d0a95791936c478d047126fc").unwrap();
970        assert_eq!(
971            req.to_string(),
972            "==a144124839f027a2d0a95791936c478d047126fc"
973        );
974    }
975}