cpu: handle vendor suffixes in parseRelease

Kernel release strings such as "3.4.35_hi3535" on some Synology devices
failed to parse because only dash and plus suffixes were stripped.
Parse the leading digits of each component instead, mirroring the Go
runtime fix.

This copies the implementation from the Go runtime's parseRelease as
updated in CL 781800.

Fixes golang/go#80094

Co-authored-by: Brad Fitzpatrick <bradfitz@golang.org>
Change-Id: I41f8fd0a1d6c012eebd5368e623b42e99220d1a3
Reviewed-on: https://go-review.googlesource.com/c/sys/+/793760
Reviewed-by: Jorropo <jorropo.pgm@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
diff --git a/cpu/parse.go b/cpu/parse.go
index 56a7e1a..12a99af 100644
--- a/cpu/parse.go
+++ b/cpu/parse.go
@@ -6,38 +6,50 @@
 
 import "strconv"
 
-// parseRelease parses a dot-separated version number. It follows the semver
-// syntax, but allows the minor and patch versions to be elided.
+// parseRelease parses a dot-separated version number from the prefix
+// of rel. It returns ok=true only if at least the major and minor
+// components were successfully parsed; the patch component is
+// best-effort. Trailing vendor or build suffixes such as
+// "-generic", "+", "_hi3535", or "-rc1" are ignored.
 //
 // This is a copy of the Go runtime's parseRelease from
-// https://golang.org/cl/209597.
+// https://golang.org/cl/209597, updated in https://golang.org/cl/781800.
 func parseRelease(rel string) (major, minor, patch int, ok bool) {
-	// Strip anything after a dash or plus.
-	for i := range len(rel) {
-		if rel[i] == '-' || rel[i] == '+' {
-			rel = rel[:i]
-			break
+	// next consumes a run of decimal digits from the front of rel,
+	// returning the parsed value. If the digits are followed by a
+	// '.', it is consumed and more is set so the caller knows to
+	// parse another component; otherwise scanning terminates and
+	// the rest of rel is discarded.
+	next := func() (n int, more, ok bool) {
+		i := 0
+		for i < len(rel) && rel[i] >= '0' && rel[i] <= '9' {
+			i++
 		}
+		if i == 0 {
+			return 0, false, false
+		}
+		n, err := strconv.Atoi(rel[:i])
+		if err != nil {
+			return 0, false, false
+		}
+		if i < len(rel) && rel[i] == '.' {
+			rel = rel[i+1:]
+			return n, true, true
+		}
+		rel = ""
+		return n, false, true
 	}
 
-	next := func() (int, bool) {
-		for i := range len(rel) {
-			if rel[i] == '.' {
-				ver, err := strconv.Atoi(rel[:i])
-				rel = rel[i+1:]
-				return ver, err == nil
-			}
-		}
-		ver, err := strconv.Atoi(rel)
-		rel = ""
-		return ver, err == nil
+	var more bool
+	if major, more, ok = next(); !ok || !more {
+		return 0, 0, 0, false
 	}
-	if major, ok = next(); !ok || rel == "" {
-		return
+	if minor, more, ok = next(); !ok {
+		return 0, 0, 0, false
 	}
-	if minor, ok = next(); !ok || rel == "" {
-		return
+	if !more {
+		return major, minor, 0, true
 	}
-	patch, ok = next()
-	return
+	patch, _, _ = next()
+	return major, minor, patch, true
 }
diff --git a/cpu/parse_test.go b/cpu/parse_test.go
index 5a35881..e12017a 100644
--- a/cpu/parse_test.go
+++ b/cpu/parse_test.go
@@ -14,12 +14,21 @@
 var parseReleaseTests = []parseReleaseTest{
 	{"", -1, -1, -1},
 	{"x", -1, -1, -1},
-	{"5", 5, 0, 0},
+	// A single component is not enough; major+minor are required.
+	{"5", -1, -1, -1},
+	{"5-x", -1, -1, -1},
 	{"5.12", 5, 12, 0},
 	{"5.12-x", 5, 12, 0},
 	{"5.12.1", 5, 12, 1},
 	{"5.12.1-x", 5, 12, 1},
 	{"5.12.1.0", 5, 12, 1},
+	{"5.15.0-91-generic", 5, 15, 0},
+	{"4.19.0+", 4, 19, 0},
+	{"6.6.0-rc1", 6, 6, 0},
+	// Synology embedded Linux appends a platform identifier after an
+	// underscore. See golang/go#79612.
+	{"3.4.35_hi3535", 3, 4, 35},
+	{"2.6.32_synology", 2, 6, 32},
 	{"5.20496382327982653440", -1, -1, -1},
 }