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