pax_global_header00006660000000000000000000000064147441200650014515gustar00rootroot0000000000000052 comment=43e6734cfea4f27fe9d692b89e2ba9efa81a963e aquasecurity-go-pep440-version-43e6734/000077500000000000000000000000001474412006500176155ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/.github/000077500000000000000000000000001474412006500211555ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/.github/dependabot.yml000066400000000000000000000004251474412006500240060ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 groups: all-dependencies: patterns: - "*" commit-message: prefix: "chore" include: "scope" aquasecurity-go-pep440-version-43e6734/.github/workflows/000077500000000000000000000000001474412006500232125ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/.github/workflows/test.yml000066400000000000000000000007321474412006500247160ustar00rootroot00000000000000on: [push, pull_request] name: Test jobs: test: strategy: matrix: go-version: [oldstable, stable] runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.63 - name: Test run: go test ./... aquasecurity-go-pep440-version-43e6734/.gitignore000066400000000000000000000004151474412006500216050ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ aquasecurity-go-pep440-version-43e6734/LICENSE000066400000000000000000000261351474412006500206310ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. aquasecurity-go-pep440-version-43e6734/README.md000066400000000000000000000026661474412006500211060ustar00rootroot00000000000000# go-pep440-version ![Test](https://github.com/aquasecurity/go-pep440-version/workflows/Test/badge.svg?branch=main) [![Go Report Card](https://goreportcard.com/badge/github.com/aquasecurity/go-pep440-version)](https://goreportcard.com/report/github.com/aquasecurity/go-pep440-version) ![GitHub](https://img.shields.io/github/license/aquasecurity/go-pep440-version) A golang library for parsing PEP 440 compliant Python versions go-pep440-version is a library for parsing versions of Python software distributions and version specifiers, and verifying versions against a set of specifiers. Versions used with go-pep440-version must follow [PEP 440](https://www.python.org/dev/peps/pep-0440/). For more details, see [pypa/packaging](https://github.com/pypa/packaging) ## Usage ### Version Parsing and Comparison See [example](./examples/comparison/main.go) ``` v1, _ := version.Parse("1.2.a") v2, _ := version.Parse("1.2") // Comparison example. There is also GreaterThan, Equal, and just // a simple Compare that returns an int allowing easy >=, <=, etc. if v1.LessThan(v2) { fmt.Printf("%s is less than %s", v1, v2) } ``` ### Version Constraints See [example](./examples/constraint/main.go) ``` v, _ := version.Parse("2.1") c, _ := version.NewSpecifiers(">= 1.0, < 1.4 || > 2.0") if c.Check(v) { fmt.Printf("%s satisfies specifiers '%s'", v, c) } ``` ## Status - [x] `>` - [x] `>=` - [x] `<` - [x] `<=` - [x] `==` - [x] `!=` - [x] `~=` - [ ] `===`aquasecurity-go-pep440-version-43e6734/examples/000077500000000000000000000000001474412006500214335ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/examples/comparison/000077500000000000000000000000001474412006500236055ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/examples/comparison/main.go000066400000000000000000000007011474412006500250560ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/aquasecurity/go-pep440-version" ) func main() { v1, err := version.Parse("1.2a1") if err != nil { log.Fatal(err) } v2, err := version.Parse("1.2") if err != nil { log.Fatal(err) } // Comparison example. There is also GreaterThan, Equal, and just // a simple Compare that returns an int allowing easy >=, <=, etc. if v1.LessThan(v2) { fmt.Printf("%s is less than %s", v1, v2) } } aquasecurity-go-pep440-version-43e6734/examples/specifier/000077500000000000000000000000001474412006500234045ustar00rootroot00000000000000aquasecurity-go-pep440-version-43e6734/examples/specifier/main.go000066400000000000000000000005241474412006500246600ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/aquasecurity/go-pep440-version" ) func main() { v, err := version.Parse("2.1") if err != nil { log.Fatal(err) } c, err := version.NewSpecifiers(">= 1.0, < 1.4 || > 2.0") if err != nil { log.Fatal(err) } if c.Check(v) { fmt.Printf("%s satisfies specifiers '%s'", v, c) } } aquasecurity-go-pep440-version-43e6734/go.mod000066400000000000000000000005721474412006500207270ustar00rootroot00000000000000module github.com/aquasecurity/go-pep440-version go 1.22.11 toolchain go1.23.4 require ( github.com/aquasecurity/go-version v0.0.1 github.com/stretchr/testify v1.10.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) aquasecurity-go-pep440-version-43e6734/go.sum000066400000000000000000000024051474412006500207510ustar00rootroot00000000000000github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= aquasecurity-go-pep440-version-43e6734/specifier.go000066400000000000000000000247301474412006500221230ustar00rootroot00000000000000package version import ( "fmt" "reflect" "regexp" "strconv" "strings" "golang.org/x/xerrors" ) var ( specifierOperators = map[string]operatorFunc{ "": specifierEqual, // not defined in PEP 440 "=": specifierEqual, // not defined in PEP 440 "==": specifierEqual, "!=": specifierNotEqual, ">": specifierGreaterThan, "<": specifierLessThan, ">=": specifierGreaterThanEqual, "<=": specifierLessThanEqual, "~=": specifierCompatible, "===": specifierArbitrary, } specifierRegexp *regexp.Regexp validConstraintRegexp *regexp.Regexp prefixRegexp *regexp.Regexp ) func init() { ops := make([]string, 0, len(specifierOperators)) for k := range specifierOperators { ops = append(ops, regexp.QuoteMeta(k)) } specifierRegexp = regexp.MustCompile(fmt.Sprintf( `(?i)(?P(%s))\s*(?P%s(\.\*)?)`, strings.Join(ops, "|"), regex)) validConstraintRegexp = regexp.MustCompile(fmt.Sprintf( `^\s*(\s*(%s)\s*(%s(\.\*)?)\s*\,?)*\s*$`, strings.Join(ops, "|"), regex)) prefixRegexp = regexp.MustCompile(`^([0-9]+)((?:a|b|c|rc)[0-9]+)$`) } type operatorFunc func(v Version, c string) bool type Specifiers struct { specifiers [][]specifier conf conf } type specifier struct { version string operator operatorFunc original string } // NewSpecifiers parses a given specifier and returns a new instance of Specifiers func NewSpecifiers(v string, opts ...SpecifierOption) (Specifiers, error) { c := new(conf) // Apply options for _, o := range opts { o.apply(c) } var sss [][]specifier for _, vv := range strings.Split(v, "||") { if strings.TrimSpace(vv) == "*" { vv = ">=0.0.0" } // Validate the segment if !validConstraintRegexp.MatchString(vv) { return Specifiers{}, xerrors.Errorf("improper constraint: %s", vv) } ss := specifierRegexp.FindAllString(vv, -1) if ss == nil { ss = append(ss, strings.TrimSpace(vv)) } var specs []specifier for _, single := range ss { s, err := newSpecifier(single) if err != nil { return Specifiers{}, err } specs = append(specs, s) } sss = append(sss, specs) } return Specifiers{ specifiers: sss, conf: *c, }, nil } func newSpecifier(s string) (specifier, error) { m := specifierRegexp.FindStringSubmatch(s) if m == nil { return specifier{}, xerrors.Errorf("improper specifier: %s", s) } operator := m[specifierRegexp.SubexpIndex("operator")] version := m[specifierRegexp.SubexpIndex("version")] if operator != "===" { if err := validate(operator, version); err != nil { return specifier{}, err } } return specifier{ version: version, operator: specifierOperators[operator], original: s, }, nil } func validate(operator, version string) error { hasWildcard := false if strings.HasSuffix(version, ".*") { hasWildcard = true version = strings.TrimSuffix(version, ".*") } v, err := Parse(version) if err != nil { return xerrors.Errorf("version parse error (%s): %w", v, err) } switch operator { case "", "=", "==", "!=": if hasWildcard && (!v.dev.isNull() || v.local != "") { return xerrors.New("the (non)equality operators don't allow to use a wild card and a dev" + " or local version together") } case "~=": if hasWildcard { return xerrors.New("a wild card is not allowed") } else if len(v.release) < 2 { return xerrors.New("the compatible operator requires at least two digits in the release segment") } else if v.local != "" { return xerrors.New("local versions cannot be specified") } default: if hasWildcard { return xerrors.New("a wild card is not allowed") } else if v.local != "" { return xerrors.New("local versions cannot be specified") } } return nil } // Check tests if a version satisfies all the specifiers. func (ss Specifiers) Check(v Version) bool { if ss.conf.includePreRelease { v.preReleaseIncluded = true } for _, s := range ss.specifiers { if andCheck(v, s) { return true } } return false } func (s specifier) check(v Version) bool { return s.operator(v, s.version) } func (s specifier) String() string { return s.original } // String returns the string format of the specifiers func (ss Specifiers) String() string { var ssStr []string for _, orS := range ss.specifiers { var sstr []string for _, andS := range orS { sstr = append(sstr, andS.String()) } ssStr = append(ssStr, strings.Join(sstr, ",")) } return strings.Join(ssStr, "||") } func andCheck(v Version, specifiers []specifier) bool { for _, c := range specifiers { if !c.check(v) { return false } } return true } func versionSplit(version string) []string { var result []string for _, v := range strings.Split(version, ".") { m := prefixRegexp.FindStringSubmatch(v) if m != nil { result = append(result, m[1:]...) } else { result = append(result, v) } } return result } func isDigist(s string) bool { if _, err := strconv.Atoi(s); err == nil { return true } return false } func padVersion(left, right []string) ([]string, []string) { var leftRelease, rightRelease []string for _, l := range left { if isDigist(l) { leftRelease = append(leftRelease, l) } } for _, r := range right { if isDigist(r) { rightRelease = append(rightRelease, r) } } // Get the rest of our versions leftRest := left[len(leftRelease):] rightRest := left[len(rightRelease):] for i := 0; i < len(leftRelease)-len(rightRelease); i++ { rightRelease = append(rightRelease, "0") } for i := 0; i < len(rightRelease)-len(leftRelease); i++ { leftRelease = append(leftRelease, "0") } return append(leftRelease, leftRest...), append(rightRelease, rightRest...) } //------------------------------------------------------------------- // Specifier functions //------------------------------------------------------------------- func specifierCompatible(prospective Version, spec string) bool { // Compatible releases have an equivalent combination of >= and ==. That is that ~=2.2 is equivalent to >=2.2,==2.*. // This allows us to implement this in terms of the other specifiers instead of implementing it ourselves. // The only thing we need to do is construct the other specifiers. var prefixElements []string for _, s := range versionSplit(spec) { if strings.HasPrefix(s, "post") || strings.HasPrefix(s, "dev") { break } prefixElements = append(prefixElements, s) } // We want everything but the last item in the version, but we want to ignore post and dev releases and // we want to treat the pre-release as it's own separate segment. prefix := strings.Join(prefixElements[:len(prefixElements)-1], ".") // Add the prefix notation to the end of our string prefix += ".*" return specifierGreaterThanEqual(prospective, spec) && specifierEqual(prospective, prefix) } func specifierEqual(prospective Version, spec string) bool { // https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/specifiers.py#L476 // We need special logic to handle prefix matching if strings.HasSuffix(spec, ".*") { // In the case of prefix matching we want to ignore local segment. prospective = MustParse(prospective.Public()) // Split the spec out by dots, and pretend that there is an implicit // dot in between a release segment and a pre-release segment. splitSpec := versionSplit(strings.TrimSuffix(spec, ".*")) // Split the prospective version out by dots, and pretend that there is an implicit dot // in between a release segment and a pre-release segment. splitProspective := versionSplit(prospective.String()) // Shorten the prospective version to be the same length as the spec // so that we can determine if the specifier is a prefix of the // prospective version or not. if len(splitProspective) > len(splitSpec) { splitProspective = splitProspective[:len(splitSpec)] } paddedSpec, paddedProspective := padVersion(splitSpec, splitProspective) return reflect.DeepEqual(paddedSpec, paddedProspective) } specVersion := MustParse(spec) if specVersion.local == "" { prospective = MustParse(prospective.Public()) } return specVersion.Equal(prospective) } func specifierNotEqual(prospective Version, spec string) bool { return !specifierEqual(prospective, spec) } func specifierLessThan(prospective Version, spec string) bool { // Convert our spec to a Version instance, since we'll want to work with it as a version. s := MustParse(spec) // Check to see if the prospective version is less than the spec version. // If it's not we can short circuit and just return False now instead of doing extra unneeded work. if !prospective.LessThan(s) { return false } // This special case is here so that, unless the specifier itself includes is a pre-release version, // that we do not accept pre-release versions for the version mentioned in the specifier // (e.g. <3.1 should not match 3.1.dev0, but should match 3.0.dev0). if !s.IsPreRelease() && prospective.IsPreRelease() { if MustParse(prospective.BaseVersion()).Equal(MustParse(s.BaseVersion())) { return false } } return true } func specifierGreaterThan(prospective Version, spec string) bool { // Convert our spec to a Version instance, since we'll want to work with it as a version. s := MustParse(spec) // Check to see if the prospective version is greater than the spec version. // If it's not we can short circuit and just return False now instead of doing extra unneeded work. if !prospective.GreaterThan(s) { return false } // This special case is here so that, unless the specifier itself includes is a post-release version, // that we do not accept post-release versions for the version mentioned in the specifier // (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). if !s.IsPostRelease() && prospective.IsPostRelease() { if MustParse(prospective.BaseVersion()).Equal(MustParse(s.BaseVersion())) { return false } } // Ensure that we do not allow a local version of the version mentioned // in the specifier, which is technically greater than, to match. if prospective.local != "" { if MustParse(prospective.BaseVersion()).Equal(MustParse(s.BaseVersion())) { return false } } return true } func specifierArbitrary(prospective Version, spec string) bool { return strings.EqualFold(prospective.String(), spec) } func specifierLessThanEqual(prospective Version, spec string) bool { p := MustParse(prospective.Public()) s := MustParse(spec) return p.LessThanOrEqual(s) } func specifierGreaterThanEqual(prospective Version, spec string) bool { p := MustParse(prospective.Public()) s := MustParse(spec) return p.GreaterThanOrEqual(s) } aquasecurity-go-pep440-version-43e6734/specifier_option.go000066400000000000000000000003251474412006500235050ustar00rootroot00000000000000package version type conf struct { includePreRelease bool } type SpecifierOption interface { apply(*conf) } type WithPreRelease bool func (o WithPreRelease) apply(c *conf) { c.includePreRelease = bool(o) } aquasecurity-go-pep440-version-43e6734/specifier_test.go000066400000000000000000000224751474412006500231660ustar00rootroot00000000000000package version import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewConstraints(t *testing.T) { tests := []struct { constraint string wantErr bool }{ // https://github.com/pypa/packaging/blob/28d2fa0742747cda4bc4530b2a5bc919b7382039/tests/test_specifiers.py#L31-L42 {"~=2.0", false}, {"==2.1.*", false}, {"==2.1.0.3", false}, {"!=2.2.*", false}, {"!=2.2.0.5", false}, {"<=5", false}, {">=7.9a1", false}, {"<1.0.dev1", false}, {">2.0.post1", false}, // TODO // {"===lolwat", false}, // https://github.com/pypa/packaging/blob/28d2fa0742747cda4bc4530b2a5bc919b7382039/tests/test_specifiers.py#L50-L86 // Operator-less specifier {"2.0", false}, // go-pep-440-version permits this case // Invalid operator {"=>2.0", true}, //Version-less specifier {"==", true}, // Local segment on operators which don't support them {"~=1.0+5", true}, {">=1.0+deadbeef", true}, {"<=1.0+abc123", true}, {">1.0+watwat", true}, {"<1.0+1.0", true}, // Prefix matching on operators which don't support them {"~=1.0.*", true}, {">=1.0.*", true}, {"<=1.0.*", true}, {">1.0.*", true}, {"<1.0.*", true}, // Combination of local and prefix matching on operators which do // support one or the other {"==1.0.*+5", true}, {"!=1.0.*+deadbeef", true}, // Prefix matching cannot be used inside of a local version {"==1.0+5.*", true}, {"!=1.0+deadbeef.*", true}, // Prefix matching must appear at the end {"==1.0.*.5", true}, // Compatible operator requires 2 digits in the release operator {"~=1", true}, // Cannot use a prefix matching after a .devN version {"==1.0.dev1.*", true}, {"!=1.0.dev1.*", true}, } for _, tt := range tests { t.Run(tt.constraint, func(t *testing.T) { _, err := NewSpecifiers(tt.constraint) if tt.wantErr { assert.NotNil(t, err) } else { assert.NoError(t, err) } }) } } func TestVersion_Check(t *testing.T) { tests := []struct { version string spec string want bool }{ // Test the equality operation {"2.0", "==2", true}, {"2.0", "==2.0", true}, {"2.0", "==2.0.0", true}, {"2.0+deadbeef", "==2", true}, {"2.0+deadbeef", "==2.0", true}, {"2.0+deadbeef", "==2.0.0", true}, {"2.0+deadbeef", "==2+deadbeef", true}, {"2.0+deadbeef", "==2.0+deadbeef", true}, {"2.0+deadbeef", "==2.0.0+deadbeef", true}, {"2.0+deadbeef.0", "==2.0.0+deadbeef.00", true}, // Test the equality operation with a prefix {"2.dev1", "==2.*", true}, {"2a1", "==2.*", true}, {"2a1.post1", "==2.*", true}, {"2b1", "==2.*", true}, {"2b1.dev1", "==2.*", true}, {"2c1", "==2.*", true}, {"2c1.post1.dev1", "==2.*", true}, {"2rc1", "==2.*", true}, {"2", "==2.*", true}, {"2.0", "==2.*", true}, {"2.0.0", "==2.*", true}, {"2.0.post1", "==2.0.post1.*", true}, {"2.0.post1.dev1", "==2.0.post1.*", true}, {"2.1+local.version", "==2.1.*", true}, // Test the in-equality operation {"2.1", "!=2", true}, {"2.1", "!=2.0", true}, {"2.0.1", "!=2", true}, {"2.0.1", "!=2.0", true}, {"2.0.1", "!=2.0.0", true}, {"2.0", "!=2.0+deadbeef", true}, // Test the in-equality operation with a prefix {"2.0", "!=3.*", true}, {"2.1", "!=2.0.*", true}, // Test the greater than equal operation {"2.0", ">=2", true}, {"2.0", ">=2.0", true}, {"2.0", ">=2.0.0", true}, {"2.0.post1", ">=2", true}, {"2.0.post1.dev1", ">=2", true}, {"3", ">=2", true}, // Test the less than equal operation {"2.0", "<=2", true}, {"2.0", "<=2.0", true}, {"2.0", "<=2.0.0", true}, {"2.0.dev1", "<=2", true}, {"2.0a1", "<=2", true}, {"2.0a1.dev1", "<=2", true}, {"2.0b1", "<=2", true}, {"2.0b1.post1", "<=2", true}, {"2.0c1", "<=2", true}, {"2.0c1.post1.dev1", "<=2", true}, {"2.0rc1", "<=2", true}, {"1", "<=2", true}, // Test the greater than operation {"3", ">2", true}, {"2.1", ">2.0", true}, {"2.0.1", ">2", true}, {"2.1.post1", ">2", true}, {"2.1+local.version", ">2", true}, // Test the less than operation {"1", "<2", true}, {"2.0", "<2.1", true}, {"2.0.dev0", "<2.1", true}, // Test the compatibility operation {"1", "~=1.0", true}, {"1.0.1", "~=1.0", true}, {"1.1", "~=1.0", true}, {"1.9999999", "~=1.0", true}, // Test that epochs are handled sanely {"2!1.0", "~=2!1.0", true}, {"2!1.0", "==2!1.*", true}, {"2!1.0", "==2!1.0", true}, {"2!1.0", "!=1.0", true}, {"1.0", "!=2!1.0", true}, {"1.0", "<=2!0.1", true}, {"2!1.0", ">=2.0", true}, {"1.0", "<2!0.1", true}, {"2!1.0", ">2.0", true}, // Test some normalization rules {"2.0.5", ">2.0dev", true}, // Test the equality operation {"2.1", "==2", false}, {"2.1", "==2.0", false}, {"2.1", "==2.0.0", false}, {"2.0", "==2.0+deadbeef", false}, //Test the equality operation with a prefix {"2.0", "==3.*", false}, {"2.1", "==2.0.*", false}, // Test the in-equality operation {"2.0", "!=2", false}, {"2.0", "!=2.0", false}, {"2.0", "!=2.0.0", false}, {"2.0+deadbeef", "!=2", false}, {"2.0+deadbeef", "!=2.0", false}, {"2.0+deadbeef", "!=2.0.0", false}, {"2.0+deadbeef", "!=2+deadbeef", false}, {"2.0+deadbeef", "!=2.0+deadbeef", false}, {"2.0+deadbeef", "!=2.0.0+deadbeef", false}, {"2.0+deadbeef.0", "!=2.0.0+deadbeef.00", false}, // Test the in-equality operation with a prefix {"2.dev1", "!=2.*", false}, {"2a1", "!=2.*", false}, {"2a1.post1", "!=2.*", false}, {"2b1", "!=2.*", false}, {"2b1.dev1", "!=2.*", false}, {"2c1", "!=2.*", false}, {"2c1.post1.dev1", "!=2.*", false}, {"2rc1", "!=2.*", false}, {"2", "!=2.*", false}, {"2.0", "!=2.*", false}, {"2.0.0", "!=2.*", false}, {"2.0.post1", "!=2.0.post1.*", false}, {"2.0.post1.dev1", "!=2.0.post1.*", false}, //Test the greater than equal operation {"2.0.dev1", ">=2", false}, {"2.0a1", ">=2", false}, {"2.0a1.dev1", ">=2", false}, {"2.0b1", ">=2", false}, {"2.0b1.post1", ">=2", false}, {"2.0c1", ">=2", false}, {"2.0c1.post1.dev1", ">=2", false}, {"2.0rc1", ">=2", false}, {"1", ">=2", false}, // Test the less than equal operation {"2.0.post1", "<=2", false}, {"2.0.post1.dev1", "<=2", false}, {"3", "<=2", false}, // Test the greater than operation {"1", ">2", false}, {"2.0.dev1", ">2", false}, {"2.0a1", ">2", false}, {"2.0a1.post1", ">2", false}, {"2.0b1", ">2", false}, {"2.0b1.dev1", ">2", false}, {"2.0c1", ">2", false}, {"2.0c1.post1.dev1", ">2", false}, {"2.0rc1", ">2", false}, {"2.0", ">2", false}, {"2.0.post1", ">2", false}, {"2.0.post1.dev1", ">2", false}, {"2.0+local.version", ">2", false}, // Test the less than operation {"2.0.dev1", "<2", false}, {"2.0a1", "<2", false}, {"2.0a1.post1", "<2", false}, {"2.0b1", "<2", false}, {"2.0b2.dev1", "<2", false}, {"2.0c1", "<2", false}, {"2.0c1.post1.dev1", "<2", false}, {"2.0rc1", "<2", false}, {"2.0", "<2", false}, {"2.post1", "<2", false}, {"2.post1.dev1", "<2", false}, {"3", "<2", false}, // Test the compatibility operation {"2.0", "~=1.0", false}, {"1.1.0", "~=1.0.0", false}, {"1.1.post1", "~=1.0.0", false}, // Test that epochs are handled sanely {"1.0", "~=2!1.0", false}, {"2!1.0", "~=1.0", false}, {"2!1.0", "==1.0", false}, {"1.0", "==2!1.0", false}, {"2!1.0", "==1.*", false}, {"1.0", "==2!1.*", false}, {"2!1.0", "!=2!1.0", false}, // local versions {"1.0.0+local", "==1.0.0", true}, {"1.0.0+local", "!=1.0.0", false}, {"1.0.0+local", "<=1.0.0", true}, {"1.0.0+local", ">=1.0.0", true}, {"1.0.0+local", "<1.0.0", false}, {"1.0.0+local", ">1.0.0", false}, // and operators {"1.0", ">= 1.0, != 1.3.4.*, < 2.0", true}, {"1.0", "~= 0.9, >= 1.0, != 1.3.4.*, < 2.0", false}, {"0.9", "~= 0.9, != 1.3.4.*, < 2.0", true}, {"2.0", ">= 1.0, != 1.3.4.*, < 2.0", false}, {"1.3.4", ">= 1.0, != 1.3.4.*, < 2.0", false}, // or operators {"1.0", "~= 0.9, >= 1.0, != 1.3.4.*, < 2.0 || ==1.0", true}, {"1.0", "~= 0.9, >= 1.0, != 1.3.4.*, < 2.0 || !=1.0", false}, // Test the equality operation not defined in PEP 440 {"2.0", "2", true}, {"2.0", "2.0", true}, {"2.0", "2.0.0", true}, {"2.1", "2", false}, {"2.1", "2.0", false}, {"2.1", "2.0.0", false}, {"2.0", "2.0+deadbeef", false}, {"2.0", "=2", true}, {"2.0", "=2.0", true}, {"2.0", "=2.0.0", true}, {"2.1", "=2", false}, {"2.1", "=2.0", false}, {"2.1", "=2.0.0", false}, {"2.0", "=2.0+deadbeef", false}, {"2.0", "*", true}, // space separated {"1.0", ">= 1.0 != 1.3.4.* < 2.0", true}, {"1.0", "~= 0.9 >= 1.0 != 1.3.4.* < 2.0", false}, {"0.9", "~= 0.9 != 1.3.4.* < 2.0", true}, {"2.0", ">= 1.0 != 1.3.4.* < 2.0", false}, {"1.3.4", ">= 1.0 != 1.3.4.* < 2.0", false}, } for _, tt := range tests { t.Run(fmt.Sprintf("%s %s", tt.version, tt.spec), func(t *testing.T) { c, err := NewSpecifiers(tt.spec) require.NoError(t, err) v, err := Parse(tt.version) require.NoError(t, err) assert.Equal(t, tt.want, c.Check(v)) }) } } func TestVersion_CheckWithPreRelease(t *testing.T) { tests := []struct { version string spec string want bool }{ {"1.3.4", "< 2.0", true}, {"2.0a1", "<2", true}, {"2.1a1", "<2", false}, } for _, tt := range tests { t.Run(fmt.Sprintf("%s %s", tt.version, tt.spec), func(t *testing.T) { c, err := NewSpecifiers(tt.spec, WithPreRelease(true)) require.NoError(t, err) v, err := Parse(tt.version) require.NoError(t, err) assert.Equal(t, tt.want, c.Check(v)) }) } } aquasecurity-go-pep440-version-43e6734/version.go000066400000000000000000000220721474412006500216340ustar00rootroot00000000000000package version import ( "bytes" "fmt" "regexp" "strings" "golang.org/x/xerrors" "github.com/aquasecurity/go-version/pkg/part" ) var ( // The compiled regular expression used to test the validity of a version. versionRegex *regexp.Regexp // https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/version.py#L459-L464 preReleaseAliases = map[string]string{ "a": "a", "alpha": "a", "b": "b", "beta": "b", "rc": "rc", "c": "rc", "pre": "rc", "preview": "rc", } // https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/version.py#L465-L466 postReleaseAliases = map[string]string{ "post": "post", "rev": "post", "r": "post", } ) const ( // The raw regular expression string used for testing the validity of a version. regex = `v?` + `(?:` + `(?:(?P[0-9]+)!)?` + // epoch `(?P[0-9]+(?:\.[0-9]+)*)` + // release segment `(?P
[-_\.]?(?P(a|b|c|rc|alpha|beta|pre|preview))[-_\.]?(?P[0-9]+)?)?` + // pre-release
		`(?P(?:-(?P[0-9]+))|(?:[-_\.]?(?Ppost|rev|r)[-_\.]?(?P[0-9]+)?))?` + // post release
		`(?P[-_\.]?(?Pdev)[-_\.]?(?P[0-9]+)?)?)` + // dev release
		`(?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?` // local version`
)

