// Copyright 2009 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.

// HTTP file system request handler

package http

import (
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
)

// A Dir implements [FileSystem] using the native file system restricted to a
// specific directory tree.
//
// While the [FileSystem.Open] method takes '/'-separated paths, a Dir's string
// value is a directory path on the native file system, not a URL, so it is separated
// by [filepath.Separator], which isn't necessarily '/'.
//
// Note that Dir could expose sensitive files and directories. Dir will follow
// symlinks pointing out of the directory tree, which can be especially dangerous
// if serving from a directory in which users are able to create arbitrary symlinks.
// Dir will also allow access to files and directories starting with a period,
// which could expose sensitive directories like .git or sensitive files like
// .htpasswd. To exclude files with a leading period, remove the files/directories
// from the server or create a custom FileSystem implementation.
//
// An empty Dir is treated as ".".
type Dir string

// mapOpenError maps the provided non-nil error from opening name
// to a possibly better non-nil error. In particular, it turns OS-specific errors
// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552.
func mapOpenError( error,  string,  rune,  func(string) (fs.FileInfo, error)) error {
	if errors.Is(, fs.ErrNotExist) || errors.Is(, fs.ErrPermission) {
		return 
	}

	 := strings.Split(, string())
	for  := range  {
		if [] == "" {
			continue
		}
		,  := (strings.Join([:+1], string()))
		if  != nil {
			return 
		}
		if !.IsDir() {
			return fs.ErrNotExist
		}
	}
	return 
}

// Open implements [FileSystem] using [os.Open], opening files for reading rooted
// and relative to the directory d.
func ( Dir) ( string) (File, error) {
	 := path.Clean("/" + )[1:]
	if  == "" {
		 = "."
	}
	,  := filepath.Localize()
	if  != nil {
		return nil, errors.New("http: invalid or unsafe file path")
	}
	 := string()
	if  == "" {
		 = "."
	}
	 := filepath.Join(, )
	,  := os.Open()
	if  != nil {
		return nil, mapOpenError(, , filepath.Separator, os.Stat)
	}
	return , nil
}

// A FileSystem implements access to a collection of named files.
// The elements in a file path are separated by slash ('/', U+002F)
// characters, regardless of host operating system convention.
// See the [FileServer] function to convert a FileSystem to a [Handler].
//
// This interface predates the [fs.FS] interface, which can be used instead:
// the [FS] adapter function converts an fs.FS to a FileSystem.
type FileSystem interface {
	Open(name string) (File, error)
}

// A File is returned by a [FileSystem]'s Open method and can be
// served by the [FileServer] implementation.
//
// The methods should behave the same as those on an [*os.File].
type File interface {
	io.Closer
	io.Reader
	io.Seeker
	Readdir(count int) ([]fs.FileInfo, error)
	Stat() (fs.FileInfo, error)
}

type anyDirs interface {
	len() int
	name(i int) string
	isDir(i int) bool
}

type fileInfoDirs []fs.FileInfo

func ( fileInfoDirs) () int          { return len() }
func ( fileInfoDirs) ( int) bool  { return [].IsDir() }
func ( fileInfoDirs) ( int) string { return [].Name() }

type dirEntryDirs []fs.DirEntry

func ( dirEntryDirs) () int          { return len() }
func ( dirEntryDirs) ( int) bool  { return [].IsDir() }
func ( dirEntryDirs) ( int) string { return [].Name() }

