@@ -24,6 +24,7 @@ import (
2424
2525 "github.com/gofiber/utils/v2"
2626 utilsbytes "github.com/gofiber/utils/v2/bytes"
27+ "github.com/gofiber/utils/v2/swar"
2728
2829 "github.com/gofiber/fiber/v3/internal/contextvalue"
2930 "github.com/gofiber/fiber/v3/log"
@@ -165,28 +166,65 @@ func readContent(rf io.ReaderFrom, name string) (int64, error) {
165166 return n , nil
166167}
167168
168- // quoteRawString escapes only characters that need quoting according to
169- // https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4 so the result may
170- // contain non-ASCII bytes.
171- func (* App ) quoteRawString (raw string ) string {
172- // Fast path: most values need no escaping at all; avoid the pooled
173- // buffer and the string allocation entirely.
174- needsEscaping := false
175- for i := 0 ; i < len (raw ); i ++ {
169+ // quoteEscapeMask marks the lanes of w holding bytes quoteRawString must
170+ // escape: '\\', '"', any C0 control (including HTAB), or DEL. Lanes >= 0x80
171+ // are never marked; non-ASCII bytes pass through verbatim. This is
172+ // utils.IndexNonQuotable's RFC 9110 set widened by HTAB, which the RFC
173+ // permits as qdtext but this function has always percent-encoded.
174+ func quoteEscapeMask (w uint64 ) uint64 {
175+ return swar .MatchByteMask (w , '\\' ) | swar .MatchByteMask (w , '"' ) |
176+ swar .MatchRangeMask (w , 0x00 , 0x1f ) | swar .MatchByteMask (w , 0x7f )
177+ }
178+
179+ // indexQuoteEscape returns the index of the first byte quoteEscapeMask
180+ // matches, or -1 if raw needs no escaping. It scans eight bytes at a time,
181+ // finishing inputs of 8+ bytes with one overlapping word; shorter inputs
182+ // are checked byte-wise.
183+ func indexQuoteEscape (raw string ) int {
184+ n := len (raw )
185+ i := 0
186+ for ; i + swar .WordLen <= n ; i += swar .WordLen {
187+ if m := quoteEscapeMask (swar .Load8 (raw , i )); m != 0 {
188+ return i + swar .FirstLane (m )
189+ }
190+ }
191+ if i == n {
192+ return - 1
193+ }
194+ if n >= swar .WordLen {
195+ if m := quoteEscapeMask (swar .Load8 (raw , n - swar .WordLen )); m != 0 {
196+ return n - swar .WordLen + swar .FirstLane (m )
197+ }
198+ return - 1
199+ }
200+ for ; i < n ; i ++ {
176201 if c := raw [i ]; c == '\\' || c == '"' || c < 0x20 || c == 0x7f {
177- needsEscaping = true
178- break
202+ return i
179203 }
180204 }
181- if ! needsEscaping {
205+ return - 1
206+ }
207+
208+ // quoteRawString escapes the characters that need quoting inside an RFC 9110
209+ // quoted-string (https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4), plus
210+ // HTAB, which the RFC permits as qdtext but this function has always
211+ // percent-encoded. The result may contain non-ASCII bytes.
212+ func (* App ) quoteRawString (raw string ) string {
213+ // Fast path: most values need no escaping at all; avoid the pooled
214+ // buffer and the string allocation entirely.
215+ end := indexQuoteEscape (raw )
216+ if end == - 1 {
182217 return raw
183218 }
184219
185220 const hex = "0123456789ABCDEF"
186221 bb := bytebufferpool .Get ()
187222 defer bytebufferpool .Put (bb )
188223
189- for i := 0 ; i < len (raw ); i ++ {
224+ // Every byte before end is quotable and tab-free, so it hits the
225+ // verbatim case of the switch below; copy it in one append.
226+ bb .B = append (bb .B , raw [:end ]... )
227+ for i := end ; i < len (raw ); i ++ {
190228 c := raw [i ]
191229 switch {
192230 case c == '\\' || c == '"' :
@@ -212,15 +250,36 @@ func (*App) quoteRawString(raw string) string {
212250 return string (bb .B )
213251}
214252
215- // isASCII reports whether the provided string contains only ASCII characters.
216- // See: https://www.rfc-editor.org/rfc/rfc0020
217- func (* App ) isASCII (s string ) bool {
218- for i := 0 ; i < len (s ); i ++ {
219- if s [i ] > 127 {
220- return false
253+ // appendLowerASCII writes the ASCII-lowercased bytes of src into dst[:0],
254+ // growing dst as needed, in a single pass over src (instead of a copy
255+ // followed by an in-place case fold). Bytes outside 'A'..'Z', including
256+ // non-ASCII, are copied unchanged. src and dst must not overlap.
257+ func appendLowerASCII (dst , src []byte ) []byte {
258+ n := len (src )
259+ // Amortized growth like append: every byte of dst[:n] is overwritten
260+ // below, so the grown slice's contents don't matter.
261+ dst = slices .Grow (dst [:0 ], n )[:n ]
262+ i := 0
263+ for ; i + swar .WordLen <= n ; i += swar .WordLen {
264+ swar .Store8 (dst , i , swar .ToLowerWord (swar .Load8 (src , i )))
265+ }
266+ if i == n {
267+ return dst
268+ }
269+ if n >= swar .WordLen {
270+ // Finish with one overlapping word; the overlapped bytes are
271+ // rewritten with the same values.
272+ swar .Store8 (dst , n - swar .WordLen , swar .ToLowerWord (swar .Load8 (src , n - swar .WordLen )))
273+ return dst
274+ }
275+ for ; i < n ; i ++ {
276+ c := src [i ]
277+ if c - 'A' <= 'Z' - 'A' {
278+ c |= 0x20
221279 }
280+ dst [i ] = c
222281 }
223- return true
282+ return dst
224283}
225284
226285// defaultString returns the value or a default value if it is set
@@ -611,27 +670,33 @@ func forEachMediaRange(header []byte, functor func([]byte)) {
611670 n := 0
612671 header = utils .TrimLeft (header , ' ' )
613672 quotes := 0
614- escaping := false
615673
616674 if hasDQuote {
617675 // Complex case. We need to keep track of quotes and quoted-pairs (i.e., characters escaped with \ )
676+ // Only ',', '"' and '\\' can change state, so jump between them
677+ // instead of visiting every byte.
618678 loop:
619679 for n < len (header ) {
680+ i := utils .IndexAny3 (header [n :], ',' , '"' , '\\' )
681+ if i == - 1 {
682+ n = len (header )
683+ break
684+ }
685+ n += i
620686 switch header [n ] {
621687 case ',' :
622688 if quotes % 2 == 0 {
623689 break loop
624690 }
625691 case '"' :
626- if ! escaping {
627- quotes ++
628- }
629- case '\\' :
630- if quotes % 2 == 1 {
631- escaping = ! escaping
692+ quotes ++
693+ default : // '\\'
694+ if quotes % 2 == 1 && n + 1 < len (header ) {
695+ // A quoted-pair escapes exactly the next byte
696+ // (RFC 9110 §5.6.4); consume it so an escaped
697+ // quote is not mistaken for a closing quote.
698+ n ++
632699 }
633- default :
634- // all other characters are ignored
635700 }
636701 n ++
637702 }
@@ -904,22 +969,25 @@ func (app *App) isEtagStale(etag string, noneMatchBytes []byte) bool {
904969
905970 // Split the header on commas that sit outside DQUOTE-delimited opaque-tags:
906971 // etagc permits "," inside the quoted tag (RFC 9110 §8.8.3), so `"v1,v2"`
907- // is a single entity tag, not two list elements.
972+ // is a single entity tag, not two list elements. Only '"' and ','
973+ // affect the split, so jump between them instead of visiting every byte.
908974 start := 0
975+ pos := 0
909976 inQuotes := false
910- for i := range len (header ) {
911- switch header [i ] {
912- case '"' :
977+ for {
978+ i := utils .IndexAny2 (header [pos :], '"' , ',' )
979+ if i == - 1 {
980+ break
981+ }
982+ i += pos
983+ pos = i + 1
984+ if header [i ] == '"' {
913985 inQuotes = ! inQuotes
914- case ',' :
915- if ! inQuotes {
916- if matchEtag (utils .TrimSpace (header [start :i ]), etag ) {
917- return false
918- }
919- start = i + 1
986+ } else if ! inQuotes {
987+ if matchEtag (utils .TrimSpace (header [start :i ]), etag ) {
988+ return false
920989 }
921- default :
922- // any other byte belongs to the current list element
990+ start = i + 1
923991 }
924992 }
925993
0 commit comments