| use crate::backport::*; |
| use crate::error::{ErrorKind, Position}; |
| use crate::identifier::Identifier; |
| use crate::{BuildMetadata, Comparator, Op, Prerelease, Version, VersionReq}; |
| use core::str::FromStr; |
| |
| /// Error parsing a SemVer version or version requirement. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// use semver::Version; |
| /// |
| /// fn main() { |
| /// let err = Version::parse("1.q.r").unwrap_err(); |
| /// |
| /// // "unexpected character 'q' while parsing minor version number" |
| /// eprintln!("{}", err); |
| /// } |
| /// ``` |
| pub struct Error { |
| pub(crate) kind: ErrorKind, |
| } |
| |
| impl FromStr for Version { |
| type Err = Error; |
| |
| fn from_str(text: &str) -> Result<Self, Self::Err> { |
| if text.is_empty() { |
| return Err(Error::new(ErrorKind::Empty)); |
| } |
| |
| let mut pos = Position::Major; |
| let (major, text) = numeric_identifier(text, pos)?; |
| let text = dot(text, pos)?; |
| |
| pos = Position::Minor; |
| let (minor, text) = numeric_identifier(text, pos)?; |
| let text = dot(text, pos)?; |
| |
| pos = Position::Patch; |
| let (patch, text) = numeric_identifier(text, pos)?; |
| |
| if text.is_empty() { |
| return Ok(Version::new(major, minor, patch)); |
| } |
| |
| let (pre, text) = if let Some(text) = text.strip_prefix('-') { |
| pos = Position::Pre; |
| let (pre, text) = prerelease_identifier(text)?; |
| if pre.is_empty() { |
| return Err(Error::new(ErrorKind::EmptySegment(pos))); |
| } |
| (pre, text) |
| } else { |
| (Prerelease::EMPTY, text) |
| }; |
| |
| let (build, text) = if let Some(text) = text.strip_prefix('+') { |
| pos = Position::Build; |
| let (build, text) = build_identifier(text)?; |
| if build.is_empty() { |
| return Err(Error::new(ErrorKind::EmptySegment(pos))); |
| } |
| (build, text) |
| } else { |
| (BuildMetadata::EMPTY, text) |
| }; |
| |
| if let Some(unexpected) = text.chars().next() { |
| return Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected))); |
| } |
| |
| Ok(Version { |
| major, |
| minor, |
| patch, |
| pre, |
| build, |
| }) |
| } |
| } |
| |
| impl FromStr for VersionReq { |
| type Err = Error; |
| |
| fn from_str(text: &str) -> Result<Self, Self::Err> { |
| let text = text.trim_start_matches(' '); |
| if let Some((ch, text)) = wildcard(text) { |
| let rest = text.trim_start_matches(' '); |
| if rest.is_empty() { |
| #[cfg(not(no_const_vec_new))] |
| return Ok(VersionReq::STAR); |
| #[cfg(no_const_vec_new)] // rustc <1.39 |
| return Ok(VersionReq { |
| comparators: Vec::new(), |
| }); |
| } else if rest.starts_with(',') { |
| return Err(Error::new(ErrorKind::WildcardNotTheOnlyComparator(ch))); |
| } else { |
| return Err(Error::new(ErrorKind::UnexpectedAfterWildcard)); |
| } |
| } |
| |
| let depth = 0; |
| let mut comparators = Vec::new(); |
| let len = version_req(text, &mut comparators, depth)?; |
| unsafe { comparators.set_len(len) } |
| Ok(VersionReq { comparators }) |
| } |
| } |
| |
| impl FromStr for Comparator { |
| type Err = Error; |
| |
| fn from_str(text: &str) -> Result<Self, Self::Err> { |
| let text = text.trim_start_matches(' '); |
| let (comparator, pos, rest) = comparator(text)?; |
| if !rest.is_empty() { |
| let unexpected = rest.chars().next().unwrap(); |
| return Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected))); |
| } |
| Ok(comparator) |
| } |
| } |
| |
| impl FromStr for Prerelease { |
| type Err = Error; |
| |
| fn from_str(text: &str) -> Result<Self, Self::Err> { |
| let (pre, rest) = prerelease_identifier(text)?; |
| if !rest.is_empty() { |
| return Err(Error::new(ErrorKind::IllegalCharacter(Position::Pre))); |
| } |
| Ok(pre) |
| } |
| } |
| |
| impl FromStr for BuildMetadata { |
| type Err = Error; |
| |
| fn from_str(text: &str) -> Result<Self, Self::Err> { |
| let (build, rest) = build_identifier(text)?; |
| if !rest.is_empty() { |
| return Err(Error::new(ErrorKind::IllegalCharacter(Position::Build))); |
| } |
| Ok(build) |
| } |
| } |
| |
| impl Error { |
| fn new(kind: ErrorKind) -> Self { |
| Error { kind } |
| } |
| } |
| |
| impl Op { |
| const DEFAULT: Self = Op::Caret; |
| } |
| |
| fn numeric_identifier(input: &str, pos: Position) -> Result<(u64, &str), Error> { |
| let mut len = 0; |
| let mut value = 0u64; |
| |
| while let Some(&digit) = input.as_bytes().get(len) { |
| if digit < b'0' || digit > b'9' { |
| break; |
| } |
| if value == 0 && len > 0 { |
| return Err(Error::new(ErrorKind::LeadingZero(pos))); |
| } |
| match value |
| .checked_mul(10) |
| .and_then(|value| value.checked_add((digit - b'0') as u64)) |
| { |
| Some(sum) => value = sum, |
| None => return Err(Error::new(ErrorKind::Overflow(pos))), |
| } |
| len += 1; |
| } |
| |
| if len > 0 { |
| Ok((value, &input[len..])) |
| } else if let Some(unexpected) = input[len..].chars().next() { |
| Err(Error::new(ErrorKind::UnexpectedChar(pos, unexpected))) |
| } else { |
| Err(Error::new(ErrorKind::UnexpectedEnd(pos))) |
| } |
| } |
| |
| fn wildcard(input: &str) -> Option<(char, &str)> { |
| if let Some(rest) = input.strip_prefix('*') { |
| Some(('*', rest)) |
| } else if let Some(rest) = input.strip_prefix('x') { |
| Some(('x', rest)) |
| } else if let Some(rest) = input.strip_prefix('X') { |
| Some(('X', rest)) |
| } else { |
| None |
| } |
| } |
| |
| fn dot(input: &str, pos: Position) -> Result<&str, Error> { |
| if let Some(rest) = input.strip_prefix('.') { |
| Ok(rest) |
| } else if let Some(unexpected) = input.chars().next() { |
| Err(Error::new(ErrorKind::UnexpectedCharAfter(pos, unexpected))) |
| } else { |
| Err(Error::new(ErrorKind::UnexpectedEnd(pos))) |
| } |
| } |
| |
| fn prerelease_identifier(input: &str) -> Result<(Prerelease, &str), Error> { |
| let (string, rest) = identifier(input, Position::Pre)?; |
| let identifier = unsafe { Identifier::new_unchecked(string) }; |
| Ok((Prerelease { identifier }, rest)) |
| } |
| |
| fn build_identifier(input: &str) -> Result<(BuildMetadata, &str), Error> { |
| let (string, rest) = identifier(input, Position::Build)?; |
| let identifier = unsafe { Identifier::new_unchecked(string) }; |
| Ok((BuildMetadata { identifier }, rest)) |
| } |
| |
| fn identifier(input: &str, pos: Position) -> Result<(&str, &str), Error> { |
| let mut accumulated_len = 0; |
| let mut segment_len = 0; |
| let mut segment_has_nondigit = false; |
| |
| loop { |
| match input.as_bytes().get(accumulated_len + segment_len) { |
| Some(b'A'..=b'Z') | Some(b'a'..=b'z') | Some(b'-') => { |
| segment_len += 1; |
| segment_has_nondigit = true; |
| } |
| Some(b'0'..=b'9') => { |
| segment_len += 1; |
| } |
| boundary => { |
| if segment_len == 0 { |
| if accumulated_len == 0 && boundary != Some(&b'.') { |
| return Ok(("", input)); |
| } else { |
| return Err(Error::new(ErrorKind::EmptySegment(pos))); |
| } |
| } |
| if pos == Position::Pre |
| && segment_len > 1 |
| && !segment_has_nondigit |
| && input[accumulated_len..].starts_with('0') |
| { |
| return Err(Error::new(ErrorKind::LeadingZero(pos))); |
| } |
| accumulated_len += segment_len; |
| if boundary == Some(&b'.') { |
| accumulated_len += 1; |
| segment_len = 0; |
| segment_has_nondigit = false; |
| } else { |
| return Ok(input.split_at(accumulated_len)); |
| } |
| } |
| } |
| } |
| } |
| |
| fn op(input: &str) -> (Op, &str) { |
| let bytes = input.as_bytes(); |
| if bytes.first() == Some(&b'=') { |
| (Op::Exact, &input[1..]) |
| } else if bytes.first() == Some(&b'>') { |
| if bytes.get(1) == Some(&b'=') { |
| (Op::GreaterEq, &input[2..]) |
| } else { |
| (Op::Greater, &input[1..]) |
| } |
| } else if bytes.first() == Some(&b'<') { |
| if bytes.get(1) == Some(&b'=') { |
| (Op::LessEq, &input[2..]) |
| } else { |
| (Op::Less, &input[1..]) |
| } |
| } else if bytes.first() == Some(&b'~') { |
| (Op::Tilde, &input[1..]) |
| } else if bytes.first() == Some(&b'^') { |
| (Op::Caret, &input[1..]) |
| } else { |
| (Op::DEFAULT, input) |
| } |
| } |
| |
| fn comparator(input: &str) -> Result<(Comparator, Position, &str), Error> { |
| let (mut op, text) = op(input); |
| let default_op = input.len() == text.len(); |
| let text = text.trim_start_matches(' '); |
| |
| let mut pos = Position::Major; |
| let (major, text) = numeric_identifier(text, pos)?; |
| let mut has_wildcard = false; |
| |
| let (minor, text) = if let Some(text) = text.strip_prefix('.') { |
| pos = Position::Minor; |
| if let Some((_, text)) = wildcard(text) { |
| has_wildcard = true; |
| if default_op { |
| op = Op::Wildcard; |
| } |
| (None, text) |
| } else { |
| let (minor, text) = numeric_identifier(text, pos)?; |
| (Some(minor), text) |
| } |
| } else { |
| (None, text) |
| }; |
| |
| let (patch, text) = if let Some(text) = text.strip_prefix('.') { |
| pos = Position::Patch; |
| if let Some((_, text)) = wildcard(text) { |
| if default_op { |
| op = Op::Wildcard; |
| } |
| (None, text) |
| } else if has_wildcard { |
| return Err(Error::new(ErrorKind::UnexpectedAfterWildcard)); |
| } else { |
| let (patch, text) = numeric_identifier(text, pos)?; |
| (Some(patch), text) |
| } |
| } else { |
| (None, text) |
| }; |
| |
| let (pre, text) = if patch.is_some() && text.starts_with('-') { |
| pos = Position::Pre; |
| let text = &text[1..]; |
| let (pre, text) = prerelease_identifier(text)?; |
| if pre.is_empty() { |
| return Err(Error::new(ErrorKind::EmptySegment(pos))); |
| } |
| (pre, text) |
| } else { |
| (Prerelease::EMPTY, text) |
| }; |
| |
| let text = if patch.is_some() && text.starts_with('+') { |
| pos = Position::Build; |
| let text = &text[1..]; |
| let (build, text) = build_identifier(text)?; |
| if build.is_empty() { |
| return Err(Error::new(ErrorKind::EmptySegment(pos))); |
| } |
| text |
| } else { |
| text |
| }; |
| |
| let text = text.trim_start_matches(' '); |
| |
| let comparator = Comparator { |
| op, |
| major, |
| minor, |
| patch, |
| pre, |
| }; |
| |
| Ok((comparator, pos, text)) |
| } |
| |
| fn version_req(input: &str, out: &mut Vec<Comparator>, depth: usize) -> Result<usize, Error> { |
| let (comparator, pos, text) = match comparator(input) { |
| Ok(success) => success, |
| Err(mut error) => { |
| if let Some((ch, mut rest)) = wildcard(input) { |
| rest = rest.trim_start_matches(' '); |
| if rest.is_empty() || rest.starts_with(',') { |
| error.kind = ErrorKind::WildcardNotTheOnlyComparator(ch); |
| } |
| } |
| return Err(error); |
| } |
| }; |
| |
| if text.is_empty() { |
| out.reserve_exact(depth + 1); |
| unsafe { out.as_mut_ptr().add(depth).write(comparator) } |
| return Ok(depth + 1); |
| } |
| |
| let text = if let Some(text) = text.strip_prefix(',') { |
| text.trim_start_matches(' ') |
| } else { |
| let unexpected = text.chars().next().unwrap(); |
| return Err(Error::new(ErrorKind::ExpectedCommaFound(pos, unexpected))); |
| }; |
| |
| const MAX_COMPARATORS: usize = 32; |
| if depth + 1 == MAX_COMPARATORS { |
| return Err(Error::new(ErrorKind::ExcessiveComparators)); |
| } |
| |
| // Recurse to collect parsed Comparator objects on the stack. We perform a |
| // single allocation to allocate exactly the right sized Vec only once the |
| // total number of comparators is known. |
| let len = version_req(text, out, depth + 1)?; |
| unsafe { out.as_mut_ptr().add(depth).write(comparator) } |
| Ok(len) |
| } |