mirror of
https://github.com/soypat/lneto.git
synced 2026-08-11 18:33:46 +00:00
add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice
This commit is contained in:
@@ -34,6 +34,7 @@ type Exchange struct {
|
||||
respHeaderOff uint16
|
||||
respHeaderLen uint16
|
||||
reqHdr httpraw.Header
|
||||
pathValues []pathValue
|
||||
|
||||
hijacked bool
|
||||
rw conn
|
||||
@@ -54,6 +55,9 @@ type ExchangeConfig struct {
|
||||
NumHeaderKVCap int
|
||||
NormalizeOutgoingKeys bool
|
||||
NoRequestBufferGrowth bool
|
||||
// MaxPathValues is how many wildcards a single pattern may bind, read back with
|
||||
// [Exchange.PathValue]. A pattern with more never matches, see [SetPathValues].
|
||||
MaxPathValues int
|
||||
}
|
||||
|
||||
// HijackRaw is a low-level implementation of http.Hijacker interface.
|
||||
@@ -99,6 +103,27 @@ func (exch *Exchange) Configure(cfg ExchangeConfig) {
|
||||
exch.reqHdr.Reset(cfg.RawBuf[:0:cfg.RequestBufferLim], cfg.NumHeaderKVCap)
|
||||
exch.reqHdr.ConfigBufferGrowth(!cfg.NoRequestBufferGrowth)
|
||||
exch.normalizeKeys = cfg.NormalizeOutgoingKeys
|
||||
internal.SliceReuse(&exch.pathValues, cfg.MaxPathValues)
|
||||
exch.pathValues = exch.pathValues[:cfg.MaxPathValues]
|
||||
}
|
||||
|
||||
// PathValue returns the segment the request path bound to the wildcard named
|
||||
// key, or nil if the matched pattern has no such wildcard. It plays the part of
|
||||
// http.Request.PathValue. See [SetPathValues] for the pattern syntax and for
|
||||
// which segments a wildcard binds.
|
||||
//
|
||||
// sm.Handle("GET /users/{id}", func(exch *httphi.Exchange) {
|
||||
// id := exch.PathValue("id") // "42" on a GET /users/42.
|
||||
// })
|
||||
func (exch *Exchange) PathValue(key string) []byte {
|
||||
for i := range exch.pathValues {
|
||||
if exch.pathValues[i].Key == key {
|
||||
return exch.pathValues[i].Value
|
||||
} else if exch.pathValues[i].Key == "" {
|
||||
break // No more keys set.
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Acquire claims the exchange for conn and resets it to serve a new request,
|
||||
@@ -120,6 +145,7 @@ func (exch *Exchange) Acquire(conn conn) bool {
|
||||
exch.rw = conn
|
||||
exch.headerWritten = false
|
||||
exch.nextFree = nil
|
||||
clear(exch.pathValues)
|
||||
exch.reqHdr.Reset(nil, 0)
|
||||
return true
|
||||
}
|
||||
|
||||
+104
-8
@@ -1,6 +1,7 @@
|
||||
package httphi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"unsafe"
|
||||
@@ -65,7 +66,7 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
||||
// Mux on the request path: the query string is the handler's business.
|
||||
path := reqhdr.RequestPath()
|
||||
meth := reqhdr.Method()
|
||||
matchedPattern, handler := mux.LookupHandler(MethodFromBytes(meth), b2s(path))
|
||||
matchedPattern, handler := mux.LookupHandler(MethodFromBytes(meth), path, exch.pathValues)
|
||||
if handler != nil {
|
||||
exch.matchedPattern = matchedPattern
|
||||
handler(exch)
|
||||
@@ -98,8 +99,9 @@ type HandlerFunc func(ex *Exchange)
|
||||
// replies 404 when it returns nil.
|
||||
type Mux interface {
|
||||
// LookupHandler matches the requestPath and method to a handler and returns it and the
|
||||
// pattern it matched.
|
||||
LookupHandler(get Method, requestPath string) (matchedPattern string, handler HandlerFunc)
|
||||
// pattern it matched. dstPathVals are set to non-zero values by Mux and can later be accessed by [Exchange.PathValue]
|
||||
// requestPath is a buffer owned by the [Exchange] usually and should not be held after LookupHandler returns.
|
||||
LookupHandler(get Method, requestPath []byte, dstPathVals []pathValue) (matchedPattern string, handler HandlerFunc)
|
||||
}
|
||||
|
||||
// MuxSlice is a [Mux] backed by a slice of registered endpoints, matched by
|
||||
@@ -107,12 +109,102 @@ type Mux interface {
|
||||
type MuxSlice struct {
|
||||
// TODO: binary search worth it?
|
||||
_handlers []struct {
|
||||
method Method
|
||||
path string
|
||||
handler HandlerFunc
|
||||
method Method
|
||||
path string
|
||||
handler HandlerFunc
|
||||
setPathVal bool
|
||||
}
|
||||
}
|
||||
|
||||
type pathValue struct {
|
||||
Key string // owned by mux.
|
||||
Value []byte // points to raw exchange buffer.
|
||||
}
|
||||
|
||||
// pathSeparator is shared so [SetPathValues] never converts a literal per call.
|
||||
var pathSeparator = []byte{'/'}
|
||||
|
||||
// SetPathValues matches requestPath against pattern and binds its wildcards
|
||||
// into dstPathVals, read back with [Exchange.PathValue]. Wildcards are whole
|
||||
// segments as per http.ServeMux: "{name}" takes one non-empty segment,
|
||||
// "{name...}" the rest including slashes, "{$}" only the path's end, and a
|
||||
// trailing slash is an anonymous "{...}". i.e: "/b/{bucket}/o/{obj...}".
|
||||
//
|
||||
// Unlike ServeMux, segments are compared and bound raw, so "/users/{id}" binds
|
||||
// "x%2Fy" and not "x/y". Which paths match is unaffected. Bound values alias
|
||||
// requestPath rather than copy it.
|
||||
func SetPathValues(dstPathVals []pathValue, pattern string, requestPath []byte) (matched, pathValSliceTooShort bool) {
|
||||
if len(pattern) == 0 || pattern[0] != '/' || len(requestPath) == 0 || requestPath[0] != '/' {
|
||||
return false, false
|
||||
}
|
||||
pattern, requestPath = pattern[1:], requestPath[1:]
|
||||
n := 0
|
||||
for {
|
||||
if len(pattern) == 0 {
|
||||
// Nothing left after a slash: an anonymous "..." taking the rest,
|
||||
// which is why "/files/" matches "/files/a/b" and "/" matches all.
|
||||
return true, false
|
||||
}
|
||||
patSeg, patRest, patMore := strings.Cut(pattern, "/")
|
||||
reqSeg, reqRest, reqMore := bytes.Cut(requestPath, pathSeparator)
|
||||
name, isMulti, isWildcard := pathWildcard(patSeg)
|
||||
switch {
|
||||
case isWildcard && name == "$":
|
||||
// Matches the end of the path and nothing else, so it must be the
|
||||
// last segment of the pattern and leave no path behind.
|
||||
return !patMore && len(requestPath) == 0, false
|
||||
|
||||
case isWildcard && isMulti:
|
||||
// Takes the remainder including slashes, possibly empty.
|
||||
if name != "" {
|
||||
if n >= len(dstPathVals) {
|
||||
return false, true
|
||||
}
|
||||
dstPathVals[n] = pathValue{Key: name, Value: requestPath}
|
||||
n++
|
||||
}
|
||||
return true, false
|
||||
|
||||
case isWildcard:
|
||||
if len(reqSeg) == 0 {
|
||||
return false, false // One segment means a non-empty one.
|
||||
}
|
||||
if n >= len(dstPathVals) {
|
||||
return false, true
|
||||
}
|
||||
dstPathVals[n] = pathValue{Key: name, Value: reqSeg}
|
||||
n++
|
||||
|
||||
default:
|
||||
if b2s(reqSeg) != patSeg {
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
if patMore != reqMore {
|
||||
// One side has a further segment and the other does not, so
|
||||
// "/health" misses "/health/" and "/files/" misses "/files".
|
||||
return false, false
|
||||
} else if !patMore {
|
||||
return true, false // Both spent on the same segment.
|
||||
}
|
||||
pattern, requestPath = patRest, reqRest
|
||||
}
|
||||
}
|
||||
|
||||
// pathWildcard picks apart a "{name}" or "{name...}" pattern segment. It
|
||||
// reports ok false for a literal segment, so "/b_{bucket}" is literal text and
|
||||
// not a wildcard, matching ServeMux's rule that wildcards be whole segments.
|
||||
func pathWildcard(segment string) (name string, isMulti, ok bool) {
|
||||
if len(segment) < 2 || segment[0] != '{' || segment[len(segment)-1] != '}' {
|
||||
return "", false, false
|
||||
}
|
||||
name = segment[1 : len(segment)-1]
|
||||
if rest, found := strings.CutSuffix(name, "..."); found {
|
||||
return rest, true, true
|
||||
}
|
||||
return name, false, true
|
||||
}
|
||||
|
||||
// Reset discards all registered handlers, reusing the backing array and growing
|
||||
// it to fit capacity registrations.
|
||||
func (sm *MuxSlice) Reset(capacity int) {
|
||||
@@ -121,13 +213,17 @@ func (sm *MuxSlice) Reset(capacity int) {
|
||||
|
||||
// LookupHandler returns the handler registered for request path, or nil if none matches.
|
||||
// The first registration matching both method and uri wins.
|
||||
func (sm *MuxSlice) LookupHandler(method Method, path string) (matched string, _ HandlerFunc) {
|
||||
func (sm *MuxSlice) LookupHandler(method Method, path []byte, dstPathVals []pathValue) (matched string, _ HandlerFunc) {
|
||||
for _, endpoint := range sm._handlers {
|
||||
if endpoint.method != MethUndefined && endpoint.method != method {
|
||||
continue
|
||||
}
|
||||
// Method matches.
|
||||
if path == endpoint.path {
|
||||
if endpoint.setPathVal {
|
||||
if ok, _ := SetPathValues(dstPathVals, endpoint.path, path); ok {
|
||||
return endpoint.path, endpoint.handler
|
||||
}
|
||||
} else if b2s(path) == endpoint.path {
|
||||
return endpoint.path, endpoint.handler
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package httphi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SetPathValues must agree with net/http.ServeMux on which patterns match which
|
||||
// paths and what each wildcard binds to. Every case below was taken from a run
|
||||
// against a real ServeMux, so this table is an oracle, not a guess.
|
||||
//
|
||||
// The one documented deviation is percent-decoding: ServeMux unescapes segments
|
||||
// before matching and binding, this does not. See TestSetPathValuesEscaping.
|
||||
func TestSetPathValues(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
pattern string
|
||||
path string
|
||||
match bool
|
||||
want string // "name=value" pairs joined by "|", in bind order.
|
||||
}{
|
||||
// Single segment wildcard binds exactly one non-empty segment.
|
||||
{pattern: "/users/{id}", path: "/users/42", match: true, want: "id=42"},
|
||||
{pattern: "/users/{id}", path: "/users/42/x", match: false},
|
||||
{pattern: "/users/{id}", path: "/users/", match: false},
|
||||
{pattern: "/users/{id}", path: "/users", match: false},
|
||||
{pattern: "/users/{id}/edit", path: "/users/42/edit", match: true, want: "id=42"},
|
||||
{pattern: "/{a}/{b}", path: "/x/y", match: true, want: "a=x|b=y"},
|
||||
|
||||
// "..." swallows the remainder, slashes included, and may bind empty.
|
||||
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/a/b/c", match: true, want: "bucket=bk|obj=a/b/c"},
|
||||
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/", match: true, want: "bucket=bk|obj="},
|
||||
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o", match: false},
|
||||
{pattern: "/files/{p...}", path: "/files/", match: true, want: "p="},
|
||||
{pattern: "/files/{p...}", path: "/files", match: false},
|
||||
|
||||
// {$} matches only the end of the path.
|
||||
{pattern: "/{$}", path: "/", match: true},
|
||||
{pattern: "/{$}", path: "/x", match: false},
|
||||
{pattern: "/a/{$}", path: "/a/", match: true},
|
||||
{pattern: "/a/{$}", path: "/a", match: false},
|
||||
{pattern: "/a/{$}", path: "/a/b", match: false},
|
||||
|
||||
// A trailing slash is an anonymous "..." wildcard, binding nothing.
|
||||
{pattern: "/files/", path: "/files/a/b", match: true},
|
||||
{pattern: "/files/", path: "/files/", match: true},
|
||||
{pattern: "/files/", path: "/files", match: false},
|
||||
{pattern: "/", path: "/anything/at/all", match: true},
|
||||
|
||||
// Literal patterns match exactly, trailing slash included.
|
||||
{pattern: "/health", path: "/health", match: true},
|
||||
{pattern: "/health", path: "/health/", match: false},
|
||||
|
||||
// An empty segment never satisfies a single wildcard.
|
||||
{pattern: "/a/{x}/b", path: "/a//b", match: false},
|
||||
} {
|
||||
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
|
||||
vals := make([]pathValue, 8)
|
||||
match, tooShort := SetPathValues(vals, test.pattern, []byte(test.path))
|
||||
if tooShort {
|
||||
t.Fatal("8 slots must be enough for these patterns")
|
||||
}
|
||||
if match != test.match {
|
||||
t.Fatalf("want match=%v, got %v", test.match, match)
|
||||
}
|
||||
if !match {
|
||||
return
|
||||
}
|
||||
if got := renderPathValues(vals); got != test.want {
|
||||
t.Errorf("want %q, got %q", test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Bound values must alias the request path buffer rather than copy it: the
|
||||
// exchange owns that memory and a copy would allocate per request.
|
||||
func TestSetPathValuesAliasesRequestBuffer(t *testing.T) {
|
||||
path := []byte("/users/42/edit")
|
||||
vals := make([]pathValue, 4)
|
||||
match, _ := SetPathValues(vals, "/users/{id}/edit", path)
|
||||
if !match {
|
||||
t.Fatal("want match")
|
||||
}
|
||||
if string(vals[0].Value) != "42" {
|
||||
t.Fatalf("want id=42, got %q", vals[0].Value)
|
||||
}
|
||||
// Mutating the request buffer must show through the bound value.
|
||||
path[7] = '9'
|
||||
if string(vals[0].Value) != "92" {
|
||||
t.Errorf("value must alias the request buffer, got %q", vals[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
// A destination too small to hold every wildcard must say so rather than bind a
|
||||
// partial set or write out of range.
|
||||
func TestSetPathValuesSliceTooShort(t *testing.T) {
|
||||
match, tooShort := SetPathValues(make([]pathValue, 1), "/{a}/{b}", []byte("/x/y"))
|
||||
if !tooShort {
|
||||
t.Error("want pathValSliceTooShort for 2 wildcards in 1 slot")
|
||||
}
|
||||
if match {
|
||||
t.Error("want match=false when values could not be bound")
|
||||
}
|
||||
// A pattern that binds nothing needs no slots at all.
|
||||
match, tooShort = SetPathValues(nil, "/health", []byte("/health"))
|
||||
if !match || tooShort {
|
||||
t.Errorf("want match with no slots needed, got match=%v tooShort=%v", match, tooShort)
|
||||
}
|
||||
}
|
||||
|
||||
// Percent escapes are compared and bound raw. ServeMux unescapes segment by
|
||||
// segment, so "/users/x%2Fy" binds id="x/y" there and id="x%2Fy" here. Matching
|
||||
// agrees either way; only the bound bytes differ.
|
||||
func TestSetPathValuesEscaping(t *testing.T) {
|
||||
vals := make([]pathValue, 4)
|
||||
if match, _ := SetPathValues(vals, "/a%2Fb/{x}", []byte("/a%2Fb/v")); !match {
|
||||
t.Error("want literal escape in pattern to match the same bytes in path")
|
||||
}
|
||||
vals = make([]pathValue, 4)
|
||||
match, _ := SetPathValues(vals, "/users/{id}", []byte("/users/x%2Fy"))
|
||||
if !match {
|
||||
t.Fatal("want match")
|
||||
}
|
||||
if got := string(vals[0].Value); got != "x%2Fy" {
|
||||
t.Errorf("want raw %q, got %q", "x%2Fy", got)
|
||||
}
|
||||
}
|
||||
|
||||
// renderPathValues joins the bound pairs for comparison, stopping at the first
|
||||
// unused slot.
|
||||
func renderPathValues(vals []pathValue) string {
|
||||
var sb strings.Builder
|
||||
for _, v := range vals {
|
||||
if v.Key == "" {
|
||||
break
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteByte('|')
|
||||
}
|
||||
sb.WriteString(v.Key)
|
||||
sb.WriteByte('=')
|
||||
sb.Write(v.Value)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Matching a request must not allocate: keys alias the mux's pattern and values
|
||||
// alias the request buffer, so nothing is copied per request.
|
||||
func TestSetPathValuesNoAlloc(t *testing.T) {
|
||||
vals := make([]pathValue, 8)
|
||||
path := []byte("/b/bk/o/a/b/c")
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
SetPathValues(vals, "/b/{bucket}/o/{obj...}", path)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("SetPathValues allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
// pathValueMux binds one wildcard pattern, standing in for a [Mux] that
|
||||
// supports them until MuxSlice sets setPathVal.
|
||||
type pathValueMux struct {
|
||||
pattern string
|
||||
handler HandlerFunc
|
||||
}
|
||||
|
||||
func (m *pathValueMux) LookupHandler(method Method, path []byte, dst []pathValue) (string, HandlerFunc) {
|
||||
if ok, _ := SetPathValues(dst, m.pattern, path); ok {
|
||||
return m.pattern, m.handler
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// A wildcard bound by one request must not be readable by the next request the
|
||||
// same pooled exchange serves. A literal pattern binds nothing, so it never
|
||||
// overwrites the previous request's slots, and the values it would leak alias a
|
||||
// buffer the new request has already overwritten.
|
||||
func TestExchangePathValueClearedBetweenRequests(t *testing.T) {
|
||||
exch := new(Exchange)
|
||||
exch.Configure(ExchangeConfig{
|
||||
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
|
||||
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: 4,
|
||||
})
|
||||
|
||||
// First request binds id=42 off a wildcard pattern.
|
||||
var gotFirst string
|
||||
wildcard := &pathValueMux{pattern: "/users/{id}", handler: func(e *Exchange) {
|
||||
gotFirst = string(e.PathValue("id"))
|
||||
e.WriteHeader(200)
|
||||
}}
|
||||
conn := newConn("GET /users/42 HTTP/1.1\r\nHost: h\r\n\r\n")
|
||||
conn.Hangup()
|
||||
if !exch.Acquire(conn) {
|
||||
t.Fatal("fresh exchange failed to acquire")
|
||||
}
|
||||
if err := Handle(exch, wildcard, nopBackoff); err != nil {
|
||||
t.Fatalf("first request: %s", err)
|
||||
}
|
||||
exch.Release()
|
||||
if gotFirst != "42" {
|
||||
t.Fatalf("want id=42 bound on the first request, got %q", gotFirst)
|
||||
}
|
||||
|
||||
// Second request matches a literal pattern, which binds nothing at all.
|
||||
var leaked []byte
|
||||
var sm MuxSlice
|
||||
sm.Handle("/health", func(e *Exchange) {
|
||||
leaked = e.PathValue("id")
|
||||
e.WriteHeader(200)
|
||||
})
|
||||
conn2 := newConn("GET /health HTTP/1.1\r\nHost: h\r\n\r\n")
|
||||
conn2.Hangup()
|
||||
if !exch.Acquire(conn2) {
|
||||
t.Fatal("released exchange failed to re-acquire")
|
||||
}
|
||||
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||
t.Fatalf("second request: %s", err)
|
||||
}
|
||||
if leaked != nil {
|
||||
t.Errorf("want no path value on a literal route, got id=%q from the previous request", leaked)
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type Router struct {
|
||||
reqBuf int
|
||||
respBuf int
|
||||
reqNumHeaderCap int
|
||||
maxPathValues int
|
||||
normalizeKeys bool
|
||||
pendingConns chan job
|
||||
mux Mux
|
||||
@@ -83,6 +84,8 @@ type RouterConfig struct {
|
||||
ResponseHeaderMinBufferSize int
|
||||
// Number of request header key/value pairs to parse before failing and returning [StatusRequestHeaderFieldsTooLarge].
|
||||
RequestNumHeaderKVCap int
|
||||
// Sets maximum number of PathValue pairs that can be set on an exchange. Accessed via [Exchange.PathValue].
|
||||
MaxPathValues int
|
||||
|
||||
// NormalizeOutgoingKeys normalizes response header field keys as they are
|
||||
// staged, i.e: "content-type" becomes "Content-Type".
|
||||
@@ -180,11 +183,13 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
||||
gen := r.gen.Load()
|
||||
numgoro := cfg.FixedNumGoroutines
|
||||
workerMode := cfg.workerMode()
|
||||
|
||||
r.reqNumHeaderCap = cfg.RequestNumHeaderKVCap
|
||||
r.reqBuf = cfg.RequestHeaderBufferSize
|
||||
r.respBuf = cfg.ResponseHeaderMinBufferSize
|
||||
r.mux = cfg.Mux
|
||||
r.log = cfg.Logger
|
||||
r.maxPathValues = cfg.MaxPathValues
|
||||
r.normalizeKeys = cfg.NormalizeOutgoingKeys
|
||||
// Freelist entries were sized by the outgoing configuration: recycling one
|
||||
// would serve a request with buffer limits cfg never asked for.
|
||||
@@ -218,6 +223,7 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
||||
NumHeaderKVCap: cfg.RequestNumHeaderKVCap,
|
||||
NormalizeOutgoingKeys: cfg.NormalizeOutgoingKeys,
|
||||
NoRequestBufferGrowth: true, // Hard memory limit.
|
||||
MaxPathValues: cfg.MaxPathValues,
|
||||
})
|
||||
go r.goroWorker(gen, jobqueue, cfg.Mux)
|
||||
}
|
||||
@@ -383,6 +389,7 @@ func (r *Router) getExchLocked(conn conn) (exch *Exchange) {
|
||||
NumHeaderKVCap: r.reqNumHeaderCap,
|
||||
NormalizeOutgoingKeys: r.normalizeKeys,
|
||||
NoRequestBufferGrowth: true,
|
||||
MaxPathValues: r.maxPathValues,
|
||||
})
|
||||
exch.Acquire(conn) // Fresh exchange, CAS cannot fail.
|
||||
return exch
|
||||
|
||||
@@ -22,6 +22,9 @@ func (f *Form) Reset(buf []byte, capKV int) {
|
||||
// ParseBytes copies the argument bytes to the Form's underlying buffer and parses them.
|
||||
func (f *Form) ParseBytes(b []byte) error {
|
||||
f.Reset(nil, 0)
|
||||
if len(b) == 0 {
|
||||
return nil // An empty body is an empty form, not a failure to read one.
|
||||
}
|
||||
err := f.kv.ReadFromBytes(b)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,7 +57,7 @@ func (f *Form) Decode() error {
|
||||
return err
|
||||
} else if len(v) == 0 {
|
||||
if nk != len(k) {
|
||||
f.kv.setAt(i, k, v)
|
||||
f.kv.setAt(i, k[:nk], v) // k[:nk]: the decoded key is shorter.
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -66,6 +66,29 @@ func TestFormDecode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decoding a valueless key that shrinks must rewrite the key without inventing
|
||||
// a value: the pair has no '=' before Decode and must have none after.
|
||||
func TestFormDecodeValuelessKeyShrinks(t *testing.T) {
|
||||
var f Form
|
||||
const body = "a=1&o%6Bay"
|
||||
if err := f.ParseBytes([]byte(body)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Decode(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = "a=1|okay"
|
||||
if got := render(&f); got != want {
|
||||
t.Errorf("want %q, got %q", want, got)
|
||||
}
|
||||
if _, value := f.Pair(1); value != nil {
|
||||
t.Errorf("want valueless pair to stay valueless, got value %q", value)
|
||||
}
|
||||
if !f.Has("okay") {
|
||||
t.Error("want decoded valueless key present")
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed escape must be reported, never silently passed through.
|
||||
func TestFormDecodeMalformed(t *testing.T) {
|
||||
for _, body := range []string{"q=%zz", "%zz=v", "q=%4"} {
|
||||
|
||||
@@ -241,49 +241,11 @@ func (h *Header) SetInt(key string, value int64, base int) {
|
||||
// Calling Set mangles the buffer.
|
||||
func (h *Header) Set(key, value string) (enoughSpace bool) {
|
||||
return h.hbuf.kv.Set(key, value)
|
||||
|
||||
// useKv := h.takeReusableSlot(key)
|
||||
// if useKv == nil {
|
||||
// h.hbuf.kv.appendPair(key, value)
|
||||
// } else {
|
||||
// useKv.value = h.hbuf.kv.reuseOrAppend(useKv.value, value)
|
||||
// }
|
||||
}
|
||||
|
||||
// takeReusableSlot returns the valid key-value entry for key with the largest
|
||||
// value buffer (best candidate for in-place reuse) and invalidates any other
|
||||
// entries sharing the key. Returns nil if the key is not present.
|
||||
func (h *Header) takeReusableSlot(key string) *argsKV {
|
||||
// hb := &h.hbuf
|
||||
var useKv *argsKV
|
||||
// for i := 0; i < len(hb.headers); i++ {
|
||||
// // Search for key-value with largest buffer for value to store value reusing buffer.
|
||||
// gotkv := &hb.headers[i]
|
||||
// if gotkv.isValidHeader() && b2s(hb.musttoken(gotkv.key)) == key {
|
||||
// if useKv == nil {
|
||||
// useKv = gotkv
|
||||
// } else if gotkv.value.len > useKv.value.len {
|
||||
// useKv.invalidate()
|
||||
// useKv = gotkv
|
||||
// } else {
|
||||
// gotkv.invalidate()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return useKv
|
||||
}
|
||||
|
||||
// Get gets the first exact-match value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key.
|
||||
func (h *Header) Get(key string) []byte {
|
||||
return h.hbuf.kv.Get(key)
|
||||
// debuglog("http:get:start")
|
||||
// kv := h.peekHeader(key)
|
||||
// if kv.isValidHeader() {
|
||||
// debuglog("http:get:found")
|
||||
// return h.hbuf.musttoken(kv.value)
|
||||
// }
|
||||
// debuglog("http:get:notfound")
|
||||
// return nil
|
||||
}
|
||||
|
||||
// GetFold gets the first value whose key matches key under ASCII case-insensitive
|
||||
|
||||
@@ -225,9 +225,11 @@ func (mb *KVBuffer) At(i int) (key, value []byte) {
|
||||
}
|
||||
func (mb *KVBuffer) setAt(i int, k, v []byte) {
|
||||
mb.flags |= flagMangledBuffer
|
||||
// Route through slice, not bytes2tok: a nil v is a pair with no '=' and must
|
||||
// stay absent rather than trip the alias check on a nil pointer.
|
||||
mb.kvs[i] = argsKV{
|
||||
key: bytes2tok(mb.buf, k),
|
||||
value: bytes2tok(mb.buf, v),
|
||||
key: mb.slice(k),
|
||||
value: mb.slice(v),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ var (
|
||||
errNoProto = errors.New("missing protocol, HTTP/0.9 unsupported")
|
||||
// ErrNeedMoreData signals a parser was handed an incomplete buffer: append
|
||||
// more data to it and call again.
|
||||
ErrNeedMoreData = errors.New("need more data: cannot find trailing lf/delimiter")
|
||||
errNoBoundary = errors.New("httpraw: multipart boundary not set")
|
||||
errUnparsed = errors.New("need to finish parsing")
|
||||
errInvalidName = errors.New("invalid header name")
|
||||
ErrNeedMoreData = errors.New("need more data: cannot find trailing lf/delimiter")
|
||||
errNoBoundary = errors.New("httpraw: multipart boundary not set")
|
||||
errUnparsed = errors.New("need to finish parsing")
|
||||
errInvalidName = errors.New("invalid header name")
|
||||
// ErrBufferExhausted signals a buffer with no room left for the data being
|
||||
// written and no permission to grow, see [KVBuffer.EnableBufferGrowth].
|
||||
// Enlarging the buffer handed to Reset is the only fix; a server answers it
|
||||
|
||||
Reference in New Issue
Block a user