// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package runtime

import (
	stderrors "errors"
	"fmt"
	"mime/multipart"
	"net/http"
	"strings"

	"github.com/go-openapi/errors"
)

// DefaultMaxUploadFilenameLength is the default cap applied to
// FileHeader.Filename for each declared file when [BindForm] is invoked
// without an explicit [BindFormMaxFilenameLen] option.
//
// Multipart headers are allocated per part; an attacker submitting
// multi-MB filenames inflates the parser's memory footprint. 1 KiB
// matches the IETF guidance for sane filename length and is enough
// for realistic uploads.
const DefaultMaxUploadFilenameLength = 1024

// DefaultMaxUploadBodySize limits the size of the body to upload forms to 32MB.
//
// Use an explicit [BindFormMaxBody] option to change this limit.
const DefaultMaxUploadBodySize = int64(32) << 20

// filenamePreviewLen caps the byte length of the FileHeader.Filename
// preview embedded as the ParseError.Value field when the helper
// rejects a too-long filename.
const filenamePreviewLen = 32

// ValidateFilenameLength enforces the FileHeader.Filename length cap
// that [BindForm] applies via [BindFormFile] declarations. Untyped
// binder paths that fetch the file via [http.Request.FormFile]
// directly (rather than declaring the file through [BindFormFile]) call
// this to opt into the same protection.
//
// Returns nil if filename length is within maxLen or maxLen <= 0.
// Otherwise returns a [*errors.ParseError] suitable for direct return
// from a parameter binder. The error embeds a truncated preview of
// the offending filename to keep the error message bounded.
func ValidateFilenameLength(paramName, paramIn, filename string, maxLen int) error {
	if maxLen <= 0 || len(filename) <= maxLen {
		return nil
	}
	preview := filename[:min(len(filename), filenamePreviewLen)]
	return errors.NewParseError(paramName, paramIn, preview,
		fmt.Errorf("filename length %d exceeds limit %d", len(filename), maxLen))
}

// FileBinder is the per-file callback invoked by [BindForm] when a
// declared file field is present.
//
// The callback is responsible for BOTH validating the file (size, MIME, etc.) AND assigning the bound
// file to its destination — typically using:
//
//	o.FieldName = &runtime.File{Data: file, Header: header}
//
// Returning a non-nil error surfaces the error in [BindForm]'s per-field
// accumulator. Errors from the binder flow through verbatim — the
// binder is expected to produce HTTP-aware errors (e.g.
// [errors.ExceedsMaximum] from go-openapi/validate).
type FileBinder func(file multipart.File, header *multipart.FileHeader) error

// BindOption configures [BindForm]. The variadic style keeps simple
// call sites simple and lets new knobs (security caps, additional
// behaviour) be added without breaking the signature.
type BindOption func(*bindConfig)

type multipartFormLimits struct {
	maxBody        int64
	maxFiles       int
	maxFilenameLen int
}

type bindConfig struct {
	multipartFormLimits

	maxParseMemory int64
	files          []formFileSpec
}

type formFileSpec struct {
	name     string
	required bool
	bind     FileBinder
}

// BindFormMaxParseMemory caps the in-memory portion of a multipart
// body. Bytes beyond this are spilled to temporary files on disk by
// the stdlib parser. 0 (the default) defers to the stdlib's 32 MB.
//
// This option does NOT cap total body bytes — see [BindFormMaxBody]
// for that. The default body cap ([DefaultMaxUploadBodySize] = 32 MB)
// is applied even when this option is not supplied, so out of the box
// [BindForm] is bounded; callers with stricter or looser requirements
// adjust via [BindFormMaxBody].
func BindFormMaxParseMemory(n int64) BindOption {
	return func(c *bindConfig) { c.maxParseMemory = n }
}

// BindFormMaxBody caps the size of the body read from a http form before parsing.
//
// The limit is set to 32MB by default. This default limit is applied for any n=0.
//
// The limit is disabled for n<0, assuming the caller has already capped the body size upstream.
func BindFormMaxBody(n int64) BindOption {
	return func(c *bindConfig) { c.maxBody = n }
}

// BindFormMaxFiles rejects parses where the total number of file
// parts across all field names exceeds n. 0 (the default) means no
// cap. Exceeding the cap is a fatal error — [BindForm] returns
// fatal=true and no per-file binders run.
func BindFormMaxFiles(n int) BindOption {
	return func(c *bindConfig) { c.maxFiles = n }
}

