webdav: escape displayname

Displayname WebDAV property should be XML escaped. With current
implementation a file with name '<.txt' would make the XML
invalid.

Fixes golang/go#17158

Change-Id: Ib3b5376094edc957ed15adf511bd1050ea13d27e
Reviewed-on: https://go-review.googlesource.com/29297
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/webdav/prop.go b/webdav/prop.go
index 1459466..3446871 100644
--- a/webdav/prop.go
+++ b/webdav/prop.go
@@ -5,6 +5,7 @@
 package webdav
 
 import (
+	"bytes"
 	"encoding/xml"
 	"fmt"
 	"io"
@@ -333,6 +334,12 @@
 	return []Propstat{pstat}, nil
 }
 
+func escapeXML(s string) string {
+	var buf bytes.Buffer
+	xml.EscapeText(&buf, []byte(s))
+	return buf.String()
+}
+
 func findResourceType(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
 	if fi.IsDir() {
 		return `<D:collection xmlns:D="DAV:"/>`, nil
@@ -345,7 +352,7 @@
 		// Hide the real name of a possibly prefixed root directory.
 		return "", nil
 	}
-	return fi.Name(), nil
+	return escapeXML(fi.Name()), nil
 }
 
 func findContentLength(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
diff --git a/webdav/webdav_test.go b/webdav/webdav_test.go
index b068aab..82605cd 100644
--- a/webdav/webdav_test.go
+++ b/webdav/webdav_test.go
@@ -203,47 +203,61 @@
 }
 
 func TestFilenameEscape(t *testing.T) {
-	re := regexp.MustCompile(`<D:href>([^<]*)</D:href>`)
-	do := func(method, urlStr string) (string, error) {
+	hrefRe := regexp.MustCompile(`<D:href>([^<]*)</D:href>`)
+	displayNameRe := regexp.MustCompile(`<D:displayname>([^<]*)</D:displayname>`)
+	do := func(method, urlStr string) (string, string, error) {
 		req, err := http.NewRequest(method, urlStr, nil)
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
 		res, err := http.DefaultClient.Do(req)
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
 		defer res.Body.Close()
 
 		b, err := ioutil.ReadAll(res.Body)
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
-		m := re.FindStringSubmatch(string(b))
-		if len(m) != 2 {
-			return "", errors.New("D:href not found")
+		hrefMatch := hrefRe.FindStringSubmatch(string(b))
+		if len(hrefMatch) != 2 {
+			return "", "", errors.New("D:href not found")
+		}
+		displayNameMatch := displayNameRe.FindStringSubmatch(string(b))
+		if len(displayNameMatch) != 2 {
+			return "", "", errors.New("D:displayname not found")
 		}
 
-		return m[1], nil
+		return hrefMatch[1], displayNameMatch[1], nil
 	}
 
 	testCases := []struct {
-		name, want string
+		name, wantHref, wantDisplayName string
 	}{{
-		name: `/foo%bar`,
-		want: `/foo%25bar`,
+		name:            `/foo%bar`,
+		wantHref:        `/foo%25bar`,
+		wantDisplayName: `foo%bar`,
 	}, {
-		name: `/こんにちわ世界`,
-		want: `/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%82%8F%E4%B8%96%E7%95%8C`,
+		name:            `/こんにちわ世界`,
+		wantHref:        `/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%82%8F%E4%B8%96%E7%95%8C`,
+		wantDisplayName: `こんにちわ世界`,
 	}, {
-		name: `/Program Files/`,
-		want: `/Program%20Files`,
+		name:            `/Program Files/`,
+		wantHref:        `/Program%20Files`,
+		wantDisplayName: `Program Files`,
 	}, {
-		name: `/go+lang`,
-		want: `/go+lang`,
+		name:            `/go+lang`,
+		wantHref:        `/go+lang`,
+		wantDisplayName: `go+lang`,
 	}, {
-		name: `/go&lang`,
-		want: `/go&amp;lang`,
+		name:            `/go&lang`,
+		wantHref:        `/go&amp;lang`,
+		wantDisplayName: `go&amp;lang`,
+	}, {
+		name:            `/go<lang`,
+		wantHref:        `/go%3Clang`,
+		wantDisplayName: `go&lt;lang`,
 	}}
 	fs := NewMemFS()
 	for _, tc := range testCases {
@@ -273,13 +287,16 @@
 
 	for _, tc := range testCases {
 		u.Path = tc.name
-		got, err := do("PROPFIND", u.String())
+		gotHref, gotDisplayName, err := do("PROPFIND", u.String())
 		if err != nil {
 			t.Errorf("name=%q: PROPFIND: %v", tc.name, err)
 			continue
 		}
-		if got != tc.want {
-			t.Errorf("name=%q: got %q, want %q", tc.name, got, tc.want)
+		if gotHref != tc.wantHref {
+			t.Errorf("name=%q: got href %q, want %q", tc.name, gotHref, tc.wantHref)
+		}
+		if gotDisplayName != tc.wantDisplayName {
+			t.Errorf("name=%q: got dispayname %q, want %q", tc.name, gotDisplayName, tc.wantDisplayName)
 		}
 	}
 }