use {
alloc::{
borrow::Cow,
string::{String, ToString},
},
core::{
cmp::Ordering,
iter::Skip,
str::{CharIndices, FromStr},
},
crate::{Error, Result, Semver},
super::parse_errors,
};
pub (super) struct Parser<'a> {
src: &'a str,
strict: bool,
chars: Skip<CharIndices<'a>>,
}
impl<'a> Parser<'a> {
pub fn parse(src: &'a str, strict: bool) -> Result<Semver> {
let src = if strict { src } else { src.trim() };
if src.is_empty() {
return Err(err!(parse_errors::EMPTY));
} else if src.len() > super::MAX_INPUT_STR_LEN{
return Err(err!(parse_errors::TOO_LARGE));
}
let mut parser = Self {
src: src,
strict,
chars: src.char_indices().skip(0),
};
let major = parser.parse_major_minor_patch_version(parse_errors::INVALID_MAJOR)?;
let minor = parser.parse_major_minor_patch_version(parse_errors::INVALID_MINOR)?;
let patch = parser.parse_major_minor_patch_version(parse_errors::INVALID_PATCH)?;
let pre_release = match parser.parse_pre_release_or_build_metadata(super::PRE_RELEASE_STARTER) {
Ok(pre_release) => Ok(pre_release),
Err(err) if err.msg() == Some(parse_errors::INVALID_TOKEN) => Err(err),
Err(err) => return Err(err),
};
let build_metadata = parser.parse_pre_release_or_build_metadata(super::BUILD_METADATA_STARTER)?;
let pre_release = match pre_release {
Ok(pre_release) => pre_release,
Err(err) if err.msg() == Some(parse_errors::INVALID_TOKEN) => {
match build_metadata {
Some(_) => None,
None => return Err(err),
}
},
Err(err) => return Err(err),
};
Ok(Semver {
major: major, minor: minor, patch: patch,
pre_release: pre_release, build_metadata: build_metadata,
})
}
fn parse_major_minor_patch_version(&mut self, err_kind: &'static str) -> Result<u64> {
let err = || Err(Error::new(line!(), module_path!(), Some(Cow::Borrowed(err_kind))));
let mut skip_last_char = false;
let mut start_index: Option<usize> = None;
let mut end_index: Option<usize> = None;
let mut invalid_char_index: Option<usize> = None;
let mut first_char_is_zero = false;
for (index, chr) in &mut self.chars {
match chr {
'.' => match start_index {
Some(_) => { skip_last_char = true; break; },
None => return err(),
},
'0'..='9' => {
if let Some(start_index) = start_index {
if let Some(gap) = index.checked_sub(start_index) {
if (gap > 0 && first_char_is_zero) || gap.cmp(&super::MAX_STR_LEN_OF_A_VERSION_NUMBER) != Ordering::Less {
return err();
}
}
} else {
start_index = Some(index);
first_char_is_zero = chr == '0';
}
end_index = Some(index);
},
_ => {
invalid_char_index = Some(index);
break;
},
};
}
match (start_index, end_index) {
(Some(start_index), Some(end_index)) if end_index >= start_index => match u64::from_str(&self.src[start_index..=end_index]) {
Ok(v) => {
self.chars = self.src.char_indices().skip(
end_index.checked_add(if skip_last_char { 2 } else { 1 }).unwrap_or(usize::max_value())
);
Ok(v)
},
Err(_) => err(),
},
_ => {
if let Some(index) = invalid_char_index { self.chars = self.src.char_indices().skip(index); }
match err_kind {
parse_errors::INVALID_MINOR => if self.strict {
match invalid_char_index {
Some(_) => err(),
None => Err(err!(parse_errors::MISSING_MINOR)),
}
} else {
Ok(0)
},
parse_errors::INVALID_PATCH => if self.strict {
match invalid_char_index {
Some(_) => err(),
None => Err(err!(parse_errors::MISSING_PATCH)),
}
} else {
Ok(0)
},
_ => err(),
}
},
}
}
fn parse_pre_release_or_build_metadata(&mut self, starter: char) -> Result<Option<String>> {
let err = match starter {
super::PRE_RELEASE_STARTER => Err(err!(parse_errors::INVALID_PRE_RELEASE)),
super::BUILD_METADATA_STARTER => Err(err!(parse_errors::INVALID_BUILD_METADATA)),
_ => return Err(err!(parse_errors::PARSER_ERROR)),
};
let mut start_index: Option<usize> = None;
let mut end_index: Option<usize> = None;
let mut first_char_is_zero: Option<bool> = None;
let mut last_char: Option<char> = None;
let mut all_are_numbers = starter == super::PRE_RELEASE_STARTER;
match self.chars.next() {
Some((index, chr)) => if chr != starter {
self.chars = self.src.char_indices().skip(index);
return Err(err!(parse_errors::INVALID_TOKEN));
},
None => return Ok(None),
};
for (index, chr) in &mut self.chars {
if start_index.is_none() { start_index = Some(index); }
match chr {
'a'..='z' | 'A'..='Z' | '-' => {
end_index = Some(index);
all_are_numbers = false;
},
'0'..='9' => {
if starter == super::PRE_RELEASE_STARTER && matches!(last_char, None | Some('.')) {
first_char_is_zero = Some(chr == '0');
}
end_index = Some(index);
},
super::BUILD_METADATA_STARTER => if starter == super::PRE_RELEASE_STARTER { break; } else { return err; },
'.' => {
if starter == super::PRE_RELEASE_STARTER {
if all_are_numbers && first_char_is_zero.unwrap_or(false) {
return err;
}
all_are_numbers = true;
first_char_is_zero = None;
}
match last_char {
Some(c) => match c {
'.' | '-' => return err,
_ => last_char = None,
},
None => return err,
};
continue;
},
_ => return err,
};
last_char = Some(chr);
}
match (last_char, start_index, end_index) {
(Some(last_char), Some(start_index), Some(end_index)) if last_char.is_ascii_alphanumeric() => {
if starter == super::PRE_RELEASE_STARTER && all_are_numbers && first_char_is_zero.unwrap_or(false) {
return err;
}
self.chars = self.src.char_indices().skip(end_index.checked_add(1).unwrap_or(usize::max_value()));
Ok(Some(self.src[start_index..=end_index].to_string()))
},
_ => err,
}
}
}