providers/snyk/schema/versionrange.go (103 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package schema import ( "fmt" "regexp" "strings" "unicode" ) type versionRange struct { minVerIncl string minVerExcl string maxVerIncl string maxVerExcl string } func parseVersionRange(rangeStr string) ([]versionRange, error) { if looksLikeParenRange(rangeStr) { return parseParenRanges(rangeStr) } return parseCmpRanges(rangeStr) } // looksLikeParenRange tries to detect the format version range(s) are provided. // The interval can be specified either in mathematical notation, where square brackets // and parentheses denote open and closed intervals respectively and any number can be omitted if there is // no corresponding limit; e.g. [0, 5), [,3], (8,); any of comma separated list such intervals can be applied; // or with help of comparison operators (<, >, <=, >=); edge cases separated by space, separate inervals are // combined with logical or || operator. // It returns true if the range looks like math notation, false otherwise func looksLikeParenRange(s string) bool { if s == "" { return false } s = strings.TrimSpace(s) return (s[0] == '[' || s[0] == '(') && (s[len(s)-1] == ']' || s[len(s)-1] == ')') } // parseParenRanges parses a sequence of intervals in mathematical notation into a slice of versionRange structs. // Single boundary in open-ended interval means equality check. func parseParenRanges(s string) (vr []versionRange, err error) { for len(s) > 0 { var r versionRange left := strings.IndexAny(s, "([") right := strings.IndexAny(s, ")]") if left == -1 || right == -1 || right < left { return nil, fmt.Errorf("invalid range %q", s) } boundaries := strings.Split(s[left+1:right], ",") if len(boundaries) == 1 { r.minVerIncl = strings.TrimSpace(boundaries[0]) r.maxVerIncl = r.minVerIncl vr = append(vr, r) s = s[right+1:] continue } if len(boundaries) != 2 { return nil, fmt.Errorf("invalid range %q", s) } if s[left] == '(' { r.minVerExcl = strings.TrimSpace(boundaries[0]) } else { r.minVerIncl = strings.TrimSpace(boundaries[0]) } if s[right] == ')' { r.maxVerExcl = strings.TrimSpace(boundaries[1]) } else { r.maxVerIncl = strings.TrimSpace(boundaries[1]) } vr = append(vr, r) // skip trailing spaces suffix := s[right+1:] for _, r := range suffix { if !unicode.IsSpace(r) { break } right++ } s = s[right+1:] } return vr, nil } // parseCmpRange parses a sequence of intervals described as comparison operators (<=, <, =, >, >=); // ranges can be combined with || operators for boolean OR logic. func parseCmpRanges(s string) (vr []versionRange, err error) { re := regexp.MustCompile(`([<>](:?=)?)\s*(\S+)`) ss := strings.Split(s, "||") for _, rs := range ss { var r versionRange if rs == "" { continue } rs = strings.TrimSpace(rs) // first, check if it is just an equality check if rs[0] == '=' { r.maxVerIncl = strings.TrimSpace(rs[1:]) r.minVerIncl = r.maxVerIncl vr = append(vr, r) continue } // then process the range matches := re.FindAllString(strings.TrimSpace(rs), -1) for _, match := range matches { switch match[0] { case '<': if match[1] == '=' { r.maxVerIncl = strings.TrimSpace(match[2:]) } else { r.maxVerExcl = strings.TrimSpace(match[1:]) } case '>': if match[1] == '=' { r.minVerIncl = strings.TrimSpace(match[2:]) } else { r.minVerExcl = strings.TrimSpace(match[1:]) } } } vr = append(vr, r) } return vr, nil }