osbase/oci/registry: add package

This adds the registry package, which contains a client and server
implementation of the OCI Distribution spec.

Change-Id: I080bb1dbc511f8e6466ca370b090d459d2b730e8
Reviewed-on: https://review.monogon.dev/c/monogon/+/4086
Tested-by: Jenkins CI
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
diff --git a/osbase/oci/registry/headers_test.go b/osbase/oci/registry/headers_test.go
new file mode 100644
index 0000000..f883ea9
--- /dev/null
+++ b/osbase/oci/registry/headers_test.go
@@ -0,0 +1,83 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package registry
+
+import "testing"
+
+func TestParseAuthenticateHeader(t *testing.T) {
+	testCases := []struct {
+		desc   string
+		header []string
+		parsed []authenticateChallenge
+	}{
+		{"absent", nil, nil},
+		{"no params",
+			[]string{"Basic, !#$%&'*+-.^_`|~019abzABZ"},
+			[]authenticateChallenge{{scheme: "Basic"}, {scheme: "!#$%&'*+-.^_`|~019abzABZ"}}},
+		{"token68",
+			[]string{"0 a", "1 abzABZ019-._~+/, 2 abc=, 3   a==="},
+			[]authenticateChallenge{
+				{scheme: "0", info: "a"},
+				{scheme: "1", info: "abzABZ019-._~+/"},
+				{scheme: "2", info: "abc="},
+				{scheme: "3", info: "a==="},
+			}},
+		{"params",
+			[]string{`0 a="=,", empty =  "", escape="\a\\\"", ` + "1 token!#$%&'*+-.^_`|~019abzABZ=!#$%&'*+-.^_`|~019abzABZ"},
+			[]authenticateChallenge{
+				{scheme: "0", params: map[string]string{"a": "=,", "empty": "", "escape": `a\"`}},
+				{scheme: "1", params: map[string]string{"token!#$%&'*+-.^_`|~019abzabz": "!#$%&'*+-.^_`|~019abzABZ"}},
+			}},
+		{"duplicate param", []string{`Basic realm="apps", REALM=other`}, nil},
+		{"empty", []string{"", " ", "\t", ",", " , ,,\t ,", "Basic"}, []authenticateChallenge{{scheme: "Basic"}}},
+		{"RFC example",
+			[]string{`Basic realm="simple", Newauth realm="apps", type=1, title="Login to \"apps\""`},
+			[]authenticateChallenge{
+				{scheme: "Basic", params: map[string]string{"realm": "simple"}},
+				{scheme: "Newauth", params: map[string]string{"realm": "apps", "type": "1", "title": `Login to "apps"`}},
+			}},
+		{"extra commas",
+			[]string{` , , Basic , , realm="simple" , , Newauth ,realm="apps",type=1` + "\t" + `, ,title="Login to \"apps\"" , , `},
+			[]authenticateChallenge{
+				{scheme: "Basic", params: map[string]string{"realm": "simple"}},
+				{scheme: "Newauth", params: map[string]string{"realm": "apps", "type": "1", "title": `Login to "apps"`}},
+			}},
+		{"missing comma between challenges", []string{"Basic\tBearer"}, nil},
+		{"missing comma between challenges 2", []string{"Basic !"}, nil},
+		{"missing comma after token68", []string{"Basic a Bearer"}, nil},
+		{"missing comma between params", []string{`Basic realm="simple" type=1`}, nil},
+		{"missing quote", []string{`Basic realm="simple`}, nil},
+		{"missing value", []string{`Basic !=`}, nil},
+	}
+	for _, tC := range testCases {
+		t.Run(tC.desc, func(t *testing.T) {
+			actual := parseAuthenticateHeader(tC.header)
+			if want, got := len(tC.parsed), len(actual); want != got {
+				t.Fatalf("Expected %d challenges, got %d", want, got)
+			}
+			for i, actualC := range actual {
+				wantC := tC.parsed[i]
+				if want, got := wantC.scheme, actualC.scheme; want != got {
+					t.Errorf("Expected scheme %q, got %q", want, got)
+				}
+				if want, got := wantC.info, actualC.info; want != got {
+					t.Errorf("Expected info %q, got %q", want, got)
+				}
+				for param, want := range wantC.params {
+					got, ok := actualC.params[param]
+					if !ok {
+						t.Errorf("Scheme %s: Missing param %q", wantC.scheme, param)
+					} else if want != got {
+						t.Errorf("Scheme %s: Expected %s=%q, got %q", wantC.scheme, param, want, got)
+					}
+				}
+				for param := range actualC.params {
+					if _, ok := wantC.params[param]; !ok {
+						t.Errorf("Scheme %s: Extra param %q", wantC.scheme, param)
+					}
+				}
+			}
+		})
+	}
+}