// BindFormMaxFilenameLen rejects per-file headers whose Filename
// length exceeds n. 0 means no cap; the default applied when this
// option is not supplied is [DefaultMaxUploadFilenameLength]. The
// cap is a per-field bind error (non-fatal); other declared files
// still run.
func BindFormMaxFilenameLen(n int) BindOption {
	return func(c *bindConfig) { c.maxFilenameLen = n }
}

// BindFormFile declares a file field to bind under the given form
// name. If required is true and the field is absent, [BindForm]
// produces the per-field error.
//
//	errors.NewParseError(name, "formData", "", http.ErrMissingFile)
//
// If required is false, absence is silent (no error, no bind).
//
// The bind callback runs only when the field is present. It is the
// site where both validation and assignment happen — see [FileBinder].
//
// FileHeader.Filename is attacker-controlled text; the binder MUST
// NOT use it directly as a filesystem path. The helper does not
// touch the filesystem.
func BindFormFile(name string, required bool, bind FileBinder) BindOption {
	return func(c *bindConfig) {
		c.files = append(c.files, formFileSpec{
			name:     name,
			required: required,
			bind:     bind,
		})
	}
}

// BindForm parses r as multipart/form-data, falling back to
// application/x-www-form-urlencoded when the request is not
// multipart. On success, r.MultipartForm and r.PostForm are populated;
// the caller can read non-file form values via [Values](r.Form) after
// the call returns.
//
// All errors produced by BindForm itself (parse failure, missing
// required field, cap exceeded) are [*errors.ParseError] values built
// via [errors.NewParseError], matching the untyped
// middleware/parameter.go path. Errors returned by per-file binders
// flow through verbatim — binders own their HTTP-aware error shape.
//
// Per-file binders declared via [BindFormFile] run in declaration
// order after a successful parse. Their errors are accumulated and
// returned wrapped in [errors.CompositeValidationError]; the caller
// typically appends the returned err to its own []error and continues
// with non-file parameter binding.
//
// Return semantics:
//
//   - fatal=true, err!=nil: parse failure or a hard cap (e.g.
//     [BindFormMaxFiles]) was exceeded. No per-file binders ran; the
//     caller MUST return err immediately.
//   - fatal=false, err!=nil: one or more per-file binders produced
//     errors. The form parsed successfully; r.Form is populated. The
//     caller appends err to its accumulator and continues.
//   - fatal=false, err==nil: full success.
//
// fatal==true implies err!=nil.
//
// Defaults applied out of the box:
//
//   - Total body bytes capped at [DefaultMaxUploadBodySize] (32 MB)
//     via [http.MaxBytesReader]. Adjust with [BindFormMaxBody]
//     (negative n disables, when the caller has already capped the
//     body upstream).
//   - FileHeader.Filename length capped at
//     [DefaultMaxUploadFilenameLength]. Adjust with
//     [BindFormMaxFilenameLen].
//
// Caller responsibilities the helper does NOT cover:
//
//   - Set [http.Server.ReadTimeout] / [http.Server.IdleTimeout] to defend
//     against slow-read attacks.
//   - Decompress Content-Encoding: gzip request bodies upstream if
//     the API accepts them, using a size-limited reader.
//   - Treat FileHeader.Filename as untrusted user input; never use
//     it directly as a filesystem path.
func BindForm(r *http.Request, opts ...BindOption) (fatal bool, err error) {
	cfg := bindConfig{
		multipartFormLimits: multipartFormLimits{
			maxFilenameLen: DefaultMaxUploadFilenameLength,
		},
	}
	for _, opt := range opts {
		opt(&cfg)
	}

	if perr := parseFormBody(r, cfg.maxParseMemory, cfg.maxBody); perr != nil {
		// Body-cap hit gets the 413 status; everything else maps to a
		// 400 ParseError. parseFormBody returns the raw stdlib error
		// in both cases — the HTTP-aware wrapping happens here.
		var maxBytesErr *http.MaxBytesError
		if stderrors.As(perr, &maxBytesErr) {
			return true, errors.New(http.StatusRequestEntityTooLarge, "formData: %v", perr)
		}
		return true, errors.NewParseError("body", "formData", "", perr)
	}

	if cfg.maxFiles > 0 {
		if got := countFileParts(r); got > cfg.maxFiles {
			return true, errors.NewParseError("body", "formData", "",
				fmt.Errorf("multipart form contains %d file parts, exceeds limit %d", got, cfg.maxFiles))
		}
	}

	var bindErrs []error
	for _, spec := range cfg.files {
		if e := bindFormFile(r, spec, cfg.maxFilenameLen); e != nil {
			bindErrs = append(bindErrs, e)
		}
	}
	if len(bindErrs) > 0 {
		return false, errors.CompositeValidationError(bindErrs...)
	}
	return false, nil
}

