├── .cloudignore ├── .gitattributes ├── .github └── CODEOWNERS ├── .gitignore ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── cloudbuild.yaml ├── doc.go ├── example_test.go ├── fmt.go ├── foreign_test.go ├── go.mod ├── legacy.go ├── legacy_test.go ├── range.go ├── range_test.go ├── semver.go ├── semver_386.s ├── semver_amd64.s ├── semver_generic.go ├── semver_native.go ├── semver_test.go ├── serialization_test.go ├── sort.go ├── sort_386.s ├── sort_amd64.s ├── sort_big.go ├── sort_generic.go ├── sort_native.go ├── sort_test.go └── testdata ├── gentoo-portage-PV.list └── regenerate.sh /.cloudignore: -------------------------------------------------------------------------------- 1 | .cloudignore 2 | .git* 3 | #!include:.gitignore 4 | 5 | go.sum 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4 2 | 3 | *.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 4 | .git* text eol=auto core.whitespace whitespace=trailing-space 5 | 6 | AUTHORS text eol=auto whitespace=trailing-space 7 | CONTRIBUTORS text eol=auto whitespace=trailing-space 8 | 9 | testdata/*.list -diff 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @wmark 2 | legacy* @mholt 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | ### Go ### 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | 30 | ### LaTeX ### 31 | *.acn 32 | *.acr 33 | *.alg 34 | *.aux 35 | *.bbl 36 | *.blg 37 | *.dvi 38 | *.fdb_latexmk 39 | *.glg 40 | *.glo 41 | *.gls 42 | *.idx 43 | *.ilg 44 | *.ind 45 | *.ist 46 | *.lof 47 | *.log 48 | *.lot 49 | *.maf 50 | *.mtc 51 | *.mtc0 52 | *.nav 53 | *.nlo 54 | *.out 55 | *.pdfsync 56 | *.ps 57 | *.snm 58 | *.synctex.gz 59 | *.toc 60 | *.vrb 61 | *.xdy 62 | *.tdo 63 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mark Kubacki 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Alvin Chung 2 | Gabriel Lima 3 | Matthew Holt 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014–2020 Mark Kubacki. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Semantic Versioning for Golang 2 | ============================== 3 | 4 | [![GoDoc](https://godoc.org/blitznote.com/src/semver?status.png)](https://godoc.org/blitznote.com/src/semver) 5 | 6 | A library for parsing and processing of *Versions* and *Ranges* in: 7 | 8 | * [Semantic Versioning](http://semver.org/) (semver) v2.0.0 notation 9 | * used by npmjs.org, pypi.org… 10 | * Gentoo's ebuild format 11 | * The fastest implementation, and the one that'll actually parse all semver variants correctly and without errors. 12 | * Sorting is in O(n). 13 | 14 | Does not rely on *regular expressions* neither does it use package *reflection*. 15 | 16 | ```bash 17 | $ sudo /bin/bash -c 'for g in /sys/bus/cpu/drivers/processor/cpu[0-9]*/cpufreq/scaling_governor; do echo performance >$g; done' 18 | $ sed -i -e 's@ignore@3rdparty@g' foreign_test.go 19 | $ go mod tidy 20 | $ go test -tags 3rdparty -run=XXX -benchmem -bench=. 21 | 22 | Benchmark_Compare-24 1.392 ns/op 0 B/op 0 allocs/op 23 | Benchmark_NewVersion-24 32.02 ns/op 0 B/op 0 allocs/op 24 | BenchmarkSemverNewRange-24 86.83 ns/op 0 B/op 0 allocs/op 25 | Benchmark_SortPtr-24 2768859 ns/op 26 | 27 | BenchmarkLibraryTwo_Compare-24 5.019 ns/op 0 B/op 0 allocs/op 28 | BenchmarkLibraryTwo_Make-24 299.5 ns/op 75 B/op 2 allocs/op 29 | BenchmarkLibraryTwo_ParseRange-24 1357 ns/op 456 B/op 13 allocs/op 30 | BenchmarkLibraryTwo_Sort-24 12771299 ns/op 31 | 32 | BenchmarkLibraryOne_Compare-24 1442 ns/op 480 B/op 17 allocs/op 33 | BenchmarkLibraryOne_NewVersion-24 1516 ns/op 535 B/op 6 allocs/op 34 | BenchmarkLibraryOne_NewConstraint-24 7024 ns/op 2092 B/op 18 allocs/op 35 | BenchmarkLibraryOne_SortPtr-24 668274885 ns/op 36 | 37 | # AMD Epyc 7401P, Linux 5.12.10, Go 1.16.5 38 | # - LibraryOne v1.3.0 sometimes segfaults 39 | # - LibraryTwo v4 errors on 19.4% of the given versions 40 | ``` 41 | 42 | Licensed under a [BSD-style license](LICENSE). 43 | 44 | Usage 45 | ----- 46 | 47 | Using _go modules_ you'd just: 48 | 49 | ```go 50 | import "blitznote.com/src/semver/v3" 51 | ``` 52 | 53 | … or, with older versions of _Go_ leave out the version suffix `/v…` and: 54 | 55 | ```bash 56 | $ dep ensure --add blitznote.com/src/semver@^3 57 | ``` 58 | 59 | After which you can use the module as usual, like this: 60 | 61 | ```go 62 | v1 := semver.MustParse("1.2.3-beta") 63 | v2 := semver.MustParse("2.0.0-alpha20140805.456-rc3+build1800") 64 | v1.Less(v2) // true 65 | 66 | r1, _ := NewRange("~1.2") 67 | r1.Contains(v1) // true 68 | r1.IsSatisfiedBy(v1) // false (pre-releases don't satisfy) 69 | ``` 70 | 71 | Also check its [go.dev](https://pkg.go.dev/blitznote.com/src/semver/v3?tab=overview) listing 72 | and [Gentoo Linux Ebuild File Format](http://devmanual.gentoo.org/ebuild-writing/file-format/), 73 | [Gentoo's notation of dependencies](http://devmanual.gentoo.org/general-concepts/dependencies/). 74 | 75 | Please Note 76 | ----------- 77 | 78 | It is, ordered from lowest to highest: 79 | 80 | alpha < beta < pre < rc < (no release type/»common«) < r (revision) < p 81 | 82 | Therefore it is: 83 | 84 | Version("1.0.0-pre1") < Version("1.0.0") < Version("1.0.0-p1") 85 | 86 | ### Limitations 87 | 88 | Version 2 no longer supports dot-tag notation. 89 | That is, `1.8.rc2` will be rejected, valid are `1.8rc2` and `1.8-rc2`. 90 | 91 | Contribute 92 | ---------- 93 | 94 | Pull requests are welcome. 95 | 96 | For anything written in Assembly, please contribute your implementation for one 97 | architecture only at first. We'll work with this and once it's in, follow up 98 | with more if you like. 99 | 100 | Please add your name and email address to a file *AUTHORS* and/or *CONTRIBUTORS*. 101 | Thanks! 102 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | tags: ['golang'] 2 | timeout: 180s 3 | options: 4 | env: 5 | - 'GOMODCACHE=/var/lib/go' 6 | - 'GOCACHE=/var/cache/go' 7 | - 'PROJECT_ID=${PROJECT_ID}' 8 | - 'REPO_NAME=${REPO_NAME}' 9 | - 'GOPROXY=off' 10 | volumes: 11 | - name: 'GOMODCACHE' 12 | path: '/var/lib/go' 13 | - name: 'GOCACHE' 14 | path: '/var/cache/go' 15 | 16 | steps: 17 | - name: 'gcr.io/cloud-builders/docker' 18 | id: 'get golang' 19 | waitFor: ['-'] 20 | entrypoint: 'bash' 21 | args: 22 | - -c 23 | - | 24 | set -ex 25 | if ! docker tag golang:cloudbuild_cache localhost/golang:latest; then 26 | docker pull mirror.gcr.io/library/golang:latest 27 | docker tag {mirror.gcr.io/library,localhost}/golang:latest 28 | fi 29 | - name: 'localhost/golang' 30 | id: 'gofmt' 31 | entrypoint: 'bash' 32 | args: ['-c', 'diff <(echo -n) <(gofmt -s -d $(find . -type f -name "*.go" -not -path "./_*"))'] 33 | - name: 'gcr.io/blitznote/golang/ineffassign' 34 | id: 'ineffassign' 35 | waitFor: ['gofmt'] 36 | args: ['.'] 37 | - name: 'gcr.io/blitznote/golang/golint' 38 | id: 'lint' 39 | waitFor: ['gofmt'] 40 | 41 | - name: 'gcr.io/blitznote/cacheutil' 42 | id: 'restore cached var-lib-go' 43 | waitFor: ['gofmt', 'ineffassign', 'lint'] 44 | args: ['restore', '/var/lib/go'] 45 | - name: 'gcr.io/blitznote/cacheutil' 46 | id: 'restore cached var-cache-go' 47 | waitFor: ['gofmt', 'ineffassign', 'lint'] 48 | args: ['restore', '/var/cache/go'] 49 | - name: 'localhost/golang' 50 | id: 'get dependencies' 51 | waitFor: ['gofmt', 'ineffassign', 'lint', 'restore cached var-lib-go'] 52 | env: ['GOPROXY=https://proxy.golang.org,direct'] 53 | entrypoint: 'go' 54 | args: ['mod', 'download'] 55 | 56 | - name: 'localhost/golang' 57 | id: 'pilot build, amd64' 58 | waitFor: ['get dependencies', 'restore cached var-cache-go'] 59 | env: ['GOARCH=amd64'] 60 | entrypoint: 'go' 61 | args: ['build', '.', 'errors'] 62 | - name: 'localhost/golang' 63 | id: 'vet, amd64' 64 | waitFor: ['pilot build, amd64'] 65 | env: ['GOARCH=amd64'] 66 | entrypoint: 'go' 67 | args: ['vet', '.'] 68 | - name: 'localhost/golang' 69 | id: 'test, amd64' 70 | waitFor: ['vet, amd64'] 71 | env: ['GOARCH=amd64'] 72 | entrypoint: 'go' 73 | args: ['test'] 74 | 75 | - name: 'localhost/golang' 76 | id: 'pilot build, x86' 77 | waitFor: ['get dependencies', 'restore cached var-cache-go'] 78 | env: ['GOARCH=386'] 79 | entrypoint: 'go' 80 | args: ['build', '.', 'errors'] 81 | - name: 'localhost/golang' 82 | id: 'vet, x86' 83 | waitFor: ['pilot build, x86'] 84 | env: ['GOARCH=386'] 85 | entrypoint: 'go' 86 | args: ['vet', '.'] 87 | - name: 'localhost/golang' 88 | id: 'test, x86' 89 | waitFor: ['vet, x86'] 90 | env: ['GOARCH=386'] 91 | entrypoint: 'go' 92 | args: ['test'] 93 | 94 | - name: 'localhost/golang' 95 | id: 'pilot build, purego' 96 | waitFor: ['get dependencies', 'restore cached var-cache-go'] 97 | entrypoint: 'go' 98 | args: ['build', '-tags', 'purego', '.', 'errors'] 99 | - name: 'localhost/golang' 100 | id: 'vet, purego' 101 | waitFor: ['pilot build, purego'] 102 | entrypoint: 'go' 103 | args: ['vet', '-tags', 'purego', '.'] 104 | - name: 'localhost/golang' 105 | id: 'test, purego' 106 | waitFor: ['vet, purego'] 107 | entrypoint: 'go' 108 | args: ['test', '-tags', 'purego'] 109 | 110 | # Cannot run tests for these architectures on this CI. 111 | - name: 'localhost/golang' 112 | id: 'pilot build, arm' 113 | waitFor: ['get dependencies', 'restore cached var-cache-go'] 114 | env: ['GOARCH=arm'] 115 | entrypoint: 'go' 116 | args: ['build', '.', 'errors'] 117 | - name: 'localhost/golang' 118 | id: 'vet, arm' 119 | waitFor: ['pilot build, arm'] 120 | env: ['GOARCH=arm'] 121 | entrypoint: 'go' 122 | args: ['vet', '.'] 123 | 124 | - name: 'localhost/golang' 125 | id: 'pilot build, mips' 126 | waitFor: ['get dependencies', 'restore cached var-cache-go'] 127 | env: ['GOARCH=mips'] 128 | entrypoint: 'go' 129 | args: ['build', '.', 'errors'] 130 | - name: 'localhost/golang' 131 | id: 'vet, mips' 132 | waitFor: ['pilot build, mips'] 133 | env: ['GOARCH=mips'] 134 | entrypoint: 'go' 135 | args: ['vet', '.'] 136 | 137 | # fin 138 | - name: 'gcr.io/blitznote/cacheutil' 139 | id: 'stash cached var-cache-go' 140 | args: ['stash', '/var/cache/go'] 141 | - name: 'gcr.io/blitznote/cacheutil' 142 | id: 'stash cached var-lib-go' 143 | waitFor: ['get dependencies'] 144 | args: ['stash', '/var/lib/go'] 145 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package semver contains types and functions for 6 | // parsing of Versions and (Version-)Ranges. 7 | package semver // import "blitznote.com/src/semver" 8 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver_test 6 | 7 | import ( 8 | "fmt" 9 | 10 | "blitznote.com/src/semver/v3" 11 | ) 12 | 13 | func Example_version() { 14 | v1 := semver.MustParse("1.2.3-beta") 15 | v2 := semver.MustParse("2.0.0-alpha20140805.456-rc3+build1800") 16 | 17 | fmt.Println(v1.Less(&v2)) 18 | 19 | // Output: true 20 | } 21 | 22 | func Example_range() { 23 | v1 := semver.MustParse("1.2.3-beta") 24 | r1, _ := semver.NewRange([]byte("~1.2")) 25 | 26 | fmt.Println(r1.Contains(v1)) 27 | fmt.Println(r1.IsSatisfiedBy(v1)) // pre-releases don't satisfy 28 | 29 | // Output: 30 | // true 31 | // false 32 | } 33 | 34 | func ExampleCompare() { 35 | v1 := semver.MustParse("v1") 36 | v2 := semver.MustParse("v2.0") 37 | v3 := semver.MustParse("v3.0.0") 38 | 39 | fmt.Println("Compare", v3, v2, "=", semver.Compare(&v3, &v2)) 40 | fmt.Println("Compare", v2, v2, "=", semver.Compare(&v2, &v2)) 41 | fmt.Println("Compare", v1, v2, "=", semver.Compare(&v1, &v2)) 42 | 43 | // Output: 44 | // Compare 3.0.0 2.0.0 = 1 45 | // Compare 2.0.0 2.0.0 = 0 46 | // Compare 1.0.0 2.0.0 = -1 47 | } 48 | 49 | func ExampleRange_Contains_first() { 50 | v := semver.MustParse("1.4.3") 51 | r, _ := semver.NewRange([]byte("^1.2")) 52 | 53 | fmt.Println(r.Contains(v)) 54 | 55 | // Output: true 56 | } 57 | 58 | func ExampleRange_Contains_second() { 59 | v := semver.MustParse("1.4.3") 60 | r, _ := semver.NewRange([]byte("1.2 <2.0.0")) 61 | 62 | fmt.Println(r.Contains(v)) 63 | 64 | // Output: true 65 | } 66 | 67 | func ExampleRange_Contains_prerelases() { 68 | v := semver.MustParse("1.4.3-beta") 69 | r, _ := semver.NewRange([]byte("1.2 <2.0.0")) 70 | 71 | fmt.Println(r.Contains(v)) 72 | 73 | // Output: true 74 | } 75 | 76 | func ExampleRange_GetLowerBoundary() { 77 | r, _ := semver.NewRange([]byte("^1.2")) 78 | fmt.Println(*r.GetLowerBoundary()) 79 | 80 | // Output: 81 | // 1.2.0 82 | } 83 | 84 | func ExampleRange_GetUpperBoundary_first() { 85 | r, _ := semver.NewRange([]byte("1.2 <2.0.0")) 86 | fmt.Println(*r.GetUpperBoundary()) 87 | 88 | // Output: 89 | // 2.0.0 90 | } 91 | 92 | func ExampleRange_GetUpperBoundary_second() { 93 | r, _ := semver.NewRange([]byte("~1.2.3")) 94 | fmt.Println(*r.GetUpperBoundary()) 95 | 96 | // Output: 97 | // 1.3.0 98 | } 99 | 100 | func ExampleRange_IsSatisfiedBy_full() { 101 | v := semver.MustParse("1.2.3") 102 | r, _ := semver.NewRange([]byte("~1.2")) 103 | 104 | fmt.Println(r.IsSatisfiedBy(v)) 105 | 106 | // Output: true 107 | } 108 | 109 | func ExampleRange_IsSatisfiedBy_prerelases() { 110 | // Unlike with Contains, this won't select prereleases. 111 | pre := semver.MustParse("1.2.3-beta") 112 | r, _ := semver.NewRange([]byte("~1.2")) 113 | 114 | fmt.Println(r.IsSatisfiedBy(pre)) 115 | 116 | // Output: false 117 | } 118 | 119 | func ExampleMustParse() { 120 | v := semver.MustParse("v1.14") 121 | fmt.Println(v) 122 | 123 | // Output: 124 | // 1.14.0 125 | } 126 | 127 | func ExampleNewVersion() { 128 | for _, str := range []string{"v1.14", "6.0.2.1", "14b6"} { 129 | v, err := semver.NewVersion([]byte(str)) 130 | fmt.Println(v, err) 131 | } 132 | 133 | // Output: 134 | // 1.14.0 135 | // 6.0.2.1 136 | // 14.0.0 Given string does not resemble a Version 137 | } 138 | 139 | func ExampleVersion_Bytes_first() { 140 | v := semver.MustParse("1.0") 141 | fmt.Println(v.Bytes()) 142 | 143 | // Output: 144 | // [49] 145 | } 146 | 147 | func ExampleVersion_Bytes_second() { 148 | v := semver.MustParse("4.8") 149 | fmt.Println(v.Bytes()) 150 | 151 | // Output: 152 | // [52 46 56] 153 | } 154 | 155 | func ExampleVersion_Bytes_minimal() { 156 | v := semver.MustParse("1.13beta") 157 | 158 | fmt.Println("Bytes() =", string(v.Bytes())) 159 | fmt.Println("String() =", v.String()) 160 | 161 | // Output: 162 | // Bytes() = 1.13-beta 163 | // String() = 1.13.0-beta 164 | } 165 | 166 | func ExampleVersion_IsAPreRelease() { 167 | v, pre := semver.MustParse("1.12"), semver.MustParse("1.13beta") 168 | 169 | fmt.Println(v, "is a pre-release:", v.IsAPreRelease()) 170 | fmt.Println(pre, "is a pre-release:", pre.IsAPreRelease()) 171 | 172 | // Output: 173 | // 1.12.0 is a pre-release: false 174 | // 1.13.0-beta is a pre-release: true 175 | } 176 | 177 | func ExampleVersion_Less() { 178 | l, r := semver.MustParse("v2"), semver.MustParse("v3") 179 | fmt.Println(l.Less(&r), ",", l, "is 'less' than", r) 180 | 181 | l, r = semver.MustParse("v1+build7"), semver.MustParse("v1+build9") 182 | fmt.Println(l.Less(&r), ",", l, "is 'less' than", r) 183 | 184 | // Output: 185 | // true , 2.0.0 is 'less' than 3.0.0 186 | // true , 1.0.0+build7 is 'less' than 1.0.0+build9 187 | } 188 | 189 | func ExampleVersion_LimitedEqual_first() { 190 | // The version prefix does, but the first pre-release type does not match. 191 | pre := semver.MustParse("1.0.0-pre") 192 | rc := semver.MustParse("1.0.0-rc") 193 | 194 | fmt.Println(pre.LimitedEqual(rc)) 195 | 196 | // Output: false 197 | } 198 | 199 | func ExampleVersion_LimitedEqual_second() { 200 | // The difference is beyond LimitedEqual's cutoff, so these "equal". 201 | a := semver.MustParse("1.0.0-beta-pre3") 202 | b := semver.MustParse("1.0.0-beta-pre5") 203 | 204 | fmt.Println(a.LimitedEqual(b)) 205 | 206 | // Output: true 207 | } 208 | 209 | func ExampleVersion_LimitedEqual_third() { 210 | regular := semver.MustParse("1.0.0") 211 | patched := semver.MustParse("1.0.0-p1") 212 | 213 | // A patched version supposedly does more, so is more than; and its unequal. 214 | fmt.Println(patched.LimitedEqual(regular)) 215 | // This will work because the regular version is a subset is in its subset. 216 | fmt.Println(regular.LimitedEqual(patched)) 217 | 218 | // Output: 219 | // false 220 | // true 221 | } 222 | 223 | func ExampleVersion_Major() { 224 | v := semver.MustParse("v1.2.3") 225 | fmt.Println(v.Major()) 226 | // Output: 1 227 | } 228 | 229 | func ExampleVersion_Minor() { 230 | v := semver.MustParse("v1.2.3") 231 | fmt.Println(v.Minor()) 232 | // Output: 2 233 | } 234 | 235 | func ExampleVersion_Patch() { 236 | v := semver.MustParse("v1.2.3") 237 | fmt.Println(v.Patch()) 238 | // Output: 3 239 | } 240 | 241 | func ExampleVersion_Scan() { 242 | a, b, c := new(semver.Version), new(semver.Version), new(semver.Version) 243 | errA := a.Scan("5.5.65") 244 | errB := b.Scan(int64(12)) 245 | errC := c.Scan(-1) 246 | 247 | fmt.Println(a, errA) 248 | fmt.Println(b, errB) 249 | fmt.Println(c, errC) 250 | 251 | // Output: 252 | // 5.5.65 253 | // 12.0.0 254 | // 0.0.0 Cannot read this type into a Version 255 | } 256 | 257 | func ExampleVersion_String() { 258 | str := "v2.1" 259 | v := semver.MustParse(str) 260 | fmt.Println(str, "is", v.String(), "but as Bytes():", string(v.Bytes())) 261 | 262 | // Output: 263 | // v2.1 is 2.1.0 but as Bytes(): 2.1 264 | } 265 | -------------------------------------------------------------------------------- /fmt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "strconv" 9 | ) 10 | 11 | func numDecimalPlaces(n int32) int { 12 | var i int 13 | for i = 1; n > 9; i++ { 14 | n = n / 10 15 | } 16 | return i 17 | } 18 | 19 | // Serialize builds a minimal human-readable representation of this Version, 20 | // and returns it as slice. 21 | // Set |minPlaces| to how many columns the prefix must contain. 22 | func (t Version) serialize(minPlaces int, quoted bool) []byte { 23 | var idx, lastNonZero, bytesNeeded int 24 | 25 | if quoted { 26 | bytesNeeded = 2 27 | } 28 | 29 | for idx, elem := range t.version { 30 | if elem != 0 { 31 | lastNonZero = idx 32 | } 33 | } 34 | 35 | // Determine how much target space is needed (i.e. the string length). 36 | for idx = 0; idx < len(t.version); idx += 5 { 37 | switch { 38 | case t.version[idx+3] != 0 || minPlaces >= idx+4: 39 | bytesNeeded += 1 + numDecimalPlaces(t.version[idx+3]) 40 | fallthrough 41 | case t.version[idx+2] != 0 || minPlaces >= idx+3: 42 | bytesNeeded += 1 + numDecimalPlaces(t.version[idx+2]) 43 | fallthrough 44 | case t.version[idx+1] != 0 || minPlaces >= idx+2: 45 | bytesNeeded += 1 + numDecimalPlaces(t.version[idx+1]) 46 | fallthrough 47 | default: 48 | bytesNeeded += numDecimalPlaces(t.version[idx]) 49 | } 50 | if idx+4 >= len(t.version) { 51 | break 52 | } 53 | 54 | if idx+4 <= lastNonZero { // X.Y.Z.N - ?a.b.c.d 55 | bytesNeeded++ 56 | } 57 | if t.version[idx+4] != 0 { // alpha, beta, … 58 | bytesNeeded += len(releaseDesc[int(t.version[idx+4])]) 59 | } 60 | 61 | if lastNonZero <= idx+4 { // We're done if the remainder is empty. 62 | break 63 | } 64 | } 65 | if t.build != 0 { 66 | bytesNeeded += len("+build") + numDecimalPlaces(t.build) 67 | } 68 | 69 | // Build the string representation 70 | target := make([]byte, 0, bytesNeeded) 71 | 72 | if quoted { 73 | target = append(target, '"') 74 | } 75 | 76 | for idx = 0; idx < len(t.version); idx += 5 { 77 | switch { 78 | case t.version[idx+3] != 0 || minPlaces >= 4: 79 | target = strconv.AppendUint(target, uint64(t.version[idx]), 10) 80 | target = append(target, '.') 81 | target = strconv.AppendUint(target, uint64(t.version[idx+1]), 10) 82 | target = append(target, '.') 83 | target = strconv.AppendUint(target, uint64(t.version[idx+2]), 10) 84 | target = append(target, '.') 85 | target = strconv.AppendUint(target, uint64(t.version[idx+3]), 10) 86 | case t.version[idx+2] != 0 || minPlaces >= 3: 87 | target = strconv.AppendUint(target, uint64(t.version[idx]), 10) 88 | target = append(target, '.') 89 | target = strconv.AppendUint(target, uint64(t.version[idx+1]), 10) 90 | target = append(target, '.') 91 | target = strconv.AppendUint(target, uint64(t.version[idx+2]), 10) 92 | case t.version[idx+1] != 0 || minPlaces >= 2: 93 | target = strconv.AppendUint(target, uint64(t.version[idx]), 10) 94 | target = append(target, '.') 95 | target = strconv.AppendUint(target, uint64(t.version[idx+1]), 10) 96 | default: 97 | target = strconv.AppendUint(target, uint64(t.version[idx]), 10) 98 | } 99 | if idx+4 >= len(t.version) { 100 | break 101 | } 102 | 103 | if idx+4 <= lastNonZero { 104 | target = append(target, '-') 105 | } 106 | if t.version[idx+4] != 0 { 107 | target = append(target, []byte(releaseDesc[int(t.version[idx+4])])...) 108 | } 109 | 110 | if lastNonZero <= idx+4 { 111 | break 112 | } 113 | minPlaces -= 5 114 | } 115 | if t.build != 0 { 116 | target = append(target, buildsuffix...) 117 | target = strconv.AppendUint(target, uint64(t.build), 10) 118 | } 119 | if quoted { 120 | target = append(target, '"') 121 | } 122 | 123 | return target 124 | } 125 | 126 | // Bytes returns a slice with the minimal human-readable representation of this Version. 127 | // 128 | // Unlike String(), which returns a minimum of columns, 129 | // this will conserve space at the expense of legibility. 130 | // In other words, `len(v.Bytes()) ≤ len(v.String())`. 131 | func (t Version) Bytes() []byte { 132 | return t.serialize(0, false) 133 | } 134 | 135 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 136 | // 137 | // Anecdotically, encoders for binary protocols use this. 138 | func (t Version) MarshalBinary() ([]byte, error) { 139 | return t.serialize(0, false), nil 140 | } 141 | 142 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 143 | func (t *Version) UnmarshalBinary(b []byte) error { 144 | return t.UnmarshalText(b) 145 | } 146 | 147 | // String returns the string representation of t. 148 | // 149 | // Anecdotically, fmt.Println will use this. 150 | func (t Version) String() string { 151 | return string(t.serialize(3, false)) 152 | } 153 | 154 | // MarshalJSON implements the json.Marshaler interface. 155 | func (t Version) MarshalJSON() ([]byte, error) { 156 | return t.serialize(0, true), nil 157 | } 158 | 159 | // MarshalText implements the encoding.TextMarshaler interface. 160 | // 161 | // Anecdotically, anything that writes XML will use this. 162 | func (t Version) MarshalText() ([]byte, error) { 163 | return t.serialize(0, false), nil 164 | } 165 | 166 | // UnmarshalJSON implements the json.Unmarshaler interface. 167 | func (t *Version) UnmarshalJSON(b []byte) error { 168 | if len(b) > 2 && b[0] == '"' || b[0] == '\'' || b[0] == '`' { 169 | // We can ignore the closing because the JSON engine will throw an error on any mismatch for us. 170 | return t.Parse(string(b[1 : len(b)-1])) 171 | } 172 | return t.Parse(string(b)) 173 | } 174 | 175 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 176 | func (t *Version) UnmarshalText(b []byte) error { 177 | t.version = [14]int32{} 178 | t.build = 0 179 | return t.unmarshalText(b) 180 | } 181 | 182 | // Scan implements the sql.Scanner interface. 183 | func (t *Version) Scan(src interface{}) error { 184 | switch v := src.(type) { 185 | case int64: 186 | if v >= 0 && v <= (1<<31-1) { // v ≤ MaxInt32 187 | *t = Version{} 188 | t.version[0] = int32(v) // It's a pristine Version, initialized to {0}. 189 | return nil 190 | } 191 | return errOutOfBounds 192 | case []byte: 193 | *t = Version{} 194 | return t.unmarshalText(v) 195 | case string: 196 | *t = Version{} 197 | return t.unmarshalText([]byte(v)) 198 | } 199 | 200 | return errInvalidType 201 | } 202 | 203 | // Value implements the driver.Valuer interface, as found in database/sql. 204 | // 205 | // Deprecated: Use string(Version) instead. 206 | func (t Version) Value() (interface{}, error) { 207 | return t.String(), nil 208 | } 209 | -------------------------------------------------------------------------------- /foreign_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package semver 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "sort" 9 | "testing" 10 | 11 | blang "github.com/blang/semver/v4" 12 | hashicorp "github.com/hashicorp/go-version" 13 | ) 14 | 15 | var benchLibraryOneV, benchLibraryOneErr = hashicorp.NewVersion(strForBenchmarks) 16 | 17 | func BenchmarkLibraryOne_NewVersion(b *testing.B) { 18 | var v, e = hashicorp.NewVersion(strForBenchmarks) 19 | lim := len(VersionsFromGentoo) 20 | 21 | for n := 0; n < b.N; n++ { 22 | v, e = hashicorp.NewVersion(string(VersionsFromGentoo[n%lim])) 23 | } 24 | benchLibraryOneV, benchLibraryOneErr = v, e 25 | } 26 | 27 | var benchLibraryOneR, benchLibraryOneRErr = hashicorp.NewConstraint(">=1.2.3, <=1.3.0") 28 | 29 | func BenchmarkLibraryOne_NewConstraint(b *testing.B) { 30 | var r, e = hashicorp.NewConstraint(">=1.2.3, <=1.3.0") 31 | for n := 0; n < b.N; n++ { 32 | r, e = hashicorp.NewConstraint(">=1.2.3, <=1.3.0") 33 | } 34 | benchLibraryOneR, benchLibraryOneRErr = r, e 35 | } 36 | 37 | var benchLibraryOneResult = 5 38 | 39 | func BenchmarkLibraryOne_Compare(b *testing.B) { 40 | var v, _ = hashicorp.NewVersion(strForBenchmarks) 41 | r := benchLibraryOneV.Compare(v) 42 | for n := 0; n < b.N; n++ { 43 | r = benchLibraryOneV.Compare(v) 44 | } 45 | benchLibraryOneResult = r 46 | } 47 | 48 | func BenchmarkLibraryOne_SortPtr(b *testing.B) { 49 | b.StopTimer() 50 | var erroneous int 51 | unsorted := make([]*hashicorp.Version, len(VersionsFromGentoo)) 52 | for n, src := range VersionsFromGentoo { 53 | if v, err := hashicorp.NewVersion(string(src)); err == nil { 54 | unsorted[n] = v 55 | } else { 56 | substitute := fmt.Sprintf("%s.%d", strForBenchmarks, rand.Intn(len(VersionsFromGentoo))) 57 | unsorted[n], _ = hashicorp.NewVersion(substitute) 58 | erroneous++ 59 | } 60 | } 61 | b.ReportMetric(float64(erroneous)/float64(len(unsorted)), "substitutes/op") 62 | data := make([]*hashicorp.Version, len(unsorted)) 63 | 64 | for i := 0; i < b.N; i++ { 65 | copy(data, unsorted) 66 | b.StartTimer() 67 | sort.Sort(hashicorp.Collection(data)) 68 | b.StopTimer() 69 | } 70 | } 71 | 72 | // Blang published their library after mine, and doing so even did imitate parts 73 | // of my first release. 74 | // Yet, as of writing this, their error rate is a staggering 30%. 75 | 76 | var benchLibraryTwoV, benchLibraryTwoErr = blang.Make(strForBenchmarks) 77 | 78 | func BenchmarkLibraryTwo_Make(b *testing.B) { 79 | var v, e = blang.Make(strForBenchmarks) 80 | lim := len(VersionsFromGentoo) 81 | 82 | for n := 0; n < b.N; n++ { 83 | v, e = blang.Make(string(VersionsFromGentoo[n%lim])) 84 | } 85 | benchLibraryTwoV, benchLibraryTwoErr = v, e 86 | } 87 | 88 | var benchLibraryTwoR, benchLibraryTwoRErr = blang.ParseRange(">=1.2.3 <=1.3.0") 89 | 90 | func BenchmarkLibraryTwo_ParseRange(b *testing.B) { 91 | var r, e = blang.ParseRange(">=1.2.3 <=1.3.0") 92 | for n := 0; n < b.N; n++ { 93 | r, e = blang.ParseRange(">=1.2.3 <=1.3.0") 94 | } 95 | benchLibraryTwoR, benchLibraryTwoRErr = r, e 96 | } 97 | 98 | var benchLibraryTwoResult = 5 99 | 100 | func BenchmarkLibraryTwo_Compare(b *testing.B) { 101 | var v, _ = blang.Make(strForBenchmarks) 102 | r := benchLibraryTwoV.Compare(v) 103 | for n := 0; n < b.N; n++ { 104 | r = benchLibraryTwoV.Compare(v) 105 | } 106 | benchLibraryTwoResult = r 107 | } 108 | 109 | func BenchmarkLibraryTwo_Sort(b *testing.B) { 110 | b.StopTimer() 111 | var erroneous int 112 | unsorted := make([]blang.Version, len(VersionsFromGentoo)) 113 | for n, src := range VersionsFromGentoo { 114 | if v, err := blang.ParseTolerant(string(src)); err == nil { 115 | unsorted[n] = v 116 | } else { 117 | substitute := fmt.Sprintf("%s.%d", strForBenchmarks, rand.Intn(len(VersionsFromGentoo))) 118 | unsorted[n], _ = blang.ParseTolerant(substitute) 119 | erroneous++ 120 | } 121 | } 122 | b.ReportMetric(float64(erroneous)/float64(len(unsorted)), "substitutes/op") 123 | data := make([]blang.Version, len(unsorted)) 124 | 125 | for i := 0; i < b.N; i++ { 126 | copy(data, unsorted) 127 | b.StartTimer() 128 | sort.Sort(blang.Versions(data)) 129 | b.StopTimer() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module blitznote.com/src/semver/v3 2 | 3 | go 1.16 4 | 5 | require github.com/smartystreets/goconvey v1.6.4 6 | -------------------------------------------------------------------------------- /legacy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The methods contained herein are considered “legacy” and 6 | // are marked for removal if any errors come up and nobody steps up 7 | // to submit patches. 8 | 9 | package semver 10 | 11 | // NextVersions returns a list of possible next versions after t. For each of 12 | // the three version points, pre-releases are given as options starting with 13 | // the minimum release type (-4 <= 0), and those release types are numbered 14 | // if numberedPre is true. Release types: 15 | // 16 | // alpha: -4 17 | // beta: -3 18 | // pre: -2 19 | // rc: -1 20 | // common: 0 21 | // 22 | // Thus, if you don't want any pre-release options, set minReleaseType to 0. 23 | // 24 | // Deprecated: This is a legacy method for the Caddyserver's build infrastructure. 25 | // Do not rely on it, they are free to~ and can change it anytime. 26 | func (t Version) NextVersions(minReleaseType int, numberedPre bool) []*Version { 27 | var next []*Version 28 | 29 | if minReleaseType < alpha || minReleaseType > common { 30 | return next 31 | } 32 | 33 | // if this is a pre-release, suggest next pre-releases or 34 | // common of same version 35 | for releaseType := t.version[idxReleaseType]; releaseType < common; releaseType++ { 36 | if releaseType == t.version[idxReleaseType] { 37 | if !numberedPre { 38 | continue 39 | } 40 | ver := t 41 | ver.version[idxRelease]++ 42 | next = append(next, &ver) 43 | } else { 44 | ver := t 45 | ver.version[idxReleaseType] = releaseType 46 | if numberedPre { 47 | ver.version[idxRelease] = 1 48 | } else { 49 | ver.version[idxRelease] = 0 50 | } 51 | next = append(next, &ver) 52 | } 53 | } 54 | if t.version[idxReleaseType] < common { 55 | ver := t 56 | ver.version[idxReleaseType] = common 57 | ver.version[idxRelease] = 0 58 | next = append(next, &ver) 59 | } 60 | 61 | // if the current version is at least common release type, 62 | // suggest patch or revision if not one of those already 63 | if t.version[idxReleaseType] == common || 64 | t.version[idxReleaseType] == patch { 65 | ver := t 66 | ver.version[idxReleaseType] = revision 67 | ver.version[idxRelease] = 1 68 | next = append(next, &ver) 69 | } 70 | if t.version[idxReleaseType] == common || 71 | t.version[idxReleaseType] == revision { 72 | ver := t 73 | ver.version[idxReleaseType] = patch 74 | ver.version[idxRelease] = 1 75 | next = append(next, &ver) 76 | } 77 | 78 | for i := idxReleaseType - 2; 0 <= i; i-- { 79 | // for each version point, iterate the release types within desired bounds 80 | for releaseType := int32(minReleaseType); releaseType <= common; releaseType++ { 81 | ver := t 82 | ver.version[i]++ 83 | for j := i + 1; j < len(ver.version); j++ { 84 | ver.version[j] = 0 // when incrementing, reset next points to 0 85 | } 86 | if i == 2 && releaseType < common { 87 | continue // patches seldom have pre-releases 88 | } 89 | ver.version[idxReleaseType] = releaseType 90 | if releaseType < common { 91 | if numberedPre { 92 | ver.version[idxRelease] = 1 93 | } else { 94 | ver.version[idxRelease] = 0 95 | } 96 | } 97 | next = append(next, &ver) 98 | } 99 | } 100 | 101 | return next 102 | } 103 | -------------------------------------------------------------------------------- /legacy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestNextVersions(t *testing.T) { 15 | toStr := func(list []*Version) []string { 16 | ss := make([]string, len(list)) 17 | for i := range list { 18 | ss[i] = fmt.Sprintf("%v", list[i]) 19 | } 20 | return ss 21 | } 22 | 23 | Convey("NextVersions works with…", t, func() { 24 | 25 | ver := "1.0.0" 26 | Convey(ver, func() { 27 | ver := MustParse(ver) 28 | 29 | Convey("Without pre-releases", func() { 30 | next := toStr(ver.NextVersions(0, false)) 31 | So(next, ShouldResemble, []string{ 32 | "1.0.0-r1", 33 | "1.0.0-p1", 34 | "1.0.1", 35 | "1.1.0", 36 | "2.0.0", 37 | }) 38 | }) 39 | 40 | Convey("With some pre-releases", func() { 41 | next := toStr(ver.NextVersions(-2, false)) 42 | So(next, ShouldResemble, []string{ 43 | "1.0.0-r1", 44 | "1.0.0-p1", 45 | "1.0.1", 46 | "1.1.0-pre", 47 | "1.1.0-rc", 48 | "1.1.0", 49 | "2.0.0-pre", 50 | "2.0.0-rc", 51 | "2.0.0", 52 | }) 53 | }) 54 | 55 | Convey("With some pre-releases and numbers", func() { 56 | next := toStr(ver.NextVersions(-2, true)) 57 | So(next, ShouldResemble, []string{ 58 | "1.0.0-r1", 59 | "1.0.0-p1", 60 | "1.0.1", 61 | "1.1.0-pre1", 62 | "1.1.0-rc1", 63 | "1.1.0", 64 | "2.0.0-pre1", 65 | "2.0.0-rc1", 66 | "2.0.0", 67 | }) 68 | }) 69 | 70 | Convey("With all pre-releases", func() { 71 | next := toStr(ver.NextVersions(-4, false)) 72 | So(next, ShouldResemble, []string{ 73 | "1.0.0-r1", 74 | "1.0.0-p1", 75 | "1.0.1", 76 | "1.1.0-alpha", 77 | "1.1.0-beta", 78 | "1.1.0-pre", 79 | "1.1.0-rc", 80 | "1.1.0", 81 | "2.0.0-alpha", 82 | "2.0.0-beta", 83 | "2.0.0-pre", 84 | "2.0.0-rc", 85 | "2.0.0", 86 | }) 87 | }) 88 | }) 89 | 90 | ver = "1.2.3" 91 | Convey(ver, func() { 92 | ver := MustParse(ver) 93 | 94 | Convey("Without pre-releases", func() { 95 | next := toStr(ver.NextVersions(0, false)) 96 | So(next, ShouldResemble, []string{ 97 | "1.2.3-r1", 98 | "1.2.3-p1", 99 | "1.2.4", 100 | "1.3.0", 101 | "2.0.0", 102 | }) 103 | }) 104 | 105 | Convey("With some pre-releases", func() { 106 | next := toStr(ver.NextVersions(-2, false)) 107 | So(next, ShouldResemble, []string{ 108 | "1.2.3-r1", 109 | "1.2.3-p1", 110 | "1.2.4", 111 | "1.3.0-pre", 112 | "1.3.0-rc", 113 | "1.3.0", 114 | "2.0.0-pre", 115 | "2.0.0-rc", 116 | "2.0.0", 117 | }) 118 | }) 119 | 120 | Convey("With all pre-releases", func() { 121 | next := toStr(ver.NextVersions(-4, false)) 122 | So(next, ShouldResemble, []string{ 123 | "1.2.3-r1", 124 | "1.2.3-p1", 125 | "1.2.4", 126 | "1.3.0-alpha", 127 | "1.3.0-beta", 128 | "1.3.0-pre", 129 | "1.3.0-rc", 130 | "1.3.0", 131 | "2.0.0-alpha", 132 | "2.0.0-beta", 133 | "2.0.0-pre", 134 | "2.0.0-rc", 135 | "2.0.0", 136 | }) 137 | }) 138 | }) 139 | 140 | ver = "1.2.0-beta2" 141 | Convey(ver, func() { 142 | ver := MustParse(ver) 143 | 144 | Convey("With all pre-releases and numbers", func() { 145 | next := toStr(ver.NextVersions(-4, true)) 146 | So(next, ShouldResemble, []string{ 147 | "1.2.0-beta3", 148 | "1.2.0-pre1", 149 | "1.2.0-rc1", 150 | "1.2.0", 151 | "1.2.1", 152 | "1.3.0-alpha1", 153 | "1.3.0-beta1", 154 | "1.3.0-pre1", 155 | "1.3.0-rc1", 156 | "1.3.0", 157 | "2.0.0-alpha1", 158 | "2.0.0-beta1", 159 | "2.0.0-pre1", 160 | "2.0.0-rc1", 161 | "2.0.0", 162 | }) 163 | }) 164 | }) 165 | 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /range.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "bytes" 9 | ) 10 | 11 | // Range is a subset of the universe of Versions: It can have a lower and upper boundary. 12 | // For example, "1.2–2.0" is such a Range, with two boundaries. 13 | type Range struct { 14 | lower Version 15 | upper Version 16 | hasLower bool 17 | equalsLower bool 18 | hasUpper bool 19 | equalsUpper bool 20 | } 21 | 22 | // NewRange translates into a Range. 23 | func NewRange(str []byte) (Range, error) { 24 | if len(str) == 0 || (len(str) == 1 && (str[0] == '*' || str[0] == 'x')) { 25 | // An empty Range contains everything. 26 | return Range{}, nil 27 | } 28 | isNaturalRange := true 29 | if bytes.HasSuffix(str, []byte(".x")) || bytes.HasSuffix(str, []byte(".*")) { 30 | str = bytes.TrimRight(str, ".x*") 31 | isNaturalRange = false 32 | } 33 | if str[0] == '^' || str[0] == '~' { 34 | return newRangeByShortcut(str) 35 | } 36 | 37 | var upperBound, lowerBound bool = true, true 38 | if len(str) >= 2 { 39 | lowerBound = !(str[0] == '<' || str[1] == '<') 40 | upperBound = !(str[0] == '>' || str[1] == '>') 41 | } 42 | var leftEnd, rightStart int 43 | if idx := bytes.IndexByte(str, byte(' ')); idx > 1 { 44 | leftEnd = idx 45 | } else if idx = bytes.IndexByte(str, byte(',')); idx > 1 { 46 | leftEnd = idx 47 | } else { 48 | leftEnd = len(str) 49 | rightStart = leftEnd 50 | } 51 | if rightStart == 0 { 52 | rightStart = bytes.LastIndexByte(str, byte(' ')) + 1 53 | if rightStart <= 0 { 54 | rightStart = bytes.LastIndexByte(str, byte(',')) + 1 55 | } 56 | } 57 | 58 | isNaturalRange = isNaturalRange && leftEnd != rightStart && (len(str)-rightStart) > 0 59 | if !isNaturalRange { 60 | leftDotCount := bytes.Count(str[:leftEnd], []byte{'.'}) 61 | switch leftDotCount { 62 | case 1: 63 | return newRangeByShortcut(append([]byte{'~'}, str...)) 64 | case 0: 65 | return newRangeByShortcut(append([]byte{'^'}, str...)) 66 | } 67 | } 68 | vr := Range{} 69 | if leftEnd == rightStart { 70 | err := vr.setBound(str, lowerBound, upperBound) 71 | return vr, err 72 | } 73 | 74 | if err := vr.setBound(str[:leftEnd], true, false); err != nil { 75 | return vr, err 76 | } 77 | if err := vr.setBound(str[rightStart:], false, true); err != nil { 78 | return vr, err 79 | } 80 | 81 | return vr, nil 82 | } 83 | 84 | func (r *Range) setBound(str []byte, isLower, isUpper bool) error { 85 | var versionStartIdx int 86 | for ; versionStartIdx < len(str); versionStartIdx++ { 87 | if isNumeric(str[versionStartIdx]) { 88 | goto startFound 89 | } 90 | } 91 | return errInvalidVersionString 92 | 93 | startFound: 94 | var err error 95 | equalOk := versionStartIdx == 0 || bytes.IndexByte(str[:versionStartIdx], '=') > 0 96 | if isUpper { 97 | r.equalsUpper, r.hasUpper = equalOk, true 98 | err = r.upper.unmarshalText(str[versionStartIdx:]) 99 | } 100 | if isLower { 101 | r.equalsLower, r.hasLower = equalOk, true 102 | if isUpper { 103 | r.lower = r.upper 104 | } else { 105 | err = r.lower.unmarshalText(str[versionStartIdx:]) 106 | } 107 | } 108 | return err 109 | } 110 | 111 | // newRangeByShortcut covers the special case of Ranges whose boundaries 112 | // are declared using prefixes. 113 | func newRangeByShortcut(str []byte) (Range, error) { 114 | t := bytes.TrimLeft(str, "~^") 115 | num, err := NewVersion(t) 116 | if err != nil { 117 | return Range{}, err 118 | } 119 | if bytes.HasPrefix(t, []byte("0.0.")) { 120 | return NewRange(t) 121 | } 122 | 123 | r := Range{lower: num, hasLower: true, equalsLower: true, hasUpper: true, upper: Version{}} 124 | 125 | switch { 126 | case bytes.HasPrefix(t, []byte("0.")): 127 | r.upper.version[0] = r.lower.version[0] 128 | r.upper.version[1] = r.lower.version[1] + 1 129 | case str[0] == '^' || bytes.IndexByte(t, '.') <= -1: 130 | r.upper.version[0] = r.lower.version[0] + 1 131 | case str[0] == '~': 132 | r.upper.version[0] = r.lower.version[0] 133 | r.upper.version[1] = r.lower.version[1] + 1 134 | } 135 | 136 | return r, nil 137 | } 138 | 139 | // GetLowerBoundary gets you the lower (left) boundary. 140 | func (r Range) GetLowerBoundary() *Version { 141 | if !r.hasLower { 142 | return nil 143 | } 144 | return &r.lower 145 | } 146 | 147 | // GetUpperBoundary gets you the high (right) boundary. 148 | func (r Range) GetUpperBoundary() *Version { 149 | if !r.hasUpper { 150 | return nil 151 | } 152 | return &r.upper 153 | } 154 | 155 | // Contains returns true if a Version is inside this Range. 156 | // 157 | // If in doubt use IsSatisfiedBy. 158 | func (r Range) Contains(v Version) bool { 159 | if r.upper == r.lower { 160 | return r.lower.LimitedEqual(v) 161 | } 162 | 163 | return r.satisfiesLowerBound(v) && r.satisfiesUpperBound(v) 164 | } 165 | 166 | // IsSatisfiedBy works like Contains, 167 | // but rejects pre-releases if neither of the bounds is a pre-release. 168 | // 169 | // Use this in the context of pulling in packages because it follows the spirit of §9 SemVer. 170 | // Also see https://github.com/npm/node-semver/issues/64 171 | func (r Range) IsSatisfiedBy(v Version) bool { 172 | if !r.Contains(v) { 173 | return false 174 | } 175 | if v.IsAPreRelease() { 176 | if r.hasLower && r.lower.IsAPreRelease() && r.lower.sharesPrefixWith(v) { 177 | return true 178 | } 179 | if r.hasUpper && r.upper.IsAPreRelease() && r.upper.sharesPrefixWith(v) { 180 | return true 181 | } 182 | return false 183 | } 184 | return true 185 | } 186 | 187 | func (r Range) satisfiesLowerBound(v Version) bool { 188 | if !r.hasLower { 189 | return true 190 | } 191 | 192 | equal := r.lower.LimitedEqual(v) 193 | if r.equalsLower && equal { 194 | return true 195 | } 196 | 197 | return r.lower.limitedLess(v) && !equal 198 | } 199 | 200 | func (r Range) satisfiesUpperBound(v Version) bool { 201 | if !r.hasUpper { 202 | return true 203 | } 204 | 205 | equal := r.upper.LimitedEqual(v) 206 | if r.equalsUpper && equal { 207 | return true 208 | } 209 | 210 | if !r.equalsUpper && r.upper.version[idxReleaseType] == common { 211 | equal = r.upper.sharesPrefixWith(v) 212 | } 213 | 214 | return v.limitedLess(r.upper) && !equal 215 | } 216 | 217 | // Satisfies is a convenience function for former NodeJS developers, 218 | // and works on two strings. 219 | // 220 | // Please see Range's IsSatisfiedBy for details. 221 | func Satisfies(aVersion, aRange string) (bool, error) { 222 | v, err := NewVersion([]byte(aVersion)) 223 | if err != nil { 224 | return false, err 225 | } 226 | r, err := NewRange([]byte(aRange)) 227 | if err != nil { 228 | return false, err 229 | } 230 | 231 | return r.IsSatisfiedBy(v), nil 232 | } 233 | -------------------------------------------------------------------------------- /range_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func hasLowerBound(aRange interface{}, aVersion ...interface{}) string { 14 | a := aRange.(Range) 15 | if s := ShouldResemble(a.lower, aVersion[0]); s != "" { 16 | return s 17 | } 18 | return ShouldBeTrue(a.equalsLower) 19 | } 20 | 21 | func isLeftClosedBy(aRange interface{}, aVersion ...interface{}) string { 22 | a := aRange.(Range) 23 | if s := ShouldResemble(a.lower, aVersion[0]); s != "" { 24 | return s 25 | } 26 | return ShouldBeFalse(a.equalsLower) 27 | } 28 | 29 | func hasUpperBound(aRange interface{}, aVersion ...interface{}) string { 30 | a := aRange.(Range) 31 | if s := ShouldResemble(a.upper, aVersion[0]); s != "" { 32 | return s 33 | } 34 | return ShouldBeTrue(a.equalsUpper) 35 | } 36 | 37 | func isRightClosedBy(aRange interface{}, aVersion ...interface{}) string { 38 | a := aRange.(Range) 39 | if s := ShouldResemble(a.upper, aVersion[0]); s != "" { 40 | return s 41 | } 42 | return ShouldBeFalse(a.equalsUpper) 43 | } 44 | 45 | func testIfResembles(actual, expected Range) { 46 | So(actual.lower, ShouldResemble, expected.lower) 47 | So(actual.equalsLower, ShouldEqual, expected.equalsLower) 48 | So(actual.upper, ShouldResemble, expected.upper) 49 | So(actual.equalsUpper, ShouldEqual, expected.equalsUpper) 50 | } 51 | 52 | func shouldContain(aRange interface{}, aVersion ...interface{}) string { 53 | a := aRange.(Range) 54 | for _, version := range aVersion { 55 | v := version.(string) 56 | ver, err := NewVersion([]byte(v)) 57 | if err != nil { 58 | return err.Error() 59 | } 60 | if s := ShouldBeTrue(a.Contains(ver)); s != "" { 61 | return v + " is not in Range" 62 | } 63 | } 64 | return "" 65 | } 66 | 67 | func shouldNotContain(aRange interface{}, aVersion ...interface{}) string { 68 | a := aRange.(Range) 69 | for _, version := range aVersion { 70 | v := version.(string) 71 | ver, err := NewVersion([]byte(v)) 72 | if err != nil { 73 | return err.Error() 74 | } 75 | if s := ShouldBeFalse(a.Contains(ver)); s != "" { 76 | return v + " is in Range" 77 | } 78 | } 79 | return "" 80 | } 81 | 82 | func TestRangeConstruction(t *testing.T) { 83 | 84 | Convey("version 1.2.3 should be part of…", t, func() { 85 | ver := MustParse("1.2.3") 86 | 87 | Convey("specific Range 1.2.3", func() { 88 | verRange, err := NewRange([]byte("1.2.3")) 89 | So(err, ShouldBeNil) 90 | if err != nil { 91 | return 92 | } 93 | So(verRange.lower, ShouldResemble, ver) 94 | }) 95 | }) 96 | 97 | v100 := MustParse("1.0.0") 98 | v120 := MustParse("1.2.0") 99 | v123 := MustParse("1.2.3") 100 | v130 := MustParse("1.3.0") 101 | v200 := MustParse("2.0.0") 102 | 103 | Convey("Range >=1.2.3 <=1.3.0…", t, func() { 104 | verRange, err := NewRange([]byte(">=1.2.3 <=1.3.0")) 105 | So(err, ShouldBeNil) 106 | if err != nil { 107 | return 108 | } 109 | 110 | Convey("has lower bound >=1.2.3", func() { 111 | So(verRange, hasLowerBound, v123) 112 | }) 113 | 114 | Convey("has upper bound <=1.3.0", func() { 115 | So(verRange, hasUpperBound, v130) 116 | }) 117 | }) 118 | 119 | Convey("Range >1.2.3 <1.3.0…", t, func() { 120 | verRange, err := NewRange([]byte(">1.2.3 <1.3.0")) 121 | So(err, ShouldBeNil) 122 | if err != nil { 123 | return 124 | } 125 | 126 | Convey("has lower bound >1.2.3", func() { 127 | So(verRange, isLeftClosedBy, v123) 128 | }) 129 | 130 | Convey("has upper bound <1.3.0", func() { 131 | So(verRange, isRightClosedBy, v130) 132 | }) 133 | }) 134 | 135 | Convey("Range 1.2.3 - 1.3.0…", t, func() { 136 | verRange, err := NewRange([]byte("1.2.3 - 1.3.0")) 137 | So(err, ShouldBeNil) 138 | if err != nil { 139 | return 140 | } 141 | 142 | Convey("has lower bound >=1.2.3", func() { 143 | So(verRange, hasLowerBound, v123) 144 | }) 145 | 146 | Convey("has upper bound <=1.3.0", func() { 147 | So(verRange, hasUpperBound, v130) 148 | }) 149 | }) 150 | 151 | Convey("Range ~1.2.3 equals: >=1.2.3 <1.3.0", t, func() { 152 | verRange, err := NewRange([]byte("~1.2.3")) 153 | So(err, ShouldBeNil) 154 | if err != nil { 155 | return 156 | } 157 | 158 | Convey("has lower bound >=1.2.3", func() { 159 | So(verRange, hasLowerBound, v123) 160 | }) 161 | 162 | Convey("has upper bound <1.3.0", func() { 163 | So(verRange, isRightClosedBy, v130) 164 | }) 165 | }) 166 | 167 | Convey("Range ^1.2.3 equals: >=1.2.3 <2.0.0", t, func() { 168 | verRange, err := NewRange([]byte("^1.2.3")) 169 | So(err, ShouldBeNil) 170 | if err != nil { 171 | return 172 | } 173 | 174 | Convey("has lower bound >=1.2.3", func() { 175 | So(verRange, hasLowerBound, v123) 176 | }) 177 | 178 | Convey("has upper bound <2.0.0", func() { 179 | So(verRange, isRightClosedBy, v200) 180 | }) 181 | }) 182 | 183 | Convey("Range ~1.2 equals: >=1.2.0 <1.3.0", t, func() { 184 | verRange, err := NewRange([]byte("~1.2")) 185 | So(err, ShouldBeNil) 186 | if err != nil { 187 | return 188 | } 189 | 190 | Convey("has lower bound >=1.2.0", func() { 191 | So(verRange, hasLowerBound, v120) 192 | }) 193 | 194 | Convey("has upper bound <1.3.0", func() { 195 | So(verRange, isRightClosedBy, v130) 196 | }) 197 | }) 198 | 199 | Convey("Range ^1.2 equals: >=1.2.0 <2.0.0", t, func() { 200 | verRange, err := NewRange([]byte("^1.2")) 201 | So(err, ShouldBeNil) 202 | if err != nil { 203 | return 204 | } 205 | 206 | Convey("has lower bound >=1.2.0", func() { 207 | So(verRange, hasLowerBound, v120) 208 | }) 209 | 210 | Convey("has upper bound <2.0.0", func() { 211 | So(verRange, isRightClosedBy, v200) 212 | }) 213 | }) 214 | 215 | Convey("Ranges ^1 and ~1 equal: >=1.0.0 <2.0.0", t, func() { 216 | r1, err := NewRange([]byte("^1")) 217 | So(err, ShouldBeNil) 218 | if err != nil { 219 | return 220 | } 221 | r2, err := NewRange([]byte("^1")) 222 | So(err, ShouldBeNil) 223 | if err != nil { 224 | return 225 | } 226 | 227 | Convey("has lower bound >=1.0.0", func() { 228 | So(r1, hasLowerBound, v100) 229 | So(r2, hasLowerBound, v100) 230 | }) 231 | 232 | Convey("has upper bound <2.0.0", func() { 233 | So(r1, isRightClosedBy, v200) 234 | So(r2, isRightClosedBy, v200) 235 | }) 236 | }) 237 | 238 | Convey("Given .x and .* notations", t, func() { 239 | refRange, _ := NewRange([]byte("^1")) 240 | 241 | Convey("1.x equals ^1", func() { 242 | r1, err := NewRange([]byte("1.x")) 243 | So(err, ShouldBeNil) 244 | if err != nil { 245 | return 246 | } 247 | testIfResembles(r1, refRange) 248 | }) 249 | 250 | Convey("1.* equals ^1", func() { 251 | r1, err := NewRange([]byte("1.*")) 252 | So(err, ShouldBeNil) 253 | if err != nil { 254 | return 255 | } 256 | testIfResembles(r1, refRange) 257 | }) 258 | 259 | Convey("1 equals ^1", func() { 260 | r1, err := NewRange([]byte("1")) 261 | So(err, ShouldBeNil) 262 | if err != nil { 263 | return 264 | } 265 | testIfResembles(r1, refRange) 266 | }) 267 | 268 | smallRange, _ := NewRange([]byte("~1.2")) 269 | 270 | Convey("1.2.x equals ~1.2", func() { 271 | r1, err := NewRange([]byte("1.2.x")) 272 | So(err, ShouldBeNil) 273 | if err != nil { 274 | return 275 | } 276 | testIfResembles(r1, smallRange) 277 | }) 278 | 279 | Convey("1.2.* equals ~1.2", func() { 280 | r1, err := NewRange([]byte("1.2.*")) 281 | So(err, ShouldBeNil) 282 | if err != nil { 283 | return 284 | } 285 | testIfResembles(r1, smallRange) 286 | }) 287 | 288 | Convey("1.2 equals ~1.2", func() { 289 | r1, err := NewRange([]byte("1.2")) 290 | So(err, ShouldBeNil) 291 | if err != nil { 292 | return 293 | } 294 | testIfResembles(r1, smallRange) 295 | }) 296 | }) 297 | 298 | Convey("Notations for 'any'", t, func() { 299 | refRange := Range{} 300 | 301 | Convey("'x'", func() { 302 | r1, err := NewRange([]byte("x")) 303 | So(err, ShouldBeNil) 304 | if err != nil { 305 | return 306 | } 307 | testIfResembles(r1, refRange) 308 | }) 309 | 310 | Convey("'*'", func() { 311 | r1, err := NewRange([]byte("*")) 312 | So(err, ShouldBeNil) 313 | if err != nil { 314 | return 315 | } 316 | testIfResembles(r1, refRange) 317 | }) 318 | 319 | Convey("'' (empty string)", func() { 320 | r1, err := NewRange([]byte("")) 321 | So(err, ShouldBeNil) 322 | if err != nil { 323 | return 324 | } 325 | testIfResembles(r1, refRange) 326 | }) 327 | }) 328 | 329 | // now come fringe cases 330 | Convey("Range ^0.1.3 and ~0.1.3 equal: >=0.1.3 <0.2.0", t, func() { 331 | r1, err := NewRange([]byte("^0.1.3")) 332 | So(err, ShouldBeNil) 333 | if err != nil { 334 | return 335 | } 336 | r2, err := NewRange([]byte("~0.1.3")) 337 | So(err, ShouldBeNil) 338 | if err != nil { 339 | return 340 | } 341 | v013 := MustParse("0.1.3") 342 | v020 := MustParse("0.2.0") 343 | 344 | Convey("have lower bound >=0.1.3", func() { 345 | So(r1, hasLowerBound, v013) 346 | So(r2, hasLowerBound, v013) 347 | }) 348 | 349 | Convey("have upper bound <0.2.0", func() { 350 | So(r1, isRightClosedBy, v020) 351 | So(r2, isRightClosedBy, v020) 352 | }) 353 | }) 354 | 355 | Convey("Range ^0.0.2 and ~0.0.2 are 0.2.0", t, func() { 356 | r1, err := NewRange([]byte("^0.0.2")) 357 | So(err, ShouldBeNil) 358 | if err != nil { 359 | return 360 | } 361 | r2, err := NewRange([]byte("~0.0.2")) 362 | So(err, ShouldBeNil) 363 | if err != nil { 364 | return 365 | } 366 | r002, _ := NewRange([]byte("0.0.2")) 367 | 368 | Convey("have the same bounds as 0.2.0", func() { 369 | testIfResembles(r1, r002) 370 | testIfResembles(r2, r002) 371 | }) 372 | }) 373 | 374 | Convey("Reject invalid ranges such as", t, func() { 375 | for _, s := range []string{ 376 | " - 1.0", "- 1.0", 377 | "1 - X", "X - 1", 378 | "1 - 5.6.7.8.9", "1.2.3.4.5 - 1", 379 | } { 380 | Convey(s, func() { 381 | _, err := NewRange([]byte(s)) 382 | So(err, ShouldNotBeNil) 383 | }) 384 | } 385 | }) 386 | } 387 | 388 | func TestSingleBound(t *testing.T) { 389 | 390 | Convey("Given a specific version as Range…", t, func() { 391 | Convey("1.2.3…", func() { 392 | verRange, _ := NewRange([]byte("1.2.3")) 393 | 394 | Convey("reject Version 1.2.4", func() { 395 | So(verRange, shouldNotContain, "1.2.4") 396 | }) 397 | 398 | Convey("accept Version 1.2.3", func() { 399 | So(verRange, shouldContain, "1.2.3") 400 | }) 401 | 402 | Convey("accept Version 1.2.3+build2014 (ignore build)", func() { 403 | So(verRange, shouldContain, "1.2.3+build2014") 404 | }) 405 | 406 | Convey("accept Version 1.2.3-p1 (patch levels are reasonably equal)", func() { 407 | So(verRange, shouldContain, "1.2.3-p1") 408 | }) 409 | 410 | Convey("reject pre-releases like 1.2.3-alpha", func() { 411 | So(verRange, shouldNotContain, "1.2.3-alpha") 412 | }) 413 | }) 414 | 415 | Convey("1.2.3-alpha20…", func() { 416 | verRange, _ := NewRange([]byte("1.2.3-alpha20")) 417 | 418 | Convey("accept Version 1.2.3-alpha20", func() { 419 | So(verRange, shouldContain, "1.2.3-alpha20") 420 | }) 421 | Convey("reject Version 1.2.3-alpha5", func() { 422 | So(verRange, shouldNotContain, "1.2.3-alpha5") 423 | }) 424 | Convey("reject Version 1.2.3-beta", func() { 425 | So(verRange, shouldNotContain, "1.2.3-beta") 426 | }) 427 | Convey("reject Version 1.2.3", func() { 428 | So(verRange, shouldNotContain, "1.2.3") 429 | }) 430 | }) 431 | }) 432 | 433 | Convey("Given the lower bound >1.2.3", t, func() { 434 | verRange, _ := NewRange([]byte(">1.2.3")) 435 | So(verRange.GetUpperBoundary(), ShouldBeNil) 436 | 437 | Convey("reject Version 1.2.3", func() { 438 | So(verRange, shouldNotContain, "1.2.3") 439 | }) 440 | 441 | Convey("reject Version 1.2.3-p1 (ignore release/pre-release)", func() { 442 | So(verRange, shouldNotContain, "1.2.3-p1") 443 | }) 444 | 445 | Convey("reject Version 1.2.3+build2014 (ignore build)", func() { 446 | So(verRange, shouldNotContain, "1.2.3+build2014") 447 | }) 448 | 449 | Convey("accept Versions…", func() { 450 | Convey("1.2.4", func() { 451 | So(verRange, shouldContain, "1.2.4") 452 | }) 453 | Convey("1.3.0", func() { 454 | So(verRange, shouldContain, "1.3.0") 455 | }) 456 | Convey("2.0.0", func() { 457 | So(verRange, shouldContain, "2.0.0") 458 | }) 459 | }) 460 | }) 461 | 462 | Convey("A lower bound >=1.2.3…", t, func() { 463 | verRange, _ := NewRange([]byte(">=1.2.3")) 464 | 465 | Convey("should contain Version 1.2.3", func() { 466 | So(verRange, shouldContain, "1.2.3") 467 | }) 468 | 469 | Convey("should contain Version 1.2.3-p1", func() { 470 | So(verRange, shouldContain, "1.2.3-p1") 471 | }) 472 | 473 | Convey("but NOT contain 1.2.3-rc (exclude pre-release)", func() { 474 | So(verRange, shouldNotContain, "1.2.3-rc") 475 | }) 476 | }) 477 | 478 | Convey("Over-specific lower bounds >=1.2.3-alpha4…", t, func() { 479 | verRange, _ := NewRange([]byte(">=1.2.3-alpha4")) 480 | 481 | Convey("should contain Version 1.2.4", func() { 482 | So(verRange, shouldContain, "1.2.4") 483 | }) 484 | 485 | Convey("should contain Version 1.2.3-alpha4", func() { 486 | So(verRange, shouldContain, "1.2.3-alpha4") 487 | }) 488 | 489 | Convey("but NOT contain 1.2.3-alpha", func() { 490 | So(verRange, shouldNotContain, "1.2.3-alpha") 491 | }) 492 | }) 493 | 494 | Convey("Upper bounds such as <1.2.3…", t, func() { 495 | verRange, _ := NewRange([]byte("<1.2.3")) 496 | So(verRange.GetLowerBoundary(), ShouldBeNil) 497 | 498 | Convey("reject Version 1.2.3", func() { 499 | So(verRange, shouldNotContain, "1.2.3") 500 | }) 501 | 502 | Convey("reject Version 1.2.3-alpha3 (ignore release/pre-release)", func() { 503 | So(verRange, shouldNotContain, "1.2.3-alpha3") 504 | }) 505 | 506 | Convey("reject Version 1.2.3+build2014 (ignore build)", func() { 507 | So(verRange, shouldNotContain, "1.2.3+build2014") 508 | }) 509 | 510 | Convey("accept Versions…", func() { 511 | Convey("1.2.0", func() { 512 | So(verRange, shouldContain, "1.2.0") 513 | }) 514 | Convey("1.1.0", func() { 515 | So(verRange, shouldContain, "1.1.0") 516 | }) 517 | Convey("1.0.0", func() { 518 | So(verRange, shouldContain, "1.0.0") 519 | }) 520 | }) 521 | }) 522 | 523 | Convey("An over-specific upper bound <1.2.3-beta20…", t, func() { 524 | verRange, _ := NewRange([]byte("<1.2.3-beta20")) 525 | 526 | Convey("reject Version 1.2.3-beta20", func() { 527 | So(verRange, shouldNotContain, "1.2.3-beta20") 528 | }) 529 | 530 | Convey("reject Version 1.2.3-beta20-alpha3 (ignore release specifier)", func() { 531 | So(verRange, shouldNotContain, "1.2.3-beta20-alpha3") 532 | }) 533 | 534 | Convey("reject Version 1.2.3-beta20+build2014 (ignore build)", func() { 535 | So(verRange, shouldNotContain, "1.2.3-beta20+build2014") 536 | }) 537 | 538 | Convey("accept Versions…", func() { 539 | Convey("1.2.3-beta19", func() { 540 | So(verRange, shouldContain, "1.2.3-beta19") 541 | }) 542 | Convey("1.2.3-alpha", func() { 543 | So(verRange, shouldContain, "1.2.3-alpha") 544 | }) 545 | Convey("1.2.0", func() { 546 | So(verRange, shouldContain, "1.2.0") 547 | }) 548 | }) 549 | }) 550 | 551 | Convey("An upper bound with equality <=1.2.3…", t, func() { 552 | verRange, _ := NewRange([]byte("<=1.2.3")) 553 | 554 | Convey("should contain Version 1.2.3", func() { 555 | So(verRange, shouldContain, "1.2.3") 556 | }) 557 | 558 | Convey("should contain Version 1.2.3-p1", func() { 559 | So(verRange, shouldContain, "1.2.3-p1") 560 | }) 561 | 562 | Convey("and, that's new, contain 1.2.3-rc (INCLUDE pre-release)", func() { 563 | So(verRange, shouldContain, "1.2.3-rc") 564 | }) 565 | }) 566 | 567 | } 568 | 569 | func TestSatisfies(t *testing.T) { 570 | 571 | Convey("Convenience function 'Satisfies'", t, func() { 572 | 573 | Convey("works with valid input", func() { 574 | t, _ := Satisfies("1.2.3", "^1.2.2") 575 | So(t, ShouldBeTrue) 576 | t, _ = Satisfies("1.2.3-pre2", "^1.2.3-pre1") 577 | So(t, ShouldBeTrue) 578 | }) 579 | 580 | Convey("yields an error on invalid Version", func() { 581 | t, err := Satisfies("1.2.3.4.5.6", "^1.2.2") 582 | So(t, ShouldBeFalse) 583 | So(err, ShouldNotBeNil) 584 | }) 585 | 586 | Convey("yields an error on invalid Range", func() { 587 | t, err := Satisfies("1.2.3", "^1.2.2/1.2.5") 588 | So(t, ShouldBeFalse) 589 | So(err, ShouldNotBeNil) 590 | }) 591 | }) 592 | 593 | Convey("Range.IsSatisfiedBy…", t, func() { 594 | 595 | Convey("rejects pre-releases", func() { 596 | t, _ := Satisfies("1.2.3-pre1", "^1.2.2") 597 | So(t, ShouldBeFalse) 598 | t, _ = Satisfies("1.2.4-pre1", "^1.2.2-pre1") 599 | So(t, ShouldBeFalse) 600 | t, _ = Satisfies("1.2.3-pre1", "<1.2.3") 601 | So(t, ShouldBeFalse) 602 | }) 603 | 604 | Convey("accepts pre-releases for a pre-release upper bound with the same prefix", func() { 605 | t, _ := Satisfies("1.2.3-pre1", "<1.2.3-pre1") 606 | So(t, ShouldBeFalse) 607 | t, _ = Satisfies("1.2.3-pre1", "<1.2.3-pre2") 608 | So(t, ShouldBeTrue) 609 | t, _ = Satisfies("1.2.3-pre1", "<=1.2.3-pre1") 610 | So(t, ShouldBeTrue) 611 | }) 612 | 613 | Convey("accepts pre-releases for a pre-release lower bound with the same prefix", func() { 614 | t, _ := Satisfies("1.2.3-pre1", ">1.2.3-pre1") 615 | So(t, ShouldBeFalse) 616 | t, _ = Satisfies("1.2.3-pre2", ">1.2.3-pre1") 617 | So(t, ShouldBeTrue) 618 | t, _ = Satisfies("1.2.3-pre1", ">=1.2.3-pre1") 619 | So(t, ShouldBeTrue) 620 | }) 621 | }) 622 | 623 | Convey("Test the examples found in README file.", t, func() { 624 | v := MustParse("1.2.3-beta") 625 | r, _ := NewRange([]byte("~1.2")) 626 | So(r.Contains(v), ShouldBeTrue) 627 | So(r.IsSatisfiedBy(v), ShouldBeFalse) 628 | }) 629 | } 630 | 631 | var benchR, benchRErr = NewRange([]byte(">=1.2.3 <=1.3.0")) 632 | 633 | func BenchmarkSemverNewRange(b *testing.B) { 634 | var r, e = NewRange([]byte(">=1.2.3 <=1.3.0")) 635 | for n := 0; n < b.N; n++ { 636 | r, e = NewRange([]byte(">=1.2.3 <=1.3.0")) 637 | } 638 | benchR, benchRErr = r, e 639 | } 640 | -------------------------------------------------------------------------------- /semver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "bytes" 9 | ) 10 | 11 | // Errors that are thrown during parsing. 12 | const ( 13 | errInvalidVersionString InvalidStringValue = "Given string does not resemble a Version" 14 | errTooManyColumns InvalidStringValue = "Version consists of too many columns" 15 | errVersionStringLength InvalidStringValue = "Version is too long" 16 | errInvalidBuildSuffix InvalidStringValue = "Version has a '+' but no +buildNNN suffix" 17 | errInvalidType InvalidStringValue = "Cannot read this type into a Version" 18 | errOutOfBounds InvalidStringValue = "The source representation does not fit into a Version" 19 | ) 20 | 21 | // alpha = -4, beta = -3, pre = -2, rc = -1, common = 0, revision = 1, patch = 2 22 | const ( 23 | alpha = iota - 4 24 | beta 25 | pre 26 | rc 27 | common 28 | revision 29 | patch 30 | ) 31 | 32 | const ( 33 | idxReleaseType = 4 34 | idxRelease = 5 35 | idxSpecifierType = 9 36 | idxSpecifier = 10 37 | ) 38 | 39 | var releaseDesc = map[int]string{ 40 | alpha: "alpha", 41 | beta: "beta", 42 | pre: "pre", 43 | rc: "rc", 44 | revision: "r", 45 | patch: "p", 46 | } 47 | 48 | var releaseValue = map[string]int{ 49 | "alpha": alpha, 50 | "beta": beta, 51 | "pre": pre, 52 | "": pre, 53 | "rc": rc, 54 | "r": revision, 55 | "p": patch, 56 | } 57 | 58 | var buildsuffix = []byte("+build") 59 | 60 | // InvalidStringValue instances are returned as error on any conversion failure. 61 | type InvalidStringValue string 62 | 63 | // Error implements the error interface. 64 | func (e InvalidStringValue) Error() string { return string(e) } 65 | 66 | // IsInvalid satisfies a function IsInvalid(). 67 | // This is used by some input validator packages. 68 | func (e InvalidStringValue) IsInvalid() bool { return true } 69 | 70 | // Version represents a version: 71 | // Columns consisting of up to four unsigned integers (1.2.4.99) 72 | // optionally further divided into 'release' and 'specifier' (1.2-634.0-99.8). 73 | type Version struct { 74 | // 0–3: version, 4: releaseType, 5–8: releaseVer, 9: releaseSpecifier, 10–: specifier 75 | version [14]int32 76 | build int32 77 | _ int32 78 | } 79 | 80 | // MustParse is NewVersion for strings, and panics on errors. 81 | // 82 | // Use this in tests or with constants, e. g. whenever you control the input. 83 | // 84 | // This is a convenience function for a cloud plattform provider. 85 | func MustParse(str string) Version { 86 | ver, err := NewVersion([]byte(str)) 87 | if err != nil { 88 | panic(err.Error()) 89 | } 90 | return ver 91 | } 92 | 93 | // NewVersion translates the given string, which must be free of whitespace, 94 | // into a single Version. 95 | // 96 | // An io.Reader will give you []byte, hence this (and most functions internally) 97 | // works on []byte to have as few conversion as possible. 98 | func NewVersion(str []byte) (Version, error) { 99 | ver := Version{} 100 | err := (&ver).unmarshalText(str) 101 | return ver, err 102 | } 103 | 104 | // Parse reads a string into the given version, overwriting any existing values. 105 | // 106 | // Deprecated: Use the idiomatic UnmarshalText instead. 107 | func (t *Version) Parse(str string) error { 108 | t.version = [14]int32{} 109 | t.build = 0 110 | 111 | return t.unmarshalText([]byte(str)) 112 | } 113 | 114 | func isNumeric(ch byte) bool { 115 | return ((ch - '0') <= 9) 116 | } 117 | 118 | func isSmallLetter(ch byte) bool { 119 | // case insensitive: (ch | 0x20) 120 | return ((ch - 'a') <= ('z' - 'a')) 121 | } 122 | 123 | // atoui consumes up to n byte from b to convert them into |val|. 124 | func atoui(b []byte) (n int, val uint32) { 125 | for ; n <= 10 && n < len(b); n++ { 126 | v := b[n] - '0' // see above 'isNumeric' 127 | if v > 9 { 128 | break 129 | } 130 | val = val*10 + uint32(v) 131 | } 132 | return 133 | } 134 | 135 | // unmarshalText implements the encoding.TextUnmarshaler interface, 136 | // but assumes the data structure is pristine. 137 | func (t *Version) unmarshalText(str []byte) error { 138 | var idx, fieldNum, column int 139 | var strlen = len(str) 140 | 141 | if strlen > 1 && str[idx] == 'v' { 142 | idx++ 143 | } 144 | 145 | for idx < strlen { 146 | r := str[idx] 147 | switch { 148 | case r == '.': 149 | idx++ 150 | column++ 151 | if column >= 4 || idx >= strlen { 152 | return errTooManyColumns 153 | } 154 | fieldNum++ 155 | fallthrough 156 | case isNumeric(r): 157 | idxDelta, n := atoui(str[idx:]) 158 | if idxDelta == 0 || idxDelta >= 10 { // strlen(maxInt) is 10 159 | return errInvalidVersionString 160 | } 161 | t.version[fieldNum] = int32(n) 162 | 163 | idx += idxDelta 164 | case r == '-' || r == '_': 165 | idx++ 166 | if idx < strlen && isNumeric(str[idx]) { 167 | column = 0 168 | switch { 169 | case fieldNum < idxReleaseType: 170 | fieldNum = idxReleaseType + 1 171 | case fieldNum < idxSpecifierType: 172 | fieldNum = idxSpecifierType + 1 173 | default: 174 | return errInvalidVersionString 175 | } 176 | continue 177 | } 178 | fallthrough 179 | case isSmallLetter(r): 180 | toIdx := idx + 1 181 | for ; toIdx < strlen && isSmallLetter(str[toIdx]); toIdx++ { 182 | } 183 | 184 | if toIdx > strlen { 185 | return errInvalidVersionString 186 | } 187 | typ, known := releaseValue[string(str[idx:toIdx])] 188 | if !known { 189 | return errInvalidVersionString 190 | } 191 | switch { 192 | case fieldNum < idxReleaseType: 193 | fieldNum = idxReleaseType 194 | case fieldNum < idxSpecifierType: 195 | fieldNum = idxSpecifierType 196 | default: 197 | return errInvalidVersionString 198 | } 199 | t.version[fieldNum] = int32(typ) 200 | if toIdx+1 < strlen && str[toIdx] == '.' { 201 | toIdx++ 202 | } 203 | 204 | fieldNum++ 205 | column = 0 206 | idx = toIdx 207 | case r == '+': 208 | if strlen < idx+len(buildsuffix)+1 || !bytes.Equal(str[idx:idx+len(buildsuffix)], buildsuffix) { 209 | return errInvalidBuildSuffix 210 | } 211 | idx += len(buildsuffix) 212 | idxDelta, n := atoui(str[idx:]) 213 | if idxDelta > 9 || idx+idxDelta < strlen { 214 | return errInvalidBuildSuffix 215 | } 216 | t.build = int32(n) 217 | return nil 218 | default: 219 | return errInvalidVersionString 220 | } 221 | } 222 | 223 | return nil 224 | } 225 | 226 | // signDelta returns the signum of the difference, 227 | // whose precision can be limited by 'cuttofIdx'. 228 | func signDelta(a, b [14]int32, cutoffIdx int) int8 { 229 | _ = a[0:cutoffIdx] 230 | for i := 0; i < len(a) && i < cutoffIdx; i++ { 231 | if a[i] == b[i] { 232 | continue 233 | } 234 | x := a[i] - b[i] 235 | return int8((x >> 31) - (-x >> 31)) 236 | } 237 | return 0 238 | } 239 | 240 | // limitedLess compares two Versions 241 | // with a precision limited to version, (pre-)release type and (pre-)release version. 242 | // 243 | // Commutative. 244 | func (t Version) limitedLess(o Version) bool { 245 | return signDelta(t.version, o.version, idxSpecifierType) < 0 246 | } 247 | 248 | // LimitedEqual returns true if two versions share the same: prefix, 249 | // which is the "actual version", (pre-)release type, and (pre-)release version. 250 | // The exception are patch-levels, which are always equal. 251 | // 252 | // Use this, for example, to tell a beta from a regular version; 253 | // or to accept a patched version as regular version. 254 | // 255 | // A thing confusing but convention is to read this from right to left. 256 | func (t Version) LimitedEqual(o Version) bool { 257 | if t.version[idxReleaseType] == common && o.version[idxReleaseType] > common { 258 | return t.sharesPrefixWith(o) 259 | } 260 | return signDelta(t.version, o.version, idxSpecifierType) == 0 261 | } 262 | 263 | // IsAPreRelease is used to discriminate pre-releases. 264 | func (t Version) IsAPreRelease() bool { 265 | return t.version[idxReleaseType] < common 266 | } 267 | 268 | // sharesPrefixWith compares two Versions with a fixed limited precision. 269 | // 270 | // A 'prefix' is the major, minor, patch and revision number. 271 | // For example: 1.2.3.4… 272 | func (t Version) sharesPrefixWith(o Version) bool { 273 | return signDelta(t.version, o.version, idxReleaseType) == 0 274 | } 275 | 276 | // Major returns the major of a version. 277 | func (t Version) Major() int { 278 | return int(t.version[0]) 279 | } 280 | 281 | // Minor returns the minor of a version. 282 | func (t Version) Minor() int { 283 | return int(t.version[1]) 284 | } 285 | 286 | // Patch returns the patch of a version. 287 | func (t Version) Patch() int { 288 | return int(t.version[2]) 289 | } 290 | 291 | // VersionPtrs represents an array with elements derived from~ but smaller than Versions. 292 | // Use this a proxy for sorting of large collections of Versions, 293 | // to minimize memory moves. 294 | type VersionPtrs []*Version 295 | 296 | var _ interface { 297 | Sort() 298 | // These are from sort.Interface: 299 | Len() int 300 | Less(int, int) bool 301 | Swap(int, int) 302 | } = VersionPtrs{} 303 | 304 | // Len implements the sort.Interface. 305 | func (p VersionPtrs) Len() int { 306 | return len(p) 307 | } 308 | 309 | // Swap implements the sort.Interface. 310 | func (p VersionPtrs) Swap(i, j int) { 311 | p[i], p[j] = p[j], p[i] 312 | } 313 | -------------------------------------------------------------------------------- /semver_386.s: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !purego 6 | // +build !go1.16 7 | 8 | #include "go_asm.h" 9 | #include "textflag.h" 10 | 11 | TEXT ·Compare(SB),NOSPLIT,$0-12 12 | MOVL a+0(FP), SI 13 | MOVL b+4(FP), DI 14 | XORL CX, CX // Index of the last examined element. 15 | 16 | MOVOU (SI), X2 17 | MOVOU (DI), X5 18 | PCMPEQL X5, X2 19 | MOVMSKPS X2, AX 20 | CMPL AX, $0x0f 21 | JNE diff 22 | MOVL $4, CX 23 | 24 | MOVOU 16(SI), X3 25 | MOVOU 16(DI), X6 26 | PCMPEQL X6, X3 27 | MOVMSKPS X3, AX 28 | CMPL AX, $0x0f 29 | JNE diff 30 | MOVL $8, CX 31 | 32 | MOVOU 32(SI), X4 33 | MOVOU 32(DI), X7 34 | PCMPEQL X7, X4 35 | MOVMSKPS X4, AX 36 | CMPL AX, $0x0f 37 | JNE diff 38 | MOVL $12, CX 39 | 40 | MOVOU 48(SI), X0 41 | MOVOU 48(DI), X1 42 | PCMPEQL X1, X0 43 | MOVMSKPS X0, AX 44 | ORL $0xc, AX // Mask undefined space, due to 'build' and then nothing. 45 | CMPL AX, $0x0f 46 | JNE diff 47 | 48 | equal: 49 | MOVL $0, ret+8(FP) 50 | RET 51 | 52 | diff: 53 | XORL $0xffff, AX // Invert mask from "equal" to "differ". 54 | BSFL AX, BX // Number of the first bit 1 from LSB on counted. 55 | XORL AX, AX 56 | ADDL BX, CX 57 | // Now compare those diverging elements. (AX, BX, DX are free) 58 | MOVL (DI)(CX*4), BX 59 | CMPL BX, (SI)(CX*4) 60 | SETLT AX 61 | LEAL -1(AX*2), AX 62 | MOVL AX, ret+8(FP) 63 | RET 64 | 65 | TEXT ·less(SB),NOSPLIT,$0-9 66 | MOVL a+0(FP), SI 67 | MOVL b+4(FP), DI 68 | 69 | XORL DX, DX 70 | XORL BX, BX 71 | less_loop: 72 | MOVOU (DI)(DX*1), X1 73 | MOVOU (SI)(DX*1), X0 74 | 75 | MOVAPS X1, X3 76 | PCMPEQL X0, X1 77 | MOVMSKPS X1, AX 78 | CMPB AX, $0x0f 79 | JNE less_determine 80 | ADDB $16, DX 81 | CMPB DX, $64 82 | JE less_eol 83 | JMP less_loop 84 | 85 | less_determine: 86 | MOVAPS X3, X1 87 | PCMPGTL X0, X3 // 3.0.1.0 |>| 2.1.0.0 -> 1.0.1.0 88 | PCMPGTL X1, X0 // 2.1.0.0 |>| 3.0.1.0 -> 0.1.0.0 89 | PSHUFL $27, X3, X3 // $27 is [0, 1, 2, 3], reverse order of elements to get a workable mask below. 90 | PSHUFL $27, X0, X0 91 | MOVMSKPS X3, CX // 1010 92 | MOVMSKPS X0, AX // 0100 93 | CMPB CX, AX 94 | SETGT BX 95 | less_eol: 96 | MOVB BX, ret+8(FP) 97 | RET 98 | -------------------------------------------------------------------------------- /semver_amd64.s: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !purego 6 | // +build !go1.16 7 | 8 | #include "go_asm.h" 9 | #include "textflag.h" 10 | 11 | TEXT ·Compare(SB),NOSPLIT,$0-24 12 | MOVQ a+0(FP), SI 13 | MOVQ b+8(FP), DI 14 | XORQ CX, CX // Index of the last examined element. 15 | 16 | MOVOU (SI), X2 17 | MOVOU (DI), X5 18 | PCMPEQL X5, X2 19 | MOVMSKPS X2, AX 20 | CMPL AX, $0x0f 21 | JNE diff 22 | MOVQ $4, CX 23 | 24 | MOVOU 16(SI), X3 25 | MOVOU 16(DI), X6 26 | PCMPEQL X6, X3 27 | MOVMSKPS X3, AX 28 | CMPL AX, $0x0f 29 | JNE diff 30 | MOVQ $8, CX 31 | 32 | MOVOU 32(SI), X4 33 | MOVOU 32(DI), X7 34 | PCMPEQL X7, X4 35 | MOVMSKPS X4, AX 36 | CMPL AX, $0x0f 37 | JNE diff 38 | MOVQ $12, CX 39 | 40 | MOVOU 48(SI), X0 41 | MOVOU 48(DI), X1 42 | PCMPEQL X1, X0 43 | MOVMSKPS X0, AX 44 | ORQ $0xc, AX // Mask undefined space, due to 'build' and then nothing. 45 | CMPL AX, $0x0f 46 | JNE diff 47 | 48 | equal: 49 | MOVQ $0, ret+16(FP) 50 | RET 51 | 52 | diff: 53 | XORQ $0xffff, AX // Invert mask from "equal" to "differ". 54 | BSFQ AX, BX // Number of the first bit 1 from LSB on counted. 55 | XORQ AX, AX 56 | ADDQ BX, CX 57 | // Now compare those diverging elements. (AX, BX, DX are free) 58 | MOVL (DI)(CX*4), BX 59 | CMPL BX, (SI)(CX*4) 60 | SETLT AX 61 | LEAQ -1(AX*2), AX 62 | MOVQ AX, ret+16(FP) 63 | RET 64 | 65 | TEXT ·less(SB),NOSPLIT,$0-17 66 | MOVQ a+0(FP), SI 67 | MOVQ b+8(FP), DI 68 | 69 | XORQ DX, DX 70 | XORQ BX, BX 71 | less_loop: 72 | MOVOU (DI)(DX*1), X1 73 | MOVOU (SI)(DX*1), X0 74 | 75 | MOVAPS X1, X3 76 | PCMPEQL X0, X1 77 | MOVMSKPS X1, AX 78 | CMPB AX, $0x0f 79 | JNE less_determine 80 | ADDB $16, DX 81 | CMPB DX, $64 82 | JE less_eol 83 | JMP less_loop 84 | 85 | less_determine: 86 | MOVAPS X3, X1 87 | PCMPGTL X0, X3 // 3.0.1.0 |>| 2.1.0.0 -> 1.0.1.0 88 | PCMPGTL X1, X0 // 2.1.0.0 |>| 3.0.1.0 -> 0.1.0.0 89 | PSHUFL $27, X3, X3 // $27 is [0, 1, 2, 3], reverse order of elements to get a workable mask below. 90 | PSHUFL $27, X0, X0 91 | MOVMSKPS X3, CX // 1010 92 | MOVMSKPS X0, AX // 0100 93 | CMPB CX, AX 94 | SETGT BX 95 | less_eol: 96 | MOVB BX, ret+16(FP) 97 | RET 98 | -------------------------------------------------------------------------------- /semver_generic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.16 purego !amd64,!386 6 | 7 | package semver 8 | 9 | // Compare computes the difference between two Versions and returns its signum. 10 | // 11 | // 1 if a > b 12 | // 0 if a == b 13 | // -1 if a < b 14 | // 15 | // The 'build' is not compared. 16 | func Compare(a, b *Version) int { 17 | for i := 0; i < len(a.version); i++ { 18 | if a.version[i] == b.version[i] { 19 | continue 20 | } 21 | x := a.version[i] - b.version[i] 22 | return int((x >> 31) - (-x >> 31)) 23 | } 24 | return 0 25 | } 26 | 27 | // compare works like the exported Compare, 28 | // only that it allows to skip fields for performance reasons. 29 | func compare(a, b *Version, skipFields uint) int { 30 | for i := int(skipFields); i < len(a.version); i++ { 31 | if a.version[i] == b.version[i] { 32 | continue 33 | } 34 | x := a.version[i] - b.version[i] 35 | return int((x >> 31) - (-x >> 31)) 36 | } 37 | return 0 38 | } 39 | 40 | // Less is a convenience function for sorting. 41 | func (t *Version) Less(o *Version) bool { 42 | for i := 0; i < len(t.version); i++ { 43 | if t.version[i] == o.version[i] { 44 | continue 45 | } 46 | return t.version[i] < o.version[i] 47 | } 48 | return t.build < o.build 49 | } 50 | 51 | // Less implements the sort.Interface. 52 | func (p VersionPtrs) Less(i, j int) bool { 53 | if p[i] == nil { 54 | return false 55 | } else if p[j] == nil { 56 | return true 57 | } 58 | return p[i].Less(p[j]) 59 | } 60 | -------------------------------------------------------------------------------- /semver_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build amd64 386 6 | // +build !purego 7 | // +build !go1.16 8 | 9 | package semver 10 | 11 | // Compare computes the difference between two Versions and returns its signum. 12 | // 13 | // 1 if a > b 14 | // 0 if a == b 15 | // -1 if a < b 16 | // 17 | // The 'build' is not compared. 18 | //go:noescape 19 | func Compare(a, b *Version) int 20 | 21 | // less returns true if t is lexically smaller than o. 22 | // As side effect, the adjacent 'build' gets compared as well. 23 | // 24 | //go:noescape 25 | func less(a, b *Version) bool 26 | 27 | // Less is a convenience function for sorting. 28 | func (t *Version) Less(o *Version) bool { 29 | return less(t, o) 30 | } 31 | 32 | // Less implements the sort.Interface. 33 | func (p VersionPtrs) Less(i, j int) bool { 34 | if p[i] == nil { 35 | return false 36 | } else if p[j] == nil { 37 | return true 38 | } 39 | return less(p[i], p[j]) 40 | } 41 | -------------------------------------------------------------------------------- /semver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "os" 11 | "testing" 12 | 13 | . "github.com/smartystreets/goconvey/convey" 14 | ) 15 | 16 | func TestNewVersion(t *testing.T) { 17 | Convey("NewVersion works with…", t, FailureContinues, func() { 18 | Convey("1.23.8", func() { 19 | refVer, err := NewVersion([]byte("1.23.8")) 20 | So(err, ShouldBeNil) 21 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, common, 0, 0, 0, 0, common, 0, 0, 0, 0}) 22 | }) 23 | 24 | Convey("v1.23.8", func() { 25 | refVer, err := NewVersion([]byte("v1.23.8")) 26 | So(err, ShouldBeNil) 27 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, common, 0, 0, 0, 0, common, 0, 0, 0, 0}) 28 | }) 29 | 30 | Convey("1.23.8-alpha", func() { 31 | refVer, err := NewVersion([]byte("1.23.8-alpha")) 32 | So(err, ShouldBeNil) 33 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, alpha, 0, 0, 0, 0, common, 0, 0, 0, 0}) 34 | }) 35 | 36 | Convey("1.23.8-alpha.6.7", func() { 37 | refVer, err := NewVersion([]byte("1.23.8-alpha.6.7")) 38 | So(err, ShouldBeNil) 39 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, alpha, 6, 7, 0, 0, common, 0, 0, 0, 0}) 40 | }) 41 | 42 | Convey("1.23.8-p.3", func() { 43 | refVer, err := NewVersion([]byte("1.23.8-p.3")) 44 | So(err, ShouldBeNil) 45 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, patch, 3, 0, 0, 0, common, 0, 0, 0, 0}) 46 | }) 47 | 48 | Convey("1.23.8-p3", func() { 49 | refVer, err := NewVersion([]byte("1.23.8-p3")) 50 | So(err, ShouldBeNil) 51 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, patch, 3, 0, 0, 0, common, 0, 0, 0, 0}) 52 | }) 53 | 54 | Convey("1.23.8-3", func() { 55 | refVer, err := NewVersion([]byte("1.23.8-3")) 56 | So(err, ShouldBeNil) 57 | So(refVer.version, ShouldResemble, [...]int32{1, 23, 8, 0, common, 3, 0, 0, 0, common, 0, 0, 0, 0}) 58 | }) 59 | 60 | Convey("0-0-0.0.0.4", func() { 61 | refVer, err := NewVersion([]byte("0-0-0.0.0.4")) 62 | So(err, ShouldBeNil) 63 | So(refVer.version, ShouldResemble, [...]int32{0, 0, 0, 0, common, 0, 0, 0, 0, common, 0, 0, 0, 4}) 64 | }) 65 | 66 | Convey("214748364 (maxInt32 clipped by one digit)", func() { 67 | refVer, err := NewVersion([]byte("214748364")) 68 | So(err, ShouldBeNil) 69 | So(refVer.version, ShouldResemble, [...]int32{214748364, 0, 0, 0, common, 0, 0, 0, 0, common, 0, 0, 0, 0}) 70 | }) 71 | }) 72 | } 73 | 74 | func TestVersion(t *testing.T) { 75 | Convey("Version 1.3.8 should be part of Version…", t, FailureContinues, func() { 76 | v := []int32{1, 3, 8, 0} 77 | 78 | Convey("1.3.8", func() { 79 | refVer, err := NewVersion([]byte("1.3.8")) 80 | So(err, ShouldBeNil) 81 | So(refVer.version[:4], ShouldResemble, v) 82 | }) 83 | 84 | Convey("1.3.8+build20140722", func() { 85 | refVer, err := NewVersion([]byte("1.3.8+build20140722")) 86 | So(refVer.version[:4], ShouldResemble, v) 87 | So(refVer.build, ShouldEqual, 20140722) 88 | So(err, ShouldBeNil) 89 | }) 90 | 91 | Convey("1.3.8+build2014", func() { 92 | refVer, err := NewVersion([]byte("1.3.8+build2014")) 93 | So(refVer.version[:4], ShouldResemble, v) 94 | So(refVer.build, ShouldEqual, 2014) 95 | So(err, ShouldBeNil) 96 | }) 97 | 98 | Convey("1.3.8-alpha", func() { 99 | refVer, err := NewVersion([]byte("1.3.8-alpha")) 100 | So(err, ShouldBeNil) 101 | So(refVer.version[:4], ShouldResemble, v) 102 | }) 103 | 104 | Convey("1.3.8-beta", func() { 105 | refVer, err := NewVersion([]byte("1.3.8-beta")) 106 | So(err, ShouldBeNil) 107 | So(refVer.version[:4], ShouldResemble, v) 108 | }) 109 | 110 | Convey("1.3.8-pre", func() { 111 | refVer, err := NewVersion([]byte("1.3.8-pre")) 112 | So(err, ShouldBeNil) 113 | So(refVer.version[:4], ShouldResemble, v) 114 | }) 115 | 116 | Convey("1.3.8-r3", func() { 117 | refVer, err := NewVersion([]byte("1.3.8-r3")) 118 | So(err, ShouldBeNil) 119 | So(refVer.version[:4], ShouldResemble, v) 120 | }) 121 | 122 | Convey("1.3.8-3", func() { 123 | refVer, err := NewVersion([]byte("1.3.8-3")) 124 | So(err, ShouldBeNil) 125 | So(refVer.version[:4], ShouldResemble, v) 126 | }) 127 | 128 | }) 129 | 130 | Convey("Working order between Versions", t, func() { 131 | 132 | Convey("equality", func() { 133 | v1 := MustParse("1.3.8") 134 | v2 := MustParse("1.3.8") 135 | So(v1, ShouldResemble, v2) 136 | So(v1.Less(&v2), ShouldBeFalse) 137 | So(v2.Less(&v1), ShouldBeFalse) 138 | So(Compare(&v1, &v2), ShouldEqual, 0) 139 | }) 140 | 141 | Convey("compare", func() { 142 | v1 := MustParse("2.2.1") 143 | v2 := MustParse("2.4.0-beta") 144 | So(Compare(&v1, &v2), ShouldEqual, -1) 145 | So(Compare(&v2, &v1), ShouldEqual, 1) 146 | }) 147 | 148 | Convey("between different release types", func() { 149 | Convey("1.0.0 < 2.0.0", func() { 150 | v1 := MustParse("1.0.0") 151 | v2 := MustParse("2.0.0") 152 | So(v1.Less(&v2), ShouldBeTrue) 153 | So(v2.Less(&v1), ShouldBeFalse) 154 | So(v1, ShouldNotResemble, v2) 155 | }) 156 | 157 | Convey("2.2.1 < 2.4.0-beta", func() { 158 | v1 := MustParse("2.2.1") 159 | v2 := MustParse("2.4.0-beta") 160 | So(v1.Less(&v2), ShouldBeTrue) 161 | So(v2.Less(&v1), ShouldBeFalse) 162 | So(v1, ShouldNotResemble, v2) 163 | }) 164 | 165 | Convey("1.0.0 < 1.0.0-p", func() { 166 | v1 := MustParse("1.0.0") 167 | v2 := MustParse("1.0.0-p") 168 | So(v1.Less(&v2), ShouldBeTrue) 169 | So(v2.Less(&v1), ShouldBeFalse) 170 | So(v1, ShouldNotResemble, v2) 171 | }) 172 | 173 | Convey("1.0.0-rc < 1.0.0", func() { 174 | v1 := MustParse("1.0.0-rc") 175 | v2 := MustParse("1.0.0") 176 | So(v1.Less(&v2), ShouldBeTrue) 177 | So(v1, ShouldNotResemble, v2) 178 | }) 179 | 180 | Convey("1.0.0-pre < 1.0.0-rc", func() { 181 | v1 := MustParse("1.0.0-pre") 182 | v2 := MustParse("1.0.0-rc") 183 | So(v1.Less(&v2), ShouldBeTrue) 184 | So(v1, ShouldNotResemble, v2) 185 | }) 186 | 187 | Convey("1.0.0-beta < 1.0.0-pre", func() { 188 | v1 := MustParse("1.0.0-beta") 189 | v2 := MustParse("1.0.0-pre") 190 | So(v1.Less(&v2), ShouldBeTrue) 191 | So(v1, ShouldNotResemble, v2) 192 | }) 193 | 194 | Convey("1.0.0-alpha < 1.0.0-beta", func() { 195 | v1 := MustParse("1.0.0-alpha") 196 | v2 := MustParse("1.0.0-beta") 197 | So(v1.Less(&v2), ShouldBeTrue) 198 | So(v1, ShouldNotResemble, v2) 199 | }) 200 | }) 201 | 202 | Convey("between same release types", func() { 203 | Convey("1.0.0-p0 < 1.0.0-p1", func() { 204 | v1 := MustParse("1.0.0-p0") 205 | v2 := MustParse("1.0.0-p1") 206 | 207 | So(v1.version, ShouldResemble, [...]int32{1, 0, 0, 0, patch, 0, 0, 0, 0, common, 0, 0, 0, 0}) 208 | So(v2.version, ShouldResemble, [...]int32{1, 0, 0, 0, patch, 1, 0, 0, 0, common, 0, 0, 0, 0}) 209 | 210 | So(v1.Less(&v2), ShouldBeTrue) 211 | So(v1, ShouldNotResemble, v2) 212 | }) 213 | }) 214 | 215 | Convey("with release type specifier", func() { 216 | Convey("1.0.0-rc4-alpha1 < 1.0.0-rc4", func() { 217 | v1 := MustParse("1.0.0-rc4-alpha1") 218 | v2 := MustParse("1.0.0-rc4") 219 | So(v1.Less(&v2), ShouldBeTrue) 220 | So(v1, ShouldNotResemble, v2) 221 | }) 222 | }) 223 | 224 | Convey("with builds", func() { 225 | Convey("1.0.0+build1 < 1.0.0+build2", func() { 226 | v1 := MustParse("1.0.0+build1") 227 | v2 := MustParse("1.0.0+build2") 228 | So(v1.Less(&v2), ShouldBeTrue) 229 | So(v1, ShouldNotResemble, v2) 230 | }) 231 | 232 | Convey("1.0.0_pre20140722+build14 < 1.0.0_pre20140722+build15", func() { 233 | v1 := MustParse("1.0.0_pre20140722+build14") 234 | v2 := MustParse("1.0.0_pre20140722+build15") 235 | So(v1, ShouldNotResemble, v2) 236 | So(v1.Less(&v2), ShouldBeTrue) 237 | }) 238 | }) 239 | 240 | }) 241 | 242 | // see http://devmanual.gentoo.org/ebuild-writing/file-format/ 243 | Convey("Gentoo's example of order works.", t, func() { 244 | v1 := MustParse("1.0.0_alpha_pre") 245 | v2 := MustParse("1.0.0_alpha_rc1") 246 | v3 := MustParse("1.0.0_beta_pre") 247 | v4 := MustParse("1.0.0_beta_p1") 248 | So(v1.version, ShouldResemble, [...]int32{1, 0, 0, 0, alpha, 0, 0, 0, 0, pre, 0, 0, 0, 0}) 249 | So(v2.version, ShouldResemble, [...]int32{1, 0, 0, 0, alpha, 0, 0, 0, 0, rc, 1, 0, 0, 0}) 250 | So(v3.version, ShouldResemble, [...]int32{1, 0, 0, 0, beta, 0, 0, 0, 0, pre, 0, 0, 0, 0}) 251 | So(v4.version, ShouldResemble, [...]int32{1, 0, 0, 0, beta, 0, 0, 0, 0, patch, 1, 0, 0, 0}) 252 | 253 | So(v1, ShouldNotResemble, v2) 254 | So(v2, ShouldNotResemble, v3) 255 | So(v3, ShouldNotResemble, v4) 256 | So(v1.Less(&v2), ShouldBeTrue) 257 | So(v2.Less(&v3), ShouldBeTrue) 258 | So(v3.Less(&v4), ShouldBeTrue) 259 | }) 260 | 261 | Convey("Reject invalid Versions.", t, func() { 262 | Convey("with surplus digits", func() { 263 | _, err := NewVersion([]byte("1.0.0.0.4")) 264 | So(err, ShouldNotBeNil) 265 | }) 266 | 267 | Convey("with surplus dots", func() { 268 | _, err := NewVersion([]byte("1..8")) 269 | So(err, ShouldNotBeNil) 270 | _, err = NewVersion([]byte("1.8.rc2")) 271 | So(err, ShouldNotBeNil) 272 | }) 273 | 274 | Convey("with unknown tags", func() { 275 | _, err := NewVersion([]byte("1.8-gazilla")) 276 | So(err, ShouldNotBeNil) 277 | _, err = NewVersion([]byte("1.8-+build4")) 278 | So(err, ShouldNotBeNil) 279 | _, err = NewVersion([]byte("1.8-a")) 280 | So(err, ShouldNotBeNil) 281 | }) 282 | 283 | Convey("with fringe builds", func() { 284 | _, err := NewVersion([]byte("10.0.17763.253+build19H3")) 285 | So(err, ShouldNotBeNil) 286 | _, err = NewVersion([]byte("10.0.17763.253+19H3")) 287 | So(err, ShouldNotBeNil) 288 | e := err.(InvalidStringValue) 289 | So(e.IsInvalid(), ShouldBeTrue) 290 | }) 291 | 292 | Convey("with excessive tags", func() { 293 | _, err := NewVersion([]byte("1.8-alpha-beta-rc")) 294 | So(err, ShouldNotBeNil) 295 | _, err = NewVersion([]byte("1.8-alpha-beta3rc")) 296 | So(err, ShouldNotBeNil) 297 | }) 298 | 299 | Convey("with trailing dashes", func() { 300 | _, err := NewVersion([]byte("5678.9-")) 301 | So(err, ShouldNotBeNil) 302 | }) 303 | 304 | Convey("with too long parts", func() { 305 | _, err := NewVersion([]byte("100000000000007000000000000000070000000000000.0.0")) 306 | So(err, ShouldNotBeNil) 307 | _, err = NewVersion([]byte("1.0.0_alpha444444444444444444444444444444444444444")) 308 | So(err, ShouldNotBeNil) 309 | _, err = NewVersion([]byte("1.0.0_alpha-rc444444444444444444444444444444444444")) 310 | So(err, ShouldNotBeNil) 311 | _, err = NewVersion([]byte("1.0.0_alpha-rc1+build44444444444444444444444444444")) 312 | So(err, ShouldNotBeNil) 313 | }) 314 | }) 315 | } 316 | 317 | func TestVersionOrder(t *testing.T) { 318 | 319 | Convey("Version 1.2.3-alpha4 should be…", t, func() { 320 | v1 := MustParse("1.2.3-alpha4") 321 | 322 | Convey("reasonably less than Version 1.2.3", func() { 323 | v2 := MustParse("1.2.3") 324 | So(v1.limitedLess(v2), ShouldBeTrue) 325 | }) 326 | 327 | Convey("reasonably less than Version 1.2.3-alpha4.0.0.1", func() { 328 | v2 := MustParse("1.2.3-alpha4.0.0.1") 329 | So(v1.limitedLess(v2), ShouldBeTrue) 330 | }) 331 | 332 | Convey("not reasonably less than 1.2.3-alpha4-p5", func() { 333 | v2 := MustParse("1.2.3-alpha4-p5") 334 | So(v1.limitedLess(v2), ShouldBeFalse) 335 | }) 336 | }) 337 | 338 | } 339 | 340 | func TestVersionAccessors(t *testing.T) { 341 | Convey("For version 1.2.3 we should have", t, func() { 342 | v := MustParse("1.2.3") 343 | 344 | Convey("major equals 1", func() { 345 | So(v.Major(), ShouldEqual, 1) 346 | }) 347 | 348 | Convey("minor equals 2", func() { 349 | So(v.Minor(), ShouldEqual, 2) 350 | }) 351 | 352 | Convey("patch equals 3", func() { 353 | So(v.Patch(), ShouldEqual, 3) 354 | }) 355 | }) 356 | } 357 | 358 | // VersionsFromGentoo is a set of about 36000 versions read from canned file 359 | // and stored in the way that required the least conversions. 360 | // 361 | // The order is as read, not sorted. 362 | // 363 | // Because this resembles a “real world” sample, use this for benchmarks. 364 | var VersionsFromGentoo = func() [][]byte { 365 | lst := make([][]byte, 0, 36300) // 0 { 372 | lst = append(lst, []byte(line)) 373 | } 374 | } 375 | } 376 | 377 | if len(lst) < 36000 { 378 | panic("testdata/*.list has been split into insufficient elements") 379 | } 380 | return lst 381 | }() 382 | 383 | var strForBenchmarks = "1.2.3-beta4.5.6" 384 | var verForBenchmarks = []byte(strForBenchmarks) 385 | var benchV, benchErr = NewVersion(append(verForBenchmarks, '5')) 386 | 387 | func BenchmarkSemverNewVersion(b *testing.B) { 388 | b.SkipNow() 389 | v, e := NewVersion(verForBenchmarks) 390 | 391 | for n := 0; n < b.N; n++ { 392 | v, e = NewVersion(verForBenchmarks) 393 | } 394 | benchV, benchErr = v, e 395 | } 396 | 397 | func Benchmark_NewVersion(b *testing.B) { 398 | v, e := NewVersion(verForBenchmarks) 399 | lim := len(VersionsFromGentoo) 400 | 401 | for n := 0; n < b.N; n++ { 402 | v, e = NewVersion(VersionsFromGentoo[n%lim]) 403 | } 404 | benchV, benchErr = v, e 405 | } 406 | 407 | var compareResult = 5 408 | 409 | func Benchmark_Compare(b *testing.B) { 410 | v, _ := NewVersion(verForBenchmarks) 411 | r := Compare(&benchV, &v) 412 | 413 | for n := 0; n < b.N; n++ { 414 | r = Compare(&benchV, &v) 415 | } 416 | compareResult = r 417 | } 418 | 419 | var benchResult bool 420 | 421 | const benchCompareIdx = 10 422 | 423 | func BenchmarkVersion_Less(b *testing.B) { 424 | t := Version{} 425 | o := Version{} 426 | o.version[benchCompareIdx] = benchCompareIdx 427 | r := t.Less(&o) 428 | 429 | for n := 0; n < b.N; n++ { 430 | r = t.Less(&o) 431 | } 432 | benchResult = r 433 | } 434 | 435 | func BenchmarkBytesCompare(b *testing.B) { 436 | b.SkipNow() 437 | var k, m [14]byte 438 | r := bytes.Compare(k[:], m[:]) 439 | 440 | for n := 0; n < b.N; n++ { 441 | r = bytes.Compare(k[:], m[:]) 442 | } 443 | compareResult = r 444 | } 445 | -------------------------------------------------------------------------------- /serialization_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package semver 6 | 7 | import ( 8 | "database/sql" 9 | "encoding/json" 10 | "testing" 11 | 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | var _ sql.Scanner = &Version{} 16 | var _ string = (Version{}).String() 17 | 18 | // var _ driver.Valuer = Version{} 19 | 20 | func TestSerialization(t *testing.T) { 21 | Convey("Versions within JSON…", t, FailureContinues, func() { 22 | Convey("get parsed into structs", func() { 23 | Convey("if quoted", func() { 24 | in := []byte(`{"ver": "2.31.4"}`) 25 | var out struct{ Ver Version } 26 | expect, _ := NewVersion([]byte("2.31.4")) 27 | So(expect.Bytes(), ShouldResemble, []byte("2.31.4")) 28 | 29 | err := json.Unmarshal(in, &out) 30 | So(err, ShouldBeNil) 31 | So(out.Ver, ShouldResemble, expect) 32 | }) 33 | Convey("even without quotes", func() { 34 | in := []byte(`{"ver": 2}`) 35 | var out struct{ Ver Version } 36 | expect, _ := NewVersion([]byte("v2")) 37 | 38 | err := json.Unmarshal(in, &out) 39 | So(err, ShouldBeNil) 40 | So(out.Ver, ShouldResemble, expect) 41 | }) 42 | }) 43 | 44 | Convey("will be serialized correctly", func() { 45 | for _, str := range []string{ 46 | "2.31.4", "14.9", "1.5.3.1", "8", 47 | "8+build66", 48 | "1.5.1-3", "1.12-rc2", 49 | "0-0-0.0.0.4", 50 | } { 51 | given, err := NewVersion([]byte(str)) 52 | So(err, ShouldBeNil) 53 | if err != nil { 54 | continue 55 | } 56 | t := struct{ Ver Version }{given} 57 | 58 | out, err := json.Marshal(&given) 59 | So(err, ShouldBeNil) 60 | So(string(out), ShouldEqual, `"`+string(str)+`"`) // cast to 'string' for legibility 61 | 62 | out, err = json.Marshal(&t) 63 | So(err, ShouldBeNil) 64 | So(string(out), ShouldEqual, `{"Ver":"`+string(str)+`"}`) // cast to 'string' for legibility 65 | } 66 | }) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !mips,!mips64,!ppc64,!s390x 6 | 7 | package semver 8 | 9 | import ( 10 | "sort" 11 | "sync" 12 | ) 13 | 14 | const ( 15 | // Less than this many elements and a "residual" sort gets called, 16 | // which usually is sort.Sort or sort.Slice. 17 | // To figure out this particular value I've run benchmarks, 18 | // but got a range of close results; you could go as low 19 | // as 64 or 32 on some architectures. 20 | thresholdForResidualSort = 128 21 | 22 | maxKeyIndex = 13 // len(Version.version) - 1 23 | ) 24 | 25 | // Radix sort—and variants will be used below—needs some scratch space, 26 | // which this pool provides. 27 | // 28 | // Don't rely on the initial size for new arrays. Expand its capacity if need be. 29 | var versionPointerBuffer = sync.Pool{ 30 | New: func() interface{} { 31 | b := make([]*Version, 40*1024) 32 | return &b 33 | }, 34 | } 35 | 36 | // Sort reorders the pointers so that the Versions appear in ascending order. 37 | // 38 | // For that it will use optimized algorithms usually less time-complex than 39 | // the generic ones found in package 'Sort'. 40 | // Specifically, variants of radix sort expected to run in O(n). 41 | // 42 | // Allocates a copy of VersionPtrs. 43 | func (p VersionPtrs) Sort() { 44 | if len(p) < thresholdForResidualSort { 45 | sort.Sort(p) 46 | return 47 | } 48 | 49 | buf := versionPointerBuffer.Get().(*[]*Version) 50 | tmp := *buf 51 | p.multikeyRadixSort(tmp, 0) 52 | for i := range tmp { 53 | tmp[i] = nil 54 | } 55 | versionPointerBuffer.Put(buf) 56 | } 57 | 58 | // multikeyRadixSort exploits the typical distribution of Version values 59 | // to use two keys at once in a radix-sort run. 60 | func (p VersionPtrs) multikeyRadixSort(tmp []*Version, keyIndex uint8) { 61 | // Some fields can be negative and need to get a bump. (Mind order in memory!) 62 | // As "alpha" is the lowest one, use its absolute value. 63 | var fieldAdjustment uint64 = 0 64 | switch keyIndex { 65 | case 3, 8: 66 | fieldAdjustment = (-alpha) << 32 67 | case 4, 9: 68 | fieldAdjustment = (-alpha) 69 | } 70 | 71 | // Collate the histogram. 72 | var offset [256]int 73 | for _, v := range p { 74 | if v == nil { 75 | continue 76 | } 77 | k := uint8(twoFieldKey(&v.version, fieldAdjustment, uint8(keyIndex))) 78 | offset[k]++ 79 | } 80 | watermark := offset[0] - offset[0] // 'watermark' will finally be the total tally. 81 | for i, count := range offset { 82 | offset[i] = watermark 83 | watermark += count 84 | } 85 | 86 | // Setup an unordered copy. 87 | // The allocated space will subsequently be recycled as scratch space. 88 | if len(tmp) >= len(p) { 89 | tmp = tmp[:len(p)] 90 | copy(tmp, p) 91 | } else { 92 | tmp = append(tmp[:0], p...) 93 | } 94 | for i := watermark; i < len(p); i++ { 95 | p[i] = nil // Fill the tail end with the 'nil' we'll be skipping. 96 | } 97 | 98 | // Order from 'tmp' into 'p'. 99 | for _, v := range tmp { 100 | if v == nil { 101 | continue 102 | } 103 | k := uint8(twoFieldKey(&v.version, fieldAdjustment, uint8(keyIndex))) 104 | p[offset[k]] = v 105 | offset[k]++ 106 | } 107 | 108 | p.multikeyRadixSortDescent(tmp, keyIndex, offset) 109 | } 110 | 111 | // multikeyRadixSortDescent is multikeyRadixSort's outsourced descent- and recurse steps. 112 | // Extracted for easier profiling. 113 | func (p VersionPtrs) multikeyRadixSortDescent(tmp []*Version, keyIndex uint8, offset [256]int) { 114 | // Collapse resolved lower fields if below unresolved larger fields. 115 | // Consider 2007.9 and 2008.6 that both map to 0b0011… and would be misordered as 9>6. 116 | for i := uint8(12); i < 16; i++ { // 12 to 15 represent "unresolved"/"consider N-11 digits". 117 | strideEnd := offset[i<<4|0x0f] 118 | for j := i << 4; j < (i<<4 | 0x0f); j++ { 119 | offset[j] = strideEnd 120 | } 121 | } 122 | 123 | // Any tailing nil are beyond offsets, henceforth no longer considered. 124 | watermark := offset[0] - offset[0] 125 | for k, ceiling := range offset { 126 | subsliceLen := ceiling - watermark // aka "stride" 127 | if subsliceLen < 2 { 128 | watermark = ceiling 129 | continue 130 | } 131 | 132 | // Recursion depth is contained in this paragraph. 133 | // If 'compare' or 'less' for a particular architecture considers the 'build' suffix 134 | // then so must 'isSorted' for this to work. 135 | subslice := p[watermark:ceiling] 136 | watermark = ceiling 137 | unresolvedLeftSide := uint8(k) >= (12 << 4) 138 | if (!unresolvedLeftSide || subsliceLen < thresholdForResidualSort) && 139 | // Else the probability to encounter an already sorted stride is too low. 140 | subslice.isSorted(uint(keyIndex)) { 141 | continue 142 | } 143 | if subsliceLen < thresholdForResidualSort { 144 | sort.Sort(subslice) 145 | continue 146 | } 147 | 148 | switch k := uint8(k); { 149 | case unresolvedLeftSide: // Unsorted trailer with values that keyFn did not resolve. 150 | maxBits := ((k >> 4) - 11) * 8 151 | subslice.radixSort(tmp, keyIndex, maxBits) 152 | case (k & 0x0f) >= 12: // This key is in order, the next is not: descent. 153 | maxBits := ((k & 0x0f) - 11) * 8 // 12 → 1 → 8 154 | subslice.radixSort(tmp, keyIndex+1, maxBits) 155 | default: 156 | subslice.multikeyRadixSort(tmp, keyIndex+2) 157 | } 158 | } 159 | } 160 | 161 | // radixSort sorts on the one field indicated by keyIndex. 162 | // maxBits really denominates the octets (bytes) to consider, and any excess MSB are assumed to be zero. 163 | // 164 | // Tailing nil are expected to have been stripped. 165 | func (p VersionPtrs) radixSort(tmp []*Version, keyIndex, maxBits uint8) { 166 | if keyIndex > maxKeyIndex { 167 | // This check makes the compiler happy, who will skip checking for that repeatedly. 168 | // In case you get this panic though, 'isSorted' and 'less'/'compare' are not 169 | // considering the same fields (usually it's 'build' that's been forgotten). 170 | panic("keyIndex out of bounds") 171 | } 172 | from, to := p, tmp[:len(p)] // Have the compiler check this once. 173 | var offset [256]uint 174 | 175 | for fromBits := maxBits - maxBits; fromBits < maxBits; fromBits += 8 { 176 | // Building the histogram again. 177 | // Although this can be done for all bytes in one run, 178 | // which would need an [1024]uint, I found it's slower in Golang. 179 | for i := range offset { 180 | offset[i] = 0 181 | } 182 | for _, v := range from { 183 | if v == nil { 184 | continue 185 | } 186 | k := uint8(v.version[keyIndex] >> fromBits) 187 | offset[k]++ 188 | } 189 | watermark := offset[0] - offset[0] 190 | for i, count := range offset { 191 | offset[i] = watermark 192 | watermark += count 193 | } 194 | 195 | // Now comes the ordering, which is stable of course. 196 | for _, v := range from { 197 | if v == nil { 198 | continue 199 | } 200 | k := uint8(v.version[keyIndex] >> fromBits) 201 | to[offset[k]] = v 202 | offset[k]++ 203 | } 204 | to, from = from, to // Prepare the next run. 205 | } 206 | if maxBits%16 != 0 { 207 | copy(to, from) 208 | } 209 | 210 | p.radixSortDescent(tmp, keyIndex) 211 | } 212 | 213 | // radixSortDescent is radixSort's outsourced descent- and recurse steps. 214 | // Extracted for easier profiling. 215 | func (p VersionPtrs) radixSortDescent(tmp []*Version, keyIndex uint8) { 216 | if keyIndex >= maxKeyIndex { 217 | return // Nothing to sort anymore. 218 | } 219 | 220 | // The descent. Unlike this, multikeyRadixSort does only one run and hence 221 | // is able to read strides from its histogram ("offset[]"). 222 | // As classical radix sort cannot (even if optimized to one run for the histogram), 223 | // the collection needs to be visited once more here. 224 | startIdx := 0 225 | lastValue := p[0].version[keyIndex] 226 | for i, v := range p { 227 | value := v.version[keyIndex] 228 | if lastValue == value { // Accumulate spans of the same value. 229 | continue 230 | } 231 | if i-startIdx < 2 { 232 | startIdx, lastValue = i, value 233 | continue 234 | } 235 | 236 | subslice := p[startIdx:i] 237 | if subslice.isSorted(uint(keyIndex + 1)) { 238 | startIdx, lastValue = i, value 239 | continue 240 | } 241 | 242 | residualLength := i - startIdx 243 | startIdx, lastValue = i, value 244 | switch { 245 | case residualLength < thresholdForResidualSort: 246 | sort.Sort(subslice) 247 | case keyIndex <= (maxKeyIndex - 2): 248 | subslice.multikeyRadixSort(tmp, keyIndex+1) 249 | default: 250 | subslice.radixSort(tmp, keyIndex+1, 32) 251 | } 252 | } 253 | // Capture trailer of same values (such as 250.100, 250.0). 254 | if residualLength := len(p) - startIdx; residualLength > 1 { 255 | subslice := p[startIdx:] 256 | if subslice.isSorted(uint(keyIndex + 1)) { 257 | return 258 | } 259 | 260 | switch { 261 | case residualLength < thresholdForResidualSort: 262 | sort.Sort(subslice) 263 | case keyIndex <= (maxKeyIndex - 2): 264 | subslice.multikeyRadixSort(tmp, keyIndex+1) 265 | default: 266 | subslice.radixSort(tmp, keyIndex+1, 32) 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /sort_386.s: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !purego 6 | // +build !go1.16 7 | 8 | #include "go_asm.h" 9 | #include "textflag.h" 10 | 11 | TEXT ·twoFieldKey(SB),NOSPLIT,$0-20 12 | MOVL v+0(FP), SI 13 | MOVQ fieldAdjustment+4(FP), X1 // Two packed L. 14 | MOVBLZX keyIndex+12(FP), AX 15 | 16 | // Contains the two relevant fields. They need to be swapped, though. 17 | MOVQ (SI)(AX*4), X0 18 | PADDL X1, X0 19 | 20 | // This function is comprised of two interleaved calculations (to hide within latencies) 21 | // which use register as follows. 22 | // X5, X6, X7: Select value of fields <=11, or NN to add to the result below. 23 | // X0 to X4: Number of digits (bytes) used. Will eventually be 0 for any <=11. 24 | // Calculated without LZCNT or POPCNT. 25 | 26 | MOVQ elevenPL(SB), X1 27 | MOVQ X0, X4 28 | MOVQ elevenPL(SB), X7 29 | PCMPGTL X1, X4 // Holds the "greater-than-11"-mask. 30 | PXOR X1, X1 31 | MOVQ X0, X5 32 | PCMPEQB X0, X1 // This saturates whole bytes, and leaves gaps inbetween. 33 | MOVQ X0, X6 34 | // X0, the input, is no longer needed. 35 | PCMPEQB X3, X3 36 | PXOR X3, X1 // ~X1 37 | MOVQ movmaskAndSwapPB(SB), X2 38 | PAND X2, X1 // 0xff → 0x01 and so forth 39 | MOVQ shiftRightByFourPL(SB), X0 40 | PCMPGTL X7, X5 41 | PAND X1, X2 // MOVQ X1, X2; X1 and X2 are the same. 42 | PAND X5, X7 43 | PANDN X6, X5 44 | PSRLL $8, X1 // XXX(mark): can be achieved with one less shift. 45 | POR X5, X7 46 | PMULLW X7, X0 47 | POR X1, X2 48 | PSRLL $8, X1 49 | POR X1, X2 50 | PSRLL $8, X1 51 | POR X1, X2 // The final mask. (Due to the chosen bits PSADBW will flip nibbles.) 52 | 53 | // Combine the two results. 54 | PAND X4, X2 55 | PADDW X0, X2 56 | PXOR X3, X3 57 | // This packs everything into an octet, swapping nibbles. Hence the *16 or <<4. 58 | PSADBW X3, X2 59 | 60 | // Converting this to a uint8 here would be inefficient. 61 | MOVL X2, ret+16(FP) 62 | RET 63 | 64 | DATA elevenPL+0x00(SB)/8, $0x0000000b0000000b // {11, 11} 65 | GLOBL elevenPL(SB), (RODATA+NOPTR), $8 66 | 67 | DATA shiftRightByFourPL+0x00(SB)/8, $0x0000000100000010 // shift the rightmost byte by <<4. 68 | GLOBL shiftRightByFourPL(SB), (RODATA+NOPTR), $8 69 | 70 | DATA movmaskAndSwapPB+0x00(SB)/8, $0x0101010110101010 // {0x01…, 0x10…} 71 | GLOBL movmaskAndSwapPB(SB), (RODATA+NOPTR), $8 72 | -------------------------------------------------------------------------------- /sort_amd64.s: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !purego 6 | // +build !go1.17 7 | 8 | #include "go_asm.h" 9 | #include "textflag.h" 10 | 11 | TEXT ·twoFieldKey(SB),NOSPLIT,$0-32 12 | MOVQ v+0(FP), SI 13 | MOVQ fieldAdjustment+8(FP), M1 // Two packed L. 14 | MOVBQZX keyIndex+16(FP), AX 15 | 16 | // Contains the two relevant fields. They need to be swapped, though. 17 | MOVQ (SI)(AX*4), M0 18 | PADDL M1, M0 19 | 20 | // This function is comprised of two interleaved calculations (to hide within latencies) 21 | // which use register as follows. 22 | // M5, M6, M7: Select value of fields <=11, or NN to add to the result below. 23 | // M0 to M4: Number of digits (bytes) used. Will eventually be 0 for any <=11. 24 | // Calculated without LZCNT or POPCNT. 25 | 26 | MOVQ $0x0000000b0000000b, DX // {11, 11} 27 | MOVQ $0x0000000100000010, CX // shift the rightmost byte by <<4. 28 | MOVQ DX, M1 29 | // PSHUFW $0xe4, M0, M4 // Just a fancy MOVQ M0, M4 // The assembler throws "invalid instruction". 30 | BYTE $0x0f; BYTE $0x70; BYTE $0xe0; BYTE $0xe4 31 | MOVQ DX, M7 32 | PCMPGTL M1, M4 // Holds the "greater-than-11"-mask. 33 | PXOR M1, M1 34 | BYTE $0x0f; BYTE $0x70; BYTE $0xe8; BYTE $0xe4 // PSHUFW // MOVQ M0, M5 35 | PCMPEQB M0, M1 // This saturates whole bytes, and leaves gaps inbetween. 36 | BYTE $0x0f; BYTE $0x70; BYTE $0xf0; BYTE $0xe4 // PSHUFW // MOVQ M0, M6 37 | // M0, the input, is no longer needed. 38 | PCMPEQB M3, M3 39 | PXOR M3, M1 // ~M1 40 | MOVQ $0x0101010110101010, BX // {0x01…, 0x10…} 41 | MOVQ BX, M2 42 | PAND M2, M1 // 0xff → 0x01 and so forth 43 | MOVQ CX, M0 44 | PCMPGTL M7, M5 45 | PAND M1, M2 // MOVQ M1, M2; M1 and M2 are the same. 46 | PAND M5, M7 47 | PANDN M6, M5 48 | PSRLL $8, M1 // XXX(mark): can be achieved with one less shift. 49 | POR M5, M7 50 | PMULLW M7, M0 51 | POR M1, M2 52 | PSRLL $8, M1 53 | POR M1, M2 54 | PSRLL $8, M1 55 | POR M1, M2 // The final mask. (Due to the chosen bits PSADBW will flip nibbles.) 56 | 57 | // Combine the two results. 58 | PAND M4, M2 59 | PADDW M0, M2 60 | PXOR M3, M3 61 | // This packs everything into an octet, swapping nibbles. Hence the *16 or <<4. 62 | BYTE $0x0f; BYTE $0xf6; BYTE $0xd3 // PSADBW M3, M2 63 | 64 | // Converting this to a uint8 here would be inefficient. 65 | MOVQ M2, ret+24(FP) 66 | EMMS 67 | RET 68 | -------------------------------------------------------------------------------- /sort_big.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build mips mips64 ppc64 s390x 6 | 7 | package semver 8 | 9 | import ( 10 | "sort" 11 | ) 12 | 13 | // Sort for bigendian is not optimized and resorts to Go's own sort. 14 | func (p VersionPtrs) Sort() { 15 | sort.Sort(p) 16 | } 17 | -------------------------------------------------------------------------------- /sort_generic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build amd64,go1.17 386,go1.16 purego,!mips,!mips64,!ppc64,!s390x !amd64,!386,!mips,!mips64,!ppc64,!s390x 6 | 7 | package semver 8 | 9 | // magnitudeAwareKey is part of twoFieldKey below, and returns small figures verbatim, 10 | // else signals magnitudes in a way susceptible to sorting. 11 | // Parameter 'x' must be non-negative. 12 | func magnitudeAwareKey(x int32) uint8 { 13 | if x <= 11 { 14 | return uint8(x) 15 | } 16 | // For all larger numbers, store the number of bytes +11. 17 | if x <= 0xffff { 18 | if x <= 0xff { 19 | return 12 20 | } 21 | return 13 22 | } 23 | if x <= 0xffffff { 24 | return 14 25 | } 26 | return 15 27 | } 28 | 29 | // twoFieldKeyGonly is part of multikeyRadixSort and derives a key from two fields in 'v'. 30 | // The order established by the keys is ascending but not total: 31 | // fields with great values map to a low-resolution key. 32 | // Fields must be non-negative. 33 | // 34 | // This is the Go-only implementation, available for benchmarks on architectures 35 | // that otherwise used an optimized variant. 36 | func twoFieldKey(v *[14]int32, fieldAdjustment uint64, keyIndex uint8) uint8 { 37 | off := int32(fieldAdjustment) 38 | n1 := magnitudeAwareKey(v[keyIndex]+off) << 4 39 | if n1 >= (12 << 4) { 40 | return n1 41 | } 42 | off = int32(fieldAdjustment >> 32) 43 | return (n1 | magnitudeAwareKey(v[keyIndex+1]+off)) 44 | } 45 | 46 | func (p VersionPtrs) isSorted(skipFields uint) bool { 47 | if len(p) < 2 || skipFields > maxKeyIndex { 48 | return true 49 | } 50 | 51 | previous := p[0] 52 | for _, ptr := range p { 53 | if previous == nil { 54 | if ptr != nil { 55 | return false 56 | } 57 | continue 58 | } 59 | 60 | if compare(previous, ptr, skipFields) > 0 { 61 | return false 62 | } 63 | previous = ptr 64 | } 65 | return true 66 | } 67 | -------------------------------------------------------------------------------- /sort_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build amd64 386,!go1.16 6 | // +build !purego 7 | // +build !go1.17 8 | 9 | package semver 10 | 11 | // twoFieldKeyGonly is part of multikeyRadixSort. 12 | // Please see the *_generic.go file for a detailed description. 13 | // 14 | //go:noescape 15 | func twoFieldKey(v *[14]int32, fieldAdjustment uint64, keyIndex uint8) uint 16 | 17 | // isSorted is called by radixSort and multikeyRadixSort, and won't contain any nil. 18 | func (p VersionPtrs) isSorted(skipFields uint) bool { 19 | if len(p) < 2 { 20 | return true 21 | } 22 | 23 | previous := p[0] 24 | for _, ptr := range p { 25 | if Compare(previous, ptr) > 0 { 26 | return false 27 | } 28 | previous = ptr 29 | } 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Semver Package Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !mips,!mips64,!ppc64,!s390x 6 | 7 | package semver 8 | 9 | import ( 10 | "fmt" 11 | "math/rand" 12 | "sort" 13 | "testing" 14 | 15 | . "github.com/smartystreets/goconvey/convey" 16 | ) 17 | 18 | func makeVersionCollection(b *testing.B) ([]Version, []*Version) { 19 | unsorted := make([]*Version, len(VersionsFromGentoo)) 20 | 21 | var erroneous int 22 | actual := make([]Version, len(VersionsFromGentoo)) 23 | for n, src := range VersionsFromGentoo { 24 | if err := actual[n].UnmarshalText(src); err != nil { 25 | substitute := fmt.Sprintf("%s.%d", strForBenchmarks, rand.Intn(len(VersionsFromGentoo))) 26 | actual[n].UnmarshalText([]byte(substitute)) 27 | erroneous++ 28 | } 29 | unsorted[n] = &actual[n] 30 | } 31 | if b != nil { 32 | b.ReportMetric(float64(erroneous)/float64(len(unsorted)), "substitutes/op") 33 | } 34 | 35 | return actual, unsorted 36 | } 37 | 38 | func containsAll(reference, dubious []*Version) bool { 39 | examine_reference: 40 | for _, ptr := range reference { 41 | if ptr == nil { 42 | continue 43 | } 44 | for _, other := range dubious { 45 | if ptr == other { 46 | continue examine_reference 47 | } 48 | } 49 | return false 50 | } 51 | return true 52 | } 53 | 54 | func TestSortPtr(t *testing.T) { 55 | Convey("VersionPtrs sorting", t, func() { 56 | _, unsorted := makeVersionCollection(nil) 57 | data := make([]*Version, len(unsorted)+1) 58 | // Insert a 'nil' as the item before the last. 59 | data[len(data)-2], data[len(data)-1] = nil, data[len(data)-2] 60 | copy(data, unsorted) 61 | 62 | x := VersionPtrs(data) 63 | lessFn := x.Less 64 | x.Sort() 65 | 66 | Convey("establishes an ascending order", func() { 67 | isSorted := sort.SliceIsSorted(data, lessFn) 68 | if isSorted { 69 | So(isSorted, ShouldBeTrue) 70 | return 71 | } 72 | 73 | precedingVersion := x[0] // Conveniently our test set does not start with a nil. 74 | for i := range x { 75 | if x[i] == nil { 76 | precedingVersion = nil 77 | continue 78 | } 79 | if precedingVersion == nil { // Case [nil, proper]. 80 | t.Error("nil not contiguous at the end, got:", i, *x[i]) 81 | break 82 | } 83 | if Compare(precedingVersion, x[i]) >= 1 { 84 | t.Error("Wrong order between:", i, *precedingVersion, *x[i]) 85 | break 86 | } 87 | precedingVersion = x[i] 88 | } 89 | So(isSorted, ShouldBeTrue) 90 | }) 91 | 92 | Convey("does not lose elements", func() { 93 | if len(data) > 100000 { 94 | // In a benchmark situation, skip this as it is O(n²). 95 | SkipSo(true, ShouldBeTrue) 96 | return 97 | } 98 | So(containsAll(unsorted, data), ShouldBeTrue) 99 | }) 100 | }) 101 | } 102 | 103 | func Benchmark_SortPtr(b *testing.B) { 104 | b.StopTimer() 105 | _, unsorted := makeVersionCollection(b) 106 | data := make([]*Version, len(unsorted)) 107 | b.ReportAllocs() 108 | 109 | for i := 0; i < b.N; i++ { 110 | copy(data, unsorted) 111 | b.StartTimer() 112 | x := VersionPtrs(data) 113 | x.Sort() 114 | b.StopTimer() 115 | if !sort.SliceIsSorted(x, x.Less) { 116 | b.Skip("Resulting slice is not in order.") 117 | break 118 | } 119 | } 120 | } 121 | 122 | func TestTwoFieldKey(t *testing.T) { 123 | Convey("twoFieldKey correctly derives keys", t, FailureContinues, func() { 124 | // {input, expected output} 125 | for _, testcase := range []struct { 126 | version string 127 | keyIndex uint8 128 | adjustment uint64 129 | expectedKey []uint8 130 | }{ 131 | // Examine output beyond thresholds for resolved/unresolved fields. 132 | {"9.16777216", 0, 0, []uint8{(9<<4 | 15)}}, 133 | {"9.65536", 0, 0, []uint8{(9<<4 | 14)}}, 134 | {"9.256", 0, 0, []uint8{(9<<4 | 13)}}, 135 | {"9.250", 0, 0, []uint8{(9<<4 | 12)}}, 136 | {"11.11", 0, 0, []uint8{(11<<4 | 11)}}, 137 | {"250.9", 0, 0, []uint8{(12<<4 | 9), (12 << 4)}}, 138 | {"256.9", 0, 0, []uint8{(13<<4 | 9), (13 << 4)}}, 139 | {"65536.9", 0, 0, []uint8{(14<<4 | 9), (14 << 4)}}, 140 | {"16777216.9", 0, 0, []uint8{(15<<4 | 9), (15 << 4)}}, 141 | // Walk indices, and non-positive fields which get a +4 (as (-alpha) = 4). 142 | {"1.2.3.4-alpha5.6.7.8", 0, 0, []uint8{(1<<4 | 2)}}, 143 | {"1.2.3.4-alpha5.6.7.8", 1, 0, []uint8{(2<<4 | 3)}}, 144 | {"1.2.3.4-alpha5.6.7.8", 2, 0, []uint8{(3<<4 | 4)}}, 145 | {"1.2.3.4-alpha5.6.7.8", 3, (-alpha) << 32, []uint8{(4<<4 | 0)}}, 146 | {"1.2.3.4-beta5.6.7.8", 3, (-alpha) << 32, []uint8{(4<<4 | 1)}}, 147 | {"1.2.3.4-5.6.7.8", 3, (-alpha) << 32, []uint8{(4<<4 | (-alpha))}}, 148 | {"1.2.3.4-r5.6.7.8", 3, (-alpha) << 32, []uint8{(4<<4 | 5)}}, 149 | {"1.2.3.4-alpha5.6.7.8", 4, (-alpha), []uint8{(0<<4 | 5)}}, 150 | {"1.2.3.4-beta5.6.7.8", 4, (-alpha), []uint8{(1<<4 | 5)}}, 151 | {"1.2.3.4-5.6.7.8", 4, (-alpha), []uint8{(4<<4 | 5)}}, 152 | {"1.2.3.4-r5.6.7.8", 4, (-alpha), []uint8{(5<<4 | 5)}}, 153 | {"1.2.3.4-r", 4, (-alpha), []uint8{(5<<4 | 0)}}, 154 | {"1.2.3.4-alpha5.6.7.8", 5, 0, []uint8{(5<<4 | 6)}}, 155 | // Deepest non-negative fields. 156 | {"1-beta6-beta7", 8, (-alpha) << 32, []uint8{(0<<4 | 1)}}, 157 | {"1-beta6-beta7", 9, (-alpha), []uint8{(1<<4 | 7)}}, 158 | } { 159 | given, _ := NewVersion([]byte(testcase.version)) 160 | gotKey := uint8(twoFieldKey(&given.version, 161 | testcase.adjustment, 162 | testcase.keyIndex)) 163 | // The keyFn could've already collapsed lower fields below unresolved larger fields. 164 | So(gotKey, ShouldBeIn, testcase.expectedKey) 165 | } 166 | }) 167 | } 168 | 169 | // By running multiple versions through key-derivation functions 170 | // the cpu's branch predictor is utilized "realistically." 171 | // That is, merely using one version might appear to be faster. 172 | 173 | var tmpForTwoFieldKey = twoFieldKey(&benchV.version, 0, 0) // To inherit its return type. 174 | 175 | func BenchmarkTwoFieldKey(b *testing.B) { 176 | b.StopTimer() 177 | versions, _ := makeVersionCollection(b) 178 | versionsLen := len(versions) 179 | b.StartTimer() 180 | 181 | for i := 0; i < b.N; i++ { 182 | v := versions[i%versionsLen] 183 | tmpForTwoFieldKey |= twoFieldKey(&v.version, 0, 0) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /testdata/regenerate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euop pipefail 3 | 4 | # For mirrors please see: https://www.gentoo.org/downloads/mirrors/ 5 | : ${GENTOO_MIRRORS:="https://mirrors.evowise.com/gentoo http://distfiles.gentoo.org"} 6 | : ${PV_list:="gentoo-portage-PV.list"} 7 | : ${portage_dt:="20190917"} 8 | if [[ ! -s "${PV_list}" ]]; then 9 | declare -ra mirrors=(${GENTOO_MIRRORS}) 10 | 11 | for mirror in "${mirrors[@]}"; do 12 | if curl --globoff --fail --progress-bar --show-error --location \ 13 | --header "Accept: application/x-xz" \ 14 | "${mirror}/snapshots/portage-${portage_dt}.tar.xz" \ 15 | | tar --xz -tv \ 16 | | grep -F .ebuild \ 17 | | sed -e 's@.*-\([0-9_.-].*\)\.ebuild$@\1@g' \ 18 | >"${PV_list}" 19 | then 20 | break 21 | else 22 | rm -f "${PV_list}" 23 | fi 24 | done 25 | 26 | if [[ ! -s "${PV_list}" ]]; then 27 | exit 2 28 | fi 29 | fi 30 | 31 | exit 0 --------------------------------------------------------------------------------