dia-semver 11.0.1

For handling Semantic Versions 2.0.0
Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Dia-Semver

Copyright (C) 2018-2022  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2022".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

use {
    alloc::{
        borrow::Cow,
        string::{String, ToString},
    },
    core::{
        cmp::Ordering,
        iter::Skip,
        str::{CharIndices, FromStr},
    },
    crate::{Error, Result, Semver},
    super::parse_errors,
};

/// # Parser
pub (super) struct Parser<'a> {

    /// # String to be parsed
    src: &'a str,

    /// # Strict mode
    strict: bool,

    /// # Chars of source string
    chars: Skip<CharIndices<'a>>,

}

impl<'a> Parser<'a> {

    /// # Parses input string
    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),
        };

        // Major, minor, patch
        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)?;

        // Pre-release (optional)
        let pre_release = match parser.parse_pre_release_or_build_metadata(super::PRE_RELEASE_STARTER) {
            Ok(pre_release) => Ok(pre_release),
            // Starter can be BUILD_METADATA_STARTER, we'll handle this error later
            Err(err) if err.msg() == Some(parse_errors::INVALID_TOKEN) => Err(err),
            Err(err) => return Err(err),
        };

        // Build metadata (optional)
        let build_metadata = parser.parse_pre_release_or_build_metadata(super::BUILD_METADATA_STARTER)?;

        // Pre-release
        let pre_release = match pre_release {
            Ok(pre_release) => pre_release,
            // If there's no build metadata, pre-release must be some string or None -- otherwise it's invalid.
            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,
        })
    }

    /// # Parses major/minor/patch version
    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(),
                }
            },
        }
    }

    /// # Parses pre-release or build metadata
    ///
    /// `starter`: can be [`PRE_RELEASE_STARTER`][const:PRE_RELEASE_STARTER] or [`BUILD_METADATA_STARTER`][const:BUILD_METADATA_STARTER].
    ///
    /// [const:PRE_RELEASE_STARTER]: ../constant.PRE_RELEASE_STARTER.html
    /// [const:BUILD_METADATA_STARTER]: ../constant.BUILD_METADATA_STARTER.html
    fn parse_pre_release_or_build_metadata(&mut self, starter: char) -> Result<Option<String>> {
        // Check starter and setup error kind
        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;

        // Check token
        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),
        };

        // Section 9: a pre-release numeric identifier MUST NOT include leading zeroes
        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);
                },
                // Pre-release is allowed to have build metadata go after it. But build metadata is not.
                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,
        }
    }

}