github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/godoc/vfs/namespace.go (about)

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package vfs
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	pathpkg "path"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  )
    16  
    17  // Setting debugNS = true will enable debugging prints about
    18  // name space translations.
    19  const debugNS = false
    20  
    21  // A NameSpace is a file system made up of other file systems
    22  // mounted at specific locations in the name space.
    23  //
    24  // The representation is a map from mount point locations
    25  // to the list of file systems mounted at that location.  A traditional
    26  // Unix mount table would use a single file system per mount point,
    27  // but we want to be able to mount multiple file systems on a single
    28  // mount point and have the system behave as if the union of those
    29  // file systems were present at the mount point.
    30  // For example, if the OS file system has a Go installation in
    31  // c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then
    32  // this name space creates the view we want for the godoc server:
    33  //
    34  //	NameSpace{
    35  //		"/": {
    36  //			{old: "/", fs: OS(`c:\Go`), new: "/"},
    37  //		},
    38  //		"/src/pkg": {
    39  //			{old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
    40  //			{old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
    41  //			{old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
    42  //		},
    43  //	}
    44  //
    45  // This is created by executing:
    46  //
    47  //	ns := NameSpace{}
    48  //	ns.Bind("/", OS(`c:\Go`), "/", BindReplace)
    49  //	ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", BindAfter)
    50  //	ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", BindAfter)
    51  //
    52  // A particular mount point entry is a triple (old, fs, new), meaning that to
    53  // operate on a path beginning with old, replace that prefix (old) with new
    54  // and then pass that path to the FileSystem implementation fs.
    55  //
    56  // If you do not explicitly mount a FileSystem at the root mountpoint "/" of the
    57  // NameSpace like above, Stat("/") will return a "not found" error which could
    58  // break typical directory traversal routines. In such cases, use NewNameSpace()
    59  // to get a NameSpace pre-initialized with an emulated empty directory at root.
    60  //
    61  // Given this name space, a ReadDir of /src/pkg/code will check each prefix
    62  // of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src,
    63  // then /), stopping when it finds one.  For the above example, /src/pkg/code
    64  // will find the mount point at /src/pkg:
    65  //
    66  //	{old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
    67  //	{old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
    68  //	{old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
    69  //
    70  // ReadDir will when execute these three calls and merge the results:
    71  //
    72  //	OS(`c:\Go`).ReadDir("/src/pkg/code")
    73  //	OS(`d:\Work1').ReadDir("/src/code")
    74  //	OS(`d:\Work2').ReadDir("/src/code")
    75  //
    76  // Note that the "/src/pkg" in "/src/pkg/code" has been replaced by
    77  // just "/src" in the final two calls.
    78  //
    79  // OS is itself an implementation of a file system: it implements
    80  // OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`).
    81  //
    82  // Because the new path is evaluated by fs (here OS(root)), another way
    83  // to read the mount table is to mentally combine fs+new, so that this table:
    84  //
    85  //	{old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
    86  //	{old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
    87  //	{old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
    88  //
    89  // reads as:
    90  //
    91  //	"/src/pkg" -> c:\Go\src\pkg
    92  //	"/src/pkg" -> d:\Work1\src
    93  //	"/src/pkg" -> d:\Work2\src
    94  //
    95  // An invariant (a redundancy) of the name space representation is that
    96  // ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s
    97  // mount table entries always have old == "/src/pkg").  The 'old' field is
    98  // useful to callers, because they receive just a []mountedFS and not any
    99  // other indication of which mount point was found.
   100  //
   101  type NameSpace map[string][]mountedFS
   102  
   103  // A mountedFS handles requests for path by replacing
   104  // a prefix 'old' with 'new' and then calling the fs methods.
   105  type mountedFS struct {
   106  	old string
   107  	fs  FileSystem
   108  	new string
   109  }
   110  
   111  // hasPathPrefix reports whether x == y or x == y + "/" + more.
   112  func hasPathPrefix(x, y string) bool {
   113  	return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/"))
   114  }
   115  
   116  // translate translates path for use in m, replacing old with new.
   117  //
   118  // mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code".
   119  func (m mountedFS) translate(path string) string {
   120  	path = pathpkg.Clean("/" + path)
   121  	if !hasPathPrefix(path, m.old) {
   122  		panic("translate " + path + " but old=" + m.old)
   123  	}
   124  	return pathpkg.Join(m.new, path[len(m.old):])
   125  }
   126  
   127  func (NameSpace) String() string {
   128  	return "ns"
   129  }
   130  
   131  // Fprint writes a text representation of the name space to w.
   132  func (ns NameSpace) Fprint(w io.Writer) {
   133  	fmt.Fprint(w, "name space {\n")
   134  	var all []string
   135  	for mtpt := range ns {
   136  		all = append(all, mtpt)
   137  	}
   138  	sort.Strings(all)
   139  	for _, mtpt := range all {
   140  		fmt.Fprintf(w, "\t%s:\n", mtpt)
   141  		for _, m := range ns[mtpt] {
   142  			fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new)
   143  		}
   144  	}
   145  	fmt.Fprint(w, "}\n")
   146  }
   147  
   148  // clean returns a cleaned, rooted path for evaluation.
   149  // It canonicalizes the path so that we can use string operations
   150  // to analyze it.
   151  func (NameSpace) clean(path string) string {
   152  	return pathpkg.Clean("/" + path)
   153  }
   154  
   155  type BindMode int
   156  
   157  const (
   158  	BindReplace BindMode = iota
   159  	BindBefore
   160  	BindAfter
   161  )
   162  
   163  // Bind causes references to old to redirect to the path new in newfs.
   164  // If mode is BindReplace, old redirections are discarded.
   165  // If mode is BindBefore, this redirection takes priority over existing ones,
   166  // but earlier ones are still consulted for paths that do not exist in newfs.
   167  // If mode is BindAfter, this redirection happens only after existing ones
   168  // have been tried and failed.
   169  func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) {
   170  	old = ns.clean(old)
   171  	new = ns.clean(new)
   172  	m := mountedFS{old, newfs, new}
   173  	var mtpt []mountedFS
   174  	switch mode {
   175  	case BindReplace:
   176  		mtpt = append(mtpt, m)
   177  	case BindAfter:
   178  		mtpt = append(mtpt, ns.resolve(old)...)
   179  		mtpt = append(mtpt, m)
   180  	case BindBefore:
   181  		mtpt = append(mtpt, m)
   182  		mtpt = append(mtpt, ns.resolve(old)...)
   183  	}
   184  
   185  	// Extend m.old, m.new in inherited mount point entries.
   186  	for i := range mtpt {
   187  		m := &mtpt[i]
   188  		if m.old != old {
   189  			if !hasPathPrefix(old, m.old) {
   190  				// This should not happen.  If it does, panic so
   191  				// that we can see the call trace that led to it.
   192  				panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new))
   193  			}
   194  			suffix := old[len(m.old):]
   195  			m.old = pathpkg.Join(m.old, suffix)
   196  			m.new = pathpkg.Join(m.new, suffix)
   197  		}
   198  	}
   199  
   200  	ns[old] = mtpt
   201  }
   202  
   203  // resolve resolves a path to the list of mountedFS to use for path.
   204  func (ns NameSpace) resolve(path string) []mountedFS {
   205  	path = ns.clean(path)
   206  	for {
   207  		if m := ns[path]; m != nil {
   208  			if debugNS {
   209  				fmt.Printf("resolve %s: %v\n", path, m)
   210  			}
   211  			return m
   212  		}
   213  		if path == "/" {
   214  			break
   215  		}
   216  		path = pathpkg.Dir(path)
   217  	}
   218  	return nil
   219  }
   220  
   221  // Open implements the FileSystem Open method.
   222  func (ns NameSpace) Open(path string) (ReadSeekCloser, error) {
   223  	var err error
   224  	for _, m := range ns.resolve(path) {
   225  		if debugNS {
   226  			fmt.Printf("tx %s: %v\n", path, m.translate(path))
   227  		}
   228  		tp := m.translate(path)
   229  		r, err1 := m.fs.Open(tp)
   230  		if err1 == nil {
   231  			return r, nil
   232  		}
   233  		// IsNotExist errors in overlay FSes can mask real errors in
   234  		// the underlying FS, so ignore them if there is another error.
   235  		if err == nil || os.IsNotExist(err) {
   236  			err = err1
   237  		}
   238  	}
   239  	if err == nil {
   240  		err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
   241  	}
   242  	return nil, err
   243  }
   244  
   245  // stat implements the FileSystem Stat and Lstat methods.
   246  func (ns NameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) {
   247  	var err error
   248  	for _, m := range ns.resolve(path) {
   249  		fi, err1 := f(m.fs, m.translate(path))
   250  		if err1 == nil {
   251  			return fi, nil
   252  		}
   253  		if err == nil {
   254  			err = err1
   255  		}
   256  	}
   257  	if err == nil {
   258  		err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist}
   259  	}
   260  	return nil, err
   261  }
   262  
   263  func (ns NameSpace) Stat(path string) (os.FileInfo, error) {
   264  	return ns.stat(path, FileSystem.Stat)
   265  }
   266  
   267  func (ns NameSpace) Lstat(path string) (os.FileInfo, error) {
   268  	return ns.stat(path, FileSystem.Lstat)
   269  }
   270  
   271  // dirInfo is a trivial implementation of os.FileInfo for a directory.
   272  type dirInfo string
   273  
   274  func (d dirInfo) Name() string       { return string(d) }
   275  func (d dirInfo) Size() int64        { return 0 }
   276  func (d dirInfo) Mode() os.FileMode  { return os.ModeDir | 0555 }
   277  func (d dirInfo) ModTime() time.Time { return startTime }
   278  func (d dirInfo) IsDir() bool        { return true }
   279  func (d dirInfo) Sys() interface{}   { return nil }
   280  
   281  var startTime = time.Now()
   282  
   283  // ReadDir implements the FileSystem ReadDir method.  It's where most of the magic is.
   284  // (The rest is in resolve.)
   285  //
   286  // Logically, ReadDir must return the union of all the directories that are named
   287  // by path.  In order to avoid misinterpreting Go packages, of all the directories
   288  // that contain Go source code, we only include the files from the first,
   289  // but we include subdirectories from all.
   290  //
   291  // ReadDir must also return directory entries needed to reach mount points.
   292  // If the name space looks like the example in the type NameSpace comment,
   293  // but c:\Go does not have a src/pkg subdirectory, we still want to be able
   294  // to find that subdirectory, because we've mounted d:\Work1 and d:\Work2
   295  // there.  So if we don't see "src" in the directory listing for c:\Go, we add an
   296  // entry for it before returning.
   297  //
   298  func (ns NameSpace) ReadDir(path string) ([]os.FileInfo, error) {
   299  	path = ns.clean(path)
   300  
   301  	// List matching directories and determine whether any of them contain
   302  	// Go files.
   303  	var (
   304  		dirs       [][]os.FileInfo
   305  		goDirIndex = -1
   306  		readDirErr error
   307  	)
   308  
   309  	for _, m := range ns.resolve(path) {
   310  		dir, err := m.fs.ReadDir(m.translate(path))
   311  		if err != nil {
   312  			if readDirErr == nil {
   313  				readDirErr = err
   314  			}
   315  			continue
   316  		}
   317  
   318  		dirs = append(dirs, dir)
   319  
   320  		if goDirIndex < 0 {
   321  			for _, f := range dir {
   322  				if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") {
   323  					goDirIndex = len(dirs) - 1
   324  					break
   325  				}
   326  			}
   327  		}
   328  	}
   329  
   330  	// Build a list of files and subdirectories. If a directory contains Go files,
   331  	// only include files from that directory. Otherwise, include files from
   332  	// all directories. Include subdirectories from all directories regardless
   333  	// of whether Go files are present.
   334  	haveName := make(map[string]bool)
   335  	var all []os.FileInfo
   336  	for i, dir := range dirs {
   337  		for _, f := range dir {
   338  			name := f.Name()
   339  			if !haveName[name] && (f.IsDir() || goDirIndex < 0 || goDirIndex == i) {
   340  				all = append(all, f)
   341  				haveName[name] = true
   342  			}
   343  		}
   344  	}
   345  
   346  	// Add any missing directories needed to reach mount points.
   347  	for old := range ns {
   348  		if hasPathPrefix(old, path) && old != path {
   349  			// Find next element after path in old.
   350  			elem := old[len(path):]
   351  			elem = strings.TrimPrefix(elem, "/")
   352  			if i := strings.Index(elem, "/"); i >= 0 {
   353  				elem = elem[:i]
   354  			}
   355  			if !haveName[elem] {
   356  				haveName[elem] = true
   357  				all = append(all, dirInfo(elem))
   358  			}
   359  		}
   360  	}
   361  
   362  	if len(all) == 0 {
   363  		return nil, readDirErr
   364  	}
   365  
   366  	sort.Sort(byName(all))
   367  	return all, nil
   368  }
   369  
   370  // RootType returns the RootType for the given path in the namespace.
   371  func (ns NameSpace) RootType(path string) RootType {
   372  	// We resolve the given path to a list of mountedFS and then return
   373  	// the root type for the filesystem which contains the path.
   374  	for _, m := range ns.resolve(path) {
   375  		_, err := m.fs.ReadDir(m.translate(path))
   376  		// Found a match, return the filesystem's root type
   377  		if err == nil {
   378  			return m.fs.RootType(path)
   379  		}
   380  	}
   381  	return ""
   382  }
   383  
   384  // byName implements sort.Interface.
   385  type byName []os.FileInfo
   386  
   387  func (f byName) Len() int           { return len(f) }
   388  func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
   389  func (f byName) Swap(i, j int)      { f[i], f[j] = f[j], f[i] }