func dirList( ResponseWriter,  *Request,  File) {
	// Prefer to use ReadDir instead of Readdir,
	// because the former doesn't require calling
	// Stat on every entry of a directory on Unix.
	var  anyDirs
	var  error
	if ,  := .(fs.ReadDirFile);  {
		var  dirEntryDirs
		,  = .ReadDir(-1)
		 = 
	} else {
		var  fileInfoDirs
		,  = .Readdir(-1)
		 = 
	}

	if  != nil {
		logf(, "http: error reading directory: %v", )
		Error(, "Error reading directory", StatusInternalServerError)
		return
	}
	sort.Slice(, func(,  int) bool { return .name() < .name() })

	.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(, "<!doctype html>\n")
	fmt.Fprintf(, "<meta name=\"viewport\" content=\"width=device-width\">\n")
	fmt.Fprintf(, "<pre>\n")
	for ,  := 0, .len();  < ; ++ {
		 := .name()
		if .isDir() {
			 += "/"
		}
		// name may contain '?' or '#', which must be escaped to remain
		// part of the URL path, and not indicate the start of a query
		// string or fragment.
		 := url.URL{Path: }
		fmt.Fprintf(, "<a href=\"%s\">%s</a>\n", .String(), htmlReplacer.Replace())
	}
	fmt.Fprintf(, "</pre>\n")
}

// GODEBUG=httpservecontentkeepheaders=1 restores the pre-1.23 behavior of not deleting
// Cache-Control, Content-Encoding, Etag, or Last-Modified headers on ServeContent errors.
var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")

// serveError serves an error from ServeFile, ServeFileFS, and ServeContent.
// Because those can all be configured by the caller by setting headers like
// Etag, Last-Modified, and Cache-Control to send on a successful response,
// the error path needs to clear them, since they may not be meant for errors.
func serveError( ResponseWriter,  string,  int) {
	 := .Header()

	 := false
	for ,  := range []string{
		"Cache-Control",
		"Content-Encoding",
		"Etag",
		"Last-Modified",
	} {
		if !.has() {
			continue
		}
		if httpservecontentkeepheaders.Value() == "1" {
			 = true
		} else {
			.Del()
		}
	}
	if  {
		httpservecontentkeepheaders.IncNonDefault()
	}

	Error(, , )
}

// ServeContent replies to the request using the content in the
// provided ReadSeeker. The main benefit of ServeContent over [io.Copy]
// is that it handles Range requests properly, sets the MIME type, and
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
// and If-Range requests.
//
// If the response's Content-Type header is not set, ServeContent
// first tries to deduce the type from name's file extension and,
// if that fails, falls back to reading the first block of the content
// and passing it to [DetectContentType].
// The name is otherwise unused; in particular it can be empty and is
// never sent in the response.
//
// If modtime is not the zero time or Unix epoch, ServeContent
// includes it in a Last-Modified header in the response. If the
// request includes an If-Modified-Since header, ServeContent uses
// modtime to decide whether the content needs to be sent at all.
//
// The content's Seek method must work: ServeContent uses
// a seek to the end of the content to determine its size.
// Note that [*os.File] implements the [io.ReadSeeker] interface.
//
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
//
// If an error occurs when serving the request (for example, when
// handling an invalid range request), ServeContent responds with an
// error message. By default, ServeContent strips the Cache-Control,
// Content-Encoding, ETag, and Last-Modified headers from error responses.
// The GODEBUG setting httpservecontentkeepheaders=1 causes ServeContent
// to preserve these headers.
func ( ResponseWriter,  *Request,  string,  time.Time,  io.ReadSeeker) {
	 := func() (int64, error) {
		,  := .Seek(0, io.SeekEnd)
		if  != nil {
			return 0, errSeeker
		}
		_,  = .Seek(0, io.SeekStart)
		if  != nil {
			return 0, errSeeker
		}
		return , nil
	}
	serveContent(, , , , , )
}

// errSeeker is returned by ServeContent's sizeFunc when the content
// doesn't seek properly. The underlying Seeker's error text isn't
// included in the sizeFunc reply so it's not sent over HTTP to end
// users.
var errSeeker = errors.New("seeker can't seek")

// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of
// all of the byte-range-spec values is greater than the content size.
var errNoOverlap = errors.New("invalid range: failed to overlap")

