golang.org/x/tools@v0.21.0/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 type NameSpace map[string][]mountedFS 101 102 // A mountedFS handles requests for path by replacing 103 // a prefix 'old' with 'new' and then calling the fs methods. 104 type mountedFS struct { 105 old string 106 fs FileSystem 107 new string 108 } 109 110 // hasPathPrefix reports whether x == y or x == y + "/" + more. 111 func hasPathPrefix(x, y string) bool { 112 return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/")) 113 } 114 115 // translate translates path for use in m, replacing old with new. 116 // 117 // mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code". 118 func (m mountedFS) translate(path string) string { 119 path = pathpkg.Clean("/" + path) 120 if !hasPathPrefix(path, m.old) { 121 panic("translate " + path + " but old=" + m.old) 122 } 123 return pathpkg.Join(m.new, path[len(m.old):]) 124 } 125 126 func (NameSpace) String() string { 127 return "ns" 128 } 129 130 // Fprint writes a text representation of the name space to w. 131 func (ns NameSpace) Fprint(w io.Writer) { 132 fmt.Fprint(w, "name space {\n") 133 var all []string 134 for mtpt := range ns { 135 all = append(all, mtpt) 136 } 137 sort.Strings(all) 138 for _, mtpt := range all { 139 fmt.Fprintf(w, "\t%s:\n", mtpt) 140 for _, m := range ns[mtpt] { 141 fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new) 142 } 143 } 144 fmt.Fprint(w, "}\n") 145 } 146 147 // clean returns a cleaned, rooted path for evaluation. 148 // It canonicalizes the path so that we can use string operations 149 // to analyze it. 150 func (NameSpace) clean(path string) string { 151 return pathpkg.Clean("/" + path) 152 } 153 154 type BindMode int 155 156 const ( 157 BindReplace BindMode = iota 158 BindBefore 159 BindAfter 160 ) 161 162 // Bind causes references to old to redirect to the path new in newfs. 163 // If mode is BindReplace, old redirections are discarded. 164 // If mode is BindBefore, this redirection takes priority over existing ones, 165 // but earlier ones are still consulted for paths that do not exist in newfs. 166 // If mode is BindAfter, this redirection happens only after existing ones 167 // have been tried and failed. 168 func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) { 169 old = ns.clean(old) 170 new = ns.clean(new) 171 m := mountedFS{old, newfs, new} 172 var mtpt []mountedFS 173 switch mode { 174 case BindReplace: 175 mtpt = append(mtpt, m) 176 case BindAfter: 177 mtpt = append(mtpt, ns.resolve(old)...) 178 mtpt = append(mtpt, m) 179 case BindBefore: 180 mtpt = append(mtpt, m) 181 mtpt = append(mtpt, ns.resolve(old)...) 182 } 183 184 // Extend m.old, m.new in inherited mount point entries. 185 for i := range mtpt { 186 m := &mtpt[i] 187 if m.old != old { 188 if !hasPathPrefix(old, m.old) { 189 // This should not happen. If it does, panic so 190 // that we can see the call trace that led to it. 191 panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new)) 192 } 193 suffix := old[len(m.old):] 194 m.old = pathpkg.Join(m.old, suffix) 195 m.new = pathpkg.Join(m.new, suffix) 196 } 197 } 198 199 ns[old] = mtpt 200 } 201 202 // resolve resolves a path to the list of mountedFS to use for path. 203 func (ns NameSpace) resolve(path string) []mountedFS { 204 path = ns.clean(path) 205 for { 206 if m := ns[path]; m != nil { 207 if debugNS { 208 fmt.Printf("resolve %s: %v\n", path, m) 209 } 210 return m 211 } 212 if path == "/" { 213 break 214 } 215 path = pathpkg.Dir(path) 216 } 217 return nil 218 } 219 220 // Open implements the FileSystem Open method. 221 func (ns NameSpace) Open(path string) (ReadSeekCloser, error) { 222 var err error 223 for _, m := range ns.resolve(path) { 224 if debugNS { 225 fmt.Printf("tx %s: %v\n", path, m.translate(path)) 226 } 227 tp := m.translate(path) 228 r, err1 := m.fs.Open(tp) 229 if err1 == nil { 230 return r, nil 231 } 232 // IsNotExist errors in overlay FSes can mask real errors in 233 // the underlying FS, so ignore them if there is another error. 234 if err == nil || os.IsNotExist(err) { 235 err = err1 236 } 237 } 238 if err == nil { 239 err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} 240 } 241 return nil, err 242 } 243 244 // stat implements the FileSystem Stat and Lstat methods. 245 func (ns NameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) { 246 var err error 247 for _, m := range ns.resolve(path) { 248 fi, err1 := f(m.fs, m.translate(path)) 249 if err1 == nil { 250 return fi, nil 251 } 252 if err == nil { 253 err = err1 254 } 255 } 256 if err == nil { 257 err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist} 258 } 259 return nil, err 260 } 261 262 func (ns NameSpace) Stat(path string) (os.FileInfo, error) { 263 return ns.stat(path, FileSystem.Stat) 264 } 265 266 func (ns NameSpace) Lstat(path string) (os.FileInfo, error) { 267 return ns.stat(path, FileSystem.Lstat) 268 } 269 270 // dirInfo is a trivial implementation of os.FileInfo for a directory. 271 type dirInfo string 272 273 func (d dirInfo) Name() string { return string(d) } 274 func (d dirInfo) Size() int64 { return 0 } 275 func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 } 276 func (d dirInfo) ModTime() time.Time { return startTime } 277 func (d dirInfo) IsDir() bool { return true } 278 func (d dirInfo) Sys() interface{} { return nil } 279 280 var startTime = time.Now() 281 282 // ReadDir implements the FileSystem ReadDir method. It's where most of the magic is. 283 // (The rest is in resolve.) 284 // 285 // Logically, ReadDir must return the union of all the directories that are named 286 // by path. In order to avoid misinterpreting Go packages, of all the directories 287 // that contain Go source code, we only include the files from the first, 288 // but we include subdirectories from all. 289 // 290 // ReadDir must also return directory entries needed to reach mount points. 291 // If the name space looks like the example in the type NameSpace comment, 292 // but c:\Go does not have a src/pkg subdirectory, we still want to be able 293 // to find that subdirectory, because we've mounted d:\Work1 and d:\Work2 294 // there. So if we don't see "src" in the directory listing for c:\Go, we add an 295 // entry for it before returning. 296 func (ns NameSpace) ReadDir(path string) ([]os.FileInfo, error) { 297 path = ns.clean(path) 298 299 // List matching directories and determine whether any of them contain 300 // Go files. 301 var ( 302 dirs [][]os.FileInfo 303 goDirIndex = -1 304 readDirErr error 305 ) 306 307 for _, m := range ns.resolve(path) { 308 dir, err := m.fs.ReadDir(m.translate(path)) 309 if err != nil { 310 if readDirErr == nil { 311 readDirErr = err 312 } 313 continue 314 } 315 316 dirs = append(dirs, dir) 317 318 if goDirIndex < 0 { 319 for _, f := range dir { 320 if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") { 321 goDirIndex = len(dirs) - 1 322 break 323 } 324 } 325 } 326 } 327 328 // Build a list of files and subdirectories. If a directory contains Go files, 329 // only include files from that directory. Otherwise, include files from 330 // all directories. Include subdirectories from all directories regardless 331 // of whether Go files are present. 332 haveName := make(map[string]bool) 333 var all []os.FileInfo 334 for i, dir := range dirs { 335 for _, f := range dir { 336 name := f.Name() 337 if !haveName[name] && (f.IsDir() || goDirIndex < 0 || goDirIndex == i) { 338 all = append(all, f) 339 haveName[name] = true 340 } 341 } 342 } 343 344 // Add any missing directories needed to reach mount points. 345 for old := range ns { 346 if hasPathPrefix(old, path) && old != path { 347 // Find next element after path in old. 348 elem := old[len(path):] 349 elem = strings.TrimPrefix(elem, "/") 350 if i := strings.Index(elem, "/"); i >= 0 { 351 elem = elem[:i] 352 } 353 if !haveName[elem] { 354 haveName[elem] = true 355 all = append(all, dirInfo(elem)) 356 } 357 } 358 } 359 360 if len(all) == 0 { 361 return nil, readDirErr 362 } 363 364 sort.Sort(byName(all)) 365 return all, nil 366 } 367 368 // RootType returns the RootType for the given path in the namespace. 369 func (ns NameSpace) RootType(path string) RootType { 370 // We resolve the given path to a list of mountedFS and then return 371 // the root type for the filesystem which contains the path. 372 for _, m := range ns.resolve(path) { 373 _, err := m.fs.ReadDir(m.translate(path)) 374 // Found a match, return the filesystem's root type 375 if err == nil { 376 return m.fs.RootType(path) 377 } 378 } 379 return "" 380 } 381 382 // byName implements sort.Interface. 383 type byName []os.FileInfo 384 385 func (f byName) Len() int { return len(f) } 386 func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() } 387 func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }