1// Package semver is an on-chain port of the core of golang.org/x/mod/semver2// (and the spirit of github.com/Masterminds/semver): parse "vMAJOR.MINOR.PATCH3// [-prerelease][+build]" strings and compare them with correct Semantic4// 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 "submitted9// versions" board, see10// [r/moul/x/daily/semverdemo](/r/moul/x/daily/semverdemo/v0).11package semver1213import (14 "errors"15 "strings"16)1718// Version is a parsed semantic version. Build metadata is retained for display19// but, per the spec, is ignored when comparing precedence.20type Version struct {21 Major, Minor, Patch int22 Pre []string // pre-release identifiers (dot-separated)23 Build string // build metadata (after '+')24 Orig string // original input25}2627var errBadVersion = errors.New("invalid semantic version")2829// Parse reads "vMAJOR.MINOR.PATCH[-prerelease][+build]". The leading 'v' is30// 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")3435 if i := strings.IndexByte(body, '+'); i >= 0 {36 v.Build = body[i+1:]37 body = body[:i]38 if v.Build == "" {39 return Version{}, errBadVersion40 }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{}, errBadVersion49 }50 }51 }5253 parts := strings.Split(body, ".")54 if len(parts) != 3 {55 return Version{}, errBadVersion56 }57 nums := [3]int{}58 for i, p := range parts {59 n, ok := parseNum(p)60 if !ok {61 return Version{}, errBadVersion62 }63 nums[i] = n64 }65 v.Major, v.Minor, v.Patch = nums[0], nums[1], nums[2]66 return v, nil67}6869// Compare returns -1, 0, or +1 as a < b, a == b, or a > b under semver70// precedence. Unparseable inputs sort after everything valid (and equal to71// 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 078 case ea != nil:79 return 180 case eb != nil:81 return -182 }83 return va.compare(vb)84}8586func (v Version) compare(o Version) int {87 if c := cmpInt(v.Major, o.Major); c != 0 {88 return c89 }90 if c := cmpInt(v.Minor, o.Minor); c != 0 {91 return c92 }93 if c := cmpInt(v.Patch, o.Patch); c != 0 {94 return c95 }96 return comparePre(v.Pre, o.Pre)97}9899// comparePre implements SemVer §11: a version WITH a pre-release has lower100// precedence than the same version WITHOUT one.101func comparePre(a, b []string) int {102 if len(a) == 0 && len(b) == 0 {103 return 0104 }105 if len(a) == 0 { // a is a release, b is a pre-release106 return 1107 }108 if len(b) == 0 {109 return -1110 }111 for i := 0; i < len(a) && i < len(b); i++ {112 if c := compareIdent(a[i], b[i]); c != 0 {113 return c114 }115 }116 return cmpInt(len(a), len(b))117}118119// compareIdent: numeric identifiers compare numerically and always rank below120// 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 -1129 case bNum:130 return 1131 }132 return strings.Compare(a, b)133}134135func cmpInt(a, b int) int {136 switch {137 case a < b:138 return -1139 case a > b:140 return 1141 }142 return 0143}144145// parseNum accepts a non-empty run of ASCII digits with no leading zero (except146// "0" itself), returning the value and whether it qualified.147func parseNum(s string) (int, bool) {148 if s == "" {149 return 0, false150 }151 if len(s) > 1 && s[0] == '0' {152 return 0, false153 }154 n := 0155 for i := 0; i < len(s); i++ {156 c := s[i]157 if c < '0' || c > '9' {158 return 0, false159 }160 n = n*10 + int(c-'0')161 }162 return n, true163}164165// validPreIdent allows [0-9A-Za-z-] and rejects empty identifiers.166func validPreIdent(s string) bool {167 if s == "" {168 return false169 }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 false176 }177 }178 return true179}180181// 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.Build189 }190 return s191}192193func ufmt(n int) string {194 if n == 0 {195 return "0"196 }197 var b []byte198 for n > 0 {199 b = append([]byte{byte('0' + n%10)}, b...)200 n /= 10201 }202 return string(b)203}204Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.