mirror of
https://github.com/soypat/lneto.git
synced 2026-08-15 12:23:44 +00:00
begin adding multipart form logic
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package httpraw
|
||||
|
||||
// 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 {
|
||||
buf []byte
|
||||
kvs []argsKV
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if buf == nil {
|
||||
buf = f.buf[:0]
|
||||
}
|
||||
*f = Form{
|
||||
buf: buf,
|
||||
kvs: f.kvs[:0],
|
||||
}
|
||||
}
|
||||
|
||||
// ParseBytes copies the argument bytes to the Form's underlying buffer and parses them.
|
||||
func (f *Form) ParseBytes(b []byte) error {
|
||||
f.Reset(nil)
|
||||
f.buf = append(f.buf[:0], b...)
|
||||
return f.Parse()
|
||||
}
|
||||
|
||||
// Parse parses the form's buffer in place.
|
||||
func (f *Form) Parse() error {
|
||||
f.kvs = f.kvs[:0]
|
||||
key, value, rest := NextQueryPair(f.buf)
|
||||
for key != nil {
|
||||
kv := argsKV{key: bytes2tok(f.buf, key)}
|
||||
if value != nil {
|
||||
kv.value = bytes2tok(f.buf, value)
|
||||
}
|
||||
f.kvs = append(f.kvs, kv)
|
||||
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.
|
||||
for i := range f.kvs {
|
||||
kv := &f.kvs[i]
|
||||
n, err := CopyDecodedPercentURL(tok2bytes(f.buf, kv.key), tok2bytes(f.buf, kv.key), plusAsSpace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kv.key.len = tokint(n)
|
||||
if !kv.HasValue() {
|
||||
continue
|
||||
}
|
||||
n, err = CopyDecodedPercentURL(tok2bytes(f.buf, kv.value), tok2bytes(f.buf, kv.value), plusAsSpace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kv.value.len = tokint(n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the amount of key-value pairs parsed.
|
||||
func (f *Form) Len() int { return len(f.kvs) }
|
||||
|
||||
// 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) {
|
||||
kv := f.kvs[i]
|
||||
key = tok2bytes(f.buf, kv.key)
|
||||
if kv.HasValue() {
|
||||
value = tok2bytes(f.buf, kv.value)
|
||||
}
|
||||
return key, value
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for i := range f.kvs {
|
||||
gotKey, value := f.Pair(i)
|
||||
if b2s(gotKey) == key {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Has returns true if key is present, with or without a value.
|
||||
func (f *Form) Has(key string) bool {
|
||||
for i := range f.kvs {
|
||||
if b2s(tok2bytes(f.buf, f.kvs[i].key)) == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AppendKeyValues appends the form's wire representation to dst and returns it.
|
||||
func (f *Form) AppendKeyValues(dst []byte) []byte {
|
||||
for i := range f.kvs {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package httpraw
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// render joins a form's pairs as "key=value", a valueless key as "key".
|
||||
func render(f *Form) string {
|
||||
var sb strings.Builder
|
||||
for i := range f.Len() {
|
||||
key, value := f.Pair(i)
|
||||
if i > 0 {
|
||||
sb.WriteByte('|')
|
||||
}
|
||||
sb.Write(key)
|
||||
if value != nil {
|
||||
sb.WriteByte('=')
|
||||
sb.Write(value)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestFormParse(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{body: "", want: ""},
|
||||
{body: "q=go", want: "q=go"},
|
||||
{body: "name=Jos%C3%A9+P%C3%A9rez&msg=hi+there&ok=on", want: "name=Jos%C3%A9+P%C3%A9rez|msg=hi+there|ok=on"},
|
||||
{body: "ok", want: "ok"}, // Flag: no '=' at all.
|
||||
{body: "ok=", want: "ok="}, // Present but empty.
|
||||
{body: "&&q=go&", want: "q=go"}, // Empty sequences skipped.
|
||||
{body: "tag=a&tag=b", want: "tag=a|tag=b"}, // Duplicates kept in order.
|
||||
{body: "=v", want: "=v"}, // Empty name kept.
|
||||
{body: "a=b=c", want: "a=b=c"}, // Only the first '=' splits.
|
||||
} {
|
||||
var f Form
|
||||
if err := f.ParseBytes([]byte(test.body)); err != nil {
|
||||
t.Fatalf("%q: %s", test.body, err)
|
||||
}
|
||||
if got := render(&f); got != test.want {
|
||||
t.Errorf("%q: want %q, got %q", test.body, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode rewrites keys and values in place: percent escapes and '+' as space.
|
||||
func TestFormDecode(t *testing.T) {
|
||||
var f Form
|
||||
const body = "name=Jos%C3%A9+P%C3%A9rez&a%20b=c%2Bd&ok"
|
||||
if err := f.ParseBytes([]byte(body)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Decode(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = "name=José Pérez|a b=c+d|ok"
|
||||
if got := render(&f); got != want {
|
||||
t.Errorf("want %q, got %q", want, got)
|
||||
}
|
||||
if got := string(f.Get("a b")); got != "c+d" {
|
||||
t.Errorf("want decoded key lookup %q, got %q", "c+d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 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"} {
|
||||
var f Form
|
||||
if err := f.ParseBytes([]byte(body)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Decode(); err == nil {
|
||||
t.Errorf("%q: want decode error, got nil", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormGetHas(t *testing.T) {
|
||||
var f Form
|
||||
if err := f.ParseBytes([]byte("tag=a&tag=b&ok&empty=")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := string(f.Get("tag")); got != "a" {
|
||||
t.Errorf("want first value %q, got %q", "a", got)
|
||||
}
|
||||
if got := f.Get("ok"); got != nil {
|
||||
t.Errorf("want nil value for valueless key, got %q", got)
|
||||
}
|
||||
if got := f.Get("nope"); got != nil {
|
||||
t.Errorf("want nil for absent key, got %q", got)
|
||||
}
|
||||
if v := f.Get("empty"); v == nil || len(v) != 0 {
|
||||
t.Errorf("want present empty value, got %v", v)
|
||||
}
|
||||
for _, key := range []string{"tag", "ok", "empty"} {
|
||||
if !f.Has(key) {
|
||||
t.Errorf("want Has(%q) true", key)
|
||||
}
|
||||
}
|
||||
if f.Has("nope") {
|
||||
t.Error("want Has(nope) false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormAppendKeyValues(t *testing.T) {
|
||||
const body = "name=go&ok&empty=&tag=a&tag=b"
|
||||
var f Form
|
||||
if err := f.ParseBytes([]byte(body)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := string(f.AppendKeyValues(nil)); got != body {
|
||||
t.Errorf("want round trip %q, got %q", body, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing into a reused Form must not allocate: the pair storage is reused.
|
||||
func TestFormParseReuseNoAlloc(t *testing.T) {
|
||||
body := []byte("name=go&tag=a&tag=b&ok")
|
||||
var f Form
|
||||
if err := f.ParseBytes(body); err != nil { // Warm up the pair storage.
|
||||
t.Fatal(err)
|
||||
}
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
f.Reset(body)
|
||||
f.Parse()
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("reused Form allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package httpraw
|
||||
|
||||
// Multipart bodies frame their fields with a delimiter instead of escaping them,
|
||||
// so a part's value has no length: it ends where the next delimiter begins. The
|
||||
// functions below split a body without storing any of it, leaving the caller to
|
||||
// decide what to keep, what to skip and when a part has grown too large:
|
||||
//
|
||||
// boundary := httpraw.MultipartBoundary(contentType)
|
||||
// for {
|
||||
// hdr, rest, err := httpraw.NextPartHeader(buf, boundary)
|
||||
// if err != nil {
|
||||
// break // ErrEndOfParts, or ErrNeedMoreData: read more into buf and retry.
|
||||
// }
|
||||
// name := httpraw.PartName(hdr)
|
||||
// for {
|
||||
// body, next, done := httpraw.NextPartBody(rest, boundary)
|
||||
// // Consume body, then compact next to the front of buf and read more.
|
||||
// rest = next
|
||||
// if done {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
type Multipart struct {
|
||||
Boundary []byte
|
||||
}
|
||||
|
||||
func ()
|
||||
|
||||
// MultipartBoundary returns the boundary parameter of a Content-Type field value,
|
||||
// i.e: "abc123" for "multipart/form-data; boundary=abc123". The leading "--" of
|
||||
// the wire delimiter is not included. Returns nil if there is no such parameter.
|
||||
func MultipartBoundary(contentType []byte) []byte { return ContentParam(contentType, "boundary") }
|
||||
|
||||
// ContentParam returns the value of a parameter of a header field value, i.e:
|
||||
// "utf-8" for key "charset" of "text/plain; charset=utf-8". Quoted values are
|
||||
// returned without their quotes and with escapes left as they appear on the
|
||||
// wire. Key matching is case insensitive, RFC 9110 5.6.6.
|
||||
func ContentParam(value []byte, key string) []byte {
|
||||
for len(value) > 0 {
|
||||
semi := bytes.IndexByte(value, ';')
|
||||
if semi < 0 {
|
||||
return nil // No parameters left.
|
||||
}
|
||||
value = trimOWS(value[semi+1:])
|
||||
eq := bytes.IndexByte(value, '=')
|
||||
if eq < 0 {
|
||||
return nil
|
||||
}
|
||||
gotKey := trimOWS(value[:eq])
|
||||
value = value[eq+1:]
|
||||
param := value
|
||||
if len(param) > 0 && param[0] == '"' {
|
||||
end := bytes.IndexByte(param[1:], '"')
|
||||
if end < 0 {
|
||||
return nil // Unterminated quoted string.
|
||||
}
|
||||
param, value = param[1:end+1], param[end+2:]
|
||||
} else {
|
||||
end := bytes.IndexByte(param, ';')
|
||||
if end >= 0 {
|
||||
param, value = param[:end], param[end:]
|
||||
} else {
|
||||
value = nil
|
||||
}
|
||||
param = trimOWS(param)
|
||||
}
|
||||
if equalFold(gotKey, key) {
|
||||
return param
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextPartHeader splits the leading part's header block off a multipart body,
|
||||
// returning it with its final CRLF and the body bytes that follow it. Returns
|
||||
// [ErrNeedMoreData] while data holds no complete delimiter and header block, and
|
||||
// [ErrEndOfParts] once the closing delimiter is reached.
|
||||
func NextPartHeader(data, boundary []byte) (partHdr, rest []byte, err error) {
|
||||
if len(boundary) == 0 {
|
||||
return nil, nil, errNoBoundary
|
||||
}
|
||||
idx := indexDelimiter(data, boundary)
|
||||
if idx < 0 {
|
||||
return nil, nil, ErrNeedMoreData
|
||||
}
|
||||
after := idx + len("--") + len(boundary)
|
||||
if after+2 > len(data) {
|
||||
return nil, nil, ErrNeedMoreData // Cannot tell a closing delimiter yet.
|
||||
} else if data[after] == '-' && data[after+1] == '-' {
|
||||
return nil, nil, ErrEndOfParts
|
||||
}
|
||||
// Delimiter is followed by CRLF, then the part's header block.
|
||||
if data[after] == '\r' {
|
||||
after++
|
||||
}
|
||||
if after >= len(data) {
|
||||
return nil, nil, ErrNeedMoreData
|
||||
} else if data[after] != '\n' {
|
||||
return nil, nil, errInvalidName // Junk between delimiter and part.
|
||||
}
|
||||
after++
|
||||
end := bytes.Index(data[after:], []byte("\r\n\r\n"))
|
||||
if end < 0 {
|
||||
return nil, nil, ErrNeedMoreData
|
||||
}
|
||||
return data[after : after+end+2], data[after+end+4:], nil
|
||||
}
|
||||
|
||||
// NextPartBody returns the part bytes available in data, holding back any tail
|
||||
// that could be the start of a delimiter. done reports the part ended, in which
|
||||
// case rest begins the next part's delimiter; otherwise rest is the held back
|
||||
// tail, which the caller compacts before reading more data into the buffer.
|
||||
func NextPartBody(data, boundary []byte) (body, rest []byte, done bool) {
|
||||
idx := indexPartEnd(data, boundary)
|
||||
if idx >= 0 {
|
||||
return data[:idx], data[idx+len("\r\n"):], true
|
||||
}
|
||||
// Longest prefix of "\r\n--"+boundary that could still be completed.
|
||||
hold := len("\r\n--") + len(boundary) - 1
|
||||
if hold > len(data) {
|
||||
hold = len(data)
|
||||
}
|
||||
return data[:len(data)-hold], data[len(data)-hold:], false
|
||||
}
|
||||
|
||||
// PartName returns the name parameter of a part's Content-Disposition field,
|
||||
// i.e: "photo" for `form-data; name="photo"; filename="beach.png"`.
|
||||
func PartName(partHdr []byte) []byte { return ContentParam(partField(partHdr), "name") }
|
||||
|
||||
// PartFileName returns the filename parameter of a part's Content-Disposition
|
||||
// field, nil when the part is not a file upload.
|
||||
func PartFileName(partHdr []byte) []byte { return ContentParam(partField(partHdr), "filename") }
|
||||
|
||||
// partField returns the Content-Disposition field value of a part header block.
|
||||
func partField(partHdr []byte) []byte {
|
||||
const key = "content-disposition"
|
||||
for len(partHdr) > 0 {
|
||||
eol := bytes.IndexByte(partHdr, '\n')
|
||||
line := partHdr
|
||||
if eol >= 0 {
|
||||
line, partHdr = partHdr[:eol], partHdr[eol+1:]
|
||||
} else {
|
||||
partHdr = nil
|
||||
}
|
||||
colon := bytes.IndexByte(line, ':')
|
||||
if colon > 0 && equalFold(line[:colon], key) {
|
||||
return line[colon+1:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// indexDelimiter returns the offset of the leading "--"+boundary in data.
|
||||
func indexDelimiter(data, boundary []byte) int {
|
||||
for i := 0; i+len("--")+len(boundary) <= len(data); i++ {
|
||||
dash := bytes.IndexByte(data[i:], '-')
|
||||
if dash < 0 {
|
||||
return -1
|
||||
}
|
||||
i += dash
|
||||
if i+len("--")+len(boundary) > len(data) {
|
||||
return -1
|
||||
}
|
||||
if data[i+1] == '-' && b2s(data[i+2:i+2+len(boundary)]) == b2s(boundary) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// indexPartEnd returns the offset of the CRLF that closes a part, that is the
|
||||
// CRLF preceding the next delimiter.
|
||||
func indexPartEnd(data, boundary []byte) int {
|
||||
for i := 0; i+len("\r\n--")+len(boundary) <= len(data); i++ {
|
||||
cr := bytes.IndexByte(data[i:], '\r')
|
||||
if cr < 0 {
|
||||
return -1
|
||||
}
|
||||
i += cr
|
||||
if i+len("\r\n--")+len(boundary) > len(data) {
|
||||
return -1
|
||||
}
|
||||
if data[i+1] == '\n' && data[i+2] == '-' && data[i+3] == '-' &&
|
||||
b2s(data[i+4:i+4+len(boundary)]) == b2s(boundary) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// trimOWS trims optional whitespace off both ends of b, RFC 9110 5.6.3.
|
||||
func trimOWS(b []byte) []byte {
|
||||
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
|
||||
b = b[1:]
|
||||
}
|
||||
for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t') {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// equalFold compares b to the ASCII lowercase key, case insensitively.
|
||||
func equalFold(b []byte, key string) bool {
|
||||
if len(b) != len(key) {
|
||||
return false
|
||||
}
|
||||
const asciiCapDiff = 'a' - 'A'
|
||||
for i := range b {
|
||||
c := b[i]
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
c += asciiCapDiff
|
||||
}
|
||||
if c != key[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package httpraw
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A two part body: a text field and a PNG upload whose bytes contain CRLFs and
|
||||
// even the boundary text, which must not desync the parser.
|
||||
const (
|
||||
multiBoundary = "----abc123"
|
||||
multiBody = "------abc123\r\n" +
|
||||
"Content-Disposition: form-data; name=\"caption\"\r\n" +
|
||||
"\r\n" +
|
||||
"hi there\r\n" +
|
||||
"------abc123\r\n" +
|
||||
"Content-Disposition: form-data; name=\"photo\"; filename=\"beach.png\"\r\n" +
|
||||
"Content-Type: image/png\r\n" +
|
||||
"\r\n" +
|
||||
"\x89PNG\r\n--not-the-boundary\r\n\x00\xff\r\n" +
|
||||
"------abc123--\r\n"
|
||||
)
|
||||
|
||||
func TestMultipartBoundary(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
contentType string
|
||||
want string
|
||||
}{
|
||||
{contentType: "multipart/form-data; boundary=abc123", want: "abc123"},
|
||||
{contentType: "multipart/form-data; boundary=\"a b\"", want: "a b"},
|
||||
{contentType: "multipart/form-data; charset=utf-8; boundary=xyz", want: "xyz"},
|
||||
{contentType: "multipart/form-data; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
|
||||
{contentType: "multipart/form-data", want: ""}, // Absent.
|
||||
{contentType: "application/x-www-form-urlencoded", want: ""},
|
||||
} {
|
||||
got := MultipartBoundary([]byte(test.contentType))
|
||||
if string(got) != test.want {
|
||||
t.Errorf("%q: want %q, got %q", test.contentType, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentParam(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
value string
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{value: "text/plain; charset=utf-8", key: "charset", want: "utf-8"},
|
||||
{value: "text/plain;charset=utf-8", key: "charset", want: "utf-8"}, // No space.
|
||||
{value: "text/plain; charset=\"utf-8\"", key: "charset", want: "utf-8"},
|
||||
{value: "form-data; name=\"photo\"; filename=\"a;b.png\"", key: "filename", want: "a;b.png"},
|
||||
{value: "form-data; name=\"photo\"", key: "nope", want: ""},
|
||||
{value: "form-data; names=x; name=y", key: "name", want: "y"}, // Prefix must not match.
|
||||
{value: "text/plain", key: "charset", want: ""},
|
||||
} {
|
||||
got := ContentParam([]byte(test.value), test.key)
|
||||
if string(got) != test.want {
|
||||
t.Errorf("%q key %q: want %q, got %q", test.value, test.key, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextPartHeader(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
hdr, rest, err := NextPartHeader([]byte(multiBody), boundary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const wantHdr = "Content-Disposition: form-data; name=\"caption\"\r\n"
|
||||
if string(hdr) != wantHdr {
|
||||
t.Errorf("want header %q, got %q", wantHdr, hdr)
|
||||
}
|
||||
if !strings.HasPrefix(string(rest), "hi there\r\n") {
|
||||
t.Errorf("want rest at part body, got %q", rest)
|
||||
}
|
||||
}
|
||||
|
||||
// Incomplete data must ask for more, never guess.
|
||||
func TestNextPartHeaderNeedMore(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
for _, data := range []string{
|
||||
"",
|
||||
"------abc", // Delimiter cut short.
|
||||
"------abc123\r\n", // No header block yet.
|
||||
"------abc123\r\nContent-Disposition: form-", // Header block unterminated.
|
||||
} {
|
||||
if _, _, err := NextPartHeader([]byte(data), boundary); err != ErrNeedMoreData {
|
||||
t.Errorf("%q: want ErrNeedMoreData, got %v", data, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The closing delimiter ends iteration.
|
||||
func TestNextPartHeaderEnd(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
if _, _, err := NextPartHeader([]byte("------abc123--\r\n"), boundary); err != ErrEndOfParts {
|
||||
t.Errorf("want ErrEndOfParts, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextPartBody(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
_, rest, err := NextPartHeader([]byte(multiBody), boundary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, rest, done := NextPartBody(rest, boundary)
|
||||
if !done {
|
||||
t.Fatal("want the part to end within the buffer")
|
||||
}
|
||||
if string(body) != "hi there" {
|
||||
t.Errorf("want body %q, got %q", "hi there", body)
|
||||
}
|
||||
if !strings.HasPrefix(string(rest), "------abc123\r\n") {
|
||||
t.Errorf("want rest at next delimiter, got %q", rest)
|
||||
}
|
||||
}
|
||||
|
||||
// A part whose bytes contain CRLFs and boundary-like text must survive intact.
|
||||
func TestNextPartBodyBinary(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
data := []byte(multiBody)
|
||||
_, rest, err := NextPartHeader(data, boundary) // caption part.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, rest, _ = NextPartBody(rest, boundary)
|
||||
_, rest, err = NextPartHeader(rest, boundary) // photo part.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, rest, done := NextPartBody(rest, boundary)
|
||||
if !done {
|
||||
t.Fatal("want the part to end within the buffer")
|
||||
}
|
||||
const want = "\x89PNG\r\n--not-the-boundary\r\n\x00\xff"
|
||||
if string(body) != want {
|
||||
t.Errorf("want body %q, got %q", want, body)
|
||||
}
|
||||
if _, _, err = NextPartHeader(rest, boundary); err != ErrEndOfParts {
|
||||
t.Errorf("want ErrEndOfParts after last part, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A delimiter split across two reads must not be mistaken for part data: the
|
||||
// tail is held back until proven not to be a delimiter.
|
||||
func TestNextPartBodySplitDelimiter(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
const part = "hi there"
|
||||
full := part + "\r\n------abc123\r\n"
|
||||
for split := 1; split < len(full); split++ {
|
||||
body, rest, done := NextPartBody([]byte(full[:split]), boundary)
|
||||
if done {
|
||||
continue // Whole delimiter already present, nothing to prove.
|
||||
}
|
||||
if len(body) > len(part) {
|
||||
t.Fatalf("split %d: emitted %q, past the end of the part", split, body)
|
||||
}
|
||||
if string(body)+string(rest) != full[:split] {
|
||||
t.Fatalf("split %d: body+rest %q%q does not reconstruct input", split, body, rest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartNameFileName(t *testing.T) {
|
||||
const photo = "Content-Disposition: form-data; name=\"photo\"; filename=\"beach.png\"\r\n" +
|
||||
"Content-Type: image/png\r\n"
|
||||
if got := string(PartName([]byte(photo))); got != "photo" {
|
||||
t.Errorf("want name %q, got %q", "photo", got)
|
||||
}
|
||||
if got := string(PartFileName([]byte(photo))); got != "beach.png" {
|
||||
t.Errorf("want filename %q, got %q", "beach.png", got)
|
||||
}
|
||||
const caption = "Content-Disposition: form-data; name=\"caption\"\r\n"
|
||||
if got := string(PartName([]byte(caption))); got != "caption" {
|
||||
t.Errorf("want name %q, got %q", "caption", got)
|
||||
}
|
||||
if got := PartFileName([]byte(caption)); got != nil {
|
||||
t.Errorf("want nil filename for a non file part, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The whole loop, as a caller writes it.
|
||||
func TestMultipartLoop(t *testing.T) {
|
||||
boundary := []byte(multiBoundary)
|
||||
rest := []byte(multiBody)
|
||||
var got []string
|
||||
for {
|
||||
hdr, next, err := NextPartHeader(rest, boundary)
|
||||
if err == ErrEndOfParts {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := string(PartName(hdr))
|
||||
total := 0
|
||||
rest = next
|
||||
for {
|
||||
body, next, done := NextPartBody(rest, boundary)
|
||||
total += len(body)
|
||||
rest = next
|
||||
if done {
|
||||
break
|
||||
}
|
||||
t.Fatal("part must complete within the buffer")
|
||||
}
|
||||
got = append(got, name+":"+strconv.Itoa(total))
|
||||
}
|
||||
want := "caption:8|photo:29"
|
||||
if strings.Join(got, "|") != want {
|
||||
t.Errorf("want %q, got %q", want, strings.Join(got, "|"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user