Add Release String Parsing to fwget

In order to generate our google storage file paths, we'll need to be
able to break down a release string. This change adds that
functionality.

BUG=b:266093164
TEST=manual and unit tests (go test./...)

Change-Id: Id3f72a701e2b4138d6335634328d3dc10e207922
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crostestutils/+/4219877
Reviewed-by: Kevin Shelton <kmshelton@chromium.org>
Tested-by: Sean Carpenter <seancarpenter@google.com>
Commit-Queue: Sean Carpenter <seancarpenter@google.com>
diff --git a/go/src/firmware/cmd/fwget/fwget.go b/go/src/firmware/cmd/fwget/fwget.go
index df4e3e4..36ec9bb 100644
--- a/go/src/firmware/cmd/fwget/fwget.go
+++ b/go/src/firmware/cmd/fwget/fwget.go
@@ -13,6 +13,7 @@
 	"fmt"
 	"io/ioutil"
 	"os"
+	"regexp"
 	"strings"
 
 	"golang.org/x/exp/slices"
@@ -24,8 +25,12 @@
 	helpUsage               = "Usage: fwget [--board <board> --firmware <ec|ap> --version <latest|stable|release number] path"
 	exampleUsage            = "fwget --board=galtic --firmware=ec --version=stable /tmp/your_image.bin "
 	exampleUsageDescription = "Example usage: download the latest stable EC image for galtic boards to /tmp/your_image.bin"
+	exampleReleaseString    = "R89-13606.459.0"
 
 	firmwareTarName = "firmware_from_source.tar.bz2"
+
+	// Example: R89-13606.459.0
+	releaseStringRegexPattern = `(R\d+)-(\d+)\.(\d+)\.(\d+)`
 )
 
 // A mapping of boards to their respective baseboards. Necessary to determine
@@ -39,12 +44,30 @@
 var supportedFirmwareTypes = []string{"ec", "ap"}
 var versionAliases = []string{"latest", "stable"}
 
+var releaseStringRegex = regexp.MustCompile(releaseStringRegexPattern)
+
 type image struct {
 	board    string
 	firmware string
 	version  string
 }
 
+type release struct {
+	milestone    string
+	majorVersion string
+	minorVersion string
+	patchNumber  string
+}
+
+func (r release) String() string {
+	return fmt.Sprintf("%s-%s.%s.%s",
+		r.milestone,
+		r.majorVersion,
+		r.minorVersion,
+		r.patchNumber,
+	)
+}
+
 func main() {
 	flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
 
@@ -55,7 +78,7 @@
 	// help := flags.Bool("help", false, "display the help message")
 	board := flags.String("board", "", "the board to fetch the firmware image for")
 	firmware := flags.String("firmware", "", "the firmware type: \"ec\" or \"ap\"")
-	version := flags.String("version", "stable", "the firmware image version: a version number, \"latest\" or \"stable\"")
+	version := flags.String("version", "stable", fmt.Sprintf("the firmware image version: \"latest\", \"stable\" or a specific release like %s", exampleReleaseString))
 
 	flags.Parse(os.Args[1:])
 
@@ -150,3 +173,18 @@
 	}
 	return nil
 }
+
+// parseReleaseString builds a release struct from a string containing a
+// release string.
+func parseReleaseString(releaseString string) (*release, error) {
+	match := releaseStringRegex.FindStringSubmatch(releaseString)
+	if len(match) != 5 {
+		return nil, fmt.Errorf("unable to parse release string \"%s\". Expected something like the following example: \"%s\"", releaseString, exampleReleaseString)
+	}
+	return &release{
+		milestone:    match[1],
+		majorVersion: match[2],
+		minorVersion: match[3],
+		patchNumber:  match[4],
+	}, nil
+}
diff --git a/go/src/firmware/cmd/fwget/fwget_test.go b/go/src/firmware/cmd/fwget/fwget_test.go
index 4b87221..03412ba 100644
--- a/go/src/firmware/cmd/fwget/fwget_test.go
+++ b/go/src/firmware/cmd/fwget/fwget_test.go
@@ -7,6 +7,7 @@
 import (
 	"flag"
 	"fmt"
+	"reflect"
 	"testing"
 )
 
@@ -118,3 +119,57 @@
 		})
 	}
 }
+
+// TestParseReleaseString tests that a release string like R89-13606.459.0
+// can be properly identified and marshalled into a release struct.
+func TestParseReleaseString(t *testing.T) {
+	testCases := []struct {
+		releaseString string
+		release       *release
+	}{
+		{
+			"R89-13606.459.0",
+			&release{
+				milestone:    "R89",
+				majorVersion: "13606",
+				minorVersion: "459",
+				patchNumber:  "0",
+			},
+		},
+		{
+			"someText/R12-123.569/before-the-string/R89-13606.459.0/someTextAfterwards",
+			&release{
+				milestone:    "R89",
+				majorVersion: "13606",
+				minorVersion: "459",
+				patchNumber:  "0",
+			},
+		},
+		{
+			"X89-13606.459.0",
+			nil,
+		},
+		{
+			"R89-ABCD.459.0",
+			nil,
+		},
+
+		{
+			"R89-ABCD.459",
+			nil,
+		},
+	}
+	for _, tt := range testCases {
+		tt := tt
+		t.Run(tt.releaseString, func(t *testing.T) {
+			t.Parallel()
+			release, _ := parseReleaseString(tt.releaseString)
+			if !reflect.DeepEqual(release, tt.release) {
+				t.Errorf(
+					"Expected release %+v\n but got %+v\n",
+					tt.release,
+					release)
+			}
+		})
+	}
+}