webdav: have escapeXML perform fewer allocations.

escapeXML was introduced in the previous commit:
https://go-review.googlesource.com/29297

Change-Id: I7d0c982049e495b312b1b8d28ba794444dd605d4
Reviewed-on: https://go-review.googlesource.com/32370
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/webdav/prop.go b/webdav/prop.go
index 3446871..83fbfa8 100644
--- a/webdav/prop.go
+++ b/webdav/prop.go
@@ -335,9 +335,23 @@
 }
 
 func escapeXML(s string) string {
-	var buf bytes.Buffer
-	xml.EscapeText(&buf, []byte(s))
-	return buf.String()
+	for i := 0; i < len(s); i++ {
+		// As an optimization, if s contains only ASCII letters, digits or a
+		// few special characters, the escaped value is s itself and we don't
+		// need to allocate a buffer and convert between string and []byte.
+		switch c := s[i]; {
+		case c == ' ' || c == '_' ||
+			('+' <= c && c <= '9') || // Digits as well as + , - . and /
+			('A' <= c && c <= 'Z') ||
+			('a' <= c && c <= 'z'):
+			continue
+		}
+		// Otherwise, go through the full escaping process.
+		var buf bytes.Buffer
+		xml.EscapeText(&buf, []byte(s))
+		return buf.String()
+	}
+	return s
 }
 
 func findResourceType(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
diff --git a/webdav/webdav_test.go b/webdav/webdav_test.go
index 82605cd..d9f8c8e 100644
--- a/webdav/webdav_test.go
+++ b/webdav/webdav_test.go
@@ -202,6 +202,44 @@
 	}
 }
 
+func TestEscapeXML(t *testing.T) {
+	// These test cases aren't exhaustive, and there is more than one way to
+	// escape e.g. a quot (as "&#34;" or "&quot;") or an apos. We presume that
+	// the encoding/xml package tests xml.EscapeText more thoroughly. This test
+	// here is just a sanity check for this package's escapeXML function, and
+	// its attempt to provide a fast path (and avoid a bytes.Buffer allocation)
+	// when escaping filenames is obviously a no-op.
+	testCases := map[string]string{
+		"":              "",
+		" ":             " ",
+		"&":             "&amp;",
+		"*":             "*",
+		"+":             "+",
+		",":             ",",
+		"-":             "-",
+		".":             ".",
+		"/":             "/",
+		"0":             "0",
+		"9":             "9",
+		":":             ":",
+		"<":             "&lt;",
+		">":             "&gt;",
+		"A":             "A",
+		"_":             "_",
+		"a":             "a",
+		"~":             "~",
+		"\u0201":        "\u0201",
+		"&amp;":         "&amp;amp;",
+		"foo&<b/ar>baz": "foo&amp;&lt;b/ar&gt;baz",
+	}
+
+	for in, want := range testCases {
+		if got := escapeXML(in); got != want {
+			t.Errorf("in=%q: got %q, want %q", in, got, want)
+		}
+	}
+}
+
 func TestFilenameEscape(t *testing.T) {
 	hrefRe := regexp.MustCompile(`<D:href>([^<]*)</D:href>`)
 	displayNameRe := regexp.MustCompile(`<D:displayname>([^<]*)</D:displayname>`)