// Version represents a single version.
type Version struct {
	epoch              part.Uint64
	release            []part.Uint64
	pre                letterNumber
	post               letterNumber
	dev                letterNumber
	local              string
	key                key
	preReleaseIncluded bool
	original           string
}

type key struct {
	epoch   part.Uint64
	release part.Parts
	pre     part.Part
	post    part.Part
	dev     part.Part
	local   part.Part
}

func (k key) compare(o key) int {
	p1 := part.Parts{k.epoch, k.release, k.pre, k.post, k.dev, k.local}
	p2 := part.Parts{o.epoch, o.release, o.pre, o.post, o.dev, o.local}
	return p1.Compare(p2)
}

type letterNumber struct {
	letter part.String
	number part.Uint64
}

func (ln letterNumber) isNull() bool {
	return ln.letter.IsNull() && ln.number.IsNull()
}

func init() {
	versionRegex = regexp.MustCompile(`(?i)^\s*` + regex + `\s*$`)
}

// MustParse is like Parse but panics if the version cannot be parsed.
func MustParse(v string) Version {
	ver, err := Parse(v)
	if err != nil {
		panic(err)
	}
	return ver
}

// Parse parses the given version and returns a new Version.
func Parse(v string) (Version, error) {
	matches := versionRegex.FindStringSubmatch(v)
	if matches == nil {
		return Version{}, xerrors.Errorf("malformed version: %s", v)
	}

	var epoch, preN, postN, devN part.Uint64
	var preL, postL, devL part.String
	var release []part.Uint64
	var local string
	var err error

	for i, name := range versionRegex.SubexpNames() {
		m := matches[i]
		if m == "" {
			continue
		}

		switch name {
		case "epoch":
			epoch, err = part.NewUint64(m)
		case "release":
			for _, str := range strings.Split(m, ".") {
				val, err := part.NewUint64(str)
				if err != nil {
					return Version{}, xerrors.Errorf("error parsing version: %w", err)
				}

				release = append(release, val)
			}
		case "pre_l":
			preL = part.String(preReleaseAliases[strings.ToLower(m)])
		case "pre_n":
			preN, err = part.NewUint64(m)
		case "post_l":
			postL = part.String(postReleaseAliases[strings.ToLower(m)])
		case "post_n1", "post_n2":
			// https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/version.py#L469-L472
			if postL == "" {
				postL = "post"
			}
			postN, err = part.NewUint64(m)
		case "dev_l":
			devL = part.String(strings.ToLower(m))
		case "dev_n":
			devN, err = part.NewUint64(m)
		case "local":
			local = strings.ToLower(m)
		}
		if err != nil {
			return Version{}, xerrors.Errorf("failed to parse version (%s): %w", v, err)
		}
	}

	pre := letterNumber{
		letter: preL,
		number: preN,
	}
	post := letterNumber{
		letter: postL,
		number: postN,
	}
	dev := letterNumber{
		letter: devL,
		number: devN,
	}

	return Version{
		epoch:    epoch,
		release:  release,
		pre:      pre,
		post:     post,
		dev:      dev,
		local:    local,
		key:      cmpkey(epoch, release, pre, post, dev, local),
		original: v,
	}, nil
}

