From 936084890addeb6a59af6777f071465fd149c9c8 Mon Sep 17 00:00:00 2001 From: Hariharan Srinath Date: Mon, 15 Feb 2016 07:44:09 +0530 Subject: [PATCH] x/tools/godoc/vfs: adds NewNameSpace() and emptyVFS for better behavior The existing implementation of NameSpace implicitly assumes that a FileSystem with a directory at the top will be mounted at the root mount point "/" of the NameSpace. If this is not the case, then Stat("/") will fail even if ReadDir("/") succeedes. This is unexpected behavior which breaks directory traversal routines (eg. http.FileServer). This CL adds an unexported implementation of FileSystem called emptyVFS that emulates an empty directory and adds a NewNameSpace() function that binds emptyVFS to "/" so that unexpected behavior does not arise even if the use does not mount anything explicitly at "/". Latest patch set causes the FileInfo of the empty top level emulated directory to return "/" for Name() and Zero Time for ModTime() and removes the related struct state fields being used in the previous implementation. Fixes golang/go#14190 Change-Id: Idce2fc3c9b81206847a33840d76b660059d53d18 Reviewed-on: https://go-review.googlesource.com/19445 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick --- godoc/vfs/emptyvfs.go | 85 ++++++++++++++++++++++++++++++++++++++ godoc/vfs/emptyvfs_test.go | 57 +++++++++++++++++++++++++ godoc/vfs/namespace.go | 5 +++ 3 files changed, 147 insertions(+) create mode 100644 godoc/vfs/emptyvfs.go create mode 100644 godoc/vfs/emptyvfs_test.go diff --git a/godoc/vfs/emptyvfs.go b/godoc/vfs/emptyvfs.go new file mode 100644 index 0000000000..01b6942f0a --- /dev/null +++ b/godoc/vfs/emptyvfs.go @@ -0,0 +1,85 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vfs + +import ( + "fmt" + "os" + "time" +) + +// NewNameSpace returns a NameSpace pre-initialized with an empty +// emulated directory mounted on the root mount point "/". This +// allows directory traversal routines to work properly even if +// a folder is not explicitly mounted at root by the user. +func NewNameSpace() NameSpace { + ns := NameSpace{} + ns.Bind("/", &emptyVFS{}, "/", BindReplace) + return ns +} + +// type emptyVFS emulates a FileSystem consisting of an empty directory +type emptyVFS struct{} + +// Open implements Opener. Since emptyVFS is an empty directory, all +// attempts to open a file should returns errors. +func (e *emptyVFS) Open(path string) (ReadSeekCloser, error) { + if path == "/" { + return nil, fmt.Errorf("open: / is a directory") + } + return nil, os.ErrNotExist +} + +// Stat returns os.FileInfo for an empty directory if the path is +// is root "/" or error. os.FileInfo is implemented by emptyVFS +func (e *emptyVFS) Stat(path string) (os.FileInfo, error) { + if path == "/" { + return e, nil + } + return nil, os.ErrNotExist +} + +func (e *emptyVFS) Lstat(path string) (os.FileInfo, error) { + return e.Stat(path) +} + +// ReadDir returns an empty os.FileInfo slice for "/", else error. +func (e *emptyVFS) ReadDir(path string) ([]os.FileInfo, error) { + if path == "/" { + return []os.FileInfo{}, nil + } + return nil, os.ErrNotExist +} + +func (e *emptyVFS) String() string { + return "emptyVFS(/)" +} + +// These functions below implement os.FileInfo for the single +// empty emulated directory. + +func (e *emptyVFS) Name() string { + return "/" +} + +func (e *emptyVFS) Size() int64 { + return 0 +} + +func (e *emptyVFS) Mode() os.FileMode { + return os.ModeDir | os.ModePerm +} + +func (e *emptyVFS) ModTime() time.Time { + return time.Time{} +} + +func (e *emptyVFS) IsDir() bool { + return true +} + +func (e *emptyVFS) Sys() interface{} { + return nil +} diff --git a/godoc/vfs/emptyvfs_test.go b/godoc/vfs/emptyvfs_test.go new file mode 100644 index 0000000000..2d8d72e6ba --- /dev/null +++ b/godoc/vfs/emptyvfs_test.go @@ -0,0 +1,57 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vfs_test + +import ( + "golang.org/x/tools/godoc/vfs" + "golang.org/x/tools/godoc/vfs/mapfs" + "testing" + "time" +) + +func TestNewNameSpace(t *testing.T) { + + // We will mount this filesystem under /fs1 + mount := mapfs.New(map[string]string{"fs1file": "abcdefgh"}) + + // Existing process. This should give error on Stat("/") + t1 := vfs.NameSpace{} + t1.Bind("/fs1", mount, "/", vfs.BindReplace) + + // using NewNameSpace. This should work fine. + t2 := vfs.NewNameSpace() + t2.Bind("/fs1", mount, "/", vfs.BindReplace) + + testcases := map[string][]bool{ + "/": []bool{false, true}, + "/fs1": []bool{true, true}, + "/fs1/fs1file": []bool{true, true}, + } + + fss := []vfs.FileSystem{t1, t2} + + for j, fs := range fss { + for k, v := range testcases { + _, err := fs.Stat(k) + result := err == nil + if result != v[j] { + t.Errorf("fs: %d, testcase: %s, want: %v, got: %v, err: %s", j, k, v[j], result, err) + } + } + } + + fi, err := t2.Stat("/") + if err != nil { + t.Fatal(err) + } + + if fi.Name() != "/" { + t.Errorf("t2.Name() : want:%s got:%s", "/", fi.Name()) + } + + if !fi.ModTime().IsZero() { + t.Errorf("t2.Modime() : want:%v got:%v", time.Time{}, fi.ModTime()) + } +} diff --git a/godoc/vfs/namespace.go b/godoc/vfs/namespace.go index dbba20cb7c..5a31fe61ea 100644 --- a/godoc/vfs/namespace.go +++ b/godoc/vfs/namespace.go @@ -53,6 +53,11 @@ const debugNS = false // operate on a path beginning with old, replace that prefix (old) with new // and then pass that path to the FileSystem implementation fs. // +// If you do not explicitly mount a FileSystem at the root mountpoint "/" of the +// NameSpace like above, Stat("/") will return a "not found" error which could +// break typical directory traversal routines. In such cases, use NewNameSpace() +// to get a NameSpace pre-initialized with an emulated empty directory at root. +// // Given this name space, a ReadDir of /src/pkg/code will check each prefix // of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src, // then /), stopping when it finds one. For the above example, /src/pkg/code