diff options
Diffstat (limited to 'semver-parser')
| -rw-r--r-- | semver-parser/.gitignore | 2 | ||||
| -rw-r--r-- | semver-parser/Cargo.toml | 11 | ||||
| -rw-r--r-- | semver-parser/LICENSE-APACHE | 201 | ||||
| -rw-r--r-- | semver-parser/LICENSE-MIT | 25 | ||||
| -rw-r--r-- | semver-parser/src/common.rs | 66 | ||||
| -rw-r--r-- | semver-parser/src/lib.rs | 8 | ||||
| -rw-r--r-- | semver-parser/src/range.rs | 696 | ||||
| -rw-r--r-- | semver-parser/src/recognize.rs | 154 | ||||
| -rw-r--r-- | semver-parser/src/version.rs | 365 | 
9 files changed, 1528 insertions, 0 deletions
| diff --git a/semver-parser/.gitignore b/semver-parser/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/semver-parser/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/semver-parser/Cargo.toml b/semver-parser/Cargo.toml new file mode 100644 index 0000000..c2be878 --- /dev/null +++ b/semver-parser/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "semver-parser" +version = "0.7.0" +authors = ["Steve Klabnik <steve@steveklabnik.com>"] +license = "MIT/Apache-2.0" +repository = "https://github.com/steveklabnik/semver-parser" +homepage = "https://github.com/steveklabnik/semver-parser" +documentation = "https://docs.rs/semver-parser" +description = """ +Parsing of the semver spec. +""" diff --git a/semver-parser/LICENSE-APACHE b/semver-parser/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/semver-parser/LICENSE-APACHE @@ -0,0 +1,201 @@ +                              Apache License +                        Version 2.0, January 2004 +                     http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +   "License" shall mean the terms and conditions for use, reproduction, +   and distribution as defined by Sections 1 through 9 of this document. + +   "Licensor" shall mean the copyright owner or entity authorized by +   the copyright owner that is granting the License. + +   "Legal Entity" shall mean the union of the acting entity and all +   other entities that control, are controlled by, or are under common +   control with that entity. For the purposes of this definition, +   "control" means (i) the power, direct or indirect, to cause the +   direction or management of such entity, whether by contract or +   otherwise, or (ii) ownership of fifty percent (50%) or more of the +   outstanding shares, or (iii) beneficial ownership of such entity. + +   "You" (or "Your") shall mean an individual or Legal Entity +   exercising permissions granted by this License. + +   "Source" form shall mean the preferred form for making modifications, +   including but not limited to software source code, documentation +   source, and configuration files. + +   "Object" form shall mean any form resulting from mechanical +   transformation or translation of a Source form, including but +   not limited to compiled object code, generated documentation, +   and conversions to other media types. + +   "Work" shall mean the work of authorship, whether in Source or +   Object form, made available under the License, as indicated by a +   copyright notice that is included in or attached to the work +   (an example is provided in the Appendix below). + +   "Derivative Works" shall mean any work, whether in Source or Object +   form, that is based on (or derived from) the Work and for which the +   editorial revisions, annotations, elaborations, or other modifications +   represent, as a whole, an original work of authorship. For the purposes +   of this License, Derivative Works shall not include works that remain +   separable from, or merely link (or bind by name) to the interfaces of, +   the Work and Derivative Works thereof. + +   "Contribution" shall mean any work of authorship, including +   the original version of the Work and any modifications or additions +   to that Work or Derivative Works thereof, that is intentionally +   submitted to Licensor for inclusion in the Work by the copyright owner +   or by an individual or Legal Entity authorized to submit on behalf of +   the copyright owner. For the purposes of this definition, "submitted" +   means any form of electronic, verbal, or written communication sent +   to the Licensor or its representatives, including but not limited to +   communication on electronic mailing lists, source code control systems, +   and issue tracking systems that are managed by, or on behalf of, the +   Licensor for the purpose of discussing and improving the Work, but +   excluding communication that is conspicuously marked or otherwise +   designated in writing by the copyright owner as "Not a Contribution." + +   "Contributor" shall mean Licensor and any individual or Legal Entity +   on behalf of whom a Contribution has been received by Licensor and +   subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +   this License, each Contributor hereby grants to You a perpetual, +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable +   copyright license to reproduce, prepare Derivative Works of, +   publicly display, publicly perform, sublicense, and distribute the +   Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +   this License, each Contributor hereby grants to You a perpetual, +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable +   (except as stated in this section) patent license to make, have made, +   use, offer to sell, sell, import, and otherwise transfer the Work, +   where such license applies only to those patent claims licensable +   by such Contributor that are necessarily infringed by their +   Contribution(s) alone or by combination of their Contribution(s) +   with the Work to which such Contribution(s) was submitted. If You +   institute patent litigation against any entity (including a +   cross-claim or counterclaim in a lawsuit) alleging that the Work +   or a Contribution incorporated within the Work constitutes direct +   or contributory patent infringement, then any patent licenses +   granted to You under this License for that Work shall terminate +   as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +   Work or Derivative Works thereof in any medium, with or without +   modifications, and in Source or Object form, provided that You +   meet the following conditions: + +   (a) You must give any other recipients of the Work or +       Derivative Works a copy of this License; and + +   (b) You must cause any modified files to carry prominent notices +       stating that You changed the files; and + +   (c) You must retain, in the Source form of any Derivative Works +       that You distribute, all copyright, patent, trademark, and +       attribution notices from the Source form of the Work, +       excluding those notices that do not pertain to any part of +       the Derivative Works; and + +   (d) If the Work includes a "NOTICE" text file as part of its +       distribution, then any Derivative Works that You distribute must +       include a readable copy of the attribution notices contained +       within such NOTICE file, excluding those notices that do not +       pertain to any part of the Derivative Works, in at least one +       of the following places: within a NOTICE text file distributed +       as part of the Derivative Works; within the Source form or +       documentation, if provided along with the Derivative Works; or, +       within a display generated by the Derivative Works, if and +       wherever such third-party notices normally appear. The contents +       of the NOTICE file are for informational purposes only and +       do not modify the License. You may add Your own attribution +       notices within Derivative Works that You distribute, alongside +       or as an addendum to the NOTICE text from the Work, provided +       that such additional attribution notices cannot be construed +       as modifying the License. + +   You may add Your own copyright statement to Your modifications and +   may provide additional or different license terms and conditions +   for use, reproduction, or distribution of Your modifications, or +   for any such Derivative Works as a whole, provided Your use, +   reproduction, and distribution of the Work otherwise complies with +   the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +   any Contribution intentionally submitted for inclusion in the Work +   by You to the Licensor shall be under the terms and conditions of +   this License, without any additional terms or conditions. +   Notwithstanding the above, nothing herein shall supersede or modify +   the terms of any separate license agreement you may have executed +   with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +   names, trademarks, service marks, or product names of the Licensor, +   except as required for reasonable and customary use in describing the +   origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +   agreed to in writing, Licensor provides the Work (and each +   Contributor provides its Contributions) on an "AS IS" BASIS, +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +   implied, including, without limitation, any warranties or conditions +   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +   PARTICULAR PURPOSE. You are solely responsible for determining the +   appropriateness of using or redistributing the Work and assume any +   risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +   whether in tort (including negligence), contract, or otherwise, +   unless required by applicable law (such as deliberate and grossly +   negligent acts) or agreed to in writing, shall any Contributor be +   liable to You for damages, including any direct, indirect, special, +   incidental, or consequential damages of any character arising as a +   result of this License or out of the use or inability to use the +   Work (including but not limited to damages for loss of goodwill, +   work stoppage, computer failure or malfunction, or any and all +   other commercial damages or losses), even if such Contributor +   has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +   the Work or Derivative Works thereof, You may choose to offer, +   and charge a fee for, acceptance of support, warranty, indemnity, +   or other liability obligations and/or rights consistent with this +   License. However, in accepting such obligations, You may act only +   on Your own behalf and on Your sole responsibility, not on behalf +   of any other Contributor, and only if You agree to indemnify, +   defend, and hold each Contributor harmless for any liability +   incurred by, or claims asserted against, such Contributor by reason +   of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +   To apply the Apache License to your work, attach the following +   boilerplate notice, with the fields enclosed by brackets "[]" +   replaced with your own identifying information. (Don't include +   the brackets!)  The text should be enclosed in the appropriate +   comment syntax for the file format. We also recommend that a +   file or class name and description of purpose be included on the +   same "printed page" as the copyright notice for easier +   identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +	http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/semver-parser/LICENSE-MIT b/semver-parser/LICENSE-MIT new file mode 100644 index 0000000..fb7494a --- /dev/null +++ b/semver-parser/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016 Steve Klabnik + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/semver-parser/src/common.rs b/semver-parser/src/common.rs new file mode 100644 index 0000000..267b4d9 --- /dev/null +++ b/semver-parser/src/common.rs @@ -0,0 +1,66 @@ +use version::Identifier; +use recognize::{Recognize, Alt, OneOrMore, Inclusive, OneByte}; +use std::str::from_utf8; + +// by the time we get here, we know that it's all valid characters, so this doesn't need to return +// a result or anything +fn parse_meta(s: &str) -> Vec<Identifier> { +    // Originally, I wanted to implement this method via calling parse, but parse is tolerant of +    // leading zeroes, and we want anything with leading zeroes to be considered alphanumeric, not +    // numeric. So the strategy is to check with a recognizer first, and then call parse once +    // we've determined that it's a number without a leading zero. +    s.split(".") +        .map(|part| { +            // another wrinkle: we made sure that any number starts with a +            // non-zero. But there's a problem: an actual zero is a number, yet +            // gets left out by this heuristic. So let's also check for the +            // single, lone zero. +            if is_alpha_numeric(part) { +                Identifier::AlphaNumeric(part.to_string()) +            } else { +                // we can unwrap here because we know it is only digits due to the regex +                Identifier::Numeric(part.parse().unwrap()) +            } +        }).collect() +} + +// parse optional metadata (preceded by the prefix character) +pub fn parse_optional_meta(s: &[u8], prefix_char: u8)-> Result<(Vec<Identifier>, usize), String> { +    if let Some(len) = prefix_char.p(s) { +        let start = len; +        if let Some(len) = letters_numbers_dash_dot(&s[start..]) { +            let end = start + len; +            Ok((parse_meta(from_utf8(&s[start..end]).unwrap()), end)) +        } else { +            Err("Error parsing prerelease".to_string()) +        } +    } else { +        Ok((Vec::new(), 0)) +    } +} + +pub fn is_alpha_numeric(s: &str) -> bool { +    if let Some((_val, len)) = numeric_identifier(s.as_bytes()) { +        // Return true for number with leading zero +        // Note: doing it this way also handily makes overflow fail over. +        len != s.len() +    } else { +        true +    } +} + +// Note: could plumb overflow error up to return value as Result +pub fn numeric_identifier(s: &[u8]) -> Option<(u64, usize)> { +    if let Some(len) = Alt(b'0', OneOrMore(Inclusive(b'0'..b'9'))).p(s) { +        from_utf8(&s[0..len]).unwrap().parse().ok().map(|val| (val, len)) +    } else { +        None +    } +} + +pub fn letters_numbers_dash_dot(s: &[u8]) -> Option<usize> { +    OneOrMore(OneByte(|c| c == b'-' || c == b'.' || +        (b'0' <= c && c <= b'9') || +        (b'a' <= c && c <= b'z') || +        (b'A' <= c && c <= b'Z'))).p(s) +} diff --git a/semver-parser/src/lib.rs b/semver-parser/src/lib.rs new file mode 100644 index 0000000..3b0d8f0 --- /dev/null +++ b/semver-parser/src/lib.rs @@ -0,0 +1,8 @@ +pub mod version; +pub mod range; + +// for private stuff the two share +mod common; + +// for recognizer combinators +mod recognize; diff --git a/semver-parser/src/range.rs b/semver-parser/src/range.rs new file mode 100644 index 0000000..858be9f --- /dev/null +++ b/semver-parser/src/range.rs @@ -0,0 +1,696 @@ +use common::{self, numeric_identifier, letters_numbers_dash_dot}; +use version::Identifier; +use std::str::{FromStr, from_utf8}; +use recognize::*; + +#[derive(Debug)] +pub struct VersionReq { +    pub predicates: Vec<Predicate>, +} + +#[derive(PartialEq,Debug)] +pub enum WildcardVersion { +    Major, +    Minor, +    Patch, +} + +#[derive(PartialEq,Debug)] +pub enum Op { +    Ex, // Exact +    Gt, // Greater than +    GtEq, // Greater than or equal to +    Lt, // Less than +    LtEq, // Less than or equal to +    Tilde, // e.g. ~1.0.0 +    Compatible, // compatible by definition of semver, indicated by ^ +    Wildcard(WildcardVersion), // x.y.*, x.*, * +} + +impl FromStr for Op { +    type Err = String; + +    fn from_str(s: &str) -> Result<Op, String> { +        match s { +            "=" => Ok(Op::Ex), +            ">" => Ok(Op::Gt), +            ">=" => Ok(Op::GtEq), +            "<" => Ok(Op::Lt), +            "<=" => Ok(Op::LtEq), +            "~" => Ok(Op::Tilde), +            "^" => Ok(Op::Compatible), +            _ => Err(String::from("Could not parse Op")), +        } +    } +} + +#[derive(PartialEq,Debug)] +pub struct Predicate { +    pub op: Op, +    pub major: u64, +    pub minor: Option<u64>, +    pub patch: Option<u64>, +    pub pre: Vec<Identifier>, +} + +fn numeric_or_wild(s: &[u8]) -> Option<(Option<u64>, usize)> { +    if let Some((val, len)) = numeric_identifier(s) { +        Some((Some(val), len)) +    } else if let Some(len) = OneOf(b"*xX").p(s) { +        Some((None, len)) +    } else { +        None +    } +} + +fn dot_numeric_or_wild(s: &[u8]) -> Option<(Option<u64>, usize)> { +    b'.'.p(s).and_then(|len| +        numeric_or_wild(&s[len..]).map(|(val, len2)| (val, len + len2)) +    ) +} + +fn operation(s: &[u8]) -> Option<(Op, usize)> { +    if let Some(len) = "=".p(s) { +        Some((Op::Ex, len)) +    } else if let Some(len) = ">=".p(s) { +        Some((Op::GtEq, len)) +    } else if let Some(len) = ">".p(s) { +        Some((Op::Gt, len)) +    } else if let Some(len) = "<=".p(s) { +        Some((Op::LtEq, len)) +    } else if let Some(len) = "<".p(s) { +        Some((Op::Lt, len)) +    } else if let Some(len) = "~".p(s) { +        Some((Op::Tilde, len)) +    } else if let Some(len) = "^".p(s) { +        Some((Op::Compatible, len)) +    } else { +        None +    } +} + +fn whitespace(s: &[u8]) -> Option<usize> { +    ZeroOrMore(OneOf(b"\t\r\n ")).p(s) +} + +pub fn parse_predicate(range: &str) -> Result<Predicate, String> { +    let s = range.trim().as_bytes(); +    let mut i = 0; +    let mut operation = if let Some((op, len)) = operation(&s[i..]) { +        i += len; +        op +    } else { +        // operations default to Compatible +        Op::Compatible +    }; +    if let Some(len) = whitespace.p(&s[i..]) { +        i += len; +    } +    let major = if let Some((major, len)) = numeric_identifier(&s[i..]) { +        i += len; +        major +    } else { +        return Err("Error parsing major version number: ".to_string()); +    }; +    let minor = if let Some((minor, len)) = dot_numeric_or_wild(&s[i..]) { +        i += len; +        if minor.is_none() { +            operation = Op::Wildcard(WildcardVersion::Minor); +        } +        minor +    } else { +        None +    }; +    let patch = if let Some((patch, len)) = dot_numeric_or_wild(&s[i..]) { +        i += len; +        if patch.is_none() { +            operation = Op::Wildcard(WildcardVersion::Patch); +        } +        patch +    } else { +        None +    }; +    let (pre, pre_len) = common::parse_optional_meta(&s[i..], b'-')?; +    i += pre_len; +    if let Some(len) = (b'+', letters_numbers_dash_dot).p(&s[i..]) { +        i += len; +    } +    if i != s.len() { +        return Err("Extra junk after valid predicate: ".to_string() + +            from_utf8(&s[i..]).unwrap()); +    } +    Ok(Predicate { +        op: operation, +        major: major, +        minor: minor, +        patch: patch, +        pre: pre, +    }) +} + +pub fn parse(ranges: &str) -> Result<VersionReq, String> { +    // null is an error +    if ranges == "\0" { +        return Err(String::from("Null is not a valid VersionReq")); +    } + +    // an empty range is a major version wildcard +    // so is a lone * or x of either capitalization +    if (ranges == "") +    || (ranges == "*") +    || (ranges == "x") +    || (ranges == "X") { +        return Ok(VersionReq { +            predicates: vec![Predicate { +                op: Op::Wildcard(WildcardVersion::Major), +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }], +        }); +    } + + +    let ranges = ranges.trim(); + +    let predicates: Result<Vec<_>, String> = ranges +        .split(",") +        .map(|range| { +            parse_predicate(range) +        }) +        .collect(); + +    let predicates = try!(predicates); + +    if predicates.len() == 0 { +        return Err(String::from("VersionReq did not parse properly")); +    } + +    Ok(VersionReq { +        predicates: predicates, +    }) +} + +#[cfg(test)] +mod tests { +    use super::*; +    use range; +    use version::Identifier; + +    #[test] +    fn test_parsing_default() { +        let r = range::parse("1.0.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 1, +                minor: Some(0), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_exact_01() { +        let r = range::parse("=1.0.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Ex, +                major: 1, +                minor: Some(0), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_exact_02() { +        let r = range::parse("=0.9.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Ex, +                major: 0, +                minor: Some(9), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_exact_03() { +        let r = range::parse("=0.1.0-beta2.a").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Ex, +                major: 0, +                minor: Some(1), +                patch: Some(0), +                pre: vec![Identifier::AlphaNumeric(String::from("beta2")), +                          Identifier::AlphaNumeric(String::from("a"))], +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_greater_than() { +        let r = range::parse("> 1.0.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Gt, +                major: 1, +                minor: Some(0), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_greater_than_01() { +        let r = range::parse(">= 1.0.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::GtEq, +                major: 1, +                minor: Some(0), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_greater_than_02() { +        let r = range::parse(">= 2.1.0-alpha2").unwrap(); + +        assert_eq!(Predicate { +                op: Op::GtEq, +                major: 2, +                minor: Some(1), +                patch: Some(0), +                pre: vec![Identifier::AlphaNumeric(String::from("alpha2"))], +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_less_than() { +        let r = range::parse("< 1.0.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Lt, +                major: 1, +                minor: Some(0), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_less_than_eq() { +        let r = range::parse("<= 2.1.0-alpha2").unwrap(); + +        assert_eq!(Predicate { +                op: Op::LtEq, +                major: 2, +                minor: Some(1), +                patch: Some(0), +                pre: vec![Identifier::AlphaNumeric(String::from("alpha2"))], +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_tilde() { +        let r = range::parse("~1").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Tilde, +                major: 1, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_parsing_compatible() { +        let r = range::parse("^0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_blank() { +        let r = range::parse("").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Major), +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_wildcard() { +        let r = range::parse("*").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Major), +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_x() { +        let r = range::parse("x").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Major), +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_capital_x() { +        let r = range::parse("X").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Major), +                major: 0, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_minor_wildcard_star() { +        let r = range::parse("1.*").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Minor), +                major: 1, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_minor_wildcard_x() { +        let r = range::parse("1.x").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Minor), +                major: 1, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_minor_wildcard_capital_x() { +        let r = range::parse("1.X").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Minor), +                major: 1, +                minor: None, +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_patch_wildcard_star() { +        let r = range::parse("1.2.*").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Patch), +                major: 1, +                minor: Some(2), +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_patch_wildcard_x() { +        let r = range::parse("1.2.x").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Patch), +                major: 1, +                minor: Some(2), +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    fn test_parsing_patch_wildcard_capital_x() { +        let r = range::parse("1.2.X").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Wildcard(WildcardVersion::Patch), +                major: 1, +                minor: Some(2), +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); +    } + +    #[test] +    pub fn test_multiple_01() { +        let r = range::parse("> 0.0.9, <= 2.5.3").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Gt, +                major: 0, +                minor: Some(0), +                patch: Some(9), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); + +        assert_eq!(Predicate { +                op: Op::LtEq, +                major: 2, +                minor: Some(5), +                patch: Some(3), +                pre: Vec::new(), +            }, +            r.predicates[1] +        ); +    } + +    #[test] +    pub fn test_multiple_02() { +        let r = range::parse("0.3.0, 0.4.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: Some(3), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: Some(4), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[1] +        ); +    } + +    #[test] +    pub fn test_multiple_03() { +        let r = range::parse("<= 0.2.0, >= 0.5.0").unwrap(); + +        assert_eq!(Predicate { +                op: Op::LtEq, +                major: 0, +                minor: Some(2), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); + +        assert_eq!(Predicate { +                op: Op::GtEq, +                major: 0, +                minor: Some(5), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[1] +        ); +    } + +    #[test] +    pub fn test_multiple_04() { +        let r = range::parse("0.1.0, 0.1.4, 0.1.6").unwrap(); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: Some(1), +                patch: Some(0), +                pre: Vec::new(), +            }, +            r.predicates[0] +        ); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: Some(1), +                patch: Some(4), +                pre: Vec::new(), +            }, +            r.predicates[1] +        ); + +        assert_eq!(Predicate { +                op: Op::Compatible, +                major: 0, +                minor: Some(1), +                patch: Some(6), +                pre: Vec::new(), +            }, +            r.predicates[2] +        ); +    } + +    #[test] +    pub fn test_multiple_05() { +        let r = range::parse(">=0.5.1-alpha3, <0.6").unwrap(); + +        assert_eq!(Predicate { +                op: Op::GtEq, +                major: 0, +                minor: Some(5), +                patch: Some(1), +                pre: vec![Identifier::AlphaNumeric(String::from("alpha3"))], +            }, +            r.predicates[0] +        ); + +        assert_eq!(Predicate { +                op: Op::Lt, +                major: 0, +                minor: Some(6), +                patch: None, +                pre: Vec::new(), +            }, +            r.predicates[1] +        ); +    } + +    #[test] +    fn test_parse_build_metadata_with_predicate() { +        assert_eq!(range::parse("^1.2.3+meta").unwrap().predicates[0].op, +                   Op::Compatible); +        assert_eq!(range::parse("~1.2.3+meta").unwrap().predicates[0].op, +                   Op::Tilde); +        assert_eq!(range::parse("=1.2.3+meta").unwrap().predicates[0].op, +                   Op::Ex); +        assert_eq!(range::parse("<=1.2.3+meta").unwrap().predicates[0].op, +                   Op::LtEq); +        assert_eq!(range::parse(">=1.2.3+meta").unwrap().predicates[0].op, +                   Op::GtEq); +        assert_eq!(range::parse("<1.2.3+meta").unwrap().predicates[0].op, +                   Op::Lt); +        assert_eq!(range::parse(">1.2.3+meta").unwrap().predicates[0].op, +                   Op::Gt); +    } + +    #[test] +    pub fn test_parse_errors() { +        assert!(range::parse("\0").is_err()); +        assert!(range::parse(">= >= 0.0.2").is_err()); +        assert!(range::parse(">== 0.0.2").is_err()); +        assert!(range::parse("a.0.0").is_err()); +        assert!(range::parse("1.0.0-").is_err()); +        assert!(range::parse(">=").is_err()); +        assert!(range::parse("> 0.1.0,").is_err()); +        assert!(range::parse("> 0.3.0, ,").is_err()); +    } + +    #[test] +    pub fn test_large_major_version() { +        assert!(range::parse("18446744073709551617.0.0").is_err()); +    } + +    #[test] +    pub fn test_large_minor_version() { +        assert!(range::parse("0.18446744073709551617.0").is_err()); +    } + +    #[test] +    pub fn test_large_patch_version() { +        assert!(range::parse("0.0.18446744073709551617").is_err()); +    } +} diff --git a/semver-parser/src/recognize.rs b/semver-parser/src/recognize.rs new file mode 100644 index 0000000..c0dd771 --- /dev/null +++ b/semver-parser/src/recognize.rs @@ -0,0 +1,154 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Licensed under either of MIT or Apache License, Version 2.0, +// at your option. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simple recognizer combinators. + +// This version is similar to a similar one in the "lang" module of +// xi-editor, but is stripped down to only the needed combinators. + +use std::ops; + +pub trait Recognize { +    fn p(&self, s: &[u8]) -> Option<usize>; +} + +impl<F: Fn(&[u8]) -> Option<usize>> Recognize for F { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        self(s) +    } +} + +pub struct OneByte<F>(pub F); + +impl<F: Fn(u8) -> bool> Recognize for OneByte<F> { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        if s.is_empty() || !self.0(s[0]) { +            None +        } else { +            Some(1) +        } +    } +} + +impl Recognize for u8 { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        OneByte(|b| b == *self).p(s) +    } +} + +/// Use Inclusive(a..b) to indicate an inclusive range. When a...b syntax becomes +/// stable, we can get rid of this and switch to that. +pub struct Inclusive<T>(pub T); + +impl Recognize for Inclusive<ops::Range<u8>> { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        OneByte(|x| x >= self.0.start && x <= self.0.end).p(s) +    } +} + +impl<'a> Recognize for &'a [u8] { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        let len = self.len(); +        if s.len() >= len && &s[..len] == *self { +            Some(len) +        } else { +            None +        } +    } +} + +impl<'a> Recognize for &'a str { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        self.as_bytes().p(s) +    } +} + +impl<P1: Recognize, P2: Recognize> Recognize for (P1, P2) { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        self.0.p(s).and_then(|len1| +            self.1.p(&s[len1..]).map(|len2| +                len1 + len2)) +    } +} + +/// Choice from two heterogeneous alternatives. +pub struct Alt<P1, P2>(pub P1, pub P2); + +impl<P1: Recognize, P2: Recognize> Recognize for Alt<P1, P2> { +    #[inline(always)] +    fn p(&self, s: &[u8]) -> Option<usize> { +        self.0.p(s).or_else(|| self.1.p(s)) +    } +} + +/// Choice from a homogenous slice of parsers. +pub struct OneOf<'a, P: 'a>(pub &'a [P]); + +impl<'a, P: Recognize> Recognize for OneOf<'a, P> { +    #[inline] +    fn p(&self, s: &[u8]) -> Option<usize> { +        for ref p in self.0 { +            if let Some(len) = p.p(s) { +                return Some(len); +            } +        } +        None +    } +} + +pub struct OneOrMore<P>(pub P); + +impl<P: Recognize> Recognize for OneOrMore<P> { +    #[inline] +    fn p(&self, s: &[u8]) -> Option<usize> { +        let mut i = 0; +        let mut count = 0; +        while let Some(len) = self.0.p(&s[i..]) { +            i += len; +            count += 1; +        } +        if count >= 1 { +            Some(i) +        } else { +            None +        } +    } +} + +pub struct ZeroOrMore<P>(pub P); + +impl<P: Recognize> Recognize for ZeroOrMore<P> { +    #[inline] +    fn p(&self, s: &[u8]) -> Option<usize> { +        let mut i = 0; +        while let Some(len) = self.0.p(&s[i..]) { +            i += len; +        } +        Some(i) +    } +} diff --git a/semver-parser/src/version.rs b/semver-parser/src/version.rs new file mode 100644 index 0000000..570f947 --- /dev/null +++ b/semver-parser/src/version.rs @@ -0,0 +1,365 @@ +use std::fmt; +use std::str::from_utf8; + +use recognize::*; + +use common::{self, numeric_identifier}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Version { +    pub major: u64, +    pub minor: u64, +    pub patch: u64, +    pub pre: Vec<Identifier>, +    pub build: Vec<Identifier>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Identifier { +    /// An identifier that's solely numbers. +    Numeric(u64), +    /// An identifier with letters and numbers. +    AlphaNumeric(String), +} + +pub fn parse(version: &str) -> Result<Version, String> { +    let s = version.trim().as_bytes(); +    let mut i = 0; +    let major = if let Some((major, len)) = numeric_identifier(&s[i..]) { +        i += len; +        major +    } else { +        return Err("Error parsing major identifier".to_string()); +    }; +    if let Some(len) = b'.'.p(&s[i..]) { +        i += len; +    } else { +        return Err("Expected dot".to_string()); +    } +    let minor = if let Some((minor, len)) = numeric_identifier(&s[i..]) { +        i += len; +        minor +    } else { +        return Err("Error parsing minor identifier".to_string()); +    }; +    if let Some(len) = b'.'.p(&s[i..]) { +        i += len; +    } else { +        return Err("Expected dot".to_string()); +    } +    let patch = if let Some((patch, len)) = numeric_identifier(&s[i..]) { +        i += len; +        patch +    } else { +        return Err("Error parsing patch identifier".to_string()); +    }; +    let (pre, pre_len) = common::parse_optional_meta(&s[i..], b'-')?; +    i += pre_len; +    let (build, build_len) = common::parse_optional_meta(&s[i..], b'+')?; +    i += build_len; +    if i != s.len() { +        return Err("Extra junk after valid version: ".to_string() + +            from_utf8(&s[i..]).unwrap()); +    } +    Ok(Version { +        major: major, +        minor: minor, +        patch: patch, +        pre: pre, +        build: build, +    }) +} + +impl fmt::Display for Version { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        try!(write!(f, "{}.{}.{}", self.major, self.minor, self.patch)); +        if !self.pre.is_empty() { +            let strs: Vec<_> = +                self.pre.iter().map(ToString::to_string).collect(); +            try!(write!(f, "-{}", strs.join("."))); +        } +        if !self.build.is_empty() { +            let strs: Vec<_> = +                self.build.iter().map(ToString::to_string).collect(); +            try!(write!(f, "+{}", strs.join("."))); +        } +        Ok(()) +    } +} + +impl fmt::Display for Identifier { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        match *self { +            Identifier::Numeric(ref id) => id.fmt(f), +            Identifier::AlphaNumeric(ref id) => id.fmt(f), +        } +    } +} + +#[cfg(test)] +mod tests { +    use version; +    use super::*; + +    #[test] +    fn parse_empty() { +        let version = ""; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "empty string incorrectly considered a valid parse"); +    } + +    #[test] +    fn parse_blank() { +        let version = "  "; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "blank string incorrectly considered a valid parse"); +    } + +    #[test] +    fn parse_no_minor_patch() { +        let version = "1"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); +    } + +    #[test] +    fn parse_no_patch() { +        let version = "1.2"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); +    } + +    #[test] +    fn parse_empty_pre() { +        let version = "1.2.3-"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); +    } + +    #[test] +    fn parse_letters() { +        let version = "a.b.c"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); +    } + +    #[test] +    fn parse_with_letters() { +        let version = "1.2.3 a.b.c"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); +    } + +    #[test] +    fn parse_basic_version() { +        let version = "1.2.3"; + +        let parsed = version::parse(version).unwrap(); + +        assert_eq!(1, parsed.major); +        assert_eq!(2, parsed.minor); +        assert_eq!(3, parsed.patch); +    } + +    #[test] +    fn parse_trims_input() { +        let version = "  1.2.3  "; + +        let parsed = version::parse(version).unwrap(); + +        assert_eq!(1, parsed.major); +        assert_eq!(2, parsed.minor); +        assert_eq!(3, parsed.patch); +    } + +    #[test] +    fn parse_no_major_leading_zeroes() { +        let version = "01.0.0"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "01 incorrectly considered a valid major version"); +    } + +    #[test] +    fn parse_no_minor_leading_zeroes() { +        let version = "0.01.0"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "01 incorrectly considered a valid minor version"); +    } + +    #[test] +    fn parse_no_patch_leading_zeroes() { +        let version = "0.0.01"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "01 incorrectly considered a valid patch version"); +    } + +    #[test] +    fn parse_no_major_overflow() { +        let version = "98765432109876543210.0.0"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid major version"); +    } + +    #[test] +    fn parse_no_minor_overflow() { +        let version = "0.98765432109876543210.0"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid minor version"); +    } + +    #[test] +    fn parse_no_patch_overflow() { +        let version = "0.0.98765432109876543210"; + +        let parsed = version::parse(version); + +        assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid patch version"); +    } + +    #[test] +    fn parse_basic_prerelease() { +        let version = "1.2.3-pre"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre"))]; +        assert_eq!(expected_pre, parsed.pre); +    } + +    #[test] +    fn parse_prerelease_alphanumeric() { +        let version = "1.2.3-alpha1"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))]; +        assert_eq!(expected_pre, parsed.pre); +    } + +    #[test] +    fn parse_prerelease_zero() { +        let version = "1.2.3-pre.0"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre")), +                                Identifier::Numeric(0)]; +        assert_eq!(expected_pre, parsed.pre); +    } + +    #[test] +    fn parse_basic_build() { +        let version = "1.2.3+build"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("build"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_build_alphanumeric() { +        let version = "1.2.3+build5"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_pre_and_build() { +        let version = "1.2.3-alpha1+build5"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))]; +        assert_eq!(expected_pre, parsed.pre); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_complex_metadata_01() { +        let version = "1.2.3-1.alpha1.9+build5.7.3aedf  "; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::Numeric(1), +                                Identifier::AlphaNumeric(String::from("alpha1")), +                                Identifier::Numeric(9)]; +        assert_eq!(expected_pre, parsed.pre); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("build5")), +                                  Identifier::Numeric(7), +                                  Identifier::AlphaNumeric(String::from("3aedf"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_complex_metadata_02() { +        let version = "0.4.0-beta.1+0851523"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")), +                                Identifier::Numeric(1)]; +        assert_eq!(expected_pre, parsed.pre); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("0851523"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_metadata_overflow() { +        let version = "0.4.0-beta.1+98765432109876543210"; + +        let parsed = version::parse(version).unwrap(); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")), +                                Identifier::Numeric(1)]; +        assert_eq!(expected_pre, parsed.pre); + +        let expected_build = vec![Identifier::AlphaNumeric(String::from("98765432109876543210"))]; +        assert_eq!(expected_build, parsed.build); +    } + +    #[test] +    fn parse_regression_01() { +        let version = "0.0.0-WIP"; + +        let parsed = version::parse(version).unwrap(); + +        assert_eq!(0, parsed.major); +        assert_eq!(0, parsed.minor); +        assert_eq!(0, parsed.patch); + +        let expected_pre = vec![Identifier::AlphaNumeric(String::from("WIP"))]; +        assert_eq!(expected_pre, parsed.pre); +    } +} | 