// UnmarshalText implements [encoding.TextUnmarshaler].
func (v *Version) UnmarshalText(data []byte) error {
	var err error
	*v, err = Parse(string(data))
	return err
}

// ref. https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/version.py#L495
func cmpkey(epoch part.Uint64, release []part.Uint64, pre, post, dev letterNumber, local string) key {
	// Set default values
	k := key{
		epoch: epoch,
		pre:   part.Parts{pre.letter, pre.number},
		post:  part.Parts{post.letter, post.number},
		dev:   part.Parts{dev.letter, dev.number},
		local: part.NegativeInfinity,
	}

	// Remove trailing zeros
	k.release = part.Uint64SliceToParts(release).Normalize()

	// https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/packaging/version.py#L514-L517
	if pre.isNull() && post.isNull() && !dev.isNull() {
		k.pre = part.NegativeInfinity
	} else if pre.isNull() {
		k.pre = part.Infinity
	}

	// Versions without a post segment should sort before those with one.
	if post.isNull() {
		k.post = part.NegativeInfinity
	}

	// Versions without a development segment should sort after those with one.
	if dev.isNull() {
		k.dev = part.Infinity
	}

	// Versions with a local segment need that segment parsed to implement the sorting rules in PEP440.
	//   - Alpha numeric segments sort before numeric segments
	//   - Alpha numeric segments sort lexicographically
	//   - Numeric segments sort numerically
	//   - Shorter versions sort before longer versions when the prefixes match exactly
	if local != "" {
		var parts part.Parts
		for _, l := range strings.Split(local, ".") {
			if p, err := part.NewUint64(l); err == nil {
				parts = append(parts, p)
			} else {
				parts = append(parts, part.NewPreString(l))
			}
		}
		k.local = parts
	}

	return k
}

