// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package multipart import ( "bytes" "crypto/rand" "errors" "fmt" "io" "maps" "net/textproto" "slices" "strings" ) // A Writer generates multipart messages. type Writer struct { w io.Writer boundary string lastpart *part } // NewWriter returns a new multipart [Writer] with a random boundary, // writing to w. func NewWriter(w io.Writer) *Writer { return &Writer{ w: w, boundary: randomBoundary(), } } // Boundary returns the [Writer]'s boundary. func (w *Writer) Boundary() string { return w.boundary } // SetBoundary overrides the [Writer]'s default randomly-generated // boundary separator with an explicit value. // // SetBoundary must be called before any parts are created, may only // contain certain ASCII characters, and must be non-empty and // at most 70 bytes long. func (w *Writer) SetBoundary(boundary string) error { if w.lastpart != nil { return errors.New("mime: SetBoundary called after write") } // rfc2046#section-5.1.1 if len(boundary) < 1 || len(boundary) > 70 { return errors.New("mime: invalid boundary length") } end := len(boundary) - 1 for i, b := range boundary { if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' { continue } switch b { case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?': continue case ' ': if i != end { continue } } return errors.New("mime: invalid boundary character") } w.boundary = boundary return nil } // FormDataContentType returns the Content-Type for an HTTP // multipart/form-data with this [Writer]'s Boundary. func (w *Writer) FormDataContentType() string { b := w.boundary // We must quote the boundary if it contains any of the // tspecials characters defined by RFC 2045, or space. if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) { b = `"` + b + `"` } return "multipart/form-data; boundary=" + b } func randomBoundary() string { var buf [30]byte _, err := io.ReadFull(rand.Reader, buf[:]) if err != nil { panic(err) } return fmt.Sprintf("%x", buf[:]) } // CreatePart creates a new multipart section with the provided // header. The body of the part should be written to the returned // [Writer]. After calling CreatePart, any previous part may no longer // be written to. func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error) { if w.lastpart != nil { if err := w.lastpart.close(); err != nil { return nil, err } } var b bytes.Buffer if w.lastpart != nil { fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary) } else { fmt.Fprintf(&b, "--%s\r\n", w.boundary) } for _, k := range slices.Sorted(maps.Keys(header)) { for _, v := range header[k] { fmt.Fprintf(&b, "%s: %s\r\n", k, v) } } fmt.Fprintf(&b, "\r\n") _, err := io.Copy(w.w, &b) if err != nil { return nil, err } p := &part{ mw: w, } w.lastpart = p return p, nil } var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } // CreateFormFile is a convenience wrapper around [Writer.CreatePart]. It creates // a new form-data header with the provided field name and file name. func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) { h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename))) h.Set("Content-Type", "application/octet-stream") return w.CreatePart(h) } // CreateFormField calls [Writer.CreatePart] with a header using the // given field name. func (w *Writer) CreateFormField(fieldname string) (io.Writer, error) { h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname))) return w.CreatePart(h) } // WriteField calls [Writer.CreateFormField] and then writes the given value. func (w *Writer) WriteField(fieldname, value string) error { p, err := w.CreateFormField(fieldname) if err != nil { return err } _, err = p.Write([]byte(value)) return err } // Close finishes the multipart message and writes the trailing // boundary end line to the output. func (w *Writer) Close() error { if w.lastpart != nil { if err := w.lastpart.close(); err != nil { return err } w.lastpart = nil } _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary) return err } type part struct { mw *Writer closed bool we error // last error that occurred writing } func (p *part) close() error { p.closed = true return p.we } func (p *part) Write(d []byte) (n int, err error) { if p.closed { return 0, errors.New("multipart: can't write to finished part") } n, err = p.mw.w.Write(d) if err != nil { p.we = err } return }