gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/gmhttp/fs_test.go (about)

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package gmhttp_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"mime"
    15  	"mime/multipart"
    16  	"net"
    17  	"net/url"
    18  	"os"
    19  	"os/exec"
    20  	"path"
    21  	"path/filepath"
    22  	"reflect"
    23  	"regexp"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	. "gitee.com/ks-custle/core-gm/gmhttp"
    30  	"gitee.com/ks-custle/core-gm/gmhttp/httptest"
    31  )
    32  
    33  const (
    34  	testFile    = "testdata/file"
    35  	testFileLen = 11
    36  )
    37  
    38  type wantRange struct {
    39  	start, end int64 // range [start,end)
    40  }
    41  
    42  var ServeFileRangeTests = []struct {
    43  	r      string
    44  	code   int
    45  	ranges []wantRange
    46  }{
    47  	{r: "", code: StatusOK},
    48  	{r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
    49  	{r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
    50  	{r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
    51  	{r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
    52  	{r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
    53  	{r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
    54  	{r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
    55  	{r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
    56  	{r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
    57  	{r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
    58  	{r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    59  	{r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    60  	{r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
    61  	{r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
    62  	{r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
    63  	{r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
    64  	{r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
    65  	{r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
    66  	{r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
    67  	{r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
    68  	{r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
    69  }
    70  
    71  func TestServeFile(t *testing.T) {
    72  	setParallel(t)
    73  	defer afterTest(t)
    74  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
    75  		ServeFile(w, r, "testdata/file")
    76  	}))
    77  	defer ts.Close()
    78  	c := ts.Client()
    79  
    80  	var err error
    81  
    82  	file, err := os.ReadFile(testFile)
    83  	if err != nil {
    84  		t.Fatal("reading file:", err)
    85  	}
    86  
    87  	// set up the Request (re-used for all tests)
    88  	var req Request
    89  	req.Header = make(Header)
    90  	if req.URL, err = url.Parse(ts.URL); err != nil {
    91  		t.Fatal("ParseURL:", err)
    92  	}
    93  	req.Method = "GET"
    94  
    95  	// straight GET
    96  	_, body := getBody(t, "straight get", req, c)
    97  	if !bytes.Equal(body, file) {
    98  		t.Fatalf("body mismatch: got %q, want %q", body, file)
    99  	}
   100  
   101  	// Range tests
   102  Cases:
   103  	for _, rt := range ServeFileRangeTests {
   104  		if rt.r != "" {
   105  			req.Header.Set("Range", rt.r)
   106  		}
   107  		resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
   108  		if resp.StatusCode != rt.code {
   109  			t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
   110  		}
   111  		if rt.code == StatusRequestedRangeNotSatisfiable {
   112  			continue
   113  		}
   114  		wantContentRange := ""
   115  		if len(rt.ranges) == 1 {
   116  			rng := rt.ranges[0]
   117  			wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   118  		}
   119  		cr := resp.Header.Get("Content-Range")
   120  		if cr != wantContentRange {
   121  			t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
   122  		}
   123  		ct := resp.Header.Get("Content-Type")
   124  		if len(rt.ranges) == 1 {
   125  			rng := rt.ranges[0]
   126  			wantBody := file[rng.start:rng.end]
   127  			if !bytes.Equal(body, wantBody) {
   128  				t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   129  			}
   130  			if strings.HasPrefix(ct, "multipart/byteranges") {
   131  				t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
   132  			}
   133  		}
   134  		if len(rt.ranges) > 1 {
   135  			typ, params, err := mime.ParseMediaType(ct)
   136  			if err != nil {
   137  				t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
   138  				continue
   139  			}
   140  			if typ != "multipart/byteranges" {
   141  				t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
   142  				continue
   143  			}
   144  			if params["boundary"] == "" {
   145  				t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
   146  				continue
   147  			}
   148  			if g, w := resp.ContentLength, int64(len(body)); g != w {
   149  				t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
   150  				continue
   151  			}
   152  			mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
   153  			for ri, rng := range rt.ranges {
   154  				part, err := mr.NextPart()
   155  				if err != nil {
   156  					t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
   157  					continue Cases
   158  				}
   159  				wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   160  				if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
   161  					t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
   162  				}
   163  				body, err := io.ReadAll(part)
   164  				if err != nil {
   165  					t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
   166  					continue Cases
   167  				}
   168  				wantBody := file[rng.start:rng.end]
   169  				if !bytes.Equal(body, wantBody) {
   170  					t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   171  				}
   172  			}
   173  			_, err = mr.NextPart()
   174  			if err != io.EOF {
   175  				t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
   176  			}
   177  		}
   178  	}
   179  }
   180  
   181  func TestServeFile_DotDot(t *testing.T) {
   182  	tests := []struct {
   183  		req        string
   184  		wantStatus int
   185  	}{
   186  		{"/testdata/file", 200},
   187  		{"/../file", 400},
   188  		{"/..", 400},
   189  		{"/../", 400},
   190  		{"/../foo", 400},
   191  		{"/..\\foo", 400},
   192  		{"/file/a", 200},
   193  		{"/file/a..", 200},
   194  		{"/file/a/..", 400},
   195  		{"/file/a\\..", 400},
   196  	}
   197  	for _, tt := range tests {
   198  		req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
   199  		if err != nil {
   200  			t.Errorf("bad request %q: %v", tt.req, err)
   201  			continue
   202  		}
   203  		rec := httptest.NewRecorder()
   204  		ServeFile(rec, req, "testdata/file")
   205  		if rec.Code != tt.wantStatus {
   206  			t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
   207  		}
   208  	}
   209  }
   210  
   211  // Tests that this doesn't panic. (Issue 30165)
   212  func TestServeFileDirPanicEmptyPath(t *testing.T) {
   213  	rec := httptest.NewRecorder()
   214  	req := httptest.NewRequest("GET", "/", nil)
   215  	req.URL.Path = ""
   216  	ServeFile(rec, req, "testdata")
   217  	res := rec.Result()
   218  	if res.StatusCode != 301 {
   219  		t.Errorf("code = %v; want 301", res.Status)
   220  	}
   221  }
   222  
   223  var fsRedirectTestData = []struct {
   224  	original, redirect string
   225  }{
   226  	{"/test/index.html", "/test/"},
   227  	{"/test/testdata", "/test/testdata/"},
   228  	{"/test/testdata/file/", "/test/testdata/file"},
   229  }
   230  
   231  func TestFSRedirect(t *testing.T) {
   232  	defer afterTest(t)
   233  	ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
   234  	defer ts.Close()
   235  
   236  	for _, data := range fsRedirectTestData {
   237  		res, err := Get(ts.URL + data.original)
   238  		if err != nil {
   239  			t.Fatal(err)
   240  		}
   241  		_ = res.Body.Close()
   242  		if g, e := res.Request.URL.Path, data.redirect; g != e {
   243  			t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
   244  		}
   245  	}
   246  }
   247  
   248  type testFileSystem struct {
   249  	open func(name string) (File, error)
   250  }
   251  
   252  func (fs *testFileSystem) Open(name string) (File, error) {
   253  	return fs.open(name)
   254  }
   255  
   256  func TestFileServerCleans(t *testing.T) {
   257  	defer afterTest(t)
   258  	ch := make(chan string, 1)
   259  	fileServer := FileServer(&testFileSystem{func(name string) (File, error) {
   260  		ch <- name
   261  		return nil, errors.New("file does not exist")
   262  	}})
   263  	tests := []struct {
   264  		reqPath, openArg string
   265  	}{
   266  		{"/foo.txt", "/foo.txt"},
   267  		{"//foo.txt", "/foo.txt"},
   268  		{"/../foo.txt", "/foo.txt"},
   269  	}
   270  	//goland:noinspection HttpUrlsUsage
   271  	req, _ := NewRequest("GET", "http://example.com", nil)
   272  	for n, test := range tests {
   273  		rec := httptest.NewRecorder()
   274  		req.URL.Path = test.reqPath
   275  		fileServer.ServeHTTP(rec, req)
   276  		if got := <-ch; got != test.openArg {
   277  			t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
   278  		}
   279  	}
   280  }
   281  
   282  func TestFileServerEscapesNames(t *testing.T) {
   283  	defer afterTest(t)
   284  	const dirListPrefix = "<pre>\n"
   285  	const dirListSuffix = "\n</pre>\n"
   286  	tests := []struct {
   287  		name, escaped string
   288  	}{
   289  		{`simple_name`, `<a href="simple_name">simple_name</a>`},
   290  		{`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
   291  		{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
   292  		{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
   293  		{`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
   294  	}
   295  
   296  	// We put each test file in its own directory in the fakeFS so we can look at it in isolation.
   297  	f := make(fakeFS)
   298  	for i, test := range tests {
   299  		testFile := &fakeFileInfo{basename: test.name}
   300  		f[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
   301  			dir:     true,
   302  			modtime: time.Unix(1000000000, 0).UTC(),
   303  			ents:    []*fakeFileInfo{testFile},
   304  		}
   305  		f[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
   306  	}
   307  
   308  	ts := httptest.NewServer(FileServer(&f))
   309  	defer ts.Close()
   310  	for i, test := range tests {
   311  		urlTmp := fmt.Sprintf("%s/%d", ts.URL, i)
   312  		res, err := Get(urlTmp)
   313  		if err != nil {
   314  			t.Fatalf("test %q: Get: %v", test.name, err)
   315  		}
   316  		b, err := io.ReadAll(res.Body)
   317  		if err != nil {
   318  			t.Fatalf("test %q: read Body: %v", test.name, err)
   319  		}
   320  		s := string(b)
   321  		if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
   322  			t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
   323  		}
   324  		if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
   325  			t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
   326  		}
   327  		_ = res.Body.Close()
   328  	}
   329  }
   330  
   331  func TestFileServerSortsNames(t *testing.T) {
   332  	defer afterTest(t)
   333  	const contents = "I am a fake file"
   334  	dirMod := time.Unix(123, 0).UTC()
   335  	fileMod := time.Unix(1000000000, 0).UTC()
   336  	f := fakeFS{
   337  		"/": &fakeFileInfo{
   338  			dir:     true,
   339  			modtime: dirMod,
   340  			ents: []*fakeFileInfo{
   341  				{
   342  					basename: "b",
   343  					modtime:  fileMod,
   344  					contents: contents,
   345  				},
   346  				{
   347  					basename: "a",
   348  					modtime:  fileMod,
   349  					contents: contents,
   350  				},
   351  			},
   352  		},
   353  	}
   354  
   355  	ts := httptest.NewServer(FileServer(&f))
   356  	defer ts.Close()
   357  
   358  	res, err := Get(ts.URL)
   359  	if err != nil {
   360  		t.Fatalf("Get: %v", err)
   361  	}
   362  	defer func(Body io.ReadCloser) {
   363  		_ = Body.Close()
   364  	}(res.Body)
   365  
   366  	b, err := io.ReadAll(res.Body)
   367  	if err != nil {
   368  		t.Fatalf("read Body: %v", err)
   369  	}
   370  	s := string(b)
   371  	if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
   372  		t.Errorf("output appears to be unsorted:\n%s", s)
   373  	}
   374  }
   375  
   376  //goland:noinspection GoUnusedFunction
   377  func mustRemoveAll(dir string) {
   378  	err := os.RemoveAll(dir)
   379  	if err != nil {
   380  		panic(err)
   381  	}
   382  }
   383  
   384  func TestFileServerImplicitLeadingSlash(t *testing.T) {
   385  	defer afterTest(t)
   386  	tempDir := t.TempDir()
   387  	if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
   388  		t.Fatalf("WriteFile: %v", err)
   389  	}
   390  	ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
   391  	defer ts.Close()
   392  	get := func(suffix string) string {
   393  		res, err := Get(ts.URL + suffix)
   394  		if err != nil {
   395  			t.Fatalf("Get %s: %v", suffix, err)
   396  		}
   397  		b, err := io.ReadAll(res.Body)
   398  		if err != nil {
   399  			t.Fatalf("ReadAll %s: %v", suffix, err)
   400  		}
   401  		_ = res.Body.Close()
   402  		return string(b)
   403  	}
   404  	if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
   405  		t.Logf("expected a directory listing with foo.txt, got %q", s)
   406  	}
   407  	if s := get("/bar/foo.txt"); s != "Hello world" {
   408  		t.Logf("expected %q, got %q", "Hello world", s)
   409  	}
   410  }
   411  
   412  func TestDirJoin(t *testing.T) {
   413  	if runtime.GOOS == "windows" {
   414  		t.Skip("skipping test on windows")
   415  	}
   416  	wfi, err := os.Stat("/etc/hosts")
   417  	if err != nil {
   418  		t.Skip("skipping test; no /etc/hosts file")
   419  	}
   420  	test := func(d Dir, name string) {
   421  		f, err := d.Open(name)
   422  		if err != nil {
   423  			t.Fatalf("open of %s: %v", name, err)
   424  		}
   425  		defer func(f File) {
   426  			_ = f.Close()
   427  		}(f)
   428  		gfi, err := f.Stat()
   429  		if err != nil {
   430  			t.Fatalf("stat of %s: %v", name, err)
   431  		}
   432  		if !os.SameFile(gfi, wfi) {
   433  			t.Errorf("%s got different file", name)
   434  		}
   435  	}
   436  	test("/etc/", "/hosts")
   437  	test("/etc/", "hosts")
   438  	test("/etc/", "../../../../hosts")
   439  	test("/etc", "/hosts")
   440  	test("/etc", "hosts")
   441  	test("/etc", "../../../../hosts")
   442  
   443  	// Not really directories, but since we use this trick in
   444  	// ServeFile, test it:
   445  	test("/etc/hosts", "")
   446  	test("/etc/hosts", "/")
   447  	test("/etc/hosts", "../")
   448  }
   449  
   450  func TestEmptyDirOpenCWD(t *testing.T) {
   451  	test := func(d Dir) {
   452  		name := "fs_test.go"
   453  		f, err := d.Open(name)
   454  		if err != nil {
   455  			t.Fatalf("open of %s: %v", name, err)
   456  		}
   457  		defer func(f File) {
   458  			_ = f.Close()
   459  		}(f)
   460  	}
   461  	test("")
   462  	test(".")
   463  	test("./")
   464  }
   465  
   466  func TestServeFileContentType(t *testing.T) {
   467  	defer afterTest(t)
   468  	const ctype = "icecream/chocolate"
   469  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   470  		switch r.FormValue("override") {
   471  		case "1":
   472  			w.Header().Set("Content-Type", ctype)
   473  		case "2":
   474  			// Explicitly inhibit sniffing.
   475  			w.Header()["Content-Type"] = []string{}
   476  		}
   477  		ServeFile(w, r, "testdata/file")
   478  	}))
   479  	defer ts.Close()
   480  	get := func(override string, want []string) {
   481  		resp, err := Get(ts.URL + "?override=" + override)
   482  		if err != nil {
   483  			t.Fatal(err)
   484  		}
   485  		if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
   486  			t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
   487  		}
   488  		_ = resp.Body.Close()
   489  	}
   490  	get("0", []string{"text/plain; charset=utf-8"})
   491  	get("1", []string{ctype})
   492  	get("2", nil)
   493  }
   494  
   495  func TestServeFileMimeType(t *testing.T) {
   496  	defer afterTest(t)
   497  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   498  		ServeFile(w, r, "testdata/style.css")
   499  	}))
   500  	defer ts.Close()
   501  	resp, err := Get(ts.URL)
   502  	if err != nil {
   503  		t.Fatal(err)
   504  	}
   505  	_ = resp.Body.Close()
   506  	want := "text/css; charset=utf-8"
   507  	if h := resp.Header.Get("Content-Type"); h != want {
   508  		t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   509  	}
   510  }
   511  
   512  func TestServeFileFromCWD(t *testing.T) {
   513  	defer afterTest(t)
   514  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   515  		ServeFile(w, r, "fs_test.go")
   516  	}))
   517  	defer ts.Close()
   518  	r, err := Get(ts.URL)
   519  	if err != nil {
   520  		t.Fatal(err)
   521  	}
   522  	_ = r.Body.Close()
   523  	if r.StatusCode != 200 {
   524  		t.Fatalf("expected 200 OK, got %s", r.Status)
   525  	}
   526  }
   527  
   528  // Issue 13996
   529  func TestServeDirWithoutTrailingSlash(t *testing.T) {
   530  	e := "/testdata/"
   531  	defer afterTest(t)
   532  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   533  		ServeFile(w, r, ".")
   534  	}))
   535  	defer ts.Close()
   536  	r, err := Get(ts.URL + "/testdata")
   537  	if err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	_ = r.Body.Close()
   541  	if g := r.Request.URL.Path; g != e {
   542  		t.Errorf("got %s, want %s", g, e)
   543  	}
   544  }
   545  
   546  // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
   547  // specified.
   548  func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
   549  func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
   550  func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
   551  	defer afterTest(t)
   552  	cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
   553  		w.Header().Set("Content-Encoding", "foo")
   554  		ServeFile(w, r, "testdata/file")
   555  
   556  		// Because the testdata is so small, it would fit in
   557  		// both the h1 and h2 Server's write buffers. For h1,
   558  		// sendfile is used, though, forcing a header flush at
   559  		// the io.Copy. http2 doesn't do a header flush so
   560  		// buffers all 11 bytes and then adds its own
   561  		// Content-Length. To prevent the Server's
   562  		// Content-Length and test ServeFile only, flush here.
   563  		w.(Flusher).Flush()
   564  	}))
   565  	defer cst.close()
   566  	resp, err := cst.c.Get(cst.ts.URL)
   567  	if err != nil {
   568  		t.Fatal(err)
   569  	}
   570  	_ = resp.Body.Close()
   571  	if g, e := resp.ContentLength, int64(-1); g != e {
   572  		t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
   573  	}
   574  }
   575  
   576  func TestServeIndexHtml(t *testing.T) {
   577  	defer afterTest(t)
   578  
   579  	for i := 0; i < 2; i++ {
   580  		var h Handler
   581  		var name string
   582  		switch i {
   583  		case 0:
   584  			h = FileServer(Dir("."))
   585  			name = "Dir"
   586  		case 1:
   587  			h = FileServer(FS(os.DirFS(".")))
   588  			name = "DirFS"
   589  		}
   590  		t.Run(name, func(t *testing.T) {
   591  			const want = "index.html says hello\n"
   592  			ts := httptest.NewServer(h)
   593  			defer ts.Close()
   594  
   595  			for _, p := range []string{"/testdata/", "/testdata/index.html"} {
   596  				res, err := Get(ts.URL + p)
   597  				if err != nil {
   598  					t.Fatal(err)
   599  				}
   600  				b, err := io.ReadAll(res.Body)
   601  				if err != nil {
   602  					t.Fatal("reading Body:", err)
   603  				}
   604  				if s := string(b); s != want {
   605  					t.Errorf("for path %q got %q, want %q", p, s, want)
   606  				}
   607  				_ = res.Body.Close()
   608  			}
   609  		})
   610  	}
   611  }
   612  
   613  func TestServeIndexHtmlFS(t *testing.T) {
   614  	defer afterTest(t)
   615  	const want = "index.html says hello\n"
   616  	ts := httptest.NewServer(FileServer(Dir(".")))
   617  	defer ts.Close()
   618  
   619  	for _, p := range []string{"/testdata/", "/testdata/index.html"} {
   620  		res, err := Get(ts.URL + p)
   621  		if err != nil {
   622  			t.Fatal(err)
   623  		}
   624  		b, err := io.ReadAll(res.Body)
   625  		if err != nil {
   626  			t.Fatal("reading Body:", err)
   627  		}
   628  		if s := string(b); s != want {
   629  			t.Errorf("for path %q got %q, want %q", p, s, want)
   630  		}
   631  		_ = res.Body.Close()
   632  	}
   633  }
   634  
   635  func TestFileServerZeroByte(t *testing.T) {
   636  	defer afterTest(t)
   637  	ts := httptest.NewServer(FileServer(Dir(".")))
   638  	defer ts.Close()
   639  
   640  	c, err := net.Dial("tcp", ts.Listener.Addr().String())
   641  	if err != nil {
   642  		t.Fatal(err)
   643  	}
   644  	defer func(c net.Conn) {
   645  		_ = c.Close()
   646  	}(c)
   647  	_, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
   648  	if err != nil {
   649  		t.Fatal(err)
   650  	}
   651  	var got bytes.Buffer
   652  	bufr := bufio.NewReader(io.TeeReader(c, &got))
   653  	res, err := ReadResponse(bufr, nil)
   654  	if err != nil {
   655  		t.Fatal("ReadResponse: ", err)
   656  	}
   657  	if res.StatusCode == 200 {
   658  		t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
   659  	}
   660  }
   661  
   662  type fakeFileInfo struct {
   663  	dir      bool
   664  	basename string
   665  	modtime  time.Time
   666  	ents     []*fakeFileInfo
   667  	contents string
   668  	err      error
   669  }
   670  
   671  func (f *fakeFileInfo) Name() string       { return f.basename }
   672  func (f *fakeFileInfo) Sys() interface{}   { return nil }
   673  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   674  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   675  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   676  func (f *fakeFileInfo) Mode() fs.FileMode {
   677  	if f.dir {
   678  		return 0755 | fs.ModeDir
   679  	}
   680  	return 0644
   681  }
   682  
   683  type fakeFile struct {
   684  	io.ReadSeeker
   685  	fi     *fakeFileInfo
   686  	path   string // as opened
   687  	entpos int
   688  }
   689  
   690  func (f *fakeFile) Close() error               { return nil }
   691  func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
   692  func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
   693  	if !f.fi.dir {
   694  		return nil, fs.ErrInvalid
   695  	}
   696  	var fis []fs.FileInfo
   697  
   698  	limit := f.entpos + count
   699  	if count <= 0 || limit > len(f.fi.ents) {
   700  		limit = len(f.fi.ents)
   701  	}
   702  	for ; f.entpos < limit; f.entpos++ {
   703  		fis = append(fis, f.fi.ents[f.entpos])
   704  	}
   705  
   706  	if len(fis) == 0 && count > 0 {
   707  		return fis, io.EOF
   708  	} else {
   709  		return fis, nil
   710  	}
   711  }
   712  
   713  type fakeFS map[string]*fakeFileInfo
   714  
   715  func (fsys fakeFS) Open(name string) (File, error) {
   716  	name = path.Clean(name)
   717  	f, ok := fsys[name]
   718  	if !ok {
   719  		return nil, fs.ErrNotExist
   720  	}
   721  	if f.err != nil {
   722  		return nil, f.err
   723  	}
   724  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   725  }
   726  
   727  func TestDirectoryIfNotModified(t *testing.T) {
   728  	defer afterTest(t)
   729  	const indexContents = "I am a fake index.html file"
   730  	fileMod := time.Unix(1000000000, 0).UTC()
   731  	fileModStr := fileMod.Format(TimeFormat)
   732  	dirMod := time.Unix(123, 0).UTC()
   733  	indexFile := &fakeFileInfo{
   734  		basename: "index.html",
   735  		modtime:  fileMod,
   736  		contents: indexContents,
   737  	}
   738  	f := fakeFS{
   739  		"/": &fakeFileInfo{
   740  			dir:     true,
   741  			modtime: dirMod,
   742  			ents:    []*fakeFileInfo{indexFile},
   743  		},
   744  		"/index.html": indexFile,
   745  	}
   746  
   747  	ts := httptest.NewServer(FileServer(f))
   748  	defer ts.Close()
   749  
   750  	res, err := Get(ts.URL)
   751  	if err != nil {
   752  		t.Fatal(err)
   753  	}
   754  	b, err := io.ReadAll(res.Body)
   755  	if err != nil {
   756  		t.Fatal(err)
   757  	}
   758  	if string(b) != indexContents {
   759  		t.Fatalf("Got body %q; want %q", b, indexContents)
   760  	}
   761  	_ = res.Body.Close()
   762  
   763  	lastMod := res.Header.Get("Last-Modified")
   764  	if lastMod != fileModStr {
   765  		t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
   766  	}
   767  
   768  	req, _ := NewRequest("GET", ts.URL, nil)
   769  	req.Header.Set("If-Modified-Since", lastMod)
   770  
   771  	c := ts.Client()
   772  	res, err = c.Do(req)
   773  	if err != nil {
   774  		t.Fatal(err)
   775  	}
   776  	if res.StatusCode != 304 {
   777  		t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
   778  	}
   779  	_ = res.Body.Close()
   780  
   781  	// Advance the index.html file's modtime, but not the directory's.
   782  	indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
   783  
   784  	res, err = c.Do(req)
   785  	if err != nil {
   786  		t.Fatal(err)
   787  	}
   788  	if res.StatusCode != 200 {
   789  		t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
   790  	}
   791  	_ = res.Body.Close()
   792  }
   793  
   794  func mustStat(t *testing.T, fileName string) fs.FileInfo {
   795  	fi, err := os.Stat(fileName)
   796  	if err != nil {
   797  		t.Fatal(err)
   798  	}
   799  	return fi
   800  }
   801  
   802  func TestServeContent(t *testing.T) {
   803  	defer afterTest(t)
   804  	type serveParam struct {
   805  		name        string
   806  		modtime     time.Time
   807  		content     io.ReadSeeker
   808  		contentType string
   809  		etag        string
   810  	}
   811  	servec := make(chan serveParam, 1)
   812  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   813  		p := <-servec
   814  		if p.etag != "" {
   815  			w.Header().Set("ETag", p.etag)
   816  		}
   817  		if p.contentType != "" {
   818  			w.Header().Set("Content-Type", p.contentType)
   819  		}
   820  		ServeContent(w, r, p.name, p.modtime, p.content)
   821  	}))
   822  	defer ts.Close()
   823  
   824  	type testCase struct {
   825  		// One of file or content must be set:
   826  		file    string
   827  		content io.ReadSeeker
   828  
   829  		modtime          time.Time
   830  		serveETag        string // optional
   831  		serveContentType string // optional
   832  		reqHeader        map[string]string
   833  		wantLastMod      string
   834  		wantContentType  string
   835  		wantContentRange string
   836  		wantStatus       int
   837  	}
   838  	htmlModTime := mustStat(t, "testdata/index.html").ModTime()
   839  	tests := map[string]testCase{
   840  		"no_last_modified": {
   841  			file:            "testdata/style.css",
   842  			wantContentType: "text/css; charset=utf-8",
   843  			wantStatus:      200,
   844  		},
   845  		"with_last_modified": {
   846  			file:            "testdata/index.html",
   847  			wantContentType: "text/html; charset=utf-8",
   848  			modtime:         htmlModTime,
   849  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   850  			wantStatus:      200,
   851  		},
   852  		"not_modified_modtime": {
   853  			file:      "testdata/style.css",
   854  			serveETag: `"foo"`, // Last-Modified sent only when no ETag
   855  			modtime:   htmlModTime,
   856  			reqHeader: map[string]string{
   857  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   858  			},
   859  			wantStatus: 304,
   860  		},
   861  		"not_modified_modtime_with_contenttype": {
   862  			file:             "testdata/style.css",
   863  			serveContentType: "text/css", // explicit content type
   864  			serveETag:        `"foo"`,    // Last-Modified sent only when no ETag
   865  			modtime:          htmlModTime,
   866  			reqHeader: map[string]string{
   867  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   868  			},
   869  			wantStatus: 304,
   870  		},
   871  		"not_modified_etag": {
   872  			file:      "testdata/style.css",
   873  			serveETag: `"foo"`,
   874  			reqHeader: map[string]string{
   875  				"If-None-Match": `"foo"`,
   876  			},
   877  			wantStatus: 304,
   878  		},
   879  		"not_modified_etag_no_seek": {
   880  			content:   panicOnSeek{nil}, // should never be called
   881  			serveETag: `W/"foo"`,        // If-None-Match uses weak ETag comparison
   882  			reqHeader: map[string]string{
   883  				"If-None-Match": `"baz", W/"foo"`,
   884  			},
   885  			wantStatus: 304,
   886  		},
   887  		"if_none_match_mismatch": {
   888  			file:      "testdata/style.css",
   889  			serveETag: `"foo"`,
   890  			reqHeader: map[string]string{
   891  				"If-None-Match": `"Foo"`,
   892  			},
   893  			wantStatus:      200,
   894  			wantContentType: "text/css; charset=utf-8",
   895  		},
   896  		"if_none_match_malformed": {
   897  			file:      "testdata/style.css",
   898  			serveETag: `"foo"`,
   899  			reqHeader: map[string]string{
   900  				"If-None-Match": `,`,
   901  			},
   902  			wantStatus:      200,
   903  			wantContentType: "text/css; charset=utf-8",
   904  		},
   905  		"range_good": {
   906  			file:      "testdata/style.css",
   907  			serveETag: `"A"`,
   908  			reqHeader: map[string]string{
   909  				"Range": "bytes=0-4",
   910  			},
   911  			wantStatus:       StatusPartialContent,
   912  			wantContentType:  "text/css; charset=utf-8",
   913  			wantContentRange: "bytes 0-4/8",
   914  		},
   915  		"range_match": {
   916  			file:      "testdata/style.css",
   917  			serveETag: `"A"`,
   918  			reqHeader: map[string]string{
   919  				"Range":    "bytes=0-4",
   920  				"If-Range": `"A"`,
   921  			},
   922  			wantStatus:       StatusPartialContent,
   923  			wantContentType:  "text/css; charset=utf-8",
   924  			wantContentRange: "bytes 0-4/8",
   925  		},
   926  		"range_match_weak_etag": {
   927  			file:      "testdata/style.css",
   928  			serveETag: `W/"A"`,
   929  			reqHeader: map[string]string{
   930  				"Range":    "bytes=0-4",
   931  				"If-Range": `W/"A"`,
   932  			},
   933  			wantStatus:      200,
   934  			wantContentType: "text/css; charset=utf-8",
   935  		},
   936  		"range_no_overlap": {
   937  			file:      "testdata/style.css",
   938  			serveETag: `"A"`,
   939  			reqHeader: map[string]string{
   940  				"Range": "bytes=10-20",
   941  			},
   942  			wantStatus:       StatusRequestedRangeNotSatisfiable,
   943  			wantContentType:  "text/plain; charset=utf-8",
   944  			wantContentRange: "bytes */8",
   945  		},
   946  		// An If-Range resource for entity "A", but entity "B" is now current.
   947  		// The Range request should be ignored.
   948  		"range_no_match": {
   949  			file:      "testdata/style.css",
   950  			serveETag: `"A"`,
   951  			reqHeader: map[string]string{
   952  				"Range":    "bytes=0-4",
   953  				"If-Range": `"B"`,
   954  			},
   955  			wantStatus:      200,
   956  			wantContentType: "text/css; charset=utf-8",
   957  		},
   958  		"range_with_modtime": {
   959  			file:    "testdata/style.css",
   960  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
   961  			reqHeader: map[string]string{
   962  				"Range":    "bytes=0-4",
   963  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   964  			},
   965  			wantStatus:       StatusPartialContent,
   966  			wantContentType:  "text/css; charset=utf-8",
   967  			wantContentRange: "bytes 0-4/8",
   968  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
   969  		},
   970  		"range_with_modtime_mismatch": {
   971  			file:    "testdata/style.css",
   972  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
   973  			reqHeader: map[string]string{
   974  				"Range":    "bytes=0-4",
   975  				"If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
   976  			},
   977  			wantStatus:      StatusOK,
   978  			wantContentType: "text/css; charset=utf-8",
   979  			wantLastMod:     "Wed, 25 Jun 2014 17:12:18 GMT",
   980  		},
   981  		"range_with_modtime_nanos": {
   982  			file:    "testdata/style.css",
   983  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
   984  			reqHeader: map[string]string{
   985  				"Range":    "bytes=0-4",
   986  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   987  			},
   988  			wantStatus:       StatusPartialContent,
   989  			wantContentType:  "text/css; charset=utf-8",
   990  			wantContentRange: "bytes 0-4/8",
   991  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
   992  		},
   993  		"unix_zero_modtime": {
   994  			content:         strings.NewReader("<html>foo"),
   995  			modtime:         time.Unix(0, 0),
   996  			wantStatus:      StatusOK,
   997  			wantContentType: "text/html; charset=utf-8",
   998  		},
   999  		"ifmatch_matches": {
  1000  			file:      "testdata/style.css",
  1001  			serveETag: `"A"`,
  1002  			reqHeader: map[string]string{
  1003  				"If-Match": `"Z", "A"`,
  1004  			},
  1005  			wantStatus:      200,
  1006  			wantContentType: "text/css; charset=utf-8",
  1007  		},
  1008  		"ifmatch_star": {
  1009  			file:      "testdata/style.css",
  1010  			serveETag: `"A"`,
  1011  			reqHeader: map[string]string{
  1012  				"If-Match": `*`,
  1013  			},
  1014  			wantStatus:      200,
  1015  			wantContentType: "text/css; charset=utf-8",
  1016  		},
  1017  		"ifmatch_failed": {
  1018  			file:      "testdata/style.css",
  1019  			serveETag: `"A"`,
  1020  			reqHeader: map[string]string{
  1021  				"If-Match": `"B"`,
  1022  			},
  1023  			wantStatus: 412,
  1024  		},
  1025  		"ifmatch_fails_on_weak_etag": {
  1026  			file:      "testdata/style.css",
  1027  			serveETag: `W/"A"`,
  1028  			reqHeader: map[string]string{
  1029  				"If-Match": `W/"A"`,
  1030  			},
  1031  			wantStatus: 412,
  1032  		},
  1033  		"if_unmodified_since_true": {
  1034  			file:    "testdata/style.css",
  1035  			modtime: htmlModTime,
  1036  			reqHeader: map[string]string{
  1037  				"If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
  1038  			},
  1039  			wantStatus:      200,
  1040  			wantContentType: "text/css; charset=utf-8",
  1041  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
  1042  		},
  1043  		"if_unmodified_since_false": {
  1044  			file:    "testdata/style.css",
  1045  			modtime: htmlModTime,
  1046  			reqHeader: map[string]string{
  1047  				"If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
  1048  			},
  1049  			wantStatus:  412,
  1050  			wantLastMod: htmlModTime.UTC().Format(TimeFormat),
  1051  		},
  1052  	}
  1053  	for testName, tt := range tests {
  1054  		var content io.ReadSeeker
  1055  		if tt.file != "" {
  1056  			f, err := os.Open(tt.file)
  1057  			if err != nil {
  1058  				t.Fatalf("test %q: %v", testName, err)
  1059  			}
  1060  			//goland:noinspection GoDeferInLoop
  1061  			defer func(f *os.File) {
  1062  				_ = f.Close()
  1063  			}(f)
  1064  			content = f
  1065  		} else {
  1066  			content = tt.content
  1067  		}
  1068  		for _, method := range []string{"GET", "HEAD"} {
  1069  			//restore content in case it is consumed by previous method
  1070  			if content, ok := content.(*strings.Reader); ok {
  1071  				_, _ = content.Seek(0, io.SeekStart)
  1072  			}
  1073  
  1074  			servec <- serveParam{
  1075  				name:        filepath.Base(tt.file),
  1076  				content:     content,
  1077  				modtime:     tt.modtime,
  1078  				etag:        tt.serveETag,
  1079  				contentType: tt.serveContentType,
  1080  			}
  1081  			req, err := NewRequest(method, ts.URL, nil)
  1082  			if err != nil {
  1083  				t.Fatal(err)
  1084  			}
  1085  			for k, v := range tt.reqHeader {
  1086  				req.Header.Set(k, v)
  1087  			}
  1088  
  1089  			c := ts.Client()
  1090  			res, err := c.Do(req)
  1091  			if err != nil {
  1092  				t.Fatal(err)
  1093  			}
  1094  			_, _ = io.Copy(io.Discard, res.Body)
  1095  			_ = res.Body.Close()
  1096  			if res.StatusCode != tt.wantStatus {
  1097  				t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
  1098  			}
  1099  			if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
  1100  				t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
  1101  			}
  1102  			if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
  1103  				t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
  1104  			}
  1105  			if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
  1106  				t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
  1107  			}
  1108  		}
  1109  	}
  1110  }
  1111  
  1112  // Issue 12991
  1113  func TestServerFileStatError(t *testing.T) {
  1114  	rec := httptest.NewRecorder()
  1115  	//goland:noinspection HttpUrlsUsage
  1116  	r, _ := NewRequest("GET", "http://foo/", nil)
  1117  	name := "file.txt"
  1118  	f := issue12991FS{}
  1119  	ExportServeFile(rec, r, f, name, false)
  1120  	if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
  1121  		t.Errorf("wanted 403 forbidden message; got: %s", body)
  1122  	}
  1123  }
  1124  
  1125  type issue12991FS struct{}
  1126  
  1127  func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
  1128  
  1129  type issue12991File struct{ File }
  1130  
  1131  func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
  1132  func (issue12991File) Close() error               { return nil }
  1133  
  1134  func TestServeContentErrorMessages(t *testing.T) {
  1135  	defer afterTest(t)
  1136  	f := fakeFS{
  1137  		"/500": &fakeFileInfo{
  1138  			err: errors.New("random error"),
  1139  		},
  1140  		"/403": &fakeFileInfo{
  1141  			err: &fs.PathError{Err: fs.ErrPermission},
  1142  		},
  1143  	}
  1144  	ts := httptest.NewServer(FileServer(f))
  1145  	defer ts.Close()
  1146  	c := ts.Client()
  1147  	for _, code := range []int{403, 404, 500} {
  1148  		res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
  1149  		if err != nil {
  1150  			t.Errorf("Error fetching /%d: %v", code, err)
  1151  			continue
  1152  		}
  1153  		if res.StatusCode != code {
  1154  			t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
  1155  		}
  1156  		_ = res.Body.Close()
  1157  	}
  1158  }
  1159  
  1160  // verifies that sendfile is being used on Linux
  1161  func TestLinuxSendfile(t *testing.T) {
  1162  	setParallel(t)
  1163  	defer afterTest(t)
  1164  	if runtime.GOOS != "linux" {
  1165  		t.Skip("skipping; linux-only test")
  1166  	}
  1167  	if _, err := exec.LookPath("strace"); err != nil {
  1168  		t.Skip("skipping; strace not found in path")
  1169  	}
  1170  
  1171  	ln, err := net.Listen("tcp", "127.0.0.1:0")
  1172  	if err != nil {
  1173  		t.Fatal(err)
  1174  	}
  1175  	lnf, err := ln.(*net.TCPListener).File()
  1176  	if err != nil {
  1177  		t.Fatal(err)
  1178  	}
  1179  	defer func(ln net.Listener) {
  1180  		_ = ln.Close()
  1181  	}(ln)
  1182  
  1183  	// Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
  1184  	if err := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
  1185  		t.Skipf("skipping; failed to run strace: %v", err)
  1186  	}
  1187  
  1188  	filename := fmt.Sprintf("1kb-%d", os.Getpid())
  1189  	fp := path.Join(os.TempDir(), filename)
  1190  
  1191  	if err := os.WriteFile(fp, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
  1192  		t.Fatal(err)
  1193  	}
  1194  	defer func(name string) {
  1195  		_ = os.Remove(name)
  1196  	}(fp)
  1197  
  1198  	var buf bytes.Buffer
  1199  	child := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=TestLinuxSendfileChild")
  1200  	child.ExtraFiles = append(child.ExtraFiles, lnf)
  1201  	child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
  1202  	child.Stdout = &buf
  1203  	child.Stderr = &buf
  1204  	if err := child.Start(); err != nil {
  1205  		t.Skipf("skipping; failed to start straced child: %v", err)
  1206  	}
  1207  
  1208  	//goland:noinspection HttpUrlsUsage
  1209  	res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
  1210  	if err != nil {
  1211  		t.Fatalf("http client error: %v", err)
  1212  	}
  1213  	_, err = io.Copy(io.Discard, res.Body)
  1214  	if err != nil {
  1215  		t.Fatalf("client body read error: %v", err)
  1216  	}
  1217  	_ = res.Body.Close()
  1218  
  1219  	// Force child to exit cleanly.
  1220  	//goland:noinspection HttpUrlsUsage
  1221  	_, _ = Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
  1222  	_ = child.Wait()
  1223  
  1224  	rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
  1225  	out := buf.String()
  1226  	if !rx.MatchString(out) {
  1227  		t.Errorf("no sendfile system call found in:\n%s", out)
  1228  	}
  1229  }
  1230  
  1231  func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
  1232  	r, err := client.Do(&req)
  1233  	if err != nil {
  1234  		t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
  1235  	}
  1236  	b, err := io.ReadAll(r.Body)
  1237  	if err != nil {
  1238  		t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
  1239  	}
  1240  	return r, b
  1241  }
  1242  
  1243  // TestLinuxSendfileChild isn't a real test. It's used as a helper process
  1244  // for TestLinuxSendfile.
  1245  func TestLinuxSendfileChild(*testing.T) {
  1246  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
  1247  		return
  1248  	}
  1249  	defer os.Exit(0)
  1250  	fd3 := os.NewFile(3, "ephemeral-port-listener")
  1251  	ln, err := net.FileListener(fd3)
  1252  	if err != nil {
  1253  		panic(err)
  1254  	}
  1255  	mux := NewServeMux()
  1256  	mux.Handle("/", FileServer(Dir(os.TempDir())))
  1257  	mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
  1258  		os.Exit(0)
  1259  	})
  1260  	s := &Server{Handler: mux}
  1261  	err = s.Serve(ln)
  1262  	if err != nil {
  1263  		panic(err)
  1264  	}
  1265  }
  1266  
  1267  // Issue 18984: tests that requests for paths beyond files return not-found errors
  1268  func TestFileServerNotDirError(t *testing.T) {
  1269  	defer afterTest(t)
  1270  	ts := httptest.NewServer(FileServer(Dir("testdata")))
  1271  	defer ts.Close()
  1272  
  1273  	res, err := Get(ts.URL + "/index.html/not-a-file")
  1274  	if err != nil {
  1275  		t.Fatal(err)
  1276  	}
  1277  	_ = res.Body.Close()
  1278  	if res.StatusCode != 404 {
  1279  		t.Errorf("StatusCode = %v; want 404", res.StatusCode)
  1280  	}
  1281  
  1282  	test := func(name string, dir Dir) {
  1283  		t.Run(name, func(t *testing.T) {
  1284  			_, err = dir.Open("/index.html/not-a-file")
  1285  			if err == nil {
  1286  				t.Fatal("err == nil; want != nil")
  1287  			}
  1288  			if !errors.Is(err, fs.ErrNotExist) {
  1289  				t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
  1290  					errors.Is(err, fs.ErrNotExist))
  1291  			}
  1292  
  1293  			_, err = dir.Open("/index.html/not-a-dir/not-a-file")
  1294  			if err == nil {
  1295  				t.Fatal("err == nil; want != nil")
  1296  			}
  1297  			if !errors.Is(err, fs.ErrNotExist) {
  1298  				t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
  1299  					errors.Is(err, fs.ErrNotExist))
  1300  			}
  1301  		})
  1302  	}
  1303  
  1304  	absPath, err := filepath.Abs("testdata")
  1305  	if err != nil {
  1306  		t.Fatal("get abs path:", err)
  1307  	}
  1308  
  1309  	test("RelativePath", "testdata")
  1310  	test("AbsolutePath", Dir(absPath))
  1311  }
  1312  
  1313  func TestFileServerCleanPath(t *testing.T) {
  1314  	tests := []struct {
  1315  		path     string
  1316  		wantCode int
  1317  		wantOpen []string
  1318  	}{
  1319  		{"/", 200, []string{"/", "/index.html"}},
  1320  		{"/dir", 301, []string{"/dir"}},
  1321  		{"/dir/", 200, []string{"/dir", "/dir/index.html"}},
  1322  	}
  1323  	for _, tt := range tests {
  1324  		var log []string
  1325  		rr := httptest.NewRecorder()
  1326  		req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
  1327  		FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
  1328  		if !reflect.DeepEqual(log, tt.wantOpen) {
  1329  			t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
  1330  		}
  1331  		if rr.Code != tt.wantCode {
  1332  			t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
  1333  		}
  1334  	}
  1335  }
  1336  
  1337  type fileServerCleanPathDir struct {
  1338  	log *[]string
  1339  }
  1340  
  1341  func (d fileServerCleanPathDir) Open(path string) (File, error) {
  1342  	*(d.log) = append(*(d.log), path)
  1343  	if path == "/" || path == "/dir" || path == "/dir/" {
  1344  		// Just return back something that's a directory.
  1345  		return Dir(".").Open(".")
  1346  	}
  1347  	return nil, fs.ErrNotExist
  1348  }
  1349  
  1350  type panicOnSeek struct{ io.ReadSeeker }
  1351  
  1352  func Test_scanETag(t *testing.T) {
  1353  	tests := []struct {
  1354  		in         string
  1355  		wantETag   string
  1356  		wantRemain string
  1357  	}{
  1358  		{`W/"etag-1"`, `W/"etag-1"`, ""},
  1359  		{`"etag-2"`, `"etag-2"`, ""},
  1360  		{`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
  1361  		{"", "", ""},
  1362  		{"W/", "", ""},
  1363  		{`W/"truc`, "", ""},
  1364  		{`w/"case-sensitive"`, "", ""},
  1365  		{`"spaced etag"`, "", ""},
  1366  	}
  1367  	for _, test := range tests {
  1368  		etag, remain := ExportScanETag(test.in)
  1369  		if etag != test.wantETag || remain != test.wantRemain {
  1370  			t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
  1371  		}
  1372  	}
  1373  }
  1374  
  1375  // Issue 40940: Ensure that we only accept non-negative suffix-lengths
  1376  // in "Range": "bytes=-N", and should reject "bytes=--2".
  1377  func TestServeFileRejectsInvalidSuffixLengths_h1(t *testing.T) {
  1378  	testServeFileRejectsInvalidSuffixLengths(t, h1Mode)
  1379  }
  1380  func TestServeFileRejectsInvalidSuffixLengths_h2(t *testing.T) {
  1381  	testServeFileRejectsInvalidSuffixLengths(t, h2Mode)
  1382  }
  1383  
  1384  func testServeFileRejectsInvalidSuffixLengths(t *testing.T, h2 bool) {
  1385  	defer afterTest(t)
  1386  	cst := httptest.NewUnstartedServer(FileServer(Dir("testdata")))
  1387  	cst.EnableHTTP2 = h2
  1388  	cst.StartTLS()
  1389  	defer cst.Close()
  1390  
  1391  	tests := []struct {
  1392  		r        string
  1393  		wantCode int
  1394  		wantBody string
  1395  	}{
  1396  		{"bytes=--6", 416, "invalid range\n"},
  1397  		{"bytes=--0", 416, "invalid range\n"},
  1398  		{"bytes=---0", 416, "invalid range\n"},
  1399  		{"bytes=-6", 206, "hello\n"},
  1400  		{"bytes=6-", 206, "html says hello\n"},
  1401  		{"bytes=-6-", 416, "invalid range\n"},
  1402  		{"bytes=-0", 206, ""},
  1403  		{"bytes=", 200, "index.html says hello\n"},
  1404  	}
  1405  
  1406  	for _, tt := range tests {
  1407  		tt := tt
  1408  		t.Run(tt.r, func(t *testing.T) {
  1409  			req, err := NewRequest("GET", cst.URL+"/index.html", nil)
  1410  			if err != nil {
  1411  				t.Fatal(err)
  1412  			}
  1413  			req.Header.Set("Range", tt.r)
  1414  			res, err := cst.Client().Do(req)
  1415  			if err != nil {
  1416  				t.Fatal(err)
  1417  			}
  1418  			if g, w := res.StatusCode, tt.wantCode; g != w {
  1419  				t.Errorf("StatusCode mismatch: got %d want %d", g, w)
  1420  			}
  1421  			slurp, err := io.ReadAll(res.Body)
  1422  			_ = res.Body.Close()
  1423  			if err != nil {
  1424  				t.Fatal(err)
  1425  			}
  1426  			if g, w := string(slurp), tt.wantBody; g != w {
  1427  				t.Fatalf("Content mismatch:\nGot:  %q\nWant: %q", g, w)
  1428  			}
  1429  		})
  1430  	}
  1431  }