From 44b7c2be1a4369fe78446459ff3439ea80f0138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 5 Feb 2026 23:49:30 +0100 Subject: [PATCH 1/7] feat: enhance MIME type handling and optimize string case conversion functions --- http.go | 17 +++++++++++++++++ strings.go | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/http.go b/http.go index db69c5d..72e5857 100644 --- a/http.go +++ b/http.go @@ -10,6 +10,12 @@ import ( ) const MIMEOctetStream = "application/octet-stream" +const ( + contentTypeApplicationJSON = "application/json" + contentTypeApplicationXML = "application/xml" + contentTypeApplicationFormURLEncoded = "application/x-www-form-urlencoded" + contentTypePrefixApplicationWithSlashLen = len("application/") +) // GetMIME returns the content-type of a file extension func GetMIME(extension string) string { @@ -74,6 +80,17 @@ func ParseVendorSpecificContentType(cType string, caseInsensitive ...bool) strin return cType } + if slashIndex+1 == contentTypePrefixApplicationWithSlashLen { + switch parsableType { + case "json": + return contentTypeApplicationJSON + case "xml": + return contentTypeApplicationXML + case "x-www-form-urlencoded": + return contentTypeApplicationFormURLEncoded + } + } + return working[:slashIndex+1] + parsableType } diff --git a/strings.go b/strings.go index ba56912..97b4e06 100644 --- a/strings.go +++ b/strings.go @@ -6,19 +6,28 @@ package utils // ToLower converts ascii string to lower-case func ToLower(b string) string { - if len(b) == 0 { + n := len(b) + if n == 0 { return b } - for i := 0; i < len(b); i++ { + table := toLowerTable + for i := 0; i < n; i++ { c := b[i] - low := toLowerTable[c] + low := table[c] if low != c { - res := make([]byte, len(b)) + res := make([]byte, n) copy(res, b[:i]) res[i] = low - for j := i + 1; j < len(b); j++ { - res[j] = toLowerTable[b[j]] + j := i + 1 + for ; j+3 < n; j += 4 { + res[j+0] = table[b[j+0]] + res[j+1] = table[b[j+1]] + res[j+2] = table[b[j+2]] + res[j+3] = table[b[j+3]] + } + for ; j < n; j++ { + res[j] = table[b[j]] } return UnsafeString(res) } @@ -28,19 +37,28 @@ func ToLower(b string) string { // ToUpper converts ascii string to upper-case func ToUpper(b string) string { - if len(b) == 0 { + n := len(b) + if n == 0 { return b } - for i := 0; i < len(b); i++ { + table := toUpperTable + for i := 0; i < n; i++ { c := b[i] - up := toUpperTable[c] + up := table[c] if up != c { - res := make([]byte, len(b)) + res := make([]byte, n) copy(res, b[:i]) res[i] = up - for j := i + 1; j < len(b); j++ { - res[j] = toUpperTable[b[j]] + j := i + 1 + for ; j+3 < n; j += 4 { + res[j+0] = table[b[j+0]] + res[j+1] = table[b[j+1]] + res[j+2] = table[b[j+2]] + res[j+3] = table[b[j+3]] + } + for ; j < n; j++ { + res[j] = table[b[j]] } return UnsafeString(res) } From 63592ef0d584b807576bafada32488c7fc3de535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 01:25:07 +0100 Subject: [PATCH 2/7] feat: improve secure token generation and enhance parsing functions for uint8 and int8 --- common.go | 57 +++++++++++++++++++++++- format.go | 46 +++++++++++-------- parse.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 199 insertions(+), 33 deletions(-) diff --git a/common.go b/common.go index 995b82b..6333b91 100644 --- a/common.go +++ b/common.go @@ -23,6 +23,11 @@ import ( // can override it to simulate failures. var randRead = rand.Read +const ( + defaultSecureTokenLength = 32 + maxFastTokenEncodedLength = 43 // base64.RawURLEncoding.EncodedLen(32) +) + const ( toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" @@ -60,8 +65,28 @@ func UUID() string { // Panics if the random source fails. func GenerateSecureToken(length int) string { if length <= 0 { - length = 32 + length = defaultSecureTokenLength } + + if length == defaultSecureTokenLength { + var randomBuf [defaultSecureTokenLength]byte + src := randomBuf[:length] + if _, err := randRead(src); err != nil { + // On Go 1.24+, crypto/rand.Read panics internally and never returns an error. + // On Go 1.23 and earlier, we panic for the same reasons: RNG failures indicate + // a broken system state (uninitialized entropy pool, misconfigured VM, etc.) + // that is almost certainly permanent rather than transient. + // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go + // https://go.dev/issue/66821 + panic(fmt.Errorf("utils: failed to read random bytes for token: %w", err)) + } + + var encoded [maxFastTokenEncodedLength]byte + encodedLen := base64.RawURLEncoding.EncodedLen(length) + base64.RawURLEncoding.Encode(encoded[:encodedLen], src) + return string(encoded[:encodedLen]) + } + bytes := make([]byte, length) if _, err := randRead(bytes); err != nil { // On Go 1.24+, crypto/rand.Read panics internally and never returns an error. @@ -78,7 +103,7 @@ func GenerateSecureToken(length int) string { // SecureToken generates a secure token with 32 bytes of entropy. // Panics if the random source fails. See GenerateSecureToken for details. func SecureToken() string { - return GenerateSecureToken(32) + return GenerateSecureToken(defaultSecureTokenLength) } // FunctionName returns function name @@ -124,6 +149,34 @@ func ConvertToBytes(humanReadableString string) int { return 0 } + // Fast path for plain byte values (e.g. "42", "42B", "42b"). + var sizeFast uint64 + maxInt := uint64(math.MaxInt) + i := 0 + for ; i < strLen; i++ { + c := humanReadableString[i] + if c < '0' || c > '9' { + break + } + d := uint64(c - '0') + if sizeFast > maxInt/10 || (sizeFast == maxInt/10 && d > maxInt%10) { + sizeFast = maxInt + } else if sizeFast < maxInt { + sizeFast = sizeFast*10 + d + } + } + if i > 0 { + if i == strLen { + return int(sizeFast) + } + if i+1 == strLen { + last := humanReadableString[i] + if last == 'b' || last == 'B' { + return int(sizeFast) + } + } + } + // Find the last digit position by scanning backwards // Also identify the unit prefix position in the same pass lastNumberPos := -1 diff --git a/format.go b/format.go index 1d427f2..3c182b9 100644 --- a/format.go +++ b/format.go @@ -6,6 +6,12 @@ var smallInts [100]string // smallNegInts contains precomputed string representations for small negative integers -1 to -99 var smallNegInts [100]string +// uint8Strs contains precomputed string representations for all uint8 values. +var uint8Strs [256]string + +// int8Strs contains precomputed string representations for all int8 values indexed by uint8(value). +var int8Strs [256]string + func init() { for i := range 100 { smallInts[i] = formatUintSmall(uint64(i)) @@ -13,6 +19,18 @@ func init() { smallNegInts[i] = "-" + smallInts[i] } } + + for i := range 256 { + v := uint8(i) + uint8Strs[i] = formatUint8Slow(v) + + sv := int8(i) + if sv >= 0 { + int8Strs[i] = uint8Strs[sv] + } else { + int8Strs[i] = "-" + uint8Strs[uint8(-sv)] + } + } } func formatUintSmall(n uint64) string { @@ -22,6 +40,13 @@ func formatUintSmall(n uint64) string { return string([]byte{byte(n/10) + '0', byte(n%10) + '0'}) } +func formatUint8Slow(n uint8) string { + if n < 100 { + return smallInts[n] + } + return string([]byte{n/100 + '0', (n/10)%10 + '0', n%10 + '0'}) +} + // formatUintBuf writes the digits of n into buf from the end and returns the start index. // buf must be at least 20 bytes. func formatUintBuf(buf *[20]byte, n uint64) int { @@ -156,29 +181,12 @@ func FormatInt16(n int16) string { // FormatUint8 formats a uint8 as a decimal string. func FormatUint8(n uint8) string { - if n < 100 { - return smallInts[n] - } - // uint8 max is 255, so max 3 digits - return string([]byte{n/100 + '0', (n/10)%10 + '0', n%10 + '0'}) + return uint8Strs[n] } // FormatInt8 formats an int8 as a decimal string. func FormatInt8(n int8) string { - if n >= 0 && n < 100 { - return smallInts[n] - } - if n < 0 && n > -100 { - return smallNegInts[-n] - } - // Only -128 to -100 and 100 to 127 reach here - if n >= 0 { - un := uint8(n) - return string([]byte{un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) - } - // n is -128 to -100 - un := uint8(-n) - return string([]byte{'-', un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) + return int8Strs[uint8(n)] } // AppendUint appends the decimal string representation of n to dst. diff --git a/parse.go b/parse.go index 51e9c37..d1cc03a 100644 --- a/parse.go +++ b/parse.go @@ -7,6 +7,26 @@ import ( const maxFracDigits = 16 +var fracScale = [...]float64{ + 1, + 0.1, + 0.01, + 0.001, + 0.0001, + 0.00001, + 0.000001, + 0.0000001, + 0.00000001, + 0.000000001, + 0.0000000001, + 0.00000000001, + 0.000000000001, + 0.0000000000001, + 0.00000000000001, + 0.000000000000001, + 0.0000000000000001, +} + type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } @@ -39,6 +59,47 @@ func ParseInt16[S byteSeq](s S) (int16, error) { // ParseInt8 parses a decimal ASCII string or byte slice into an int8. func ParseInt8[S byteSeq](s S) (int8, error) { + if len(s) == 0 { + return 0, &strconv.NumError{Func: "ParseInt8", Num: "", Err: strconv.ErrSyntax} + } + + neg := false + i := 0 + switch s[0] { + case '-': + neg = true + i++ + case '+': + i++ + } + if i == len(s) { + return 0, &strconv.NumError{Func: "ParseInt8", Num: string(s), Err: strconv.ErrSyntax} + } + + if len(s)-i <= 3 { + var n uint16 + for ; i < len(s); i++ { + c := s[i] - '0' + if c > 9 { + return 0, &strconv.NumError{Func: "ParseInt8", Num: string(s), Err: strconv.ErrSyntax} + } + n = n*10 + uint16(c) + } + if neg { + if n > 128 { + return 0, &strconv.NumError{Func: "ParseInt8", Num: string(s), Err: strconv.ErrRange} + } + if n == 128 { + return math.MinInt8, nil + } + return -int8(n), nil + } + if n > math.MaxInt8 { + return 0, &strconv.NumError{Func: "ParseInt8", Num: string(s), Err: strconv.ErrRange} + } + return int8(n), nil + } + return parseSigned[S, int8]("ParseInt8", s, math.MinInt8, math.MaxInt8) } @@ -54,6 +115,25 @@ func ParseUint16[S byteSeq](s S) (uint16, error) { // ParseUint8 parses a decimal ASCII string or byte slice into a uint8. func ParseUint8[S byteSeq](s S) (uint8, error) { + if len(s) == 0 { + return 0, &strconv.NumError{Func: "ParseUint8", Num: "", Err: strconv.ErrSyntax} + } + + if len(s) <= 3 { + var n uint16 + for i := range len(s) { + c := s[i] - '0' + if c > 9 { + return 0, &strconv.NumError{Func: "ParseUint8", Num: string(s), Err: strconv.ErrSyntax} + } + n = n*10 + uint16(c) + } + if n > math.MaxUint8 { + return 0, &strconv.NumError{Func: "ParseUint8", Num: string(s), Err: strconv.ErrRange} + } + return uint8(n), nil + } + return parseUnsigned[S, uint8]("ParseUint8", s, uint8(math.MaxUint8)) } @@ -95,10 +175,24 @@ func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} } - // Parse digits - n, err := parseDigits(s, i) - if err != nil { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} + digitsLen := len(s) - i + var ( + n uint64 + err error + ) + if digitsLen <= 19 { + for ; i < len(s); i++ { + c := s[i] - '0' + if c > 9 { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + } + n = n*10 + uint64(c) + } + } else { + n, err = parseDigits(s, i) + if err != nil { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} + } } if !neg { @@ -125,12 +219,25 @@ func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } - // Parse digits directly from index 0 - n, err := parseDigits(s, 0) - // Check for overflow - if err != nil { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} + var ( + n uint64 + err error + ) + if len(s) <= 19 { + for i := range len(s) { + c := s[i] - '0' + if c > 9 { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + } + n = n*10 + uint64(c) + } + } else { + n, err = parseDigits(s, 0) + if err != nil { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} + } } + // Check for overflow if n > uint64(maxRange) { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrRange} } @@ -172,7 +279,6 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { } var fracPart uint64 - var fracDiv uint64 = 1 var fracDigits int if i < len(s) && s[i] == '.' { i++ @@ -185,7 +291,6 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrRange} } fracPart = fracPart*10 + uint64(c) - fracDiv *= 10 fracDigits++ i++ } @@ -233,7 +338,7 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { f := float64(intPart) if fracPart > 0 { - f += float64(fracPart) / float64(fracDiv) + f += float64(fracPart) * fracScale[fracDigits] } if exp != 0 { f *= math.Pow10(int(exp)) From 0732d437635054421583cceb8192996339b9a8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 01:30:03 +0100 Subject: [PATCH 3/7] feat: optimize string iteration using range for improved performance --- strings.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings.go b/strings.go index 97b4e06..2994790 100644 --- a/strings.go +++ b/strings.go @@ -12,7 +12,7 @@ func ToLower(b string) string { } table := toLowerTable - for i := 0; i < n; i++ { + for i := range n { c := b[i] low := table[c] if low != c { @@ -43,7 +43,7 @@ func ToUpper(b string) string { } table := toUpperTable - for i := 0; i < n; i++ { + for i := range n { c := b[i] up := table[c] if up != c { From a16efc7432b5cadb7dda1e04127606f6c4dce49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 08:52:42 +0100 Subject: [PATCH 4/7] feat: optimize TrimSpace function to handle empty strings and improve performance --- byteseq.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/byteseq.go b/byteseq.go index 99ce831..521a03d 100644 --- a/byteseq.go +++ b/byteseq.go @@ -92,10 +92,13 @@ func TrimRight[S byteSeq](s S, cutset byte) S { // This is an optimized version that's faster than strings/bytes.TrimSpace for ASCII strings. // It removes the following ASCII whitespace characters: space, tab, newline, carriage return, vertical tab, and form feed. func TrimSpace[S byteSeq](s S) S { - i, j := 0, len(s)-1 + n := len(s) + if n == 0 { + return s + } - // fast path for empty string - if j < 0 { + i, j := 0, n-1 + if !whitespaceTable[s[i]] && !whitespaceTable[s[j]] { return s } From de1fc572947dab1dba020a51c63e9d3931d35361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 08:57:24 +0100 Subject: [PATCH 5/7] feat: optimize number parsing logic to prevent overflow and improve performance --- parse.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/parse.go b/parse.go index d1cc03a..7f20ed9 100644 --- a/parse.go +++ b/parse.go @@ -8,7 +8,6 @@ import ( const maxFracDigits = 16 var fracScale = [...]float64{ - 1, 0.1, 0.01, 0.001, @@ -146,11 +145,11 @@ func parseDigits[S byteSeq](s S, i int) (uint64, error) { if c > 9 { return 0, strconv.ErrSyntax } - nn := n*10 + uint64(c) - if nn < n { + d := uint64(c) + if n > math.MaxUint64/10 || (n == math.MaxUint64/10 && d > math.MaxUint64%10) { return 0, strconv.ErrRange } - n = nn + n = n*10 + d } return n, nil } @@ -270,11 +269,11 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { if c > 9 { break } - nn := intPart*10 + uint64(c) - if nn < intPart { + d := uint64(c) + if intPart > math.MaxUint64/10 || (intPart == math.MaxUint64/10 && d > math.MaxUint64%10) { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrRange} } - intPart = nn + intPart = intPart*10 + d i++ } @@ -338,7 +337,7 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { f := float64(intPart) if fracPart > 0 { - f += float64(fracPart) * fracScale[fracDigits] + f += float64(fracPart) * fracScale[fracDigits-1] } if exp != 0 { f *= math.Pow10(int(exp)) From 4365ba91701152964be8add142d0b74069b0f4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 10:26:45 +0100 Subject: [PATCH 6/7] feat: optimize integer parsing to prevent overflow and improve performance --- parse.go | 96 ++++++++++++++++++++++---------------------------------- 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/parse.go b/parse.go index 7f20ed9..6936303 100644 --- a/parse.go +++ b/parse.go @@ -7,25 +7,6 @@ import ( const maxFracDigits = 16 -var fracScale = [...]float64{ - 0.1, - 0.01, - 0.001, - 0.0001, - 0.00001, - 0.000001, - 0.0000001, - 0.00000001, - 0.000000001, - 0.0000000001, - 0.00000000001, - 0.000000000001, - 0.0000000000001, - 0.00000000000001, - 0.000000000000001, - 0.0000000000000001, -} - type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } @@ -43,6 +24,21 @@ func ParseUint[S byteSeq](s S) (uint64, error) { // ParseInt parses a decimal ASCII string or byte slice into an int64. // Returns the parsed value and nil on success, else 0 and an error. func ParseInt[S byteSeq](s S) (int64, error) { + if len(s) > 0 && s[0] != '-' && s[0] != '+' && len(s) <= 19 { + var n uint64 + for i := range len(s) { + c := s[i] - '0' + if c > 9 { + return 0, &strconv.NumError{Func: "ParseInt", Num: string(s), Err: strconv.ErrSyntax} + } + n = n*10 + uint64(c) + } + if n > uint64(math.MaxInt64) { + return 0, &strconv.NumError{Func: "ParseInt", Num: string(s), Err: strconv.ErrRange} + } + return int64(n), nil + } + return parseSigned[S, int64]("ParseInt", s, math.MinInt64, math.MaxInt64) } @@ -140,16 +136,23 @@ func ParseUint8[S byteSeq](s S) (uint8, error) { // It returns an error if any non-digit is encountered or overflow happens. func parseDigits[S byteSeq](s S, i int) (uint64, error) { var n uint64 + const ( + cutoff = math.MaxUint64 / 10 + cutlim = math.MaxUint64 % 10 + ) + digits := 0 for ; i < len(s); i++ { c := s[i] - '0' if c > 9 { return 0, strconv.ErrSyntax } d := uint64(c) - if n > math.MaxUint64/10 || (n == math.MaxUint64/10 && d > math.MaxUint64%10) { + // Any value with <= 19 digits is guaranteed to fit in uint64. + if digits >= 19 && (n > cutoff || (n == cutoff && d > cutlim)) { return 0, strconv.ErrRange } n = n*10 + d + digits++ } return n, nil } @@ -174,24 +177,10 @@ func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} } - digitsLen := len(s) - i - var ( - n uint64 - err error - ) - if digitsLen <= 19 { - for ; i < len(s); i++ { - c := s[i] - '0' - if c > 9 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} - } - n = n*10 + uint64(c) - } - } else { - n, err = parseDigits(s, i) - if err != nil { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} - } + // Parse digits. + n, err := parseDigits(s, i) + if err != nil { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} } if !neg { @@ -218,23 +207,10 @@ func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } - var ( - n uint64 - err error - ) - if len(s) <= 19 { - for i := range len(s) { - c := s[i] - '0' - if c > 9 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} - } - n = n*10 + uint64(c) - } - } else { - n, err = parseDigits(s, 0) - if err != nil { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} - } + // Parse digits directly from index 0. + n, err := parseDigits(s, 0) + if err != nil { + return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} } // Check for overflow if n > uint64(maxRange) { @@ -269,15 +245,16 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { if c > 9 { break } - d := uint64(c) - if intPart > math.MaxUint64/10 || (intPart == math.MaxUint64/10 && d > math.MaxUint64%10) { + nn := intPart*10 + uint64(c) + if nn < intPart { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrRange} } - intPart = intPart*10 + d + intPart = nn i++ } var fracPart uint64 + var fracDiv uint64 = 1 var fracDigits int if i < len(s) && s[i] == '.' { i++ @@ -290,6 +267,7 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrRange} } fracPart = fracPart*10 + uint64(c) + fracDiv *= 10 fracDigits++ i++ } @@ -337,7 +315,7 @@ func parseFloat[S byteSeq](fn string, s S) (float64, error) { f := float64(intPart) if fracPart > 0 { - f += float64(fracPart) * fracScale[fracDigits-1] + f += float64(fracPart) / float64(fracDiv) } if exp != 0 { f *= math.Pow10(int(exp)) From 3d4eff596bc01ff644e6f821cf18a6abde8443d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 6 Feb 2026 21:08:01 +0100 Subject: [PATCH 7/7] feat: refactor secure token generation to improve error handling and performance --- common.go | 36 +++++++++++++----------------------- common_test.go | 15 --------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/common.go b/common.go index 6333b91..cc569b0 100644 --- a/common.go +++ b/common.go @@ -19,15 +19,21 @@ import ( "github.com/google/uuid" ) -// randRead is a package-level indirection for crypto/rand.Read so tests -// can override it to simulate failures. -var randRead = rand.Read - const ( defaultSecureTokenLength = 32 maxFastTokenEncodedLength = 43 // base64.RawURLEncoding.EncodedLen(32) ) +func readRandomOrPanic(dst []byte) { + if _, err := rand.Read(dst); err != nil { + // On supported Go versions (1.24+), crypto/rand.Read panics internally and + // does not return errors. This check preserves explicit panic semantics if + // the behavior changes or an alternate implementation is used in the future. + // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go + panic(fmt.Errorf("utils: failed to read random bytes for token: %w", err)) + } +} + const ( toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" @@ -70,16 +76,8 @@ func GenerateSecureToken(length int) string { if length == defaultSecureTokenLength { var randomBuf [defaultSecureTokenLength]byte - src := randomBuf[:length] - if _, err := randRead(src); err != nil { - // On Go 1.24+, crypto/rand.Read panics internally and never returns an error. - // On Go 1.23 and earlier, we panic for the same reasons: RNG failures indicate - // a broken system state (uninitialized entropy pool, misconfigured VM, etc.) - // that is almost certainly permanent rather than transient. - // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go - // https://go.dev/issue/66821 - panic(fmt.Errorf("utils: failed to read random bytes for token: %w", err)) - } + src := randomBuf[:] + readRandomOrPanic(src) var encoded [maxFastTokenEncodedLength]byte encodedLen := base64.RawURLEncoding.EncodedLen(length) @@ -88,15 +86,7 @@ func GenerateSecureToken(length int) string { } bytes := make([]byte, length) - if _, err := randRead(bytes); err != nil { - // On Go 1.24+, crypto/rand.Read panics internally and never returns an error. - // On Go 1.23 and earlier, we panic for the same reasons: RNG failures indicate - // a broken system state (uninitialized entropy pool, misconfigured VM, etc.) - // that is almost certainly permanent rather than transient. - // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go - // https://go.dev/issue/66821 - panic(fmt.Errorf("utils: failed to read random bytes for token: %w", err)) - } + readRandomOrPanic(bytes) return base64.RawURLEncoding.EncodeToString(bytes) } diff --git a/common_test.go b/common_test.go index 73b481f..3b64d64 100644 --- a/common_test.go +++ b/common_test.go @@ -5,7 +5,6 @@ package utils import ( - "errors" "net" "os" "testing" @@ -116,20 +115,6 @@ func Test_GenerateSecureToken_Concurrency(t *testing.T) { require.Len(t, results, iterations) } -func Test_GenerateSecureToken_ErrorOnRandFail(t *testing.T) { - // Save and restore original randRead - orig := randRead - defer func() { randRead = orig }() - - // Simulate read failure - randRead = func(_ []byte) (int, error) { - return 0, errors.New("simulated failure") - } - - // Should panic on failure - require.Panics(t, func() { GenerateSecureToken(16) }) -} - func Test_SecureToken(t *testing.T) { t.Parallel() token := SecureToken()