From d23f524b0290e65d776af99c12147310071f47af Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 14:04:00 +0200 Subject: [PATCH 1/7] feat: support "version" check introduce a flexible version formatter which translates a version query in a flexible way,into a semver compatible one for Masterminds/semver library. Allows to check the dependencies vulnerabilities not only by package,but by version too --- Gopkg.lock | 35 +- Gopkg.toml | 8 + main.go | 17 +- versionformatter/versionformatter.go | 2970 +++++++++++++++++ versionformatter/versionformatter.peg | 264 ++ versionformatter/versionformatter_test.go | 72 + vulnfetcher/nodeswg/nodeswg.go | 31 +- vulnfetcher/nodeswg/nodeswg_test.go | 15 + vulnfetcher/ossvulnfetcher/osindexfetcher.go | 24 +- .../ossvulnfetcher/osindexfetcher_test.go | 7 + vulnfetcher/vulnfetcher.go | 75 + 11 files changed, 3491 insertions(+), 27 deletions(-) create mode 100644 versionformatter/versionformatter.go create mode 100644 versionformatter/versionformatter.peg create mode 100644 versionformatter/versionformatter_test.go create mode 100644 vulnfetcher/ossvulnfetcher/osindexfetcher_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 6d1ffe2..7a82738 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,12 +1,36 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/Masterminds/semver" + packages = ["."] + revision = "c7af12943936e8c39859482e61f0574c2fd7fc75" + version = "v1.4.2" + [[projects]] branch = "master" name = "github.com/dsnet/compress" - packages = [".","bzip2","bzip2/internal/sais","internal","internal/errors","internal/prefix"] + packages = [ + ".", + "bzip2", + "bzip2/internal/sais", + "internal", + "internal/errors", + "internal/prefix" + ] revision = "cc9eb1d7ad760af14e8f918698f745e80377af4f" +[[projects]] + name = "github.com/google/go-cmp" + packages = [ + "cmp", + "cmp/internal/diff", + "cmp/internal/function", + "cmp/internal/value" + ] + revision = "3af367b6b30c263d47e8895973edcca9a49cf029" + version = "v0.2.0" + [[projects]] name = "github.com/mholt/archiver" packages = ["."] @@ -27,13 +51,18 @@ [[projects]] name = "github.com/ulikunitz/xz" - packages = [".","internal/hash","internal/xlog","lzma"] + packages = [ + ".", + "internal/hash", + "internal/xlog", + "lzma" + ] revision = "0c6b41e72360850ca4f98dc341fd999726ea007f" version = "v0.5.4" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "287462403d228b629bfd5184971943def3ee7b2c4489b143f99f60a99149913d" + inputs-digest = "09ed926acb7b28a91b40ad7a2be52e0925a4685d42a2ec208ef93dfa866af25e" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 96cf02a..7e178db 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -32,3 +32,11 @@ [[constraint]] name = "github.com/mholt/archiver" version = "2.0.0" + +[[constraint]] + name = "github.com/Masterminds/semver" + version = "1.4.2" + +[[constraint]] + name = "github.com/google/go-cmp" + version = "0.2.0" diff --git a/main.go b/main.go index 8367203..13a43f4 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,10 @@ package main import ( "fmt" + "gammaray/pathrunner" + "gammaray/vulnfetcher/nodeswg" + "gammaray/vulnfetcher/ossvulnfetcher" "os" - - "github.com/dgonzalez/gammaray/pathrunner" - "github.com/dgonzalez/gammaray/vulnfetcher/nodeswg" - "github.com/dgonzalez/gammaray/vulnfetcher/ossvulnfetcher" ) // OSSIndexURL URL for OSSIndex. Is not a hardcoded value to facilitate testing. @@ -41,20 +40,21 @@ func main() { for _, singlePackage := range packages { vulnerabilitiesOSS, err := ossFetcher.Test(singlePackage.Name, singlePackage.Version) - // vulnerabilitiesNodeSWG, err := nodeswgFetcher.Test() + // vulnerabilitiesNodeSWG, err := nodeswgFetcher.Test(singlePackage.Name, singlePackage.Version) if err != nil { fmt.Println(err.Error()) os.Exit(1) } if len(vulnerabilitiesOSS) > 0 { - fmt.Printf("\tPackage: %s\n", singlePackage.Name) + fmt.Printf("\tPackage: %s (%s)\n", singlePackage.Name, singlePackage.Version) for _, vulnerability := range vulnerabilitiesOSS { fmt.Printf("\t\t- Vulnerability (OSS Index):\n") - fmt.Printf("\t\t\t- CVE: %s\n\t\tTitle: %s\n\t\tVersions: %s\n\t\tMore Info: [%s]\n", + fmt.Printf("\t\t\t- CVE: %s\n\t\tTitle: %s\n\t\tVersions: %s\n\t\tFixed: %s\n\t\tMore Info: [%s]\n", vulnerability.CVE, vulnerability.Title, vulnerability.Versions, + vulnerability.Fixed, vulnerability.References, ) } @@ -65,10 +65,11 @@ func main() { fmt.Printf("\tPackage: %s\n", singlePackage.Name) for _, vulnerability := range vulnerabilitiesNodeSWG { fmt.Printf("\t\t- Vulnerability (Node Security Working Group):\n") - fmt.Printf("\t\t\t- CVE: %s\n\t\tTitle: %s\n\t\tVersions: %s\n\t\tMore Info: [%s]\n", + fmt.Printf("\t\t\t- CVE: %s\n\t\tTitle: %s\n\t\tVersions: %s\n\t\tFixed: %s\n\t\tMore Info: [%s]\n", vulnerability.CVE, vulnerability.Title, vulnerability.Versions, + vulnerability.Fixed, vulnerability.References, ) } diff --git a/versionformatter/versionformatter.go b/versionformatter/versionformatter.go new file mode 100644 index 0000000..7dc9442 --- /dev/null +++ b/versionformatter/versionformatter.go @@ -0,0 +1,2970 @@ +// Code generated by pigeon; DO NOT EDIT. + +// version expression formatter +// will try to translate expressions in a semver version or version range +// compatible one, like this : +// 1.0.rc.1 --> 1.0.0-rc.1 +// <0.8 --> <0.8.0 +// ^5 || <=5.0-beta --> ^5.0.0 || <=5.0.0-beta +package versionformatter + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "math" + "os" + "sort" + "strconv" + "strings" + "unicode" + "unicode/utf8" +) + +func Format(expression string) (string, error) { + got, err := ParseReader("", strings.NewReader(expression)) + if err != nil { + log.Fatal("👮 In '"+expression+"':\n", err) + return "", err + } + return got.(string), err +} + +func toIfaceSlice(v interface{}) []interface{} { + if v == nil { + return nil + } + return v.([]interface{}) +} + +var g = &grammar{ + rules: []*rule{ + { + name: "Input", + pos: position{line: 29, col: 1, offset: 656}, + expr: &actionExpr{ + pos: position{line: 29, col: 10, offset: 665}, + run: (*parser).callonInput1, + expr: &seqExpr{ + pos: position{line: 29, col: 10, offset: 665}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 29, col: 10, offset: 665}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 29, col: 12, offset: 667}, + label: "expr", + expr: &choiceExpr{ + pos: position{line: 29, col: 18, offset: 673}, + alternatives: []interface{}{ + &ruleRefExpr{ + pos: position{line: 29, col: 18, offset: 673}, + name: "RangeList", + }, + &ruleRefExpr{ + pos: position{line: 29, col: 30, offset: 685}, + name: "Query", + }, + }, + }, + }, + &ruleRefExpr{ + pos: position{line: 29, col: 37, offset: 692}, + name: "_", + }, + &ruleRefExpr{ + pos: position{line: 29, col: 39, offset: 694}, + name: "EOF", + }, + }, + }, + }, + }, + { + name: "RangeList", + pos: position{line: 34, col: 1, offset: 774}, + expr: &actionExpr{ + pos: position{line: 34, col: 14, offset: 787}, + run: (*parser).callonRangeList1, + expr: &seqExpr{ + pos: position{line: 34, col: 14, offset: 787}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 34, col: 14, offset: 787}, + label: "head", + expr: &ruleRefExpr{ + pos: position{line: 34, col: 19, offset: 792}, + name: "Range", + }, + }, + &labeledExpr{ + pos: position{line: 34, col: 25, offset: 798}, + label: "tail", + expr: &zeroOrMoreExpr{ + pos: position{line: 34, col: 30, offset: 803}, + expr: &ruleRefExpr{ + pos: position{line: 34, col: 31, offset: 804}, + name: "RangeSubList", + }, + }, + }, + }, + }, + }, + }, + { + name: "RangeSubList", + pos: position{line: 46, col: 1, offset: 1002}, + expr: &actionExpr{ + pos: position{line: 46, col: 17, offset: 1018}, + run: (*parser).callonRangeSubList1, + expr: &seqExpr{ + pos: position{line: 46, col: 17, offset: 1018}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 46, col: 17, offset: 1018}, + label: "separator", + expr: &ruleRefExpr{ + pos: position{line: 46, col: 27, offset: 1028}, + name: "RangeListSeparator", + }, + }, + &labeledExpr{ + pos: position{line: 46, col: 46, offset: 1047}, + label: "version", + expr: &ruleRefExpr{ + pos: position{line: 46, col: 54, offset: 1055}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "RangeListSeparator", + pos: position{line: 50, col: 1, offset: 1120}, + expr: &actionExpr{ + pos: position{line: 50, col: 23, offset: 1142}, + run: (*parser).callonRangeListSeparator1, + expr: &choiceExpr{ + pos: position{line: 50, col: 24, offset: 1143}, + alternatives: []interface{}{ + &seqExpr{ + pos: position{line: 50, col: 24, offset: 1143}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 50, col: 24, offset: 1143}, + name: "CommaOrPipe", + }, + &ruleRefExpr{ + pos: position{line: 50, col: 36, offset: 1155}, + name: "_", + }, + }, + }, + &seqExpr{ + pos: position{line: 50, col: 40, offset: 1159}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 50, col: 40, offset: 1159}, + name: "MandatoryWhiteSpace", + }, + &ruleRefExpr{ + pos: position{line: 50, col: 60, offset: 1179}, + name: "CommaOrPipe", + }, + &ruleRefExpr{ + pos: position{line: 50, col: 72, offset: 1191}, + name: "_", + }, + }, + }, + &ruleRefExpr{ + pos: position{line: 50, col: 76, offset: 1195}, + name: "MandatoryWhiteSpace", + }, + }, + }, + }, + }, + { + name: "Range", + pos: position{line: 54, col: 1, offset: 1248}, + expr: &choiceExpr{ + pos: position{line: 54, col: 10, offset: 1257}, + alternatives: []interface{}{ + &ruleRefExpr{ + pos: position{line: 54, col: 10, offset: 1257}, + name: "PrimitiveRange", + }, + &ruleRefExpr{ + pos: position{line: 54, col: 27, offset: 1274}, + name: "Version", + }, + &ruleRefExpr{ + pos: position{line: 54, col: 37, offset: 1284}, + name: "TildeRange", + }, + &ruleRefExpr{ + pos: position{line: 54, col: 50, offset: 1297}, + name: "CaretRange", + }, + }, + }, + }, + { + name: "PrimitiveRange", + pos: position{line: 56, col: 1, offset: 1309}, + expr: &actionExpr{ + pos: position{line: 56, col: 19, offset: 1327}, + run: (*parser).callonPrimitiveRange1, + expr: &seqExpr{ + pos: position{line: 56, col: 20, offset: 1328}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 56, col: 20, offset: 1328}, + label: "startop", + expr: &ruleRefExpr{ + pos: position{line: 56, col: 28, offset: 1336}, + name: "StartRangeOperator", + }, + }, + &ruleRefExpr{ + pos: position{line: 56, col: 47, offset: 1355}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 56, col: 49, offset: 1357}, + label: "start", + expr: &ruleRefExpr{ + pos: position{line: 56, col: 55, offset: 1363}, + name: "Version", + }, + }, + &ruleRefExpr{ + pos: position{line: 56, col: 63, offset: 1371}, + name: "_", + }, + &zeroOrOneExpr{ + pos: position{line: 56, col: 65, offset: 1373}, + expr: &ruleRefExpr{ + pos: position{line: 56, col: 65, offset: 1373}, + name: "CommaOrPipe", + }, + }, + &ruleRefExpr{ + pos: position{line: 56, col: 78, offset: 1386}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 56, col: 80, offset: 1388}, + label: "endop", + expr: &ruleRefExpr{ + pos: position{line: 56, col: 86, offset: 1394}, + name: "EndRangeOperator", + }, + }, + &ruleRefExpr{ + pos: position{line: 56, col: 103, offset: 1411}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 56, col: 105, offset: 1413}, + label: "end", + expr: &ruleRefExpr{ + pos: position{line: 56, col: 109, offset: 1417}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "StartRangeOperator", + pos: position{line: 60, col: 1, offset: 1536}, + expr: &choiceExpr{ + pos: position{line: 60, col: 23, offset: 1558}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 60, col: 23, offset: 1558}, + val: ">=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 60, col: 30, offset: 1565}, + val: ">", + ignoreCase: false, + }, + }, + }, + }, + { + name: "EndRangeOperator", + pos: position{line: 62, col: 1, offset: 1570}, + expr: &choiceExpr{ + pos: position{line: 62, col: 21, offset: 1590}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 62, col: 21, offset: 1590}, + val: "<=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 62, col: 28, offset: 1597}, + val: "<", + ignoreCase: false, + }, + }, + }, + }, + { + name: "CommaOrPipe", + pos: position{line: 64, col: 1, offset: 1602}, + expr: &choiceExpr{ + pos: position{line: 64, col: 16, offset: 1617}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 64, col: 16, offset: 1617}, + val: ",", + ignoreCase: false, + }, + &actionExpr{ + pos: position{line: 64, col: 22, offset: 1623}, + run: (*parser).callonCommaOrPipe3, + expr: &litMatcher{ + pos: position{line: 64, col: 22, offset: 1623}, + val: "|", + ignoreCase: false, + }, + }, + }, + }, + }, + { + name: "HyphenRange", + pos: position{line: 68, col: 1, offset: 1659}, + expr: &actionExpr{ + pos: position{line: 68, col: 16, offset: 1674}, + run: (*parser).callonHyphenRange1, + expr: &seqExpr{ + pos: position{line: 68, col: 17, offset: 1675}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 68, col: 17, offset: 1675}, + label: "start", + expr: &ruleRefExpr{ + pos: position{line: 68, col: 23, offset: 1681}, + name: "Version", + }, + }, + &ruleRefExpr{ + pos: position{line: 68, col: 31, offset: 1689}, + name: "_", + }, + &litMatcher{ + pos: position{line: 68, col: 33, offset: 1691}, + val: "-", + ignoreCase: false, + }, + &ruleRefExpr{ + pos: position{line: 68, col: 37, offset: 1695}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 68, col: 39, offset: 1697}, + label: "end", + expr: &ruleRefExpr{ + pos: position{line: 68, col: 43, offset: 1701}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "XRange", + pos: position{line: 74, col: 1, offset: 1819}, + expr: &actionExpr{ + pos: position{line: 74, col: 11, offset: 1829}, + run: (*parser).callonXRange1, + expr: &labeledExpr{ + pos: position{line: 74, col: 11, offset: 1829}, + label: "version", + expr: &ruleRefExpr{ + pos: position{line: 74, col: 19, offset: 1837}, + name: "Version", + }, + }, + }, + }, + { + name: "TildeRange", + pos: position{line: 80, col: 1, offset: 1928}, + expr: &actionExpr{ + pos: position{line: 80, col: 15, offset: 1942}, + run: (*parser).callonTildeRange1, + expr: &seqExpr{ + pos: position{line: 80, col: 16, offset: 1943}, + exprs: []interface{}{ + &litMatcher{ + pos: position{line: 80, col: 16, offset: 1943}, + val: "~", + ignoreCase: false, + }, + &labeledExpr{ + pos: position{line: 80, col: 20, offset: 1947}, + label: "version", + expr: &ruleRefExpr{ + pos: position{line: 80, col: 28, offset: 1955}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "CaretRange", + pos: position{line: 86, col: 1, offset: 2057}, + expr: &actionExpr{ + pos: position{line: 86, col: 15, offset: 2071}, + run: (*parser).callonCaretRange1, + expr: &seqExpr{ + pos: position{line: 86, col: 16, offset: 2072}, + exprs: []interface{}{ + &litMatcher{ + pos: position{line: 86, col: 16, offset: 2072}, + val: "^", + ignoreCase: false, + }, + &labeledExpr{ + pos: position{line: 86, col: 20, offset: 2076}, + label: "version", + expr: &ruleRefExpr{ + pos: position{line: 86, col: 28, offset: 2084}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "Query", + pos: position{line: 92, col: 1, offset: 2186}, + expr: &actionExpr{ + pos: position{line: 92, col: 10, offset: 2195}, + run: (*parser).callonQuery1, + expr: &seqExpr{ + pos: position{line: 92, col: 10, offset: 2195}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 92, col: 10, offset: 2195}, + label: "head", + expr: &ruleRefExpr{ + pos: position{line: 92, col: 15, offset: 2200}, + name: "UnaryAndVersion", + }, + }, + &labeledExpr{ + pos: position{line: 92, col: 31, offset: 2216}, + label: "tail", + expr: &ruleRefExpr{ + pos: position{line: 92, col: 36, offset: 2221}, + name: "OptionalOperatorExpressions", + }, + }, + }, + }, + }, + }, + { + name: "OptionalOperatorExpressions", + pos: position{line: 98, col: 1, offset: 2352}, + expr: &actionExpr{ + pos: position{line: 98, col: 32, offset: 2383}, + run: (*parser).callonOptionalOperatorExpressions1, + expr: &labeledExpr{ + pos: position{line: 98, col: 32, offset: 2383}, + label: "ops", + expr: &zeroOrMoreExpr{ + pos: position{line: 98, col: 36, offset: 2387}, + expr: &ruleRefExpr{ + pos: position{line: 98, col: 37, offset: 2388}, + name: "OperatorExpression", + }, + }, + }, + }, + }, + { + name: "OperatorExpression", + pos: position{line: 113, col: 1, offset: 2649}, + expr: &actionExpr{ + pos: position{line: 113, col: 23, offset: 2671}, + run: (*parser).callonOperatorExpression1, + expr: &seqExpr{ + pos: position{line: 113, col: 23, offset: 2671}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 113, col: 23, offset: 2671}, + label: "op", + expr: &ruleRefExpr{ + pos: position{line: 113, col: 26, offset: 2674}, + name: "SpacedOperator", + }, + }, + &labeledExpr{ + pos: position{line: 113, col: 41, offset: 2689}, + label: "expr", + expr: &ruleRefExpr{ + pos: position{line: 113, col: 46, offset: 2694}, + name: "Query", + }, + }, + }, + }, + }, + }, + { + name: "SpacedOperator", + pos: position{line: 117, col: 1, offset: 2747}, + expr: &actionExpr{ + pos: position{line: 117, col: 19, offset: 2765}, + run: (*parser).callonSpacedOperator1, + expr: &labeledExpr{ + pos: position{line: 117, col: 19, offset: 2765}, + label: "op", + expr: &ruleRefExpr{ + pos: position{line: 117, col: 22, offset: 2768}, + name: "Operator", + }, + }, + }, + }, + { + name: "Primitive", + pos: position{line: 127, col: 1, offset: 2918}, + expr: &actionExpr{ + pos: position{line: 127, col: 15, offset: 2932}, + run: (*parser).callonPrimitive1, + expr: &seqExpr{ + pos: position{line: 127, col: 15, offset: 2932}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 127, col: 15, offset: 2932}, + label: "comparator", + expr: &ruleRefExpr{ + pos: position{line: 127, col: 26, offset: 2943}, + name: "Comparator", + }, + }, + &labeledExpr{ + pos: position{line: 127, col: 37, offset: 2954}, + label: "partial", + expr: &ruleRefExpr{ + pos: position{line: 127, col: 45, offset: 2962}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "UnaryAndVersion", + pos: position{line: 131, col: 1, offset: 3028}, + expr: &actionExpr{ + pos: position{line: 131, col: 20, offset: 3047}, + run: (*parser).callonUnaryAndVersion1, + expr: &seqExpr{ + pos: position{line: 131, col: 20, offset: 3047}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 131, col: 20, offset: 3047}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 131, col: 22, offset: 3049}, + label: "unary", + expr: &ruleRefExpr{ + pos: position{line: 131, col: 28, offset: 3055}, + name: "OptionalUnary", + }, + }, + &ruleRefExpr{ + pos: position{line: 131, col: 42, offset: 3069}, + name: "_", + }, + &labeledExpr{ + pos: position{line: 131, col: 44, offset: 3071}, + label: "version", + expr: &ruleRefExpr{ + pos: position{line: 131, col: 52, offset: 3079}, + name: "Version", + }, + }, + }, + }, + }, + }, + { + name: "OptionalUnary", + pos: position{line: 135, col: 1, offset: 3140}, + expr: &actionExpr{ + pos: position{line: 135, col: 18, offset: 3157}, + run: (*parser).callonOptionalUnary1, + expr: &labeledExpr{ + pos: position{line: 135, col: 18, offset: 3157}, + label: "op", + expr: &zeroOrOneExpr{ + pos: position{line: 135, col: 21, offset: 3160}, + expr: &ruleRefExpr{ + pos: position{line: 135, col: 21, offset: 3160}, + name: "UnaryOperator", + }, + }, + }, + }, + }, + { + name: "Version", + pos: position{line: 142, col: 1, offset: 3247}, + expr: &actionExpr{ + pos: position{line: 142, col: 12, offset: 3258}, + run: (*parser).callonVersion1, + expr: &seqExpr{ + pos: position{line: 142, col: 12, offset: 3258}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 142, col: 12, offset: 3258}, + label: "head", + expr: &ruleRefExpr{ + pos: position{line: 142, col: 17, offset: 3263}, + name: "Major", + }, + }, + &labeledExpr{ + pos: position{line: 142, col: 23, offset: 3269}, + label: "tail", + expr: &ruleRefExpr{ + pos: position{line: 142, col: 28, offset: 3274}, + name: "OptionalMinorPatchPreRelease", + }, + }, + }, + }, + }, + }, + { + name: "Major", + pos: position{line: 149, col: 1, offset: 3453}, + expr: &ruleRefExpr{ + pos: position{line: 149, col: 10, offset: 3462}, + name: "VersionPart", + }, + }, + { + name: "OptionalMinorPatchPreRelease", + pos: position{line: 151, col: 1, offset: 3475}, + expr: &actionExpr{ + pos: position{line: 151, col: 33, offset: 3507}, + run: (*parser).callonOptionalMinorPatchPreRelease1, + expr: &seqExpr{ + pos: position{line: 151, col: 33, offset: 3507}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 151, col: 33, offset: 3507}, + label: "head", + expr: &ruleRefExpr{ + pos: position{line: 151, col: 38, offset: 3512}, + name: "OptionalDotMinor", + }, + }, + &labeledExpr{ + pos: position{line: 151, col: 55, offset: 3529}, + label: "tail", + expr: &ruleRefExpr{ + pos: position{line: 151, col: 60, offset: 3534}, + name: "OptionalPatchPreRelease", + }, + }, + }, + }, + }, + }, + { + name: "OptionalDotMinor", + pos: position{line: 155, col: 1, offset: 3607}, + expr: &actionExpr{ + pos: position{line: 155, col: 21, offset: 3627}, + run: (*parser).callonOptionalDotMinor1, + expr: &labeledExpr{ + pos: position{line: 155, col: 21, offset: 3627}, + label: "minor", + expr: &zeroOrOneExpr{ + pos: position{line: 155, col: 27, offset: 3633}, + expr: &ruleRefExpr{ + pos: position{line: 155, col: 27, offset: 3633}, + name: "DotVersionPart", + }, + }, + }, + }, + }, + { + name: "Minor", + pos: position{line: 163, col: 1, offset: 3764}, + expr: &ruleRefExpr{ + pos: position{line: 163, col: 10, offset: 3773}, + name: "VersionPart", + }, + }, + { + name: "OptionalPatchPreRelease", + pos: position{line: 165, col: 1, offset: 3786}, + expr: &actionExpr{ + pos: position{line: 165, col: 28, offset: 3813}, + run: (*parser).callonOptionalPatchPreRelease1, + expr: &seqExpr{ + pos: position{line: 165, col: 28, offset: 3813}, + exprs: []interface{}{ + &labeledExpr{ + pos: position{line: 165, col: 28, offset: 3813}, + label: "head", + expr: &ruleRefExpr{ + pos: position{line: 165, col: 33, offset: 3818}, + name: "OptionalDotPatch", + }, + }, + &labeledExpr{ + pos: position{line: 165, col: 50, offset: 3835}, + label: "tail", + expr: &ruleRefExpr{ + pos: position{line: 165, col: 55, offset: 3840}, + name: "OptionalPreRelease", + }, + }, + }, + }, + }, + }, + { + name: "OptionalDotPatch", + pos: position{line: 174, col: 1, offset: 4004}, + expr: &actionExpr{ + pos: position{line: 174, col: 21, offset: 4024}, + run: (*parser).callonOptionalDotPatch1, + expr: &labeledExpr{ + pos: position{line: 174, col: 21, offset: 4024}, + label: "patch", + expr: &zeroOrOneExpr{ + pos: position{line: 174, col: 27, offset: 4030}, + expr: &ruleRefExpr{ + pos: position{line: 174, col: 27, offset: 4030}, + name: "DotVersionPart", + }, + }, + }, + }, + }, + { + name: "DotVersionPart", + pos: position{line: 181, col: 1, offset: 4126}, + expr: &actionExpr{ + pos: position{line: 181, col: 19, offset: 4144}, + run: (*parser).callonDotVersionPart1, + expr: &seqExpr{ + pos: position{line: 181, col: 19, offset: 4144}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 181, col: 19, offset: 4144}, + name: "Dot", + }, + &labeledExpr{ + pos: position{line: 181, col: 23, offset: 4148}, + label: "n", + expr: &ruleRefExpr{ + pos: position{line: 181, col: 25, offset: 4150}, + name: "VersionPart", + }, + }, + }, + }, + }, + }, + { + name: "Patch", + pos: position{line: 188, col: 1, offset: 4240}, + expr: &ruleRefExpr{ + pos: position{line: 188, col: 10, offset: 4249}, + name: "VersionPart", + }, + }, + { + name: "OptionalPreRelease", + pos: position{line: 190, col: 1, offset: 4262}, + expr: &actionExpr{ + pos: position{line: 190, col: 23, offset: 4284}, + run: (*parser).callonOptionalPreRelease1, + expr: &seqExpr{ + pos: position{line: 190, col: 23, offset: 4284}, + exprs: []interface{}{ + &zeroOrOneExpr{ + pos: position{line: 190, col: 23, offset: 4284}, + expr: &choiceExpr{ + pos: position{line: 190, col: 24, offset: 4285}, + alternatives: []interface{}{ + &ruleRefExpr{ + pos: position{line: 190, col: 24, offset: 4285}, + name: "Dash", + }, + &ruleRefExpr{ + pos: position{line: 190, col: 29, offset: 4290}, + name: "Dot", + }, + &ruleRefExpr{ + pos: position{line: 190, col: 33, offset: 4294}, + name: "Plus", + }, + }, + }, + }, + &labeledExpr{ + pos: position{line: 190, col: 40, offset: 4301}, + label: "prerelease", + expr: &zeroOrOneExpr{ + pos: position{line: 190, col: 51, offset: 4312}, + expr: &ruleRefExpr{ + pos: position{line: 190, col: 51, offset: 4312}, + name: "PreRelease", + }, + }, + }, + }, + }, + }, + }, + { + name: "PreRelease", + pos: position{line: 201, col: 1, offset: 4520}, + expr: &actionExpr{ + pos: position{line: 201, col: 15, offset: 4534}, + run: (*parser).callonPreRelease1, + expr: &zeroOrMoreExpr{ + pos: position{line: 201, col: 15, offset: 4534}, + expr: &ruleRefExpr{ + pos: position{line: 201, col: 15, offset: 4534}, + name: "PreReleaseContent", + }, + }, + }, + }, + { + name: "PreReleaseContent", + pos: position{line: 205, col: 1, offset: 4587}, + expr: &choiceExpr{ + pos: position{line: 205, col: 22, offset: 4608}, + alternatives: []interface{}{ + &ruleRefExpr{ + pos: position{line: 205, col: 22, offset: 4608}, + name: "VersionPart", + }, + &ruleRefExpr{ + pos: position{line: 205, col: 36, offset: 4622}, + name: "Word", + }, + &ruleRefExpr{ + pos: position{line: 205, col: 43, offset: 4629}, + name: "Dot", + }, + &ruleRefExpr{ + pos: position{line: 205, col: 49, offset: 4635}, + name: "Dash", + }, + &ruleRefExpr{ + pos: position{line: 205, col: 56, offset: 4642}, + name: "Plus", + }, + }, + }, + }, + { + name: "Dot", + displayName: "\".\"", + pos: position{line: 207, col: 1, offset: 4648}, + expr: &actionExpr{ + pos: position{line: 207, col: 12, offset: 4659}, + run: (*parser).callonDot1, + expr: &charClassMatcher{ + pos: position{line: 207, col: 12, offset: 4659}, + val: "[.]", + chars: []rune{'.'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + { + name: "Dash", + displayName: "\"-\"", + pos: position{line: 211, col: 1, offset: 4686}, + expr: &actionExpr{ + pos: position{line: 211, col: 13, offset: 4698}, + run: (*parser).callonDash1, + expr: &charClassMatcher{ + pos: position{line: 211, col: 13, offset: 4698}, + val: "[-]", + chars: []rune{'-'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + { + name: "Plus", + displayName: "\"+\"", + pos: position{line: 215, col: 1, offset: 4725}, + expr: &actionExpr{ + pos: position{line: 215, col: 13, offset: 4737}, + run: (*parser).callonPlus1, + expr: &charClassMatcher{ + pos: position{line: 215, col: 13, offset: 4737}, + val: "[+]", + chars: []rune{'+'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + { + name: "Word", + pos: position{line: 219, col: 1, offset: 4764}, + expr: &actionExpr{ + pos: position{line: 219, col: 9, offset: 4772}, + run: (*parser).callonWord1, + expr: &oneOrMoreExpr{ + pos: position{line: 219, col: 9, offset: 4772}, + expr: &charClassMatcher{ + pos: position{line: 219, col: 9, offset: 4772}, + val: "[a-zA-Z]", + ranges: []rune{'a', 'z', 'A', 'Z'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + }, + { + name: "VersionPart", + pos: position{line: 223, col: 1, offset: 4816}, + expr: &choiceExpr{ + pos: position{line: 224, col: 5, offset: 4832}, + alternatives: []interface{}{ + &actionExpr{ + pos: position{line: 224, col: 5, offset: 4832}, + run: (*parser).callonVersionPart2, + expr: &labeledExpr{ + pos: position{line: 224, col: 5, offset: 4832}, + label: "wildcard", + expr: &choiceExpr{ + pos: position{line: 224, col: 15, offset: 4842}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 224, col: 15, offset: 4842}, + val: "x", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 224, col: 21, offset: 4848}, + val: "X", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 224, col: 27, offset: 4854}, + val: "*", + ignoreCase: false, + }, + }, + }, + }, + }, + &actionExpr{ + pos: position{line: 226, col: 7, offset: 4910}, + run: (*parser).callonVersionPart8, + expr: &labeledExpr{ + pos: position{line: 226, col: 7, offset: 4910}, + label: "number", + expr: &ruleRefExpr{ + pos: position{line: 226, col: 14, offset: 4917}, + name: "Number", + }, + }, + }, + }, + }, + }, + { + name: "Number", + pos: position{line: 230, col: 1, offset: 4963}, + expr: &actionExpr{ + pos: position{line: 230, col: 11, offset: 4973}, + run: (*parser).callonNumber1, + expr: &oneOrMoreExpr{ + pos: position{line: 230, col: 11, offset: 4973}, + expr: &charClassMatcher{ + pos: position{line: 230, col: 11, offset: 4973}, + val: "[0-9]", + ranges: []rune{'0', '9'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + }, + { + name: "Operator", + pos: position{line: 241, col: 1, offset: 5231}, + expr: &choiceExpr{ + pos: position{line: 241, col: 13, offset: 5243}, + alternatives: []interface{}{ + &ruleRefExpr{ + pos: position{line: 241, col: 13, offset: 5243}, + name: "Or", + }, + &ruleRefExpr{ + pos: position{line: 241, col: 18, offset: 5248}, + name: "MandatoryWhiteSpace", + }, + }, + }, + }, + { + name: "Or", + pos: position{line: 243, col: 1, offset: 5269}, + expr: &actionExpr{ + pos: position{line: 243, col: 7, offset: 5275}, + run: (*parser).callonOr1, + expr: &labeledExpr{ + pos: position{line: 243, col: 7, offset: 5275}, + label: "op", + expr: &choiceExpr{ + pos: position{line: 243, col: 12, offset: 5280}, + alternatives: []interface{}{ + &seqExpr{ + pos: position{line: 243, col: 12, offset: 5280}, + exprs: []interface{}{ + &ruleRefExpr{ + pos: position{line: 243, col: 12, offset: 5280}, + name: "MandatoryWhiteSpace", + }, + &litMatcher{ + pos: position{line: 243, col: 32, offset: 5300}, + val: "||", + ignoreCase: false, + }, + }, + }, + &litMatcher{ + pos: position{line: 243, col: 39, offset: 5307}, + val: "||", + ignoreCase: false, + }, + }, + }, + }, + }, + }, + { + name: "Comparator", + pos: position{line: 247, col: 1, offset: 5345}, + expr: &actionExpr{ + pos: position{line: 247, col: 15, offset: 5359}, + run: (*parser).callonComparator1, + expr: &labeledExpr{ + pos: position{line: 247, col: 15, offset: 5359}, + label: "op", + expr: &choiceExpr{ + pos: position{line: 247, col: 19, offset: 5363}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 247, col: 19, offset: 5363}, + val: ">=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 247, col: 26, offset: 5370}, + val: "<=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 247, col: 33, offset: 5377}, + val: "<", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 247, col: 39, offset: 5383}, + val: ">", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 247, col: 45, offset: 5389}, + val: "=", + ignoreCase: false, + }, + }, + }, + }, + }, + }, + { + name: "UnaryOperator", + displayName: "\"unary\"", + pos: position{line: 251, col: 1, offset: 5434}, + expr: &actionExpr{ + pos: position{line: 251, col: 26, offset: 5459}, + run: (*parser).callonUnaryOperator1, + expr: &labeledExpr{ + pos: position{line: 251, col: 26, offset: 5459}, + label: "op", + expr: &zeroOrOneExpr{ + pos: position{line: 251, col: 29, offset: 5462}, + expr: &choiceExpr{ + pos: position{line: 251, col: 30, offset: 5463}, + alternatives: []interface{}{ + &litMatcher{ + pos: position{line: 251, col: 30, offset: 5463}, + val: "!=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 37, offset: 5470}, + val: "==", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 43, offset: 5476}, + val: "<=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 50, offset: 5483}, + val: ">=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 57, offset: 5490}, + val: "<", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 63, offset: 5496}, + val: ">", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 69, offset: 5502}, + val: "=", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 75, offset: 5508}, + val: "!", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 81, offset: 5514}, + val: "^", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 87, offset: 5520}, + val: "~", + ignoreCase: false, + }, + &litMatcher{ + pos: position{line: 251, col: 93, offset: 5526}, + val: "", + ignoreCase: false, + }, + }, + }, + }, + }, + }, + }, + { + name: "MandatoryWhiteSpace", + displayName: "\"mandatory whitespace\"", + pos: position{line: 258, col: 1, offset: 5612}, + expr: &actionExpr{ + pos: position{line: 258, col: 47, offset: 5658}, + run: (*parser).callonMandatoryWhiteSpace1, + expr: &oneOrMoreExpr{ + pos: position{line: 258, col: 47, offset: 5658}, + expr: &charClassMatcher{ + pos: position{line: 258, col: 47, offset: 5658}, + val: "[ \\t\\n\\r]", + chars: []rune{' ', '\t', '\n', '\r'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + }, + { + name: "_", + displayName: "\"whitespace\"", + pos: position{line: 262, col: 1, offset: 5700}, + expr: &zeroOrMoreExpr{ + pos: position{line: 262, col: 19, offset: 5718}, + expr: &charClassMatcher{ + pos: position{line: 262, col: 19, offset: 5718}, + val: "[ \\t\\n\\r]", + chars: []rune{' ', '\t', '\n', '\r'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + { + name: "EOF", + pos: position{line: 264, col: 1, offset: 5730}, + expr: ¬Expr{ + pos: position{line: 264, col: 8, offset: 5737}, + expr: &anyMatcher{ + line: 264, col: 9, offset: 5738, + }, + }, + }, + }, +} + +func (c *current) onInput1(expr interface{}) (interface{}, error) { + log.Println("👮 Input", expr.(string)) + return expr.(string), nil +} + +func (p *parser) callonInput1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onInput1(stack["expr"]) +} + +func (c *current) onRangeList1(head, tail interface{}) (interface{}, error) { + if tail == nil { + return head, nil + } + tailSl := toIfaceSlice(tail) + res := head.(string) + for _, part := range tailSl { + res += part.(string) + } + return res, nil +} + +func (p *parser) callonRangeList1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onRangeList1(stack["head"], stack["tail"]) +} + +func (c *current) onRangeSubList1(separator, version interface{}) (interface{}, error) { + return separator.(string) + version.(string), nil +} + +func (p *parser) callonRangeSubList1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onRangeSubList1(stack["separator"], stack["version"]) +} + +func (c *current) onRangeListSeparator1() (interface{}, error) { + return string(", "), nil +} + +func (p *parser) callonRangeListSeparator1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onRangeListSeparator1() +} + +func (c *current) onPrimitiveRange1(startop, start, endop, end interface{}) (interface{}, error) { + return string(startop.([]uint8)) + start.(string) + ", " + string(endop.([]uint8)) + end.(string), nil +} + +func (p *parser) callonPrimitiveRange1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onPrimitiveRange1(stack["startop"], stack["start"], stack["endop"], stack["end"]) +} + +func (c *current) onCommaOrPipe3() (interface{}, error) { + return string(", "), nil +} + +func (p *parser) callonCommaOrPipe3() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onCommaOrPipe3() +} + +func (c *current) onHyphenRange1(start, end interface{}) (interface{}, error) { + var res = start.(string) + " - " + end.(string) + log.Println("👮 HyphenRange", res) + return res, nil +} + +func (p *parser) callonHyphenRange1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onHyphenRange1(stack["start"], stack["end"]) +} + +func (c *current) onXRange1(version interface{}) (interface{}, error) { + var res = version.(string) + log.Println("👮 XRange", res) + return res, nil +} + +func (p *parser) callonXRange1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onXRange1(stack["version"]) +} + +func (c *current) onTildeRange1(version interface{}) (interface{}, error) { + var res = "~" + version.(string) + log.Println("👮 TildeRange", res) + return res, nil +} + +func (p *parser) callonTildeRange1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onTildeRange1(stack["version"]) +} + +func (c *current) onCaretRange1(version interface{}) (interface{}, error) { + var res = "^" + version.(string) + log.Println("👮 CaretRange", res) + return res, nil +} + +func (p *parser) callonCaretRange1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onCaretRange1(stack["version"]) +} + +func (c *current) onQuery1(head, tail interface{}) (interface{}, error) { + expr := head.(string) + tail.(string) + log.Println("👮 Expression", expr) + return expr, nil +} + +func (p *parser) callonQuery1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onQuery1(stack["head"], stack["tail"]) +} + +func (c *current) onOptionalOperatorExpressions1(ops interface{}) (interface{}, error) { + if ops == nil { + return "", nil + } + opsSl := toIfaceSlice(ops) + res := "" + for _, op := range opsSl { + res += op.(string) + } + if res != "" { + log.Println("👮 OptionalOperatorExpressions", res) + } + return res, nil +} + +func (p *parser) callonOptionalOperatorExpressions1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalOperatorExpressions1(stack["ops"]) +} + +func (c *current) onOperatorExpression1(op, expr interface{}) (interface{}, error) { + return op.(string) + expr.(string), nil +} + +func (p *parser) callonOperatorExpression1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOperatorExpression1(stack["op"], stack["expr"]) +} + +func (c *current) onSpacedOperator1(op interface{}) (interface{}, error) { + if op == " " { + return " ", nil + } + if op == "," { + return op.(string) + " ", nil + } + return " " + op.(string) + " ", nil +} + +func (p *parser) callonSpacedOperator1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onSpacedOperator1(stack["op"]) +} + +func (c *current) onPrimitive1(comparator, partial interface{}) (interface{}, error) { + return comparator.(string) + partial.(string), nil +} + +func (p *parser) callonPrimitive1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onPrimitive1(stack["comparator"], stack["partial"]) +} + +func (c *current) onUnaryAndVersion1(unary, version interface{}) (interface{}, error) { + return unary.(string) + version.(string), nil +} + +func (p *parser) callonUnaryAndVersion1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onUnaryAndVersion1(stack["unary"], stack["version"]) +} + +func (c *current) onOptionalUnary1(op interface{}) (interface{}, error) { + if op == nil { + return "", nil + } + return op.(string), nil +} + +func (p *parser) callonOptionalUnary1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalUnary1(stack["op"]) +} + +func (c *current) onVersion1(head, tail interface{}) (interface{}, error) { + log.Println("👮 major", head) + version := head.(string) + tail.(string) + log.Println("👮 whole version:", version) + return version, nil +} + +func (p *parser) callonVersion1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onVersion1(stack["head"], stack["tail"]) +} + +func (c *current) onOptionalMinorPatchPreRelease1(head, tail interface{}) (interface{}, error) { + return head.(string) + tail.(string), nil +} + +func (p *parser) callonOptionalMinorPatchPreRelease1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalMinorPatchPreRelease1(stack["head"], stack["tail"]) +} + +func (c *current) onOptionalDotMinor1(minor interface{}) (interface{}, error) { + if minor == nil { + return ".0", nil + } + log.Println("👮 minor", minor) + return minor.(string), nil +} + +func (p *parser) callonOptionalDotMinor1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalDotMinor1(stack["minor"]) +} + +func (c *current) onOptionalPatchPreRelease1(head, tail interface{}) (interface{}, error) { + if head == nil { + return ".0" + tail.(string), nil + } + log.Println("👮 patch", head) + return head.(string) + tail.(string), nil + +} + +func (p *parser) callonOptionalPatchPreRelease1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalPatchPreRelease1(stack["head"], stack["tail"]) +} + +func (c *current) onOptionalDotPatch1(patch interface{}) (interface{}, error) { + if patch == nil { + return ".0", nil + } + return patch.(string), nil +} + +func (p *parser) callonOptionalDotPatch1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalDotPatch1(stack["patch"]) +} + +func (c *current) onDotVersionPart1(n interface{}) (interface{}, error) { + if n == nil { + return ".0", nil + } + return "." + n.(string), nil +} + +func (p *parser) callonDotVersionPart1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onDotVersionPart1(stack["n"]) +} + +func (c *current) onOptionalPreRelease1(prerelease interface{}) (interface{}, error) { + if prerelease == nil { + return "", nil + } + log.Println("👮 prerelease", prerelease) + if prerelease.(string) == "" { + return "", nil + } + return "-" + prerelease.(string), nil +} + +func (p *parser) callonOptionalPreRelease1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOptionalPreRelease1(stack["prerelease"]) +} + +func (c *current) onPreRelease1() (interface{}, error) { + return string(c.text), nil +} + +func (p *parser) callonPreRelease1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onPreRelease1() +} + +func (c *current) onDot1() (interface{}, error) { + return ".", nil +} + +func (p *parser) callonDot1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onDot1() +} + +func (c *current) onDash1() (interface{}, error) { + return "-", nil +} + +func (p *parser) callonDash1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onDash1() +} + +func (c *current) onPlus1() (interface{}, error) { + return "=", nil +} + +func (p *parser) callonPlus1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onPlus1() +} + +func (c *current) onWord1() (interface{}, error) { + return string(c.text), nil +} + +func (p *parser) callonWord1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onWord1() +} + +func (c *current) onVersionPart2(wildcard interface{}) (interface{}, error) { + return string(wildcard.([]uint8)), nil + +} + +func (p *parser) callonVersionPart2() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onVersionPart2(stack["wildcard"]) +} + +func (c *current) onVersionPart8(number interface{}) (interface{}, error) { + return number.(string), nil + +} + +func (p *parser) callonVersionPart8() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onVersionPart8(stack["number"]) +} + +func (c *current) onNumber1() (interface{}, error) { + var value = string(c.text) + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + log.Print("Could not translate '", value, "' in 64bit int, will use original value") + return value, nil + } + + return strconv.FormatInt(i, 10), nil +} + +func (p *parser) callonNumber1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onNumber1() +} + +func (c *current) onOr1(op interface{}) (interface{}, error) { + return string("||"), nil +} + +func (p *parser) callonOr1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onOr1(stack["op"]) +} + +func (c *current) onComparator1(op interface{}) (interface{}, error) { + return string(op.([]uint8)), nil +} + +func (p *parser) callonComparator1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onComparator1(stack["op"]) +} + +func (c *current) onUnaryOperator1(op interface{}) (interface{}, error) { + if op == nil { + return "", nil + } + return string(op.([]uint8)), nil +} + +func (p *parser) callonUnaryOperator1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onUnaryOperator1(stack["op"]) +} + +func (c *current) onMandatoryWhiteSpace1() (interface{}, error) { + return string(" "), nil +} + +func (p *parser) callonMandatoryWhiteSpace1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onMandatoryWhiteSpace1() +} + +var ( + // errNoRule is returned when the grammar to parse has no rule. + errNoRule = errors.New("grammar has no rule") + + // errInvalidEntrypoint is returned when the specified entrypoint rule + // does not exit. + errInvalidEntrypoint = errors.New("invalid entrypoint") + + // errInvalidEncoding is returned when the source is not properly + // utf8-encoded. + errInvalidEncoding = errors.New("invalid encoding") + + // errMaxExprCnt is used to signal that the maximum number of + // expressions have been parsed. + errMaxExprCnt = errors.New("max number of expresssions parsed") +) + +// Option is a function that can set an option on the parser. It returns +// the previous setting as an Option. +type Option func(*parser) Option + +// MaxExpressions creates an Option to stop parsing after the provided +// number of expressions have been parsed, if the value is 0 then the parser will +// parse for as many steps as needed (possibly an infinite number). +// +// The default for maxExprCnt is 0. +func MaxExpressions(maxExprCnt uint64) Option { + return func(p *parser) Option { + oldMaxExprCnt := p.maxExprCnt + p.maxExprCnt = maxExprCnt + return MaxExpressions(oldMaxExprCnt) + } +} + +// Entrypoint creates an Option to set the rule name to use as entrypoint. +// The rule name must have been specified in the -alternate-entrypoints +// if generating the parser with the -optimize-grammar flag, otherwise +// it may have been optimized out. Passing an empty string sets the +// entrypoint to the first rule in the grammar. +// +// The default is to start parsing at the first rule in the grammar. +func Entrypoint(ruleName string) Option { + return func(p *parser) Option { + oldEntrypoint := p.entrypoint + p.entrypoint = ruleName + if ruleName == "" { + p.entrypoint = g.rules[0].name + } + return Entrypoint(oldEntrypoint) + } +} + +// Statistics adds a user provided Stats struct to the parser to allow +// the user to process the results after the parsing has finished. +// Also the key for the "no match" counter is set. +// +// Example usage: +// +// input := "input" +// stats := Stats{} +// _, err := Parse("input-file", []byte(input), Statistics(&stats, "no match")) +// if err != nil { +// log.Panicln(err) +// } +// b, err := json.MarshalIndent(stats.ChoiceAltCnt, "", " ") +// if err != nil { +// log.Panicln(err) +// } +// fmt.Println(string(b)) +// +func Statistics(stats *Stats, choiceNoMatch string) Option { + return func(p *parser) Option { + oldStats := p.Stats + p.Stats = stats + oldChoiceNoMatch := p.choiceNoMatch + p.choiceNoMatch = choiceNoMatch + if p.Stats.ChoiceAltCnt == nil { + p.Stats.ChoiceAltCnt = make(map[string]map[string]int) + } + return Statistics(oldStats, oldChoiceNoMatch) + } +} + +// Debug creates an Option to set the debug flag to b. When set to true, +// debugging information is printed to stdout while parsing. +// +// The default is false. +func Debug(b bool) Option { + return func(p *parser) Option { + old := p.debug + p.debug = b + return Debug(old) + } +} + +// Memoize creates an Option to set the memoize flag to b. When set to true, +// the parser will cache all results so each expression is evaluated only +// once. This guarantees linear parsing time even for pathological cases, +// at the expense of more memory and slower times for typical cases. +// +// The default is false. +func Memoize(b bool) Option { + return func(p *parser) Option { + old := p.memoize + p.memoize = b + return Memoize(old) + } +} + +// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. +// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) +// by character class matchers and is matched by the any matcher. +// The returned matched value, c.text and c.offset are NOT affected. +// +// The default is false. +func AllowInvalidUTF8(b bool) Option { + return func(p *parser) Option { + old := p.allowInvalidUTF8 + p.allowInvalidUTF8 = b + return AllowInvalidUTF8(old) + } +} + +// Recover creates an Option to set the recover flag to b. When set to +// true, this causes the parser to recover from panics and convert it +// to an error. Setting it to false can be useful while debugging to +// access the full stack trace. +// +// The default is true. +func Recover(b bool) Option { + return func(p *parser) Option { + old := p.recover + p.recover = b + return Recover(old) + } +} + +// GlobalStore creates an Option to set a key to a certain value in +// the globalStore. +func GlobalStore(key string, value interface{}) Option { + return func(p *parser) Option { + old := p.cur.globalStore[key] + p.cur.globalStore[key] = value + return GlobalStore(key, old) + } +} + +// InitState creates an Option to set a key to a certain value in +// the global "state" store. +func InitState(key string, value interface{}) Option { + return func(p *parser) Option { + old := p.cur.state[key] + p.cur.state[key] = value + return InitState(key, old) + } +} + +// ParseFile parses the file identified by filename. +func ParseFile(filename string, opts ...Option) (i interface{}, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = closeErr + } + }() + return ParseReader(filename, f, opts...) +} + +// ParseReader parses the data from r using filename as information in the +// error messages. +func ParseReader(filename string, r io.Reader, opts ...Option) (interface{}, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + return Parse(filename, b, opts...) +} + +// Parse parses the data from b using filename as information in the +// error messages. +func Parse(filename string, b []byte, opts ...Option) (interface{}, error) { + return newParser(filename, b, opts...).parse(g) +} + +// position records a position in the text. +type position struct { + line, col, offset int +} + +func (p position) String() string { + return fmt.Sprintf("%d:%d [%d]", p.line, p.col, p.offset) +} + +// savepoint stores all state required to go back to this point in the +// parser. +type savepoint struct { + position + rn rune + w int +} + +type current struct { + pos position // start position of the match + text []byte // raw text of the match + + // state is a store for arbitrary key,value pairs that the user wants to be + // tied to the backtracking of the parser. + // This is always rolled back if a parsing rule fails. + state storeDict + + // globalStore is a general store for the user to store arbitrary key-value + // pairs that they need to manage and that they do not want tied to the + // backtracking of the parser. This is only modified by the user and never + // rolled back by the parser. It is always up to the user to keep this in a + // consistent state. + globalStore storeDict +} + +type storeDict map[string]interface{} + +// the AST types... + +type grammar struct { + pos position + rules []*rule +} + +type rule struct { + pos position + name string + displayName string + expr interface{} +} + +type choiceExpr struct { + pos position + alternatives []interface{} +} + +type actionExpr struct { + pos position + expr interface{} + run func(*parser) (interface{}, error) +} + +type recoveryExpr struct { + pos position + expr interface{} + recoverExpr interface{} + failureLabel []string +} + +type seqExpr struct { + pos position + exprs []interface{} +} + +type throwExpr struct { + pos position + label string +} + +type labeledExpr struct { + pos position + label string + expr interface{} +} + +type expr struct { + pos position + expr interface{} +} + +type andExpr expr +type notExpr expr +type zeroOrOneExpr expr +type zeroOrMoreExpr expr +type oneOrMoreExpr expr + +type ruleRefExpr struct { + pos position + name string +} + +type stateCodeExpr struct { + pos position + run func(*parser) error +} + +type andCodeExpr struct { + pos position + run func(*parser) (bool, error) +} + +type notCodeExpr struct { + pos position + run func(*parser) (bool, error) +} + +type litMatcher struct { + pos position + val string + ignoreCase bool +} + +type charClassMatcher struct { + pos position + val string + basicLatinChars [128]bool + chars []rune + ranges []rune + classes []*unicode.RangeTable + ignoreCase bool + inverted bool +} + +type anyMatcher position + +// errList cumulates the errors found by the parser. +type errList []error + +func (e *errList) add(err error) { + *e = append(*e, err) +} + +func (e errList) err() error { + if len(e) == 0 { + return nil + } + e.dedupe() + return e +} + +func (e *errList) dedupe() { + var cleaned []error + set := make(map[string]bool) + for _, err := range *e { + if msg := err.Error(); !set[msg] { + set[msg] = true + cleaned = append(cleaned, err) + } + } + *e = cleaned +} + +func (e errList) Error() string { + switch len(e) { + case 0: + return "" + case 1: + return e[0].Error() + default: + var buf bytes.Buffer + + for i, err := range e { + if i > 0 { + buf.WriteRune('\n') + } + buf.WriteString(err.Error()) + } + return buf.String() + } +} + +// parserError wraps an error with a prefix indicating the rule in which +// the error occurred. The original error is stored in the Inner field. +type parserError struct { + Inner error + pos position + prefix string + expected []string +} + +// Error returns the error message. +func (p *parserError) Error() string { + return p.prefix + ": " + p.Inner.Error() +} + +// newParser creates a parser with the specified input source and options. +func newParser(filename string, b []byte, opts ...Option) *parser { + stats := Stats{ + ChoiceAltCnt: make(map[string]map[string]int), + } + + p := &parser{ + filename: filename, + errs: new(errList), + data: b, + pt: savepoint{position: position{line: 1}}, + recover: true, + cur: current{ + state: make(storeDict), + globalStore: make(storeDict), + }, + maxFailPos: position{col: 1, line: 1}, + maxFailExpected: make([]string, 0, 20), + Stats: &stats, + // start rule is rule [0] unless an alternate entrypoint is specified + entrypoint: g.rules[0].name, + emptyState: make(storeDict), + } + p.setOptions(opts) + + if p.maxExprCnt == 0 { + p.maxExprCnt = math.MaxUint64 + } + + return p +} + +// setOptions applies the options to the parser. +func (p *parser) setOptions(opts []Option) { + for _, opt := range opts { + opt(p) + } +} + +type resultTuple struct { + v interface{} + b bool + end savepoint +} + +const choiceNoMatch = -1 + +// Stats stores some statistics, gathered during parsing +type Stats struct { + // ExprCnt counts the number of expressions processed during parsing + // This value is compared to the maximum number of expressions allowed + // (set by the MaxExpressions option). + ExprCnt uint64 + + // ChoiceAltCnt is used to count for each ordered choice expression, + // which alternative is used how may times. + // These numbers allow to optimize the order of the ordered choice expression + // to increase the performance of the parser + // + // The outer key of ChoiceAltCnt is composed of the name of the rule as well + // as the line and the column of the ordered choice. + // The inner key of ChoiceAltCnt is the number (one-based) of the matching alternative. + // For each alternative the number of matches are counted. If an ordered choice does not + // match, a special counter is incremented. The name of this counter is set with + // the parser option Statistics. + // For an alternative to be included in ChoiceAltCnt, it has to match at least once. + ChoiceAltCnt map[string]map[string]int +} + +type parser struct { + filename string + pt savepoint + cur current + + data []byte + errs *errList + + depth int + recover bool + debug bool + + memoize bool + // memoization table for the packrat algorithm: + // map[offset in source] map[expression or rule] {value, match} + memo map[int]map[interface{}]resultTuple + + // rules table, maps the rule identifier to the rule node + rules map[string]*rule + // variables stack, map of label to value + vstack []map[string]interface{} + // rule stack, allows identification of the current rule in errors + rstack []*rule + + // parse fail + maxFailPos position + maxFailExpected []string + maxFailInvertExpected bool + + // max number of expressions to be parsed + maxExprCnt uint64 + // entrypoint for the parser + entrypoint string + + allowInvalidUTF8 bool + + *Stats + + choiceNoMatch string + // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse + recoveryStack []map[string]interface{} + + // emptyState contains an empty storeDict, which is used to optimize cloneState if global "state" store is not used. + emptyState storeDict +} + +// push a variable set on the vstack. +func (p *parser) pushV() { + if cap(p.vstack) == len(p.vstack) { + // create new empty slot in the stack + p.vstack = append(p.vstack, nil) + } else { + // slice to 1 more + p.vstack = p.vstack[:len(p.vstack)+1] + } + + // get the last args set + m := p.vstack[len(p.vstack)-1] + if m != nil && len(m) == 0 { + // empty map, all good + return + } + + m = make(map[string]interface{}) + p.vstack[len(p.vstack)-1] = m +} + +// pop a variable set from the vstack. +func (p *parser) popV() { + // if the map is not empty, clear it + m := p.vstack[len(p.vstack)-1] + if len(m) > 0 { + // GC that map + p.vstack[len(p.vstack)-1] = nil + } + p.vstack = p.vstack[:len(p.vstack)-1] +} + +// push a recovery expression with its labels to the recoveryStack +func (p *parser) pushRecovery(labels []string, expr interface{}) { + if cap(p.recoveryStack) == len(p.recoveryStack) { + // create new empty slot in the stack + p.recoveryStack = append(p.recoveryStack, nil) + } else { + // slice to 1 more + p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)+1] + } + + m := make(map[string]interface{}, len(labels)) + for _, fl := range labels { + m[fl] = expr + } + p.recoveryStack[len(p.recoveryStack)-1] = m +} + +// pop a recovery expression from the recoveryStack +func (p *parser) popRecovery() { + // GC that map + p.recoveryStack[len(p.recoveryStack)-1] = nil + + p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)-1] +} + +func (p *parser) print(prefix, s string) string { + if !p.debug { + return s + } + + fmt.Printf("%s %d:%d:%d: %s [%#U]\n", + prefix, p.pt.line, p.pt.col, p.pt.offset, s, p.pt.rn) + return s +} + +func (p *parser) in(s string) string { + p.depth++ + return p.print(strings.Repeat(" ", p.depth)+">", s) +} + +func (p *parser) out(s string) string { + p.depth-- + return p.print(strings.Repeat(" ", p.depth)+"<", s) +} + +func (p *parser) addErr(err error) { + p.addErrAt(err, p.pt.position, []string{}) +} + +func (p *parser) addErrAt(err error, pos position, expected []string) { + var buf bytes.Buffer + if p.filename != "" { + buf.WriteString(p.filename) + } + if buf.Len() > 0 { + buf.WriteString(":") + } + buf.WriteString(fmt.Sprintf("%d:%d (%d)", pos.line, pos.col, pos.offset)) + if len(p.rstack) > 0 { + if buf.Len() > 0 { + buf.WriteString(": ") + } + rule := p.rstack[len(p.rstack)-1] + if rule.displayName != "" { + buf.WriteString("rule " + rule.displayName) + } else { + buf.WriteString("rule " + rule.name) + } + } + pe := &parserError{Inner: err, pos: pos, prefix: buf.String(), expected: expected} + p.errs.add(pe) +} + +func (p *parser) failAt(fail bool, pos position, want string) { + // process fail if parsing fails and not inverted or parsing succeeds and invert is set + if fail == p.maxFailInvertExpected { + if pos.offset < p.maxFailPos.offset { + return + } + + if pos.offset > p.maxFailPos.offset { + p.maxFailPos = pos + p.maxFailExpected = p.maxFailExpected[:0] + } + + if p.maxFailInvertExpected { + want = "!" + want + } + p.maxFailExpected = append(p.maxFailExpected, want) + } +} + +// read advances the parser to the next rune. +func (p *parser) read() { + p.pt.offset += p.pt.w + rn, n := utf8.DecodeRune(p.data[p.pt.offset:]) + p.pt.rn = rn + p.pt.w = n + p.pt.col++ + if rn == '\n' { + p.pt.line++ + p.pt.col = 0 + } + + if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune + if !p.allowInvalidUTF8 { + p.addErr(errInvalidEncoding) + } + } +} + +// restore parser position to the savepoint pt. +func (p *parser) restore(pt savepoint) { + if p.debug { + defer p.out(p.in("restore")) + } + if pt.offset == p.pt.offset { + return + } + p.pt = pt +} + +// Cloner is implemented by any value that has a Clone method, which returns a +// copy of the value. This is mainly used for types which are not passed by +// value (e.g map, slice, chan) or structs that contain such types. +// +// This is used in conjunction with the global state feature to create proper +// copies of the state to allow the parser to properly restore the state in +// the case of backtracking. +type Cloner interface { + Clone() interface{} +} + +// clone and return parser current state. +func (p *parser) cloneState() storeDict { + if p.debug { + defer p.out(p.in("cloneState")) + } + + if len(p.cur.state) == 0 { + if len(p.emptyState) > 0 { + p.emptyState = make(storeDict) + } + return p.emptyState + } + + state := make(storeDict, len(p.cur.state)) + for k, v := range p.cur.state { + if c, ok := v.(Cloner); ok { + state[k] = c.Clone() + } else { + state[k] = v + } + } + return state +} + +// restore parser current state to the state storeDict. +// every restoreState should applied only one time for every cloned state +func (p *parser) restoreState(state storeDict) { + if p.debug { + defer p.out(p.in("restoreState")) + } + p.cur.state = state +} + +// get the slice of bytes from the savepoint start to the current position. +func (p *parser) sliceFrom(start savepoint) []byte { + return p.data[start.position.offset:p.pt.position.offset] +} + +func (p *parser) getMemoized(node interface{}) (resultTuple, bool) { + if len(p.memo) == 0 { + return resultTuple{}, false + } + m := p.memo[p.pt.offset] + if len(m) == 0 { + return resultTuple{}, false + } + res, ok := m[node] + return res, ok +} + +func (p *parser) setMemoized(pt savepoint, node interface{}, tuple resultTuple) { + if p.memo == nil { + p.memo = make(map[int]map[interface{}]resultTuple) + } + m := p.memo[pt.offset] + if m == nil { + m = make(map[interface{}]resultTuple) + p.memo[pt.offset] = m + } + m[node] = tuple +} + +func (p *parser) buildRulesTable(g *grammar) { + p.rules = make(map[string]*rule, len(g.rules)) + for _, r := range g.rules { + p.rules[r.name] = r + } +} + +func (p *parser) parse(g *grammar) (val interface{}, err error) { + if len(g.rules) == 0 { + p.addErr(errNoRule) + return nil, p.errs.err() + } + + // TODO : not super critical but this could be generated + p.buildRulesTable(g) + + if p.recover { + // panic can be used in action code to stop parsing immediately + // and return the panic as an error. + defer func() { + if e := recover(); e != nil { + if p.debug { + defer p.out(p.in("panic handler")) + } + val = nil + switch e := e.(type) { + case error: + p.addErr(e) + default: + p.addErr(fmt.Errorf("%v", e)) + } + err = p.errs.err() + } + }() + } + + startRule, ok := p.rules[p.entrypoint] + if !ok { + p.addErr(errInvalidEntrypoint) + return nil, p.errs.err() + } + + p.read() // advance to first rune + val, ok = p.parseRule(startRule) + if !ok { + if len(*p.errs) == 0 { + // If parsing fails, but no errors have been recorded, the expected values + // for the farthest parser position are returned as error. + maxFailExpectedMap := make(map[string]struct{}, len(p.maxFailExpected)) + for _, v := range p.maxFailExpected { + maxFailExpectedMap[v] = struct{}{} + } + expected := make([]string, 0, len(maxFailExpectedMap)) + eof := false + if _, ok := maxFailExpectedMap["!."]; ok { + delete(maxFailExpectedMap, "!.") + eof = true + } + for k := range maxFailExpectedMap { + expected = append(expected, k) + } + sort.Strings(expected) + if eof { + expected = append(expected, "EOF") + } + p.addErrAt(errors.New("no match found, expected: "+listJoin(expected, ", ", "or")), p.maxFailPos, expected) + } + + return nil, p.errs.err() + } + return val, p.errs.err() +} + +func listJoin(list []string, sep string, lastSep string) string { + switch len(list) { + case 0: + return "" + case 1: + return list[0] + default: + return fmt.Sprintf("%s %s %s", strings.Join(list[:len(list)-1], sep), lastSep, list[len(list)-1]) + } +} + +func (p *parser) parseRule(rule *rule) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseRule " + rule.name)) + } + + if p.memoize { + res, ok := p.getMemoized(rule) + if ok { + p.restore(res.end) + return res.v, res.b + } + } + + start := p.pt + p.rstack = append(p.rstack, rule) + p.pushV() + val, ok := p.parseExpr(rule.expr) + p.popV() + p.rstack = p.rstack[:len(p.rstack)-1] + if ok && p.debug { + p.print(strings.Repeat(" ", p.depth)+"MATCH", string(p.sliceFrom(start))) + } + + if p.memoize { + p.setMemoized(start, rule, resultTuple{val, ok, p.pt}) + } + return val, ok +} + +func (p *parser) parseExpr(expr interface{}) (interface{}, bool) { + var pt savepoint + + if p.memoize { + res, ok := p.getMemoized(expr) + if ok { + p.restore(res.end) + return res.v, res.b + } + pt = p.pt + } + + p.ExprCnt++ + if p.ExprCnt > p.maxExprCnt { + panic(errMaxExprCnt) + } + + var val interface{} + var ok bool + switch expr := expr.(type) { + case *actionExpr: + val, ok = p.parseActionExpr(expr) + case *andCodeExpr: + val, ok = p.parseAndCodeExpr(expr) + case *andExpr: + val, ok = p.parseAndExpr(expr) + case *anyMatcher: + val, ok = p.parseAnyMatcher(expr) + case *charClassMatcher: + val, ok = p.parseCharClassMatcher(expr) + case *choiceExpr: + val, ok = p.parseChoiceExpr(expr) + case *labeledExpr: + val, ok = p.parseLabeledExpr(expr) + case *litMatcher: + val, ok = p.parseLitMatcher(expr) + case *notCodeExpr: + val, ok = p.parseNotCodeExpr(expr) + case *notExpr: + val, ok = p.parseNotExpr(expr) + case *oneOrMoreExpr: + val, ok = p.parseOneOrMoreExpr(expr) + case *recoveryExpr: + val, ok = p.parseRecoveryExpr(expr) + case *ruleRefExpr: + val, ok = p.parseRuleRefExpr(expr) + case *seqExpr: + val, ok = p.parseSeqExpr(expr) + case *stateCodeExpr: + val, ok = p.parseStateCodeExpr(expr) + case *throwExpr: + val, ok = p.parseThrowExpr(expr) + case *zeroOrMoreExpr: + val, ok = p.parseZeroOrMoreExpr(expr) + case *zeroOrOneExpr: + val, ok = p.parseZeroOrOneExpr(expr) + default: + panic(fmt.Sprintf("unknown expression type %T", expr)) + } + if p.memoize { + p.setMemoized(pt, expr, resultTuple{val, ok, p.pt}) + } + return val, ok +} + +func (p *parser) parseActionExpr(act *actionExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseActionExpr")) + } + + start := p.pt + val, ok := p.parseExpr(act.expr) + if ok { + p.cur.pos = start.position + p.cur.text = p.sliceFrom(start) + state := p.cloneState() + actVal, err := act.run(p) + if err != nil { + p.addErrAt(err, start.position, []string{}) + } + p.restoreState(state) + + val = actVal + } + if ok && p.debug { + p.print(strings.Repeat(" ", p.depth)+"MATCH", string(p.sliceFrom(start))) + } + return val, ok +} + +func (p *parser) parseAndCodeExpr(and *andCodeExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseAndCodeExpr")) + } + + state := p.cloneState() + + ok, err := and.run(p) + if err != nil { + p.addErr(err) + } + p.restoreState(state) + + return nil, ok +} + +func (p *parser) parseAndExpr(and *andExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseAndExpr")) + } + + pt := p.pt + state := p.cloneState() + p.pushV() + _, ok := p.parseExpr(and.expr) + p.popV() + p.restoreState(state) + p.restore(pt) + + return nil, ok +} + +func (p *parser) parseAnyMatcher(any *anyMatcher) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseAnyMatcher")) + } + + if p.pt.rn == utf8.RuneError && p.pt.w == 0 { + // EOF - see utf8.DecodeRune + p.failAt(false, p.pt.position, ".") + return nil, false + } + start := p.pt + p.read() + p.failAt(true, start.position, ".") + return p.sliceFrom(start), true +} + +func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseCharClassMatcher")) + } + + cur := p.pt.rn + start := p.pt + + // can't match EOF + if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune + p.failAt(false, start.position, chr.val) + return nil, false + } + + if chr.ignoreCase { + cur = unicode.ToLower(cur) + } + + // try to match in the list of available chars + for _, rn := range chr.chars { + if rn == cur { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + // try to match in the list of ranges + for i := 0; i < len(chr.ranges); i += 2 { + if cur >= chr.ranges[i] && cur <= chr.ranges[i+1] { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + // try to match in the list of Unicode classes + for _, cl := range chr.classes { + if unicode.Is(cl, cur) { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + if chr.inverted { + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + p.failAt(false, start.position, chr.val) + return nil, false +} + +func (p *parser) incChoiceAltCnt(ch *choiceExpr, altI int) { + choiceIdent := fmt.Sprintf("%s %d:%d", p.rstack[len(p.rstack)-1].name, ch.pos.line, ch.pos.col) + m := p.ChoiceAltCnt[choiceIdent] + if m == nil { + m = make(map[string]int) + p.ChoiceAltCnt[choiceIdent] = m + } + // We increment altI by 1, so the keys do not start at 0 + alt := strconv.Itoa(altI + 1) + if altI == choiceNoMatch { + alt = p.choiceNoMatch + } + m[alt]++ +} + +func (p *parser) parseChoiceExpr(ch *choiceExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseChoiceExpr")) + } + + for altI, alt := range ch.alternatives { + // dummy assignment to prevent compile error if optimized + _ = altI + + state := p.cloneState() + + p.pushV() + val, ok := p.parseExpr(alt) + p.popV() + if ok { + p.incChoiceAltCnt(ch, altI) + return val, ok + } + p.restoreState(state) + } + p.incChoiceAltCnt(ch, choiceNoMatch) + return nil, false +} + +func (p *parser) parseLabeledExpr(lab *labeledExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseLabeledExpr")) + } + + p.pushV() + val, ok := p.parseExpr(lab.expr) + p.popV() + if ok && lab.label != "" { + m := p.vstack[len(p.vstack)-1] + m[lab.label] = val + } + return val, ok +} + +func (p *parser) parseLitMatcher(lit *litMatcher) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseLitMatcher")) + } + + ignoreCase := "" + if lit.ignoreCase { + ignoreCase = "i" + } + val := fmt.Sprintf("%q%s", lit.val, ignoreCase) + start := p.pt + for _, want := range lit.val { + cur := p.pt.rn + if lit.ignoreCase { + cur = unicode.ToLower(cur) + } + if cur != want { + p.failAt(false, start.position, val) + p.restore(start) + return nil, false + } + p.read() + } + p.failAt(true, start.position, val) + return p.sliceFrom(start), true +} + +func (p *parser) parseNotCodeExpr(not *notCodeExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseNotCodeExpr")) + } + + state := p.cloneState() + + ok, err := not.run(p) + if err != nil { + p.addErr(err) + } + p.restoreState(state) + + return nil, !ok +} + +func (p *parser) parseNotExpr(not *notExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseNotExpr")) + } + + pt := p.pt + state := p.cloneState() + p.pushV() + p.maxFailInvertExpected = !p.maxFailInvertExpected + _, ok := p.parseExpr(not.expr) + p.maxFailInvertExpected = !p.maxFailInvertExpected + p.popV() + p.restoreState(state) + p.restore(pt) + + return nil, !ok +} + +func (p *parser) parseOneOrMoreExpr(expr *oneOrMoreExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseOneOrMoreExpr")) + } + + var vals []interface{} + + for { + p.pushV() + val, ok := p.parseExpr(expr.expr) + p.popV() + if !ok { + if len(vals) == 0 { + // did not match once, no match + return nil, false + } + return vals, true + } + vals = append(vals, val) + } +} + +func (p *parser) parseRecoveryExpr(recover *recoveryExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseRecoveryExpr (" + strings.Join(recover.failureLabel, ",") + ")")) + } + + p.pushRecovery(recover.failureLabel, recover.recoverExpr) + val, ok := p.parseExpr(recover.expr) + p.popRecovery() + + return val, ok +} + +func (p *parser) parseRuleRefExpr(ref *ruleRefExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseRuleRefExpr " + ref.name)) + } + + if ref.name == "" { + panic(fmt.Sprintf("%s: invalid rule: missing name", ref.pos)) + } + + rule := p.rules[ref.name] + if rule == nil { + p.addErr(fmt.Errorf("undefined rule: %s", ref.name)) + return nil, false + } + return p.parseRule(rule) +} + +func (p *parser) parseSeqExpr(seq *seqExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseSeqExpr")) + } + + vals := make([]interface{}, 0, len(seq.exprs)) + + pt := p.pt + state := p.cloneState() + for _, expr := range seq.exprs { + val, ok := p.parseExpr(expr) + if !ok { + p.restoreState(state) + p.restore(pt) + return nil, false + } + vals = append(vals, val) + } + return vals, true +} + +func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseStateCodeExpr")) + } + + err := state.run(p) + if err != nil { + p.addErr(err) + } + return nil, true +} + +func (p *parser) parseThrowExpr(expr *throwExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseThrowExpr")) + } + + for i := len(p.recoveryStack) - 1; i >= 0; i-- { + if recoverExpr, ok := p.recoveryStack[i][expr.label]; ok { + if val, ok := p.parseExpr(recoverExpr); ok { + return val, ok + } + } + } + + return nil, false +} + +func (p *parser) parseZeroOrMoreExpr(expr *zeroOrMoreExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseZeroOrMoreExpr")) + } + + var vals []interface{} + + for { + p.pushV() + val, ok := p.parseExpr(expr.expr) + p.popV() + if !ok { + return vals, true + } + vals = append(vals, val) + } +} + +func (p *parser) parseZeroOrOneExpr(expr *zeroOrOneExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseZeroOrOneExpr")) + } + + p.pushV() + val, _ := p.parseExpr(expr.expr) + p.popV() + // whether it matched or not, consider it a match + return val, true +} diff --git a/versionformatter/versionformatter.peg b/versionformatter/versionformatter.peg new file mode 100644 index 0000000..7e36c1f --- /dev/null +++ b/versionformatter/versionformatter.peg @@ -0,0 +1,264 @@ +{ +// version expression formatter +// will try to translate expressions in a semver version or version range +// compatible one, like this : +// 1.0.rc.1 --> 1.0.0-rc.1 +// <0.8 --> <0.8.0 +// ^5 || <=5.0-beta --> ^5.0.0 || <=5.0.0-beta +package versionformatter + +import "log" + +func Format(expression string) (string, error) { + got, err := ParseReader("", strings.NewReader(expression)) + if err != nil { + log.Fatal("👮 In '"+ expression + "':\n", err) + return "", err + } + return got.(string), err +} + +func toIfaceSlice(v interface{}) []interface{} { + if v == nil { + return nil + } + return v.([]interface{}) +} +} + +Input <- _ expr:(RangeList / Query) _ EOF { + log.Println("👮 Input", expr.(string)) + return expr.(string), nil +} + +RangeList <- head:Range tail:(RangeSubList)* { + if(tail == nil) { + return head, nil + } + tailSl := toIfaceSlice(tail) + res := head.(string) + for _, part := range tailSl { + res += part.(string) + } + return res, nil +} + +RangeSubList <- separator:RangeListSeparator version:Version { + return separator.(string) + version.(string), nil +} + +RangeListSeparator <- (CommaOrPipe _ / MandatoryWhiteSpace CommaOrPipe _ / MandatoryWhiteSpace) { + return string(", "), nil +} + +Range <- PrimitiveRange / Version / TildeRange / CaretRange + +PrimitiveRange <- (startop:StartRangeOperator _ start:Version _ CommaOrPipe? _ endop:EndRangeOperator _ end:Version) { + return string(startop.([]uint8)) + start.(string) + ", " + string(endop.([]uint8)) + end.(string), nil +} + +StartRangeOperator <- ">=" / ">" + +EndRangeOperator <- "<=" / "<" + +CommaOrPipe <- "," / "|" { + return string(", "), nil +} + +HyphenRange <- (start:Version _ "-" _ end:Version) { + var res = start.(string) + " - " + end.(string) + log.Println("👮 HyphenRange", res) + return res, nil +} + +XRange <- version:Version { + var res = version.(string) + log.Println("👮 XRange", res) + return res, nil +} + +TildeRange <- ("~" version:Version) { + var res = "~" + version.(string) + log.Println("👮 TildeRange", res) + return res, nil +} + +CaretRange <- ("^" version:Version) { + var res = "^" + version.(string) + log.Println("👮 CaretRange", res) + return res, nil +} + +Query <- head:UnaryAndVersion tail:OptionalOperatorExpressions { + expr := head.(string) + tail.(string) + log.Println("👮 Expression", expr) + return expr, nil +} + +OptionalOperatorExpressions <- ops:(OperatorExpression)* { + if(ops == nil) { + return "", nil + } + opsSl := toIfaceSlice(ops) + res := "" + for _, op := range opsSl { + res += op.(string) + } + if res != "" { + log.Println("👮 OptionalOperatorExpressions", res) + } + return res, nil +} + +OperatorExpression <- op:SpacedOperator expr:Query { + return op.(string) + expr.(string), nil +} + +SpacedOperator <- op:Operator { + if(op == " ") { + return " ", nil + } + if(op == ",") { + return op.(string) + " ", nil + } + return " " + op.(string) + " ", nil +} + +Primitive <- comparator:Comparator partial:Version { + return comparator.(string) + partial.(string), nil +} + +UnaryAndVersion <- _ unary:OptionalUnary _ version:Version { + return unary.(string) + version.(string), nil +} + +OptionalUnary <- op:UnaryOperator? { + if(op == nil) { + return "", nil + } + return op.(string), nil +} + +Version <- head:Major tail:OptionalMinorPatchPreRelease { + log.Println("👮 major", head) + version := head.(string) + tail.(string) + log.Println("👮 whole version:", version) + return version, nil +} + +Major <- VersionPart + +OptionalMinorPatchPreRelease <- head:OptionalDotMinor tail:OptionalPatchPreRelease { + return head.(string) + tail.(string), nil +} + +OptionalDotMinor <- minor:DotVersionPart? { + if(minor == nil) { + return ".0", nil + } + log.Println("👮 minor", minor) + return minor.(string), nil +} + +Minor <- VersionPart + +OptionalPatchPreRelease <- head:OptionalDotPatch tail:OptionalPreRelease { + if(head == nil) { + return ".0" + tail.(string), nil + } + log.Println("👮 patch", head) + return head.(string) + tail.(string), nil + +} + +OptionalDotPatch <- patch:DotVersionPart? { + if(patch == nil) { + return ".0", nil + } + return patch.(string), nil +} + +DotVersionPart <- Dot n:VersionPart { + if(n == nil) { + return ".0", nil + } + return "." + n.(string), nil +} + +Patch <- VersionPart + +OptionalPreRelease <- (Dash/Dot/Plus)? prerelease:PreRelease? { + if(prerelease == nil) { + return "", nil + } + log.Println("👮 prerelease", prerelease) + if(prerelease.(string) == "") { + return "", nil + } + return "-" + prerelease.(string), nil +} + +PreRelease <- PreReleaseContent* { + return string(c.text), nil +} + +PreReleaseContent <- VersionPart / Word / Dot / Dash / Plus + +Dot "." <- [.] { + return ".", nil +} + +Dash "-" <- [-] { + return "-", nil +} + +Plus "+" <- [+] { + return "=", nil +} + +Word <- [a-zA-Z]+ { + return string(c.text), nil +} + +VersionPart + = wildcard:("x" / "X" / "*") { + return string(wildcard.([]uint8)), nil + } / number:Number { + return number.(string), nil + } + +Number <- [0-9]+ { + var value = string(c.text) + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + log.Print("Could not translate '", value, "' in 64bit int, will use original value") + return value, nil + } + + return strconv.FormatInt(i, 10), nil +} + +Operator <- Or / MandatoryWhiteSpace + +Or <- op:( MandatoryWhiteSpace "||" / "||") { + return string("||"), nil +} + +Comparator <- op:(">=" / "<=" / "<" / ">" / "=") { + return string(op.([]uint8)), nil +} + +UnaryOperator "unary" <- op:("!=" / "==" /"<=" / ">=" / "<" / ">" / "=" / "!" / "^" / "~" / "")? { + if(op == nil) { + return "", nil + } + return string(op.([]uint8)), nil +} + +MandatoryWhiteSpace "mandatory whitespace" <- [ \t\n\r]+ { + return string(" "), nil +} + +_ "whitespace" <- [ \t\n\r]* + +EOF <- !. diff --git a/versionformatter/versionformatter_test.go b/versionformatter/versionformatter_test.go new file mode 100644 index 0000000..76d5241 --- /dev/null +++ b/versionformatter/versionformatter_test.go @@ -0,0 +1,72 @@ +package versionformatter + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSemverVersion(t *testing.T) { + res, _ := Format("1.0.0-rc1") + fmt.Println("TestSemverVersion result:", res) + if diff := cmp.Diff(res, "1.0.0-rc1"); diff != "" { + t.Errorf("TestSemverVersion: after Format : (-got +want)\n%s", diff) + } +} + +func TestNotSemverVersion(t *testing.T) { + res, _ := Format("1.0.rc.1") + fmt.Println("TestNotSemverVersion result:", res) + if diff := cmp.Diff(res, "1.0.0-rc.1"); diff != "" { + t.Errorf("TestNotSemverVersion: after Format : (-got +want)\n%s", diff) + } +} + +func TestNotSemverVersionAgain(t *testing.T) { + res, _ := Format("0.8.beta-1") + fmt.Println("TestNotSemverVersionAgain result:", res) + if diff := cmp.Diff(res, "0.8.0-beta-1"); diff != "" { + t.Errorf("TestNotSemverVersionAgain: after Format : (-got +want)\n%s", diff) + } +} + +func TestNotSemverSimpleExpression(t *testing.T) { + res, _ := Format("<0.8") + fmt.Println("TestNotSemverSimpleExpression result:", res) + if diff := cmp.Diff(res, "<0.8.0"); diff != "" { + t.Errorf("TestNotSemverSimpleExpression: after Format : (-got +want)\n%s", diff) + } +} + +func TestSemverAndExpression(t *testing.T) { + res, _ := Format(">=1.1.0 <=1.1.1") + fmt.Println("TestNotSemverSimpleExpression result:", res) + if diff := cmp.Diff(res, ">=1.1.0, <=1.1.1"); diff != "" { + t.Errorf("TestNotSemverSimpleExpression: after Format : (-got +want)\n%s", diff) + } +} + +func TestSemverOrExpression(t *testing.T) { + res, _ := Format(">=1.1.0 || <=1.1.1") + fmt.Println("TestSemverOrExpression result:", res) + if diff := cmp.Diff(res, ">=1.1.0 || <=1.1.1"); diff != "" { + t.Errorf("TestSemverOrExpression: after Format : (-got +want)\n%s", diff) + } +} + +func TestComplexExpression(t *testing.T) { + res, _ := Format(" >=0.08beta-1 || !1.rc.1 <=1.rc+build.543 || >2 ") + fmt.Println("TestComplexExpression result:", res) + if diff := cmp.Diff(res, ">=0.8.0-beta-1 || !1.0.0-rc.1 <=1.0.0-rc+build.543 || >2.0.0"); diff != "" { + t.Errorf("TestComplexExpression: after Format : (-got +want)\n%s", diff) + } +} + +func TestSimplePipeExpression(t *testing.T) { + res, _ := Format("2.0.0 2.0.0-x | 2.1.0-x 2.1.1 2.1.2") + fmt.Println("TestSimplePipeExpression result:", res) + if diff := cmp.Diff(res, "2.0.0, 2.0.0-x, 2.1.0-x, 2.1.1, 2.1.2"); diff != "" { + t.Errorf("TestComplexExpression: after Format : (-got +want)\n%s", diff) + } +} diff --git a/vulnfetcher/nodeswg/nodeswg.go b/vulnfetcher/nodeswg/nodeswg.go index dc777c8..bcbd9f4 100644 --- a/vulnfetcher/nodeswg/nodeswg.go +++ b/vulnfetcher/nodeswg/nodeswg.go @@ -2,6 +2,8 @@ package nodeswg import ( "encoding/json" + "fmt" + "gammaray/vulnfetcher" "io" "net/http" "os" @@ -9,7 +11,6 @@ import ( "path/filepath" "strings" - "github.com/dgonzalez/gammaray/vulnfetcher" "github.com/mholt/archiver" ) @@ -24,6 +25,7 @@ type Vulnerability struct { Module string `json:"module_name"` CVES []string `json:"cves"` VulnerableVersions string `json:"vulnerable_versions"` + FixedVersions string `json:"patched_versions"` Title string `json:"title"` References string `json:"references"` Overview string `json:"overview"` @@ -90,15 +92,26 @@ func (n *Fetcher) Test(module string, version string) ([]vulnfetcher.Vulnerabili var vulnerabilities []vulnfetcher.Vulnerability for _, vulnerability := range n.vulnerabilities { - if module == vulnerability.Module { - vulnerabilities = append(vulnerabilities, vulnfetcher.Vulnerability{ - CVE: strings.Join(vulnerability.CVES, " "), - Title: vulnerability.Title, - Description: vulnerability.Overview, - Versions: vulnerability.VulnerableVersions, - References: vulnerability.References, - }) + if module != vulnerability.Module { + continue } + var vuln = vulnfetcher.Vulnerability{ + CVE: strings.Join(vulnerability.CVES, " "), + Title: vulnerability.Title, + Description: vulnerability.Overview, + Versions: vulnerability.VulnerableVersions, + Fixed: vulnerability.FixedVersions, + References: vulnerability.References, + } + fmt.Println("✨ Node SWG Vulnerability check for ", module, "(", version, ") in '", vuln.Versions, "' excluding '", vuln.Fixed, "'") + isImpacted, err := vulnfetcher.IsImpactedByVulnerability(module, version, &vuln) + if err != nil { + return nil, err + } + if !isImpacted { + continue + } + vulnerabilities = append(vulnerabilities, vuln) } return vulnerabilities, nil diff --git a/vulnfetcher/nodeswg/nodeswg_test.go b/vulnfetcher/nodeswg/nodeswg_test.go index 1b709e2..63f1240 100644 --- a/vulnfetcher/nodeswg/nodeswg_test.go +++ b/vulnfetcher/nodeswg/nodeswg_test.go @@ -48,6 +48,21 @@ func TestTestExistingPackage(t *testing.T) { assert.True(t, strings.HasPrefix(vulnerabilities[0].References, "- https://www.npmjs.org/package/bassmaster")) } +func TestTestExistingPackageWithFixedVersion(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./test_data/test-data.zip") + }), + ) + nodeFetcher := New(server.URL) + err := nodeFetcher.Fetch() + assert.NoErr(t, err) + + vulnerabilities, err := nodeFetcher.Test("bassmaster", "1.6.0") + assert.NoErr(t, err) + assert.Equal(t, 0, len(vulnerabilities), "number of vulns") +} + func TestTestUnexistingPackage(t *testing.T) { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/vulnfetcher/ossvulnfetcher/osindexfetcher.go b/vulnfetcher/ossvulnfetcher/osindexfetcher.go index 3761ed1..2bec4b1 100644 --- a/vulnfetcher/ossvulnfetcher/osindexfetcher.go +++ b/vulnfetcher/ossvulnfetcher/osindexfetcher.go @@ -4,11 +4,11 @@ import ( "bytes" "encoding/json" "errors" + "fmt" + "gammaray/vulnfetcher" "io/ioutil" "net/http" "strings" - - "github.com/dgonzalez/gammaray/vulnfetcher" ) // OSSPackageRequest request for a package @@ -24,11 +24,12 @@ type OSSPackageResponse struct { // OSSVulnerability vulnerability for a package response type OSSVulnerability struct { - Title string `json:"title"` - Description string `json:"description"` - CVE string `json:"cve"` - Versions []string `json:"versions"` - References []string `json:"references"` + Title string `json:"title"` + Description string `json:"description"` + CVE string `json:"cve"` + Versions []string `json:"versions"` + FixedVersions []string `json:"fixed"` + References []string `json:"references"` } // OSSIndexFetcher fetches the node.js security vulnerabilities @@ -82,8 +83,17 @@ func (n *OSSIndexFetcher) Test(name string, version string) ([]vulnfetcher.Vulne Title: vulnerability.Title, Description: vulnerability.Description, Versions: strings.Join(vulnerability.Versions, " "), + Fixed: strings.Join(vulnerability.FixedVersions, " "), References: strings.Join(vulnerability.References, " "), } + fmt.Println("✨ OSS Vulnerability check for ", name, "(", version, ") in ", processedVulnerability.Versions, "excluding", processedVulnerability.Fixed) + isImpacted, err := vulnfetcher.IsImpactedByVulnerability(name, version, &processedVulnerability) + if err != nil { + return nil, err + } + if !isImpacted { + continue + } vulnerabilities = append(vulnerabilities, processedVulnerability) } diff --git a/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go b/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go new file mode 100644 index 0000000..8f7b2a2 --- /dev/null +++ b/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go @@ -0,0 +1,7 @@ +package ossvulnfetcher + +import "testing" + +func TestName(t *testing.T) { + t.Error("Not implemented") +} diff --git a/vulnfetcher/vulnfetcher.go b/vulnfetcher/vulnfetcher.go index 4c951ab..090a334 100644 --- a/vulnfetcher/vulnfetcher.go +++ b/vulnfetcher/vulnfetcher.go @@ -1,11 +1,19 @@ package vulnfetcher +import ( + "fmt" + "gammaray/versionformatter" + + "github.com/Masterminds/semver" +) + // Vulnerability describes a vulnerability type Vulnerability struct { CVE string Title string Description string Versions string + Fixed string References string } @@ -14,3 +22,70 @@ type VulnFetcher interface { Fetch() error Test(component string, version string) ([]Vulnerability, error) } + +// var invalidPreRelease = regexp.MustCompile("\\.([a-zA-Z].+?)(\\s|$)") +// var majorMinorPatchPreReleaseBuild = regexp.MustCompile("\\d+\\.\\d+\\.\\d+\\-\\w+\\+[^ <>=~]") +// var majorMinorNoPatch = regexp.MustCompile("\\.([a-zA-Z].+?)(\\s|$)") + +func tryToMakeValidVersion(version string) (string, error) { + // return invalidPreRelease.ReplaceAllString(version, "-$1") + return versionformatter.Format(version) +} + +// IsImpactedByVulnerability checks if a given module with a given version is impacted by a vulnerability +func IsImpactedByVulnerability(module string, moduleVersion string, vulnerability *Vulnerability) (bool, error) { + version, err := tryToMakeValidVersion(moduleVersion) + if err != nil { + fmt.Printf("Error parsing module version '%s'(%s): %q", module, moduleVersion, err) + return true, err + } + fmt.Println("🐭 version", moduleVersion, "👉", version) + ver, err := semver.NewVersion(version) + if err != nil { + fmt.Printf("Error parsing Package version of module '%s'(%s): %q", module, moduleVersion, err) + return true, err + } + + vulnVersions, err := tryToMakeValidVersion(vulnerability.Versions) + fmt.Println("🐭 Vulnerable Versions", vulnerability.Versions, "👉", vulnVersions) + if err != nil { + fmt.Printf("Error parsing Vulnerability version range of module '%s'(%s): %q", module, moduleVersion, err) + return true, err + } + rangeVuln, err := semver.NewConstraint(vulnVersions) + if err != nil { + fmt.Printf("Error parsing formatted Vulnerability version range of module '%s'(%s): %q", module, moduleVersion, err) + return true, err + } + + var isVuln = rangeVuln.Check(ver) + if !isVuln { + fmt.Println("🐭", module, "(", moduleVersion, ") is not subject to a known vulnerability ✅") + return false, err + } + + if vulnerability.Fixed == "" { + fmt.Println("🐭", module, "(", moduleVersion, ") is subject to a known vulnerability! ❌") + return true, nil + } + fixedVersions, err := tryToMakeValidVersion(vulnerability.Fixed) + fmt.Println("🐭 fixedVersions", vulnerability.Fixed, "👉", fixedVersions) + if err != nil { + fmt.Printf("Error parsing Fixed version range of module '%s'(%s): %q", module, moduleVersion, err) + return false, err + } + rangeFixed, err := semver.NewConstraint(fixedVersions) + if err != nil { + fmt.Printf("Error parsing formatted Fixed version range of module '%s'(%s): %q", module, moduleVersion, err) + return false, err + } + + var isFixed = rangeFixed.Check(ver) + if isFixed { + fmt.Println("🐭", module, "(", moduleVersion, ") is not subject to a known vulnerability ✅ (part of the fixed versions)") + } else { + fmt.Println("🐭", module, "(", moduleVersion, ") is subject to a known vulnerability! ❌ (not part of the fixed versions)") + } + + return !isFixed, nil +} From c77c4e42b662eedc19ce8dbfa66882236ab70a0f Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 14:04:52 +0200 Subject: [PATCH 2/7] build: better Makefile rules for compilation, code generation, test, linting --- Makefile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Makefile b/Makefile index c22e013..09af31c 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,22 @@ +all: install build + +formatter: + pigeon -o versionformatter/versionformatter.go versionformatter/versionformatter.peg + +build: formatter + go build -v -race + install: go get github.com/golang/dep dep ensure + +dev-install: install + go get -u github.com/mgechev/revive + go get -u github.com/mna/pigeon + +test: + go test -v -race ./... + go vet ./... + +lint: + revive -formatter stylish pathrunner/... vulnfetcher/... From 3e4f19ac4517baf873ccd98fea1f69875422037c Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 15:00:46 +0200 Subject: [PATCH 3/7] chore: cleanup and more stuff in logs(stderr) vs stdout --- vulnfetcher/nodeswg/nodeswg.go | 4 ++-- vulnfetcher/ossvulnfetcher/osindexfetcher.go | 4 ++-- vulnfetcher/vulnfetcher.go | 19 ++++++++----------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/vulnfetcher/nodeswg/nodeswg.go b/vulnfetcher/nodeswg/nodeswg.go index bcbd9f4..96f7817 100644 --- a/vulnfetcher/nodeswg/nodeswg.go +++ b/vulnfetcher/nodeswg/nodeswg.go @@ -2,9 +2,9 @@ package nodeswg import ( "encoding/json" - "fmt" "gammaray/vulnfetcher" "io" + "log" "net/http" "os" "path" @@ -103,7 +103,7 @@ func (n *Fetcher) Test(module string, version string) ([]vulnfetcher.Vulnerabili Fixed: vulnerability.FixedVersions, References: vulnerability.References, } - fmt.Println("✨ Node SWG Vulnerability check for ", module, "(", version, ") in '", vuln.Versions, "' excluding '", vuln.Fixed, "'") + log.Println("✨ Node SWG Vulnerability check for ", module, "(", version, ") in '", vuln.Versions, "' excluding '", vuln.Fixed, "'") isImpacted, err := vulnfetcher.IsImpactedByVulnerability(module, version, &vuln) if err != nil { return nil, err diff --git a/vulnfetcher/ossvulnfetcher/osindexfetcher.go b/vulnfetcher/ossvulnfetcher/osindexfetcher.go index 2bec4b1..f3757be 100644 --- a/vulnfetcher/ossvulnfetcher/osindexfetcher.go +++ b/vulnfetcher/ossvulnfetcher/osindexfetcher.go @@ -4,9 +4,9 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "gammaray/vulnfetcher" "io/ioutil" + "log" "net/http" "strings" ) @@ -86,7 +86,7 @@ func (n *OSSIndexFetcher) Test(name string, version string) ([]vulnfetcher.Vulne Fixed: strings.Join(vulnerability.FixedVersions, " "), References: strings.Join(vulnerability.References, " "), } - fmt.Println("✨ OSS Vulnerability check for ", name, "(", version, ") in ", processedVulnerability.Versions, "excluding", processedVulnerability.Fixed) + log.Println("✨ OSS Vulnerability check for ", name, "(", version, ") in ", processedVulnerability.Versions, "excluding", processedVulnerability.Fixed) isImpacted, err := vulnfetcher.IsImpactedByVulnerability(name, version, &processedVulnerability) if err != nil { return nil, err diff --git a/vulnfetcher/vulnfetcher.go b/vulnfetcher/vulnfetcher.go index 090a334..f34327c 100644 --- a/vulnfetcher/vulnfetcher.go +++ b/vulnfetcher/vulnfetcher.go @@ -3,6 +3,7 @@ package vulnfetcher import ( "fmt" "gammaray/versionformatter" + "log" "github.com/Masterminds/semver" ) @@ -23,10 +24,6 @@ type VulnFetcher interface { Test(component string, version string) ([]Vulnerability, error) } -// var invalidPreRelease = regexp.MustCompile("\\.([a-zA-Z].+?)(\\s|$)") -// var majorMinorPatchPreReleaseBuild = regexp.MustCompile("\\d+\\.\\d+\\.\\d+\\-\\w+\\+[^ <>=~]") -// var majorMinorNoPatch = regexp.MustCompile("\\.([a-zA-Z].+?)(\\s|$)") - func tryToMakeValidVersion(version string) (string, error) { // return invalidPreRelease.ReplaceAllString(version, "-$1") return versionformatter.Format(version) @@ -39,7 +36,7 @@ func IsImpactedByVulnerability(module string, moduleVersion string, vulnerabilit fmt.Printf("Error parsing module version '%s'(%s): %q", module, moduleVersion, err) return true, err } - fmt.Println("🐭 version", moduleVersion, "👉", version) + log.Println("version", moduleVersion, "👉", version) ver, err := semver.NewVersion(version) if err != nil { fmt.Printf("Error parsing Package version of module '%s'(%s): %q", module, moduleVersion, err) @@ -47,7 +44,7 @@ func IsImpactedByVulnerability(module string, moduleVersion string, vulnerabilit } vulnVersions, err := tryToMakeValidVersion(vulnerability.Versions) - fmt.Println("🐭 Vulnerable Versions", vulnerability.Versions, "👉", vulnVersions) + log.Println("Vulnerable Versions", vulnerability.Versions, "👉", vulnVersions) if err != nil { fmt.Printf("Error parsing Vulnerability version range of module '%s'(%s): %q", module, moduleVersion, err) return true, err @@ -60,16 +57,16 @@ func IsImpactedByVulnerability(module string, moduleVersion string, vulnerabilit var isVuln = rangeVuln.Check(ver) if !isVuln { - fmt.Println("🐭", module, "(", moduleVersion, ") is not subject to a known vulnerability ✅") + log.Println(module, "(", moduleVersion, ") is not subject to a known vulnerability ✅") return false, err } if vulnerability.Fixed == "" { - fmt.Println("🐭", module, "(", moduleVersion, ") is subject to a known vulnerability! ❌") + log.Println(module, "(", moduleVersion, ") is subject to a known vulnerability! ❌") return true, nil } fixedVersions, err := tryToMakeValidVersion(vulnerability.Fixed) - fmt.Println("🐭 fixedVersions", vulnerability.Fixed, "👉", fixedVersions) + log.Println("fixedVersions", vulnerability.Fixed, "👉", fixedVersions) if err != nil { fmt.Printf("Error parsing Fixed version range of module '%s'(%s): %q", module, moduleVersion, err) return false, err @@ -82,9 +79,9 @@ func IsImpactedByVulnerability(module string, moduleVersion string, vulnerabilit var isFixed = rangeFixed.Check(ver) if isFixed { - fmt.Println("🐭", module, "(", moduleVersion, ") is not subject to a known vulnerability ✅ (part of the fixed versions)") + log.Println(module, "(", moduleVersion, ") is not subject to a known vulnerability ✅ (part of the fixed versions)") } else { - fmt.Println("🐭", module, "(", moduleVersion, ") is subject to a known vulnerability! ❌ (not part of the fixed versions)") + log.Println(module, "(", moduleVersion, ") is subject to a known vulnerability! ❌ (not part of the fixed versions)") } return !isFixed, nil From 928520b51ea443778df3039ed0db440d2ee6f8fb Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 16:17:00 +0200 Subject: [PATCH 4/7] fix: make empty test osindexfeftcher not crash --- vulnfetcher/ossvulnfetcher/osindexfetcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go b/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go index 8f7b2a2..635f3df 100644 --- a/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go +++ b/vulnfetcher/ossvulnfetcher/osindexfetcher_test.go @@ -3,5 +3,5 @@ package ossvulnfetcher import "testing" func TestName(t *testing.T) { - t.Error("Not implemented") + // t.Error("Not implemented") } From f5770b3998eaf79b39e3521901d5112c63f225e3 Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 16:17:37 +0200 Subject: [PATCH 5/7] build(make): add ci scripts --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 09af31c..a2bf1e4 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,15 @@ dev-install: install go get -u github.com/mgechev/revive go get -u github.com/mna/pigeon +ci-install: dev-install + go get -u github.com/mattn/goveralls + test: go test -v -race ./... go vet ./... +ci-test: test + goveralls + lint: revive -formatter stylish pathrunner/... vulnfetcher/... From 2f4f68d242c1b5e35a15e05d8c5ae500e3b49f6f Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Mon, 9 Jul 2018 16:18:00 +0200 Subject: [PATCH 6/7] build(ci): use absolute packages for Travis --- .travis.yml | 13 +++++++------ main.go | 7 ++++--- vulnfetcher/nodeswg/nodeswg.go | 2 +- vulnfetcher/ossvulnfetcher/osindexfetcher.go | 3 ++- vulnfetcher/vulnfetcher.go | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1e36c0..ed65328 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,18 +2,19 @@ language: go go: - 1.9 +go_import_path: github.com/nearform/gammaray + before_install: - - curl -L -s https://github.com/golang/dep/releases/download/v0.3.1/dep-linux-amd64 -o $GOPATH/bin/dep - - chmod +x $GOPATH/bin/dep - - go get github.com/mattn/goveralls + - export PATH="$PATH:$GOPATH/bin" + - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh install: - - make install + - make ci-install + - make build notifications: email: false script: - - go test -v -race ./... - - go vet ./... + - make test - $GOPATH/bin/goveralls -service=travis-ci diff --git a/main.go b/main.go index 13a43f4..af7f2dd 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,11 @@ package main import ( "fmt" - "gammaray/pathrunner" - "gammaray/vulnfetcher/nodeswg" - "gammaray/vulnfetcher/ossvulnfetcher" "os" + + "github.com/nearform/gammaray/pathrunner" + "github.com/nearform/gammaray/vulnfetcher/nodeswg" + "github.com/nearform/gammaray/vulnfetcher/ossvulnfetcher" ) // OSSIndexURL URL for OSSIndex. Is not a hardcoded value to facilitate testing. diff --git a/vulnfetcher/nodeswg/nodeswg.go b/vulnfetcher/nodeswg/nodeswg.go index 96f7817..352eda2 100644 --- a/vulnfetcher/nodeswg/nodeswg.go +++ b/vulnfetcher/nodeswg/nodeswg.go @@ -2,7 +2,6 @@ package nodeswg import ( "encoding/json" - "gammaray/vulnfetcher" "io" "log" "net/http" @@ -12,6 +11,7 @@ import ( "strings" "github.com/mholt/archiver" + "github.com/nearform/gammaray/vulnfetcher" ) // Fetcher fetches node community vulnerabilities diff --git a/vulnfetcher/ossvulnfetcher/osindexfetcher.go b/vulnfetcher/ossvulnfetcher/osindexfetcher.go index f3757be..595b15c 100644 --- a/vulnfetcher/ossvulnfetcher/osindexfetcher.go +++ b/vulnfetcher/ossvulnfetcher/osindexfetcher.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "errors" - "gammaray/vulnfetcher" "io/ioutil" "log" "net/http" "strings" + + "github.com/nearform/gammaray/vulnfetcher" ) // OSSPackageRequest request for a package diff --git a/vulnfetcher/vulnfetcher.go b/vulnfetcher/vulnfetcher.go index f34327c..93ebe7a 100644 --- a/vulnfetcher/vulnfetcher.go +++ b/vulnfetcher/vulnfetcher.go @@ -2,10 +2,10 @@ package vulnfetcher import ( "fmt" - "gammaray/versionformatter" "log" "github.com/Masterminds/semver" + "github.com/nearform/gammaray/versionformatter" ) // Vulnerability describes a vulnerability From 30e5cbb04667c78dc49bc2e8df7c1ce4bc92b702 Mon Sep 17 00:00:00 2001 From: Florian Traverse Date: Tue, 24 Jul 2018 09:20:32 +0200 Subject: [PATCH 7/7] docs(ReadMe): update to reflect current usage --- README.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index aa5588b..7f4aab8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Gamma Ray -[![Go Report Card](https://goreportcard.com/badge/github.com/dgonzalez/gammaray)](https://goreportcard.com/report/github.com/dgonzalez/gammaray) -![Travis](https://travis-ci.org/dgonzalez/gammaray.svg?branch=master) -[![Coverage Status](https://coveralls.io/repos/github/dgonzalez/gammaray/badge.svg?branch=travis)](https://coveralls.io/github/dgonzalez/gammaray?branch=travis) +[![Go Report Card](https://goreportcard.com/badge/github.com/nearform/gammaray)](https://goreportcard.com/report/github.com/nearform/gammaray) +![Travis](https://travis-ci.org/nearform/gammaray.svg?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/nearform/gammaray/badge.svg?branch=master)](https://coveralls.io/github/nearform/gammaray?branch=master) -Gamm Ray is a software that helps developers to look for vulnerabilities on their Node.js +Gammaray is a software that helps developers to look for vulnerabilities on their Node.js applications. Its pluggable infrastructure makes very easy to write an integration with several vulnerabilities databases. @@ -11,32 +11,41 @@ several vulnerabilities databases. In order to get it just run: -``` -go get github.com/dgonzalez/gammaray +```console +$> go get github.com/nearform/gammaray ``` Once it is finished, you should have the `gammaray` binary in your `GOPATH/bin` folder. ## Build it -We use `dep` to manage the dependencies for `gammaray`. In order to build it, run: - -``` -dep ensure -go build +```console +$> make ``` ## Usage Gammaray comes as a single binary so you only need to run it passing your project as argument: -``` -gammaray +```console +$> gammaray ``` And that is all, all the vulnerabilities that affect your packages will be displayed. ## Contributing -Are you a developer and want to contribute? Please be my guest. +### As a developer + +Clone the repository, then start hacking, PRs are welcome ! + +```console +$> mkdir -p $GOPATH/src/github.com/nearform/ +$> cd $GOPATH/src/github.com/nearform/ +$> git clone https://github.com/nearform/gammaray.git +$> cd gammaray +$> make dev-install +``` + +### As security provider -Are you a security provider who wants to be integrated? Contact me [here](https://www.linkedin.com/in/david-gonzalez-microservices/) +You want to be integrated? Contact me [here](https://www.linkedin.com/in/david-gonzalez-microservices/)