From 7f5dd7657a7a0a68d5aecfcb0791c58cd651d62e Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Fri, 1 Mar 2024 14:50:47 +0100 Subject: [PATCH] fix: patch unsupported glob operators (#551) --- .../src/version_spec/constraint.rs | 2 +- .../src/version_spec/mod.rs | 60 ++++++++++++++++++- .../src/version_spec/parse.rs | 39 +++++++++--- .../src/version_spec/version_tree.rs | 31 ++++++---- 4 files changed, 111 insertions(+), 21 deletions(-) diff --git a/crates/rattler_conda_types/src/version_spec/constraint.rs b/crates/rattler_conda_types/src/version_spec/constraint.rs index eb65f6301..dd4280a14 100644 --- a/crates/rattler_conda_types/src/version_spec/constraint.rs +++ b/crates/rattler_conda_types/src/version_spec/constraint.rs @@ -247,7 +247,7 @@ mod test { ); assert_eq!( Constraint::from_str("1.2.3$"), - Err(ParseConstraintError::RegexConstraintsNotSupported) + Err(ParseConstraintError::UnterminatedRegex) ); assert_eq!( Constraint::from_str("1.*.3"), diff --git a/crates/rattler_conda_types/src/version_spec/mod.rs b/crates/rattler_conda_types/src/version_spec/mod.rs index 8e34594e5..6571e8ce6 100644 --- a/crates/rattler_conda_types/src/version_spec/mod.rs +++ b/crates/rattler_conda_types/src/version_spec/mod.rs @@ -182,6 +182,16 @@ impl FromStr for VersionSpec { } } +impl Display for VersionOperators { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + VersionOperators::Range(r) => write!(f, "{r}"), + VersionOperators::StrictRange(r) => write!(f, "{r}"), + VersionOperators::Exact(r) => write!(f, "{r}"), + } + } +} + impl Display for RangeOperator { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -307,7 +317,12 @@ impl VersionSpec { #[cfg(test)] mod tests { - use crate::version_spec::{EqualityOperator, LogicalOperator, RangeOperator}; + use assert_matches::assert_matches; + + use crate::version_spec::parse::ParseConstraintError; + use crate::version_spec::{ + EqualityOperator, LogicalOperator, ParseVersionSpecError, RangeOperator, + }; use crate::{Version, VersionSpec}; use std::str::FromStr; @@ -416,4 +431,47 @@ mod tests { VersionSpec::from_str(">=2.10").unwrap() ); } + + #[test] + fn issue_star_operator() { + assert_eq!( + VersionSpec::from_str(">=*").unwrap(), + VersionSpec::from_str("*").unwrap() + ); + assert_eq!( + VersionSpec::from_str("==*").unwrap(), + VersionSpec::from_str("*").unwrap() + ); + assert_eq!( + VersionSpec::from_str("=*").unwrap(), + VersionSpec::from_str("*").unwrap() + ); + assert_eq!( + VersionSpec::from_str("~=*").unwrap(), + VersionSpec::from_str("*").unwrap() + ); + assert_eq!( + VersionSpec::from_str("<=*").unwrap(), + VersionSpec::from_str("*").unwrap() + ); + + assert_matches!( + VersionSpec::from_str(">*").unwrap_err(), + ParseVersionSpecError::InvalidConstraint( + ParseConstraintError::GlobVersionIncompatibleWithOperator(_) + ) + ); + assert_matches!( + VersionSpec::from_str("!=*").unwrap_err(), + ParseVersionSpecError::InvalidConstraint( + ParseConstraintError::GlobVersionIncompatibleWithOperator(_) + ) + ); + assert_matches!( + VersionSpec::from_str("<*").unwrap_err(), + ParseVersionSpecError::InvalidConstraint( + ParseConstraintError::GlobVersionIncompatibleWithOperator(_) + ) + ); + } } diff --git a/crates/rattler_conda_types/src/version_spec/parse.rs b/crates/rattler_conda_types/src/version_spec/parse.rs index fa596801c..1854b4b1e 100644 --- a/crates/rattler_conda_types/src/version_spec/parse.rs +++ b/crates/rattler_conda_types/src/version_spec/parse.rs @@ -51,8 +51,8 @@ fn operator_parser(input: &str) -> IResult<&str, VersionOperators, ParseVersionO #[derive(Debug, Clone, Error, Eq, PartialEq)] pub enum ParseConstraintError { - #[error("'.' is incompatible with '{0}' operator'")] - GlobVersionIncompatibleWithOperator(RangeOperator), + #[error("'*' is incompatible with '{0}' operator'")] + GlobVersionIncompatibleWithOperator(VersionOperators), #[error("regex constraints are not supported")] RegexConstraintsNotSupported, #[error("unterminated unsupported regular expression")] @@ -84,13 +84,14 @@ impl<'i> ParseError<&'i str> for ParseConstraintError { /// Parses a regex constraint. Returns an error if no terminating `$` is found. fn regex_constraint_parser(input: &str) -> IResult<&str, Constraint, ParseConstraintError> { - let (_rest, (_, _, terminator)) = - tuple((char('^'), take_while(|c| c != '$'), opt(char('$'))))(input)?; - match terminator { - Some(_) => Err(nom::Err::Failure( + let (_rest, (preceder, _, terminator)) = + tuple((opt(char('^')), take_while(|c| c != '$'), opt(char('$'))))(input)?; + match (preceder, terminator) { + (None, None) => Err(nom::Err::Error(ParseConstraintError::UnterminatedRegex)), + (_, None) | (None, _) => Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)), + _ => Err(nom::Err::Failure( ParseConstraintError::RegexConstraintsNotSupported, )), - None => Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)), } } @@ -128,8 +129,30 @@ fn logical_constraint_parser(input: &str) -> IResult<&str, Constraint, ParseCons })) })?; + // Handle the case where no version was specified. These cases don't make any sense (e.g. + // ``>=*``) but they do exist in the wild. This code here tries to map it to something that at + // least makes some sort of sense. But this is not the case for everything, for instance what + // what is ment with `!=*` or `<*`? + // See: https://github.com/AnacondaRecipes/repodata-hotfixes/issues/220 + if version_str == "*" { + return match op.expect( + "if no operator was specified for the star then this is not a logical constraint", + ) { + VersionOperators::Range(RangeOperator::GreaterEquals | RangeOperator::LessEquals) + | VersionOperators::StrictRange( + StrictRangeOperator::Compatible | StrictRangeOperator::StartsWith, + ) + | VersionOperators::Exact(EqualityOperator::Equals) => Ok((rest, Constraint::Any)), + op => { + return Err(nom::Err::Error( + ParseConstraintError::GlobVersionIncompatibleWithOperator(op), + )); + } + }; + } + // Parse the string as a version - let (version_rest, version) = version_parser(input).map_err(|e| { + let (version_rest, version) = version_parser(version_str).map_err(|e| { e.map(|e| { ParseConstraintError::InvalidVersion(ParseVersionError { kind: e, diff --git a/crates/rattler_conda_types/src/version_spec/version_tree.rs b/crates/rattler_conda_types/src/version_spec/version_tree.rs index 4fb483a42..c779a1b71 100644 --- a/crates/rattler_conda_types/src/version_spec/version_tree.rs +++ b/crates/rattler_conda_types/src/version_spec/version_tree.rs @@ -103,6 +103,18 @@ pub(crate) fn recognize_version<'a, E: ParseError<&'a str> + ContextError<&'a st )))(input) } +/// Recognize a version followed by a .* or *, or just a * +pub(crate) fn recognize_version_with_star<'a, E: ParseError<&'a str> + ContextError<&'a str>>( + input: &'a str, +) -> Result<(&'a str, &'a str), nom::Err> { + alt(( + // A version with an optional * or .*. + terminated(recognize_version, opt(alt((tag(".*"), tag("*"))))), + // Just a * + tag("*"), + ))(input) +} + /// A parser that recognized a constraint but does not actually parse it. pub(crate) fn recognize_constraint<'a, E: ParseError<&'a str> + ContextError<&'a str>>( input: &'a str, @@ -111,18 +123,15 @@ pub(crate) fn recognize_constraint<'a, E: ParseError<&'a str> + ContextError<&'a // Any (* or *.*) terminated(tag("*"), cut(opt(tag(".*")))), // Regex - recognize(delimited(tag("^"), not(tag("$")), tag("$"))), + recognize(delimited(opt(tag("^")), not(tag("$")), tag("$"))), // Version with optional operator followed by optional glob. - recognize(terminated( - preceded( - opt(delimited( - opt(multispace0), - parse_operator, - opt(multispace0), - )), - cut(context("version", recognize_version)), - ), - opt(alt((tag(".*"), tag("*")))), + recognize(preceded( + opt(delimited( + opt(multispace0), + parse_operator, + opt(multispace0), + )), + cut(context("version", recognize_version_with_star)), )), ))(input) }