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 } } }