godoc: fix panic in Presentation.ServeFile

The redirect to drop index.html must be done using r.URL.Path,
not relpath, because those might differ. Cutting len("index.html")
bytes off a string that doesn't end in index.html is incorrect.

While we're here, silence an annoying log print during go test.

For golang/go#40665.

Change-Id: I36553b041f53eab9c42da6b77184e90800a97e92
Reviewed-on: https://go-review.googlesource.com/c/tools/+/251080
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/godoc/meta.go b/godoc/meta.go
index 41ade39..260833d 100644
--- a/godoc/meta.go
+++ b/godoc/meta.go
@@ -7,7 +7,9 @@
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"log"
+	"os"
 	pathpkg "path"
 	"strings"
 	"time"
@@ -64,7 +66,11 @@
 	scan = func(dir string) {
 		fis, err := c.fs.ReadDir(dir)
 		if err != nil {
-			log.Println("updateMetadata:", err)
+			if dir == "/doc" && errors.Is(err, os.ErrNotExist) {
+				// Be quiet during tests that don't have a /doc tree.
+				return
+			}
+			log.Printf("updateMetadata %s: %v", dir, err)
 			return
 		}
 		for _, fi := range fis {
diff --git a/godoc/server.go b/godoc/server.go
index 1751441..8724291 100644
--- a/godoc/server.go
+++ b/godoc/server.go
@@ -754,9 +754,15 @@
 }
 
 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
-	relpath := r.URL.Path
+	if strings.HasSuffix(r.URL.Path, "/index.html") {
+		// We'll show index.html for the directory.
+		// Use the dir/ version as canonical instead of dir/index.html.
+		http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
+		return
+	}
 
 	// Check to see if we need to redirect or serve another file.
+	relpath := r.URL.Path
 	if m := p.Corpus.MetadataFor(relpath); m != nil {
 		if m.Path != relpath {
 			// Redirect to canonical path.
@@ -772,12 +778,6 @@
 
 	switch pathpkg.Ext(relpath) {
 	case ".html":
-		if strings.HasSuffix(relpath, "/index.html") {
-			// We'll show index.html for the directory.
-			// Use the dir/ version as canonical instead of dir/index.html.
-			http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
-			return
-		}
 		p.ServeHTMLDoc(w, r, abspath, relpath)
 		return
 
diff --git a/godoc/server_test.go b/godoc/server_test.go
index a7ed514..f862135 100644
--- a/godoc/server_test.go
+++ b/godoc/server_test.go
@@ -5,7 +5,12 @@
 package godoc
 
 import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
 	"testing"
+	"text/template"
 
 	"golang.org/x/tools/godoc/vfs/mapfs"
 )
@@ -67,3 +72,41 @@
 		t.Errorf("pInfo.PDoc.Funcs[0].Doc = %q; want %q", got, want)
 	}
 }
+
+func TestRedirectAndMetadata(t *testing.T) {
+	c := NewCorpus(mapfs.New(map[string]string{
+		"doc/y/index.html": "Hello, y.",
+		"doc/x/index.html": `<!--{
+		"Path": "/doc/x/"
+}-->
+
+Hello, x.
+`}))
+	c.updateMetadata()
+	p := &Presentation{
+		Corpus:    c,
+		GodocHTML: template.Must(template.New("").Parse(`{{printf "%s" .Body}}`)),
+	}
+	r := &http.Request{URL: &url.URL{}}
+
+	// Test that redirect is sent back correctly.
+	// Used to panic. See golang.org/issue/40665.
+	for _, elem := range []string{"x", "y"} {
+		dir := "/doc/" + elem + "/"
+		r.URL.Path = dir + "index.html"
+		rw := httptest.NewRecorder()
+		p.ServeFile(rw, r)
+		loc := rw.Result().Header.Get("Location")
+		if rw.Code != 301 || loc != dir {
+			t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
+		}
+
+		r.URL.Path = dir
+		rw = httptest.NewRecorder()
+		p.ServeFile(rw, r)
+		if rw.Code != 200 || !strings.Contains(rw.Body.String(), "Hello, "+elem) {
+			t.Fatalf("GET %s: expected 200 w/ Hello, %s: got %d w/ body:\n%s",
+				r.URL.Path, elem, rw.Code, rw.Body)
+		}
+	}
+}