m/p/bootparam: add Consoles param parser

Adds the Consoles() helper function which processes all console params
and puts them into a set.

Change-Id: I7333bf5c22e6cd79bea0155c6a558e79bf6e824b
Reviewed-on: https://review.monogon.dev/c/monogon/+/1525
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/pkg/bootparam/BUILD.bazel b/metropolis/pkg/bootparam/BUILD.bazel
index 33a9d91..4b63f28 100644
--- a/metropolis/pkg/bootparam/BUILD.bazel
+++ b/metropolis/pkg/bootparam/BUILD.bazel
@@ -2,17 +2,23 @@
 
 go_library(
     name = "bootparam",
-    srcs = ["bootparam.go"],
+    srcs = [
+        "bootparam.go",
+        "params.go",
+    ],
     importpath = "source.monogon.dev/metropolis/pkg/bootparam",
     visibility = ["//visibility:public"],
 )
 
 go_test(
     name = "bootparam_test",
-    srcs = ["bootparam_test.go"],
+    srcs = [
+        "bootparam_test.go",
+        "params_test.go",
+    ],
+    embed = [":bootparam"],
     gc_goopts = ["-d=libfuzzer"],
     deps = [
-        ":bootparam",
         "//metropolis/pkg/bootparam/ref",
         "@com_github_google_go_cmp//cmp",
     ],
diff --git a/metropolis/pkg/bootparam/params.go b/metropolis/pkg/bootparam/params.go
new file mode 100644
index 0000000..bbb4fae
--- /dev/null
+++ b/metropolis/pkg/bootparam/params.go
@@ -0,0 +1,26 @@
+package bootparam
+
+import (
+	"regexp"
+	"strings"
+)
+
+var validTTYRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
+
+// Consoles returns the set of consoles passed to the kernel, i.e. the values
+// passed to the console= directive. It normalizes away any possibly present
+// /dev/ prefix, returning values like ttyS0. It returns an empty set in case
+// no valid console parameters exist.
+func (p Params) Consoles() map[string]bool {
+	consoles := make(map[string]bool)
+	for _, pa := range p {
+		if pa.Param == "console" {
+			consoleParts := strings.Split(pa.Value, ",")
+			consoleName := strings.TrimPrefix(consoleParts[0], "/dev/")
+			if validTTYRegexp.MatchString(consoleName) {
+				consoles[consoleName] = true
+			}
+		}
+	}
+	return consoles
+}
diff --git a/metropolis/pkg/bootparam/params_test.go b/metropolis/pkg/bootparam/params_test.go
new file mode 100644
index 0000000..c76dd88
--- /dev/null
+++ b/metropolis/pkg/bootparam/params_test.go
@@ -0,0 +1,43 @@
+package bootparam
+
+import "testing"
+
+func TestConsoles(t *testing.T) {
+	cases := []struct {
+		name     string
+		cmdline  string
+		consoles []string
+	}{
+		{"Empty", "", []string{}},
+		{"None", "notconsole=test", []string{}},
+		{"Single", "asdf=ttyS1 console=ttyS0,115200", []string{"ttyS0"}},
+		{"MultipleSame", "console=ttyS0 noop console=ttyS0", []string{"ttyS0"}},
+		{"MultipleDiff", "console=tty27 console=ttyACM0", []string{"tty27", "ttyACM0"}},
+		{"WithDev", "console=/dev/ttyXYZ0", []string{"ttyXYZ0"}},
+		{"BrokenBadDev", "console=/etc/passwd", []string{}},
+		{"BrokenNoValue", "console=", []string{}},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			p, _, err := Unmarshal(c.cmdline)
+			if err != nil {
+				t.Fatalf("Failed to parse cmdline %q: %v", c.cmdline, err)
+			}
+			consoles := p.Consoles()
+			wantConsoles := make(map[string]bool)
+			for _, con := range c.consoles {
+				wantConsoles[con] = true
+			}
+			for con := range wantConsoles {
+				if !consoles[con] {
+					t.Errorf("Expected console %s to be returned but it wasn't", con)
+				}
+			}
+			for con := range consoles {
+				if !wantConsoles[con] {
+					t.Errorf("Didn't expect console %s to be returned but it was", con)
+				}
+			}
+		})
+	}
+}