// if name is empty, filename is unknown. (used for mime type, before sniffing)
// if modtime.IsZero(), modtime is unknown.
// content must be seeked to the beginning of the file.
// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
func serveContent( ResponseWriter,  *Request,  string,  time.Time,  func() (int64, error),  io.ReadSeeker) {
	setLastModified(, )
	,  := checkPreconditions(, , )
	if  {
		return
	}

	 := StatusOK

	// If Content-Type isn't set, use the file's extension to find it, but
	// if the Content-Type is unset explicitly, do not sniff the type.
	,  := .Header()["Content-Type"]
	var  string
	if ! {
		 = mime.TypeByExtension(filepath.Ext())
		if  == "" {
			// read a chunk to decide between utf-8 text and binary
			var  [sniffLen]byte
			,  := io.ReadFull(, [:])
			 = DetectContentType([:])
			,  := .Seek(0, io.SeekStart) // rewind to output whole file
			if  != nil {
				serveError(, "seeker can't seek", StatusInternalServerError)
				return
			}
		}
		.Header().Set("Content-Type", )
	} else if len() > 0 {
		 = [0]
	}

	,  := ()
	if  != nil {
		serveError(, .Error(), StatusInternalServerError)
		return
	}
	if  < 0 {
		// Should never happen but just to be sure
		serveError(, "negative content size computed", StatusInternalServerError)
		return
	}

	// handle Content-Range header.
	 := 
	var  io.Reader = 
	,  := parseRange(, )
	switch  {
	case nil:
	case errNoOverlap:
		if  == 0 {
			// Some clients add a Range header to all requests to
			// limit the size of the response. If the file is empty,
			// ignore the range header and respond with a 200 rather
			// than a 416.
			 = nil
			break
		}
		.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", ))
		fallthrough
	default:
		serveError(, .Error(), StatusRequestedRangeNotSatisfiable)
		return
	}

	if sumRangesSize() >  {
		// The total number of bytes in all the ranges
		// is larger than the size of the file by
		// itself, so this is probably an attack, or a
		// dumb client. Ignore the range request.
		 = nil
	}
	switch {
	case len() == 1:
		// RFC 7233, Section 4.1:
		// "If a single part is being transferred, the server
		// generating the 206 response MUST generate a
		// Content-Range header field, describing what range
		// of the selected representation is enclosed, and a
		// payload consisting of the range.
		// ...
		// A server MUST NOT generate a multipart response to
		// a request for a single range, since a client that
		// does not request multiple parts might not support
		// multipart responses."
		 := [0]
		if ,  := .Seek(.start, io.SeekStart);  != nil {
			serveError(, .Error(), StatusRequestedRangeNotSatisfiable)
			return
		}
		 = .length
		 = StatusPartialContent
		.Header().Set("Content-Range", .contentRange())
	case len() > 1:
		 = rangesMIMESize(, , )
		 = StatusPartialContent

		,  := io.Pipe()
		 := multipart.NewWriter()
		.Header().Set("Content-Type", "multipart/byteranges; boundary="+.Boundary())
		 = 
		defer .Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
		go func() {
			for ,  := range  {
				,  := .CreatePart(.mimeHeader(, ))
				if  != nil {
					.CloseWithError()
					return
				}
				if ,  := .Seek(.start, io.SeekStart);  != nil {
					.CloseWithError()
					return
				}
				if ,  := io.CopyN(, , .length);  != nil {
					.CloseWithError()
					return
				}
			}
			.Close()
			.Close()
		}()
	}

	.Header().Set("Accept-Ranges", "bytes")

	// We should be able to unconditionally set the Content-Length here.
	//
	// However, there is a pattern observed in the wild that this breaks:
	// The user wraps the ResponseWriter in one which gzips data written to it,
	// and sets "Content-Encoding: gzip".
	//
	// The user shouldn't be doing this; the serveContent path here depends
	// on serving seekable data with a known length. If you want to compress
	// on the fly, then you shouldn't be using ServeFile/ServeContent, or
	// you should compress the entire file up-front and provide a seekable
	// view of the compressed data.
	//
	// However, since we've observed this pattern in the wild, and since
	// setting Content-Length here breaks code that mostly-works today,
	// skip setting Content-Length if the user set Content-Encoding.
	//
	// If this is a range request, always set Content-Length.
	// If the user isn't changing the bytes sent in the ResponseWrite,
	// the Content-Length will be correct.
	// If the user is changing the bytes sent, then the range request wasn't
	// going to work properly anyway and we aren't worse off.
	//
	// A possible future improvement on this might be to look at the type
	// of the ResponseWriter, and always set Content-Length if it's one
	// that we recognize.
	if len() > 0 || .Header().Get("Content-Encoding") == "" {
		.Header().Set("Content-Length", strconv.FormatInt(, 10))
	}
	.WriteHeader()

	if .Method != "HEAD" {
		io.CopyN(, , )
	}
}

