webdav: make the memFS (not the memFSNode) the canonical source of a
file name.

This fixes inconsistent stat names after a file is renamed.

Change-Id: Ie90f8abaa31d46a87834266053b61d7770f854e2
Reviewed-on: https://go-review.googlesource.com/3051
Reviewed-by: Nick Cooper <nmvc@google.com>
Reviewed-by: Dave Cheney <dave@cheney.net>
diff --git a/webdav/file.go b/webdav/file.go
index eac339f..eb6abd5 100644
--- a/webdav/file.go
+++ b/webdav/file.go
@@ -194,7 +194,7 @@
 				Err:  os.ErrNotExist,
 			}
 		}
-		if !child.IsDir() {
+		if !child.mode.IsDir() {
 			return &os.PathError{
 				Op:   op,
 				Path: original,
@@ -245,7 +245,6 @@
 		return os.ErrExist
 	}
 	dir.children[frag] = &memFSNode{
-		name:     frag,
 		children: make(map[string]*memFSNode),
 		mode:     perm.Perm() | os.ModeDir,
 		modTime:  time.Now(),
@@ -267,7 +266,7 @@
 		if flag&(os.O_WRONLY|os.O_RDWR) != 0 {
 			return nil, os.ErrPermission
 		}
-		n = &fs.root
+		n, frag = &fs.root, "/"
 
 	} else {
 		n = dir.children[frag]
@@ -281,7 +280,6 @@
 			}
 			if n == nil {
 				n = &memFSNode{
-					name: frag,
 					mode: perm.Perm(),
 				}
 				dir.children[frag] = n
@@ -298,11 +296,12 @@
 	}
 
 	children := make([]os.FileInfo, 0, len(n.children))
-	for _, c := range n.children {
-		children = append(children, c)
+	for cName, c := range n.children {
+		children = append(children, c.stat(cName))
 	}
 	return &memFile{
 		n:                n,
+		nameSnapshot:     frag,
 		childrenSnapshot: children,
 	}, nil
 }
@@ -359,12 +358,9 @@
 	if !ok {
 		return os.ErrNotExist
 	}
-	if oNode.IsDir() {
+	if oNode.children != nil {
 		if nNode, ok := nDir.children[nFrag]; ok {
-			nNode.mu.Lock()
-			isDir := nNode.mode.IsDir()
-			nNode.mu.Unlock()
-			if !isDir {
+			if nNode.children == nil {
 				return errNotADirectory
 			}
 			if len(nNode.children) != 0 {
@@ -387,10 +383,10 @@
 	}
 	if dir == nil {
 		// We're stat'ting the root.
-		return &fs.root, nil
+		return fs.root.stat("/"), nil
 	}
 	if n, ok := dir.children[frag]; ok {
-		return n, nil
+		return n.stat(path.Base(name)), nil
 	}
 	return nil, os.ErrNotExist
 }
@@ -398,53 +394,46 @@
 // A memFSNode represents a single entry in the in-memory filesystem and also
 // implements os.FileInfo.
 type memFSNode struct {
-	name string
-
 	// children is protected by memFS.mu.
 	children map[string]*memFSNode
 
 	mu      sync.Mutex
-	modTime time.Time
-	mode    os.FileMode
 	data    []byte
+	mode    os.FileMode
+	modTime time.Time
 }
 
-func (n *memFSNode) Name() string {
-	return n.name
-}
-
-func (n *memFSNode) Size() int64 {
+func (n *memFSNode) stat(name string) *memFileInfo {
 	n.mu.Lock()
 	defer n.mu.Unlock()
-	return int64(len(n.data))
+	return &memFileInfo{
+		name:    name,
+		size:    int64(len(n.data)),
+		mode:    n.mode,
+		modTime: n.modTime,
+	}
 }
 
-func (n *memFSNode) Mode() os.FileMode {
-	n.mu.Lock()
-	defer n.mu.Unlock()
-	return n.mode
+type memFileInfo struct {
+	name    string
+	size    int64
+	mode    os.FileMode
+	modTime time.Time
 }
 
-func (n *memFSNode) ModTime() time.Time {
-	n.mu.Lock()
-	defer n.mu.Unlock()
-	return n.modTime
-}
-
-func (n *memFSNode) IsDir() bool {
-	return n.Mode().IsDir()
-}
-
-func (n *memFSNode) Sys() interface{} {
-	return nil
-}
+func (f *memFileInfo) Name() string       { return f.name }
+func (f *memFileInfo) Size() int64        { return f.size }
+func (f *memFileInfo) Mode() os.FileMode  { return f.mode }
+func (f *memFileInfo) ModTime() time.Time { return f.modTime }
+func (f *memFileInfo) IsDir() bool        { return f.mode.IsDir() }
+func (f *memFileInfo) Sys() interface{}   { return nil }
 
 // A memFile is a File implementation for a memFSNode. It is a per-file (not
-// per-node) read/write position, and if the node is a directory, a snapshot of
-// that node's children.
+// per-node) read/write position, and a snapshot of the memFS' tree structure
+// (a node's name and children) for that node.
 type memFile struct {
-	n *memFSNode
-	// childrenSnapshot is a snapshot of n.children.
+	n                *memFSNode
+	nameSnapshot     string
 	childrenSnapshot []os.FileInfo
 	// pos is protected by n.mu.
 	pos int
@@ -518,7 +507,7 @@
 }
 
 func (f *memFile) Stat() (os.FileInfo, error) {
-	return f.n, nil
+	return f.n.stat(f.nameSnapshot), nil
 }
 
 func (f *memFile) Write(p []byte) (int, error) {
diff --git a/webdav/file_test.go b/webdav/file_test.go
index a9bb3b7..6601fce 100644
--- a/webdav/file_test.go
+++ b/webdav/file_test.go
@@ -11,6 +11,8 @@
 	"os"
 	"path"
 	"path/filepath"
+	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 	"testing"
@@ -202,10 +204,10 @@
 			}
 		}
 
-		i := 0
+		i, prevFrag := 0, ""
 		err := fs.walk("test", tc.dir, func(dir *memFSNode, frag string, final bool) error {
 			got := walkStep{
-				name:  dir.name,
+				name:  prevFrag,
 				frag:  frag,
 				final: final,
 			}
@@ -214,7 +216,7 @@
 			if got != want {
 				return fmt.Errorf("got %+v, want %+v", got, want)
 			}
-			i++
+			i, prevFrag = i+1, frag
 			return nil
 		})
 		if err != nil {
@@ -223,6 +225,36 @@
 	}
 }
 
+// find appends to ss the names of the named file and its children. It is
+// analogous to the Unix find command.
+//
+// The returned strings are not guaranteed to be in any particular order.
+func find(ss []string, fs FileSystem, name string) ([]string, error) {
+	stat, err := fs.Stat(name)
+	if err != nil {
+		return nil, err
+	}
+	ss = append(ss, name)
+	if stat.IsDir() {
+		f, err := fs.OpenFile(name, os.O_RDONLY, 0)
+		if err != nil {
+			return nil, err
+		}
+		defer f.Close()
+		children, err := f.Readdir(-1)
+		if err != nil {
+			return nil, err
+		}
+		for _, c := range children {
+			ss, err = find(ss, fs, path.Join(name, c.Name()))
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	return ss, nil
+}
+
 func testFS(t *testing.T, fs FileSystem) {
 	errStr := func(err error) string {
 		switch {
@@ -236,8 +268,8 @@
 		return "ok"
 	}
 
-	// The non-"stat" test cases should change the file system state. The
-	// indentation of the "stat"s helps distinguish such test cases.
+	// The non-"find" non-"stat" test cases should change the file system state. The
+	// indentation of the "find"s and "stat"s helps distinguish such test cases.
 	testCases := []string{
 		"  stat / want dir",
 		"  stat /a want errNotExist",
@@ -252,6 +284,7 @@
 		"  stat /d want dir",
 		"create /d/e EEE want ok",
 		"  stat /d/e want 3",
+		"  find / /a /d /d/e",
 		"create /d/f FFFF want ok",
 		"create /d/g GGGGGGG want ok",
 		"mk-dir /d/m want ok",
@@ -263,6 +296,7 @@
 		"  stat /d/h want errNotExist",
 		"  stat /d/m want dir",
 		"  stat /d/m/p want 5",
+		"  find / /a /d /d/e /d/f /d/g /d/m /d/m/p",
 		"rm-all /d want ok",
 		"  stat /a want 1",
 		"  stat /d want errNotExist",
@@ -271,6 +305,7 @@
 		"  stat /d/g want errNotExist",
 		"  stat /d/m want errNotExist",
 		"  stat /d/m/p want errNotExist",
+		"  find / /a",
 		"mk-dir /d/m want errNotExist",
 		"mk-dir /d want ok",
 		"create /d/f FFFF want ok",
@@ -285,6 +320,7 @@
 		"  stat /c want errNotExist",
 		"  stat /d want dir",
 		"  stat /d/m want dir",
+		"  find / /a /b /d /d/m",
 		"rename /b /b want ok",
 		"  stat /b want 2",
 		"  stat /c want errNotExist",
@@ -293,6 +329,7 @@
 		"  stat /c want 2",
 		"  stat /d/m want dir",
 		"  stat /d/n want errNotExist",
+		"  find / /a /c /d /d/m",
 		"rename /d/m /d/n want ok",
 		"create /d/n/q QQQQ want ok",
 		"  stat /d/m want errNotExist",
@@ -302,6 +339,7 @@
 		"rename /c /d/n/q want ok",
 		"  stat /c want errNotExist",
 		"  stat /d/n/q want 2",
+		"  find / /a /d /d/n /d/n/q",
 		"create /d/n/r RRRRR want ok",
 		"mk-dir /u want ok",
 		"mk-dir /u/v want ok",
@@ -316,10 +354,12 @@
 		"  stat /t want dir",
 		"  stat /t/q want 2",
 		"  stat /t/r want 5",
+		"  find / /a /d /t /t/q /t/r /u /u/v",
 		"rename /t / want err",
 		"rename /t /u/v want ok",
 		"  stat /u/v/r want 5",
 		"rename / /x want err",
+		"  find / /a /d /u /u/v /u/v/q /u/v/r",
 	}
 
 	for i, tc := range testCases {
@@ -352,6 +392,17 @@
 				}
 			}
 
+		case "find":
+			got, err := find(nil, fs, "/")
+			if err != nil {
+				t.Fatalf("test case #%d %q: find: %v", i, tc, err)
+			}
+			sort.Strings(got)
+			want := strings.Split(arg, " ")
+			if !reflect.DeepEqual(got, want) {
+				t.Fatalf("test case #%d %q:\ngot  %s\nwant %s", i, tc, got, want)
+			}
+
 		case "mk-dir", "rename", "rm-all", "stat":
 			nParts := 3
 			if op == "rename" {
@@ -372,12 +423,22 @@
 				opErr = fs.RemoveAll(parts[0])
 			case "stat":
 				var stat os.FileInfo
-				if stat, opErr = fs.Stat(parts[0]); opErr == nil {
+				fileName := parts[0]
+				if stat, opErr = fs.Stat(fileName); opErr == nil {
 					if stat.IsDir() {
 						got = "dir"
 					} else {
 						got = strconv.Itoa(int(stat.Size()))
 					}
+
+					if fileName == "/" {
+						// For a Dir FileSystem, the virtual file system root maps to a
+						// real file system name like "/tmp/webdav-test012345", which does
+						// not end with "/". We skip such cases.
+					} else if statName := stat.Name(); path.Base(fileName) != statName {
+						t.Fatalf("test case #%d %q: file name %q inconsistent with stat name %q",
+							i, tc, fileName, statName)
+					}
 				}
 			}
 			if got == "" {