// Compare compares this version to another version. This
// returns -1, 0, or 1 if this version is smaller, equal,
// or larger than the other version, respectively.
func (v Version) Compare(other Version) int {
	// A quick, efficient equality check
	if v.String() == other.String() {
		return 0
	}

	k1 := v.key
	k2 := other.key

	k1.release = k1.release.Padding(len(k2.release), part.Zero)
	k2.release = k2.release.Padding(len(k2.release), part.Zero)

	return k1.compare(k2)
}

// Equal tests if two versions are equal.
func (v Version) Equal(o Version) bool {
	return v.Compare(o) == 0
}

// GreaterThan tests if this version is greater than another version.
func (v Version) GreaterThan(o Version) bool {
	return v.Compare(o) > 0
}

// GreaterThanOrEqual tests if this version is greater than or equal to another version.
func (v Version) GreaterThanOrEqual(o Version) bool {
	return v.Compare(o) >= 0
}

// LessThan tests if this version is less than another version.
func (v Version) LessThan(o Version) bool {
	return v.Compare(o) < 0
}

// LessThanOrEqual tests if this version is less than or equal to another version.
func (v Version) LessThanOrEqual(o Version) bool {
	return v.Compare(o) <= 0
}

// String returns the full version string included pre-release
// and metadata information.
func (v Version) String() string {
	var buf bytes.Buffer

	// Epoch
	if v.epoch != 0 {
		fmt.Fprintf(&buf, "%d!", v.epoch)
	}

	// Release segment
	if len(v.release) != 0 {
		fmt.Fprintf(&buf, "%d", v.release[0])
		for _, r := range v.release[1:] {
			fmt.Fprintf(&buf, ".%d", r)
		}
	}

	// Pre-release
	if !v.pre.isNull() {
		fmt.Fprintf(&buf, "%s%d", v.pre.letter, v.pre.number)
	}

	// Post-release
	if !v.post.isNull() {
		fmt.Fprintf(&buf, ".post%d", v.post.number)
	}

	// Development release
	if !v.dev.isNull() {
		fmt.Fprintf(&buf, ".dev%d", v.dev.number)
	}

	// Local version segment
	if v.local != "" {
		fmt.Fprintf(&buf, "+%s", v.local)
	}

	return buf.String()
}

