Files
go-omni/httpx/multipart.go

174 lines
4.1 KiB
Go

package httpx
import (
"bytes"
"errors"
"io"
"mime/multipart"
"net/http"
"net/textproto"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
)
// MultipartReader streams request parts and retains parsing failures, so a
// partially consumed or invalid upload cannot later complete successfully.
// It belongs to a single request and must be read sequentially.
type MultipartReader struct {
reader *multipart.Reader
input *multipartInput
current *MultipartPart
err error
done bool
}
// NextPart closes the preceding part and returns the next part. Like the
// standard library, it decodes quoted-printable content; NextRawPart does not.
// Only a valid final boundary yields io.EOF; malformed input yields an error
// with status 400. Body limits remain detectable as *http.MaxBytesError.
func (r *MultipartReader) NextPart() (*MultipartPart, error) { return r.next(false) }
func (r *MultipartReader) NextRawPart() (*MultipartPart, error) { return r.next(true) }
func (r *MultipartReader) next(raw bool) (*MultipartPart, error) {
if r.err != nil {
return nil, r.err
}
if r.done {
return nil, io.EOF
}
if r.current != nil {
if err := r.current.Close(); err != nil {
return nil, err
}
}
var part *multipart.Part
var err error
if raw {
part, err = r.reader.NextRawPart()
} else {
part, err = r.reader.NextPart()
}
if err == io.EOF {
// The standard parser also returns bare EOF for truncated MIME
// headers. Require an actual closing delimiter before treating EOF
// as successful completion.
if !r.input.ended() {
return nil, r.record(io.ErrUnexpectedEOF)
}
r.done = true
return nil, io.EOF
}
if err != nil {
return nil, r.record(err)
}
r.current = &MultipartPart{Header: part.Header, part: part, owner: r}
return r.current, nil
}
func (r *MultipartReader) record(err error) error {
if err == nil || err == io.EOF {
return err
}
if r.err == nil {
var limit *http.MaxBytesError
if errors.As(err, &limit) {
r.err = err
} else {
r.err = apierrors.New(400, "INVALID_MULTIPART", "invalid multipart request body").WithCause(err)
}
}
return r.err
}
// MultipartPart is a streaming request part. Close drains unread content and
// reports read/parse errors; it never buffers the whole file or creates a file.
type MultipartPart struct {
Header textproto.MIMEHeader
part *multipart.Part
owner *MultipartReader
closed bool
err error
}
func (p *MultipartPart) FormName() string { return p.part.FormName() }
func (p *MultipartPart) FileName() string { return p.part.FileName() }
func (p *MultipartPart) Read(buf []byte) (int, error) {
if p.closed {
if p.err != nil {
return 0, p.err
}
return 0, io.EOF
}
n, err := p.part.Read(buf)
return n, p.owner.record(err)
}
func (p *MultipartPart) Close() error {
if !p.closed {
_, p.err = io.Copy(io.Discard, p)
p.closed = true
}
return p.err
}
// multipartInput observes closing-delimiter lines without buffering file
// content or changing bytes. The MIME parser still validates their syntax and
// placement. Keep only a prefix offset, including across reader boundaries.
type multipartInput struct {
io.Reader
boundary string
matched int
carriage, final, eof bool
}
func (r *multipartInput) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
r.observe(p[:n])
if err == io.EOF {
r.eof = true
}
return n, err
}
func (r *multipartInput) ended() bool {
return r.final || (r.eof && r.matched == len(r.boundary) && !r.carriage)
}
func (r *multipartInput) observe(data []byte) {
for len(data) > 0 && !r.final {
if r.matched < 0 {
newline := bytes.IndexByte(data, '\n')
if newline < 0 {
return
}
data, r.matched, r.carriage = data[newline+1:], 0, false
continue
}
c := data[0]
data = data[1:]
if r.matched < len(r.boundary) {
if c == r.boundary[r.matched] {
r.matched++
continue
}
} else {
switch {
case c == '\n':
r.final = true
continue
case !r.carriage && (c == ' ' || c == '\t'):
continue
case !r.carriage && c == '\r':
r.carriage = true
continue
}
}
r.matched, r.carriage = -1, false
if c == '\n' {
r.matched = 0
}
}
}