Files
lneto/http/httpraw/form.go
T
Pat Whittingslow 5c54030f19 add http/httphi (#171)
* add http/httphi

* begin adding httphi tests

* claude found neat bugs

* add low level Handle function and more tests

* more tests, run go generate

* add Hijacker-like functionality

* improve locking and acquisition of Exchanges in reconfiguring

* several bugfixes, add internal.IntLen, round up http-linux example with new router API

* small nit

* add benchmarks

* add query handling

* remove ForEach pattern, allocates in TinyGo

* massive documentation push and code reordering in files

* Router.Handle returns error after being torn down

* run go fix

* rework Mux interface to receive a string request path

* add MethodFrom

* minor doc nit

* fail on incomplete staging

* add raw buffer access

* add streaming API distinct from Exchange

* begin adding multipart form logic

* finish rounding up multipart form parsing

* remove status type

* first Multipart approach

* begin adding readMultiPart

* add Exchange.ReadMultiparts reimagining of clanker slop

* ai insists with backoffs

* simplify clanker slop

* apply go fix

* add a pattern argument to Mux

* explicit header key/value alloc and add ExchangeConfig

* fix tests after excplicit header alloc change

* fix examples

* run go fix

* expose rawsock as experimental package (will use for external benchmarks)

* remove backoff from form parsing

* @MDr164 suggestions get potential fixes

* apply go fix

* add examples

* add README.md

* fix rawsock tinygo implementation

* apply @MDr164 various fixes

* update documentation on ContentLength methods and fix bug in Form reset on empty body

* fix tests

* add fuzz tests

* run go fix

* io.ErrNoProgress on parsing form spin

* run go fix

* remove backoff assumption from Router

* httphi.Handle rejects unsupported protocols

* go format router.go

* add kvbuffer

* rewrite Cookie with KVBuffer

* mid refactor of KVBuffer into Header

* work on KVBuffer exhausted semantics

* add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice

* add PathValue example

* document all the things; improve req Query semantics; add Form.EnableBufferGrowth

* unexport kvBuffer

* add Exchange.PathValueAppend

* use stdlib in example instead of rawsock

* remove rawsock from http example

* add darwin arch rawsock

* fix example

* rename Router.TeardownGoroutines to Shutdown matching http.Server.Shutdown

* rename types and identifiers

* @MDr164 Content-Type and Transfer-Encoding bug catches
2026-07-29 20:20:21 -03:00

146 lines
4.3 KiB
Go

package httpraw
import "bytes"
// Form holds "application/x-www-form-urlencoded" key-value pairs, the encoding
// HTML forms use for POST bodies and query strings alike. Methods function
// similarly to eponymous [Cookie] methods.
//
// Pairs are stored as they appear on the wire, percent-encoded and with '+'
// undecoded, until [Form.Decode] rewrites them in place. The caller bounds the
// data: Form parses the buffer it is handed and reads nothing more.
type Form struct {
kv kvBuffer
}
// EnableBufferGrowth allows the form's buffer to grow past what [Form.Reset] was
// handed. See [kvBuffer.EnableBufferGrowth].
func (f *Form) EnableBufferGrowth(enableGrowth bool) { f.kv.EnableBufferGrowth(enableGrowth) }
// Reset discards parsed pairs and sets the buffer to parse in place.
// If buf is nil the current buffer is reused.
func (f *Form) Reset(buf []byte, capKV int) {
f.kv.Reset(buf, capKV)
}
// 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
}
return f.Parse()
}
// Parse parses the form's buffer in place.
func (f *Form) Parse() error {
f.kv.discardKVs()
key, value, rest := NextQueryPair(f.kv.buf)
for key != nil {
if !f.kv.setInternal(key, value) {
return ErrBufferExhausted
}
key, value, rest = NextQueryPair(rest)
}
return nil
}
// Decode rewrites every key and value in place, replacing percent escapes and
// '+' with the bytes they encode. Decoding only shrinks, so no memory is added.
func (f *Form) Decode() error {
const plusAsSpace = true // Form encoded data, unlike a path.
nkvs := f.kv.Len()
for i := range nkvs {
k, v := f.kv.At(i)
nk, err := CopyDecodedPercentURL(k, k, plusAsSpace)
if err != nil {
return err
} else if len(v) == 0 {
if nk != len(k) {
f.kv.setAt(i, k[:nk], v) // k[:nk]: the decoded key is shorter.
}
continue
}
nv, err := CopyDecodedPercentURL(v, v, plusAsSpace)
if err != nil {
return err
}
if nk != len(k) || nv != len(v) {
f.kv.setAt(i, k[:nk], v[:nv])
}
}
return nil
}
// Len returns the amount of key-value pairs parsed.
func (f *Form) Len() int { return f.kv.Len() }
// Pair returns the i'th key-value pair in wire order. The value is nil for a
// pair with no '=', i.e: "ok" in "ok&q=go", which distinguishes it from "ok="
// where the value is present and empty.
func (f *Form) Pair(i int) (key, value []byte) {
return f.kv.At(i)
}
// Get returns the value of the first pair matching key, nil if absent or if the
// pair has no value. Bytes are compared as stored, so call [Form.Decode] first
// when keys may be encoded.
func (f *Form) Get(key string) []byte { return f.kv.Get(key) }
// Has returns true if key is present, with or without a value.
func (f *Form) Has(key string) bool { return f.kv.Present(key) }
// AppendKeyValues appends the form's wire representation to dst and returns it.
func (f *Form) AppendKeyValues(dst []byte) []byte {
nkv := f.kv.Len()
for i := range nkv {
key, value := f.Pair(i)
if i > 0 {
dst = append(dst, '&')
}
dst = append(dst, key...)
if value != nil {
dst = append(dst, '=')
dst = append(dst, value...)
}
}
return dst
}
// NextQueryPair splits the leading key-value pair off a query string and returns
// what remains of it. Loop until rawkey is nil:
//
// rawkey, rawval, rest := httpraw.NextQueryPair(h.RequestQuery())
// for rawkey != nil {
// // use rawkey, rawval.
// rawkey, rawval, rest = httpraw.NextQueryPair(rest)
// }
//
// A pair with no '=' yields a nil rawval, i.e: "debug" in "?debug&q=go", which
// distinguishes it from "?debug=" where the value is present and empty. Empty
// sequences are skipped, so "?&&q=go&" yields a single pair. Only '&' separates
// pairs and only the first '=' splits a pair.
func NextQueryPair(query []byte) (rawkey, rawval, rest []byte) {
for len(query) > 0 {
pair := query
amp := bytes.IndexByte(query, '&')
if amp >= 0 {
pair, query = query[:amp], query[amp+1:]
} else {
query = nil
}
if len(pair) == 0 {
continue // Empty sequence, see WHATWG URL urlencoded parsing.
}
if before, after, ok := bytes.Cut(pair, []byte{'='}); ok {
return before, after, query
}
return pair, nil, query
}
return nil, nil, nil
}