pkg/bootparam: add bootparam pkg

This adds the bootparam package which can marshal and unmarshal the Linux
kernel command line into boot parameters and a rest section passed to
init.

This is a very quirky format, thus there is a fuzz testing harness
against the reference implementation from the kernel included to verify
correctness.

A set of weird edge cases is rejected by Unmarshal instead of parsing
to nonsensical data as the reference implementation does to save on
complexity in the parser.

Change-Id: I6debfa67e69ae8db4e0356f34ecb127ea27d18de
Reviewed-on: https://review.monogon.dev/c/monogon/+/1125
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/bootparam/bootparam_test.go b/metropolis/pkg/bootparam/bootparam_test.go
new file mode 100644
index 0000000..a0032a4
--- /dev/null
+++ b/metropolis/pkg/bootparam/bootparam_test.go
@@ -0,0 +1,60 @@
+// If this is bootparam we have an import cycle
+package bootparam_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"source.monogon.dev/metropolis/pkg/bootparam"
+	"source.monogon.dev/metropolis/pkg/bootparam/ref"
+)
+
+// Fuzzers can be run with
+// bazel test //metropolis/pkg/bootparam:bootparam_test
+//   --test_arg=-test.fuzz=FuzzMarshal
+//   --test_arg=-test.fuzzcachedir=/tmp/fuzz
+//   --test_arg=-test.fuzztime=60s
+
+func FuzzUnmarshal(f *testing.F) {
+	f.Add(`initrd="\test\some=value" root=yolo "definitely quoted" ro rootflags=`)
+	f.Fuzz(func(t *testing.T, a string) {
+		refOut, refRest := ref.Parse(a)
+		out, rest, err := bootparam.Unmarshal(a)
+		if err != nil {
+			return
+		}
+		if diff := cmp.Diff(refOut, out); diff != "" {
+			t.Errorf("Parse(%q): params mismatch (-want +got):\n%s", a, diff)
+		}
+		if refRest != rest {
+			t.Errorf("Parse(%q): expected rest to be %q, got %q", a, refRest, rest)
+		}
+	})
+}
+
+func FuzzMarshal(f *testing.F) {
+	// Choose delimiters which mean nothing to the parser
+	f.Add("a:b;assd:9dsf;1234", "some fancy rest")
+	f.Fuzz(func(t *testing.T, paramsRaw string, rest string) {
+		paramsSeparated := strings.Split(paramsRaw, ";")
+		var params bootparam.Params
+		for _, p := range paramsSeparated {
+			a, b, _ := strings.Cut(p, ":")
+			params = append(params, bootparam.Param{Param: a, Value: b})
+		}
+		rest = bootparam.TrimLeftSpace(rest)
+		encoded, err := bootparam.Marshal(params, rest)
+		if err != nil {
+			return // Invalid input
+		}
+		refOut, refRest := ref.Parse(encoded)
+		if diff := cmp.Diff(refOut, params); diff != "" {
+			t.Errorf("Marshal(%q): params mismatch (-want +got):\n%s", paramsRaw, diff)
+		}
+		if refRest != rest {
+			t.Errorf("Parse(%q, %q): expected rest to be %q, got %q", paramsRaw, rest, refRest, rest)
+		}
+	})
+}