net/http: make FileServer sort directory entries

Fixes #11879

Change-Id: If021f86b2764e01c69674e6a423699b822596f15
Reviewed-on: https://go-review.googlesource.com/14161
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/src/net/http/fs.go b/src/net/http/fs.go
index 7572023..394c87d2 100644
--- a/src/net/http/fs.go
+++ b/src/net/http/fs.go
@@ -17,6 +17,7 @@
 	"os"
 	"path"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -68,24 +69,28 @@
 }
 
 func dirList(w ResponseWriter, f File) {
+	dirs, err := f.Readdir(-1)
+	if err != nil {
+		// TODO: log err.Error() to the Server.ErrorLog, once it's possible
+		// for a handler to get at its Server via the ResponseWriter. See
+		// Issue 12438.
+		Error(w, "Error reading directory", StatusInternalServerError)
+		return
+	}
+	sort.Sort(byName(dirs))
+
 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 	fmt.Fprintf(w, "<pre>\n")
-	for {
-		dirs, err := f.Readdir(100)
-		if err != nil || len(dirs) == 0 {
-			break
+	for _, d := range dirs {
+		name := d.Name()
+		if d.IsDir() {
+			name += "/"
 		}
-		for _, d := range dirs {
-			name := d.Name()
-			if d.IsDir() {
-				name += "/"
-			}
-			// name may contain '?' or '#', which must be escaped to remain
-			// part of the URL path, and not indicate the start of a query
-			// string or fragment.
-			url := url.URL{Path: name}
-			fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
-		}
+		// name may contain '?' or '#', which must be escaped to remain
+		// part of the URL path, and not indicate the start of a query
+		// string or fragment.
+		url := url.URL{Path: name}
+		fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
 	}
 	fmt.Fprintf(w, "</pre>\n")
 }
@@ -585,3 +590,9 @@
 	}
 	return
 }
+
+type byName []os.FileInfo
+
+func (s byName) Len() int           { return len(s) }
+func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
+func (s byName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
diff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go
index 794dabc..9b235d2 100644
--- a/src/net/http/fs_test.go
+++ b/src/net/http/fs_test.go
@@ -283,6 +283,49 @@
 	}
 }
 
+func TestFileServerSortsNames(t *testing.T) {
+	defer afterTest(t)
+	const contents = "I am a fake file"
+	dirMod := time.Unix(123, 0).UTC()
+	fileMod := time.Unix(1000000000, 0).UTC()
+	fs := fakeFS{
+		"/": &fakeFileInfo{
+			dir:     true,
+			modtime: dirMod,
+			ents: []*fakeFileInfo{
+				{
+					basename: "b",
+					modtime:  fileMod,
+					contents: contents,
+				},
+				{
+					basename: "a",
+					modtime:  fileMod,
+					contents: contents,
+				},
+			},
+		},
+	}
+
+	ts := httptest.NewServer(FileServer(&fs))
+	defer ts.Close()
+
+	res, err := Get(ts.URL)
+	if err != nil {
+		t.Fatalf("Get: %v", err)
+	}
+	defer res.Body.Close()
+
+	b, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		t.Fatalf("read Body: %v", err)
+	}
+	s := string(b)
+	if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
+		t.Errorf("output appears to be unsorted:\n%s", s)
+	}
+}
+
 func mustRemoveAll(dir string) {
 	err := os.RemoveAll(dir)
 	if err != nil {