diff --git a/default.go b/default.go index 725cc0e..f406b41 100644 --- a/default.go +++ b/default.go @@ -32,22 +32,22 @@ const ( ) const ( - // UUIDPattern Regex for [UUID] that allows uppercase + // UUIDPattern Regex for [UUID] that allows uppercase. // // Deprecated: [strfmt] no longer uses regular expressions to validate UUIDs. UUIDPattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{32}$)` - // UUID3Pattern Regex for [UUID3] that allows uppercase + // UUID3Pattern Regex for [UUID3] that allows uppercase. // // Deprecated: [strfmt] no longer uses regular expressions to validate UUIDs. UUID3Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{12}3[0-9a-f]{3}?[0-9a-f]{16}$)` - // UUID4Pattern Regex for [UUID4] that allows uppercase + // UUID4Pattern Regex for [UUID4] that allows uppercase. // // Deprecated: [strfmt] no longer uses regular expressions to validate UUIDs. UUID4Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$)` - // UUID5Pattern Regex for [UUID]5 that allows uppercase + // UUID5Pattern Regex for [UUID]5 that allows uppercase. // // Deprecated: [strfmt] no longer uses regular expressions to validate UUIDs. UUID5Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}5[0-9a-f]{3}[89ab][0-9a-f]{15}$)` @@ -88,8 +88,8 @@ var ( // IsHostname returns true when the string is a valid hostname. // -// It follows the rules detailed at https://url.spec.whatwg.org/#concept-host-parser -// and implemented by most modern web browsers. +// It follows the rules detailed at https://url.spec.whatwg.org/#concept-host-parser and implemented by most modern web +// browsers. // // It supports IDNA rules regarding internationalized names with unicode. // @@ -142,13 +142,13 @@ func IsHostname(str string) bool { return true } -// domainEndsAsNumber determines if a domain name ends with a decimal, octal or hex digit, -// accounting for a possible trailing dot (the last part being empty in that case). +// domainEndsAsNumber determines if a domain name ends with a decimal, octal or hex digit, accounting for a possible +// trailing dot (the last part being empty in that case). // // It returns the last non-trailing dot part and if that part consists only of (dec/hex/oct) digits. func domainEndsAsNumber(parts []string) (lastPart string, lastIndex int, ok bool) { - // NOTE: using ParseUint(x, 0, 32) is not an option, as the IPv4 format supported why WHATWG - // doesn't support notations such as "0b1001" (binary digits) or "0o666" (alternate notation for octal digits). + // NOTE: using ParseUint(x, 0, 32) is not an option, as the IPv4 format supported why WHATWG doesn't support notations + // such as "0b1001" (binary digits) or "0o666" (alternate notation for octal digits). lastIndex = len(parts) - 1 lastPart = parts[lastIndex] if len(lastPart) == 0 { @@ -233,8 +233,9 @@ func isValidIPv6(str string) bool { // "0o07.2.3.4" func isValidIPv4(parts []string) bool { // NOTE: using ParseUint(x, 0, 32) is not an option, even though it would simplify this code a lot. - // The IPv4 format supported why WHATWG doesn't support notations such as "0b1001" (binary digits) - // or "0o666" (alternate notation for octal digits). + // + // The IPv4 format supported why WHATWG doesn't support notations such as "0b1001" (binary digits) or "0o666" + // (alternate notation for octal digits). const ( maxPartsInIPv4 = 4 maxDigitsInPart = 11 // max size of a 4-bytes hex or octal digit @@ -463,14 +464,22 @@ func init() { //nolint:gochecknoinits // registers all default string formats in Default.Add("password", &pw, func(_ string) bool { return true }) } -// Base64 represents a base64 encoded string, using URLEncoding alphabet. +// base64Encoding is the canonical alphabet for the [Base64] format. +// +// OpenAPI `format: byte` means standard base64 (RFC 4648 §4, the `+/` alphabet), not base64url. +// This is the single seam every [Base64] path encodes and decodes through, so a future URL-safe variant only swaps the +// encoding here. +// See go-openapi/strfmt#87. +var base64Encoding = base64.StdEncoding //nolint:gochecknoglobals // canonical alphabet seam for the Base64 format + +// Base64 represents a base64 encoded string, using the standard RFC 4648 alphabet. // // swagger:strfmt byte. type Base64 []byte // MarshalText turns this instance into text. func (b Base64) MarshalText() ([]byte, error) { - enc := base64.URLEncoding + enc := base64Encoding src := []byte(b) buf := make([]byte, enc.EncodedLen(len(src))) enc.Encode(buf, src) @@ -479,7 +488,7 @@ func (b Base64) MarshalText() ([]byte, error) { // UnmarshalText hydrates this instance from text. func (b *Base64) UnmarshalText(data []byte) error { // validation is performed later on - enc := base64.URLEncoding + enc := base64Encoding dbuf := make([]byte, enc.DecodedLen(len(data))) n, err := enc.Decode(dbuf, data) @@ -495,14 +504,14 @@ func (b *Base64) UnmarshalText(data []byte) error { // validation is performed l func (b *Base64) Scan(raw any) error { switch v := raw.(type) { case []byte: - dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(v))) - n, err := base64.StdEncoding.Decode(dbuf, v) + dbuf := make([]byte, base64Encoding.DecodedLen(len(v))) + n, err := base64Encoding.Decode(dbuf, v) if err != nil { return err } *b = dbuf[:n] case string: - vv, err := base64.StdEncoding.DecodeString(v) + vv, err := base64Encoding.DecodeString(v) if err != nil { return err } @@ -520,7 +529,7 @@ func (b Base64) Value() (driver.Value, error) { } func (b Base64) String() string { - return base64.StdEncoding.EncodeToString([]byte(b)) + return base64Encoding.EncodeToString([]byte(b)) } // MarshalJSON returns the Base64 as JSON. @@ -534,7 +543,7 @@ func (b *Base64) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &b64str); err != nil { return err } - vb, err := base64.StdEncoding.DecodeString(b64str) + vb, err := base64Encoding.DecodeString(b64str) if err != nil { return err } @@ -1040,7 +1049,7 @@ func (u *MAC) DeepCopy() *MAC { return out } -// UUID represents a [uuid] string format +// UUID represents a [uuid] string format. // // swagger:strfmt uuid. type UUID string @@ -1905,6 +1914,7 @@ func (r *RGBColor) DeepCopy() *RGBColor { } // Password represents a password. +// // This has no validations and is mainly used as a marker for UI components. // // swagger:strfmt password. @@ -1978,11 +1988,10 @@ func (r *Password) DeepCopy() *Password { } func isRequestURI(rawurl string) bool { - // url.ParseRequestURI assumes the input contains no "#fragment" - // (RFC 3986 §3.5). A URI with a fragment and an empty path, such as - // "https://host#@frag", is therefore misread as userinfo and rejected - // as "invalid userinfo". Strip the fragment first so the absolute - // request URI validates, matching url.Parse's RFC 3986 handling. + // url.ParseRequestURI assumes the input contains no "#fragment" (RFC 3986 §3.5). + // A URI with a fragment and an empty path, such as "https://host#@frag", is therefore misread as userinfo and rejected + // as "invalid userinfo". + // Strip the fragment first so the absolute request URI validates, matching url.Parse's RFC 3986 handling. if i := strings.IndexByte(rawurl, '#'); i >= 0 { rawurl = rawurl[:i] } @@ -2009,19 +2018,20 @@ func isCIDR(str string) bool { } // isMAC checks if a string is valid MAC address. +// // Possible MAC formats: -// 01:23:45:67:89:ab -// 01:23:45:67:89:ab:cd:ef -// 01-23-45-67-89-ab -// 01-23-45-67-89-ab-cd-ef -// 0123.4567.89ab -// 0123.4567.89ab.cdef. +// - 01:23:45:67:89:ab +// - 01:23:45:67:89:ab:cd:ef +// - 01-23-45-67-89-ab +// - 01-23-45-67-89-ab-cd-ef +// - 0123.4567.89ab 0123.4567.89ab.cdef func isMAC(str string) bool { _, err := net.ParseMAC(str) return err == nil } // isISBN checks if the string is an ISBN (version 10 or 13). +// // If version value is not equal to 10 or 13, it will be checks both variants. func isISBN(str string, version int) bool { sanitized := whiteSpacesAndMinus.ReplaceAllString(str, "") @@ -2120,7 +2130,7 @@ func isRGBcolor(str string) bool { // isBase64 checks if a string is base64 encoded. func isBase64(str string) bool { - _, err := base64.StdEncoding.DecodeString(str) + _, err := base64Encoding.DecodeString(str) return err == nil } diff --git a/default_test.go b/default_test.go index b0fb40e..dd2768b 100644 --- a/default_test.go +++ b/default_test.go @@ -508,7 +508,7 @@ func TestFormatPassword(t *testing.T) { func TestFormatBase64(t *testing.T) { const b64 string = "This is a byte array with unprintable chars, but it also isn" - str := base64.URLEncoding.EncodeToString([]byte(b64)) + str := base64.StdEncoding.EncodeToString([]byte(b64)) b := []byte(b64) expected := Base64(b) bj := []byte("\"" + str + "\"") @@ -554,6 +554,66 @@ func TestFormatBase64(t *testing.T) { require.Error(t, err) } +// TestBase64StandardAlphabet locks issue #87: every Base64 serialization path uses the +// standard RFC 4648 alphabet (+/), so a single value round-trips identically across text, +// JSON, SQL, and BSON. The payload {0xFF, 0xFF} is chosen because it lands on alphabet +// indices 62/63, the only place where standard base64 ("//8=") and base64url ("__8=") +// disagree — so this test genuinely proves the alphabet, not merely self-consistency. +func TestBase64StandardAlphabet(t *testing.T) { + raw := []byte{0xFF, 0xFF} + const std = "//8=" // standard base64 of {0xFF, 0xFF} + const url = "__8=" // base64url of the same bytes + value := Base64(raw) + + t.Run("every encode path emits standard base64", func(t *testing.T) { + txt, err := value.MarshalText() + require.NoError(t, err) + assert.Equal(t, std, string(txt)) + + assert.Equal(t, std, value.String()) + + js, err := value.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, `"`+std+`"`, string(js)) + + sqlv, err := value.Value() + require.NoError(t, err) + assert.Equal(t, std, sqlv) + }) + + t.Run("every decode path accepts standard base64", func(t *testing.T) { + var fromText Base64 + require.NoError(t, fromText.UnmarshalText([]byte(std))) + assert.Equal(t, value, fromText) + + var fromJSON Base64 + require.NoError(t, fromJSON.UnmarshalJSON([]byte(`"`+std+`"`))) + assert.Equal(t, value, fromJSON) + + var fromScanStr Base64 + require.NoError(t, fromScanStr.Scan(std)) + assert.Equal(t, value, fromScanStr) + + var fromScanBytes Base64 + require.NoError(t, fromScanBytes.Scan([]byte(std))) + assert.Equal(t, value, fromScanBytes) + }) + + t.Run("bson round-trips via standard alphabet", func(t *testing.T) { + doc, err := value.MarshalBSON() + require.NoError(t, err) + var back Base64 + require.NoError(t, back.UnmarshalBSON(doc)) + assert.Equal(t, value, back) + }) + + t.Run("text path no longer accepts base64url", func(t *testing.T) { + var b Base64 + require.Error(t, b.UnmarshalText([]byte(url)), + "base64url must be rejected by the standard-alphabet text path") + }) +} + type testableFormat interface { encoding.TextMarshaler encoding.TextUnmarshaler diff --git a/mongo.go b/mongo.go index be904ff..b518298 100644 --- a/mongo.go +++ b/mongo.go @@ -4,7 +4,6 @@ package strfmt import ( - "encoding/base64" "encoding/binary" "fmt" "time" @@ -140,7 +139,7 @@ func (b *Base64) UnmarshalBSON(data []byte) error { return fmt.Errorf("couldn't unmarshal bson bytes as base64: %w", ErrFormat) } - vb, err := base64.StdEncoding.DecodeString(s) + vb, err := base64Encoding.DecodeString(s) if err != nil { return err }