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.go b/osbase/oci/registry/headers.go
new file mode 100644
index 0000000..3d1de2f
--- /dev/null
+++ b/osbase/oci/registry/headers.go
@@ -0,0 +1,179 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package registry
+
+import "strings"
+
+type authenticateChallenge struct {
+	scheme string
+	info   string
+	params map[string]string
+}
+
+// parseAuthenticateHeader parses the values of a WWW-Authenticate HTTP header.
+// parameter names are converted to lower case.
+// If any value fails to parse, it returns nil.
+func parseAuthenticateHeader(authenticate []string) []authenticateChallenge {
+	// From RFC 9110:
+	// WWW-Authenticate = #challenge
+	// challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+	// auth-scheme = token
+	// token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
+	// auth-param = token BWS "=" BWS ( token / quoted-string )
+	// #element => [ element ] *( OWS "," OWS [ element ] )
+	// token = 1*tchar
+	// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+	//       / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+	//       / DIGIT / ALPHA
+	// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+	// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
+	// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+	// obs-text = %x80-FF
+	// OWS = *( SP / HTAB )
+	// BWS = OWS
+	// VCHAR = %x21-7E
+
+	var challenges []authenticateChallenge
+	for _, a := range authenticate {
+		for {
+			a = strings.TrimLeft(a, " \t,") // Consume commas and OWS
+			if a == "" {
+				break
+			}
+			var scheme string
+			scheme, a = scanToken(a) // Consume auth-scheme
+			if scheme == "" {
+				return nil
+			}
+			challenge := authenticateChallenge{
+				scheme: scheme,
+			}
+			if !strings.HasPrefix(a, " ") { // Check for 1*SP
+				a = strings.TrimLeft(a, " \t") // Consume OWS
+				if a != "" && a[0] != ',' {    // Check for mandatory comma
+					return nil
+				}
+				challenges = append(challenges, challenge)
+				continue
+			}
+			a = strings.TrimLeft(a, " ") // Consume 1*SP
+
+			// Check for token68
+			i := 0
+			for i < len(a) && charType[a[i]]&charTypeToken68 != 0 {
+				i++
+			}
+			if i != 0 {
+				for i < len(a) && a[i] == '=' { // Consume *"="
+					i++
+				}
+				remain := strings.TrimLeft(a[i:], " \t") // Consume OWS
+				if remain == "" || remain[0] == ',' {    // Check for mandatory comma
+					// Confirmed token68
+					challenge.info = a[:i]
+					challenges = append(challenges, challenge)
+					a = remain
+					continue
+				}
+			}
+
+			challenge.params = make(map[string]string)
+			for {
+				// Check for auth-param
+				remain := strings.TrimLeft(a, " \t,") // Consume commas and OWS
+				var name string
+				name, remain = scanToken(remain) // Consume token
+				if name == "" {
+					break
+				}
+				remain = strings.TrimLeft(remain, " \t") // Consume BWS
+				var ok bool
+				if remain, ok = strings.CutPrefix(remain, "="); !ok { // Consume "="
+					break
+				}
+				remain = strings.TrimLeft(remain, " \t") // Consume BWS
+				var value string
+				if remain, ok = strings.CutPrefix(remain, `"`); ok { // Check for quoted-string
+					i := 0
+					for i < len(remain) {
+						if charType[remain[i]]&charTypeQdtext != 0 {
+							i++
+						} else if remain[i] == '\\' && i+1 < len(remain) && charType[remain[i+1]]&charTypeQuotedPair != 0 {
+							value += remain[:i]
+							remain = remain[i+1:] // Drop the backslash to unescape the string
+							i = 1
+						} else {
+							break
+						}
+					}
+					value += remain[:i]
+					remain = remain[i:]
+					if remain, ok = strings.CutPrefix(remain, `"`); !ok { // Consume quote
+						break
+					}
+				} else {
+					value, remain = scanToken(remain) // Consume token
+					if value == "" {
+						break
+					}
+				}
+				// Confirmed auth-param
+				name = strings.ToLower(name) // name is case-insensitive
+				if _, ok := challenge.params[name]; ok {
+					return nil // each parameter name MUST only occur once
+				}
+				challenge.params[name] = value
+				a = remain
+				a = strings.TrimLeft(a, " \t") // Consume OWS
+				if a != "" && a[0] != ',' {    // Check for mandatory comma
+					return nil
+				}
+			}
+			challenges = append(challenges, challenge)
+			a = strings.TrimLeft(a, " \t") // Consume OWS
+			if a != "" && a[0] != ',' {    // Check for mandatory comma
+				return nil
+			}
+		}
+	}
+	return challenges
+}
+
+var charType [256]uint8
+
+const (
+	charTypeToken = 1 << iota
+	charTypeToken68
+	charTypeQdtext
+	charTypeQuotedPair
+)
+
+func init() {
+	for _, c := range "!#$%&'*+-.^_`|~" {
+		charType[c] |= charTypeToken
+	}
+	for _, c := range "-._~+/" {
+		charType[c] |= charTypeToken68
+	}
+	for c := range 256 {
+		if '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
+			charType[c] |= charTypeToken | charTypeToken68
+		}
+		if c == '\t' || c == ' ' || 0x21 <= c && c != 0x7f {
+			if c != '"' && c != '\\' {
+				charType[c] |= charTypeQdtext
+			}
+			charType[c] |= charTypeQuotedPair
+		}
+	}
+}
+
+func scanToken(s string) (token string, remain string) {
+	for i := range len(s) {
+		if charType[s[i]]&charTypeToken == 0 {
+			return s[:i], s[i:]
+		}
+	}
+	return s, ""
+}