Skip to content

Commit

Permalink
fix: patch unsupported glob operators (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
baszalmstra authored Mar 1, 2024
1 parent 7e0a130 commit 7f5dd76
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 21 deletions.
2 changes: 1 addition & 1 deletion crates/rattler_conda_types/src/version_spec/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
60 changes: 59 additions & 1 deletion crates/rattler_conda_types/src/version_spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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(_)
)
);
}
}
39 changes: 31 additions & 8 deletions crates/rattler_conda_types/src/version_spec/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)),
}
}

Expand Down Expand Up @@ -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,
Expand Down
31 changes: 20 additions & 11 deletions crates/rattler_conda_types/src/version_spec/version_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<E>> {
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,
Expand All @@ -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)
}
Expand Down

0 comments on commit 7f5dd76

Please sign in to comment.