// MarshalText implements [encoding.TextMarshaler].
func (v Version) MarshalText() ([]byte, error) {
	return []byte(v.String()), nil
}

// BaseVersion returns the base version
func (v Version) BaseVersion() string {
	var buf bytes.Buffer

	// Epoch
	if v.epoch != 0 {
		fmt.Fprintf(&buf, "%d!", v.epoch)
	}

	// Release segment
	if len(v.release) != 0 {
		fmt.Fprintf(&buf, "%d", v.release[0])
		for _, r := range v.release[1:] {
			fmt.Fprintf(&buf, ".%d", r)
		}
	}

	return buf.String()
}

// Original returns the original parsed version as-is, including any
// potential whitespace, `v` prefix, etc.
func (v Version) Original() string {
	return v.original
}

// Local returns the local version
func (v Version) Local() string {
	return v.local
}

// Public returns the public version
func (v Version) Public() string {
	return strings.SplitN(v.String(), "+", 2)[0]
}

// IsPreRelease returns if it is a pre-release
func (v Version) IsPreRelease() bool {
	if v.preReleaseIncluded {
		return false
	}
	return !v.pre.isNull() || !v.dev.isNull()
}

// IsPostRelease returns if it is a post-release
func (v Version) IsPostRelease() bool {
	return !v.post.isNull()
}
aquasecurity-go-pep440-version-43e6734/version_test.go000066400000000000000000000203741474412006500226760ustar00rootroot00000000000000package version_test

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/aquasecurity/go-pep440-version"
)

