semver.gno
4.64 Kb · 203 lines
1// Package semver is an on-chain port of the core of golang.org/x/mod/semver
2// (and the spirit of github.com/Masterminds/semver): parse "vMAJOR.MINOR.PATCH
3// [-prerelease][+build]" strings and compare them with correct Semantic
4// Versioning 2.0.0 precedence. Pure and deterministic — no time, randomness,
5// goroutines, or I/O.
6//
7// This package holds only the pure parsing/comparison API. For a live,
8// stateful gnoweb demo that imports it and keeps an on-chain "submitted
9// versions" board, see
10// [r/moul/x/daily/semverdemo](/r/moul/x/daily/semverdemo/v0).
11package semver
12
13import (
14 "errors"
15 "strings"
16)
17
18// Version is a parsed semantic version. Build metadata is retained for display
19// but, per the spec, is ignored when comparing precedence.
20type Version struct {
21 Major, Minor, Patch int
22 Pre []string // pre-release identifiers (dot-separated)
23 Build string // build metadata (after '+')
24 Orig string // original input
25}
26
27var errBadVersion = errors.New("invalid semantic version")
28
29// Parse reads "vMAJOR.MINOR.PATCH[-prerelease][+build]". The leading 'v' is
30// optional. Numeric fields must be non-empty digit runs without leading zeros.
31func Parse(s string) (Version, error) {
32 v := Version{Orig: s}
33 body := strings.TrimPrefix(s, "v")
34
35 if i := strings.IndexByte(body, '+'); i >= 0 {
36 v.Build = body[i+1:]
37 body = body[:i]
38 if v.Build == "" {
39 return Version{}, errBadVersion
40 }
41 }
42 if i := strings.IndexByte(body, '-'); i >= 0 {
43 pre := body[i+1:]
44 body = body[:i]
45 v.Pre = strings.Split(pre, ".")
46 for _, id := range v.Pre {
47 if !validPreIdent(id) {
48 return Version{}, errBadVersion
49 }
50 }
51 }
52
53 parts := strings.Split(body, ".")
54 if len(parts) != 3 {
55 return Version{}, errBadVersion
56 }
57 nums := [3]int{}
58 for i, p := range parts {
59 n, ok := parseNum(p)
60 if !ok {
61 return Version{}, errBadVersion
62 }
63 nums[i] = n
64 }
65 v.Major, v.Minor, v.Patch = nums[0], nums[1], nums[2]
66 return v, nil
67}
68
69// Compare returns -1, 0, or +1 as a < b, a == b, or a > b under semver
70// precedence. Unparseable inputs sort after everything valid (and equal to
71// each other), so Compare never panics.
72func Compare(a, b string) int {
73 va, ea := Parse(a)
74 vb, eb := Parse(b)
75 switch {
76 case ea != nil && eb != nil:
77 return 0
78 case ea != nil:
79 return 1
80 case eb != nil:
81 return -1
82 }
83 return va.compare(vb)
84}
85
86func (v Version) compare(o Version) int {
87 if c := cmpInt(v.Major, o.Major); c != 0 {
88 return c
89 }
90 if c := cmpInt(v.Minor, o.Minor); c != 0 {
91 return c
92 }
93 if c := cmpInt(v.Patch, o.Patch); c != 0 {
94 return c
95 }
96 return comparePre(v.Pre, o.Pre)
97}
98
99// comparePre implements SemVer §11: a version WITH a pre-release has lower
100// precedence than the same version WITHOUT one.
101func comparePre(a, b []string) int {
102 if len(a) == 0 && len(b) == 0 {
103 return 0
104 }
105 if len(a) == 0 { // a is a release, b is a pre-release
106 return 1
107 }
108 if len(b) == 0 {
109 return -1
110 }
111 for i := 0; i < len(a) && i < len(b); i++ {
112 if c := compareIdent(a[i], b[i]); c != 0 {
113 return c
114 }
115 }
116 return cmpInt(len(a), len(b))
117}
118
119// compareIdent: numeric identifiers compare numerically and always rank below
120// alphanumeric ones; alphanumerics compare in ASCII order.
121func compareIdent(a, b string) int {
122 an, aNum := parseNum(a)
123 bn, bNum := parseNum(b)
124 switch {
125 case aNum && bNum:
126 return cmpInt(an, bn)
127 case aNum:
128 return -1
129 case bNum:
130 return 1
131 }
132 return strings.Compare(a, b)
133}
134
135func cmpInt(a, b int) int {
136 switch {
137 case a < b:
138 return -1
139 case a > b:
140 return 1
141 }
142 return 0
143}
144
145// parseNum accepts a non-empty run of ASCII digits with no leading zero (except
146// "0" itself), returning the value and whether it qualified.
147func parseNum(s string) (int, bool) {
148 if s == "" {
149 return 0, false
150 }
151 if len(s) > 1 && s[0] == '0' {
152 return 0, false
153 }
154 n := 0
155 for i := 0; i < len(s); i++ {
156 c := s[i]
157 if c < '0' || c > '9' {
158 return 0, false
159 }
160 n = n*10 + int(c-'0')
161 }
162 return n, true
163}
164
165// validPreIdent allows [0-9A-Za-z-] and rejects empty identifiers.
166func validPreIdent(s string) bool {
167 if s == "" {
168 return false
169 }
170 for i := 0; i < len(s); i++ {
171 c := s[i]
172 ok := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
173 (c >= 'A' && c <= 'Z') || c == '-'
174 if !ok {
175 return false
176 }
177 }
178 return true
179}
180
181// Canonical renders the parsed version back to a canonical string.
182func (v Version) Canonical() string {
183 s := ufmt(v.Major) + "." + ufmt(v.Minor) + "." + ufmt(v.Patch)
184 if len(v.Pre) > 0 {
185 s += "-" + strings.Join(v.Pre, ".")
186 }
187 if v.Build != "" {
188 s += "+" + v.Build
189 }
190 return s
191}
192
193func ufmt(n int) string {
194 if n == 0 {
195 return "0"
196 }
197 var b []byte
198 for n > 0 {
199 b = append([]byte{byte('0' + n%10)}, b...)
200 n /= 10
201 }
202 return string(b)
203}