Skip to content
Merged

Bugs #113

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
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ require (
)

require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
go.uber.org/ratelimit v0.3.1
golang.org/x/sys v0.42.0 // indirect
google.golang.org/protobuf v1.36.11
)
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down Expand Up @@ -42,14 +40,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
Expand Down
59 changes: 58 additions & 1 deletion pkg/codec/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func DecodeBase64(encoded string) ([]byte, error) {
// 0–9, the next 26 letters ('A'–'Z') as values 10–35, and the final 26 letters
// ('a'–'z') as values 36–61.
//
// Leading '0' characters are significant: like base58's leading '1's, each
// leading '0' in the input represents one leading zero byte in the output.
// This preserves binary payloads (e.g. protobuf) whose first bytes are 0x00,
// which a plain big-integer round trip would silently drop.
//
// An error is returned if the input string contains invalid characters.
//
// Example usage:
Expand Down Expand Up @@ -66,7 +71,59 @@ func DecodeBase62(s string) ([]byte, error) {
result.Add(&result, big.NewInt(int64(val)))
}

// Convert big.Int to a byte slice.
// Count leading '0' characters: each one encodes a leading zero byte that
// the big.Int representation cannot carry.
leadingZeros := 0
for _, c := range s {
if c != '0' {
break
}
leadingZeros++
}

decoded := result.Bytes()
if leadingZeros > 0 {
withZeros := make([]byte, leadingZeros+len(decoded))
copy(withZeros[leadingZeros:], decoded)
decoded = withZeros
}
return decoded, nil
}

// EncodeBase62 encodes bytes to a base62 string using the same alphabet as
// DecodeBase62 ([0-9A-Za-z]). Leading zero bytes are encoded as leading '0'
// characters so that DecodeBase62(EncodeBase62(b)) round-trips exactly.
func EncodeBase62(data []byte) string {
if len(data) == 0 {
return ""
}

leadingZeros := 0
for _, b := range data {
if b != 0 {
break
}
leadingZeros++
}

const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var num big.Int
num.SetBytes(data)

var digits []byte
base := big.NewInt(62)
mod := new(big.Int)
for num.Sign() > 0 {
num.DivMod(&num, base, mod)
digits = append(digits, alphabet[mod.Int64()])
}

encoded := make([]byte, leadingZeros+len(digits))
for i := range leadingZeros {
encoded[i] = '0'
}
for i, d := range digits {
encoded[leadingZeros+len(digits)-1-i] = d
}
return string(encoded)
}
36 changes: 36 additions & 0 deletions pkg/codec/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,39 @@ func TestDecodeBase62(t *testing.T) {
})
}
}

// TestBase62RoundTrip verifies EncodeBase62/DecodeBase62 round-trip exactly,
// including binary payloads with leading zero bytes (regression for the
// big.Int round trip silently dropping leading 0x00 bytes).
func TestBase62RoundTrip(t *testing.T) {
testCases := []struct {
name string
data []byte
}{
{"simple ascii", []byte("Hello World")},
{"single zero byte", []byte{0x00}},
{"leading zero bytes", []byte{0x00, 0x00, 0x01, 0x02}},
{"all zero bytes", []byte{0x00, 0x00, 0x00}},
{"binary with embedded zeros", []byte{0x0a, 0x00, 0xff, 0x00}},
{"empty", []byte{}},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
encoded := EncodeBase62(tc.data)
decoded, err := DecodeBase62(encoded)
if err != nil {
t.Fatalf("DecodeBase62(%q) failed: %v", encoded, err)
}
if len(decoded) != len(tc.data) {
t.Fatalf("round trip changed length: %d -> %d (encoded %q, decoded %x, original %x)",
len(tc.data), len(decoded), encoded, decoded, tc.data)
}
for i := range decoded {
if decoded[i] != tc.data[i] {
t.Fatalf("round trip mismatch at byte %d: got %x, want %x", i, decoded, tc.data)
}
}
})
}
}
9 changes: 7 additions & 2 deletions pkg/common/middleware_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ func NewMiddlewareChain(middlewares ...Middleware) MiddlewareChain {
return middlewares
}

// Append adds middleware to the end of the chain
// Append returns a new chain with the given middleware added to the end.
// The original chain is never modified, and the result has its own backing
// array, so multiple Appends on the same parent chain are safe.
func (c MiddlewareChain) Append(middlewares ...Middleware) MiddlewareChain {
return append(c, middlewares...)
result := make(MiddlewareChain, 0, len(c)+len(middlewares))
result = append(result, c...)
result = append(result, middlewares...)
return result
}

// Prepend adds middleware to the beginning of the chain
Expand Down
36 changes: 36 additions & 0 deletions pkg/common/middleware_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,39 @@ func TestEmptyMiddlewareChain(t *testing.T) {
t.Errorf("Expected body %q, got %q", "OK", w.Body.String())
}
}

// TestMiddlewareChainAppendDoesNotAliasParent verifies that two Appends on the
// same parent chain don't share a backing array (regression: append(c, ...)
// could let the second Append clobber the first chain's middleware).
func TestMiddlewareChainAppendDoesNotAliasParent(t *testing.T) {
record := func(tag string, log *[]string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*log = append(*log, tag)
next.ServeHTTP(w, r)
})
}
}

var log []string
// Build a parent with spare capacity so naive append would share arrays.
parent := make(MiddlewareChain, 0, 4)
parent = parent.Append(record("base", &log))

chainA := parent.Append(record("a", &log))
chainB := parent.Append(record("b", &log)) // must not overwrite chainA's "a"

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})

log = nil
chainA.Then(handler).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
if len(log) != 2 || log[0] != "base" || log[1] != "a" {
t.Fatalf("chainA executed %v, want [base a]", log)
}

log = nil
chainB.Then(handler).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
if len(log) != 2 || log[0] != "base" || log[1] != "b" {
t.Fatalf("chainB executed %v, want [base b]", log)
}
}
4 changes: 3 additions & 1 deletion pkg/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ type RateLimitConfig[T comparable, U any] struct {
UserIDFromUser func(user U) T

// UserIDToString converts the user ID (type T) to a string for use as a map key.
// Required only when Strategy is StrategyUser.
// Used only when Strategy is StrategyUser. Optional: if nil, a default
// conversion is used (handles string, int, int64, fmt.Stringer, and falls
// back to fmt.Sprint).
UserIDToString func(userID T) string

// KeyExtractor provides a custom function to generate the rate limit key from the request.
Expand Down
Loading
Loading