github.com/v2fly/tools@v0.100.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 // 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] }