// scanETag determines if a syntactically valid ETag is present at s. If so,
// the ETag and remaining text after consuming ETag is returned. Otherwise,
// it returns "", "".
func scanETag( string) ( string,  string) {
	 = textproto.TrimString()
	 := 0
	if strings.HasPrefix(, "W/") {
		 = 2
	}
	if len([:]) < 2 || [] != '"' {
		return "", ""
	}
	// ETag is either W/"text" or "text".
	// See RFC 7232 2.3.
	for  :=  + 1;  < len(); ++ {
		 := []
		switch {
		// Character values allowed in ETags.
		case  == 0x21 ||  >= 0x23 &&  <= 0x7E ||  >= 0x80:
		case  == '"':
			return [:+1], [+1:]
		default:
			return "", ""
		}
	}
	return "", ""
}

// etagStrongMatch reports whether a and b match using strong ETag comparison.
// Assumes a and b are valid ETags.
func etagStrongMatch(,  string) bool {
	return  ==  &&  != "" && [0] == '"'
}

// etagWeakMatch reports whether a and b match using weak ETag comparison.
// Assumes a and b are valid ETags.
func etagWeakMatch(,  string) bool {
	return strings.TrimPrefix(, "W/") == strings.TrimPrefix(, "W/")
}

// condResult is the result of an HTTP request precondition check.
// See https://tools.ietf.org/html/rfc7232 section 3.
type condResult int

const (
	condNone condResult = iota
	condTrue
	condFalse
)

func checkIfMatch( ResponseWriter,  *Request) condResult {
	 := .Header.Get("If-Match")
	if  == "" {
		return condNone
	}
	for {
		 = textproto.TrimString()
		if len() == 0 {
			break
		}
		if [0] == ',' {
			 = [1:]
			continue
		}
		if [0] == '*' {
			return condTrue
		}
		,  := scanETag()
		if  == "" {
			break
		}
		if etagStrongMatch(, .Header().get("Etag")) {
			return condTrue
		}
		 = 
	}

	return condFalse
}

func checkIfUnmodifiedSince( *Request,  time.Time) condResult {
	 := .Header.Get("If-Unmodified-Since")
	if  == "" || isZeroTime() {
		return condNone
	}
	,  := ParseTime()
	if  != nil {
		return condNone
	}

	// The Last-Modified header truncates sub-second precision so
	// the modtime needs to be truncated too.
	 = .Truncate(time.Second)
	if  := .Compare();  <= 0 {
		return condTrue
	}
	return condFalse
}

func checkIfNoneMatch( ResponseWriter,  *Request) condResult {
	 := .Header.get("If-None-Match")
	if  == "" {
		return condNone
	}
	 := 
	for {
		 = textproto.TrimString()
		if len() == 0 {
			break
		}
		if [0] == ',' {
			 = [1:]
			continue
		}
		if [0] == '*' {
			return condFalse
		}
		,  := scanETag()
		if  == "" {
			break
		}
		if etagWeakMatch(, .Header().get("Etag")) {
			return condFalse
		}
		 = 
	}
	return condTrue
}

