x/net/webdav: add a Dir type, analogous to http.Dir.

LGTM=nmvc, dave
R=dave, nmvc
CC=bradfitz, dr.volker.dobler, golang-codereviews, robert.stepanek
https://golang.org/cl/173100044
diff --git a/webdav/file.go b/webdav/file.go
index a2953ae..097fe6b 100644
--- a/webdav/file.go
+++ b/webdav/file.go
@@ -8,21 +8,84 @@
 	"io"
 	"net/http"
 	"os"
+	"path"
+	"path/filepath"
+	"strings"
 )
 
-// TODO: comment that paths are always "/"-separated, even for Windows servers.
-
+// A FileSystem implements access to a collection of named files. The elements
+// in a file path are separated by slash ('/', U+002F) characters, regardless
+// of host operating system convention.
+//
+// Each method has the same semantics as the os package's function of the same
+// name.
 type FileSystem interface {
-	Mkdir(path string, perm os.FileMode) error
-	OpenFile(path string, flag int, perm os.FileMode) (File, error)
-	RemoveAll(path string) error
-	Stat(path string) (os.FileInfo, error)
+	Mkdir(name string, perm os.FileMode) error
+	OpenFile(name string, flag int, perm os.FileMode) (File, error)
+	RemoveAll(name string) error
+	Stat(name string) (os.FileInfo, error)
 }
 
+// A File is returned by a FileSystem's OpenFile method and can be served by a
+// Handler.
 type File interface {
 	http.File
 	io.Writer
 }
 
+// A Dir implements FileSystem using the native file system restricted to a
+// specific directory tree.
+//
+// While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's
+// string value is a filename on the native file system, not a URL, so it is
+// separated by filepath.Separator, which isn't necessarily '/'.
+//
+// An empty Dir is treated as ".".
+type Dir string
+
+func (d Dir) resolve(name string) string {
+	// This implementation is based on Dir.Open's code in the standard net/http package.
+	if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
+		strings.Contains(name, "\x00") {
+		return ""
+	}
+	dir := string(d)
+	if dir == "" {
+		dir = "."
+	}
+	return filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
+}
+
+func (d Dir) Mkdir(name string, perm os.FileMode) error {
+	if name = d.resolve(name); name == "" {
+		return os.ErrNotExist
+	}
+	return os.Mkdir(name, perm)
+}
+
+func (d Dir) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
+	if name = d.resolve(name); name == "" {
+		return nil, os.ErrNotExist
+	}
+	return os.OpenFile(name, flag, perm)
+}
+
+func (d Dir) RemoveAll(name string) error {
+	if name = d.resolve(name); name == "" {
+		return os.ErrNotExist
+	}
+	if name == filepath.Clean(string(d)) {
+		// Prohibit removing the virtual root directory.
+		return os.ErrInvalid
+	}
+	return os.RemoveAll(name)
+}
+
+func (d Dir) Stat(name string) (os.FileInfo, error) {
+	if name = d.resolve(name); name == "" {
+		return nil, os.ErrNotExist
+	}
+	return os.Stat(name)
+}
+
 // TODO: a MemFS implementation.
-// TODO: a RealFS implementation, backed by the real, OS-provided file system.