#![deny(
bad_style,
const_err,
dead_code,
improper_ctypes,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
no_mangle_generic_items,
non_shorthand_field_patterns,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
rust_2018_idioms,
trivial_casts,
trivial_numeric_casts,
unconditional_recursion,
unsafe_code,
unused_allocation,
unused_comparisons,
unused_extern_crates,
unused_import_braces,
unused_parens,
unused_qualifications,
unused_results,
unused,
while_true
)]
use std::{fmt::Display, ops::Range};
pub fn parse<'input, V>(input: &'input str) -> Result<V::Out, Error<'input>>
where
V: VersionBuilder<'input>,
{
parse_internal::<V>(input, &DFA, &LOOKUP)
}
pub trait VersionBuilder<'input> {
type Out;
fn new() -> Self;
#[allow(unused)]
fn set_major(&mut self, major: u64) {}
#[allow(unused)]
fn set_minor(&mut self, minor: u64) {}
#[allow(unused)]
fn set_patch(&mut self, patch: u64) {}
#[allow(unused)]
fn add_additional(&mut self, num: u64) {}
#[allow(unused)]
fn add_pre_release(&mut self, pre_release: &'input str) {}
#[allow(unused)]
fn add_build(&mut self, build: &'input str) {}
fn build(self) -> Self::Out;
}
#[cfg(any(test, feature = "semver", feature = "semver10",))]
fn try_num(s: &str) -> Result<u64, &str> {
match s.parse::<u64>() {
Ok(num) if !s.starts_with('0') || s == "0" => Ok(num),
_ => Err(s),
}
}
#[cfg(any(test, feature = "semver"))]
fn try_num_semver(s: &str) -> semver::Identifier {
try_num(s).map_err(String::from).map_or_else(
semver::Identifier::AlphaNumeric,
semver::Identifier::Numeric,
)
}
#[cfg(feature = "semver10")]
fn try_num_semver10(s: &str) -> semver10::Identifier {
try_num(s).map_err(String::from).map_or_else(
semver10::Identifier::AlphaNumeric,
semver10::Identifier::Numeric,
)
}
#[cfg(any(test, feature = "semver"))]
impl<'input> VersionBuilder<'input> for semver::Version {
type Out = Self;
fn new() -> Self {
semver::Version::new(0, 0, 0)
}
fn set_major(&mut self, major: u64) {
self.major = major;
}
fn set_minor(&mut self, minor: u64) {
self.minor = minor;
}
fn set_patch(&mut self, patch: u64) {
self.patch = patch;
}
fn add_additional(&mut self, num: u64) {
self.build.push(semver::Identifier::Numeric(num))
}
fn add_pre_release(&mut self, pre_release: &'input str) {
self.pre.push(try_num_semver(pre_release))
}
fn add_build(&mut self, build: &'input str) {
self.build.push(try_num_semver(build))
}
fn build(self) -> Self::Out {
self
}
}
#[cfg(feature = "semver10")]
impl<'input> VersionBuilder<'input> for semver10::Version {
type Out = Self;
fn new() -> Self {
semver10::Version::new(0, 0, 0)
}
fn set_major(&mut self, major: u64) {
self.major = major;
}
fn set_minor(&mut self, minor: u64) {
self.minor = minor;
}
fn set_patch(&mut self, patch: u64) {
self.patch = patch;
}
fn add_additional(&mut self, num: u64) {
self.build.push(semver10::Identifier::Numeric(num))
}
fn add_pre_release(&mut self, pre_release: &'input str) {
self.pre.push(try_num_semver10(pre_release))
}
fn add_build(&mut self, build: &'input str) {
self.build.push(try_num_semver10(build))
}
fn build(self) -> Self::Out {
self
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Error<'input> {
input: &'input str,
span: Span,
error: ErrorKind,
}
impl<'input> Error<'input> {
#[inline]
pub fn owned(&self) -> OwnedError {
OwnedError {
input: self.input.into(),
span: self.span,
error: self.error,
}
}
#[inline]
pub fn input(&self) -> &'input str {
self.input
}
#[inline]
pub fn error_span(&self) -> Range<usize> {
self.span.into()
}
#[inline]
pub fn error_kind(&self) -> ErrorKind {
self.error
}
#[inline]
pub fn erroneous_input(&self) -> &'input str {
&self.input[self.error_span()]
}
pub fn error_line(&self) -> String {
match &self.error {
ErrorKind::MissingMajorNumber => {
String::from("Could not parse the major identifier: No input")
}
ErrorKind::MissingMinorNumber => {
String::from("Could not parse the minor identifier: No input")
}
ErrorKind::MissingPatchNumber => {
String::from("Could not parse the patch identifier: No input")
}
ErrorKind::MissingPreRelease => {
String::from("Could not parse the pre-release identifier: No input")
}
ErrorKind::MissingBuild => {
String::from("Could not parse the build identifier: No input")
}
ErrorKind::NumberOverflow => format!(
"The value `{}` overflows the range of an u64",
self.erroneous_input()
),
ErrorKind::UnexpectedInput => format!("Unexpected `{}`", self.erroneous_input()),
}
}
pub fn indicate_erroneous_input(&self) -> String {
format!(
"{0:~<start$}{0:^<width$}",
"",
start = self.span.start,
width = (self.span.end - self.span.start)
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct OwnedError {
input: String,
span: Span,
error: ErrorKind,
}
impl OwnedError {
pub fn borrowed(&self) -> Error<'_> {
Error {
input: &self.input,
span: self.span,
error: self.error,
}
}
#[inline]
pub fn input(&self) -> &str {
self.borrowed().input()
}
#[inline]
pub fn error_span(&self) -> Range<usize> {
self.borrowed().error_span()
}
#[inline]
pub fn error_kind(&self) -> ErrorKind {
self.borrowed().error_kind()
}
#[inline]
pub fn erroneous_input(&self) -> &str {
self.borrowed().erroneous_input()
}
#[inline]
pub fn error_line(&self) -> String {
self.borrowed().error_line()
}
#[inline]
pub fn indicate_erroneous_input(&self) -> String {
self.borrowed().indicate_erroneous_input()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum ErrorKind {
MissingMajorNumber,
MissingMinorNumber,
MissingPatchNumber,
MissingPreRelease,
MissingBuild,
NumberOverflow,
UnexpectedInput,
}
impl Display for Error<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&self.error_line())?;
if f.alternate() {
writeln!(f)?;
writeln!(f, "| {}", self.input)?;
writeln!(f, "| {}", self.indicate_erroneous_input())?;
}
Ok(())
}
}
impl Display for OwnedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.borrowed().fmt(f)
}
}
impl std::error::Error for Error<'_> {}
impl std::error::Error for OwnedError {}
impl PartialOrd for Error<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.error.partial_cmp(&other.error)
}
}
impl Ord for Error<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.error.cmp(&other.error)
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
struct Span {
start: usize,
end: usize,
}
impl Span {
fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
}
impl From<Span> for Range<usize> {
fn from(s: Span) -> Self {
s.start..s.end
}
}
fn is_release_identifier(v: &str) -> bool {
v == "r" || eq_bytes_ignore_case(v, "final") || eq_bytes_ignore_case(v, "release")
}
fn eq_bytes_ignore_case(left: &str, right: &str) -> bool {
if left.len() != right.len() {
return false;
}
left.bytes()
.map(|c| c.to_ascii_lowercase())
.zip(right.bytes())
.all(|(c1, c2)| c1 == c2)
}
macro_rules! accept {
(
$op: ident,
$index: ident,
$b: ident,
$state: ident,
$new_state: ident,
$start: ident,
$v: ident,
$num: ident,
$input: ident
) => {
#[allow(unused_assignments)]
{
match $op {
Accept::None => {}
Accept::SaveStart => {
$start = $index;
}
Accept::Major => match $input[$start..$index].parse::<u64>() {
Ok(num) => $v.set_major(num),
_ => {
return Err(Error {
input: $input,
error: ErrorKind::NumberOverflow,
span: Span::new($start, $index),
});
}
},
Accept::Minor => {
let segment = &$input[$start..$index];
match segment.parse::<u64>() {
Ok(num) => $v.set_minor(num),
_ => {
$v.add_pre_release(segment);
if $new_state < State::ExpectPre {
$new_state = State::ExpectPre;
}
}
}
}
#[cfg(feature = "strict")]
Accept::RequireMinor => match $input[$start..$index].parse::<u64>() {
Ok(num) => $v.set_minor(num),
_ => {
return Err(Error {
input: $input,
error: ErrorKind::NumberOverflow,
span: Span::new($start, $index),
});
}
},
Accept::Patch => {
let segment = &$input[$start..$index];
match segment.parse::<u64>() {
Ok(num) => $v.set_patch(num),
_ => {
$v.add_pre_release(segment);
if $new_state < State::ExpectPre {
$new_state = State::ExpectPre;
}
}
}
}
#[cfg(feature = "strict")]
Accept::RequirePatch => match $input[$start..$index].parse::<u64>() {
Ok(num) => $v.set_patch(num),
_ => {
return Err(Error {
input: $input,
error: ErrorKind::NumberOverflow,
span: Span::new($start, $index),
});
}
},
Accept::Add => {
let segment = &$input[$start..$index];
match segment.parse::<u64>() {
Ok(num) => $v.add_additional(num),
_ => {
$v.add_pre_release(segment);
if $new_state < State::ExpectPre {
$new_state = State::ExpectPre;
}
}
}
}
Accept::Pre => {
let segment = &$input[$start..$index];
if is_release_identifier(segment) {
if $new_state < State::EndOfInput {
return Err(Error {
input: $input,
error: ErrorKind::UnexpectedInput,
span: span_from($input, $index),
});
}
$v.add_build(segment);
} else {
$v.add_pre_release(segment);
}
}
Accept::Build => {
$v.add_build(&$input[$start..$index]);
}
Accept::ErrorMissing => {
let span = if $index < $input.len() {
span_from($input, $index)
} else {
Span::new($index, $index)
};
let error = match $state {
State::ExpectMajor | State::RequireMajor => ErrorKind::MissingMajorNumber,
State::ExpectMinor | State::ParseMajor => ErrorKind::MissingMinorNumber,
State::ExpectPatch | State::ParseMinor => ErrorKind::MissingPatchNumber,
State::ExpectAdd | State::ExpectPre => ErrorKind::MissingPreRelease,
State::ExpectBuild => ErrorKind::MissingBuild,
_ => unreachable!(),
};
return Err(Error {
input: $input,
error,
span,
});
}
Accept::ErrorUnexpected => {
return Err(Error {
input: $input,
error: ErrorKind::UnexpectedInput,
span: span_from($input, $index),
});
}
}
}
};
}
#[inline]
fn parse_internal<'input, V>(
input: &'input str,
dfa: &Dfa,
lookup: &Lookup,
) -> Result<V::Out, Error<'input>>
where
V: VersionBuilder<'input>,
{
let mut start = 0_usize;
let mut v = V::new();
let mut state = State::ExpectMajor;
for (index, b) in input.bytes().enumerate() {
let (mut new_state, accepts) = transduce(lookup, dfa, state, b);
accept!(accepts, index, b, state, new_state, start, v, num, input);
state = new_state;
}
let (mut new_state, accepts) = transition(dfa, state, Class::EndOfInput);
let index = input.len();
accept!(accepts, index, b, state, new_state, start, v, num, input);
Ok(v.build())
}
const MAGIC: u64 = 0b11_1010_0101_0101_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000;
const fn utf8_len(input: u8) -> usize {
((MAGIC >> ((input >> 3) << 1)) & 0b11) as usize + 1
}
const STATES: usize = 15;
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[repr(u8)]
enum State {
ExpectMajor,
ParseMajor,
ExpectMinor,
ParseMinor,
ExpectPatch,
ParsePatch,
ExpectAdd,
ParseAdd,
ExpectPre,
ParsePre,
ExpectBuild,
ParseBuild,
RequireMajor,
EndOfInput,
Error,
}
impl Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&format!("{:?}", self))
}
}
const CLASSES: usize = 9;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[repr(u8)]
enum Class {
Number,
Alpha,
Dot,
Hyphen,
Plus,
V,
Whitespace,
EndOfInput,
Unexpected,
}
impl Display for Class {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Class::Number => f.pad("[0-9]"),
Class::Alpha => f.pad("[a-zA-Z]"),
Class::Dot => f.pad("[.]"),
Class::Hyphen => f.pad("[-]"),
Class::Plus => f.pad("[+]"),
Class::V => f.pad("[vV]"),
Class::Whitespace => f.pad("<whitespace>"),
Class::EndOfInput => f.pad("EOI"),
Class::Unexpected => f.pad("<otherwise>"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
enum Accept {
None,
SaveStart,
Major,
Minor,
#[cfg(feature = "strict")]
RequireMinor,
Patch,
#[cfg(feature = "strict")]
RequirePatch,
Add,
Pre,
Build,
ErrorMissing,
ErrorUnexpected,
}
type Lookup = [Class; 256];
const fn class_lookup() -> Lookup {
let error_class = Class::Unexpected;
let mut table = [error_class; 256];
let mut c = 0_usize;
while c <= 0xFF {
let class = match c as u8 as char {
'.' => Class::Dot,
'-' => Class::Hyphen,
'+' => Class::Plus,
'v' | 'V' => Class::V,
'0'..='9' => Class::Number,
'A'..='Z' | 'a'..='z' => Class::Alpha,
'\t' | '\n' | '\x0C' | '\r' | ' ' => Class::Whitespace,
_ => Class::Unexpected,
};
table[c] = class;
c += 1;
}
table
}
#[inline(always)]
const fn dfa_index(s: State, c: Class) -> u8 {
(s as u8) * (CLASSES as u8) + (c as u8)
}
macro_rules! dfa {
($transitions:ident: $($($cls:ident)|+ @ $state:ident -> $target:ident),+,) => {
$(
$(
$transitions[dfa_index(State::$state, Class::$cls) as usize] = State::$target;
)+
)+
};
}
macro_rules! accept {
($accepts:ident: $($($cls:ident)|+ @ $state:ident -> $accept:ident),+,) => {
$(
$(
$accepts[dfa_index(State::$state, Class::$cls) as usize] = Accept::$accept;
)+
)+
};
}
type Dfa = ([State; STATES * CLASSES], [Accept; STATES * CLASSES]);
const fn dfa() -> Dfa {
let mut transitions = [State::Error; STATES * CLASSES];
dfa!(transitions:
Whitespace @ ExpectMajor -> ExpectMajor,
V @ ExpectMajor -> RequireMajor,
Number @ ExpectMajor -> ParseMajor,
Number @ RequireMajor -> ParseMajor,
Whitespace | EndOfInput @ ParseMajor -> EndOfInput,
Number @ ParseMajor -> ParseMajor,
Dot @ ParseMajor -> ExpectMinor,
Hyphen @ ParseMajor -> ExpectPre,
Plus @ ParseMajor -> ExpectBuild,
Number @ ExpectMinor -> ParseMinor,
Alpha | V @ ExpectMinor -> ParsePre,
Whitespace | EndOfInput @ ParseMinor -> EndOfInput,
Number @ ParseMinor -> ParseMinor,
Alpha | V @ ParseMinor -> ParsePre,
Dot @ ParseMinor -> ExpectPatch,
Hyphen @ ParseMinor -> ExpectPre,
Plus @ ParseMinor -> ExpectBuild,
Number @ ExpectPatch -> ParsePatch,
Alpha | V @ ExpectPatch -> ParsePre,
Whitespace | EndOfInput @ ParsePatch -> EndOfInput,
Number @ ParsePatch -> ParsePatch,
Alpha | V @ ParsePatch -> ParsePre,
Dot @ ParsePatch -> ExpectAdd,
Hyphen @ ParsePatch -> ExpectPre,
Plus @ ParsePatch -> ExpectBuild,
Number @ ExpectAdd -> ParseAdd,
Alpha | V @ ExpectAdd -> ParsePre,
Whitespace | EndOfInput @ ParseAdd -> EndOfInput,
Number @ ParseAdd -> ParseAdd,
Alpha | V @ ParseAdd -> ParsePre,
Dot @ ParseAdd -> ExpectAdd,
Hyphen @ ParseAdd -> ExpectPre,
Plus @ ParseAdd -> ExpectBuild,
Number | Alpha | V @ ExpectPre -> ParsePre,
Whitespace | EndOfInput @ ParsePre -> EndOfInput,
Number | Alpha | V @ ParsePre -> ParsePre,
Dot | Hyphen @ ParsePre -> ExpectPre,
Plus @ ParsePre -> ExpectBuild,
Number | Alpha | V @ ExpectBuild -> ParseBuild,
Whitespace | EndOfInput @ ParseBuild -> EndOfInput,
Number | Alpha | V @ ParseBuild -> ParseBuild,
Dot | Hyphen @ ParseBuild -> ExpectBuild,
Whitespace | EndOfInput @ EndOfInput -> EndOfInput,
);
let mut accepts = [Accept::None; STATES * CLASSES];
accept!(accepts:
Number @ ExpectMajor -> SaveStart,
EndOfInput @ ExpectMajor -> ErrorMissing,
Alpha | Dot | Hyphen | Plus | Unexpected @ ExpectMajor -> ErrorUnexpected,
Number @ RequireMajor -> SaveStart,
EndOfInput @ RequireMajor -> ErrorMissing,
Whitespace | Alpha | V | Dot | Hyphen | Plus | Unexpected @ RequireMajor -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParseMajor -> Major,
Alpha | V | Unexpected @ ParseMajor -> ErrorUnexpected,
Number | V | Alpha @ ExpectMinor -> SaveStart,
EndOfInput @ ExpectMinor -> ErrorMissing,
Whitespace | Dot | Hyphen | Plus | Unexpected @ ExpectMinor -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParseMinor -> Minor,
Unexpected @ ParseMinor -> ErrorUnexpected,
Number | V | Alpha @ ExpectPatch -> SaveStart,
EndOfInput @ ExpectPatch -> ErrorMissing,
Whitespace | Dot | Hyphen | Plus | Unexpected @ ExpectPatch -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParsePatch -> Patch,
Unexpected @ ParsePatch -> ErrorUnexpected,
Number | V | Alpha @ ExpectAdd -> SaveStart,
EndOfInput @ ExpectAdd -> ErrorMissing,
Whitespace | Dot | Hyphen | Plus | Unexpected @ ExpectAdd -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParseAdd -> Add,
Unexpected @ ParseAdd -> ErrorUnexpected,
Number | V | Alpha @ ExpectPre -> SaveStart,
EndOfInput @ ExpectPre -> ErrorMissing,
Whitespace | Dot | Hyphen | Plus | Unexpected @ ExpectPre -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParsePre -> Pre,
Unexpected @ ParsePre -> ErrorUnexpected,
Number | V | Alpha @ ExpectBuild -> SaveStart,
EndOfInput @ ExpectBuild -> ErrorMissing,
Whitespace | Dot | Hyphen | Plus | Unexpected @ ExpectBuild -> ErrorUnexpected,
Whitespace | EndOfInput | Dot | Hyphen | Plus @ ParseBuild -> Build,
Unexpected @ ParseBuild -> ErrorUnexpected,
Alpha | V | Number | Dot | Hyphen | Plus | Unexpected @ EndOfInput -> ErrorUnexpected,
);
(transitions, accepts)
}
static LOOKUP: Lookup = class_lookup();
static DFA: Dfa = dfa();
#[inline(always)]
const fn transduce(class_lookup: &Lookup, dfa: &Dfa, s: State, b: u8) -> (State, Accept) {
transition(dfa, s, class_lookup[b as usize])
}
#[inline(always)]
const fn transition(dfa: &Dfa, s: State, class: Class) -> (State, Accept) {
let index = dfa_index(s, class) as usize;
(dfa.0[index], dfa.1[index])
}
#[inline]
fn span_from(input: &str, index: usize) -> Span {
Span::new(index, index + utf8_len(input.as_bytes()[index]))
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[cfg(feature = "semver")]
#[cfg_attr(feature = "semver", test)]
fn test_parse_semver() {
let actual = parse::<Version>(" v1.2.3.4.5-Foo-bar+baz-qux ").ok();
let expected = semver::Version::parse("1.2.3-Foo.bar+4.5.baz.qux").ok();
assert_eq!(actual, expected)
}
#[cfg(feature = "semver10")]
#[cfg_attr(feature = "semver10", test)]
fn test_parse_semver10() {
let actual = parse::<semver10::Version>(" v1.2.3.4.5-Foo-bar+baz-qux ").ok();
let expected = semver10::Version::parse("1.2.3-Foo.bar+4.5.baz.qux").ok();
assert_eq!(actual, expected)
}
mod semver_helpers {
pub(super) use semver::{Identifier, Version};
pub(super) trait IntoIdentifier {
fn into_itentifier(self) -> Identifier;
}
impl IntoIdentifier for &str {
fn into_itentifier(self) -> Identifier {
Identifier::AlphaNumeric(self.into())
}
}
impl IntoIdentifier for u64 {
fn into_itentifier(self) -> Identifier {
Identifier::Numeric(self)
}
}
}
use semver_helpers::*;
macro_rules! vers {
($major:literal . $minor:literal . $patch:literal) => {
Version {
major: $major,
minor: $minor,
patch: $patch,
pre: Vec::new(),
build: Vec::new(),
}
};
($major:literal . $minor:literal . $patch:literal - $( $pre:literal )-+ ) => {
Version {
major: $major,
minor: $minor,
patch: $patch,
pre: vec![ $( $pre.into_itentifier(), )* ],
build: Vec::new(),
}
};
($major:literal . $minor:literal . $patch:literal + $( $build:literal )-+ ) => {
Version {
major: $major,
minor: $minor,
patch: $patch,
pre: Vec::new(),
build: vec![ $( $build.into_itentifier(), )* ],
}
};
($major:literal . $minor:literal . $patch:literal - $( $pre:literal )-+ + $( $build:literal )-+ ) => {
Version {
major: $major,
minor: $minor,
patch: $patch,
pre: vec![ $( $pre.into_itentifier(), )* ],
build: vec![ $( $build.into_itentifier(), )* ],
}
};
}
#[test_case("1" => Ok(vers!(1 . 0 . 0)); "major only")]
#[test_case("1.2" => Ok(vers!(1 . 2 . 0)); "major.minor only")]
#[test_case("1.2.3" => Ok(vers!(1 . 2 . 3)); "major.minor.patch")]
#[test_case(" 1.2.3 " => Ok(vers!(1 . 2 . 3)); "with whitespace")]
fn test_simple(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.2.3-alpha1" => Ok(vers!(1 . 2 . 3 - "alpha1")))]
#[test_case(" 1.2.3-alpha2 " => Ok(vers!(1 . 2 . 3 - "alpha2")))]
#[test_case("3.1.0-M13-beta3" => Ok(vers!(3 . 1 . 0 - "M13" - "beta3")))]
#[test_case("1.2.3-alpha01.drop02" => Ok(vers!(1 . 2 . 3 - "alpha01" - "drop02")))]
#[test_case("1.4.1-alpha01" => Ok(vers!(1 . 4 . 1 - "alpha01")))]
#[test_case("1.4-alpha02" => Ok(vers!(1 . 4 . 0 - "alpha02")))]
#[test_case("1-alpha03" => Ok(vers!(1 . 0 . 0 - "alpha03")))]
#[test_case("1.9.3.RC1" => Ok(vers!(1 . 9 . 3 - "RC1")))]
#[test_case("1.9.RC2" => Ok(vers!(1 . 9 . 0 - "RC2")))]
#[test_case("1.RC3" => Ok(vers!(1 . 0 . 0 - "RC3")))]
#[test_case("1.3.3-7" => Ok(vers!(1 . 3 . 3 - 7)))]
#[test_case("5.9.0-202009080501-r" => Ok(vers!(5 . 9 . 0 - 202009080501 + "r")))]
#[test_case("1.2.3.RC.4" => Ok(vers!(1 . 2 . 3 - "RC" - 4)))]
fn test_pre_release(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.2.3+build1" => Ok(vers!(1 . 2 . 3 + "build1")))]
#[test_case(" 1.2.3+build2 " => Ok(vers!(1 . 2 . 3 + "build2")))]
#[test_case("3.1.0+build3-r021" => Ok(vers!(3 . 1 . 0 + "build3" - "r021")))]
#[test_case("1.2.3+build01.drop02" => Ok(vers!(1 . 2 . 3 + "build01" - "drop02")))]
#[test_case("1.4.1+build01" => Ok(vers!(1 . 4 . 1 + "build01")))]
#[test_case("1.4+build02" => Ok(vers!(1 . 4 . 0 + "build02")))]
#[test_case("1+build03" => Ok(vers!(1 . 0 . 0 + "build03")))]
#[test_case("7.2.0+28-2f9fb552" => Ok(vers!(7 . 2 . 0 + 28 - "2f9fb552" )))]
#[test_case("1.3.3.7" => Ok(vers!(1 . 3 . 3 + 7)))]
#[test_case("5.9.0.202009080501-r" => Ok(vers!(5 . 9 . 0 + 202009080501 - "r")))]
fn test_build(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.3.3.7" => Ok(vers!(1 . 3 . 3 + 7)))]
#[test_case("1.3.3.0" => Ok(vers!(1 . 3 . 3 + 0)))]
#[test_case("1.3.3.00" => Ok(vers!(1 . 3 . 3 + 0)))]
#[test_case("1.3.3.07" => Ok(vers!(1 . 3 . 3 + 7)))]
#[test_case("1.3.3.7.4.2" => Ok(vers!(1 . 3 . 3 + 7 - 4 - 2)))]
#[test_case("1.3.3.7.04.02" => Ok(vers!(1 . 3 . 3 + 7 - 4 - 2)))]
#[test_case("1.3.3.9876543210987654321098765432109876543210" => Ok(vers!(1 . 3 . 3 - "9876543210987654321098765432109876543210")))]
#[test_case("1.3.3.9876543210987654321098765432109876543210.4.2" => Ok(vers!(1 . 3 . 3 - "9876543210987654321098765432109876543210" - 4 - 2)))]
#[test_case("1.3.3.7.foo" => Ok(vers!(1 . 3 . 3 - "foo" + 7)))]
#[test_case("1.3.3.7-bar" => Ok(vers!(1 . 3 . 3 - "bar" + 7)))]
#[test_case("1.3.3.7+baz" => Ok(vers!(1 . 3 . 3 + 7 - "baz")))]
fn test_additional_numbers(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.2.3-alpha1+build5" => Ok(vers!(1 . 2 . 3 - "alpha1" + "build5" )))]
#[test_case(" 1.2.3-alpha2+build6 " => Ok(vers!(1 . 2 . 3 - "alpha2" + "build6" )))]
#[test_case("1.2.3-1.alpha1.9+build5.7.3aedf " => Ok(vers!(1 . 2 . 3 - 1 - "alpha1" - 9 + "build5" - 7 - "3aedf" )))]
#[test_case("0.4.0-beta.1+0851523" => Ok(vers!(0 . 4 . 0 - "beta" - 1 + "0851523" )))]
fn test_combined(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("2.7.3.Final" => Ok(vers!(2 . 7 . 3 + "Final" )); "full dot final")]
#[test_case("2.7.3-Final" => Ok(vers!(2 . 7 . 3 + "Final" )); "full hyphen final")]
#[test_case("2.7.3+Final" => Ok(vers!(2 . 7 . 3 + "Final" )); "full plus final")]
#[test_case("2.7.3.Release" => Ok(vers!(2 . 7 . 3 + "Release" )); "full dot release")]
#[test_case("2.7.3-Release" => Ok(vers!(2 . 7 . 3 + "Release" )); "full hyphen release")]
#[test_case("2.7.3+Release" => Ok(vers!(2 . 7 . 3 + "Release" )); "full plus release")]
#[test_case("2.7.Final" => Ok(vers!(2 . 7 . 0 + "Final" )); "minor dot final")]
#[test_case("2.7-Final" => Ok(vers!(2 . 7 . 0 + "Final" )); "minor hyphen final")]
#[test_case("2.7+Final" => Ok(vers!(2 . 7 . 0 + "Final" )); "minor plus final")]
#[test_case("2.7.Release" => Ok(vers!(2 . 7 . 0 + "Release" )); "minor dot release")]
#[test_case("2.7-Release" => Ok(vers!(2 . 7 . 0 + "Release" )); "minor hyphen release")]
#[test_case("2.7+Release" => Ok(vers!(2 . 7 . 0 + "Release" )); "minor plus release")]
#[test_case("2.Final" => Ok(vers!(2 . 0 . 0 + "Final" )); "major dot final")]
#[test_case("2-Final" => Ok(vers!(2 . 0 . 0 + "Final" )); "major hyphen final")]
#[test_case("2+Final" => Ok(vers!(2 . 0 . 0 + "Final" )); "major plus final")]
#[test_case("2.Release" => Ok(vers!(2 . 0 . 0 + "Release" )); "major dot release")]
#[test_case("2-Release" => Ok(vers!(2 . 0 . 0 + "Release" )); "major hyphen release")]
#[test_case("2+Release" => Ok(vers!(2 . 0 . 0 + "Release" )); "major plus release")]
#[test_case("2.7.3.r" => Ok(vers!(2 . 7 . 3 + "r" )); "full dot r")]
#[test_case("2.7.3-r" => Ok(vers!(2 . 7 . 3 + "r" )); "full hyphen r")]
#[test_case("2.7.3+r" => Ok(vers!(2 . 7 . 3 + "r" )); "full plus r")]
#[test_case("2.7.r" => Ok(vers!(2 . 7 . 0 + "r" )); "minor dot r")]
#[test_case("2.7-r" => Ok(vers!(2 . 7 . 0 + "r" )); "minor hyphen r")]
#[test_case("2.7+r" => Ok(vers!(2 . 7 . 0 + "r" )); "minor plus r")]
#[test_case("2.r" => Ok(vers!(2 . 0 . 0 + "r" )); "major dot r")]
#[test_case("2-r" => Ok(vers!(2 . 0 . 0 + "r" )); "major hyphen r")]
#[test_case("2+r" => Ok(vers!(2 . 0 . 0 + "r" )); "major plus r")]
fn test_with_release_identifier(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("2020.4.9" => Ok(vers!(2020 . 4 . 9)))]
#[test_case("2020.04.09" => Ok(vers!(2020 . 4 . 9)))]
#[test_case("2020.4" => Ok(vers!(2020 . 4 . 0)))]
#[test_case("2020.04" => Ok(vers!(2020 . 4 . 0)))]
fn test_date_versions(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1" => Ok(vers!(1 . 0 . 0)))]
#[test_case("01" => Ok(vers!(1 . 0 . 0)))]
#[test_case("00001" => Ok(vers!(1 . 0 . 0)))]
#[test_case("1.2.3-1" => Ok(vers!(1 . 2 . 3 - 1)))]
#[test_case("1.2.3-01" => Ok(vers!(1 . 2 . 3 - "01")))]
#[test_case("1.2.3-0001" => Ok(vers!(1 . 2 . 3 - "0001")))]
#[test_case("2.3.4+1" => Ok(vers!(2 . 3 . 4 + 1)))]
#[test_case("2.3.4+01" => Ok(vers!(2 . 3 . 4 + "01")))]
#[test_case("2.3.4+0001" => Ok(vers!(2 . 3 . 4 + "0001")))]
fn test_leading_zeroes(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("v1" => Ok(vers!(1 . 0 . 0)))]
#[test_case(" v2 " => Ok(vers!(2 . 0 . 0)))]
#[test_case("v1.2.3" => Ok(vers!(1 . 2 . 3)))]
#[test_case("v1.3.3-7" => Ok(vers!(1 . 3 . 3 - 7)))]
#[test_case("V3" => Ok(vers!(3 . 0 . 0)))]
#[test_case(" V5 " => Ok(vers!(5 . 0 . 0)))]
#[test_case("V2.3.4" => Ok(vers!(2 . 3 . 4)))]
#[test_case("V4.2.4-2" => Ok(vers!(4 . 2 . 4 - 2)))]
fn test_leading_v(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.v2" => Ok(vers!(1 . 0 . 0 - "v2")))]
#[test_case("1-v3" => Ok(vers!(1 . 0 . 0 - "v3")))]
#[test_case("1+v4" => Ok(vers!(1 . 0 . 0 + "v4")))]
#[test_case("1.2.v3" => Ok(vers!(1 . 2 . 0 - "v3")))]
#[test_case("1.2-v4" => Ok(vers!(1 . 2 . 0 - "v4")))]
#[test_case("1.2+v5" => Ok(vers!(1 . 2 . 0 + "v5")))]
#[test_case("1.2.3.v4" => Ok(vers!(1 . 2 . 3 - "v4")))]
#[test_case("1.2.3-v5" => Ok(vers!(1 . 2 . 3 - "v5")))]
#[test_case("1.2.3+v6" => Ok(vers!(1 . 2 . 3 + "v6")))]
#[test_case("1.2.3-alpha.v4" => Ok(vers!(1 . 2 . 3 - "alpha" - "v4")))]
#[test_case("1.2.3-alpha-v5" => Ok(vers!(1 . 2 . 3 - "alpha" - "v5")))]
#[test_case("1.2.3-alpha+v6" => Ok(vers!(1 . 2 . 3 - "alpha" + "v6")))]
#[test_case("1.2.3+build.v4" => Ok(vers!(1 . 2 . 3 + "build" - "v4")))]
#[test_case("1.2.3+build-v5" => Ok(vers!(1 . 2 . 3 + "build" - "v5")))]
#[test_case("1.2.3-alpha.v6+build.v7" => Ok(vers!(1 . 2 . 3 - "alpha" - "v6" + "build" - "v7")))]
fn test_v_num_in_the_middle(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("1.9876543210987654321098765432109876543210" => Ok(vers!(1 . 0 . 0 - "9876543210987654321098765432109876543210")))]
#[test_case("1.9876543210987654321098765432109876543210.2" => Ok(vers!(1 . 0 . 0 - "9876543210987654321098765432109876543210" - 2)))]
#[test_case("1.2.9876543210987654321098765432109876543210" => Ok(vers!(1 . 2 . 0 - "9876543210987654321098765432109876543210")))]
#[test_case("1.2.3.9876543210987654321098765432109876543210" => Ok(vers!(1 . 2 . 3 - "9876543210987654321098765432109876543210")))]
#[test_case("1.2.3-9876543210987654321098765432109876543211" => Ok(vers!(1 . 2 . 3 - "9876543210987654321098765432109876543211")))]
#[test_case("1.2.3+9876543210987654321098765432109876543213" => Ok(vers!(1 . 2 . 3 + "9876543210987654321098765432109876543213")))]
fn test_too_large_numbers(input: &str) -> Result<Version, Error<'_>> {
parse::<Version>(input)
}
#[test_case("" => Err((ErrorKind::MissingMajorNumber, Span::new(0, 0))); "empty input")]
#[test_case(" " => Err((ErrorKind::MissingMajorNumber, Span::new(2, 2))); "whitespace input")]
#[test_case("." => Err((ErrorKind::UnexpectedInput, Span::new(0, 1))); "dot")]
#[test_case("🙈" => Err((ErrorKind::UnexpectedInput, Span::new(0, 4))); "emoji")]
#[test_case("v" => Err((ErrorKind::MissingMajorNumber, Span::new(1, 1))); "v")]
#[test_case("val" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "val")]
#[test_case("v🙈" => Err((ErrorKind::UnexpectedInput, Span::new(1, 5))); "v-emoji")]
#[test_case("1🙈" => Err((ErrorKind::UnexpectedInput, Span::new(1, 5))); "1-emoji")]
#[test_case("1." => Err((ErrorKind::MissingMinorNumber, Span::new(2, 2))); "eoi after major")]
#[test_case("1. " => Err((ErrorKind::UnexpectedInput, Span::new(2, 3))); "whitespace after major")]
#[test_case("1.2." => Err((ErrorKind::MissingPatchNumber, Span::new(4, 4))); "eoi after minor")]
#[test_case("1.2. " => Err((ErrorKind::UnexpectedInput, Span::new(4, 5))); "whitespace after minor")]
#[test_case("1.2.3-" => Err((ErrorKind::MissingPreRelease, Span::new(6, 6))); "eoi after hyphen")]
#[test_case("1.2.3- " => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "whitespace after hyphen")]
#[test_case("1.2.3.4-" => Err((ErrorKind::MissingPreRelease, Span::new(8, 8))); "eoi after additional")]
#[test_case("1.2.3.4- " => Err((ErrorKind::UnexpectedInput, Span::new(8, 9))); "whitespace after additional")]
#[test_case("1.2.3+" => Err((ErrorKind::MissingBuild, Span::new(6, 6))); "eoi after plus")]
#[test_case("1.2.3+ " => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "whitespace after plus")]
#[test_case("1.2.3-." => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "pre release trailing dot")]
#[test_case("1.2.3--" => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "pre release trailing hyphen")]
#[test_case("1.2.3-+" => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "pre release trailing plus")]
#[test_case("1.2.3-🙈" => Err((ErrorKind::UnexpectedInput, Span::new(6, 10))); "pre release trailing emoji")]
#[test_case("1.2.3+." => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "build trailing dot")]
#[test_case("1.2.3+-" => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "build trailing hyphen")]
#[test_case("1.2.3++" => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "build trailing plus")]
#[test_case("1.2.3-🙈" => Err((ErrorKind::UnexpectedInput, Span::new(6, 10))); "build trailing emoji")]
#[test_case("v.1.2.3" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "v followed by dot")]
#[test_case("v-2.3.4" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "v followed by hyphen")]
#[test_case("v+3.4.5" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "v followed by plus")]
#[test_case("vv1.2.3" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "v followed by v")]
#[test_case("v v1.2.3" => Err((ErrorKind::UnexpectedInput, Span::new(1, 2))); "v followed by whitespace")]
#[test_case("a1.2.3" => Err((ErrorKind::UnexpectedInput, Span::new(0, 1))); "starting with a-1")]
#[test_case("a.b.c" => Err((ErrorKind::UnexpectedInput, Span::new(0, 1))); "starting with a-dot")]
#[test_case("1.+.0" => Err((ErrorKind::UnexpectedInput, Span::new(2, 3))); "plus as minor")]
#[test_case("1.2.." => Err((ErrorKind::UnexpectedInput, Span::new(4, 5))); "dot as patch")]
#[test_case("123456789012345678901234567890" => Err((ErrorKind::NumberOverflow, Span::new(0, 30))); "number overflows u64")]
#[test_case("1 abc" => Err((ErrorKind::UnexpectedInput, Span::new(2, 3))); "a following parsed number 1")]
#[test_case("1.2.3 abc" => Err((ErrorKind::UnexpectedInput, Span::new(6, 7))); "a following parsed number 1.2.3")]
fn test_simple_errors(input: &str) -> Result<Version, (ErrorKind, Span)> {
parse::<Version>(input).map_err(|e| (e.error, e.span))
}
#[test_case("" => r#"Could not parse the major identifier: No input
|
|
"#; "empty string")]
#[test_case(" " => r#"Could not parse the major identifier: No input
|
| ~~
"#; "blank string")]
#[test_case("1.2.3-" => r#"Could not parse the pre-release identifier: No input
| 1.2.3-
| ~~~~~~
"#)]
#[test_case("1.2.3+" => r#"Could not parse the build identifier: No input
| 1.2.3+
| ~~~~~~
"#)]
#[test_case("a.b.c" => r#"Unexpected `a`
| a.b.c
| ^
"#)]
#[test_case("1.+.0" => r#"Unexpected `+`
| 1.+.0
| ~~^
"#)]
#[test_case("1.2.." => r#"Unexpected `.`
| 1.2..
| ~~~~^
"#)]
#[test_case("123456789012345678901234567890" => r#"The value `123456789012345678901234567890` overflows the range of an u64
| 123456789012345678901234567890
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"#)]
#[test_case("1.2.3 abc" => r#"Unexpected `a`
| 1.2.3 abc
| ~~~~~~^
"#)]
fn test_full_errors(input: &str) -> String {
format!("{:#}", parse::<Version>(input).unwrap_err())
}
#[test_case("Final"; "final pascal")]
#[test_case("FINAL"; "final upper")]
#[test_case("final"; "final lower")]
#[test_case("FiNaL"; "final upper sponge")]
#[test_case("fInAL"; "final lower sponge")]
#[test_case("Release"; "release pascal")]
#[test_case("RELEASE"; "release upper")]
#[test_case("release"; "release lower")]
#[test_case("ReLeAsE"; "release upper sponge")]
#[test_case("rElEAse"; "release lower sponge")]
#[test_case("r"; "release lower r")]
fn test_is_release_identifier(v: &str) {
assert!(is_release_identifier(v));
}
#[test_case("1.2.3" => parse::<Version>("1.2.3.Final"); "dot final")]
#[test_case("1.2.3" => parse::<Version>("1.2.3.Release"); "dot release")]
#[test_case("1.2.3" => parse::<Version>("1.2.3-Final"); "hyphen final")]
#[test_case("1.2.3" => parse::<Version>("1.2.3-Release"); "hyphen release")]
#[test_case("1.2.3" => parse::<Version>("1.2.3+Final"); "plus final")]
#[test_case("1.2.3" => parse::<Version>("1.2.3+Release"); "plus release")]
fn test_release_cmp(v: &str) -> Result<Version, Error<'_>> {
parse::<Version>(v)
}
#[test_case(" ", ' '; "space")]
#[test_case(" ", ' '; "two spaces")]
#[test_case("1", '1'; "singel ascii number")]
#[test_case("123", '1'; "first ascii numbner")]
#[test_case("🦀", '🦀'; "emoji")]
#[test_case("🦀🦀", '🦀'; "multiple emoji")]
#[test_case("🦀🙉🦀", '🦀'; "different emoji")]
#[test_case("🙉🦀🙉", '🙉'; "other emoji")]
#[test_case("3🦀", '3'; "ascii and emoji")]
#[test_case("🦀3", '🦀'; "emoji and ascii")]
#[test_case("∰42", '∰'; "whatever that is")]
#[test_case("ßss", 'ß'; "the esszett")]
#[test_case("äae", 'ä'; "ae")]
#[test_case("åao", 'å'; "ao")]
fn test_utf8_len(input: &str, expected: char) {
let len = utf8_len(input.bytes().next().unwrap());
assert_eq!(
len,
expected.len_utf8(),
"input {} has not the same length as '{}', expected {} but got {} instead",
input,
expected,
len,
expected.len_utf8()
);
}
}
#[cfg(feature = "strict")]
pub mod strict {
use super::*;
pub fn parse<'input, V>(input: &'input str) -> Result<V::Out, Error<'input>>
where
V: VersionBuilder<'input>,
{
parse_internal::<V>(input, &STRICT_DFA, &LOOKUP)
}
static STRICT_DFA: Dfa = strict_dfa();
pub(super) const fn strict_dfa() -> Dfa {
let mut transitions = [State::Error; STATES * CLASSES];
dfa!(transitions:
Whitespace @ ExpectMajor -> ExpectMajor,
Number @ ExpectMajor -> ParseMajor,
Number @ ParseMajor -> ParseMajor,
Dot @ ParseMajor -> ExpectMinor,
Number @ ExpectMinor -> ParseMinor,
Number @ ParseMinor -> ParseMinor,
Dot @ ParseMinor -> ExpectPatch,
Number @ ExpectPatch -> ParsePatch,
Whitespace | EndOfInput @ ParsePatch -> EndOfInput,
Number @ ParsePatch -> ParsePatch,
Hyphen @ ParsePatch -> ExpectPre,
Plus @ ParsePatch -> ExpectBuild,
Number @ ExpectPre -> ParsePre,
V @ ExpectPre -> ParsePre,
Alpha @ ExpectPre -> ParsePre,
Whitespace | EndOfInput @ ParsePre -> EndOfInput,
Number | Alpha | V @ ParsePre -> ParsePre,
Dot @ ParsePre -> ExpectPre,
Plus @ ParsePre -> ExpectBuild,
Number | Alpha | V @ ExpectBuild -> ParseBuild,
Whitespace | EndOfInput @ ParseBuild -> EndOfInput,
Number | Alpha | V @ ParseBuild -> ParseBuild,
Dot @ ParseBuild -> ExpectBuild,
Whitespace | EndOfInput @ EndOfInput -> EndOfInput,
);
let mut accepts = [Accept::None; STATES * CLASSES];
accept!(accepts:
Number @ ExpectMajor -> SaveStart,
EndOfInput @ ExpectMajor -> ErrorMissing,
Alpha | V | Dot | Hyphen | Plus | Unexpected @ ExpectMajor -> ErrorUnexpected,
Dot @ ParseMajor -> Major,
EndOfInput @ ParseMajor -> ErrorMissing,
Alpha | V | Hyphen | Plus | Whitespace | Unexpected @ ParseMajor -> ErrorUnexpected,
Number @ ExpectMinor -> SaveStart,
EndOfInput @ ExpectMinor -> ErrorMissing,
Alpha | V | Dot | Hyphen | Plus | Whitespace | Unexpected @ ExpectMinor -> ErrorUnexpected,
Dot @ ParseMinor -> RequireMinor,
EndOfInput @ ParseMinor -> ErrorMissing,
Alpha | V | Hyphen | Plus | Whitespace | Unexpected @ ParseMinor -> ErrorUnexpected,
Number @ ExpectPatch -> SaveStart,
EndOfInput @ ExpectPatch -> ErrorMissing,
Alpha | V | Dot | Hyphen | Plus | Whitespace | Unexpected @ ExpectPatch -> ErrorUnexpected,
Hyphen | Plus | Whitespace | EndOfInput @ ParsePatch -> RequirePatch,
Alpha | V | Dot | Unexpected @ ParsePatch -> ErrorUnexpected,
Number | V | Alpha @ ExpectPre -> SaveStart,
EndOfInput @ ExpectPre -> ErrorMissing,
Dot | Hyphen | Plus | Whitespace | Unexpected @ ExpectPre -> ErrorUnexpected,
Dot | Plus | Whitespace | EndOfInput @ ParsePre -> Pre,
Hyphen | Unexpected @ ParsePre -> ErrorUnexpected,
Number | V | Alpha @ ExpectBuild -> SaveStart,
EndOfInput @ ExpectBuild -> ErrorMissing,
Dot | Hyphen | Plus | Whitespace | Unexpected @ ExpectBuild -> ErrorUnexpected,
Dot | Whitespace | EndOfInput @ ParseBuild -> Build,
Hyphen | Plus | Unexpected @ ParseBuild -> ErrorUnexpected,
Alpha | V | Number | Dot | Hyphen | Plus | Unexpected @ EndOfInput -> ErrorUnexpected,
);
(transitions, accepts)
}
#[cfg(test)]
mod tests {
use super::*;
use semver::Version;
use test_case::test_case;
#[test_case("1.2.3")]
#[test_case(" 1.2.4 ")]
#[test_case("1.2.3-alpha1")]
#[test_case(" 1.2.3-alpha2 ")]
#[test_case("1.2.3-alpha01.drop02")]
#[test_case("1.4.1-alpha01")]
#[test_case("1.3.3-7")]
#[test_case("1.2.3+build1")]
#[test_case(" 1.2.3+build2 ")]
#[test_case("1.2.3+build01.drop02")]
#[test_case("1.4.1+build01")]
#[test_case("1.2.3-alpha1+build5")]
#[test_case(" 1.2.3-alpha2+build6 ")]
#[test_case("1.2.3-1.alpha1.9+build5.7.3aedf ")]
#[test_case("0.4.0-beta.1+0851523")]
#[test_case("2.7.3+Final")]
#[test_case("2.7.3+Release")]
#[test_case("2.7.3+r")]
#[test_case("2020.4.9")]
#[test_case("2020.04.09")]
#[test_case("2.3.4+1")]
#[test_case("2.3.4+01")]
#[test_case("2.3.4+0001")]
#[test_case("1.2.3+v6")]
#[test_case("1.2.3-alpha.v4")]
#[test_case("1.2.3-alpha+v6")]
#[test_case("1.2.3+build.v4")]
#[test_case("1.2.3-alpha.v6+build.v7")]
#[test_case("1.2.3-9876543210987654321098765432109876543211")]
#[test_case("1.2.3+9876543210987654321098765432109876543213")]
fn test_positive(input: &str) {
assert!(parse::<Version>(input).is_ok())
}
#[test_case(" "; "spaces")]
#[test_case(" v2 ")]
#[test_case(" V5 ")]
#[test_case("."; "dot")]
#[test_case(""; "empty")]
#[test_case("🙈"; "emoji")]
#[test_case("00001")]
#[test_case("01")]
#[test_case("1 abc")]
#[test_case("1-alpha03")]
#[test_case("1-v3")]
#[test_case("1. "; "1 dot space")]
#[test_case("1."; "1 dot")]
#[test_case("1.+.0")]
#[test_case("1.2-v4")]
#[test_case("1.2. "; "1 2 dot space")]
#[test_case("1.2.."; "1 2 dot dot space")]
#[test_case("1.2." ; "1 2 dot")]
#[test_case("1.2.3 abc")]
#[test_case("1.2.3- "; "1 2 3 hyphen space")]
#[test_case("1.2.3--"; "1 2 3 hyphen hyphen")]
#[test_case("1.2.3-."; "1 2 3 hyphen dot")]
#[test_case("1.2.3-"; "1 2 3 hyphen")]
#[test_case("1.2.3-+"; "1 2 3 hyphen plus")]
#[test_case("1.2.3-🙈")]
#[test_case("1.2.3-alpha-v5")]
#[test_case("1.2.3.4- "; "1 2 3 4 hyphen space")]
#[test_case("1.2.3.4-"; "1 2 3 4 hyphen")]
#[test_case("1.2.3.9876543210987654321098765432109876543210")]
#[test_case("1.2.3.RC.4")]
#[test_case("1.2.3.v4")]
#[test_case("1.2.3+ "; "1 2 3 plus space")]
#[test_case("1.2.3+-"; "1 2 3 plus hyphen")]
#[test_case("1.2.3+."; "1 2 3 plus dot")]
#[test_case("1.2.3+" ; "1 2 3 plus")]
#[test_case("1.2.3++"; "1 2 3 plus plus")]
#[test_case("1.2.3+build-v5")]
#[test_case("1.2.9876543210987654321098765432109876543210")]
#[test_case("1.2.v3")]
#[test_case("1.2")]
#[test_case("1.2+v5")]
#[test_case("1.3.3.0")]
#[test_case("1.3.3.00")]
#[test_case("1.3.3.07")]
#[test_case("1.3.3.7-bar")]
#[test_case("1.3.3.7.04.02")]
#[test_case("1.3.3.7.4.2")]
#[test_case("1.3.3.7.foo")]
#[test_case("1.3.3.7")]
#[test_case("1.3.3.7+baz")]
#[test_case("1.3.3.9876543210987654321098765432109876543210.4.2")]
#[test_case("1.3.3.9876543210987654321098765432109876543210")]
#[test_case("1.4-alpha02")]
#[test_case("1.4+build02")]
#[test_case("1.9.3.RC1")]
#[test_case("1.9.RC2")]
#[test_case("1.9876543210987654321098765432109876543210.2")]
#[test_case("1.9876543210987654321098765432109876543210")]
#[test_case("1.RC3")]
#[test_case("1.v2")]
#[test_case("1")]
#[test_case("1+build03")]
#[test_case("1+v4")]
#[test_case("1🙈"; "1 emoji")]
#[test_case("123456789012345678901234567890")]
#[test_case("2-Final")]
#[test_case("3-r")]
#[test_case("4-Release")]
#[test_case("2.7-Final")]
#[test_case("2.8-r")]
#[test_case("2.9-Release")]
#[test_case("2.7.3.Final")]
#[test_case("2.7.4.r")]
#[test_case("2.7.5.Release")]
#[test_case("2.8.Final")]
#[test_case("2.9.r")]
#[test_case("2.6.Release")]
#[test_case("2.5+Final")]
#[test_case("2.4+r")]
#[test_case("2.3+Release")]
#[test_case("3.Final")]
#[test_case("2.r")]
#[test_case("3.Release")]
#[test_case("4+Final")]
#[test_case("5+r")]
#[test_case("2+Release")]
#[test_case("2020.04")]
#[test_case("2020.4")]
#[test_case("3.1.0-M13-beta3")]
#[test_case("3.1.0+build3-r021")]
#[test_case("5.9.0-202009080501-r")]
#[test_case("7.2.0+28-2f9fb552")]
#[test_case("a.b.c")]
#[test_case("a1.2.3")]
#[test_case("v v1.2.3")]
#[test_case("v-2.3.4")]
#[test_case("v.1.2.3")]
#[test_case("v")]
#[test_case("v+3.4.5")]
#[test_case("v🙈"; "v emoji")]
#[test_case("v1.2.3")]
#[test_case("v1.3.3-7")]
#[test_case("v1")]
#[test_case("V2.3.4")]
#[test_case("V3")]
#[test_case("V4.2.4-2")]
#[test_case("val")]
#[test_case("vv1.2.3")]
fn test_negative(input: &str) {
assert!(parse::<Version>(input).is_err());
}
}
}
#[cfg(all(feature = "generator", feature = "strict"))]
pub mod generator {
#[cfg(test)]
use std::collections::HashMap;
use super::*;
pub fn generate_20000(seed: u32) -> String {
let classes = [
Class::Number,
Class::Alpha,
Class::Dot,
Class::Hyphen,
Class::Plus,
];
let dfa = strict::strict_dfa();
let mut seed = if seed == 0 { 0xBAD_5EED } else { seed };
let mut state = State::ExpectMajor;
let mut candidates = [Class::Whitespace; 6];
let mut result = Vec::with_capacity(20_000);
let size = 20_000_usize;
loop {
let mut cls = 0;
let mut num_candidates = 0;
while cls < classes.len() {
let possible_state = transition(&dfa, state, classes[cls]).0;
if possible_state != State::Error {
candidates[num_candidates] = classes[cls];
num_candidates += 1;
}
cls += 1;
}
let (new_seed, candidate) = roll(seed, num_candidates);
let candidate = candidates[candidate];
let (new_seed, ch) = char_for(new_seed, candidate);
seed = new_seed;
state = transition(&dfa, state, candidate).0;
result.push(ch);
if result.len() >= size && transition(&dfa, state, Class::EndOfInput).0 != State::Error
{
break;
}
}
String::from_utf8(result).unwrap()
}
const fn char_for(seed: u32, class: Class) -> (u32, u8) {
match class {
Class::Number => {
let (seed, number) = roll(seed, 10);
(seed, b'0' + number as u8)
}
Class::Alpha => {
let (seed, use_upper) = roll(seed, 2);
let (seed, mut number) = roll(seed, 25);
if number >= 21 {
number += 1;
}
let start = if use_upper == 0 { b'a' } else { b'A' };
(seed, start + number as u8)
}
Class::Dot => (seed, b'.'),
Class::Hyphen => (seed, b'-'),
Class::Plus => (seed, b'+'),
Class::Whitespace | Class::EndOfInput | Class::Unexpected | Class::V => (seed, b' '),
}
}
const fn roll(seed: u32, upper: usize) -> (u32, usize) {
let x = seed;
let x = x ^ x.wrapping_shl(13);
let x = x ^ x.wrapping_shr(17);
let x = x ^ x.wrapping_shl(5);
(x, x as usize % upper)
}
#[cfg(test)]
fn dot() -> String {
struct Node {
name: String,
accepts: bool,
};
struct Edge {
from: State,
to: State,
matches: String,
accepts: bool,
}
let mut nodes = [
State::ExpectMajor,
State::ParseMajor,
State::ExpectMinor,
State::ParseMinor,
State::ExpectPatch,
State::ParsePatch,
State::ExpectAdd,
State::ParseAdd,
State::ExpectPre,
State::ParsePre,
State::ExpectBuild,
State::ParseBuild,
State::RequireMajor,
]
.iter()
.map(|s| {
(
*s,
Node {
name: format!("{}", s),
accepts: false,
},
)
})
.collect::<HashMap<State, Node>>();
let classes = [
Class::Number,
Class::Alpha,
Class::Dot,
Class::Hyphen,
Class::Plus,
Class::V,
Class::Whitespace,
Class::EndOfInput,
Class::Unexpected,
];
let edges = nodes
.iter_mut()
.map(|(&s, node)| {
let mut targets = HashMap::new();
for &c in &classes {
let (target, accept) = transition(&DFA, s, c);
if target != State::Error {
let accepts = accept != Accept::None && accept != Accept::SaveStart;
targets
.entry((target, accepts))
.or_insert_with(Vec::new)
.push(c);
}
}
let ends = targets
.remove(&(State::EndOfInput, true))
.or(targets.remove(&(State::EndOfInput, false)));
if ends.is_some() {
node.accepts = true;
}
(s, targets)
})
.collect::<HashMap<State, HashMap<(State, bool), Vec<Class>>>>();
let edges = edges
.into_iter()
.flat_map(|(from, targets)| {
let targets = targets
.into_iter()
.map(|((to, accepts), classes)| {
let has_alpha = classes.contains(&Class::Alpha);
let matches = classes
.into_iter()
.filter(|c| !has_alpha || *c != Class::V)
.map(|c| c.to_string())
.collect::<String>();
Edge {
from,
to,
matches,
accepts,
}
})
.collect::<Vec<Edge>>();
targets
})
.collect::<Vec<Edge>>();
let mut statements = nodes
.into_iter()
.map(|(state, node)| {
let shape_color = if node.accepts {
" shape=doublecircle color=blue3"
} else {
""
};
format!(r#"{}[label="{}"{}]"#, state, node.name, shape_color)
})
.collect::<Vec<_>>();
statements.extend(edges.into_iter().map(
|Edge {
from,
to,
matches,
accepts,
}| {
let color = if accepts {
" color=green3 fontcolor=green4 penwidth=1.7"
} else {
""
};
format!(r#"{} -> {}[label="{}"{}]"#, from, to, matches, color)
},
));
statements.insert(
0,
String::from(r#"edge [fontname="JetBrains Mono,Fira Code,Monospace"]"#),
);
statements.insert(0, String::from(r#"node [margin=0.02 shape=circle]"#));
statements.insert(0, String::from(r#"graph [rankdir=LR]"#));
let statements = statements.join(";\n ");
format!("digraph dfa {{\n {};\n}}", statements)
}
#[test]
fn print_dot() {
println!("{}", dot());
}
}