diff --git a/crates/rattler_conda_types/benches/parse.rs b/crates/rattler_conda_types/benches/parse.rs index 24012a3e7..acea3785f 100644 --- a/crates/rattler_conda_types/benches/parse.rs +++ b/crates/rattler_conda_types/benches/parse.rs @@ -8,6 +8,18 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("parse complex version", |b| { b.iter(|| black_box("1!1.0b2.post345.dev456+3.2.20.rc3").parse::()) }); + c.bench_function("parse logical constraint", |b| { + b.iter(|| black_box(">=3.1").parse::()) + }); + c.bench_function("parse wildcard constraint", |b| { + b.iter(|| black_box("3.1.*").parse::()) + }); + c.bench_function("parse simple version spec", |b| { + b.iter(|| black_box(">=3.1").parse::()) + }); + c.bench_function("parse complex version spec", |b| { + b.iter(|| black_box("(>=2.1.0,<3.0)|(~=3.2.1,~3.2.2.1)|(==4.1)").parse::()) + }); } criterion_group!(benches, criterion_benchmark); diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index 5418188a0..e575b7bf4 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -18,7 +18,7 @@ use smallvec::SmallVec; pub use parse::{ParseVersionError, ParseVersionErrorKind}; mod flags; -mod parse; +pub(crate) mod parse; mod segment; mod with_source; @@ -218,7 +218,7 @@ impl Version { }; version_segments.iter().map(move |&segment| { let start = idx; - idx += segment.len(); + idx += segment.len() as usize; SegmentIter { offset: start, version: self, @@ -312,12 +312,12 @@ impl Version { let mut idx = if self.has_epoch() { 1 } else { 0 }; idx += self.segments[..start] .iter() - .map(|segment| segment.len()) + .map(|segment| segment.len() as usize) .sum::(); let version_segments = &self.segments[start..]; Either::Left(version_segments.iter().map(move |&segment| { let start = idx; - idx += segment.len(); + idx += segment.len() as usize; SegmentIter { offset: start, version: self, @@ -914,7 +914,7 @@ impl<'v> SegmentIter<'v> { /// Returns the number of components stored in the version. Note that the number of components /// returned by [`Self::components`] might differ because it might include an implicit default. pub fn component_count(&self) -> usize { - self.segment.len() + self.segment.len() as usize } /// Returns an iterator over the components of this segment. @@ -924,7 +924,7 @@ impl<'v> SegmentIter<'v> { let version = self.version; // Create an iterator over all component - let segment_components = (self.offset..self.offset + self.segment.len()) + let segment_components = (self.offset..self.offset + self.segment.len() as usize) .map(move |idx| &version.components[idx]); // Add an implicit default if this segment has one diff --git a/crates/rattler_conda_types/src/version/parse.rs b/crates/rattler_conda_types/src/version/parse.rs index f0b313640..7f7d02da1 100644 --- a/crates/rattler_conda_types/src/version/parse.rs +++ b/crates/rattler_conda_types/src/version/parse.rs @@ -3,12 +3,12 @@ use crate::version::flags::Flags; use crate::version::segment::Segment; use crate::version::{ComponentVec, SegmentVec}; use nom::branch::alt; -use nom::bytes::complete::{tag_no_case, take_while}; +use nom::bytes::complete::tag_no_case; use nom::character::complete::{alpha1, char, digit1, one_of}; -use nom::combinator::{cut, eof, map, map_res, opt, value}; +use nom::combinator::{map, opt, value}; use nom::error::{ErrorKind, FromExternalError, ParseError}; -use nom::sequence::{preceded, terminated}; -use nom::{IResult, Parser}; +use nom::sequence::terminated; +use nom::IResult; use smallvec::SmallVec; use std::{ convert::Into, @@ -83,6 +83,9 @@ pub enum ParseVersionErrorKind { /// Cannot mix and match dashes and underscores #[error("cannot use both underscores and dashes as version segment seperators")] CannotMixAndMatchDashesAndUnderscores, + /// Expected the end of the string + #[error("encountered more characters but expected none")] + ExpectedEof, /// Nom error #[error("{0:?}")] Nom(ErrorKind), @@ -125,119 +128,145 @@ fn numeral_parser(input: &str) -> IResult<&str, u64, ParseVersionErrorKind> { } /// Parses a single version [`Component`]. -fn component_parser<'i>( - separator: Option, -) -> impl Parser<&'i str, Component, ParseVersionErrorKind> { - move |input: &'i str| { - alt(( - // Parse a numeral - map(numeral_parser, Component::Numeral), - // Parse special case components - value(Component::Post, tag_no_case("post")), - value(Component::Dev, tag_no_case("dev")), - // Parse an identifier - map(alpha1, |alpha: &'i str| { - Component::Iden(alpha.to_lowercase().into_boxed_str()) - }), - // Parse a `_` or `-` at the end of the string. - map_res(terminated(one_of("-_"), eof), |c: char| { - let is_dash = match (c, separator) { - ('-', Some('-') | None) => true, - ('_', Some('_') | None) => false, - _ => return Err(ParseVersionErrorKind::CannotMixAndMatchDashesAndUnderscores), - }; - Ok(Component::UnderscoreOrDash { is_dash }) - }), - ))(input) - } +fn component_parser<'i>(input: &'i str) -> IResult<&'i str, Component, ParseVersionErrorKind> { + alt(( + // Parse a numeral + map(numeral_parser, Component::Numeral), + // Parse special case components + value(Component::Post, tag_no_case("post")), + value(Component::Dev, tag_no_case("dev")), + // Parse an identifier + map(alpha1, |alpha: &'i str| { + Component::Iden(alpha.to_lowercase().into_boxed_str()) + }), + ))(input) } /// Parses a version segment from a list of components. fn segment_parser<'i>( components: &mut ComponentVec, - separator: Option, -) -> impl Parser<&'i str, Segment, ParseVersionErrorKind> + '_ { - move |input| { - // Parse the first component of the segment - let (mut rest, first_component) = match component_parser(separator).parse(input) { - Ok(result) => result, - // Convert undefined parse errors into an expect error - Err(nom::Err::Error(ParseVersionErrorKind::Nom(_))) => { - return Err(nom::Err::Error(ParseVersionErrorKind::ExpectedComponent)) + input: &'i str, +) -> IResult<&'i str, Segment, ParseVersionErrorKind> { + // Parse the first component of the segment + let (mut rest, first_component) = match component_parser(input) { + Ok(result) => result, + // Convert undefined parse errors into an expect error + Err(nom::Err::Error(ParseVersionErrorKind::Nom(_))) => { + return Err(nom::Err::Error(ParseVersionErrorKind::ExpectedComponent)) + } + Err(e) => return Err(e), + }; + + // If the first component is not numeric we add a default component since each segment must + // always start with a number. + let mut component_count = 0u16; + let has_implicit_default = !first_component.is_numeric(); + + // Add the first component + components.push(first_component); + component_count += 1; + + // Loop until we can't find any more components + loop { + let (remaining, component) = match opt(component_parser)(rest) { + Ok((i, o)) => (i, o), + Err(e) => { + // Remove any components that we may have added. + components.drain(components.len() - (component_count as usize)..); + return Err(e); } - Err(e) => return Err(e), }; - - // If the first component is not numeric we add a default component since each segment must - // always start with a number. - let mut component_count = 0u16; - let has_implicit_default = !first_component.is_numeric(); - - // Add the first component - components.push(first_component); - component_count += 1; - - // Loop until we can't find any more components - loop { - let (remaining, component) = match opt(component_parser(separator))(rest) { - Ok((i, o)) => (i, o), - Err(e) => { - // Remove any components that we may have added. - components.drain(components.len() - (component_count as usize)..); - return Err(e); - } - }; - match component { - Some(component) => { - components.push(component); - component_count = match component_count.checked_add(1) { - Some(length) => length, - None => { - return Err(nom::Err::Error( - ParseVersionErrorKind::TooManyComponentsInASegment, - )) - } + match component { + Some(component) => { + components.push(component); + component_count = match component_count.checked_add(1) { + Some(length) => length, + None => { + return Err(nom::Err::Failure( + ParseVersionErrorKind::TooManyComponentsInASegment, + )) } } - None => { - let segment = Segment::new(component_count) - .ok_or(nom::Err::Error( - ParseVersionErrorKind::TooManyComponentsInASegment, - ))? - .with_implicit_default(has_implicit_default); + } + None => { + let segment = Segment::new(component_count) + .ok_or(nom::Err::Failure( + ParseVersionErrorKind::TooManyComponentsInASegment, + ))? + .with_implicit_default(has_implicit_default); - break Ok((remaining, segment)); - } + break Ok((remaining, segment)); } - rest = remaining; } + rest = remaining; } } -fn final_version_part_parser( +/// Parses a trailing underscore or dash. +fn trailing_dash_underscore_parser( + input: &str, + dash_or_underscore: Option, +) -> IResult<&str, (Option, Option), ParseVersionErrorKind> { + // Parse a - or _. Return early if it cannot be found. + let (rest, Some(separator)) = opt(one_of::<_,_,(&str, ErrorKind)>("-_"))(input).map_err(|e| e.map(|(_, kind)| ParseVersionErrorKind::Nom(kind)))? else { + return Ok((input, (None, dash_or_underscore))); + }; + + // Make sure dashes and underscores are not mixed. + let dash_or_underscore = match (dash_or_underscore, separator) { + (None, '-') => Some('-'), + (None, '_') => Some('_'), + (Some('-'), '_') | (Some('_'), '-') => { + return Err(nom::Err::Error( + ParseVersionErrorKind::CannotMixAndMatchDashesAndUnderscores, + )) + } + _ => dash_or_underscore, + }; + + Ok(( + rest, + ( + Some(Component::UnderscoreOrDash { + is_dash: separator == '-', + }), + dash_or_underscore, + ), + )) +} + +fn version_part_parser<'i>( components: &mut ComponentVec, segments: &mut SegmentVec, - input: &str, + input: &'i str, dash_or_underscore: Option, -) -> Result, nom::Err> { +) -> IResult<&'i str, Option, ParseVersionErrorKind> { let mut dash_or_underscore = dash_or_underscore; - let first_segment_idx = segments.len(); + let mut recovery_segment_idx = segments.len(); // Parse the first segment of the version. It must exists. - let (mut input, first_segment_length) = - segment_parser(components, dash_or_underscore).parse(input)?; + let (mut input, first_segment_length) = segment_parser(components, input)?; segments.push(first_segment_length); + + // Iterate over any additional segments that we find. let result = loop { - // Parse either eof or a version segment separator. - let (rest, separator) = match alt((map(one_of("-._"), Some), value(None, eof)))(input) { - Ok((_, None)) => break Ok(dash_or_underscore), + // Parse a version segment separator. + let (rest, separator) = match opt(one_of("-._"))(input) { + Ok((_, None)) => { + // No additional separator found, exit early. + return Ok((input, dash_or_underscore)); + } Ok((rest, Some(separator))) => (rest, separator), + Err(nom::Err::Error(_)) => { + // If an error occured we convert it to a segment separator not found error instead. break Err(nom::Err::Error( ParseVersionErrorKind::ExpectedSegmentSeparator, - )) + )); } - Err(e) => return Err(e), + + // Failure are propagated + Err(e) => break Err(e), }; // Make sure dashes and underscores are not mixed. @@ -245,7 +274,7 @@ fn final_version_part_parser( (None, '-') => dash_or_underscore = Some('-'), (None, '_') => dash_or_underscore = Some('_'), (Some('-'), '_') | (Some('_'), '-') => { - break Err(nom::Err::Error( + break Err(nom::Err::Failure( ParseVersionErrorKind::CannotMixAndMatchDashesAndUnderscores, )) } @@ -253,25 +282,73 @@ fn final_version_part_parser( } // Parse the next segment. - let (rest, segment) = match segment_parser(components, dash_or_underscore).parse(rest) { + let (rest, segment) = match segment_parser(components, rest) { Ok(result) => result, + Err(nom::Err::Error(_)) => { + // If parsing of a segment failed, check if perhaps the seperator is followed by an + // underscore or dash. + match trailing_dash_underscore_parser(rest, dash_or_underscore)? { + (rest, (Some(component), dash_or_underscore)) => { + // We are parsing multiple dashes or underscores ("..__"), add a new segment + // just for the trailing underscore/dash + components.push(component); + segments.push( + Segment::new(1) + .unwrap() + .with_implicit_default(true) + .with_separator(Some(separator)) + .unwrap(), + ); + + // Since the trailing is always at the end we immediately return + return Ok((rest, dash_or_underscore)); + } + (rest, (None, dash_or_underscore)) if separator == '-' || separator == '_' => { + // We are parsing a single dash or underscore (".._"), update the last + // segment we added + let segment = segments + .last_mut() + .expect("there must be at least one segment added"); + components.push(Component::UnderscoreOrDash { + is_dash: separator == '-', + }); + + *segment = segment + .len() + .checked_add(1) + .and_then(|len| segment.with_component_count(len)) + .ok_or(nom::Err::Failure( + ParseVersionErrorKind::TooManyComponentsInASegment, + ))?; + + // Since the trailing is always at the end we immediately return + return Ok((rest, dash_or_underscore)); + } + _ => return Ok((input, dash_or_underscore)), + } + } + + // Failures are propagated Err(e) => break Err(e), }; + segments.push( segment .with_separator(Some(separator)) .expect("unrecognized separator"), ); - + recovery_segment_idx += 1; input = rest; }; - // If there was an error, revert the `segment_lengths` array. - if result.is_err() { - segments.drain(first_segment_idx..); + match result { + // If there was an error, revert the `segment_lengths` array. + Err(e) => { + segments.drain(recovery_segment_idx..); + Err(e) + } + Ok(separator) => Ok((input, separator)), } - - result } pub fn version_parser(input: &str) -> IResult<&str, Version, ParseVersionErrorKind> { @@ -291,16 +368,15 @@ pub fn version_parser(input: &str) -> IResult<&str, Version, ParseVersionErrorKi flags = flags.with_has_epoch(true); } - // Scan the input to find the version segments. - let (rest, common_part) = recognize_segments(input)?; - let (rest, local_part) = opt(preceded(char('+'), cut(recognize_segments)))(rest)?; - - // Parse the common version part - let dash_or_underscore = - final_version_part_parser(&mut components, &mut segments, common_part, None)?; + // Parse the common part of the version + let (rest, dash_or_underscore) = + match version_part_parser(&mut components, &mut segments, input, None) { + Ok(result) => result, + Err(e) => return Err(e), + }; // Parse the local version part - if let Some(local_part) = local_part { + let rest = if let Ok((local_version_part, _)) = char::<_, (&str, ErrorKind)>('+')(rest) { let first_local_segment_idx = segments.len(); // Encode the local segment index into the flags. @@ -322,52 +398,49 @@ pub fn version_parser(input: &str) -> IResult<&str, Version, ParseVersionErrorKi } } - // Parse the segments - final_version_part_parser( + match version_part_parser( &mut components, &mut segments, - local_part, + local_version_part, dash_or_underscore, - )?; - } + ) { + Ok((rest, _)) => rest, + Err(e) => return Err(e), + } + } else { + rest + }; - return Ok(( + Ok(( rest, Version { flags, components, segments, }, - )); - - /// A helper function to crudely recognize version segments. - fn recognize_segments<'i, E: ParseError<&'i str>>( - input: &'i str, - ) -> IResult<&'i str, &'i str, E> { - take_while(|c: char| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')(input) - } -} - -pub fn final_version_parser(input: &str) -> Result { - match version_parser(input) { - Ok(("", version)) => Ok(version), - Ok(_) => Err(ParseVersionErrorKind::ExpectedSegmentSeparator), - Err(nom::Err::Failure(e) | nom::Err::Error(e)) => Err(e), - Err(_) => unreachable!("not streaming, so no other error possible"), - } + )) } impl FromStr for Version { type Err = ParseVersionError; fn from_str(s: &str) -> Result { - final_version_parser(s).map_err(|kind| ParseVersionError::new(s, kind)) + match version_parser(s) { + Ok(("", version)) => Ok(version), + Ok(_) => Err(ParseVersionError::new( + s, + ParseVersionErrorKind::ExpectedEof, + )), + Err(nom::Err::Failure(e) | nom::Err::Error(e)) => Err(ParseVersionError::new(s, e)), + Err(_) => unreachable!("not streaming, so no other error possible"), + } } } #[cfg(test)] mod test { - use super::{final_version_parser, Version}; + use super::Version; + use crate::version::parse::version_parser; use crate::version::SegmentFormatter; use serde::Serialize; use std::collections::BTreeMap; @@ -375,6 +448,14 @@ mod test { use std::path::Path; use std::str::FromStr; + #[test] + fn test_parse_star() { + assert_eq!( + version_parser("1.*"), + Ok((".*", Version::from_str("1").unwrap())) + ); + } + #[test] fn test_parse() { let versions = [ @@ -411,12 +492,12 @@ mod test { let mut index_map: BTreeMap = BTreeMap::default(); for version_str in versions { - let version_or_error = match final_version_parser(version_str) { + let version_or_error = match Version::from_str(version_str) { Ok(version) => { assert_eq!(version_str, version.to_string().as_str()); VersionOrError::Version(version) } - Err(e) => VersionOrError::Error(e.to_string()), + Err(e) => VersionOrError::Error(e.kind.to_string()), }; index_map.insert(version_str.to_owned(), version_or_error); } diff --git a/crates/rattler_conda_types/src/version/segment.rs b/crates/rattler_conda_types/src/version/segment.rs index c0aec7b51..96db0a3b8 100644 --- a/crates/rattler_conda_types/src/version/segment.rs +++ b/crates/rattler_conda_types/src/version/segment.rs @@ -35,9 +35,21 @@ impl Segment { )) } + pub fn with_component_count(self, len: u16) -> Option { + // The number of components is too large. + if len > COMPONENT_COUNT_MASK { + return None; + } + + let component_mask = (len & COMPONENT_COUNT_MASK) << COMPONENT_COUNT_OFFSET; + Some(Self( + self.0 & !(COMPONENT_COUNT_MASK << COMPONENT_COUNT_OFFSET) | component_mask, + )) + } + /// Returns the number of components in this segment - pub fn len(self) -> usize { - ((self.0 >> COMPONENT_COUNT_OFFSET) & COMPONENT_COUNT_MASK) as usize + pub fn len(self) -> u16 { + (self.0 >> COMPONENT_COUNT_OFFSET) & COMPONENT_COUNT_MASK } /// Sets whether the segment starts with an implicit default `Component`. This is the case when @@ -109,6 +121,23 @@ mod test { assert_eq!(Segment::new(8191).unwrap().len(), 8191); assert_eq!(Segment::new(8192), None); + assert_eq!( + Segment::new(1) + .unwrap() + .with_component_count(1337) + .unwrap() + .len(), + 1337 + ); + assert_eq!( + Segment::new(1) + .unwrap() + .with_component_count(4096) + .unwrap() + .len(), + 4096 + ); + assert_eq!(Segment::new(4096).unwrap().has_implicit_default(), false); assert_eq!( Segment::new(4096) diff --git a/crates/rattler_conda_types/src/version/snapshots/rattler_conda_types__version__parse__test__parse.snap b/crates/rattler_conda_types/src/version/snapshots/rattler_conda_types__version__parse__test__parse.snap index e52980769..526f09251 100644 --- a/crates/rattler_conda_types/src/version/snapshots/rattler_conda_types__version__parse__test__parse.snap +++ b/crates/rattler_conda_types/src/version/snapshots/rattler_conda_types__version__parse__test__parse.snap @@ -70,7 +70,7 @@ expression: index_map }, ), "1@2": Error( - "expected a '.', '-', or '_'", + "encountered more characters but expected none", ), "1_": Version( Version { @@ -100,6 +100,6 @@ expression: index_map }, ), "1___": Error( - "expected a version component e.g. `2` or `rc`", + "encountered more characters but expected none", ), } diff --git a/crates/rattler_conda_types/src/version_spec/constraint.rs b/crates/rattler_conda_types/src/version_spec/constraint.rs index 084cc84e9..889359751 100644 --- a/crates/rattler_conda_types/src/version_spec/constraint.rs +++ b/crates/rattler_conda_types/src/version_spec/constraint.rs @@ -1,7 +1,8 @@ +use super::ParseConstraintError; use super::VersionOperator; -use crate::{ParseVersionError, Version}; +use crate::version_spec::parse::constraint_parser; +use crate::Version; use std::str::FromStr; -use thiserror::Error; /// A single version constraint (e.g. `>3.4.5` or `1.2.*`) #[allow(clippy::large_enum_variant)] @@ -14,45 +15,6 @@ pub(crate) enum Constraint { Comparison(VersionOperator, Version), } -#[derive(Debug, Clone, Error, Eq, PartialEq)] -pub enum ParseConstraintError { - #[error("cannot parse version: {0}")] - InvalidVersion(#[source] ParseVersionError), - #[error("version operator followed by a whitespace")] - OperatorFollowedByWhitespace, - #[error("'.' is incompatible with '{0}' operator'")] - GlobVersionIncompatibleWithOperator(VersionOperator), - #[error("regex constraints are not supported")] - RegexConstraintsNotSupported, - #[error("invalid operator")] - InvalidOperator, -} - -/// Parses an operator from a string. Returns the operator and the rest of the string. -fn parse_operator(s: &str) -> Option<(VersionOperator, &str)> { - if let Some(rest) = s.strip_prefix("==") { - Some((VersionOperator::Equals, rest)) - } else if let Some(rest) = s.strip_prefix("!=") { - Some((VersionOperator::NotEquals, rest)) - } else if let Some(rest) = s.strip_prefix("<=") { - Some((VersionOperator::LessEquals, rest)) - } else if let Some(rest) = s.strip_prefix(">=") { - Some((VersionOperator::GreaterEquals, rest)) - } else if let Some(rest) = s.strip_prefix("~=") { - Some((VersionOperator::Compatible, rest)) - } else if let Some(rest) = s.strip_prefix('<') { - Some((VersionOperator::Less, rest)) - } else if let Some(rest) = s.strip_prefix('>') { - Some((VersionOperator::Greater, rest)) - } else if let Some(rest) = s.strip_prefix('=') { - Some((VersionOperator::StartsWith, rest)) - } else if s.starts_with(|c: char| c.is_alphanumeric()) { - Some((VersionOperator::Equals, s)) - } else { - None - } -} - /// Returns true if the specified character is the first character of a version constraint. pub(crate) fn is_start_of_version_constraint(c: char) -> bool { matches!(c, '>' | '<' | '=' | '!' | '~') @@ -61,59 +23,12 @@ pub(crate) fn is_start_of_version_constraint(c: char) -> bool { impl FromStr for Constraint { type Err = ParseConstraintError; - fn from_str(s: &str) -> Result { - let s = s.trim(); - if s == "*" { - Ok(Constraint::Any) - } else if s.starts_with('^') || s.ends_with('$') { - Err(ParseConstraintError::RegexConstraintsNotSupported) - } else if s.starts_with(is_start_of_version_constraint) { - let (op, version_str) = - parse_operator(s).ok_or(ParseConstraintError::InvalidOperator)?; - if !version_str.starts_with(char::is_alphanumeric) { - return Err(ParseConstraintError::InvalidOperator); - } - if version_str.starts_with(char::is_whitespace) { - return Err(ParseConstraintError::OperatorFollowedByWhitespace); - } - let (version_str, op) = if let Some(version_str) = version_str - .strip_suffix(".*") - .or(version_str.strip_suffix('*')) - { - match op { - VersionOperator::StartsWith | VersionOperator::GreaterEquals => { - (version_str, op) - } - VersionOperator::Greater => (version_str, VersionOperator::GreaterEquals), - VersionOperator::NotEquals => (version_str, VersionOperator::NotStartsWith), - op => { - // return Err(ParseConstraintError::GlobVersionIncompatibleWithOperator( - // op, - // )) - tracing::warn!("Using .* with relational operator is superfluous and deprecated and will be removed in a future version of conda. Your spec was {version_str}.*, but conda is ignoring the .* and treating it as {version_str}"); - (version_str, op) - } - } - } else { - (version_str, op) - }; - Ok(Constraint::Comparison( - op, - Version::from_str(version_str).map_err(ParseConstraintError::InvalidVersion)?, - )) - } else if s.ends_with('*') { - let version_str = s.trim_end_matches('*').trim_end_matches('.'); - Ok(Constraint::Comparison( - VersionOperator::StartsWith, - Version::from_str(version_str).map_err(ParseConstraintError::InvalidVersion)?, - )) - } else if s.contains('*') { - Err(ParseConstraintError::RegexConstraintsNotSupported) - } else { - Ok(Constraint::Comparison( - VersionOperator::Equals, - Version::from_str(s).map_err(ParseConstraintError::InvalidVersion)?, - )) + fn from_str(input: &str) -> Result { + match constraint_parser(input) { + Ok(("", version)) => Ok(version), + Ok((_, _)) => Err(ParseConstraintError::ExpectedEof), + Err(nom::Err::Failure(e) | nom::Err::Error(e)) => Err(e), + Err(_) => unreachable!("not streaming, so no other error possible"), } } } @@ -143,27 +58,31 @@ mod test { fn test_invalid_op() { assert_eq!( Constraint::from_str("<>1.2.3"), - Err(ParseConstraintError::InvalidOperator) + Err(ParseConstraintError::InvalidOperator(String::from("<>"))) ); assert_eq!( Constraint::from_str("=!1.2.3"), - Err(ParseConstraintError::InvalidOperator) + Err(ParseConstraintError::InvalidOperator(String::from("=!"))) ); assert_eq!( Constraint::from_str("1.2.3"), - Err(ParseConstraintError::InvalidOperator) + Err(ParseConstraintError::InvalidOperator(String::from(""))) ); assert_eq!( Constraint::from_str("!=!1.2.3"), - Err(ParseConstraintError::InvalidOperator) + Err(ParseConstraintError::InvalidOperator(String::from("!=!"))) ); assert_eq!( Constraint::from_str("<=>1.2.3"), - Err(ParseConstraintError::InvalidOperator) + Err(ParseConstraintError::InvalidOperator(String::from("<=>"))) + ); + assert_eq!( + Constraint::from_str("=>1.2.3"), + Err(ParseConstraintError::InvalidOperator(String::from("=>"))) ); } @@ -225,6 +144,13 @@ mod test { Version::from_str("1.2.3").unwrap() )) ); + assert_eq!( + Constraint::from_str(">=1!1.2"), + Ok(Constraint::Comparison( + VersionOperator::GreaterEquals, + Version::from_str("1!1.2").unwrap() + )) + ); } #[test] @@ -289,10 +215,10 @@ mod test { Version::from_str("1.2").unwrap() )) ); - assert!(matches!( + assert_eq!( Constraint::from_str("1.2.*.*"), - Err(ParseConstraintError::InvalidVersion(_)) - )); + Err(ParseConstraintError::RegexConstraintsNotSupported) + ); } #[test] @@ -310,7 +236,7 @@ mod test { fn test_regex() { assert_eq!( Constraint::from_str("^1.2.3"), - Err(ParseConstraintError::RegexConstraintsNotSupported) + Err(ParseConstraintError::UnterminatedRegex) ); assert_eq!( Constraint::from_str("1.2.3$"), diff --git a/crates/rattler_conda_types/src/version_spec/mod.rs b/crates/rattler_conda_types/src/version_spec/mod.rs index 27bbffc05..7ea7a2530 100644 --- a/crates/rattler_conda_types/src/version_spec/mod.rs +++ b/crates/rattler_conda_types/src/version_spec/mod.rs @@ -2,11 +2,12 @@ //! [`crate::MatchSpec`], e.g.: `>=3.4,<4.0`. mod constraint; +pub(crate) mod parse; pub(crate) mod version_tree; -use crate::version_spec::constraint::{Constraint, ParseConstraintError}; use crate::version_spec::version_tree::ParseVersionTreeError; use crate::{ParseVersionError, Version}; +pub(crate) use constraint::Constraint; use serde::{Serialize, Serializer}; use std::convert::TryFrom; use std::fmt::{Display, Formatter}; @@ -15,6 +16,7 @@ use thiserror::Error; use version_tree::VersionTree; pub(crate) use constraint::is_start_of_version_constraint; +pub(crate) use parse::ParseConstraintError; /// An operator to compare two versions. #[allow(missing_docs)] diff --git a/crates/rattler_conda_types/src/version_spec/parse.rs b/crates/rattler_conda_types/src/version_spec/parse.rs new file mode 100644 index 000000000..f9cf13228 --- /dev/null +++ b/crates/rattler_conda_types/src/version_spec/parse.rs @@ -0,0 +1,343 @@ +use crate::version::parse::version_parser; +use crate::version_spec::constraint::Constraint; +use crate::version_spec::VersionOperator; +use crate::{ParseVersionError, ParseVersionErrorKind}; +use nom::{ + branch::alt, + bytes::complete::{tag, take_while, take_while1}, + character::complete::char, + combinator::{opt, value}, + error::{ErrorKind, ParseError}, + sequence::{terminated, tuple}, + IResult, +}; +use thiserror::Error; + +#[derive(Debug, Clone, Error, Eq, PartialEq)] +enum ParseVersionOperatorError<'i> { + #[error("invalid operator '{0}'")] + InvalidOperator(&'i str), + #[error("expected version operator")] + ExpectedOperator, +} + +/// Parses a version operator, returns an error if the operator is not recognized or not found. +fn operator_parser(input: &str) -> IResult<&str, VersionOperator, ParseVersionOperatorError> { + // Take anything that looks like an operator. + let (rest, operator_str) = take_while1(|c| "=!<>~".contains(c))(input).map_err( + |_: nom::Err>| { + nom::Err::Error(ParseVersionOperatorError::ExpectedOperator) + }, + )?; + + let op = match operator_str { + "==" => VersionOperator::Equals, + "!=" => VersionOperator::NotEquals, + "<=" => VersionOperator::LessEquals, + ">=" => VersionOperator::GreaterEquals, + "<" => VersionOperator::Less, + ">" => VersionOperator::Greater, + "=" => VersionOperator::StartsWith, + "~=" => VersionOperator::Compatible, + _ => { + return Err(nom::Err::Failure( + ParseVersionOperatorError::InvalidOperator(operator_str), + )) + } + }; + + Ok((rest, op)) +} + +#[derive(Debug, Clone, Error, Eq, PartialEq)] +pub enum ParseConstraintError { + #[error("'.' is incompatible with '{0}' operator'")] + GlobVersionIncompatibleWithOperator(VersionOperator), + #[error("regex constraints are not supported")] + RegexConstraintsNotSupported, + #[error("unterminated unsupported regular expression")] + UnterminatedRegex, + #[error("invalid operator '{0}'")] + InvalidOperator(String), + #[error(transparent)] + InvalidVersion(#[from] ParseVersionError), + /// Expected a version + #[error("expected a version")] + ExpectedVersion, + /// Expected the end of the string + #[error("encountered more characters but expected none")] + ExpectedEof, + /// Nom error + #[error("{0:?}")] + Nom(ErrorKind), +} + +impl<'i> ParseError<&'i str> for ParseConstraintError { + fn from_error_kind(_: &'i str, kind: ErrorKind) -> Self { + ParseConstraintError::Nom(kind) + } + + fn append(_: &'i str, _: ErrorKind, other: Self) -> Self { + other + } +} + +/// 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( + ParseConstraintError::RegexConstraintsNotSupported, + )), + None => Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)), + } +} + +/// Parses the any constraint. This matches "*" and ".*" +fn any_constraint_parser(input: &str) -> IResult<&str, Constraint, ParseConstraintError> { + value(Constraint::Any, terminated(tag("*"), opt(tag(".*"))))(input) +} + +/// Parses a constraint with an operator in front of it. +fn logical_constraint_parser(input: &str) -> IResult<&str, Constraint, ParseConstraintError> { + // Parse the optional preceding operator + let (input, op) = match operator_parser(input) { + Err( + nom::Err::Failure(ParseVersionOperatorError::InvalidOperator(op)) + | nom::Err::Error(ParseVersionOperatorError::InvalidOperator(op)), + ) => { + return Err(nom::Err::Failure(ParseConstraintError::InvalidOperator( + op.to_owned(), + ))) + } + Err(nom::Err::Error(_)) => (input, None), + Ok((rest, op)) => (rest, Some(op)), + _ => unreachable!(), + }; + + // Take everything that looks like a version and use that to parse the version. Any error means + // no characters were detected that belong to the version. + let (rest, version_str) = take_while1::<_, _, (&str, ErrorKind)>(|c: char| { + c.is_alphanumeric() || "!-_.*".contains(c) + })(input) + .map_err(|_| { + nom::Err::Error(ParseConstraintError::InvalidVersion(ParseVersionError { + kind: ParseVersionErrorKind::Empty, + version: String::from(""), + })) + })?; + + // Parse the string as a version + let (version_rest, version) = version_parser(input).map_err(|e| { + e.map(|e| { + ParseConstraintError::InvalidVersion(ParseVersionError { + kind: e, + version: String::from(""), + }) + }) + })?; + + // Convert the operator and the wildcard to something understandable + let op = match (version_rest, op) { + // The version was successfully parsed + ("", Some(op)) => op, + ("", None) => VersionOperator::Equals, + + // The version ends in a wildcard pattern + ("*" | ".*", Some(VersionOperator::StartsWith)) => VersionOperator::StartsWith, + ("*" | ".*", Some(VersionOperator::GreaterEquals)) => VersionOperator::GreaterEquals, + ("*" | ".*", Some(VersionOperator::Greater)) => VersionOperator::GreaterEquals, + ("*" | ".*", Some(VersionOperator::NotEquals)) => VersionOperator::NotStartsWith, + (glob @ "*" | glob @ ".*", Some(op)) => { + tracing::warn!("Using {glob} with relational operator is superfluous and deprecated and will be removed in a future version of conda."); + op + } + ("*" | ".*", None) => VersionOperator::StartsWith, + + // The version string kinda looks like a regular expression. + (version_remainder, _) if version_str.contains('*') || version_remainder.ends_with('$') => { + return Err(nom::Err::Error( + ParseConstraintError::RegexConstraintsNotSupported, + )); + } + + // Otherwise its just a generic error. + _ => { + return Err(nom::Err::Error(ParseConstraintError::InvalidVersion( + ParseVersionError { + version: version_str.to_owned(), + kind: ParseVersionErrorKind::ExpectedEof, + }, + ))) + } + }; + + Ok((rest, Constraint::Comparison(op, version))) +} + +/// Parses a version constraint. +pub(crate) fn constraint_parser(input: &str) -> IResult<&str, Constraint, ParseConstraintError> { + alt(( + regex_constraint_parser, + any_constraint_parser, + logical_constraint_parser, + ))(input) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Version; + use std::str::FromStr; + + #[test] + fn test_operator_parser() { + assert_eq!( + operator_parser(">3.1"), + Ok(("3.1", VersionOperator::Greater)) + ); + assert_eq!( + operator_parser(">=3.1"), + Ok(("3.1", VersionOperator::GreaterEquals)) + ); + assert_eq!(operator_parser("<3.1"), Ok(("3.1", VersionOperator::Less))); + assert_eq!( + operator_parser("<=3.1"), + Ok(("3.1", VersionOperator::LessEquals)) + ); + assert_eq!( + operator_parser("==3.1"), + Ok(("3.1", VersionOperator::Equals)) + ); + assert_eq!( + operator_parser("!=3.1"), + Ok(("3.1", VersionOperator::NotEquals)) + ); + assert_eq!( + operator_parser("=3.1"), + Ok(("3.1", VersionOperator::StartsWith)) + ); + assert_eq!( + operator_parser("~=3.1"), + Ok(("3.1", VersionOperator::Compatible)) + ); + + assert_eq!( + operator_parser("<==>3.1"), + Err(nom::Err::Failure( + ParseVersionOperatorError::InvalidOperator("<==>") + )) + ); + assert_eq!( + operator_parser("3.1"), + Err(nom::Err::Error(ParseVersionOperatorError::ExpectedOperator)) + ); + } + + #[test] + fn parse_regex_constraint() { + assert_eq!( + regex_constraint_parser("^.*"), + Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)) + ); + assert_eq!( + regex_constraint_parser("^"), + Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)) + ); + assert_eq!( + regex_constraint_parser("^$"), + Err(nom::Err::Failure( + ParseConstraintError::RegexConstraintsNotSupported + )) + ); + assert_eq!( + regex_constraint_parser("^1.2.3$"), + Err(nom::Err::Failure( + ParseConstraintError::RegexConstraintsNotSupported + )) + ); + } + + #[test] + fn parse_logical_constraint() { + assert_eq!( + logical_constraint_parser("3.1"), + Ok(( + "", + Constraint::Comparison(VersionOperator::Equals, Version::from_str("3.1").unwrap()) + )) + ); + + assert_eq!( + logical_constraint_parser(">3.1"), + Ok(( + "", + Constraint::Comparison(VersionOperator::Greater, Version::from_str("3.1").unwrap()) + )) + ); + + assert_eq!( + logical_constraint_parser("3.1*"), + Ok(( + "", + Constraint::Comparison( + VersionOperator::StartsWith, + Version::from_str("3.1").unwrap() + ) + )) + ); + + assert_eq!( + logical_constraint_parser("3.1.*"), + Ok(( + "", + Constraint::Comparison( + VersionOperator::StartsWith, + Version::from_str("3.1").unwrap() + ) + )) + ); + + assert_eq!( + logical_constraint_parser("~=3.1"), + Ok(( + "", + Constraint::Comparison( + VersionOperator::Compatible, + Version::from_str("3.1").unwrap() + ) + )) + ); + + assert_eq!( + logical_constraint_parser(">=3.1*"), + Ok(( + "", + Constraint::Comparison( + VersionOperator::GreaterEquals, + Version::from_str("3.1").unwrap() + ) + )) + ); + } + + #[test] + fn parse_constraint() { + // Regular expressions + assert_eq!( + constraint_parser("^1.2.3$"), + Err(nom::Err::Failure( + ParseConstraintError::RegexConstraintsNotSupported + )) + ); + assert_eq!( + constraint_parser("^1.2.3"), + Err(nom::Err::Failure(ParseConstraintError::UnterminatedRegex)) + ); + + // Any constraints + assert_eq!(constraint_parser("*"), Ok(("", Constraint::Any))); + assert_eq!(constraint_parser("*.*"), Ok(("", Constraint::Any))); + } +}