// parseFormBody parses the request body. Content-Type drives the
// parser: multipart/form-data → r.ParseMultipartForm, everything else
// → r.ParseForm (stdlib's parsePostForm only actually reads the body
// when Content-Type is application/x-www-form-urlencoded, so calling
// ParseForm is safe for unrecognised types).
//
// Caveat: ParseMultipartForm calls ParseForm internally and discards its error
// when the body turns out not to be multipart, returning ErrNotMultipart instead
// — the subsequent retry then short-circuits because r.PostForm is already
// set. Content-type-based routing avoids the lossy detour.
//
// Returns the raw stdlib error on failure; the caller (BindForm)
// handles HTTP-aware wrapping (413 for MaxBytesError, 400 ParseError
// otherwise).
//
// maxMemory == 0 falls through to the stdlib default (32 MB).
// maxBody == 0 defaults to DefaultMaxUploadBodySize; maxBody < 0
// disables the body cap (caller has capped upstream).
func parseFormBody(r *http.Request, maxMemory, maxBody int64) error {
	if r.Body != nil && maxBody >= 0 {
		if maxBody == 0 {
			maxBody = DefaultMaxUploadBodySize
		}
		r.Body = http.MaxBytesReader(nil, r.Body, maxBody)
	}

	mt, _, _ := ContentType(r.Header)
	if mt == MultipartFormMime {
		//nolint:gosec // G120: false positive -- see below
		// gosec doesn't track the Body.
		// See https://github.com/securego/gosec/blob/de65614d10a6b84029e3e1215567b8ce7e490f23/testutils/g120_samples.go#L57
		return r.ParseMultipartForm(maxMemory)
	}
	return r.ParseForm()
}

func countFileParts(r *http.Request) int {
	if r.MultipartForm == nil {
		return 0
	}
	var n int
	for _, fhs := range r.MultipartForm.File {
		n += len(fhs)
	}

	return n
}

// FormFile resolves a file field from a parsed form body, transparently
// handling both content types accepted for `type: file` parameters by
// the OpenAPI 2.0 spec:
//
//   - multipart/form-data — delegates to [http.Request.FormFile].
//   - application/x-www-form-urlencoded — looks up the field in
//     r.PostForm and synthesizes a [multipart.File] backed by the
//     value bytes plus a [multipart.FileHeader] with Filename equal
//     to the field name and Size set to the byte length.
//
// Returns [http.ErrMissingFile] when the field is absent under either
// content type. Callers must have parsed the body upstream (e.g. via
// [BindForm] or [http.Request.ParseForm]) before reading from the
// urlencoded path — [http.Request.FormFile] takes care of parsing on
// the multipart path.
//
// Presence is the only criterion for binding a urlencoded file: an
// empty value (e.g. `file=`) is bound as a zero-byte file.
func FormFile(r *http.Request, name string) (multipart.File, *multipart.FileHeader, error) {
	file, header, err := r.FormFile(name)
	if err == nil {
		return file, header, nil
	}
	if !stderrors.Is(err, http.ErrNotMultipart) {
		return nil, nil, err
	}

	values, present := r.PostForm[name]
	if !present {
		return nil, nil, http.ErrMissingFile
	}
	value := values[0]
	return urlencodedFile{Reader: strings.NewReader(value)},
		&multipart.FileHeader{Filename: name, Size: int64(len(value))},
		nil
}

// urlencodedFile adapts a urlencoded form value (already buffered in
// memory by [http.Request.ParseForm]) to the [multipart.File]
// interface. The embedded [strings.Reader] supplies Read/ReadAt/Seek;
// Close is a no-op since there is no resource to release.
type urlencodedFile struct {
	*strings.Reader
}

func (urlencodedFile) Close() error { return nil }

func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error {
	file, header, err := FormFile(r, spec.name)
	if err != nil {
		if stderrors.Is(err, http.ErrMissingFile) {
			if spec.required {
				return errors.New(http.StatusBadRequest, "formData: %v", http.ErrMissingFile)
			}

			return nil
		}

		return errors.NewParseError(spec.name, "formData", "", err)
	}

	if err := ValidateFilenameLength(spec.name, "formData", header.Filename, maxFilenameLen); err != nil {
		return err
	}

	if spec.bind == nil {
		return nil
	}

	return spec.bind(file, header)
}
