Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 43 additions & 33 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}$)`
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
}
Expand All @@ -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, "")
Expand Down Expand Up @@ -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
}
62 changes: 61 additions & 1 deletion default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "\"")
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package strfmt

import (
"encoding/base64"
"encoding/binary"
"fmt"
"time"
Expand Down Expand Up @@ -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
}
Expand Down
Loading