func checkIfModifiedSince( *Request,  time.Time) condResult {
	if .Method != "GET" && .Method != "HEAD" {
		return condNone
	}
	 := .Header.Get("If-Modified-Since")
	if  == "" || isZeroTime() {
		return condNone
	}
	,  := ParseTime()
	if  != nil {
		return condNone
	}
	// The Last-Modified header truncates sub-second precision so
	// the modtime needs to be truncated too.
	 = .Truncate(time.Second)
	if  := .Compare();  <= 0 {
		return condFalse
	}
	return condTrue
}

func checkIfRange( ResponseWriter,  *Request,  time.Time) condResult {
	if .Method != "GET" && .Method != "HEAD" {
		return condNone
	}
	 := .Header.get("If-Range")
	if  == "" {
		return condNone
	}
	,  := scanETag()
	if  != "" {
		if etagStrongMatch(, .Header().Get("Etag")) {
			return condTrue
		} else {
			return condFalse
		}
	}
	// The If-Range value is typically the ETag value, but it may also be
	// the modtime date. See golang.org/issue/8367.
	if .IsZero() {
		return condFalse
	}
	,  := ParseTime()
	if  != nil {
		return condFalse
	}
	if .Unix() == .Unix() {
		return condTrue
	}
	return condFalse
}

var unixEpochTime = time.Unix(0, 0)

// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
func isZeroTime( time.Time) bool {
	return .IsZero() || .Equal(unixEpochTime)
}

func setLastModified( ResponseWriter,  time.Time) {
	if !isZeroTime() {
		.Header().Set("Last-Modified", .UTC().Format(TimeFormat))
	}
}

func writeNotModified( ResponseWriter) {
	// RFC 7232 section 4.1:
	// a sender SHOULD NOT generate representation metadata other than the
	// above listed fields unless said metadata exists for the purpose of
	// guiding cache updates (e.g., Last-Modified might be useful if the
	// response does not have an ETag field).
	 := .Header()
	delete(, "Content-Type")
	delete(, "Content-Length")
	delete(, "Content-Encoding")
	if .Get("Etag") != "" {
		delete(, "Last-Modified")
	}
	.WriteHeader(StatusNotModified)
}

// checkPreconditions evaluates request preconditions and reports whether a precondition
// resulted in sending StatusNotModified or StatusPreconditionFailed.
func checkPreconditions( ResponseWriter,  *Request,  time.Time) ( bool,  string) {
	// This function carefully follows RFC 7232 section 6.
	 := checkIfMatch(, )
	if  == condNone {
		 = checkIfUnmodifiedSince(, )
	}
	if  == condFalse {
		.WriteHeader(StatusPreconditionFailed)
		return true, ""
	}
	switch checkIfNoneMatch(, ) {
	case condFalse:
		if .Method == "GET" || .Method == "HEAD" {
			writeNotModified()
			return true, ""
		} else {
			.WriteHeader(StatusPreconditionFailed)
			return true, ""
		}
	case condNone:
		if checkIfModifiedSince(, ) == condFalse {
			writeNotModified()
			return true, ""
		}
	}

	 = .Header.get("Range")
	if  != "" && checkIfRange(, , ) == condFalse {
		 = ""
	}
	return false, 
}