var (
	versions = []string{
		// Implicit epoch of 0
		"1.0.dev456",
		"1.0a1",
		"1.0a2.dev456",
		"1.0a12.dev456",
		"1.0a12",
		"1.0b1.dev456",
		"1.0b2",
		"1.0b2.post345.dev456",
		"1.0b2.post345",
		"1.0b2-346",
		"1.0c1.dev456",
		"1.0c1",
		"1.0rc2",
		"1.0c3",
		"1.0",
		"1.0.post456.dev34",
		"1.0.post456",
		"1.1.dev1",
		"1.2+123abc",
		"1.2+123abc456",
		"1.2+abc",
		"1.2+abc123",
		"1.2+abc123def",
		"1.2+1234.abc",
		"1.2+123456",
		"1.2.r32+123456",
		"1.2.rev33+123456",
		// Explicit epoch of 1
		"1!1.0.dev456",
		"1!1.0a1",
		"1!1.0a2.dev456",
		"1!1.0a12.dev456",
		"1!1.0a12",
		"1!1.0b1.dev456",
		"1!1.0b2",
		"1!1.0b2.post345.dev456",
		"1!1.0b2.post345",
		"1!1.0b2-346",
		"1!1.0c1.dev456",
		"1!1.0c1",
		"1!1.0rc2",
		"1!1.0c3",
		"1!1.0",
		"1!1.0.post456.dev34",
		"1!1.0.post456",
		"1!1.1.dev1",
		"1!1.2+123abc",
		"1!1.2+123abc456",
		"1!1.2+abc",
		"1!1.2+abc123",
		"1!1.2+abc123def",
		"1!1.2+1234.abc",
		"1!1.2+123456",
		"1!1.2.r32+123456",
		"1!1.2.rev33+123456",
	}
)

