├── .github └── workflows │ ├── codeql.yaml │ ├── fuzz.yaml │ ├── golangci-lint.yml │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── benchmark_test.go ├── collection.go ├── collection_test.go ├── constraints.go ├── constraints_test.go ├── doc.go ├── go.mod ├── version.go └── version_test.go /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: "CodeQL" 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | CodeQL-Build: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | # required for all workflows 16 | security-events: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v3 24 | with: 25 | languages: go 26 | 27 | - name: Autobuild 28 | uses: github/codeql-action/autobuild@v3 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v3 32 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yaml: -------------------------------------------------------------------------------- 1 | name: Fuzz Testing 2 | on: 3 | # Perform Fuzz testing on PRs and on a daily basis. Daily will continue to 4 | # look for problems. Doing this on PRs will look for issues introduced in 5 | # a change. 6 | pull_request: 7 | schedule: 8 | - cron: '33 23 * * *' # Run at 11:33 every day 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | env: 13 | cache-key: fuzzing 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v5 17 | with: 18 | # There are no dependencies so there is no go.sum file. This is needed 19 | # for the cache key generation. So, caching doesn't happen and a 20 | # warning is presented on each run. Disabling the Go cache and caching 21 | # the go-build cache separately for fuzzing. 22 | cache: false 23 | go-version: "1.23" 24 | # The cache path may be different on different runners. GitHub may change 25 | # this in the future. So, we dynamically fetch it. 26 | - name: Get Go Cache Paths 27 | id: go-cache-paths 28 | run: echo "go-build=$(go env GOCACHE)" >> $GITHUB_OUTPUT 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | - name: Restore Cache 32 | id: cache-restore 33 | uses: actions/cache/restore@v4 34 | with: 35 | path: ${{ steps.go-cache-paths.outputs.go-build }} 36 | key: ${{ env.cache-key }} 37 | - name: Fuzz 38 | run: make fuzz 39 | # Cannot overwrite the existing cache (id's are immutable) so we delete it. 40 | - name: Delete Previous Cache 41 | if: ${{ steps.cache-restore.outputs.cache-hit }} 42 | continue-on-error: true 43 | run: | 44 | gh extension install actions/gh-actions-cache 45 | gh actions-cache delete "${{ env.cache-key }}" --confirm 46 | env: 47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | # Saving the cache so that Fuzz testing can be additive to previous fuzz testing. 49 | - name: Save Cache 50 | uses: actions/cache/save@v4 51 | with: 52 | path: ${{ steps.go-cache-paths.outputs.go-build }} 53 | key: ${{ env.cache-key }} 54 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | golangci: 9 | name: golangci-lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: "1.23" 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v4 21 | with: 22 | version: v1.60 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Tests 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.21.x, 1.22.x, 1.23.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Test 18 | env: 19 | GO111MODULE: on 20 | run: go test -cover . 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _fuzz/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 2m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - misspell 8 | - govet 9 | - staticcheck 10 | - errcheck 11 | - unparam 12 | - ineffassign 13 | - nakedret 14 | - gocyclo 15 | - dupl 16 | - goimports 17 | - revive 18 | - gosec 19 | - gosimple 20 | - typecheck 21 | - unused 22 | 23 | linters-settings: 24 | gofmt: 25 | simplify: true 26 | dupl: 27 | threshold: 600 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.3.0 (2024-08-27) 4 | 5 | ### Added 6 | 7 | - #238: Add LessThanEqual and GreaterThanEqual functions (thanks @grosser) 8 | - #213: nil version equality checking (thanks @KnutZuidema) 9 | 10 | ### Changed 11 | 12 | - #241: Simplify StrictNewVersion parsing (thanks @grosser) 13 | - Testing support up through Go 1.23 14 | - Minimum version set to 1.21 as this is what's tested now 15 | - Fuzz testing now supports caching 16 | 17 | ## 3.2.1 (2023-04-10) 18 | 19 | ### Changed 20 | 21 | - #198: Improved testing around pre-release names 22 | - #200: Improved code scanning with addition of CodeQL 23 | - #201: Testing now includes Go 1.20. Go 1.17 has been dropped 24 | - #202: Migrated Fuzz testing to Go built-in Fuzzing. CI runs daily 25 | - #203: Docs updated for security details 26 | 27 | ### Fixed 28 | 29 | - #199: Fixed issue with range transformations 30 | 31 | ## 3.2.0 (2022-11-28) 32 | 33 | ### Added 34 | 35 | - #190: Added text marshaling and unmarshaling 36 | - #167: Added JSON marshalling for constraints (thanks @SimonTheLeg) 37 | - #173: Implement encoding.TextMarshaler and encoding.TextUnmarshaler on Version (thanks @MarkRosemaker) 38 | - #179: Added New() version constructor (thanks @kazhuravlev) 39 | 40 | ### Changed 41 | 42 | - #182/#183: Updated CI testing setup 43 | 44 | ### Fixed 45 | 46 | - #186: Fixing issue where validation of constraint section gave false positives 47 | - #176: Fix constraints check with *-0 (thanks @mtt0) 48 | - #181: Fixed Caret operator (^) gives unexpected results when the minor version in constraint is 0 (thanks @arshchimni) 49 | - #161: Fixed godoc (thanks @afirth) 50 | 51 | ## 3.1.1 (2020-11-23) 52 | 53 | ### Fixed 54 | 55 | - #158: Fixed issue with generated regex operation order that could cause problem 56 | 57 | ## 3.1.0 (2020-04-15) 58 | 59 | ### Added 60 | 61 | - #131: Add support for serializing/deserializing SQL (thanks @ryancurrah) 62 | 63 | ### Changed 64 | 65 | - #148: More accurate validation messages on constraints 66 | 67 | ## 3.0.3 (2019-12-13) 68 | 69 | ### Fixed 70 | 71 | - #141: Fixed issue with <= comparison 72 | 73 | ## 3.0.2 (2019-11-14) 74 | 75 | ### Fixed 76 | 77 | - #134: Fixed broken constraint checking with ^0.0 (thanks @krmichelos) 78 | 79 | ## 3.0.1 (2019-09-13) 80 | 81 | ### Fixed 82 | 83 | - #125: Fixes issue with module path for v3 84 | 85 | ## 3.0.0 (2019-09-12) 86 | 87 | This is a major release of the semver package which includes API changes. The Go 88 | API is compatible with ^1. The Go API was not changed because many people are using 89 | `go get` without Go modules for their applications and API breaking changes cause 90 | errors which we have or would need to support. 91 | 92 | The changes in this release are the handling based on the data passed into the 93 | functions. These are described in the added and changed sections below. 94 | 95 | ### Added 96 | 97 | - StrictNewVersion function. This is similar to NewVersion but will return an 98 | error if the version passed in is not a strict semantic version. For example, 99 | 1.2.3 would pass but v1.2.3 or 1.2 would fail because they are not strictly 100 | speaking semantic versions. This function is faster, performs fewer operations, 101 | and uses fewer allocations than NewVersion. 102 | - Fuzzing has been performed on NewVersion, StrictNewVersion, and NewConstraint. 103 | The Makefile contains the operations used. For more information on you can start 104 | on Wikipedia at https://en.wikipedia.org/wiki/Fuzzing 105 | - Now using Go modules 106 | 107 | ### Changed 108 | 109 | - NewVersion has proper prerelease and metadata validation with error messages 110 | to signal an issue with either of them 111 | - ^ now operates using a similar set of rules to npm/js and Rust/Cargo. If the 112 | version is >=1 the ^ ranges works the same as v1. For major versions of 0 the 113 | rules have changed. The minor version is treated as the stable version unless 114 | a patch is specified and then it is equivalent to =. One difference from npm/js 115 | is that prereleases there are only to a specific version (e.g. 1.2.3). 116 | Prereleases here look over multiple versions and follow semantic version 117 | ordering rules. This pattern now follows along with the expected and requested 118 | handling of this packaged by numerous users. 119 | 120 | ## 1.5.0 (2019-09-11) 121 | 122 | ### Added 123 | 124 | - #103: Add basic fuzzing for `NewVersion()` (thanks @jesse-c) 125 | 126 | ### Changed 127 | 128 | - #82: Clarify wildcard meaning in range constraints and update tests for it (thanks @greysteil) 129 | - #83: Clarify caret operator range for pre-1.0.0 dependencies (thanks @greysteil) 130 | - #72: Adding docs comment pointing to vert for a cli 131 | - #71: Update the docs on pre-release comparator handling 132 | - #89: Test with new go versions (thanks @thedevsaddam) 133 | - #87: Added $ to ValidPrerelease for better validation (thanks @jeremycarroll) 134 | 135 | ### Fixed 136 | 137 | - #78: Fix unchecked error in example code (thanks @ravron) 138 | - #70: Fix the handling of pre-releases and the 0.0.0 release edge case 139 | - #97: Fixed copyright file for proper display on GitHub 140 | - #107: Fix handling prerelease when sorting alphanum and num 141 | - #109: Fixed where Validate sometimes returns wrong message on error 142 | 143 | ## 1.4.2 (2018-04-10) 144 | 145 | ### Changed 146 | 147 | - #72: Updated the docs to point to vert for a console appliaction 148 | - #71: Update the docs on pre-release comparator handling 149 | 150 | ### Fixed 151 | 152 | - #70: Fix the handling of pre-releases and the 0.0.0 release edge case 153 | 154 | ## 1.4.1 (2018-04-02) 155 | 156 | ### Fixed 157 | 158 | - Fixed #64: Fix pre-release precedence issue (thanks @uudashr) 159 | 160 | ## 1.4.0 (2017-10-04) 161 | 162 | ### Changed 163 | 164 | - #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill) 165 | 166 | ## 1.3.1 (2017-07-10) 167 | 168 | ### Fixed 169 | 170 | - Fixed #57: number comparisons in prerelease sometimes inaccurate 171 | 172 | ## 1.3.0 (2017-05-02) 173 | 174 | ### Added 175 | 176 | - #45: Added json (un)marshaling support (thanks @mh-cbon) 177 | - Stability marker. See https://masterminds.github.io/stability/ 178 | 179 | ### Fixed 180 | 181 | - #51: Fix handling of single digit tilde constraint (thanks @dgodd) 182 | 183 | ### Changed 184 | 185 | - #55: The godoc icon moved from png to svg 186 | 187 | ## 1.2.3 (2017-04-03) 188 | 189 | ### Fixed 190 | 191 | - #46: Fixed 0.x.x and 0.0.x in constraints being treated as * 192 | 193 | ## Release 1.2.2 (2016-12-13) 194 | 195 | ### Fixed 196 | 197 | - #34: Fixed issue where hyphen range was not working with pre-release parsing. 198 | 199 | ## Release 1.2.1 (2016-11-28) 200 | 201 | ### Fixed 202 | 203 | - #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha" 204 | properly. 205 | 206 | ## Release 1.2.0 (2016-11-04) 207 | 208 | ### Added 209 | 210 | - #20: Added MustParse function for versions (thanks @adamreese) 211 | - #15: Added increment methods on versions (thanks @mh-cbon) 212 | 213 | ### Fixed 214 | 215 | - Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and 216 | might not satisfy the intended compatibility. The change here ignores pre-releases 217 | on constraint checks (e.g., ~ or ^) when a pre-release is not part of the 218 | constraint. For example, `^1.2.3` will ignore pre-releases while 219 | `^1.2.3-alpha` will include them. 220 | 221 | ## Release 1.1.1 (2016-06-30) 222 | 223 | ### Changed 224 | 225 | - Issue #9: Speed up version comparison performance (thanks @sdboyer) 226 | - Issue #8: Added benchmarks (thanks @sdboyer) 227 | - Updated Go Report Card URL to new location 228 | - Updated Readme to add code snippet formatting (thanks @mh-cbon) 229 | - Updating tagging to v[SemVer] structure for compatibility with other tools. 230 | 231 | ## Release 1.1.0 (2016-03-11) 232 | 233 | - Issue #2: Implemented validation to provide reasons a versions failed a 234 | constraint. 235 | 236 | ## Release 1.0.1 (2015-12-31) 237 | 238 | - Fixed #1: * constraint failing on valid versions. 239 | 240 | ## Release 1.0.0 (2015-10-20) 241 | 242 | - Initial release 243 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2019, Matt Butcher and Matt Farina 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH=$(shell go env GOPATH) 2 | GOLANGCI_LINT=$(GOPATH)/bin/golangci-lint 3 | 4 | .PHONY: lint 5 | lint: $(GOLANGCI_LINT) 6 | @echo "==> Linting codebase" 7 | @$(GOLANGCI_LINT) run 8 | 9 | .PHONY: test 10 | test: 11 | @echo "==> Running tests" 12 | GO111MODULE=on go test -v 13 | 14 | .PHONY: test-cover 15 | test-cover: 16 | @echo "==> Running Tests with coverage" 17 | GO111MODULE=on go test -cover . 18 | 19 | .PHONY: fuzz 20 | fuzz: 21 | @echo "==> Running Fuzz Tests" 22 | go env GOCACHE 23 | go test -fuzz=FuzzNewVersion -fuzztime=15s . 24 | go test -fuzz=FuzzStrictNewVersion -fuzztime=15s . 25 | go test -fuzz=FuzzNewConstraint -fuzztime=15s . 26 | 27 | $(GOLANGCI_LINT): 28 | # Install golangci-lint. The configuration for it is in the .golangci.yml 29 | # file in the root of the repository 30 | echo ${GOPATH} 31 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.56.2 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SemVer 2 | 3 | The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to: 4 | 5 | * Parse semantic versions 6 | * Sort semantic versions 7 | * Check if a semantic version fits within a set of constraints 8 | * Optionally work with a `v` prefix 9 | 10 | [![Stability: 11 | Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html) 12 | [![](https://github.com/Masterminds/semver/workflows/Tests/badge.svg)](https://github.com/Masterminds/semver/actions) 13 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/semver/v3) 14 | [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver) 15 | 16 | ## Package Versions 17 | 18 | Note, import `github.com/Masterminds/semver/v3` to use the latest version. 19 | 20 | There are three major versions fo the `semver` package. 21 | 22 | * 3.x.x is the stable and active version. This version is focused on constraint 23 | compatibility for range handling in other tools from other languages. It has 24 | a similar API to the v1 releases. The development of this version is on the master 25 | branch. The documentation for this version is below. 26 | * 2.x was developed primarily for [dep](https://github.com/golang/dep). There are 27 | no tagged releases and the development was performed by [@sdboyer](https://github.com/sdboyer). 28 | There are API breaking changes from v1. This version lives on the [2.x branch](https://github.com/Masterminds/semver/tree/2.x). 29 | * 1.x.x is the original release. It is no longer maintained. You should use the 30 | v3 release instead. You can read the documentation for the 1.x.x release 31 | [here](https://github.com/Masterminds/semver/blob/release-1/README.md). 32 | 33 | ## Parsing Semantic Versions 34 | 35 | There are two functions that can parse semantic versions. The `StrictNewVersion` 36 | function only parses valid version 2 semantic versions as outlined in the 37 | specification. The `NewVersion` function attempts to coerce a version into a 38 | semantic version and parse it. For example, if there is a leading v or a version 39 | listed without all 3 parts (e.g. `v1.2`) it will attempt to coerce it into a valid 40 | semantic version (e.g., 1.2.0). In both cases a `Version` object is returned 41 | that can be sorted, compared, and used in constraints. 42 | 43 | When parsing a version an error is returned if there is an issue parsing the 44 | version. For example, 45 | 46 | v, err := semver.NewVersion("1.2.3-beta.1+build345") 47 | 48 | The version object has methods to get the parts of the version, compare it to 49 | other versions, convert the version back into a string, and get the original 50 | string. Getting the original string is useful if the semantic version was coerced 51 | into a valid form. 52 | 53 | ## Sorting Semantic Versions 54 | 55 | A set of versions can be sorted using the `sort` package from the standard library. 56 | For example, 57 | 58 | ```go 59 | raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",} 60 | vs := make([]*semver.Version, len(raw)) 61 | for i, r := range raw { 62 | v, err := semver.NewVersion(r) 63 | if err != nil { 64 | t.Errorf("Error parsing version: %s", err) 65 | } 66 | 67 | vs[i] = v 68 | } 69 | 70 | sort.Sort(semver.Collection(vs)) 71 | ``` 72 | 73 | ## Checking Version Constraints 74 | 75 | There are two methods for comparing versions. One uses comparison methods on 76 | `Version` instances and the other uses `Constraints`. There are some important 77 | differences to notes between these two methods of comparison. 78 | 79 | 1. When two versions are compared using functions such as `Compare`, `LessThan`, 80 | and others it will follow the specification and always include pre-releases 81 | within the comparison. It will provide an answer that is valid with the 82 | comparison section of the spec at https://semver.org/#spec-item-11 83 | 2. When constraint checking is used for checks or validation it will follow a 84 | different set of rules that are common for ranges with tools like npm/js 85 | and Rust/Cargo. This includes considering pre-releases to be invalid if the 86 | ranges does not include one. If you want to have it include pre-releases a 87 | simple solution is to include `-0` in your range. 88 | 3. Constraint ranges can have some complex rules including the shorthand use of 89 | ~ and ^. For more details on those see the options below. 90 | 91 | There are differences between the two methods or checking versions because the 92 | comparison methods on `Version` follow the specification while comparison ranges 93 | are not part of the specification. Different packages and tools have taken it 94 | upon themselves to come up with range rules. This has resulted in differences. 95 | For example, npm/js and Cargo/Rust follow similar patterns while PHP has a 96 | different pattern for ^. The comparison features in this package follow the 97 | npm/js and Cargo/Rust lead because applications using it have followed similar 98 | patters with their versions. 99 | 100 | Checking a version against version constraints is one of the most featureful 101 | parts of the package. 102 | 103 | ```go 104 | c, err := semver.NewConstraint(">= 1.2.3") 105 | if err != nil { 106 | // Handle constraint not being parsable. 107 | } 108 | 109 | v, err := semver.NewVersion("1.3") 110 | if err != nil { 111 | // Handle version not being parsable. 112 | } 113 | // Check if the version meets the constraints. The variable a will be true. 114 | a := c.Check(v) 115 | ``` 116 | 117 | ### Basic Comparisons 118 | 119 | There are two elements to the comparisons. First, a comparison string is a list 120 | of space or comma separated AND comparisons. These are then separated by || (OR) 121 | comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a 122 | comparison that's greater than or equal to 1.2 and less than 3.0.0 or is 123 | greater than or equal to 4.2.3. 124 | 125 | The basic comparisons are: 126 | 127 | * `=`: equal (aliased to no operator) 128 | * `!=`: not equal 129 | * `>`: greater than 130 | * `<`: less than 131 | * `>=`: greater than or equal to 132 | * `<=`: less than or equal to 133 | 134 | ### Working With Prerelease Versions 135 | 136 | Pre-releases, for those not familiar with them, are used for software releases 137 | prior to stable or generally available releases. Examples of pre-releases include 138 | development, alpha, beta, and release candidate releases. A pre-release may be 139 | a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the 140 | order of precedence, pre-releases come before their associated releases. In this 141 | example `1.2.3-beta.1 < 1.2.3`. 142 | 143 | According to the Semantic Version specification, pre-releases may not be 144 | API compliant with their release counterpart. It says, 145 | 146 | > A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. 147 | 148 | SemVer's comparisons using constraints without a pre-release comparator will skip 149 | pre-release versions. For example, `>=1.2.3` will skip pre-releases when looking 150 | at a list of releases while `>=1.2.3-0` will evaluate and find pre-releases. 151 | 152 | The reason for the `0` as a pre-release version in the example comparison is 153 | because pre-releases can only contain ASCII alphanumerics and hyphens (along with 154 | `.` separators), per the spec. Sorting happens in ASCII sort order, again per the 155 | spec. The lowest character is a `0` in ASCII sort order 156 | (see an [ASCII Table](http://www.asciitable.com/)) 157 | 158 | Understanding ASCII sort ordering is important because A-Z comes before a-z. That 159 | means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case 160 | sensitivity doesn't apply here. This is due to ASCII sort ordering which is what 161 | the spec specifies. 162 | 163 | ### Hyphen Range Comparisons 164 | 165 | There are multiple methods to handle ranges and the first is hyphens ranges. 166 | These look like: 167 | 168 | * `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` 169 | * `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` 170 | 171 | Note that `1.2-1.4.5` without whitespace is parsed completely differently; it's 172 | parsed as a single constraint `1.2.0` with _prerelease_ `1.4.5`. 173 | 174 | ### Wildcards In Comparisons 175 | 176 | The `x`, `X`, and `*` characters can be used as a wildcard character. This works 177 | for all comparison operators. When used on the `=` operator it falls 178 | back to the patch level comparison (see tilde below). For example, 179 | 180 | * `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` 181 | * `>= 1.2.x` is equivalent to `>= 1.2.0` 182 | * `<= 2.x` is equivalent to `< 3` 183 | * `*` is equivalent to `>= 0.0.0` 184 | 185 | ### Tilde Range Comparisons (Patch) 186 | 187 | The tilde (`~`) comparison operator is for patch level ranges when a minor 188 | version is specified and major level changes when the minor number is missing. 189 | For example, 190 | 191 | * `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` 192 | * `~1` is equivalent to `>= 1, < 2` 193 | * `~2.3` is equivalent to `>= 2.3, < 2.4` 194 | * `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` 195 | * `~1.x` is equivalent to `>= 1, < 2` 196 | 197 | ### Caret Range Comparisons (Major) 198 | 199 | The caret (`^`) comparison operator is for major level changes once a stable 200 | (1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts 201 | as the API stability level. This is useful when comparisons of API versions as a 202 | major change is API breaking. For example, 203 | 204 | * `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` 205 | * `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` 206 | * `^2.3` is equivalent to `>= 2.3, < 3` 207 | * `^2.x` is equivalent to `>= 2.0.0, < 3` 208 | * `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` 209 | * `^0.2` is equivalent to `>=0.2.0 <0.3.0` 210 | * `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` 211 | * `^0.0` is equivalent to `>=0.0.0 <0.1.0` 212 | * `^0` is equivalent to `>=0.0.0 <1.0.0` 213 | 214 | ## Validation 215 | 216 | In addition to testing a version against a constraint, a version can be validated 217 | against a constraint. When validation fails a slice of errors containing why a 218 | version didn't meet the constraint is returned. For example, 219 | 220 | ```go 221 | c, err := semver.NewConstraint("<= 1.2.3, >= 1.4") 222 | if err != nil { 223 | // Handle constraint not being parseable. 224 | } 225 | 226 | v, err := semver.NewVersion("1.3") 227 | if err != nil { 228 | // Handle version not being parseable. 229 | } 230 | 231 | // Validate a version against a constraint. 232 | a, msgs := c.Validate(v) 233 | // a is false 234 | for _, m := range msgs { 235 | fmt.Println(m) 236 | 237 | // Loops over the errors which would read 238 | // "1.3 is greater than 1.2.3" 239 | // "1.3 is less than 1.4" 240 | } 241 | ``` 242 | 243 | ## Contribute 244 | 245 | If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues) 246 | or [create a pull request](https://github.com/Masterminds/semver/pulls). 247 | 248 | ## Security 249 | 250 | Security is an important consideration for this project. The project currently 251 | uses the following tools to help discover security issues: 252 | 253 | * [CodeQL](https://github.com/Masterminds/semver) 254 | * [gosec](https://github.com/securego/gosec) 255 | * Daily Fuzz testing 256 | 257 | If you believe you have found a security vulnerability you can privately disclose 258 | it through the [GitHub security page](https://github.com/Masterminds/semver/security). 259 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of semver are currently supported: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.x | :white_check_mark: | 10 | | 2.x | :x: | 11 | | 1.x | :x: | 12 | 13 | Fixes are only released for the latest minor version in the form of a patch release. 14 | 15 | ## Reporting a Vulnerability 16 | 17 | You can privately disclose a vulnerability through GitHubs 18 | [private vulnerability reporting](https://github.com/Masterminds/semver/security/advisories) 19 | mechanism. 20 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | /* Constraint creation benchmarks */ 8 | 9 | func benchNewConstraint(c string, b *testing.B) { 10 | b.ReportAllocs() 11 | b.ResetTimer() 12 | for i := 0; i < b.N; i++ { 13 | _, _ = NewConstraint(c) 14 | } 15 | } 16 | 17 | func BenchmarkNewConstraintUnary(b *testing.B) { 18 | b.ReportAllocs() 19 | b.ResetTimer() 20 | benchNewConstraint("=2.0", b) 21 | } 22 | 23 | func BenchmarkNewConstraintTilde(b *testing.B) { 24 | b.ReportAllocs() 25 | b.ResetTimer() 26 | benchNewConstraint("~2.0.0", b) 27 | } 28 | 29 | func BenchmarkNewConstraintCaret(b *testing.B) { 30 | b.ReportAllocs() 31 | b.ResetTimer() 32 | benchNewConstraint("^2.0.0", b) 33 | } 34 | 35 | func BenchmarkNewConstraintWildcard(b *testing.B) { 36 | b.ReportAllocs() 37 | b.ResetTimer() 38 | benchNewConstraint("1.x", b) 39 | } 40 | 41 | func BenchmarkNewConstraintRange(b *testing.B) { 42 | b.ReportAllocs() 43 | b.ResetTimer() 44 | benchNewConstraint(">=2.1.x, <3.1.0", b) 45 | } 46 | 47 | func BenchmarkNewConstraintUnion(b *testing.B) { 48 | b.ReportAllocs() 49 | b.ResetTimer() 50 | benchNewConstraint("~2.0.0 || =3.1.0", b) 51 | } 52 | 53 | /* Check benchmarks */ 54 | 55 | func benchCheckVersion(c, v string, b *testing.B) { 56 | b.ReportAllocs() 57 | b.ResetTimer() 58 | version, _ := NewVersion(v) 59 | constraint, _ := NewConstraint(c) 60 | 61 | for i := 0; i < b.N; i++ { 62 | constraint.Check(version) 63 | } 64 | } 65 | 66 | func BenchmarkCheckVersionUnary(b *testing.B) { 67 | b.ReportAllocs() 68 | b.ResetTimer() 69 | benchCheckVersion("=2.0", "2.0.0", b) 70 | } 71 | 72 | func BenchmarkCheckVersionTilde(b *testing.B) { 73 | b.ReportAllocs() 74 | b.ResetTimer() 75 | benchCheckVersion("~2.0.0", "2.0.5", b) 76 | } 77 | 78 | func BenchmarkCheckVersionCaret(b *testing.B) { 79 | b.ReportAllocs() 80 | b.ResetTimer() 81 | benchCheckVersion("^2.0.0", "2.1.0", b) 82 | } 83 | 84 | func BenchmarkCheckVersionWildcard(b *testing.B) { 85 | b.ReportAllocs() 86 | b.ResetTimer() 87 | benchCheckVersion("1.x", "1.4.0", b) 88 | } 89 | 90 | func BenchmarkCheckVersionRange(b *testing.B) { 91 | b.ReportAllocs() 92 | b.ResetTimer() 93 | benchCheckVersion(">=2.1.x, <3.1.0", "2.4.5", b) 94 | } 95 | 96 | func BenchmarkCheckVersionUnion(b *testing.B) { 97 | b.ReportAllocs() 98 | b.ResetTimer() 99 | benchCheckVersion("~2.0.0 || =3.1.0", "3.1.0", b) 100 | } 101 | 102 | func benchValidateVersion(c, v string, b *testing.B) { 103 | b.ReportAllocs() 104 | b.ResetTimer() 105 | version, _ := NewVersion(v) 106 | constraint, _ := NewConstraint(c) 107 | 108 | for i := 0; i < b.N; i++ { 109 | constraint.Validate(version) 110 | } 111 | } 112 | 113 | /* Validate benchmarks, including fails */ 114 | 115 | func BenchmarkValidateVersionUnary(b *testing.B) { 116 | b.ReportAllocs() 117 | b.ResetTimer() 118 | benchValidateVersion("=2.0", "2.0.0", b) 119 | } 120 | 121 | func BenchmarkValidateVersionUnaryFail(b *testing.B) { 122 | b.ReportAllocs() 123 | b.ResetTimer() 124 | benchValidateVersion("=2.0", "2.0.1", b) 125 | } 126 | 127 | func BenchmarkValidateVersionTilde(b *testing.B) { 128 | b.ReportAllocs() 129 | b.ResetTimer() 130 | benchValidateVersion("~2.0.0", "2.0.5", b) 131 | } 132 | 133 | func BenchmarkValidateVersionTildeFail(b *testing.B) { 134 | b.ReportAllocs() 135 | b.ResetTimer() 136 | benchValidateVersion("~2.0.0", "1.0.5", b) 137 | } 138 | 139 | func BenchmarkValidateVersionCaret(b *testing.B) { 140 | b.ReportAllocs() 141 | b.ResetTimer() 142 | benchValidateVersion("^2.0.0", "2.1.0", b) 143 | } 144 | 145 | func BenchmarkValidateVersionCaretFail(b *testing.B) { 146 | b.ReportAllocs() 147 | b.ResetTimer() 148 | benchValidateVersion("^2.0.0", "4.1.0", b) 149 | } 150 | 151 | func BenchmarkValidateVersionWildcard(b *testing.B) { 152 | b.ReportAllocs() 153 | b.ResetTimer() 154 | benchValidateVersion("1.x", "1.4.0", b) 155 | } 156 | 157 | func BenchmarkValidateVersionWildcardFail(b *testing.B) { 158 | b.ReportAllocs() 159 | b.ResetTimer() 160 | benchValidateVersion("1.x", "2.4.0", b) 161 | } 162 | 163 | func BenchmarkValidateVersionRange(b *testing.B) { 164 | b.ReportAllocs() 165 | b.ResetTimer() 166 | benchValidateVersion(">=2.1.x, <3.1.0", "2.4.5", b) 167 | } 168 | 169 | func BenchmarkValidateVersionRangeFail(b *testing.B) { 170 | b.ReportAllocs() 171 | b.ResetTimer() 172 | benchValidateVersion(">=2.1.x, <3.1.0", "1.4.5", b) 173 | } 174 | 175 | func BenchmarkValidateVersionUnion(b *testing.B) { 176 | b.ReportAllocs() 177 | b.ResetTimer() 178 | benchValidateVersion("~2.0.0 || =3.1.0", "3.1.0", b) 179 | } 180 | 181 | func BenchmarkValidateVersionUnionFail(b *testing.B) { 182 | b.ReportAllocs() 183 | b.ResetTimer() 184 | benchValidateVersion("~2.0.0 || =3.1.0", "3.1.1", b) 185 | } 186 | 187 | /* Version creation benchmarks */ 188 | 189 | func benchNewVersion(v string, b *testing.B) { 190 | for i := 0; i < b.N; i++ { 191 | _, _ = NewVersion(v) 192 | } 193 | } 194 | 195 | func benchStrictNewVersion(v string, b *testing.B) { 196 | for i := 0; i < b.N; i++ { 197 | _, _ = StrictNewVersion(v) 198 | } 199 | } 200 | 201 | func BenchmarkNewVersionSimple(b *testing.B) { 202 | b.ReportAllocs() 203 | b.ResetTimer() 204 | benchNewVersion("1.0.0", b) 205 | } 206 | 207 | func BenchmarkCoerceNewVersionSimple(b *testing.B) { 208 | b.ReportAllocs() 209 | b.ResetTimer() 210 | benchStrictNewVersion("1.0.0", b) 211 | } 212 | 213 | func BenchmarkNewVersionPre(b *testing.B) { 214 | b.ReportAllocs() 215 | b.ResetTimer() 216 | benchNewVersion("1.0.0-alpha", b) 217 | } 218 | 219 | func BenchmarkStrictNewVersionPre(b *testing.B) { 220 | b.ReportAllocs() 221 | b.ResetTimer() 222 | benchStrictNewVersion("1.0.0-alpha", b) 223 | } 224 | 225 | func BenchmarkNewVersionMeta(b *testing.B) { 226 | b.ReportAllocs() 227 | b.ResetTimer() 228 | benchNewVersion("1.0.0+metadata", b) 229 | } 230 | 231 | func BenchmarkStrictNewVersionMeta(b *testing.B) { 232 | b.ReportAllocs() 233 | b.ResetTimer() 234 | benchStrictNewVersion("1.0.0+metadata", b) 235 | } 236 | 237 | func BenchmarkNewVersionMetaDash(b *testing.B) { 238 | b.ReportAllocs() 239 | b.ResetTimer() 240 | benchNewVersion("1.0.0-alpha.1+meta.data", b) 241 | } 242 | 243 | func BenchmarkStrictNewVersionMetaDash(b *testing.B) { 244 | b.ReportAllocs() 245 | b.ResetTimer() 246 | benchStrictNewVersion("1.0.0-alpha.1+meta.data", b) 247 | } 248 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | // Collection is a collection of Version instances and implements the sort 4 | // interface. See the sort package for more details. 5 | // https://golang.org/pkg/sort/ 6 | type Collection []*Version 7 | 8 | // Len returns the length of a collection. The number of Version instances 9 | // on the slice. 10 | func (c Collection) Len() int { 11 | return len(c) 12 | } 13 | 14 | // Less is needed for the sort interface to compare two Version objects on the 15 | // slice. If checks if one is less than the other. 16 | func (c Collection) Less(i, j int) bool { 17 | return c[i].LessThan(c[j]) 18 | } 19 | 20 | // Swap is needed for the sort interface to replace the Version objects 21 | // at two different positions in the slice. 22 | func (c Collection) Swap(i, j int) { 23 | c[i], c[j] = c[j], c[i] 24 | } 25 | -------------------------------------------------------------------------------- /collection_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestCollection(t *testing.T) { 10 | raw := []string{ 11 | "1.2.3", 12 | "1.0", 13 | "1.3", 14 | "2", 15 | "0.4.2", 16 | } 17 | 18 | vs := make([]*Version, len(raw)) 19 | for i, r := range raw { 20 | v, err := NewVersion(r) 21 | if err != nil { 22 | t.Errorf("Error parsing version: %s", err) 23 | } 24 | 25 | vs[i] = v 26 | } 27 | 28 | sort.Sort(Collection(vs)) 29 | 30 | e := []string{ 31 | "0.4.2", 32 | "1.0.0", 33 | "1.2.3", 34 | "1.3.0", 35 | "2.0.0", 36 | } 37 | 38 | a := make([]string, len(vs)) 39 | for i, v := range vs { 40 | a[i] = v.String() 41 | } 42 | 43 | if !reflect.DeepEqual(a, e) { 44 | t.Error("Sorting Collection failed") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /constraints.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // Constraints is one or more constraint that a semantic version can be 12 | // checked against. 13 | type Constraints struct { 14 | constraints [][]*constraint 15 | } 16 | 17 | // NewConstraint returns a Constraints instance that a Version instance can 18 | // be checked against. If there is a parse error it will be returned. 19 | func NewConstraint(c string) (*Constraints, error) { 20 | 21 | // Rewrite - ranges into a comparison operation. 22 | c = rewriteRange(c) 23 | 24 | ors := strings.Split(c, "||") 25 | or := make([][]*constraint, len(ors)) 26 | for k, v := range ors { 27 | 28 | // TODO: Find a way to validate and fetch all the constraints in a simpler form 29 | 30 | // Validate the segment 31 | if !validConstraintRegex.MatchString(v) { 32 | return nil, fmt.Errorf("improper constraint: %s", v) 33 | } 34 | 35 | cs := findConstraintRegex.FindAllString(v, -1) 36 | if cs == nil { 37 | cs = append(cs, v) 38 | } 39 | result := make([]*constraint, len(cs)) 40 | for i, s := range cs { 41 | pc, err := parseConstraint(s) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | result[i] = pc 47 | } 48 | or[k] = result 49 | } 50 | 51 | o := &Constraints{constraints: or} 52 | return o, nil 53 | } 54 | 55 | // Check tests if a version satisfies the constraints. 56 | func (cs Constraints) Check(v *Version) bool { 57 | // TODO(mattfarina): For v4 of this library consolidate the Check and Validate 58 | // functions as the underlying functions make that possible now. 59 | // loop over the ORs and check the inner ANDs 60 | for _, o := range cs.constraints { 61 | joy := true 62 | for _, c := range o { 63 | if check, _ := c.check(v); !check { 64 | joy = false 65 | break 66 | } 67 | } 68 | 69 | if joy { 70 | return true 71 | } 72 | } 73 | 74 | return false 75 | } 76 | 77 | // Validate checks if a version satisfies a constraint. If not a slice of 78 | // reasons for the failure are returned in addition to a bool. 79 | func (cs Constraints) Validate(v *Version) (bool, []error) { 80 | // loop over the ORs and check the inner ANDs 81 | var e []error 82 | 83 | // Capture the prerelease message only once. When it happens the first time 84 | // this var is marked 85 | var prerelesase bool 86 | for _, o := range cs.constraints { 87 | joy := true 88 | for _, c := range o { 89 | // Before running the check handle the case there the version is 90 | // a prerelease and the check is not searching for prereleases. 91 | if c.con.pre == "" && v.pre != "" { 92 | if !prerelesase { 93 | em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 94 | e = append(e, em) 95 | prerelesase = true 96 | } 97 | joy = false 98 | 99 | } else { 100 | 101 | if _, err := c.check(v); err != nil { 102 | e = append(e, err) 103 | joy = false 104 | } 105 | } 106 | } 107 | 108 | if joy { 109 | return true, []error{} 110 | } 111 | } 112 | 113 | return false, e 114 | } 115 | 116 | func (cs Constraints) String() string { 117 | buf := make([]string, len(cs.constraints)) 118 | var tmp bytes.Buffer 119 | 120 | for k, v := range cs.constraints { 121 | tmp.Reset() 122 | vlen := len(v) 123 | for kk, c := range v { 124 | tmp.WriteString(c.string()) 125 | 126 | // Space separate the AND conditions 127 | if vlen > 1 && kk < vlen-1 { 128 | tmp.WriteString(" ") 129 | } 130 | } 131 | buf[k] = tmp.String() 132 | } 133 | 134 | return strings.Join(buf, " || ") 135 | } 136 | 137 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 138 | func (cs *Constraints) UnmarshalText(text []byte) error { 139 | temp, err := NewConstraint(string(text)) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | *cs = *temp 145 | 146 | return nil 147 | } 148 | 149 | // MarshalText implements the encoding.TextMarshaler interface. 150 | func (cs Constraints) MarshalText() ([]byte, error) { 151 | return []byte(cs.String()), nil 152 | } 153 | 154 | var constraintOps map[string]cfunc 155 | var constraintRegex *regexp.Regexp 156 | var constraintRangeRegex *regexp.Regexp 157 | 158 | // Used to find individual constraints within a multi-constraint string 159 | var findConstraintRegex *regexp.Regexp 160 | 161 | // Used to validate an segment of ANDs is valid 162 | var validConstraintRegex *regexp.Regexp 163 | 164 | const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` + 165 | `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + 166 | `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` 167 | 168 | func init() { 169 | constraintOps = map[string]cfunc{ 170 | "": constraintTildeOrEqual, 171 | "=": constraintTildeOrEqual, 172 | "!=": constraintNotEqual, 173 | ">": constraintGreaterThan, 174 | "<": constraintLessThan, 175 | ">=": constraintGreaterThanEqual, 176 | "=>": constraintGreaterThanEqual, 177 | "<=": constraintLessThanEqual, 178 | "=<": constraintLessThanEqual, 179 | "~": constraintTilde, 180 | "~>": constraintTilde, 181 | "^": constraintCaret, 182 | } 183 | 184 | ops := `=||!=|>|<|>=|=>|<=|=<|~|~>|\^` 185 | 186 | constraintRegex = regexp.MustCompile(fmt.Sprintf( 187 | `^\s*(%s)\s*(%s)\s*$`, 188 | ops, 189 | cvRegex)) 190 | 191 | constraintRangeRegex = regexp.MustCompile(fmt.Sprintf( 192 | `\s*(%s)\s+-\s+(%s)\s*`, 193 | cvRegex, cvRegex)) 194 | 195 | findConstraintRegex = regexp.MustCompile(fmt.Sprintf( 196 | `(%s)\s*(%s)`, 197 | ops, 198 | cvRegex)) 199 | 200 | // The first time a constraint shows up will look slightly different from 201 | // future times it shows up due to a leading space or comma in a given 202 | // string. 203 | validConstraintRegex = regexp.MustCompile(fmt.Sprintf( 204 | `^(\s*(%s)\s*(%s)\s*)((?:\s+|,\s*)(%s)\s*(%s)\s*)*$`, 205 | ops, 206 | cvRegex, 207 | ops, 208 | cvRegex)) 209 | } 210 | 211 | // An individual constraint 212 | type constraint struct { 213 | // The version used in the constraint check. For example, if a constraint 214 | // is '<= 2.0.0' the con a version instance representing 2.0.0. 215 | con *Version 216 | 217 | // The original parsed version (e.g., 4.x from != 4.x) 218 | orig string 219 | 220 | // The original operator for the constraint 221 | origfunc string 222 | 223 | // When an x is used as part of the version (e.g., 1.x) 224 | minorDirty bool 225 | dirty bool 226 | patchDirty bool 227 | } 228 | 229 | // Check if a version meets the constraint 230 | func (c *constraint) check(v *Version) (bool, error) { 231 | return constraintOps[c.origfunc](v, c) 232 | } 233 | 234 | // String prints an individual constraint into a string 235 | func (c *constraint) string() string { 236 | return c.origfunc + c.orig 237 | } 238 | 239 | type cfunc func(v *Version, c *constraint) (bool, error) 240 | 241 | func parseConstraint(c string) (*constraint, error) { 242 | if len(c) > 0 { 243 | m := constraintRegex.FindStringSubmatch(c) 244 | if m == nil { 245 | return nil, fmt.Errorf("improper constraint: %s", c) 246 | } 247 | 248 | cs := &constraint{ 249 | orig: m[2], 250 | origfunc: m[1], 251 | } 252 | 253 | ver := m[2] 254 | minorDirty := false 255 | patchDirty := false 256 | dirty := false 257 | if isX(m[3]) || m[3] == "" { 258 | ver = fmt.Sprintf("0.0.0%s", m[6]) 259 | dirty = true 260 | } else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" { 261 | minorDirty = true 262 | dirty = true 263 | ver = fmt.Sprintf("%s.0.0%s", m[3], m[6]) 264 | } else if isX(strings.TrimPrefix(m[5], ".")) || m[5] == "" { 265 | dirty = true 266 | patchDirty = true 267 | ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6]) 268 | } 269 | 270 | con, err := NewVersion(ver) 271 | if err != nil { 272 | 273 | // The constraintRegex should catch any regex parsing errors. So, 274 | // we should never get here. 275 | return nil, errors.New("constraint Parser Error") 276 | } 277 | 278 | cs.con = con 279 | cs.minorDirty = minorDirty 280 | cs.patchDirty = patchDirty 281 | cs.dirty = dirty 282 | 283 | return cs, nil 284 | } 285 | 286 | // The rest is the special case where an empty string was passed in which 287 | // is equivalent to * or >=0.0.0 288 | con, err := StrictNewVersion("0.0.0") 289 | if err != nil { 290 | 291 | // The constraintRegex should catch any regex parsing errors. So, 292 | // we should never get here. 293 | return nil, errors.New("constraint Parser Error") 294 | } 295 | 296 | cs := &constraint{ 297 | con: con, 298 | orig: c, 299 | origfunc: "", 300 | minorDirty: false, 301 | patchDirty: false, 302 | dirty: true, 303 | } 304 | return cs, nil 305 | } 306 | 307 | // Constraint functions 308 | func constraintNotEqual(v *Version, c *constraint) (bool, error) { 309 | if c.dirty { 310 | 311 | // If there is a pre-release on the version but the constraint isn't looking 312 | // for them assume that pre-releases are not compatible. See issue 21 for 313 | // more details. 314 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 315 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 316 | } 317 | 318 | if c.con.Major() != v.Major() { 319 | return true, nil 320 | } 321 | if c.con.Minor() != v.Minor() && !c.minorDirty { 322 | return true, nil 323 | } else if c.minorDirty { 324 | return false, fmt.Errorf("%s is equal to %s", v, c.orig) 325 | } else if c.con.Patch() != v.Patch() && !c.patchDirty { 326 | return true, nil 327 | } else if c.patchDirty { 328 | // Need to handle prereleases if present 329 | if v.Prerelease() != "" || c.con.Prerelease() != "" { 330 | eq := comparePrerelease(v.Prerelease(), c.con.Prerelease()) != 0 331 | if eq { 332 | return true, nil 333 | } 334 | return false, fmt.Errorf("%s is equal to %s", v, c.orig) 335 | } 336 | return false, fmt.Errorf("%s is equal to %s", v, c.orig) 337 | } 338 | } 339 | 340 | eq := v.Equal(c.con) 341 | if eq { 342 | return false, fmt.Errorf("%s is equal to %s", v, c.orig) 343 | } 344 | 345 | return true, nil 346 | } 347 | 348 | func constraintGreaterThan(v *Version, c *constraint) (bool, error) { 349 | 350 | // If there is a pre-release on the version but the constraint isn't looking 351 | // for them assume that pre-releases are not compatible. See issue 21 for 352 | // more details. 353 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 354 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 355 | } 356 | 357 | var eq bool 358 | 359 | if !c.dirty { 360 | eq = v.Compare(c.con) == 1 361 | if eq { 362 | return true, nil 363 | } 364 | return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) 365 | } 366 | 367 | if v.Major() > c.con.Major() { 368 | return true, nil 369 | } else if v.Major() < c.con.Major() { 370 | return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) 371 | } else if c.minorDirty { 372 | // This is a range case such as >11. When the version is something like 373 | // 11.1.0 is it not > 11. For that we would need 12 or higher 374 | return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) 375 | } else if c.patchDirty { 376 | // This is for ranges such as >11.1. A version of 11.1.1 is not greater 377 | // which one of 11.2.1 is greater 378 | eq = v.Minor() > c.con.Minor() 379 | if eq { 380 | return true, nil 381 | } 382 | return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) 383 | } 384 | 385 | // If we have gotten here we are not comparing pre-preleases and can use the 386 | // Compare function to accomplish that. 387 | eq = v.Compare(c.con) == 1 388 | if eq { 389 | return true, nil 390 | } 391 | return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) 392 | } 393 | 394 | func constraintLessThan(v *Version, c *constraint) (bool, error) { 395 | // If there is a pre-release on the version but the constraint isn't looking 396 | // for them assume that pre-releases are not compatible. See issue 21 for 397 | // more details. 398 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 399 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 400 | } 401 | 402 | eq := v.Compare(c.con) < 0 403 | if eq { 404 | return true, nil 405 | } 406 | return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig) 407 | } 408 | 409 | func constraintGreaterThanEqual(v *Version, c *constraint) (bool, error) { 410 | 411 | // If there is a pre-release on the version but the constraint isn't looking 412 | // for them assume that pre-releases are not compatible. See issue 21 for 413 | // more details. 414 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 415 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 416 | } 417 | 418 | eq := v.Compare(c.con) >= 0 419 | if eq { 420 | return true, nil 421 | } 422 | return false, fmt.Errorf("%s is less than %s", v, c.orig) 423 | } 424 | 425 | func constraintLessThanEqual(v *Version, c *constraint) (bool, error) { 426 | // If there is a pre-release on the version but the constraint isn't looking 427 | // for them assume that pre-releases are not compatible. See issue 21 for 428 | // more details. 429 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 430 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 431 | } 432 | 433 | var eq bool 434 | 435 | if !c.dirty { 436 | eq = v.Compare(c.con) <= 0 437 | if eq { 438 | return true, nil 439 | } 440 | return false, fmt.Errorf("%s is greater than %s", v, c.orig) 441 | } 442 | 443 | if v.Major() > c.con.Major() { 444 | return false, fmt.Errorf("%s is greater than %s", v, c.orig) 445 | } else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty { 446 | return false, fmt.Errorf("%s is greater than %s", v, c.orig) 447 | } 448 | 449 | return true, nil 450 | } 451 | 452 | // ~*, ~>* --> >= 0.0.0 (any) 453 | // ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0 454 | // ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0 455 | // ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0 456 | // ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0 457 | // ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0 458 | func constraintTilde(v *Version, c *constraint) (bool, error) { 459 | // If there is a pre-release on the version but the constraint isn't looking 460 | // for them assume that pre-releases are not compatible. See issue 21 for 461 | // more details. 462 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 463 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 464 | } 465 | 466 | if v.LessThan(c.con) { 467 | return false, fmt.Errorf("%s is less than %s", v, c.orig) 468 | } 469 | 470 | // ~0.0.0 is a special case where all constraints are accepted. It's 471 | // equivalent to >= 0.0.0. 472 | if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 && 473 | !c.minorDirty && !c.patchDirty { 474 | return true, nil 475 | } 476 | 477 | if v.Major() != c.con.Major() { 478 | return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) 479 | } 480 | 481 | if v.Minor() != c.con.Minor() && !c.minorDirty { 482 | return false, fmt.Errorf("%s does not have same major and minor version as %s", v, c.orig) 483 | } 484 | 485 | return true, nil 486 | } 487 | 488 | // When there is a .x (dirty) status it automatically opts in to ~. Otherwise 489 | // it's a straight = 490 | func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) { 491 | // If there is a pre-release on the version but the constraint isn't looking 492 | // for them assume that pre-releases are not compatible. See issue 21 for 493 | // more details. 494 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 495 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 496 | } 497 | 498 | if c.dirty { 499 | return constraintTilde(v, c) 500 | } 501 | 502 | eq := v.Equal(c.con) 503 | if eq { 504 | return true, nil 505 | } 506 | 507 | return false, fmt.Errorf("%s is not equal to %s", v, c.orig) 508 | } 509 | 510 | // ^* --> (any) 511 | // ^1.2.3 --> >=1.2.3 <2.0.0 512 | // ^1.2 --> >=1.2.0 <2.0.0 513 | // ^1 --> >=1.0.0 <2.0.0 514 | // ^0.2.3 --> >=0.2.3 <0.3.0 515 | // ^0.2 --> >=0.2.0 <0.3.0 516 | // ^0.0.3 --> >=0.0.3 <0.0.4 517 | // ^0.0 --> >=0.0.0 <0.1.0 518 | // ^0 --> >=0.0.0 <1.0.0 519 | func constraintCaret(v *Version, c *constraint) (bool, error) { 520 | // If there is a pre-release on the version but the constraint isn't looking 521 | // for them assume that pre-releases are not compatible. See issue 21 for 522 | // more details. 523 | if v.Prerelease() != "" && c.con.Prerelease() == "" { 524 | return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 525 | } 526 | 527 | // This less than handles prereleases 528 | if v.LessThan(c.con) { 529 | return false, fmt.Errorf("%s is less than %s", v, c.orig) 530 | } 531 | 532 | var eq bool 533 | 534 | // ^ when the major > 0 is >=x.y.z < x+1 535 | if c.con.Major() > 0 || c.minorDirty { 536 | 537 | // ^ has to be within a major range for > 0. Everything less than was 538 | // filtered out with the LessThan call above. This filters out those 539 | // that greater but not within the same major range. 540 | eq = v.Major() == c.con.Major() 541 | if eq { 542 | return true, nil 543 | } 544 | return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) 545 | } 546 | 547 | // ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1 548 | if c.con.Major() == 0 && v.Major() > 0 { 549 | return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) 550 | } 551 | // If the con Minor is > 0 it is not dirty 552 | if c.con.Minor() > 0 || c.patchDirty { 553 | eq = v.Minor() == c.con.Minor() 554 | if eq { 555 | return true, nil 556 | } 557 | return false, fmt.Errorf("%s does not have same minor version as %s. Expected minor versions to match when constraint major version is 0", v, c.orig) 558 | } 559 | // ^ when the minor is 0 and minor > 0 is =0.0.z 560 | if c.con.Minor() == 0 && v.Minor() > 0 { 561 | return false, fmt.Errorf("%s does not have same minor version as %s", v, c.orig) 562 | } 563 | 564 | // At this point the major is 0 and the minor is 0 and not dirty. The patch 565 | // is not dirty so we need to check if they are equal. If they are not equal 566 | eq = c.con.Patch() == v.Patch() 567 | if eq { 568 | return true, nil 569 | } 570 | return false, fmt.Errorf("%s does not equal %s. Expect version and constraint to equal when major and minor versions are 0", v, c.orig) 571 | } 572 | 573 | func isX(x string) bool { 574 | switch x { 575 | case "x", "*", "X": 576 | return true 577 | default: 578 | return false 579 | } 580 | } 581 | 582 | func rewriteRange(i string) string { 583 | m := constraintRangeRegex.FindAllStringSubmatch(i, -1) 584 | if m == nil { 585 | return i 586 | } 587 | o := i 588 | for _, v := range m { 589 | t := fmt.Sprintf(">= %s, <= %s ", v[1], v[11]) 590 | o = strings.Replace(o, v[0], t, 1) 591 | } 592 | 593 | return o 594 | } 595 | -------------------------------------------------------------------------------- /constraints_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestParseConstraint(t *testing.T) { 12 | tests := []struct { 13 | in string 14 | f cfunc 15 | v string 16 | err bool 17 | }{ 18 | {">= 1.2", constraintGreaterThanEqual, "1.2.0", false}, 19 | {"1.0", constraintTildeOrEqual, "1.0.0", false}, 20 | {"foo", nil, "", true}, 21 | {"<= 1.2", constraintLessThanEqual, "1.2.0", false}, 22 | {"=< 1.2", constraintLessThanEqual, "1.2.0", false}, 23 | {"=> 1.2", constraintGreaterThanEqual, "1.2.0", false}, 24 | {"v1.2", constraintTildeOrEqual, "1.2.0", false}, 25 | {"=1.5", constraintTildeOrEqual, "1.5.0", false}, 26 | {"> 1.3", constraintGreaterThan, "1.3.0", false}, 27 | {"< 1.4.1", constraintLessThan, "1.4.1", false}, 28 | {"< 40.50.10", constraintLessThan, "40.50.10", false}, 29 | } 30 | 31 | for _, tc := range tests { 32 | c, err := parseConstraint(tc.in) 33 | if tc.err && err == nil { 34 | t.Errorf("Expected error for %s didn't occur", tc.in) 35 | } else if !tc.err && err != nil { 36 | t.Errorf("Unexpected error for %s", tc.in) 37 | } 38 | 39 | // If an error was expected continue the loop and don't try the other 40 | // tests as they will cause errors. 41 | if tc.err { 42 | continue 43 | } 44 | 45 | if tc.v != c.con.String() { 46 | t.Errorf("Incorrect version found on %s", tc.in) 47 | } 48 | 49 | f1 := reflect.ValueOf(tc.f) 50 | f2 := reflect.ValueOf(constraintOps[c.origfunc]) 51 | if f1 != f2 { 52 | t.Errorf("Wrong constraint found for %s", tc.in) 53 | } 54 | } 55 | } 56 | 57 | func TestConstraintCheck(t *testing.T) { 58 | tests := []struct { 59 | constraint string 60 | version string 61 | check bool 62 | }{ 63 | {"=2.0.0", "1.2.3", false}, 64 | {"=2.0.0", "2.0.0", true}, 65 | {"=2.0", "1.2.3", false}, 66 | {"=2.0", "2.0.0", true}, 67 | {"=2.0", "2.0.1", true}, 68 | {"4.1", "4.1.0", true}, 69 | {"!=4.1.0", "4.1.0", false}, 70 | {"!=4.1.0", "4.1.1", true}, 71 | {"!=4.1", "4.1.0", false}, 72 | {"!=4.1", "4.1.1", false}, 73 | {"!=4.1", "5.1.0-alpha.1", false}, 74 | {"!=4.1-alpha", "4.1.0", true}, 75 | {"!=4.1", "5.1.0", true}, 76 | {"<11", "0.1.0", true}, 77 | {"<11", "11.1.0", false}, 78 | {"<1.1", "0.1.0", true}, 79 | {"<1.1", "1.1.0", false}, 80 | {"<1.1", "1.1.1", false}, 81 | {"<=11", "1.2.3", true}, 82 | {"<=11", "12.2.3", false}, 83 | {"<=11", "11.2.3", true}, 84 | {"<=1.1", "1.2.3", false}, 85 | {"<=1.1", "0.1.0", true}, 86 | {"<=1.1", "1.1.0", true}, 87 | {"<=1.1", "1.1.1", true}, 88 | {">1.1", "4.1.0", true}, 89 | {">1.1", "1.1.0", false}, 90 | {">0", "0", false}, 91 | {">0", "1", true}, 92 | {">0", "0.0.1-alpha", false}, 93 | {">0.0", "0.0.1-alpha", false}, 94 | {">0-0", "0.0.1-alpha", false}, 95 | {">0.0-0", "0.0.1-alpha", false}, 96 | {">0", "0.0.0-alpha", false}, 97 | {">0-0", "0.0.0-alpha", false}, 98 | {">0.0.0-0", "0.0.0-alpha", true}, 99 | {">1.2.3-alpha.1", "1.2.3-alpha.2", true}, 100 | {">1.2.3-alpha.1", "1.3.3-alpha.2", true}, 101 | {">11", "11.1.0", false}, 102 | {">11.1", "11.1.0", false}, 103 | {">11.1", "11.1.1", false}, 104 | {">11.1", "11.2.1", true}, 105 | {">=11", "11.1.2", true}, 106 | {">=11.1", "11.1.2", true}, 107 | {">=11.1", "11.0.2", false}, 108 | {">=1.1", "4.1.0", true}, 109 | {">=1.1", "1.1.0", true}, 110 | {">=1.1", "0.0.9", false}, 111 | {">=0", "0.0.1-alpha", false}, 112 | {">=0.0", "0.0.1-alpha", false}, 113 | {">=0-0", "0.0.1-alpha", true}, 114 | {">=0.0-0", "0.0.1-alpha", true}, 115 | {">=0", "0.0.0-alpha", false}, 116 | {">=0-0", "0.0.0-alpha", true}, 117 | {">=0.0.0-0", "0.0.0-alpha", true}, 118 | {">=0.0.0-0", "1.2.3", true}, 119 | {">=0.0.0-0", "3.4.5-beta.1", true}, 120 | {"<0", "0.0.0-alpha", false}, 121 | {"<0-z", "0.0.0-alpha", true}, 122 | {">=0", "0", true}, 123 | {"=0", "1", false}, 124 | {"*", "1", true}, 125 | {"*", "4.5.6", true}, 126 | {"*", "1.2.3-alpha.1", false}, 127 | {"*-0", "1.2.3-alpha.1", true}, 128 | {"2.*", "1", false}, 129 | {"2.*", "3.4.5", false}, 130 | {"2.*", "2.1.1", true}, 131 | {"2.1.*", "2.1.1", true}, 132 | {"2.1.*", "2.2.1", false}, 133 | {"", "1", true}, // An empty string is treated as * or wild card 134 | {"", "4.5.6", true}, 135 | {"", "1.2.3-alpha.1", false}, 136 | {"2", "1", false}, 137 | {"2", "3.4.5", false}, 138 | {"2", "2.1.1", true}, 139 | {"2.1", "2.1.1", true}, 140 | {"2.1", "2.2.1", false}, 141 | {"~1.2.3", "1.2.4", true}, 142 | {"~1.2.3", "1.3.4", false}, 143 | {"~1.2", "1.2.4", true}, 144 | {"~1.2", "1.3.4", false}, 145 | {"~1", "1.2.4", true}, 146 | {"~1", "2.3.4", false}, 147 | {"~0.2.3", "0.2.5", true}, 148 | {"~0.2.3", "0.3.5", false}, 149 | {"~1.2.3-beta.2", "1.2.3-beta.4", true}, 150 | 151 | // This next test is a case that is different from npm/js semver handling. 152 | // Their prereleases are only range scoped to patch releases. This is 153 | // technically not following semver as docs note. In our case we are 154 | // following semver. 155 | {"~1.2.3-beta.2", "1.2.4-beta.2", true}, 156 | {"~1.2.3-beta.2", "1.3.4-beta.2", false}, 157 | {"^1.2.3", "1.8.9", true}, 158 | {"^1.2.3", "2.8.9", false}, 159 | {"^1.2.3", "1.2.1", false}, 160 | {"^1.1.0", "2.1.0", false}, 161 | {"^1.2.0", "2.2.1", false}, 162 | {"^1.2.0", "1.2.1-alpha.1", false}, 163 | {"^1.2.0-alpha.0", "1.2.1-alpha.1", true}, 164 | {"^1.2.0-alpha.0", "1.2.1-alpha.0", true}, 165 | {"^1.2.0-alpha.2", "1.2.0-alpha.1", false}, 166 | {"^1.2", "1.8.9", true}, 167 | {"^1.2", "2.8.9", false}, 168 | {"^1", "1.8.9", true}, 169 | {"^1", "2.8.9", false}, 170 | {"^0.2.3", "0.2.5", true}, 171 | {"^0.2.3", "0.5.6", false}, 172 | {"^0.2", "0.2.5", true}, 173 | {"^0.2", "0.5.6", false}, 174 | {"^0.0.3", "0.0.3", true}, 175 | {"^0.0.3", "0.0.4", false}, 176 | {"^0.0", "0.0.3", true}, 177 | {"^0.0", "0.1.4", false}, 178 | {"^0.0", "1.0.4", false}, 179 | {"^0", "0.2.3", true}, 180 | {"^0", "1.1.4", false}, 181 | {"^0.2.3-beta.2", "0.2.3-beta.4", true}, 182 | 183 | // This next test is a case that is different from npm/js semver handling. 184 | // Their prereleases are only range scoped to patch releases. This is 185 | // technically not following semver as docs note. In our case we are 186 | // following semver. 187 | {"^0.2.3-beta.2", "0.2.4-beta.2", true}, 188 | {"^0.2.3-beta.2", "0.3.4-beta.2", false}, 189 | {"^0.2.3-beta.2", "0.2.3-beta.2", true}, 190 | } 191 | 192 | for _, tc := range tests { 193 | c, err := parseConstraint(tc.constraint) 194 | if err != nil { 195 | t.Errorf("err: %s", err) 196 | continue 197 | } 198 | 199 | v, err := NewVersion(tc.version) 200 | if err != nil { 201 | t.Errorf("err: %s", err) 202 | continue 203 | } 204 | 205 | a, _ := c.check(v) 206 | if a != tc.check { 207 | t.Errorf("Constraint %q failing with %q", tc.constraint, tc.version) 208 | } 209 | } 210 | } 211 | 212 | func TestNewConstraint(t *testing.T) { 213 | tests := []struct { 214 | input string 215 | ors int 216 | count int 217 | err bool 218 | }{ 219 | {">= 1.1", 1, 1, false}, 220 | {">40.50.60, < 50.70", 1, 2, false}, 221 | {"2.0", 1, 1, false}, 222 | {"v2.3.5-20161202202307-sha.e8fc5e5", 1, 1, false}, 223 | {">= bar", 0, 0, true}, 224 | {"BAR >= 1.2.3", 0, 0, true}, 225 | 226 | // Test with space separated AND 227 | 228 | {">= 1.2.3 < 2.0", 1, 2, false}, 229 | {">= 1.2.3 < 2.0 || => 3.0 < 4", 2, 2, false}, 230 | 231 | // Test with commas separating AND 232 | {">= 1.2.3, < 2.0", 1, 2, false}, 233 | {">= 1.2.3, < 2.0 || => 3.0, < 4", 2, 2, false}, 234 | 235 | // The 3 - 4 should be broken into 2 by the range rewriting 236 | {"3 - 4 || => 3.0, < 4", 2, 2, false}, 237 | 238 | // Due to having 4 parts these should produce an error. See 239 | // https://github.com/Masterminds/semver/issues/185 for the reason for 240 | // these tests. 241 | {"12.3.4.1234", 0, 0, true}, 242 | {"12.23.4.1234", 0, 0, true}, 243 | {"12.3.34.1234", 0, 0, true}, 244 | {"12.3.34 ~1.2.3", 1, 2, false}, 245 | {"12.3.34~ 1.2.3", 0, 0, true}, 246 | 247 | {"1.0.0 - 2.0.0, <=2.0.0", 1, 3, false}, 248 | } 249 | 250 | for _, tc := range tests { 251 | v, err := NewConstraint(tc.input) 252 | if tc.err && err == nil { 253 | t.Errorf("expected but did not get error for: %s", tc.input) 254 | continue 255 | } else if !tc.err && err != nil { 256 | t.Errorf("unexpectederror for input %s: %s", tc.input, err) 257 | continue 258 | } 259 | if tc.err { 260 | continue 261 | } 262 | 263 | l := len(v.constraints) 264 | if tc.ors != l { 265 | t.Errorf("Expected %s to have %d ORs but got %d", 266 | tc.input, tc.ors, l) 267 | } 268 | 269 | l = len(v.constraints[0]) 270 | if tc.count != l { 271 | t.Errorf("Expected %s to have %d constraints but got %d", 272 | tc.input, tc.count, l) 273 | } 274 | } 275 | } 276 | 277 | func TestConstraintsCheck(t *testing.T) { 278 | tests := []struct { 279 | constraint string 280 | version string 281 | check bool 282 | }{ 283 | {"*", "1.2.3", true}, 284 | {"~0.0.0", "1.2.3", true}, 285 | {"0.x.x", "1.2.3", false}, 286 | {"0.0.x", "1.2.3", false}, 287 | {"0.0.0", "1.2.3", false}, 288 | {"*", "1.2.3", true}, 289 | {"^0.0.0", "1.2.3", false}, 290 | {"= 2.0", "1.2.3", false}, 291 | {"= 2.0", "2.0.0", true}, 292 | {"4.1", "4.1.0", true}, 293 | {"4.1.x", "4.1.3", true}, 294 | {"1.x", "1.4", true}, 295 | {"!=4.1", "4.1.0", false}, 296 | {"!=4.1-alpha", "4.1.0-alpha", false}, 297 | {"!=4.1-alpha", "4.1.1-alpha", false}, 298 | {"!=4.1-alpha", "4.1.0", true}, 299 | {"!=4.1", "5.1.0", true}, 300 | {"!=4.x", "5.1.0", true}, 301 | {"!=4.x", "4.1.0", false}, 302 | {"!=4.1.x", "4.2.0", true}, 303 | {"!=4.2.x", "4.2.3", false}, 304 | {">1.1", "4.1.0", true}, 305 | {">1.1", "1.1.0", false}, 306 | {"<1.1", "0.1.0", true}, 307 | {"<1.1", "1.1.0", false}, 308 | {"<1.1", "1.1.1", false}, 309 | {"<1.x", "1.1.1", false}, 310 | {"<1.x", "0.1.1", true}, 311 | {"<1.x", "2.0.0", false}, 312 | {"<1.1.x", "1.2.1", false}, 313 | {"<1.1.x", "1.1.500", false}, 314 | {"<1.1.x", "1.0.500", true}, 315 | {"<1.2.x", "1.1.1", true}, 316 | {">=1.1", "4.1.0", true}, 317 | {">=1.1", "4.1.0-beta", false}, 318 | {">=1.1", "1.1.0", true}, 319 | {">=1.1", "0.0.9", false}, 320 | {"<=1.1", "0.1.0", true}, 321 | {"<=1.1", "0.1.0-alpha", false}, 322 | {"<=1.1-a", "0.1.0-alpha", true}, 323 | {"<=1.1", "1.1.0", true}, 324 | {"<=1.x", "1.1.0", true}, 325 | {"<=2.x", "3.0.0", false}, 326 | {"<=1.1", "1.1.1", true}, 327 | {"<=1.1.x", "1.2.500", false}, 328 | {"<=4.5", "3.4.0", true}, 329 | {"<=4.5", "3.7.0", true}, 330 | {"<=4.5", "4.6.3", false}, 331 | {">1.1, <2", "1.1.1", false}, 332 | {">1.1, <2", "1.2.1", true}, 333 | {">1.1, <3", "4.3.2", false}, 334 | {">=1.1, <2, !=1.2.3", "1.2.3", false}, 335 | {">1.1 <2", "1.1.1", false}, 336 | {">1.1 <2", "1.2.1", true}, 337 | {">1.1 <3", "4.3.2", false}, 338 | {">=1.1 <2 !=1.2.3", "1.2.3", false}, 339 | {">=1.1, <2, !=1.2.3 || > 3", "4.1.2", true}, 340 | {">=1.1, <2, !=1.2.3 || > 3", "3.1.2", false}, 341 | {">=1.1, <2, !=1.2.3 || >= 3", "3.0.0", true}, 342 | {">=1.1, <2, !=1.2.3 || > 3", "3.0.0", false}, 343 | {">=1.1, <2, !=1.2.3 || > 3", "1.2.3", false}, 344 | {">=1.1 <2 !=1.2.3", "1.2.3", false}, 345 | {">=1.1 <2 !=1.2.3 || > 3", "4.1.2", true}, 346 | {">=1.1 <2 !=1.2.3 || > 3", "3.1.2", false}, 347 | {">=1.1 <2 !=1.2.3 || >= 3", "3.0.0", true}, 348 | {">=1.1 <2 !=1.2.3 || > 3", "3.0.0", false}, 349 | {">=1.1 <2 !=1.2.3 || > 3", "1.2.3", false}, 350 | {"> 1.1, < 2", "1.1.1", false}, 351 | {"> 1.1, <2", "1.2.1", true}, 352 | {">1.1, < 3", "4.3.2", false}, 353 | {">= 1.1, < 2, !=1.2.3", "1.2.3", false}, 354 | {"> 1.1 < 2", "1.1.1", false}, 355 | {">1.1 < 2", "1.2.1", true}, 356 | {"> 1.1 <3", "4.3.2", false}, 357 | {">=1.1 < 2 != 1.2.3", "1.2.3", false}, 358 | {">= 1.1, <2, !=1.2.3 || > 3", "4.1.2", true}, 359 | {">= 1.1, <2, != 1.2.3 || > 3", "3.1.2", false}, 360 | {">= 1.1, <2, != 1.2.3 || >= 3", "3.0.0", true}, 361 | {">= 1.1, <2, !=1.2.3 || > 3", "3.0.0", false}, 362 | {">= 1.1, <2, !=1.2.3 || > 3", "1.2.3", false}, 363 | {">= 1.1 <2 != 1.2.3", "1.2.3", false}, 364 | {">= 1.1 <2 != 1.2.3 || > 3", "4.1.2", true}, 365 | {">= 1.1 <2 != 1.2.3 || > 3", "3.1.2", false}, 366 | {">= 1.1 <2 != 1.2.3 || >= 3", "3.0.0", true}, 367 | {">= 1.1 < 2 !=1.2.3 || > 3", "3.0.0", false}, 368 | {">=1.1 < 2 !=1.2.3 || > 3", "1.2.3", false}, 369 | {"1.1 - 2", "1.1.1", true}, 370 | {"1.5.0 - 4.5", "3.7.0", true}, 371 | {"1.1-3", "4.3.2", false}, 372 | {"^1.1", "1.1.1", true}, 373 | {"^1.1", "4.3.2", false}, 374 | {"^1.x", "1.1.1", true}, 375 | {"^2.x", "1.1.1", false}, 376 | {"^1.x", "2.1.1", false}, 377 | {"^1.x", "1.1.1-beta1", false}, 378 | {"^1.1.2-alpha", "1.2.1-beta1", true}, 379 | {"^1.2.x-alpha", "1.1.1-beta1", false}, 380 | {"^0.0.1", "0.0.1", true}, 381 | {"^0.0.1", "0.3.1", false}, 382 | {"~*", "2.1.1", true}, 383 | {"~1", "2.1.1", false}, 384 | {"~1", "1.3.5", true}, 385 | {"~1", "1.4", true}, 386 | {"~1.x", "2.1.1", false}, 387 | {"~1.x", "1.3.5", true}, 388 | {"~1.x", "1.4", true}, 389 | {"~1.1", "1.1.1", true}, 390 | {"~1.1", "1.1.1-alpha", false}, 391 | {"~1.1-alpha", "1.1.1-beta", true}, 392 | {"~1.1.1-beta", "1.1.1-alpha", false}, 393 | {"~1.1.1-beta", "1.1.1", true}, 394 | {"~1.2.3", "1.2.5", true}, 395 | {"~1.2.3", "1.2.2", false}, 396 | {"~1.2.3", "1.3.2", false}, 397 | {"~1.1", "1.2.3", false}, 398 | {"~1.3", "2.4.5", false}, 399 | 400 | // Ranges should work in conjunction with other constraints anded together. 401 | {"1.0.0 - 2.0.0 <=2.0.0", "1.5.0", true}, 402 | {"1.0.0 - 2.0.0, <=2.0.0", "1.5.0", true}, 403 | } 404 | 405 | for _, tc := range tests { 406 | c, err := NewConstraint(tc.constraint) 407 | if err != nil { 408 | t.Errorf("err: %s", err) 409 | continue 410 | } 411 | 412 | v, err := NewVersion(tc.version) 413 | if err != nil { 414 | t.Errorf("err: %s", err) 415 | continue 416 | } 417 | 418 | a := c.Check(v) 419 | if a != tc.check { 420 | t.Errorf("Constraint '%s' failing with '%s'", tc.constraint, tc.version) 421 | } 422 | } 423 | } 424 | 425 | func TestRewriteRange(t *testing.T) { 426 | tests := []struct { 427 | c string 428 | nc string 429 | }{ 430 | {"2 - 3", ">= 2, <= 3 "}, 431 | {"2 - 3, 2 - 3", ">= 2, <= 3 ,>= 2, <= 3 "}, 432 | {"2 - 3, 4.0.0 - 5.1", ">= 2, <= 3 ,>= 4.0.0, <= 5.1 "}, 433 | {"2 - 3 4.0.0 - 5.1", ">= 2, <= 3 >= 4.0.0, <= 5.1 "}, 434 | {"1.0.0 - 2.0.0 <=2.0.0", ">= 1.0.0, <= 2.0.0 <=2.0.0"}, 435 | } 436 | 437 | for _, tc := range tests { 438 | o := rewriteRange(tc.c) 439 | 440 | if o != tc.nc { 441 | t.Errorf("Range %s rewritten incorrectly as %q instead of expected %q", tc.c, o, tc.nc) 442 | } 443 | } 444 | } 445 | 446 | func TestIsX(t *testing.T) { 447 | tests := []struct { 448 | t string 449 | c bool 450 | }{ 451 | {"A", false}, 452 | {"%", false}, 453 | {"X", true}, 454 | {"x", true}, 455 | {"*", true}, 456 | } 457 | 458 | for _, tc := range tests { 459 | a := isX(tc.t) 460 | if a != tc.c { 461 | t.Errorf("Function isX error on %s", tc.t) 462 | } 463 | } 464 | } 465 | 466 | func TestConstraintsValidate(t *testing.T) { 467 | tests := []struct { 468 | constraint string 469 | version string 470 | check bool 471 | }{ 472 | {"*", "1.2.3", true}, 473 | {"~0.0.0", "1.2.3", true}, 474 | {"= 2.0", "1.2.3", false}, 475 | {"= 2.0", "2.0.0", true}, 476 | {"4.1", "4.1.0", true}, 477 | {"4.1.x", "4.1.3", true}, 478 | {"1.x", "1.4", true}, 479 | {"!=4.1", "4.1.0", false}, 480 | {"!=4.1", "5.1.0", true}, 481 | {"!=4.x", "5.1.0", true}, 482 | {"!=4.x", "4.1.0", false}, 483 | {"!=4.1.x", "4.2.0", true}, 484 | {"!=4.2.x", "4.2.3", false}, 485 | {">1.1", "4.1.0", true}, 486 | {">1.1", "1.1.0", false}, 487 | {"<1.1", "0.1.0", true}, 488 | {"<1.1", "1.1.0", false}, 489 | {"<1.1", "1.1.1", false}, 490 | {"<1.x", "1.1.1", false}, 491 | {"<2.x", "1.1.1", true}, 492 | {"<1.x", "2.1.1", false}, 493 | {"<1.1.x", "1.2.1", false}, 494 | {"<1.1.x", "1.1.500", false}, 495 | {"<1.2.x", "1.1.1", true}, 496 | {">=1.1", "4.1.0", true}, 497 | {">=1.1", "1.1.0", true}, 498 | {">=1.1", "0.0.9", false}, 499 | {"<=1.1", "0.1.0", true}, 500 | {"<=1.1", "1.1.0", true}, 501 | {"<=1.x", "1.1.0", true}, 502 | {"<=2.x", "3.1.0", false}, 503 | {"<=1.1", "1.1.1", true}, 504 | {"<=1.1.x", "1.2.500", false}, 505 | {">1.1, <2", "1.1.1", false}, 506 | {">1.1, <2", "1.2.1", true}, 507 | {">1.1, <3", "4.3.2", false}, 508 | {">=1.1, <2, !=1.2.3", "1.2.3", false}, 509 | {">=1.1, <2, !=1.2.3 || > 3", "3.1.2", false}, 510 | {">=1.1, <2, !=1.2.3 || > 3", "4.1.2", true}, 511 | {">=1.1, <2, !=1.2.3 || >= 3", "3.0.0", true}, 512 | {">=1.1, <2, !=1.2.3 || > 3", "3.0.0", false}, 513 | {">=1.1, <2, !=1.2.3 || > 3", "1.2.3", false}, 514 | {"1.1 - 2", "1.1.1", true}, 515 | {"1.1-3", "4.3.2", false}, 516 | {"^1.1", "1.1.1", true}, 517 | {"^1.1", "1.1.1-alpha", false}, 518 | {"^1.1.1-alpha", "1.1.1-beta", true}, 519 | {"^1.1.1-beta", "1.1.1-alpha", false}, 520 | {"^1.1", "4.3.2", false}, 521 | {"^1.x", "1.1.1", true}, 522 | {"^2.x", "1.1.1", false}, 523 | {"^1.x", "2.1.1", false}, 524 | {"^0.0.1", "0.1.3", false}, 525 | {"^0.0.1", "0.0.1", true}, 526 | {"~*", "2.1.1", true}, 527 | {"~1", "2.1.1", false}, 528 | {"~1", "1.3.5", true}, 529 | {"~1", "1.3.5-beta", false}, 530 | {"~1.x", "2.1.1", false}, 531 | {"~1.x", "1.3.5", true}, 532 | {"~1.x", "1.3.5-beta", false}, 533 | {"~1.3.6-alpha", "1.3.5-beta", false}, 534 | {"~1.3.5-alpha", "1.3.5-beta", true}, 535 | {"~1.3.5-beta", "1.3.5-alpha", false}, 536 | {"~1.x", "1.4", true}, 537 | {"~1.1", "1.1.1", true}, 538 | {"~1.2.3", "1.2.5", true}, 539 | {"~1.2.3", "1.2.2", false}, 540 | {"~1.2.3", "1.3.2", false}, 541 | {"~1.1", "1.2.3", false}, 542 | {"~1.3", "2.4.5", false}, 543 | } 544 | 545 | for _, tc := range tests { 546 | c, err := NewConstraint(tc.constraint) 547 | if err != nil { 548 | t.Errorf("err: %s", err) 549 | continue 550 | } 551 | 552 | v, err := NewVersion(tc.version) 553 | if err != nil { 554 | t.Errorf("err: %s", err) 555 | continue 556 | } 557 | 558 | a, msgs := c.Validate(v) 559 | if a != tc.check { 560 | t.Errorf("Constraint '%s' failing with '%s'", tc.constraint, tc.version) 561 | } else if !a && len(msgs) == 0 { 562 | t.Errorf("%q failed with %q but no errors returned", tc.constraint, tc.version) 563 | } 564 | 565 | // if a == false { 566 | // for _, m := range msgs { 567 | // t.Errorf("%s", m) 568 | // } 569 | // } 570 | } 571 | 572 | v, err := StrictNewVersion("1.2.3") 573 | if err != nil { 574 | t.Errorf("err: %s", err) 575 | } 576 | 577 | c, err := NewConstraint("!= 1.2.5, ^2, <= 1.1.x") 578 | if err != nil { 579 | t.Errorf("err: %s", err) 580 | } 581 | 582 | _, msgs := c.Validate(v) 583 | if len(msgs) != 2 { 584 | t.Error("Invalid number of validations found") 585 | } 586 | e := msgs[0].Error() 587 | if e != "1.2.3 is less than 2" { 588 | t.Error("Did not get expected message: 1.2.3 is less than 2") 589 | } 590 | e = msgs[1].Error() 591 | if e != "1.2.3 is greater than 1.1.x" { 592 | t.Error("Did not get expected message: 1.2.3 is greater than 1.1.x") 593 | } 594 | 595 | tests2 := []struct { 596 | constraint, version, msg string 597 | }{ 598 | {"2.x", "1.2.3", "1.2.3 is less than 2.x"}, 599 | {"2", "1.2.3", "1.2.3 is less than 2"}, 600 | {"= 2.0", "1.2.3", "1.2.3 is less than 2.0"}, 601 | {"!=4.1", "4.1.0", "4.1.0 is equal to 4.1"}, 602 | {"!=4.x", "4.1.0", "4.1.0 is equal to 4.x"}, 603 | {"!=4.2.x", "4.2.3", "4.2.3 is equal to 4.2.x"}, 604 | {">1.1", "1.1.0", "1.1.0 is less than or equal to 1.1"}, 605 | {"<1.1", "1.1.0", "1.1.0 is greater than or equal to 1.1"}, 606 | {"<1.1", "1.1.1", "1.1.1 is greater than or equal to 1.1"}, 607 | {"<1.x", "2.1.1", "2.1.1 is greater than or equal to 1.x"}, 608 | {"<1.1.x", "1.2.1", "1.2.1 is greater than or equal to 1.1.x"}, 609 | {">=1.1", "0.0.9", "0.0.9 is less than 1.1"}, 610 | {"<=2.x", "3.1.0", "3.1.0 is greater than 2.x"}, 611 | {"<=1.1", "1.2.1", "1.2.1 is greater than 1.1"}, 612 | {"<=1.1.x", "1.2.500", "1.2.500 is greater than 1.1.x"}, 613 | {">1.1, <3", "4.3.2", "4.3.2 is greater than or equal to 3"}, 614 | {">=1.1, <2, !=1.2.3", "1.2.3", "1.2.3 is equal to 1.2.3"}, 615 | {">=1.1, <2, !=1.2.3 || > 3", "3.0.0", "3.0.0 is greater than or equal to 2"}, 616 | {">=1.1, <2, !=1.2.3 || > 3", "1.2.3", "1.2.3 is equal to 1.2.3"}, 617 | {"1.1 - 3", "4.3.2", "4.3.2 is greater than 3"}, 618 | {"^1.1", "4.3.2", "4.3.2 does not have same major version as 1.1"}, 619 | {"^1.12.7", "1.6.6", "1.6.6 is less than 1.12.7"}, 620 | {"^2.x", "1.1.1", "1.1.1 is less than 2.x"}, 621 | {"^1.x", "2.1.1", "2.1.1 does not have same major version as 1.x"}, 622 | {"^0.2", "0.3.0", "0.3.0 does not have same minor version as 0.2. Expected minor versions to match when constraint major version is 0"}, 623 | {"^0.2", "0.1.1", "0.1.1 is less than 0.2"}, 624 | {"^0.0.3", "0.1.1", "0.1.1 does not have same minor version as 0.0.3"}, 625 | {"^0.0.3", "0.0.4", "0.0.4 does not equal 0.0.3. Expect version and constraint to equal when major and minor versions are 0"}, 626 | {"^0.0.3", "0.0.2", "0.0.2 is less than 0.0.3"}, 627 | {"~1", "2.1.2", "2.1.2 does not have same major version as 1"}, 628 | {"~1.x", "2.1.1", "2.1.1 does not have same major version as 1.x"}, 629 | {"~1.2.3", "1.2.2", "1.2.2 is less than 1.2.3"}, 630 | {"~1.2.3", "1.3.2", "1.3.2 does not have same major and minor version as 1.2.3"}, 631 | {"~1.1", "1.2.3", "1.2.3 does not have same major and minor version as 1.1"}, 632 | {"~1.3", "2.4.5", "2.4.5 does not have same major version as 1.3"}, 633 | {"> 1.2.3", "1.2.3-beta.1", "1.2.3-beta.1 is a prerelease version and the constraint is only looking for release versions"}, 634 | } 635 | 636 | for _, tc := range tests2 { 637 | c, err := NewConstraint(tc.constraint) 638 | if err != nil { 639 | t.Errorf("constraint parsing err: %s", err) 640 | continue 641 | } 642 | 643 | v, err := StrictNewVersion(tc.version) 644 | if err != nil { 645 | t.Errorf("version parsing err: %s", err) 646 | continue 647 | } 648 | 649 | _, msgs := c.Validate(v) 650 | if len(msgs) == 0 { 651 | t.Errorf("Did not get error message on constraint %q", tc.constraint) 652 | } else { 653 | e := msgs[0].Error() 654 | if e != tc.msg { 655 | t.Errorf("Did not get expected message. Expected %q, got %q", tc.msg, e) 656 | } 657 | } 658 | } 659 | } 660 | 661 | func TestConstraintString(t *testing.T) { 662 | tests := []struct { 663 | constraint string 664 | st string 665 | }{ 666 | {"*", "*"}, 667 | {">=1.2.3", ">=1.2.3"}, 668 | {">= 1.2.3", ">=1.2.3"}, 669 | {"2.x, >=1.2.3 || >4.5.6, < 5.7", "2.x >=1.2.3 || >4.5.6 <5.7"}, 670 | {"2.x, >=1.2.3 || >4.5.6, < 5.7 || >40.50.60, < 50.70", "2.x >=1.2.3 || >4.5.6 <5.7 || >40.50.60 <50.70"}, 671 | {"1.2", "1.2"}, 672 | } 673 | 674 | for _, tc := range tests { 675 | c, err := NewConstraint(tc.constraint) 676 | if err != nil { 677 | t.Errorf("cannot create constraint for %q, err: %s", tc.constraint, err) 678 | continue 679 | } 680 | 681 | if c.String() != tc.st { 682 | t.Errorf("expected constraint from %q to be a string as %q but got %q", tc.constraint, tc.st, c.String()) 683 | } 684 | 685 | if _, err = NewConstraint(c.String()); err != nil { 686 | t.Errorf("expected string from constrint %q to parse as valid but got err: %s", tc.constraint, err) 687 | } 688 | } 689 | } 690 | 691 | func TestTextMarshalConstraints(t *testing.T) { 692 | tests := []struct { 693 | constraint string 694 | want string 695 | }{ 696 | {"1.2.3", "1.2.3"}, 697 | {">=1.2.3", ">=1.2.3"}, 698 | {"<=1.2.3", "<=1.2.3"}, 699 | {"1 <=1.2.3", "1 <=1.2.3"}, 700 | {"1, <=1.2.3", "1 <=1.2.3"}, 701 | {">1, <=1.2.3", ">1 <=1.2.3"}, 702 | {"> 1 , <=1.2.3", ">1 <=1.2.3"}, 703 | } 704 | 705 | for _, tc := range tests { 706 | cs, err := NewConstraint(tc.constraint) 707 | if err != nil { 708 | t.Errorf("Error creating constraints: %s", err) 709 | } 710 | 711 | out, err2 := cs.MarshalText() 712 | if err2 != nil { 713 | t.Errorf("Error constraint version: %s", err2) 714 | } 715 | 716 | got := string(out) 717 | if got != tc.want { 718 | t.Errorf("Error marshaling constraint, unexpected marshaled content: got=%q want=%q", got, tc.want) 719 | } 720 | 721 | // Test that this works for JSON as well as text. When JSON marshaling 722 | // functions are missing it falls through to TextMarshal. 723 | // NOTE: To not escape the < and > (which json.Marshal does) you need 724 | // a custom encoder where html escaping is disabled. This must be done 725 | // in the top level encoder being used to marshal the constraints. 726 | buf := new(bytes.Buffer) 727 | enc := json.NewEncoder(buf) 728 | enc.SetEscapeHTML(false) 729 | err = enc.Encode(cs) 730 | if err != nil { 731 | t.Errorf("Error unmarshaling constraint: %s", err) 732 | } 733 | got = buf.String() 734 | // The encoder used here adds a newline so we add that to what we want 735 | // so they align. The newline is an artifact of the testing. 736 | want := fmt.Sprintf("%q\n", tc.want) 737 | if got != want { 738 | t.Errorf("Error marshaling constraint, unexpected marshaled content: got=%q want=%q", got, want) 739 | } 740 | } 741 | } 742 | 743 | func TestTextUnmarshalConstraints(t *testing.T) { 744 | tests := []struct { 745 | constraint string 746 | want string 747 | }{ 748 | {"1.2.3", "1.2.3"}, 749 | {">=1.2.3", ">=1.2.3"}, 750 | {"<=1.2.3", "<=1.2.3"}, 751 | {">1 <=1.2.3", ">1 <=1.2.3"}, 752 | {"> 1 <=1.2.3", ">1 <=1.2.3"}, 753 | {">1, <=1.2.3", ">1 <=1.2.3"}, 754 | } 755 | 756 | for _, tc := range tests { 757 | cs := Constraints{} 758 | err := cs.UnmarshalText([]byte(tc.constraint)) 759 | if err != nil { 760 | t.Errorf("Error unmarshaling constraints: %s", err) 761 | } 762 | got := cs.String() 763 | if got != tc.want { 764 | t.Errorf("Error unmarshaling constraint, unexpected object content: got=%q want=%q", got, tc.want) 765 | } 766 | 767 | // Test that this works for JSON as well as text. When JSON unmarshaling 768 | // functions are missing it falls through to TextUnmarshal. 769 | err = json.Unmarshal([]byte(fmt.Sprintf("%q", tc.constraint)), &cs) 770 | if err != nil { 771 | t.Errorf("Error unmarshaling constraints: %s", err) 772 | } 773 | got = cs.String() 774 | if got != tc.want { 775 | t.Errorf("Error unmarshaling constraint, unexpected object content: got=%q want=%q", got, tc.want) 776 | } 777 | } 778 | } 779 | 780 | func FuzzNewConstraint(f *testing.F) { 781 | testcases := []string{ 782 | "v1.2.3", 783 | " ", 784 | "......", 785 | "1", 786 | "1.2.3-beta.1", 787 | "1.2.3+foo", 788 | "2.3.4-alpha.1+bar", 789 | "lorem ipsum", 790 | "*", 791 | "!=1.2.3", 792 | "^4.5", 793 | "1.0.0 - 2", 794 | "1.2.3.4.5.6", 795 | ">= 1", 796 | "~9.8.7", 797 | "<= 12.13.14", 798 | "987654321.123456789.654123789", 799 | "1.x", 800 | "2.3.x", 801 | "9.2-beta.0", 802 | } 803 | 804 | for _, tc := range testcases { 805 | f.Add(tc) 806 | } 807 | 808 | f.Fuzz(func(_ *testing.T, a string) { 809 | _, _ = NewConstraint(a) 810 | }) 811 | } 812 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go. 3 | 4 | Specifically it provides the ability to: 5 | 6 | - Parse semantic versions 7 | - Sort semantic versions 8 | - Check if a semantic version fits within a set of constraints 9 | - Optionally work with a `v` prefix 10 | 11 | # Parsing Semantic Versions 12 | 13 | There are two functions that can parse semantic versions. The `StrictNewVersion` 14 | function only parses valid version 2 semantic versions as outlined in the 15 | specification. The `NewVersion` function attempts to coerce a version into a 16 | semantic version and parse it. For example, if there is a leading v or a version 17 | listed without all 3 parts (e.g. 1.2) it will attempt to coerce it into a valid 18 | semantic version (e.g., 1.2.0). In both cases a `Version` object is returned 19 | that can be sorted, compared, and used in constraints. 20 | 21 | When parsing a version an optional error can be returned if there is an issue 22 | parsing the version. For example, 23 | 24 | v, err := semver.NewVersion("1.2.3-beta.1+b345") 25 | 26 | The version object has methods to get the parts of the version, compare it to 27 | other versions, convert the version back into a string, and get the original 28 | string. For more details please see the documentation 29 | at https://godoc.org/github.com/Masterminds/semver. 30 | 31 | # Sorting Semantic Versions 32 | 33 | A set of versions can be sorted using the `sort` package from the standard library. 34 | For example, 35 | 36 | raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",} 37 | vs := make([]*semver.Version, len(raw)) 38 | for i, r := range raw { 39 | v, err := semver.NewVersion(r) 40 | if err != nil { 41 | t.Errorf("Error parsing version: %s", err) 42 | } 43 | 44 | vs[i] = v 45 | } 46 | 47 | sort.Sort(semver.Collection(vs)) 48 | 49 | # Checking Version Constraints and Comparing Versions 50 | 51 | There are two methods for comparing versions. One uses comparison methods on 52 | `Version` instances and the other is using Constraints. There are some important 53 | differences to notes between these two methods of comparison. 54 | 55 | 1. When two versions are compared using functions such as `Compare`, `LessThan`, 56 | and others it will follow the specification and always include prereleases 57 | within the comparison. It will provide an answer valid with the comparison 58 | spec section at https://semver.org/#spec-item-11 59 | 2. When constraint checking is used for checks or validation it will follow a 60 | different set of rules that are common for ranges with tools like npm/js 61 | and Rust/Cargo. This includes considering prereleases to be invalid if the 62 | ranges does not include on. If you want to have it include pre-releases a 63 | simple solution is to include `-0` in your range. 64 | 3. Constraint ranges can have some complex rules including the shorthard use of 65 | ~ and ^. For more details on those see the options below. 66 | 67 | There are differences between the two methods or checking versions because the 68 | comparison methods on `Version` follow the specification while comparison ranges 69 | are not part of the specification. Different packages and tools have taken it 70 | upon themselves to come up with range rules. This has resulted in differences. 71 | For example, npm/js and Cargo/Rust follow similar patterns which PHP has a 72 | different pattern for ^. The comparison features in this package follow the 73 | npm/js and Cargo/Rust lead because applications using it have followed similar 74 | patters with their versions. 75 | 76 | Checking a version against version constraints is one of the most featureful 77 | parts of the package. 78 | 79 | c, err := semver.NewConstraint(">= 1.2.3") 80 | if err != nil { 81 | // Handle constraint not being parsable. 82 | } 83 | 84 | v, err := semver.NewVersion("1.3") 85 | if err != nil { 86 | // Handle version not being parsable. 87 | } 88 | // Check if the version meets the constraints. The a variable will be true. 89 | a := c.Check(v) 90 | 91 | # Basic Comparisons 92 | 93 | There are two elements to the comparisons. First, a comparison string is a list 94 | of comma or space separated AND comparisons. These are then separated by || (OR) 95 | comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a 96 | comparison that's greater than or equal to 1.2 and less than 3.0.0 or is 97 | greater than or equal to 4.2.3. This can also be written as 98 | `">= 1.2, < 3.0.0 || >= 4.2.3"` 99 | 100 | The basic comparisons are: 101 | 102 | - `=`: equal (aliased to no operator) 103 | - `!=`: not equal 104 | - `>`: greater than 105 | - `<`: less than 106 | - `>=`: greater than or equal to 107 | - `<=`: less than or equal to 108 | 109 | # Hyphen Range Comparisons 110 | 111 | There are multiple methods to handle ranges and the first is hyphens ranges. 112 | These look like: 113 | 114 | - `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5` 115 | - `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` 116 | 117 | # Wildcards In Comparisons 118 | 119 | The `x`, `X`, and `*` characters can be used as a wildcard character. This works 120 | for all comparison operators. When used on the `=` operator it falls 121 | back to the tilde operation. For example, 122 | 123 | - `1.2.x` is equivalent to `>= 1.2.0 < 1.3.0` 124 | - `>= 1.2.x` is equivalent to `>= 1.2.0` 125 | - `<= 2.x` is equivalent to `<= 3` 126 | - `*` is equivalent to `>= 0.0.0` 127 | 128 | Tilde Range Comparisons (Patch) 129 | 130 | The tilde (`~`) comparison operator is for patch level ranges when a minor 131 | version is specified and major level changes when the minor number is missing. 132 | For example, 133 | 134 | - `~1.2.3` is equivalent to `>= 1.2.3 < 1.3.0` 135 | - `~1` is equivalent to `>= 1, < 2` 136 | - `~2.3` is equivalent to `>= 2.3 < 2.4` 137 | - `~1.2.x` is equivalent to `>= 1.2.0 < 1.3.0` 138 | - `~1.x` is equivalent to `>= 1 < 2` 139 | 140 | Caret Range Comparisons (Major) 141 | 142 | The caret (`^`) comparison operator is for major level changes once a stable 143 | (1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts 144 | as the API stability level. This is useful when comparisons of API versions as a 145 | major change is API breaking. For example, 146 | 147 | - `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` 148 | - `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` 149 | - `^2.3` is equivalent to `>= 2.3, < 3` 150 | - `^2.x` is equivalent to `>= 2.0.0, < 3` 151 | - `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` 152 | - `^0.2` is equivalent to `>=0.2.0 <0.3.0` 153 | - `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` 154 | - `^0.0` is equivalent to `>=0.0.0 <0.1.0` 155 | - `^0` is equivalent to `>=0.0.0 <1.0.0` 156 | 157 | # Validation 158 | 159 | In addition to testing a version against a constraint, a version can be validated 160 | against a constraint. When validation fails a slice of errors containing why a 161 | version didn't meet the constraint is returned. For example, 162 | 163 | c, err := semver.NewConstraint("<= 1.2.3, >= 1.4") 164 | if err != nil { 165 | // Handle constraint not being parseable. 166 | } 167 | 168 | v, _ := semver.NewVersion("1.3") 169 | if err != nil { 170 | // Handle version not being parseable. 171 | } 172 | 173 | // Validate a version against a constraint. 174 | a, msgs := c.Validate(v) 175 | // a is false 176 | for _, m := range msgs { 177 | fmt.Println(m) 178 | 179 | // Loops over the errors which would read 180 | // "1.3 is greater than 1.2.3" 181 | // "1.3 is less than 1.4" 182 | } 183 | */ 184 | package semver 185 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Masterminds/semver/v3 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // The compiled version of the regex created at init() is cached here so it 15 | // only needs to be created once. 16 | var versionRegex *regexp.Regexp 17 | 18 | var ( 19 | // ErrInvalidSemVer is returned a version is found to be invalid when 20 | // being parsed. 21 | ErrInvalidSemVer = errors.New("Invalid Semantic Version") 22 | 23 | // ErrEmptyString is returned when an empty string is passed in for parsing. 24 | ErrEmptyString = errors.New("Version string empty") 25 | 26 | // ErrInvalidCharacters is returned when invalid characters are found as 27 | // part of a version 28 | ErrInvalidCharacters = errors.New("Invalid characters in version") 29 | 30 | // ErrSegmentStartsZero is returned when a version segment starts with 0. 31 | // This is invalid in SemVer. 32 | ErrSegmentStartsZero = errors.New("Version segment starts with 0") 33 | 34 | // ErrInvalidMetadata is returned when the metadata is an invalid format 35 | ErrInvalidMetadata = errors.New("Invalid Metadata string") 36 | 37 | // ErrInvalidPrerelease is returned when the pre-release is an invalid format 38 | ErrInvalidPrerelease = errors.New("Invalid Prerelease string") 39 | ) 40 | 41 | // semVerRegex is the regular expression used to parse a semantic version. 42 | // This is not the official regex from the semver spec. It has been modified to allow for loose handling 43 | // where versions like 2.1 are detected. 44 | const semVerRegex string = `v?(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?` + 45 | `(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` + 46 | `(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?` 47 | 48 | // Version represents a single semantic version. 49 | type Version struct { 50 | major, minor, patch uint64 51 | pre string 52 | metadata string 53 | original string 54 | } 55 | 56 | func init() { 57 | versionRegex = regexp.MustCompile("^" + semVerRegex + "$") 58 | } 59 | 60 | const ( 61 | num string = "0123456789" 62 | allowed string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + num 63 | ) 64 | 65 | // StrictNewVersion parses a given version and returns an instance of Version or 66 | // an error if unable to parse the version. Only parses valid semantic versions. 67 | // Performs checking that can find errors within the version. 68 | // If you want to coerce a version such as 1 or 1.2 and parse it as the 1.x 69 | // releases of semver did, use the NewVersion() function. 70 | func StrictNewVersion(v string) (*Version, error) { 71 | // Parsing here does not use RegEx in order to increase performance and reduce 72 | // allocations. 73 | 74 | if len(v) == 0 { 75 | return nil, ErrEmptyString 76 | } 77 | 78 | // Split the parts into [0]major, [1]minor, and [2]patch,prerelease,build 79 | parts := strings.SplitN(v, ".", 3) 80 | if len(parts) != 3 { 81 | return nil, ErrInvalidSemVer 82 | } 83 | 84 | sv := &Version{ 85 | original: v, 86 | } 87 | 88 | // Extract build metadata 89 | if strings.Contains(parts[2], "+") { 90 | extra := strings.SplitN(parts[2], "+", 2) 91 | sv.metadata = extra[1] 92 | parts[2] = extra[0] 93 | if err := validateMetadata(sv.metadata); err != nil { 94 | return nil, err 95 | } 96 | } 97 | 98 | // Extract build prerelease 99 | if strings.Contains(parts[2], "-") { 100 | extra := strings.SplitN(parts[2], "-", 2) 101 | sv.pre = extra[1] 102 | parts[2] = extra[0] 103 | if err := validatePrerelease(sv.pre); err != nil { 104 | return nil, err 105 | } 106 | } 107 | 108 | // Validate the number segments are valid. This includes only having positive 109 | // numbers and no leading 0's. 110 | for _, p := range parts { 111 | if !containsOnly(p, num) { 112 | return nil, ErrInvalidCharacters 113 | } 114 | 115 | if len(p) > 1 && p[0] == '0' { 116 | return nil, ErrSegmentStartsZero 117 | } 118 | } 119 | 120 | // Extract major, minor, and patch 121 | var err error 122 | sv.major, err = strconv.ParseUint(parts[0], 10, 64) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | sv.minor, err = strconv.ParseUint(parts[1], 10, 64) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | sv.patch, err = strconv.ParseUint(parts[2], 10, 64) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return sv, nil 138 | } 139 | 140 | // NewVersion parses a given version and returns an instance of Version or 141 | // an error if unable to parse the version. If the version is SemVer-ish it 142 | // attempts to convert it to SemVer. If you want to validate it was a strict 143 | // semantic version at parse time see StrictNewVersion(). 144 | func NewVersion(v string) (*Version, error) { 145 | m := versionRegex.FindStringSubmatch(v) 146 | if m == nil { 147 | return nil, ErrInvalidSemVer 148 | } 149 | 150 | sv := &Version{ 151 | metadata: m[5], 152 | pre: m[4], 153 | original: v, 154 | } 155 | 156 | var err error 157 | sv.major, err = strconv.ParseUint(m[1], 10, 64) 158 | if err != nil { 159 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 160 | } 161 | 162 | if m[2] != "" { 163 | sv.minor, err = strconv.ParseUint(m[2], 10, 64) 164 | if err != nil { 165 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 166 | } 167 | } else { 168 | sv.minor = 0 169 | } 170 | 171 | if m[3] != "" { 172 | sv.patch, err = strconv.ParseUint(m[3], 10, 64) 173 | if err != nil { 174 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 175 | } 176 | } else { 177 | sv.patch = 0 178 | } 179 | 180 | // Perform some basic due diligence on the extra parts to ensure they are 181 | // valid. 182 | 183 | if sv.pre != "" { 184 | if err = validatePrerelease(sv.pre); err != nil { 185 | return nil, err 186 | } 187 | } 188 | 189 | if sv.metadata != "" { 190 | if err = validateMetadata(sv.metadata); err != nil { 191 | return nil, err 192 | } 193 | } 194 | 195 | return sv, nil 196 | } 197 | 198 | // New creates a new instance of Version with each of the parts passed in as 199 | // arguments instead of parsing a version string. 200 | func New(major, minor, patch uint64, pre, metadata string) *Version { 201 | v := Version{ 202 | major: major, 203 | minor: minor, 204 | patch: patch, 205 | pre: pre, 206 | metadata: metadata, 207 | original: "", 208 | } 209 | 210 | v.original = v.String() 211 | 212 | return &v 213 | } 214 | 215 | // MustParse parses a given version and panics on error. 216 | func MustParse(v string) *Version { 217 | sv, err := NewVersion(v) 218 | if err != nil { 219 | panic(err) 220 | } 221 | return sv 222 | } 223 | 224 | // String converts a Version object to a string. 225 | // Note, if the original version contained a leading v this version will not. 226 | // See the Original() method to retrieve the original value. Semantic Versions 227 | // don't contain a leading v per the spec. Instead it's optional on 228 | // implementation. 229 | func (v Version) String() string { 230 | var buf bytes.Buffer 231 | 232 | fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch) 233 | if v.pre != "" { 234 | fmt.Fprintf(&buf, "-%s", v.pre) 235 | } 236 | if v.metadata != "" { 237 | fmt.Fprintf(&buf, "+%s", v.metadata) 238 | } 239 | 240 | return buf.String() 241 | } 242 | 243 | // Original returns the original value passed in to be parsed. 244 | func (v *Version) Original() string { 245 | return v.original 246 | } 247 | 248 | // Major returns the major version. 249 | func (v Version) Major() uint64 { 250 | return v.major 251 | } 252 | 253 | // Minor returns the minor version. 254 | func (v Version) Minor() uint64 { 255 | return v.minor 256 | } 257 | 258 | // Patch returns the patch version. 259 | func (v Version) Patch() uint64 { 260 | return v.patch 261 | } 262 | 263 | // Prerelease returns the pre-release version. 264 | func (v Version) Prerelease() string { 265 | return v.pre 266 | } 267 | 268 | // Metadata returns the metadata on the version. 269 | func (v Version) Metadata() string { 270 | return v.metadata 271 | } 272 | 273 | // originalVPrefix returns the original 'v' prefix if any. 274 | func (v Version) originalVPrefix() string { 275 | // Note, only lowercase v is supported as a prefix by the parser. 276 | if v.original != "" && v.original[:1] == "v" { 277 | return v.original[:1] 278 | } 279 | return "" 280 | } 281 | 282 | // IncPatch produces the next patch version. 283 | // If the current version does not have prerelease/metadata information, 284 | // it unsets metadata and prerelease values, increments patch number. 285 | // If the current version has any of prerelease or metadata information, 286 | // it unsets both values and keeps current patch value 287 | func (v Version) IncPatch() Version { 288 | vNext := v 289 | // according to http://semver.org/#spec-item-9 290 | // Pre-release versions have a lower precedence than the associated normal version. 291 | // according to http://semver.org/#spec-item-10 292 | // Build metadata SHOULD be ignored when determining version precedence. 293 | if v.pre != "" { 294 | vNext.metadata = "" 295 | vNext.pre = "" 296 | } else { 297 | vNext.metadata = "" 298 | vNext.pre = "" 299 | vNext.patch = v.patch + 1 300 | } 301 | vNext.original = v.originalVPrefix() + "" + vNext.String() 302 | return vNext 303 | } 304 | 305 | // IncMinor produces the next minor version. 306 | // Sets patch to 0. 307 | // Increments minor number. 308 | // Unsets metadata. 309 | // Unsets prerelease status. 310 | func (v Version) IncMinor() Version { 311 | vNext := v 312 | vNext.metadata = "" 313 | vNext.pre = "" 314 | vNext.patch = 0 315 | vNext.minor = v.minor + 1 316 | vNext.original = v.originalVPrefix() + "" + vNext.String() 317 | return vNext 318 | } 319 | 320 | // IncMajor produces the next major version. 321 | // Sets patch to 0. 322 | // Sets minor to 0. 323 | // Increments major number. 324 | // Unsets metadata. 325 | // Unsets prerelease status. 326 | func (v Version) IncMajor() Version { 327 | vNext := v 328 | vNext.metadata = "" 329 | vNext.pre = "" 330 | vNext.patch = 0 331 | vNext.minor = 0 332 | vNext.major = v.major + 1 333 | vNext.original = v.originalVPrefix() + "" + vNext.String() 334 | return vNext 335 | } 336 | 337 | // SetPrerelease defines the prerelease value. 338 | // Value must not include the required 'hyphen' prefix. 339 | func (v Version) SetPrerelease(prerelease string) (Version, error) { 340 | vNext := v 341 | if len(prerelease) > 0 { 342 | if err := validatePrerelease(prerelease); err != nil { 343 | return vNext, err 344 | } 345 | } 346 | vNext.pre = prerelease 347 | vNext.original = v.originalVPrefix() + "" + vNext.String() 348 | return vNext, nil 349 | } 350 | 351 | // SetMetadata defines metadata value. 352 | // Value must not include the required 'plus' prefix. 353 | func (v Version) SetMetadata(metadata string) (Version, error) { 354 | vNext := v 355 | if len(metadata) > 0 { 356 | if err := validateMetadata(metadata); err != nil { 357 | return vNext, err 358 | } 359 | } 360 | vNext.metadata = metadata 361 | vNext.original = v.originalVPrefix() + "" + vNext.String() 362 | return vNext, nil 363 | } 364 | 365 | // LessThan tests if one version is less than another one. 366 | func (v *Version) LessThan(o *Version) bool { 367 | return v.Compare(o) < 0 368 | } 369 | 370 | // LessThanEqual tests if one version is less or equal than another one. 371 | func (v *Version) LessThanEqual(o *Version) bool { 372 | return v.Compare(o) <= 0 373 | } 374 | 375 | // GreaterThan tests if one version is greater than another one. 376 | func (v *Version) GreaterThan(o *Version) bool { 377 | return v.Compare(o) > 0 378 | } 379 | 380 | // GreaterThanEqual tests if one version is greater or equal than another one. 381 | func (v *Version) GreaterThanEqual(o *Version) bool { 382 | return v.Compare(o) >= 0 383 | } 384 | 385 | // Equal tests if two versions are equal to each other. 386 | // Note, versions can be equal with different metadata since metadata 387 | // is not considered part of the comparable version. 388 | func (v *Version) Equal(o *Version) bool { 389 | if v == o { 390 | return true 391 | } 392 | if v == nil || o == nil { 393 | return false 394 | } 395 | return v.Compare(o) == 0 396 | } 397 | 398 | // Compare compares this version to another one. It returns -1, 0, or 1 if 399 | // the version smaller, equal, or larger than the other version. 400 | // 401 | // Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is 402 | // lower than the version without a prerelease. Compare always takes into account 403 | // prereleases. If you want to work with ranges using typical range syntaxes that 404 | // skip prereleases if the range is not looking for them use constraints. 405 | func (v *Version) Compare(o *Version) int { 406 | // Compare the major, minor, and patch version for differences. If a 407 | // difference is found return the comparison. 408 | if d := compareSegment(v.Major(), o.Major()); d != 0 { 409 | return d 410 | } 411 | if d := compareSegment(v.Minor(), o.Minor()); d != 0 { 412 | return d 413 | } 414 | if d := compareSegment(v.Patch(), o.Patch()); d != 0 { 415 | return d 416 | } 417 | 418 | // At this point the major, minor, and patch versions are the same. 419 | ps := v.pre 420 | po := o.Prerelease() 421 | 422 | if ps == "" && po == "" { 423 | return 0 424 | } 425 | if ps == "" { 426 | return 1 427 | } 428 | if po == "" { 429 | return -1 430 | } 431 | 432 | return comparePrerelease(ps, po) 433 | } 434 | 435 | // UnmarshalJSON implements JSON.Unmarshaler interface. 436 | func (v *Version) UnmarshalJSON(b []byte) error { 437 | var s string 438 | if err := json.Unmarshal(b, &s); err != nil { 439 | return err 440 | } 441 | temp, err := NewVersion(s) 442 | if err != nil { 443 | return err 444 | } 445 | v.major = temp.major 446 | v.minor = temp.minor 447 | v.patch = temp.patch 448 | v.pre = temp.pre 449 | v.metadata = temp.metadata 450 | v.original = temp.original 451 | return nil 452 | } 453 | 454 | // MarshalJSON implements JSON.Marshaler interface. 455 | func (v Version) MarshalJSON() ([]byte, error) { 456 | return json.Marshal(v.String()) 457 | } 458 | 459 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 460 | func (v *Version) UnmarshalText(text []byte) error { 461 | temp, err := NewVersion(string(text)) 462 | if err != nil { 463 | return err 464 | } 465 | 466 | *v = *temp 467 | 468 | return nil 469 | } 470 | 471 | // MarshalText implements the encoding.TextMarshaler interface. 472 | func (v Version) MarshalText() ([]byte, error) { 473 | return []byte(v.String()), nil 474 | } 475 | 476 | // Scan implements the SQL.Scanner interface. 477 | func (v *Version) Scan(value interface{}) error { 478 | var s string 479 | s, _ = value.(string) 480 | temp, err := NewVersion(s) 481 | if err != nil { 482 | return err 483 | } 484 | v.major = temp.major 485 | v.minor = temp.minor 486 | v.patch = temp.patch 487 | v.pre = temp.pre 488 | v.metadata = temp.metadata 489 | v.original = temp.original 490 | return nil 491 | } 492 | 493 | // Value implements the Driver.Valuer interface. 494 | func (v Version) Value() (driver.Value, error) { 495 | return v.String(), nil 496 | } 497 | 498 | func compareSegment(v, o uint64) int { 499 | if v < o { 500 | return -1 501 | } 502 | if v > o { 503 | return 1 504 | } 505 | 506 | return 0 507 | } 508 | 509 | func comparePrerelease(v, o string) int { 510 | // split the prelease versions by their part. The separator, per the spec, 511 | // is a . 512 | sparts := strings.Split(v, ".") 513 | oparts := strings.Split(o, ".") 514 | 515 | // Find the longer length of the parts to know how many loop iterations to 516 | // go through. 517 | slen := len(sparts) 518 | olen := len(oparts) 519 | 520 | l := slen 521 | if olen > slen { 522 | l = olen 523 | } 524 | 525 | // Iterate over each part of the prereleases to compare the differences. 526 | for i := 0; i < l; i++ { 527 | // Since the lentgh of the parts can be different we need to create 528 | // a placeholder. This is to avoid out of bounds issues. 529 | stemp := "" 530 | if i < slen { 531 | stemp = sparts[i] 532 | } 533 | 534 | otemp := "" 535 | if i < olen { 536 | otemp = oparts[i] 537 | } 538 | 539 | d := comparePrePart(stemp, otemp) 540 | if d != 0 { 541 | return d 542 | } 543 | } 544 | 545 | // Reaching here means two versions are of equal value but have different 546 | // metadata (the part following a +). They are not identical in string form 547 | // but the version comparison finds them to be equal. 548 | return 0 549 | } 550 | 551 | func comparePrePart(s, o string) int { 552 | // Fastpath if they are equal 553 | if s == o { 554 | return 0 555 | } 556 | 557 | // When s or o are empty we can use the other in an attempt to determine 558 | // the response. 559 | if s == "" { 560 | if o != "" { 561 | return -1 562 | } 563 | return 1 564 | } 565 | 566 | if o == "" { 567 | if s != "" { 568 | return 1 569 | } 570 | return -1 571 | } 572 | 573 | // When comparing strings "99" is greater than "103". To handle 574 | // cases like this we need to detect numbers and compare them. According 575 | // to the semver spec, numbers are always positive. If there is a - at the 576 | // start like -99 this is to be evaluated as an alphanum. numbers always 577 | // have precedence over alphanum. Parsing as Uints because negative numbers 578 | // are ignored. 579 | 580 | oi, n1 := strconv.ParseUint(o, 10, 64) 581 | si, n2 := strconv.ParseUint(s, 10, 64) 582 | 583 | // The case where both are strings compare the strings 584 | if n1 != nil && n2 != nil { 585 | if s > o { 586 | return 1 587 | } 588 | return -1 589 | } else if n1 != nil { 590 | // o is a string and s is a number 591 | return -1 592 | } else if n2 != nil { 593 | // s is a string and o is a number 594 | return 1 595 | } 596 | // Both are numbers 597 | if si > oi { 598 | return 1 599 | } 600 | return -1 601 | } 602 | 603 | // Like strings.ContainsAny but does an only instead of any. 604 | func containsOnly(s string, comp string) bool { 605 | return strings.IndexFunc(s, func(r rune) bool { 606 | return !strings.ContainsRune(comp, r) 607 | }) == -1 608 | } 609 | 610 | // From the spec, "Identifiers MUST comprise only 611 | // ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. 612 | // Numeric identifiers MUST NOT include leading zeroes.". These segments can 613 | // be dot separated. 614 | func validatePrerelease(p string) error { 615 | eparts := strings.Split(p, ".") 616 | for _, p := range eparts { 617 | if p == "" { 618 | return ErrInvalidMetadata 619 | } else if containsOnly(p, num) { 620 | if len(p) > 1 && p[0] == '0' { 621 | return ErrSegmentStartsZero 622 | } 623 | } else if !containsOnly(p, allowed) { 624 | return ErrInvalidPrerelease 625 | } 626 | } 627 | 628 | return nil 629 | } 630 | 631 | // From the spec, "Build metadata MAY be denoted by 632 | // appending a plus sign and a series of dot separated identifiers immediately 633 | // following the patch or pre-release version. Identifiers MUST comprise only 634 | // ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty." 635 | func validateMetadata(m string) error { 636 | eparts := strings.Split(m, ".") 637 | for _, p := range eparts { 638 | if p == "" { 639 | return ErrInvalidMetadata 640 | } else if !containsOnly(p, allowed) { 641 | return ErrInvalidMetadata 642 | } 643 | } 644 | return nil 645 | } 646 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestStrictNewVersion(t *testing.T) { 11 | tests := []struct { 12 | version string 13 | err bool 14 | }{ 15 | {"1.2.3", false}, 16 | {"1.2.3-alpha.01", true}, 17 | {"1.2.3+test.01", false}, 18 | {"1.2.3-alpha.-1", false}, 19 | {"v1.2.3", true}, 20 | {"1.0", true}, 21 | {"v1.0", true}, 22 | {"1", true}, 23 | {"v1", true}, 24 | {"1.2", true}, 25 | {"1.2.beta", true}, 26 | {"v1.2.beta", true}, 27 | {"foo", true}, 28 | {"1.2-5", true}, 29 | {"v1.2-5", true}, 30 | {"1.2-beta.5", true}, 31 | {"v1.2-beta.5", true}, 32 | {"\n1.2", true}, 33 | {"\nv1.2", true}, 34 | {"1.2.0-x.Y.0+metadata", false}, 35 | {"v1.2.0-x.Y.0+metadata", true}, 36 | {"1.2.0-x.Y.0+metadata-width-hypen", false}, 37 | {"v1.2.0-x.Y.0+metadata-width-hypen", true}, 38 | {"1.2.3-rc1-with-hypen", false}, 39 | {"v1.2.3-rc1-with-hypen", true}, 40 | {"1.2.3.4", true}, 41 | {"v1.2.3.4", true}, 42 | {"1.2.2147483648", false}, 43 | {"1.2147483648.3", false}, 44 | {"2147483648.3.0", false}, 45 | 46 | // The SemVer spec in a pre-release expects to allow [0-9A-Za-z-]. But, 47 | // the lack of all 3 parts in this version should produce an error. 48 | {"20221209-update-renovatejson-v4", true}, 49 | 50 | // Various cases that are invalid semver 51 | {"1.1.2+.123", true}, // A leading . in build metadata. This would signify that the first segment is empty 52 | {"1.0.0-alpha_beta", true}, // An underscore in the pre-release is an invalid character 53 | {"1.0.0-alpha..", true}, // Multiple empty segments 54 | {"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value 55 | {"01.1.1", true}, // A leading 0 on a number segment 56 | {"1.01.1", true}, // A leading 0 on a number segment 57 | {"1.1.01", true}, // A leading 0 on a number segment 58 | {"9.8.7+meta+meta", true}, // Multiple metadata parts 59 | {"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment 60 | {"1.2.3-0123", true}, 61 | {"1.2.3-0123.0123", true}, 62 | {"+invalid", true}, 63 | {"-invalid", true}, 64 | {"-invalid.01", true}, 65 | {"alpha+beta", true}, 66 | {"1.2.3-alpha_beta+foo", true}, 67 | {"1.0.0-alpha..1", true}, 68 | } 69 | 70 | for _, tc := range tests { 71 | _, err := StrictNewVersion(tc.version) 72 | if tc.err && err == nil { 73 | t.Fatalf("expected error for version: %s", tc.version) 74 | } else if !tc.err && err != nil { 75 | t.Fatalf("error for version %s: %s", tc.version, err) 76 | } 77 | } 78 | } 79 | 80 | func TestNewVersion(t *testing.T) { 81 | tests := []struct { 82 | version string 83 | err bool 84 | }{ 85 | {"1.2.3", false}, 86 | {"1.2.3-alpha.01", true}, 87 | {"1.2.3+test.01", false}, 88 | {"1.2.3-alpha.-1", false}, 89 | {"v1.2.3", false}, 90 | {"1.0", false}, 91 | {"v1.0", false}, 92 | {"1", false}, 93 | {"v1", false}, 94 | {"1.2.beta", true}, 95 | {"v1.2.beta", true}, 96 | {"foo", true}, 97 | {"1.2-5", false}, 98 | {"v1.2-5", false}, 99 | {"1.2-beta.5", false}, 100 | {"v1.2-beta.5", false}, 101 | {"\n1.2", true}, 102 | {"\nv1.2", true}, 103 | {"1.2.0-x.Y.0+metadata", false}, 104 | {"v1.2.0-x.Y.0+metadata", false}, 105 | {"1.2.0-x.Y.0+metadata-width-hypen", false}, 106 | {"v1.2.0-x.Y.0+metadata-width-hypen", false}, 107 | {"1.2.3-rc1-with-hypen", false}, 108 | {"v1.2.3-rc1-with-hypen", false}, 109 | {"1.2.3.4", true}, 110 | {"v1.2.3.4", true}, 111 | {"1.2.2147483648", false}, 112 | {"1.2147483648.3", false}, 113 | {"2147483648.3.0", false}, 114 | 115 | // Due to having 4 parts these should produce an error. See 116 | // https://github.com/Masterminds/semver/issues/185 for the reason for 117 | // these tests. 118 | {"12.3.4.1234", true}, 119 | {"12.23.4.1234", true}, 120 | {"12.3.34.1234", true}, 121 | 122 | // The SemVer spec in a pre-release expects to allow [0-9A-Za-z-]. 123 | {"20221209-update-renovatejson-v4", false}, 124 | 125 | // Various cases that are invalid semver 126 | {"1.1.2+.123", true}, // A leading . in build metadata. This would signify that the first segment is empty 127 | {"1.0.0-alpha_beta", true}, // An underscore in the pre-release is an invalid character 128 | {"1.0.0-alpha..", true}, // Multiple empty segments 129 | {"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value 130 | {"01.1.1", true}, // A leading 0 on a number segment 131 | {"1.01.1", true}, // A leading 0 on a number segment 132 | {"1.1.01", true}, // A leading 0 on a number segment 133 | {"9.8.7+meta+meta", true}, // Multiple metadata parts 134 | {"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment 135 | } 136 | 137 | for _, tc := range tests { 138 | _, err := NewVersion(tc.version) 139 | if tc.err && err == nil { 140 | t.Fatalf("expected error for version: %s", tc.version) 141 | } else if !tc.err && err != nil { 142 | t.Fatalf("error for version %s: %s", tc.version, err) 143 | } 144 | } 145 | } 146 | 147 | func TestNew(t *testing.T) { 148 | // v0.1.2 149 | v := New(0, 1, 2, "", "") 150 | 151 | if v.String() != "0.1.2" { 152 | t.Errorf("expected version 0.1.2 but got %q", v.String()) 153 | } 154 | 155 | // v1.2.3-alpha.1+foo.bar 156 | v = New(1, 2, 3, "alpha.1", "foo.bar") 157 | 158 | if v.String() != "1.2.3-alpha.1+foo.bar" { 159 | t.Errorf("expected version 1.2.3-alpha.1+foo.bar but got %q", v.String()) 160 | } 161 | } 162 | 163 | func TestOriginal(t *testing.T) { 164 | tests := []string{ 165 | "1.2.3", 166 | "v1.2.3", 167 | "1.0", 168 | "v1.0", 169 | "1", 170 | "v1", 171 | "1.2-5", 172 | "v1.2-5", 173 | "1.2-beta.5", 174 | "v1.2-beta.5", 175 | "1.2.0-x.Y.0+metadata", 176 | "v1.2.0-x.Y.0+metadata", 177 | "1.2.0-x.Y.0+metadata-width-hypen", 178 | "v1.2.0-x.Y.0+metadata-width-hypen", 179 | "1.2.3-rc1-with-hypen", 180 | "v1.2.3-rc1-with-hypen", 181 | } 182 | 183 | for _, tc := range tests { 184 | v, err := NewVersion(tc) 185 | if err != nil { 186 | t.Errorf("Error parsing version %s", tc) 187 | } 188 | 189 | o := v.Original() 190 | if o != tc { 191 | t.Errorf("Error retrieving original. Expected '%s' but got '%v'", tc, v) 192 | } 193 | } 194 | } 195 | 196 | func TestParts(t *testing.T) { 197 | v, err := NewVersion("1.2.3-beta.1+build.123") 198 | if err != nil { 199 | t.Error("Error parsing version 1.2.3-beta.1+build.123") 200 | } 201 | 202 | if v.Major() != 1 { 203 | t.Error("Major() returning wrong value") 204 | } 205 | if v.Minor() != 2 { 206 | t.Error("Minor() returning wrong value") 207 | } 208 | if v.Patch() != 3 { 209 | t.Error("Patch() returning wrong value") 210 | } 211 | if v.Prerelease() != "beta.1" { 212 | t.Error("Prerelease() returning wrong value") 213 | } 214 | if v.Metadata() != "build.123" { 215 | t.Error("Metadata() returning wrong value") 216 | } 217 | } 218 | 219 | func TestCoerceString(t *testing.T) { 220 | tests := []struct { 221 | version string 222 | expected string 223 | }{ 224 | {"1.2.3", "1.2.3"}, 225 | {"v1.2.3", "1.2.3"}, 226 | {"1.0", "1.0.0"}, 227 | {"v1.0", "1.0.0"}, 228 | {"1", "1.0.0"}, 229 | {"v1", "1.0.0"}, 230 | {"1.2-5", "1.2.0-5"}, 231 | {"v1.2-5", "1.2.0-5"}, 232 | {"1.2-beta.5", "1.2.0-beta.5"}, 233 | {"v1.2-beta.5", "1.2.0-beta.5"}, 234 | {"1.2.0-x.Y.0+metadata", "1.2.0-x.Y.0+metadata"}, 235 | {"v1.2.0-x.Y.0+metadata", "1.2.0-x.Y.0+metadata"}, 236 | {"1.2.0-x.Y.0+metadata-width-hypen", "1.2.0-x.Y.0+metadata-width-hypen"}, 237 | {"v1.2.0-x.Y.0+metadata-width-hypen", "1.2.0-x.Y.0+metadata-width-hypen"}, 238 | {"1.2.3-rc1-with-hypen", "1.2.3-rc1-with-hypen"}, 239 | {"v1.2.3-rc1-with-hypen", "1.2.3-rc1-with-hypen"}, 240 | } 241 | 242 | for _, tc := range tests { 243 | v, err := NewVersion(tc.version) 244 | if err != nil { 245 | t.Errorf("Error parsing version %s", tc) 246 | } 247 | 248 | s := v.String() 249 | if s != tc.expected { 250 | t.Errorf("Error generating string. Expected '%s' but got '%s'", tc.expected, s) 251 | } 252 | } 253 | } 254 | 255 | func TestCompare(t *testing.T) { 256 | tests := []struct { 257 | v1 string 258 | v2 string 259 | expected int 260 | }{ 261 | {"1.2.3", "1.5.1", -1}, 262 | {"2.2.3", "1.5.1", 1}, 263 | {"2.2.3", "2.2.2", 1}, 264 | {"3.2-beta", "3.2-beta", 0}, 265 | {"1.3", "1.1.4", 1}, 266 | {"4.2", "4.2-beta", 1}, 267 | {"4.2-beta", "4.2", -1}, 268 | {"4.2-alpha", "4.2-beta", -1}, 269 | {"4.2-alpha", "4.2-alpha", 0}, 270 | {"4.2-beta.2", "4.2-beta.1", 1}, 271 | {"4.2-beta2", "4.2-beta1", 1}, 272 | {"4.2-beta", "4.2-beta.2", -1}, 273 | {"4.2-beta", "4.2-beta.foo", -1}, 274 | {"4.2-beta.2", "4.2-beta", 1}, 275 | {"4.2-beta.foo", "4.2-beta", 1}, 276 | {"1.2+bar", "1.2+baz", 0}, 277 | {"1.0.0-beta.4", "1.0.0-beta.-2", -1}, 278 | {"1.0.0-beta.-2", "1.0.0-beta.-3", -1}, 279 | {"1.0.0-beta.-3", "1.0.0-beta.5", 1}, 280 | } 281 | 282 | for _, tc := range tests { 283 | v1, err := NewVersion(tc.v1) 284 | if err != nil { 285 | t.Errorf("Error parsing version: %s", err) 286 | } 287 | 288 | v2, err := NewVersion(tc.v2) 289 | if err != nil { 290 | t.Errorf("Error parsing version: %s", err) 291 | } 292 | 293 | a := v1.Compare(v2) 294 | e := tc.expected 295 | if a != e { 296 | t.Errorf( 297 | "Comparison of '%s' and '%s' failed. Expected '%d', got '%d'", 298 | tc.v1, tc.v2, e, a, 299 | ) 300 | } 301 | } 302 | } 303 | 304 | func TestLessThan(t *testing.T) { 305 | tests := []struct { 306 | v1 string 307 | v2 string 308 | expected bool 309 | }{ 310 | {"1.2.3", "1.5.1", true}, 311 | {"2.2.3", "1.5.1", false}, 312 | {"3.2-beta", "3.2-beta", false}, 313 | } 314 | 315 | for _, tc := range tests { 316 | v1, err := NewVersion(tc.v1) 317 | if err != nil { 318 | t.Errorf("Error parsing version: %s", err) 319 | } 320 | 321 | v2, err := NewVersion(tc.v2) 322 | if err != nil { 323 | t.Errorf("Error parsing version: %s", err) 324 | } 325 | 326 | a := v1.LessThan(v2) 327 | e := tc.expected 328 | if a != e { 329 | t.Errorf( 330 | "Comparison of '%s' and '%s' failed. Expected '%t', got '%t'", 331 | tc.v1, tc.v2, e, a, 332 | ) 333 | } 334 | } 335 | } 336 | 337 | func TestLessThanEqual(t *testing.T) { 338 | tests := []struct { 339 | v1 string 340 | v2 string 341 | expected bool 342 | }{ 343 | {"1.2.3", "1.5.1", true}, 344 | {"2.2.3", "1.5.1", false}, 345 | {"1.5.1", "1.5.1", true}, 346 | } 347 | 348 | for _, tc := range tests { 349 | v1, err := NewVersion(tc.v1) 350 | if err != nil { 351 | t.Errorf("Error parsing version: %s", err) 352 | } 353 | 354 | v2, err := NewVersion(tc.v2) 355 | if err != nil { 356 | t.Errorf("Error parsing version: %s", err) 357 | } 358 | 359 | a := v1.LessThanEqual(v2) 360 | e := tc.expected 361 | if a != e { 362 | t.Errorf( 363 | "Comparison of '%s' and '%s' failed. Expected '%t', got '%t'", 364 | tc.v1, tc.v2, e, a, 365 | ) 366 | } 367 | } 368 | } 369 | 370 | func TestGreaterThan(t *testing.T) { 371 | tests := []struct { 372 | v1 string 373 | v2 string 374 | expected bool 375 | }{ 376 | {"1.2.3", "1.5.1", false}, 377 | {"2.2.3", "1.5.1", true}, 378 | {"3.2-beta", "3.2-beta", false}, 379 | {"3.2.0-beta.1", "3.2.0-beta.5", false}, 380 | {"3.2-beta.4", "3.2-beta.2", true}, 381 | {"7.43.0-SNAPSHOT.99", "7.43.0-SNAPSHOT.103", false}, 382 | {"7.43.0-SNAPSHOT.FOO", "7.43.0-SNAPSHOT.103", true}, 383 | {"7.43.0-SNAPSHOT.99", "7.43.0-SNAPSHOT.BAR", false}, 384 | } 385 | 386 | for _, tc := range tests { 387 | v1, err := NewVersion(tc.v1) 388 | if err != nil { 389 | t.Errorf("Error parsing version: %s", err) 390 | } 391 | 392 | v2, err := NewVersion(tc.v2) 393 | if err != nil { 394 | t.Errorf("Error parsing version: %s", err) 395 | } 396 | 397 | a := v1.GreaterThan(v2) 398 | e := tc.expected 399 | if a != e { 400 | t.Errorf( 401 | "Comparison of '%s' and '%s' failed. Expected '%t', got '%t'", 402 | tc.v1, tc.v2, e, a, 403 | ) 404 | } 405 | } 406 | } 407 | 408 | func TestGreaterThanEqual(t *testing.T) { 409 | tests := []struct { 410 | v1 string 411 | v2 string 412 | expected bool 413 | }{ 414 | {"1.2.3", "1.5.1", false}, 415 | {"2.2.3", "1.5.1", true}, 416 | {"1.5.1", "1.5.1", true}, 417 | } 418 | 419 | for _, tc := range tests { 420 | v1, err := NewVersion(tc.v1) 421 | if err != nil { 422 | t.Errorf("Error parsing version: %s", err) 423 | } 424 | 425 | v2, err := NewVersion(tc.v2) 426 | if err != nil { 427 | t.Errorf("Error parsing version: %s", err) 428 | } 429 | 430 | a := v1.GreaterThanEqual(v2) 431 | e := tc.expected 432 | if a != e { 433 | t.Errorf( 434 | "Comparison of '%s' and '%s' failed. Expected '%t', got '%t'", 435 | tc.v1, tc.v2, e, a, 436 | ) 437 | } 438 | } 439 | } 440 | 441 | func TestEqual(t *testing.T) { 442 | tests := []struct { 443 | v1 *Version 444 | v2 *Version 445 | expected bool 446 | }{ 447 | {MustParse("1.2.3"), MustParse("1.5.1"), false}, 448 | {MustParse("2.2.3"), MustParse("1.5.1"), false}, 449 | {MustParse("3.2-beta"), MustParse("3.2-beta"), true}, 450 | {MustParse("3.2-beta+foo"), MustParse("3.2-beta+bar"), true}, 451 | {nil, nil, true}, 452 | {nil, MustParse("1.2.3"), false}, 453 | {MustParse("1.2.3"), nil, false}, 454 | } 455 | 456 | for _, tc := range tests { 457 | a := tc.v1.Equal(tc.v2) 458 | e := tc.expected 459 | if a != e { 460 | t.Errorf( 461 | "Comparison of '%s' and '%s' failed. Expected '%t', got '%t'", 462 | tc.v1, tc.v2, e, a, 463 | ) 464 | } 465 | } 466 | } 467 | 468 | func TestInc(t *testing.T) { 469 | tests := []struct { 470 | v1 string 471 | expected string 472 | how string 473 | expectedOriginal string 474 | }{ 475 | {"1.2.3", "1.2.4", "patch", "1.2.4"}, 476 | {"v1.2.4", "1.2.5", "patch", "v1.2.5"}, 477 | {"1.2.3", "1.3.0", "minor", "1.3.0"}, 478 | {"v1.2.4", "1.3.0", "minor", "v1.3.0"}, 479 | {"1.2.3", "2.0.0", "major", "2.0.0"}, 480 | {"v1.2.4", "2.0.0", "major", "v2.0.0"}, 481 | {"1.2.3+meta", "1.2.4", "patch", "1.2.4"}, 482 | {"1.2.3-beta+meta", "1.2.3", "patch", "1.2.3"}, 483 | {"v1.2.4-beta+meta", "1.2.4", "patch", "v1.2.4"}, 484 | {"1.2.3-beta+meta", "1.3.0", "minor", "1.3.0"}, 485 | {"v1.2.4-beta+meta", "1.3.0", "minor", "v1.3.0"}, 486 | {"1.2.3-beta+meta", "2.0.0", "major", "2.0.0"}, 487 | {"v1.2.4-beta+meta", "2.0.0", "major", "v2.0.0"}, 488 | } 489 | 490 | for _, tc := range tests { 491 | v1, err := NewVersion(tc.v1) 492 | if err != nil { 493 | t.Errorf("Error parsing version: %s", err) 494 | } 495 | var v2 Version 496 | switch tc.how { 497 | case "patch": 498 | v2 = v1.IncPatch() 499 | case "minor": 500 | v2 = v1.IncMinor() 501 | case "major": 502 | v2 = v1.IncMajor() 503 | } 504 | 505 | a := v2.String() 506 | e := tc.expected 507 | if a != e { 508 | t.Errorf( 509 | "Inc %q failed. Expected %q got %q", 510 | tc.how, e, a, 511 | ) 512 | } 513 | 514 | a = v2.Original() 515 | e = tc.expectedOriginal 516 | if a != e { 517 | t.Errorf( 518 | "Inc %q failed. Expected original %q got %q", 519 | tc.how, e, a, 520 | ) 521 | } 522 | } 523 | } 524 | 525 | func TestSetPrerelease(t *testing.T) { 526 | tests := []struct { 527 | v1 string 528 | prerelease string 529 | expectedVersion string 530 | expectedPrerelease string 531 | expectedOriginal string 532 | expectedErr error 533 | }{ 534 | {"1.2.3", "**", "1.2.3", "", "1.2.3", ErrInvalidPrerelease}, 535 | {"1.2.3", "beta", "1.2.3-beta", "beta", "1.2.3-beta", nil}, 536 | {"v1.2.4", "beta", "1.2.4-beta", "beta", "v1.2.4-beta", nil}, 537 | } 538 | 539 | for _, tc := range tests { 540 | v1, err := NewVersion(tc.v1) 541 | if err != nil { 542 | t.Errorf("Error parsing version: %s", err) 543 | } 544 | 545 | v2, err := v1.SetPrerelease(tc.prerelease) 546 | if err != tc.expectedErr { 547 | t.Errorf("Expected to get err=%s, but got err=%s", tc.expectedErr, err) 548 | } 549 | 550 | a := v2.Prerelease() 551 | e := tc.expectedPrerelease 552 | if a != e { 553 | t.Errorf("Expected prerelease value=%q, but got %q", e, a) 554 | } 555 | 556 | a = v2.String() 557 | e = tc.expectedVersion 558 | if a != e { 559 | t.Errorf("Expected version string=%q, but got %q", e, a) 560 | } 561 | 562 | a = v2.Original() 563 | e = tc.expectedOriginal 564 | if a != e { 565 | t.Errorf("Expected version original=%q, but got %q", e, a) 566 | } 567 | } 568 | } 569 | 570 | func TestSetMetadata(t *testing.T) { 571 | tests := []struct { 572 | v1 string 573 | metadata string 574 | expectedVersion string 575 | expectedMetadata string 576 | expectedOriginal string 577 | expectedErr error 578 | }{ 579 | {"1.2.3", "**", "1.2.3", "", "1.2.3", ErrInvalidMetadata}, 580 | {"1.2.3", "meta", "1.2.3+meta", "meta", "1.2.3+meta", nil}, 581 | {"v1.2.4", "meta", "1.2.4+meta", "meta", "v1.2.4+meta", nil}, 582 | } 583 | 584 | for _, tc := range tests { 585 | v1, err := NewVersion(tc.v1) 586 | if err != nil { 587 | t.Errorf("Error parsing version: %s", err) 588 | } 589 | 590 | v2, err := v1.SetMetadata(tc.metadata) 591 | if err != tc.expectedErr { 592 | t.Errorf("Expected to get err=%s, but got err=%s", tc.expectedErr, err) 593 | } 594 | 595 | a := v2.Metadata() 596 | e := tc.expectedMetadata 597 | if a != e { 598 | t.Errorf("Expected metadata value=%q, but got %q", e, a) 599 | } 600 | 601 | a = v2.String() 602 | e = tc.expectedVersion 603 | if e != a { 604 | t.Errorf("Expected version string=%q, but got %q", e, a) 605 | } 606 | 607 | a = v2.Original() 608 | e = tc.expectedOriginal 609 | if a != e { 610 | t.Errorf("Expected version original=%q, but got %q", e, a) 611 | } 612 | } 613 | } 614 | 615 | func TestOriginalVPrefix(t *testing.T) { 616 | tests := []struct { 617 | version string 618 | vprefix string 619 | }{ 620 | {"1.2.3", ""}, 621 | {"v1.2.4", "v"}, 622 | } 623 | 624 | for _, tc := range tests { 625 | v1, _ := NewVersion(tc.version) 626 | a := v1.originalVPrefix() 627 | e := tc.vprefix 628 | if a != e { 629 | t.Errorf("Expected vprefix=%q, but got %q", e, a) 630 | } 631 | } 632 | } 633 | 634 | func TestJsonMarshal(t *testing.T) { 635 | sVer := "1.1.1" 636 | x, err := StrictNewVersion(sVer) 637 | if err != nil { 638 | t.Errorf("Error creating version: %s", err) 639 | } 640 | out, err2 := json.Marshal(x) 641 | if err2 != nil { 642 | t.Errorf("Error marshaling version: %s", err2) 643 | } 644 | got := string(out) 645 | want := fmt.Sprintf("%q", sVer) 646 | if got != want { 647 | t.Errorf("Error marshaling unexpected marshaled content: got=%q want=%q", got, want) 648 | } 649 | } 650 | 651 | func TestJsonUnmarshal(t *testing.T) { 652 | sVer := "1.1.1" 653 | ver := &Version{} 654 | err := json.Unmarshal([]byte(fmt.Sprintf("%q", sVer)), ver) 655 | if err != nil { 656 | t.Errorf("Error unmarshaling version: %s", err) 657 | } 658 | got := ver.String() 659 | want := sVer 660 | if got != want { 661 | t.Errorf("Error unmarshaling unexpected object content: got=%q want=%q", got, want) 662 | } 663 | } 664 | 665 | func TestTextMarshal(t *testing.T) { 666 | sVer := "1.1.1" 667 | 668 | x, err := StrictNewVersion(sVer) 669 | if err != nil { 670 | t.Errorf("Error creating version: %s", err) 671 | } 672 | 673 | out, err2 := x.MarshalText() 674 | if err2 != nil { 675 | t.Errorf("Error marshaling version: %s", err2) 676 | } 677 | 678 | got := string(out) 679 | want := sVer 680 | if got != want { 681 | t.Errorf("Error marshaling unexpected marshaled content: got=%q want=%q", got, want) 682 | } 683 | } 684 | 685 | func TestTextUnmarshal(t *testing.T) { 686 | sVer := "1.1.1" 687 | ver := &Version{} 688 | 689 | err := ver.UnmarshalText([]byte(sVer)) 690 | if err != nil { 691 | t.Errorf("Error unmarshaling version: %s", err) 692 | } 693 | 694 | got := ver.String() 695 | want := sVer 696 | if got != want { 697 | t.Errorf("Error unmarshaling unexpected object content: got=%q want=%q", got, want) 698 | } 699 | } 700 | 701 | func TestSQLScanner(t *testing.T) { 702 | sVer := "1.1.1" 703 | x, err := StrictNewVersion(sVer) 704 | if err != nil { 705 | t.Errorf("Error creating version: %s", err) 706 | } 707 | var s sql.Scanner = x 708 | var out *Version 709 | var ok bool 710 | if out, ok = s.(*Version); !ok { 711 | t.Errorf("Error expected Version type, got=%T want=%T", s, Version{}) 712 | } 713 | got := out.String() 714 | want := sVer 715 | if got != want { 716 | t.Errorf("Error sql scanner unexpected scan content: got=%q want=%q", got, want) 717 | } 718 | } 719 | 720 | func TestDriverValuer(t *testing.T) { 721 | sVer := "1.1.1" 722 | x, err := StrictNewVersion(sVer) 723 | if err != nil { 724 | t.Errorf("Error creating version: %s", err) 725 | } 726 | got, err := x.Value() 727 | if err != nil { 728 | t.Fatalf("Error getting value, got %v", err) 729 | } 730 | want := sVer 731 | if got != want { 732 | t.Errorf("Error driver valuer unexpected value content: got=%q want=%q", got, want) 733 | } 734 | } 735 | 736 | func TestValidatePrerelease(t *testing.T) { 737 | tests := []struct { 738 | pre string 739 | expected error 740 | }{ 741 | {"foo", nil}, 742 | {"alpha.1", nil}, 743 | {"alpha.01", ErrSegmentStartsZero}, 744 | {"foo☃︎", ErrInvalidPrerelease}, 745 | {"alpha.0-1", nil}, 746 | } 747 | 748 | for _, tc := range tests { 749 | if err := validatePrerelease(tc.pre); err != tc.expected { 750 | t.Errorf("Unexpected error %q for prerelease %q", err, tc.pre) 751 | } 752 | } 753 | } 754 | 755 | func TestValidateMetadata(t *testing.T) { 756 | tests := []struct { 757 | meta string 758 | expected error 759 | }{ 760 | {"foo", nil}, 761 | {"alpha.1", nil}, 762 | {"alpha.01", nil}, 763 | {"foo☃︎", ErrInvalidMetadata}, 764 | {"alpha.0-1", nil}, 765 | {"al-pha.1Phe70CgWe050H9K1mJwRUqTNQXZRERwLOEg37wpXUb4JgzgaD5YkL52ABnoyiE", nil}, 766 | } 767 | 768 | for _, tc := range tests { 769 | if err := validateMetadata(tc.meta); err != tc.expected { 770 | t.Errorf("Unexpected error %q for metadata %q", err, tc.meta) 771 | } 772 | } 773 | } 774 | 775 | func FuzzNewVersion(f *testing.F) { 776 | testcases := []string{"v1.2.3", " ", "......", "1", "1.2.3-beta.1", "1.2.3+foo", "2.3.4-alpha.1+bar", "lorem ipsum"} 777 | 778 | for _, tc := range testcases { 779 | f.Add(tc) 780 | } 781 | 782 | f.Fuzz(func(_ *testing.T, a string) { 783 | _, _ = NewVersion(a) 784 | }) 785 | } 786 | 787 | func FuzzStrictNewVersion(f *testing.F) { 788 | testcases := []string{"v1.2.3", " ", "......", "1", "1.2.3-beta.1", "1.2.3+foo", "2.3.4-alpha.1+bar", "lorem ipsum"} 789 | 790 | for _, tc := range testcases { 791 | f.Add(tc) 792 | } 793 | 794 | f.Fuzz(func(_ *testing.T, a string) { 795 | _, _ = StrictNewVersion(a) 796 | }) 797 | } 798 | --------------------------------------------------------------------------------