// name is '/'-separated, not filepath.Separator.
func serveFile( ResponseWriter,  *Request,  FileSystem,  string,  bool) {
	const  = "/index.html"

	// redirect .../index.html to .../
	// can't use Redirect() because that would make the path absolute,
	// which would be a problem running under StripPrefix
	if strings.HasSuffix(.URL.Path, ) {
		localRedirect(, , "./")
		return
	}

	,  := .Open()
	if  != nil {
		,  := toHTTPError()
		serveError(, , )
		return
	}
	defer .Close()

	,  := .Stat()
	if  != nil {
		,  := toHTTPError()
		serveError(, , )
		return
	}

	if  {
		// redirect to canonical path: / at end of directory url
		// r.URL.Path always begins with /
		 := .URL.Path
		if .IsDir() {
			if [len()-1] != '/' {
				localRedirect(, , path.Base()+"/")
				return
			}
		} else if [len()-1] == '/' {
			 := path.Base()
			if  == "/" ||  == "." {
				// The FileSystem maps a path like "/" or "/./" to a file instead of a directory.
				 := "http: attempting to traverse a non-directory"
				serveError(, , StatusInternalServerError)
				return
			}
			localRedirect(, , "../"+)
			return
		}
	}

	if .IsDir() {
		 := .URL.Path
		// redirect if the directory name doesn't end in a slash
		if  == "" || [len()-1] != '/' {
			localRedirect(, , path.Base()+"/")
			return
		}

		// use contents of index.html for directory, if present
		 := strings.TrimSuffix(, "/") + 
		,  := .Open()
		if  == nil {
			defer .Close()
			,  := .Stat()
			if  == nil {
				 = 
				 = 
			}
		}
	}

	// Still a directory? (we didn't find an index.html file)
	if .IsDir() {
		if checkIfModifiedSince(, .ModTime()) == condFalse {
			writeNotModified()
			return
		}
		setLastModified(, .ModTime())
		dirList(, , )
		return
	}

	// serveContent will check modification time
	 := func() (int64, error) { return .Size(), nil }
	serveContent(, , .Name(), .ModTime(), , )
}

// toHTTPError returns a non-specific HTTP error message and status code
// for a given non-nil error value. It's important that toHTTPError does not
// actually return err.Error(), since msg and httpStatus are returned to users,
// and historically Go's ServeContent always returned just "404 Not Found" for
// all errors. We don't want to start leaking information in error messages.
func toHTTPError( error) ( string,  int) {
	if errors.Is(, fs.ErrNotExist) {
		return "404 page not found", StatusNotFound
	}
	if errors.Is(, fs.ErrPermission) {
		return "403 Forbidden", StatusForbidden
	}
	// Default:
	return "500 Internal Server Error", StatusInternalServerError
}

// localRedirect gives a Moved Permanently response.
// It does not convert relative paths to absolute paths like Redirect does.
func localRedirect( ResponseWriter,  *Request,  string) {
	if  := .URL.RawQuery;  != "" {
		 += "?" + 
	}
	.Header().Set("Location", )
	.WriteHeader(StatusMovedPermanently)
}

// ServeFile replies to the request with the contents of the named
// file or directory.
//
// If the provided file or directory name is a relative path, it is
// interpreted relative to the current directory and may ascend to
// parent directories. If the provided name is constructed from user
// input, it should be sanitized before calling [ServeFile].
//
// As a precaution, ServeFile will reject requests where r.URL.Path
// contains a ".." path element; this protects against callers who
// might unsafely use [filepath.Join] on r.URL.Path without sanitizing
// it and then use that filepath.Join result as the name argument.
//
// As another special case, ServeFile redirects any request where r.URL.Path
// ends in "/index.html" to the same path, without the final
// "index.html". To avoid such redirects either modify the path or
// use [ServeContent].
//
// Outside of those two special cases, ServeFile does not use
// r.URL.Path for selecting the file or directory to serve; only the
// file or directory provided in the name argument is used.
func ( ResponseWriter,  *Request,  string) {
	if containsDotDot(.URL.Path) {
		// Too many programs use r.URL.Path to construct the argument to
		// serveFile. Reject the request under the assumption that happened
		// here and ".." may not be wanted.
		// Note that name might not contain "..", for example if code (still
		// incorrectly) used filepath.Join(myDir, r.URL.Path).
		serveError(, "invalid URL path", StatusBadRequest)
		return
	}
	,  := filepath.Split()
	serveFile(, , Dir(), , false)
}