// https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/tests/test_version.py#L85-L87
func TestParseValidVersion(t *testing.T) {
	for _, v := range versions {
		t.Run(v, func(t *testing.T) {
			_, err := version.Parse(v)
			assert.NoError(t, err)
		})
	}
}

// https://github.com/pypa/packaging/blob/a6407e3a7e19bd979e93f58cfc7f6641a7378c46/tests/test_version.py#L102-L104
func TestParseInvalidVersion(t *testing.T) {
	versions := []string{
		// Non sensical versions should be invalid
		"french toast",
		// Versions with invalid local versions
		"1.0+a+",
		"1.0++",
		"1.0+_foobar",
		"1.0+foo&asd",
		"1.0+1+1",
	}
	for _, v := range versions {
		t.Run(v, func(t *testing.T) {
			_, err := version.Parse(v)
			assert.Error(t, err)
		})
	}
}

func TestVersion_String(t *testing.T) {
	tests := []struct {
		version string
		want    string
	}{
		// Various development release incarnations
		{"1.0dev", "1.0.dev0"},
		{"1.0.dev", "1.0.dev0"},
		{"1.0dev1", "1.0.dev1"},
		{"1.0-dev", "1.0.dev0"},
		{"1.0-dev1", "1.0.dev1"},
		{"1.0DEV", "1.0.dev0"},
		{"1.0.DEV", "1.0.dev0"},
		{"1.0DEV1", "1.0.dev1"},
		{"1.0DEV", "1.0.dev0"},
		{"1.0.DEV1", "1.0.dev1"},
		{"1.0-DEV", "1.0.dev0"},
		{"1.0-DEV1", "1.0.dev1"},
		// Various alpha incarnations
		{"1.0a", "1.0a0"},
		{"1.0.a", "1.0a0"},
		{"1.0.a1", "1.0a1"},
		{"1.0-a", "1.0a0"},
		{"1.0-a1", "1.0a1"},
		{"1.0alpha", "1.0a0"},
		{"1.0.alpha", "1.0a0"},
		{"1.0.alpha1", "1.0a1"},
		{"1.0-alpha", "1.0a0"},
		{"1.0-alpha1", "1.0a1"},
		{"1.0A", "1.0a0"},
		{"1.0.A", "1.0a0"},
		{"1.0.A1", "1.0a1"},
		{"1.0-A", "1.0a0"},
		{"1.0-A1", "1.0a1"},
		{"1.0ALPHA", "1.0a0"},
		{"1.0.ALPHA", "1.0a0"},
		{"1.0.ALPHA1", "1.0a1"},
		{"1.0-ALPHA", "1.0a0"},
		{"1.0-ALPHA1", "1.0a1"},
		// Various beta incarnations
		{"1.0b", "1.0b0"},
		{"1.0.b", "1.0b0"},
		{"1.0.b1", "1.0b1"},
		{"1.0-b", "1.0b0"},
		{"1.0-b1", "1.0b1"},
		{"1.0beta", "1.0b0"},
		{"1.0.beta", "1.0b0"},
		{"1.0.beta1", "1.0b1"},
		{"1.0-beta", "1.0b0"},
		{"1.0-beta1", "1.0b1"},
		{"1.0B", "1.0b0"},
		{"1.0.B", "1.0b0"},
		{"1.0.B1", "1.0b1"},
		{"1.0-B", "1.0b0"},
		{"1.0-B1", "1.0b1"},
		{"1.0BETA", "1.0b0"},
		{"1.0.BETA", "1.0b0"},
		{"1.0.BETA1", "1.0b1"},
		{"1.0-BETA", "1.0b0"},
		{"1.0-BETA1", "1.0b1"},
		// Various release candidate incarnations
		{"1.0c", "1.0rc0"},
		{"1.0.c", "1.0rc0"},
		{"1.0.c1", "1.0rc1"},
		{"1.0-c", "1.0rc0"},
		{"1.0-c1", "1.0rc1"},
		{"1.0rc", "1.0rc0"},
		{"1.0.rc", "1.0rc0"},
		{"1.0.rc1", "1.0rc1"},
		{"1.0-rc", "1.0rc0"},
		{"1.0-rc1", "1.0rc1"},
		{"1.0C", "1.0rc0"},
		{"1.0.C", "1.0rc0"},
		{"1.0.C1", "1.0rc1"},
		{"1.0-C", "1.0rc0"},
		{"1.0-C1", "1.0rc1"},
		{"1.0RC", "1.0rc0"},
		{"1.0.RC", "1.0rc0"},
		{"1.0.RC1", "1.0rc1"},
		{"1.0-RC", "1.0rc0"},
		{"1.0-RC1", "1.0rc1"},
		// Various post release incarnations
		{"1.0post", "1.0.post0"},
		{"1.0.post", "1.0.post0"},
		{"1.0post1", "1.0.post1"},
		{"1.0post", "1.0.post0"},
		{"1.0-post", "1.0.post0"},
		{"1.0-post1", "1.0.post1"},
		{"1.0POST", "1.0.post0"},
		{"1.0.POST", "1.0.post0"},
		{"1.0POST1", "1.0.post1"},
		{"1.0POST", "1.0.post0"},
		{"1.0r", "1.0.post0"},
		{"1.0rev", "1.0.post0"},
		{"1.0.POST1", "1.0.post1"},
		{"1.0.r1", "1.0.post1"},
		{"1.0.rev1", "1.0.post1"},
		{"1.0-POST", "1.0.post0"},
		{"1.0-POST1", "1.0.post1"},
		{"1.0-5", "1.0.post5"},
		{"1.0-r5", "1.0.post5"},
		{"1.0-rev5", "1.0.post5"},
		// Local version case insensitivity
		{"1.0+AbC", "1.0+abc"},
		// Integer Normalization
		{"1.01", "1.1"},
		{"1.0a05", "1.0a5"},
		{"1.0b07", "1.0b7"},
		{"1.0c056", "1.0rc56"},
		{"1.0rc09", "1.0rc9"},
		{"1.0.post000", "1.0.post0"},
		{"1.1.dev09000", "1.1.dev9000"},
		{"00!1.2", "1.2"},
		{"0100!0.0", "100!0.0"},
		// Various other normalizations
		{"v1.0", "1.0"},
		{"   v1.0\t\n", "1.0"},
	}
	for _, tt := range tests {
		t.Run(tt.version, func(t *testing.T) {
			v, err := version.Parse(tt.version)
			require.NoError(t, err)

			assert.Equal(t, tt.want, v.String())
		})
	}
	t.Run("Zero Value", func(t *testing.T) {
		v := version.Version{}
		assert.Equal(t, "", v.String())
	})
}

