feat: release managed service generation and contract diagnostics
This commit is contained in:
173
httpx/multipart.go
Normal file
173
httpx/multipart.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user