// ServeFileFS replies to the request with the contents
// of the named file or directory from the file system fsys.
// The files provided by fsys must implement [io.Seeker].
//
// If the provided name is constructed from user input, it should be
// sanitized before calling [ServeFileFS].
//
// As a precaution, ServeFileFS will reject requests where r.URL.Path
// contains a ".." path element; this protects against callers who
// might unsafely use [filepath.Join] on r.URL.Path without sanitizing
// it and then use that filepath.Join result as the name argument.
//
// As another special case, ServeFileFS redirects any request where r.URL.Path
// ends in "/index.html" to the same path, without the final
// "index.html". To avoid such redirects either modify the path or
// use [ServeContent].
//
// Outside of those two special cases, ServeFileFS does not use
// r.URL.Path for selecting the file or directory to serve; only the
// file or directory provided in the name argument is used.
func ( ResponseWriter,  *Request,  fs.FS,  string) {
	if containsDotDot(.URL.Path) {
		// Too many programs use r.URL.Path to construct the argument to
		// serveFile. Reject the request under the assumption that happened
		// here and ".." may not be wanted.
		// Note that name might not contain "..", for example if code (still
		// incorrectly) used filepath.Join(myDir, r.URL.Path).
		serveError(, "invalid URL path", StatusBadRequest)
		return
	}
	serveFile(, , FS(), , false)
}

func containsDotDot( string) bool {
	if !strings.Contains(, "..") {
		return false
	}
	for ,  := range strings.FieldsFunc(, isSlashRune) {
		if  == ".." {
			return true
		}
	}
	return false
}

func isSlashRune( rune) bool { return  == '/' ||  == '\\' }

type fileHandler struct {
	root FileSystem
}

type ioFS struct {
	fsys fs.FS
}

type ioFile struct {
	file fs.File
}

func ( ioFS) ( string) (File, error) {
	if  == "/" {
		 = "."
	} else {
		 = strings.TrimPrefix(, "/")
	}
	,  := .fsys.Open()
	if  != nil {
		return nil, mapOpenError(, , '/', func( string) (fs.FileInfo, error) {
			return fs.Stat(.fsys, )
		})
	}
	return ioFile{}, nil
}

func ( ioFile) () error               { return .file.Close() }
func ( ioFile) ( []byte) (int, error) { return .file.Read() }
func ( ioFile) () (fs.FileInfo, error) { return .file.Stat() }

var errMissingSeek = errors.New("io.File missing Seek method")
var errMissingReadDir = errors.New("io.File directory missing ReadDir method")

func ( ioFile) ( int64,  int) (int64, error) {
	,  := .file.(io.Seeker)
	if ! {
		return 0, errMissingSeek
	}
	return .Seek(, )
}

func ( ioFile) ( int) ([]fs.DirEntry, error) {
	,  := .file.(fs.ReadDirFile)
	if ! {
		return nil, errMissingReadDir
	}
	return .ReadDir()
}

func ( ioFile) ( int) ([]fs.FileInfo, error) {
	,  := .file.(fs.ReadDirFile)
	if ! {
		return nil, errMissingReadDir
	}
	var  []fs.FileInfo
	for {
		,  := .ReadDir( - len())
		for ,  := range  {
			,  := .Info()
			if  != nil {
				// Pretend it doesn't exist, like (*os.File).Readdir does.
				continue
			}
			 = append(, )
		}
		if  != nil {
			return , 
		}
		if  < 0 || len() >=  {
			break
		}
	}
	return , nil
}

// FS converts fsys to a [FileSystem] implementation,
// for use with [FileServer] and [NewFileTransport].
// The files provided by fsys must implement [io.Seeker].
func ( fs.FS) FileSystem {
	return ioFS{}
}

// FileServer returns a handler that serves HTTP requests
// with the contents of the file system rooted at root.
//
// As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final
// "index.html".
//
// To use the operating system's file system implementation,
// use [http.Dir]:
//
//	http.Handle("/", http.FileServer(http.Dir("/tmp")))
//
// To use an [fs.FS] implementation, use [http.FileServerFS] instead.
func ( FileSystem) Handler {
	return &fileHandler{}
}