func TestVersion_LessThan_LessThanOrEqual(t *testing.T) {
	var tests [][2]string
	for i, v1 := range versions {
		for _, v2 := range versions[i+1:] {
			tests = append(tests, [2]string{v1, v2})
		}
	}
	for _, tt := range tests {
		t.Run(tt[0]+" < "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.True(t, v1.LessThan(v2))
		})
		t.Run(tt[0]+" >= "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.False(t, v1.GreaterThanOrEqual(v2))
		})
	}
}

func TestVersion_LessThanOrEqual(t *testing.T) {
	var tests [][2]string
	for i, v1 := range versions {
		for _, v2 := range versions[i:] {
			tests = append(tests, [2]string{v1, v2})
		}
	}
	for _, tt := range tests {
		t.Run(tt[0]+" <= "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.True(t, v1.LessThanOrEqual(v2))
		})
		t.Run(tt[0]+" > "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.False(t, v1.GreaterThan(v2))
		})
	}
}

func TestVersion_Equal(t *testing.T) {
	var tests [][2]string
	for _, v := range versions {
		tests = append(tests, [2]string{v, v})
	}
	for _, tt := range tests {
		t.Run(tt[0]+" = "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.True(t, v1.Equal(v2))
		})
	}
}

func TestVersion_GreaterThan(t *testing.T) {
	var tests [][2]string
	for i, v1 := range versions {
		for _, v2 := range versions[:i] {
			tests = append(tests, [2]string{v1, v2})
		}
	}
	for _, tt := range tests {
		t.Run(tt[0]+" > "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.True(t, v1.GreaterThan(v2))
		})
		t.Run(tt[0]+" <= "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.False(t, v1.LessThanOrEqual(v2))
		})
	}
}

func TestVersion_GreaterThanOrEqual(t *testing.T) {
	var tests [][2]string
	for i, v1 := range versions {
		for _, v2 := range versions[:i+1] {
			tests = append(tests, [2]string{v1, v2})
		}
	}
	for _, tt := range tests {
		t.Run(tt[0]+" >= "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.True(t, v1.GreaterThanOrEqual(v2))
		})
		t.Run(tt[0]+" < "+tt[1], func(t *testing.T) {
			v1, v2 := parseVersions(t, tt[0], tt[1])
			assert.False(t, v1.LessThan(v2))
		})
	}
}

func parseVersions(t *testing.T, s1, s2 string) (version.Version, version.Version) {
	t.Helper()

	v1, err := version.Parse(s1)
	require.NoError(t, err)

	v2, err := version.Parse(s2)
	require.NoError(t, err)

	return v1, v2
}

func TestVersion_JSON(t *testing.T) {
	versionString := "1.0.post456.dev34"
	v := version.MustParse(versionString)

	jsonString := fmt.Sprintf(`{"version": "%s"}`, versionString)
	type vs struct {
		Version version.Version `json:"version"`
	}

	t.Run("Unmarshal", func(t *testing.T) {
		fromJson := vs{}
		if err := json.Unmarshal([]byte(jsonString), &fromJson); assert.NoError(t, err) {
			assert.Equal(t, vs{Version: v}, fromJson)
		}
	})

	t.Run("Marshal", func(t *testing.T) {
		if toJson, err := json.Marshal(vs{Version: v}); assert.NoError(t, err) {
			assert.JSONEq(t, jsonString, string(toJson))
		}
	})
}