// FileServerFS returns a handler that serves HTTP requests
// with the contents of the file system fsys.
// The files provided by fsys must implement [io.Seeker].
//
// As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final
// "index.html".
//
//	http.Handle("/", http.FileServerFS(fsys))
func ( fs.FS) Handler {
	return FileServer(FS())
}

func ( *fileHandler) ( ResponseWriter,  *Request) {
	 := .URL.Path
	if !strings.HasPrefix(, "/") {
		 = "/" + 
		.URL.Path = 
	}
	serveFile(, , .root, path.Clean(), true)
}

// httpRange specifies the byte range to be sent to the client.
type httpRange struct {
	start, length int64
}

func ( httpRange) ( int64) string {
	return fmt.Sprintf("bytes %d-%d/%d", .start, .start+.length-1, )
}

func ( httpRange) ( string,  int64) textproto.MIMEHeader {
	return textproto.MIMEHeader{
		"Content-Range": {.contentRange()},
		"Content-Type":  {},
	}
}

// parseRange parses a Range header string as per RFC 7233.
// errNoOverlap is returned if none of the ranges overlap.
func parseRange( string,  int64) ([]httpRange, error) {
	if  == "" {
		return nil, nil // header not present
	}
	const  = "bytes="
	if !strings.HasPrefix(, ) {
		return nil, errors.New("invalid range")
	}
	var  []httpRange
	 := false
	for ,  := range strings.Split([len():], ",") {
		 = textproto.TrimString()
		if  == "" {
			continue
		}
		, ,  := strings.Cut(, "-")
		if ! {
			return nil, errors.New("invalid range")
		}
		,  = textproto.TrimString(), textproto.TrimString()
		var  httpRange
		if  == "" {
			// If no start is specified, end specifies the
			// range start relative to the end of the file,
			// and we are dealing with <suffix-length>
			// which has to be a non-negative integer as per
			// RFC 7233 Section 2.1 "Byte-Ranges".
			if  == "" || [0] == '-' {
				return nil, errors.New("invalid range")
			}
			,  := strconv.ParseInt(, 10, 64)
			if  < 0 ||  != nil {
				return nil, errors.New("invalid range")
			}
			if  >  {
				 = 
			}
			.start =  - 
			.length =  - .start
		} else {
			,  := strconv.ParseInt(, 10, 64)
			if  != nil ||  < 0 {
				return nil, errors.New("invalid range")
			}
			if  >=  {
				// If the range begins after the size of the content,
				// then it does not overlap.
				 = true
				continue
			}
			.start = 
			if  == "" {
				// If no end is specified, range extends to end of the file.
				.length =  - .start
			} else {
				,  := strconv.ParseInt(, 10, 64)
				if  != nil || .start >  {
					return nil, errors.New("invalid range")
				}
				if  >=  {
					 =  - 1
				}
				.length =  - .start + 1
			}
		}
		 = append(, )
	}
	if  && len() == 0 {
		// The specified ranges did not overlap with the content.
		return nil, errNoOverlap
	}
	return , nil
}

// countingWriter counts how many bytes have been written to it.
type countingWriter int64

func ( *countingWriter) ( []byte) ( int,  error) {
	* += countingWriter(len())
	return len(), nil
}

// rangesMIMESize returns the number of bytes it takes to encode the
// provided ranges as a multipart response.
func rangesMIMESize( []httpRange,  string,  int64) ( int64) {
	var  countingWriter
	 := multipart.NewWriter(&)
	for ,  := range  {
		.CreatePart(.mimeHeader(, ))
		 += .length
	}
	.Close()
	 += int64()
	return
}

func sumRangesSize( []httpRange) ( int64) {
	for ,  := range  {
		 += .length
	}
	return
}