diff --git a/lib/godoc/package.html b/lib/godoc/package.html index fdebbf5d9180f4830c7f0566459448f731c51932..c326e34cfc66b62481b5001505607b8d27e18726 100644 --- a/lib/godoc/package.html +++ b/lib/godoc/package.html @@ -70,7 +70,7 @@ <p> <span style="font-size:90%"> {{range .}} - <a href="/{{.|srcLink}}">{{.|filename|html}}</a> + <a href="{{.|srcLink|html}}">{{.|filename|html}}</a> {{end}} </span> </p> diff --git a/src/cmd/godoc/appinit.go b/src/cmd/godoc/appinit.go index e65be40945d014fc50943e923ac22d222e3a38ad..343e196f261942fd0c8593fe199a83d691a65655 100644 --- a/src/cmd/godoc/appinit.go +++ b/src/cmd/godoc/appinit.go @@ -42,8 +42,7 @@ func init() { log.Fatalf("%s: %s\n", zipfile, err) } // rc is never closed (app running forever) - fs = NewZipFS(rc) - fsHttp = NewHttpZipFS(rc, *goroot) + fs.Bind("/", NewZipFS(rc, zipFilename), "/", bindReplace) // initialize http handlers readTemplates() @@ -53,9 +52,6 @@ func init() { // initialize default directory tree with corresponding timestamp. initFSTree() - // initialize directory trees for user-defined file systems (-path flag). - initDirTrees() - // Immediately update metadata. updateMetadata() diff --git a/src/cmd/godoc/codewalk.go b/src/cmd/godoc/codewalk.go index 018259f7dc07c90be61909ba61848c08c51d3f2f..2804ebbe5df85e3cbec0d9b70e960e8a6c562a05 100644 --- a/src/cmd/godoc/codewalk.go +++ b/src/cmd/godoc/codewalk.go @@ -31,7 +31,7 @@ import ( // Handler for /doc/codewalk/ and below. func codewalk(w http.ResponseWriter, r *http.Request) { relpath := r.URL.Path[len("/doc/codewalk/"):] - abspath := absolutePath(r.URL.Path[1:], *goroot) + abspath := r.URL.Path r.ParseForm() if f := r.FormValue("fileprint"); f != "" { @@ -130,7 +130,7 @@ func loadCodewalk(filename string) (*Codewalk, error) { i = len(st.Src) } filename := st.Src[0:i] - data, err := ReadFile(fs, absolutePath(filename, *goroot)) + data, err := ReadFile(fs, filename) if err != nil { st.Err = err continue @@ -208,7 +208,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string // of the codewalk pages. It is a separate iframe and does not get // the usual godoc HTML wrapper. func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) { - abspath := absolutePath(f, *goroot) + abspath := f data, err := ReadFile(fs, abspath) if err != nil { log.Print(err) diff --git a/src/cmd/godoc/dirtrees.go b/src/cmd/godoc/dirtrees.go index 1acde99bd2cb850f65dcb8f2376f2f48bfaa6710..b5726367ce5054a19a8692e2d86ee97bbb3e4b85 100644 --- a/src/cmd/godoc/dirtrees.go +++ b/src/cmd/godoc/dirtrees.go @@ -13,7 +13,7 @@ import ( "go/token" "log" "os" - "path/filepath" + pathpkg "path" "strings" ) @@ -35,7 +35,7 @@ func isGoFile(fi os.FileInfo) bool { name := fi.Name() return !fi.IsDir() && len(name) > 0 && name[0] != '.' && // ignore .files - filepath.Ext(name) == ".go" + pathpkg.Ext(name) == ".go" } func isPkgFile(fi os.FileInfo) bool { @@ -50,12 +50,11 @@ func isPkgDir(fi os.FileInfo) bool { } type treeBuilder struct { - pathFilter func(string) bool - maxDepth int + maxDepth int } func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory { - if b.pathFilter != nil && !b.pathFilter(path) || name == testdataDirName { + if name == testdataDirName { return nil } @@ -92,7 +91,7 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i // though the directory doesn't contain any real package files - was bug) if synopses[0] == "" { // no "optimal" package synopsis yet; continue to collect synopses - file, err := parseFile(fset, filepath.Join(path, d.Name()), + file, err := parseFile(fset, pathpkg.Join(path, d.Name()), parser.ParseComments|parser.PackageClauseOnly) if err == nil { hasPkgFiles = true @@ -126,7 +125,7 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i for _, d := range list { if isPkgDir(d) { name := d.Name() - dd := b.newDirTree(fset, filepath.Join(path, name), name, depth+1) + dd := b.newDirTree(fset, pathpkg.Join(path, name), name, depth+1) if dd != nil { dirs[i] = dd i++ @@ -170,7 +169,7 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i // are assumed to contain package files even if their contents are not known // (i.e., in this case the tree may contain directories w/o any package files). // -func newDirectory(root string, pathFilter func(string) bool, maxDepth int) *Directory { +func newDirectory(root string, maxDepth int) *Directory { // The root could be a symbolic link so use Stat not Lstat. d, err := fs.Stat(root) // If we fail here, report detailed error messages; otherwise @@ -186,7 +185,7 @@ func newDirectory(root string, pathFilter func(string) bool, maxDepth int) *Dire if maxDepth < 0 { maxDepth = 1e6 // "infinity" } - b := treeBuilder{pathFilter, maxDepth} + b := treeBuilder{maxDepth} // the file set provided is only for local parsing, no position // information escapes and thus we don't need to save the set return b.newDirTree(token.NewFileSet(), root, d.Name(), 0) @@ -235,10 +234,20 @@ func (dir *Directory) lookupLocal(name string) *Directory { return nil } +func splitPath(p string) []string { + if strings.HasPrefix(p, "/") { + p = p[1:] + } + if p == "" { + return nil + } + return strings.Split(p, "/") +} + // lookup looks for the *Directory for a given path, relative to dir. func (dir *Directory) lookup(path string) *Directory { - d := strings.Split(dir.Path, string(filepath.Separator)) - p := strings.Split(path, string(filepath.Separator)) + d := splitPath(dir.Path) + p := splitPath(path) i := 0 for i < len(d) { if i >= len(p) || d[i] != p[i] { @@ -311,8 +320,8 @@ func (root *Directory) listing(skipRoot bool) *DirList { if strings.HasPrefix(d.Path, root.Path) { path = d.Path[len(root.Path):] } - // remove trailing separator if any - path must be relative - if len(path) > 0 && path[0] == filepath.Separator { + // remove leading separator if any - path must be relative + if len(path) > 0 && path[0] == '/' { path = path[1:] } p.Path = path diff --git a/src/cmd/godoc/filesystem.go b/src/cmd/godoc/filesystem.go index 4e48c9e68392d6a316beba5100a4f0125a92b58a..0f1c6632c8d67b12f4d5b73530df80fd1cd739f4 100644 --- a/src/cmd/godoc/filesystem.go +++ b/src/cmd/godoc/filesystem.go @@ -12,16 +12,56 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "os" + pathpkg "path" + "path/filepath" + "sort" + "strings" + "time" ) +// fs is the file system that godoc reads from and serves. +// It is a virtual file system that operates on slash-separated paths, +// and its root corresponds to the Go distribution root: /src/pkg +// holds the source tree, and so on. This means that the URLs served by +// the godoc server are the same as the paths in the virtual file +// system, which helps keep things simple. +// +// New file trees - implementations of FileSystem - can be added to +// the virtual file system using nameSpace's Bind method. +// The usual setup is to bind OS(runtime.GOROOT) to the root +// of the name space and then bind any GOPATH/src directories +// on top of /src/pkg, so that all sources are in /src/pkg. +// +// For more about name spaces, see the nameSpace type's +// documentation below. +// +// The use of this virtual file system means that most code processing +// paths can assume they are slash-separated and should be using +// package path (often imported as pathpkg) to manipulate them, +// even on Windows. +// +var fs = nameSpace{} // the underlying file system for godoc + +// Setting debugNS = true will enable debugging prints about +// name space translations. +const debugNS = false + // The FileSystem interface specifies the methods godoc is using // to access the file system for which it serves documentation. type FileSystem interface { - Open(path string) (io.ReadCloser, error) + Open(path string) (readSeekCloser, error) Lstat(path string) (os.FileInfo, error) Stat(path string) (os.FileInfo, error) ReadDir(path string) ([]os.FileInfo, error) + String() string +} + +type readSeekCloser interface { + io.Reader + io.Seeker + io.Closer } // ReadFile reads the file named by path from fs and returns the contents. @@ -34,16 +74,31 @@ func ReadFile(fs FileSystem, path string) ([]byte, error) { return ioutil.ReadAll(rc) } -// ---------------------------------------------------------------------------- -// OS-specific FileSystem implementation +// OS returns an implementation of FileSystem reading from the +// tree rooted at root. Recording a root is convenient everywhere +// but necessary on Windows, because the slash-separated path +// passed to Open has no way to specify a drive letter. Using a root +// lets code refer to OS(`c:\`), OS(`d:\`) and so on. +func OS(root string) FileSystem { + return osFS(root) +} + +type osFS string -var OS FileSystem = osFS{} +func (root osFS) String() string { return "os(" + string(root) + ")" } -// osFS is the OS-specific implementation of FileSystem -type osFS struct{} +func (root osFS) resolve(path string) string { + // Clean the path so that it cannot possibly begin with ../. + // If it did, the result of filepath.Join would be outside the + // tree rooted at root. We probably won't ever see a path + // with .. in it, but be safe anyway. + path = pathpkg.Clean("/" + path) -func (osFS) Open(path string) (io.ReadCloser, error) { - f, err := os.Open(path) + return filepath.Join(string(root), path) +} + +func (root osFS) Open(path string) (readSeekCloser, error) { + f, err := os.Open(root.resolve(path)) if err != nil { return nil, err } @@ -57,14 +112,433 @@ func (osFS) Open(path string) (io.ReadCloser, error) { return f, nil } -func (osFS) Lstat(path string) (os.FileInfo, error) { - return os.Lstat(path) +func (root osFS) Lstat(path string) (os.FileInfo, error) { + return os.Lstat(root.resolve(path)) +} + +func (root osFS) Stat(path string) (os.FileInfo, error) { + return os.Stat(root.resolve(path)) +} + +func (root osFS) ReadDir(path string) ([]os.FileInfo, error) { + return ioutil.ReadDir(root.resolve(path)) // is sorted +} + +// hasPathPrefix returns true if x == y or x == y + "/" + more +func hasPathPrefix(x, y string) bool { + return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/")) +} + +// A nameSpace is a file system made up of other file systems +// mounted at specific locations in the name space. +// +// The representation is a map from mount point locations +// to the list of file systems mounted at that location. A traditional +// Unix mount table would use a single file system per mount point, +// but we want to be able to mount multiple file systems on a single +// mount point and have the system behave as if the union of those +// file systems were present at the mount point. +// For example, if the OS file system has a Go installation in +// c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then +// this name space creates the view we want for the godoc server: +// +// nameSpace{ +// "/": { +// {old: "/", fs: OS(`c:\Go`), new: "/"}, +// }, +// "/src/pkg": { +// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, +// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, +// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, +// }, +// } +// +// This is created by executing: +// +// ns := nameSpace{} +// ns.Bind("/", OS(`c:\Go`), "/", bindReplace) +// ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", bindAfter) +// ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", bindAfter) +// +// A particular mount point entry is a triple (old, fs, new), meaning that to +// operate on a path beginning with old, replace that prefix (old) with new +// and then pass that path to the FileSystem implementation fs. +// +// 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 +// will find the mount point at /src/pkg: +// +// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, +// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, +// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, +// +// ReadDir will when execute these three calls and merge the results: +// +// OS(`c:\Go`).ReadDir("/src/pkg/code") +// OS(`d:\Work1').ReadDir("/src/code") +// OS(`d:\Work2').ReadDir("/src/code") +// +// Note that the "/src/pkg" in "/src/pkg/code" has been replaced by +// just "/src" in the final two calls. +// +// OS is itself an implementation of a file system: it implements +// OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`). +// +// Because the new path is evaluated by fs (here OS(root)), another way +// to read the mount table is to mentally combine fs+new, so that this table: +// +// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, +// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, +// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, +// +// reads as: +// +// "/src/pkg" -> c:\Go\src\pkg +// "/src/pkg" -> d:\Work1\src +// "/src/pkg" -> d:\Work2\src +// +// An invariant (a redundancy) of the name space representation is that +// ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s +// mount table entries always have old == "/src/pkg"). The 'old' field is +// useful to callers, because they receive just a []mountedFS and not any +// other indication of which mount point was found. +// +type nameSpace map[string][]mountedFS + +// A mountedFS handles requests for path by replacing +// a prefix 'old' with 'new' and then calling the fs methods. +type mountedFS struct { + old string + fs FileSystem + new string +} + +// translate translates path for use in m, replacing old with new. +// +// mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code". +func (m mountedFS) translate(path string) string { + path = pathpkg.Clean("/" + path) + if !hasPathPrefix(path, m.old) { + panic("translate " + path + " but old=" + m.old) + } + return pathpkg.Join(m.new, path[len(m.old):]) +} + +func (nameSpace) String() string { + return "ns" +} + +// Fprint writes a text representation of the name space to w. +func (ns nameSpace) Fprint(w io.Writer) { + fmt.Fprint(w, "name space {\n") + var all []string + for mtpt := range ns { + all = append(all, mtpt) + } + sort.Strings(all) + for _, mtpt := range all { + fmt.Fprintf(w, "\t%s:\n", mtpt) + for _, m := range ns[mtpt] { + fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new) + } + } + fmt.Fprint(w, "}\n") +} + +// clean returns a cleaned, rooted path for evaluation. +// It canonicalizes the path so that we can use string operations +// to analyze it. +func (nameSpace) clean(path string) string { + return pathpkg.Clean("/" + path) +} + +// Bind causes references to old to redirect to the path new in newfs. +// If mode is bindReplace, old redirections are discarded. +// If mode is bindBefore, this redirection takes priority over existing ones, +// but earlier ones are still consulted for paths that do not exist in newfs. +// If mode is bindAfter, this redirection happens only after existing ones +// have been tried and failed. + +const ( + bindReplace = iota + bindBefore + bindAfter +) + +func (ns nameSpace) Bind(old string, newfs FileSystem, new string, mode int) { + old = ns.clean(old) + new = ns.clean(new) + m := mountedFS{old, newfs, new} + var mtpt []mountedFS + switch mode { + case bindReplace: + mtpt = append(mtpt, m) + case bindAfter: + mtpt = append(mtpt, ns.resolve(old)...) + mtpt = append(mtpt, m) + case bindBefore: + mtpt = append(mtpt, m) + mtpt = append(mtpt, ns.resolve(old)...) + } + + // Extend m.old, m.new in inherited mount point entries. + for i := range mtpt { + m := &mtpt[i] + if m.old != old { + if !hasPathPrefix(old, m.old) { + // This should not happen. If it does, panic so + // that we can see the call trace that led to it. + panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new)) + } + suffix := old[len(m.old):] + m.old = pathpkg.Join(m.old, suffix) + m.new = pathpkg.Join(m.new, suffix) + } + } + + ns[old] = mtpt +} + +// resolve resolves a path to the list of mountedFS to use for path. +func (ns nameSpace) resolve(path string) []mountedFS { + path = ns.clean(path) + for { + if m := ns[path]; m != nil { + if debugNS { + fmt.Printf("resolve %s: %v\n", path, m) + } + return m + } + if path == "/" { + break + } + path = pathpkg.Dir(path) + } + return nil +} + +// Open implements the FileSystem Open method. +func (ns nameSpace) Open(path string) (readSeekCloser, error) { + var err error + for _, m := range ns.resolve(path) { + if debugNS { + fmt.Printf("tx %s: %v\n", path, m.translate(path)) + } + r, err1 := m.fs.Open(m.translate(path)) + if err1 == nil { + return r, nil + } + if err == nil { + err = err1 + } + } + if err == nil { + err = &os.PathError{"open", path, os.ErrNotExist} + } + return nil, err +} + +// stat implements the FileSystem Stat and Lstat methods. +func (ns nameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) { + var err error + for _, m := range ns.resolve(path) { + fi, err1 := f(m.fs, m.translate(path)) + if err1 == nil { + return fi, nil + } + if err == nil { + err = err1 + } + } + if err == nil { + err = &os.PathError{"stat", path, os.ErrNotExist} + } + return nil, err +} + +func (ns nameSpace) Stat(path string) (os.FileInfo, error) { + return ns.stat(path, FileSystem.Stat) +} + +func (ns nameSpace) Lstat(path string) (os.FileInfo, error) { + return ns.stat(path, FileSystem.Lstat) +} + +// dirInfo is a trivial implementation of os.FileInfo for a directory. +type dirInfo string + +func (d dirInfo) Name() string { return string(d) } +func (d dirInfo) Size() int64 { return 0 } +func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 } +func (d dirInfo) ModTime() time.Time { return startTime } +func (d dirInfo) IsDir() bool { return true } +func (d dirInfo) Sys() interface{} { return nil } + +var startTime = time.Now() + +// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is. +// (The rest is in resolve.) +// +// Logically, ReadDir must return the union of all the directories that are named +// by path. In order to avoid misinterpreting Go packages, of all the directories +// that contain Go source code, we only include the files from the first, +// but we include subdirectories from all. +// +// ReadDir must also return directory entries needed to reach mount points. +// If the name space looks like the example in the type nameSpace comment, +// but c:\Go does not have a src/pkg subdirectory, we still want to be able +// to find that subdirectory, because we've mounted d:\Work1 and d:\Work2 +// there. So if we don't see "src" in the directory listing for c:\Go, we add an +// entry for it before returning. +// +func (ns nameSpace) ReadDir(path string) ([]os.FileInfo, error) { + path = ns.clean(path) + + var ( + haveGo = false + haveName = map[string]bool{} + all []os.FileInfo + err error + ) + + for _, m := range ns.resolve(path) { + dir, err1 := m.fs.ReadDir(m.translate(path)) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + + // If we don't yet have Go files in 'all' and this directory + // has some, add all the files from this directory. + // Otherwise, only add subdirectories. + useFiles := false + if !haveGo { + for _, d := range dir { + if strings.HasSuffix(d.Name(), ".go") { + useFiles = true + haveGo = true + break + } + } + } + + for _, d := range dir { + name := d.Name() + if (d.IsDir() || useFiles) && !haveName[name] { + haveName[name] = true + all = append(all, d) + } + } + } + + // Built union. Add any missing directories needed to reach mount points. + for old := range ns { + if hasPathPrefix(old, path) && old != path { + // Find next element after path in old. + elem := old[len(path):] + if strings.HasPrefix(elem, "/") { + elem = elem[1:] + } + if i := strings.Index(elem, "/"); i >= 0 { + elem = elem[:i] + } + if !haveName[elem] { + haveName[elem] = true + all = append(all, dirInfo(elem)) + } + } + } + + if len(all) == 0 { + return nil, err + } + + sort.Sort(byName(all)) + return all, nil +} + +// byName implements sort.Interface. +type byName []os.FileInfo + +func (f byName) Len() int { return len(f) } +func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() } +func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } + +// An httpFS implements http.FileSystem using a FileSystem. +type httpFS struct { + fs FileSystem +} + +func (h *httpFS) Open(name string) (http.File, error) { + fi, err := h.fs.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return &httpDir{h.fs, name, nil}, nil + } + f, err := h.fs.Open(name) + if err != nil { + return nil, err + } + return &httpFile{h.fs, f, name}, nil +} + +// httpDir implements http.File for a directory in a FileSystem. +type httpDir struct { + fs FileSystem + name string + pending []os.FileInfo +} + +func (h *httpDir) Close() error { return nil } +func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) } +func (h *httpDir) Read([]byte) (int, error) { + return 0, fmt.Errorf("cannot Read from directory %s", h.name) +} + +func (h *httpDir) Seek(offset int64, whence int) (int64, error) { + if offset == 0 && whence == 0 { + h.pending = nil + return 0, nil + } + return 0, fmt.Errorf("unsupported Seek in directory %s", h.name) +} + +func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) { + if h.pending == nil { + d, err := h.fs.ReadDir(h.name) + if err != nil { + return nil, err + } + if d == nil { + d = []os.FileInfo{} // not nil + } + h.pending = d + } + + if len(h.pending) == 0 && count > 0 { + return nil, io.EOF + } + if count <= 0 || count > len(h.pending) { + count = len(h.pending) + } + d := h.pending[:count] + h.pending = h.pending[count:] + return d, nil } -func (osFS) Stat(path string) (os.FileInfo, error) { - return os.Stat(path) +// httpFile implements http.File for a file (not directory) in a FileSystem. +type httpFile struct { + fs FileSystem + readSeekCloser + name string } -func (osFS) ReadDir(path string) ([]os.FileInfo, error) { - return ioutil.ReadDir(path) // is sorted +func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) } +func (h *httpFile) Readdir(int) ([]os.FileInfo, error) { + return nil, fmt.Errorf("cannot Readdir from file %s", h.name) } diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go index e5f7a73d4f451ceedfb370885dcc9a8fd45500f2..486b3863e331b044df25406425e4b3bff00a5616 100644 --- a/src/cmd/godoc/godoc.go +++ b/src/cmd/godoc/godoc.go @@ -20,7 +20,7 @@ import ( "net/http" "net/url" "os" - "path" + pathpkg "path" "path/filepath" "regexp" "runtime" @@ -55,12 +55,9 @@ var ( // file system roots // TODO(gri) consider the invariant that goroot always end in '/' - goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") - testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") - pkgPath = flag.String("path", "", "additional package directories (colon-separated)") - filter = flag.String("filter", "", "filter file containing permitted package directory paths") - filterMin = flag.Int("filter_minutes", 0, "filter file update interval in minutes; disabled if <= 0") - filterDelay delayTime // actual filter update interval in minutes; usually filterDelay == filterMin, but filterDelay may back off exponentially + goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") + testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") + pkgPath = flag.String("path", "", "additional package directories (colon-separated)") // layout control tabwidth = flag.Int("tabwidth", 4, "tab width") @@ -74,34 +71,31 @@ var ( maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown") indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") - // file system mapping - fs FileSystem // the underlying file system for godoc - fsHttp http.FileSystem // the underlying file system for http - fsMap Mapping // user-defined mapping - fsTree RWValue // *Directory tree of packages, updated with each sync - pathFilter RWValue // filter used when building fsMap directory trees - fsModified RWValue // timestamp of last call to invalidateIndex - docMetadata RWValue // mapping from paths to *Metadata + // file system information + fsTree RWValue // *Directory tree of packages, updated with each sync + fsModified RWValue // timestamp of last call to invalidateIndex + docMetadata RWValue // mapping from paths to *Metadata // http handlers fileServer http.Handler // default file server - cmdHandler httpHandler - pkgHandler httpHandler + cmdHandler docServer + pkgHandler docServer ) func initHandlers() { - paths := filepath.SplitList(*pkgPath) - gorootSrc := filepath.Join(build.Default.GOROOT, "src", "pkg") - for _, p := range build.Default.SrcDirs() { - if p != gorootSrc { - paths = append(paths, p) + // Add named directories in -path argument as + // subdirectories of src/pkg. + for _, p := range filepath.SplitList(*pkgPath) { + _, elem := filepath.Split(p) + if elem == "" { + log.Fatal("invalid -path argument: %q has no final element", p) } + fs.Bind("/src/pkg/"+elem, OS(p), "/", bindReplace) } - fsMap.Init(paths) - fileServer = http.FileServer(fsHttp) - cmdHandler = httpHandler{"/cmd/", filepath.Join(*goroot, "src", "cmd"), false} - pkgHandler = httpHandler{"/pkg/", filepath.Join(*goroot, "src", "pkg"), true} + fileServer = http.FileServer(&httpFS{fs}) + cmdHandler = docServer{"/cmd/", "/src/cmd", false} + pkgHandler = docServer{"/pkg/", "/src/pkg", true} } func registerPublicHandlers(mux *http.ServeMux) { @@ -115,7 +109,7 @@ func registerPublicHandlers(mux *http.ServeMux) { } func initFSTree() { - dir := newDirectory(filepath.Join(*goroot, *testDir), nil, -1) + dir := newDirectory(pathpkg.Join("/", *testDir), -1) if dir == nil { log.Println("Warning: FSTree is nil") return @@ -124,177 +118,6 @@ func initFSTree() { invalidateIndex() } -// ---------------------------------------------------------------------------- -// Directory filters - -// isParentOf returns true if p is a parent of (or the same as) q -// where p and q are directory paths. -func isParentOf(p, q string) bool { - n := len(p) - return strings.HasPrefix(q, p) && (len(q) <= n || q[n] == '/') -} - -func setPathFilter(list []string) { - if len(list) == 0 { - pathFilter.set(nil) - return - } - - // len(list) > 0 - pathFilter.set(func(path string) bool { - // list is sorted in increasing order and for each path all its children are removed - i := sort.Search(len(list), func(i int) bool { return list[i] > path }) - // Now we have list[i-1] <= path < list[i]. - // Path may be a child of list[i-1] or a parent of list[i]. - return i > 0 && isParentOf(list[i-1], path) || i < len(list) && isParentOf(path, list[i]) - }) -} - -func getPathFilter() func(string) bool { - f, _ := pathFilter.get() - if f != nil { - return f.(func(string) bool) - } - return nil -} - -// readDirList reads a file containing a newline-separated list -// of directory paths and returns the list of paths. -func readDirList(filename string) ([]string, error) { - contents, err := ReadFile(fs, filename) - if err != nil { - return nil, err - } - // create a sorted list of valid directory names - filter := func(path string) bool { - d, e := fs.Lstat(path) - if e != nil && err == nil { - // remember first error and return it from readDirList - // so we have at least some information if things go bad - err = e - } - return e == nil && isPkgDir(d) - } - list := canonicalizePaths(strings.Split(string(contents), "\n"), filter) - // for each parent path, remove all its children q - // (requirement for binary search to work when filtering) - i := 0 - for _, q := range list { - if i == 0 || !isParentOf(list[i-1], q) { - list[i] = q - i++ - } - } - return list[0:i], err -} - -// updateMappedDirs computes the directory tree for -// each user-defined file system mapping. If a filter -// is provided, it is used to filter directories. -// -func updateMappedDirs(filter func(string) bool) { - if !fsMap.IsEmpty() { - fsMap.Iterate(func(path string, value *RWValue) bool { - value.set(newDirectory(path, filter, -1)) - return true - }) - invalidateIndex() - } -} - -func updateFilterFile() { - updateMappedDirs(nil) // no filter for accuracy - - // collect directory tree leaf node paths - var buf bytes.Buffer - fsMap.Iterate(func(_ string, value *RWValue) bool { - v, _ := value.get() - if v != nil && v.(*Directory) != nil { - v.(*Directory).writeLeafs(&buf) - } - return true - }) - - // update filter file - if err := writeFileAtomically(*filter, buf.Bytes()); err != nil { - log.Printf("writeFileAtomically(%s): %s", *filter, err) - filterDelay.backoff(24 * time.Hour) // back off exponentially, but try at least once a day - } else { - filterDelay.set(*filterMin) // revert to regular filter update schedule - } -} - -func initDirTrees() { - // setup initial path filter - if *filter != "" { - list, err := readDirList(*filter) - if err != nil { - log.Printf("readDirList(%s): %s", *filter, err) - } - if *verbose || len(list) == 0 { - log.Printf("found %d directory paths in file %s", len(list), *filter) - } - setPathFilter(list) - } - - go updateMappedDirs(getPathFilter()) // use filter for speed - - // start filter update goroutine, if enabled. - if *filter != "" && *filterMin > 0 { - filterDelay.set(time.Duration(*filterMin) * time.Minute) // initial filter update delay - go func() { - for { - if *verbose { - log.Printf("start update of %s", *filter) - } - updateFilterFile() - delay, _ := filterDelay.get() - dt := delay.(time.Duration) - if *verbose { - log.Printf("next filter update in %s", dt) - } - time.Sleep(dt) - } - }() - } -} - -// ---------------------------------------------------------------------------- -// Path mapping - -// Absolute paths are file system paths (backslash-separated on Windows), -// but relative paths are always slash-separated. - -func absolutePath(relpath, defaultRoot string) string { - abspath := fsMap.ToAbsolute(relpath) - if abspath == "" { - // no user-defined mapping found; use default mapping - abspath = filepath.Join(defaultRoot, filepath.FromSlash(relpath)) - } - return abspath -} - -func relativeURL(abspath string) string { - relpath := fsMap.ToRelative(abspath) - if relpath == "" { - // prefix must end in a path separator - prefix := *goroot - if len(prefix) > 0 && prefix[len(prefix)-1] != filepath.Separator { - prefix += string(filepath.Separator) - } - if strings.HasPrefix(abspath, prefix) { - // no user-defined mapping found; use default mapping - relpath = filepath.ToSlash(abspath[len(prefix):]) - } - } - // Only if path is an invalid absolute path is relpath == "" - // at this point. This should never happen since absolute paths - // are only created via godoc for files that do exist. However, - // it is ok to return ""; it will simply provide a link to the - // top of the pkg or src directories. - return relpath -} - // ---------------------------------------------------------------------------- // Tab conversion @@ -391,7 +214,7 @@ func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { } func filenameFunc(path string) string { - _, localname := filepath.Split(path) + _, localname := pathpkg.Split(path) return localname } @@ -581,7 +404,7 @@ func splitExampleName(s string) (name, suffix string) { } func pkgLinkFunc(path string) string { - relpath := relativeURL(path) + relpath := path[1:] // because of the irregular mapping under goroot // we need to correct certain relative paths if strings.HasPrefix(relpath, "src/pkg/") { @@ -597,7 +420,7 @@ func posLink_urlFunc(node ast.Node, fset *token.FileSet) string { if p := node.Pos(); p.IsValid() { pos := fset.Position(p) - relpath = relativeURL(pos.Filename) + relpath = pos.Filename line = pos.Line low = pos.Offset } @@ -626,6 +449,10 @@ func posLink_urlFunc(node ast.Node, fset *token.FileSet) string { return buf.String() } +func srcLinkFunc(s string) string { + return pathpkg.Clean("/" + s) +} + // fmap describes the template functions installed with all godoc templates. // Convention: template function names ending in "_html" or "_url" produce // HTML- or URL-escaped strings; all other function results may @@ -652,7 +479,7 @@ var fmap = template.FuncMap{ // support for URL attributes "pkgLink": pkgLinkFunc, - "srcLink": relativeURL, + "srcLink": srcLinkFunc, "posLink_url": posLink_urlFunc, // formatting of Examples @@ -662,10 +489,10 @@ var fmap = template.FuncMap{ } func readTemplate(name string) *template.Template { - path := filepath.Join(*goroot, "lib", "godoc", name) + path := "lib/godoc/" + name if *templateDir != "" { defaultpath := path - path = filepath.Join(*templateDir, name) + path = pathpkg.Join(*templateDir, name) if _, err := fs.Stat(path); err != nil { log.Print("readTemplate:", err) path = defaultpath @@ -722,7 +549,6 @@ func servePage(w http.ResponseWriter, title, subtitle, query string, content []b d := struct { Title string Subtitle string - PkgRoots []string SearchBox bool Query string Version string @@ -731,7 +557,6 @@ func servePage(w http.ResponseWriter, title, subtitle, query string, content []b }{ title, subtitle, - fsMap.PrefixList(), *indexEnabled, query, runtime.Version(), @@ -799,7 +624,7 @@ func applyTemplate(t *template.Template, name string, data interface{}) []byte { } func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { - canonical := path.Clean(r.URL.Path) + canonical := pathpkg.Clean(r.URL.Path) if !strings.HasSuffix("/", canonical) { canonical += "/" } @@ -820,7 +645,7 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, tit var buf bytes.Buffer buf.WriteString("<pre>") - FormatText(&buf, src, 1, filepath.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) + FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) buf.WriteString("</pre>") servePage(w, title+" "+relpath, "", "", buf.Bytes()) @@ -856,10 +681,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) { relpath = m.filePath } + abspath := relpath relpath = relpath[1:] // strip leading slash - abspath := absolutePath(relpath, *goroot) - switch path.Ext(relpath) { + switch pathpkg.Ext(relpath) { case ".html": if strings.HasSuffix(relpath, "/index.html") { // We'll show index.html for the directory. @@ -886,8 +711,8 @@ func serveFile(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - if index := filepath.Join(abspath, "index.html"); isTextFile(index) { - serveHTMLDoc(w, r, index, relativeURL(index)) + if index := pathpkg.Join(abspath, "index.html"); isTextFile(index) { + serveHTMLDoc(w, r, index, index) return } serveDirectory(w, r, abspath, relpath) @@ -992,7 +817,7 @@ func (info *PageInfo) IsEmpty() bool { return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil } -type httpHandler struct { +type docServer struct { pattern string // url pattern; e.g. "/pkg/" fsRoot string // file system root to which the pattern is mapped isPkg bool // true if this handler serves real package documentation (as opposed to command documentation) @@ -1029,7 +854,7 @@ func inList(name string, list []string) bool { // directories, PageInfo.Dirs is nil. If a directory read error occurred, // PageInfo.Err is set to the respective error but the error is not logged. // -func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo { +func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo { var pkgFiles []string // If we're showing the default package, restrict to the ones @@ -1043,7 +868,7 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf // to choose, set ctxt.GOOS and ctxt.GOARCH before // calling ctxt.ScanDir. ctxt := build.Default - ctxt.IsAbsPath = path.IsAbs + ctxt.IsAbsPath = pathpkg.IsAbs ctxt.ReadDir = fsReadDir ctxt.OpenFile = fsOpenFile dir, err := ctxt.ImportDir(abspath, 0) @@ -1091,13 +916,13 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf // the package with dirname, and the 3rd choice is a package // that is not called "main" if there is exactly one such // package. Otherwise, don't select a package. - dirpath, dirname := filepath.Split(abspath) + dirpath, dirname := pathpkg.Split(abspath) // If the dirname is "go" we might be in a sub-directory for // .go files - use the outer directory name instead for better // results. if dirname == "go" { - _, dirname = filepath.Split(filepath.Clean(dirpath)) + _, dirname = pathpkg.Split(pathpkg.Clean(dirpath)) } var choice3 *ast.Package @@ -1161,7 +986,7 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf if mode&allMethods != 0 { m |= doc.AllMethods } - pdoc = doc.New(pkg, path.Clean(relpath), m) // no trailing '/' in importpath + pdoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath } else { // show source code // TODO(gri) Consider eliminating export filtering in this mode, @@ -1183,35 +1008,12 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf dir = tree.(*Directory).lookup(abspath) timestamp = ts } - if dir == nil { - // the path may refer to a user-specified file system mapped - // via fsMap; lookup that mapping and corresponding RWValue - // if any - var v *RWValue - fsMap.Iterate(func(path string, value *RWValue) bool { - if isParentOf(path, abspath) { - // mapping found - v = value - return false - } - return true - }) - if v != nil { - // found a RWValue associated with a user-specified file - // system; a non-nil RWValue stores a (possibly out-of-date) - // directory tree for that file system - if tree, ts := v.get(); tree != nil && tree.(*Directory) != nil { - dir = tree.(*Directory).lookup(abspath) - timestamp = ts - } - } - } if dir == nil { // no directory tree present (too early after startup or // command-line mode); compute one level for this page // note: cannot use path filter here because in general // it doesn't contain the fsTree path - dir = newDirectory(abspath, nil, 1) + dir = newDirectory(abspath, 1) timestamp = time.Now() } @@ -1230,13 +1032,13 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf } } -func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - relpath := path.Clean(r.URL.Path[len(h.pattern):]) - abspath := absolutePath(relpath, h.fsRoot) + relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) + abspath := pathpkg.Join(h.fsRoot, relpath) mode := getPageInfoMode(r) if relpath == builtinPkgPath { mode = noFiltering @@ -1264,13 +1066,13 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { title = "Package " + info.PDoc.Name case info.PDoc.Name == fakePkgName: // assume that the directory name is the command name - _, pkgname := path.Split(relpath) + _, pkgname := pathpkg.Split(relpath) title = "Command " + pkgname default: title = "Command " + info.PDoc.Name } default: - title = "Directory " + relativeURL(info.Dirname) + title = "Directory " + info.Dirname if *showTimestamps { subtitle = "Last update: " + info.DirTime.String() } @@ -1414,7 +1216,7 @@ func updateMetadata() { return } for _, fi := range fis { - name := filepath.Join(dir, fi.Name()) + name := pathpkg.Join(dir, fi.Name()) if fi.IsDir() { scan(name) // recurse continue @@ -1434,7 +1236,7 @@ func updateMetadata() { continue } // Store relative filesystem path in Metadata. - meta.filePath = filepath.Join("/", name[len(*goroot):]) + meta.filePath = name if meta.Path == "" { // If no Path, canonical path is actual path. meta.Path = meta.filePath @@ -1444,7 +1246,7 @@ func updateMetadata() { metadata[meta.filePath] = &meta } } - scan(filepath.Join(*goroot, "doc")) + scan("/doc") docMetadata.set(metadata) } @@ -1519,13 +1321,9 @@ func feedDirnames(root *RWValue, c chan<- string) { // of all the file systems under godoc's observation. // func fsDirnames() <-chan string { - c := make(chan string, 256) // asynchronous for fewer context switches + c := make(chan string, 256) // buffered for fewer context switches go func() { feedDirnames(&fsTree, c) - fsMap.Iterate(func(_ string, root *RWValue) bool { - feedDirnames(root, c) - return true - }) close(c) }() return c diff --git a/src/cmd/godoc/httpzip.go b/src/cmd/godoc/httpzip.go deleted file mode 100644 index 12e99646df8cafa89dcbe79762491e04212fb965..0000000000000000000000000000000000000000 --- a/src/cmd/godoc/httpzip.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2011 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. - -// This file provides an implementation of the http.FileSystem -// interface based on the contents of a .zip file. -// -// Assumptions: -// -// - The file paths stored in the zip file must use a slash ('/') as path -// separator; and they must be relative (i.e., they must not start with -// a '/' - this is usually the case if the file was created w/o special -// options). -// - The zip file system treats the file paths found in the zip internally -// like absolute paths w/o a leading '/'; i.e., the paths are considered -// relative to the root of the file system. -// - All path arguments to file system methods are considered relative to -// the root specified with NewHttpZipFS (even if the paths start with a '/'). - -// TODO(gri) Should define a commonly used FileSystem API that is the same -// for http and godoc. Then we only need one zip-file based file -// system implementation. - -package main - -import ( - "archive/zip" - "fmt" - "io" - "net/http" - "os" - "path" - "sort" - "strings" - "time" -) - -type fileInfo struct { - name string - mode os.FileMode - size int64 - mtime time.Time -} - -func (fi *fileInfo) Name() string { return fi.name } -func (fi *fileInfo) Mode() os.FileMode { return fi.mode } -func (fi *fileInfo) Size() int64 { return fi.size } -func (fi *fileInfo) ModTime() time.Time { return fi.mtime } -func (fi *fileInfo) IsDir() bool { return fi.mode.IsDir() } -func (fi *fileInfo) Sys() interface{} { return nil } - -// httpZipFile is the zip-file based implementation of http.File -type httpZipFile struct { - path string // absolute path within zip FS without leading '/' - info os.FileInfo - io.ReadCloser // nil for directory - list zipList -} - -func (f *httpZipFile) Close() error { - if !f.info.IsDir() { - return f.ReadCloser.Close() - } - f.list = nil - return nil -} - -func (f *httpZipFile) Stat() (os.FileInfo, error) { - return f.info, nil -} - -func (f *httpZipFile) Readdir(count int) ([]os.FileInfo, error) { - var list []os.FileInfo - dirname := f.path + "/" - prevname := "" - for i, e := range f.list { - if count == 0 { - f.list = f.list[i:] - break - } - if !strings.HasPrefix(e.Name, dirname) { - f.list = nil - break // not in the same directory anymore - } - name := e.Name[len(dirname):] // local name - var mode os.FileMode - var size int64 - var mtime time.Time - if i := strings.IndexRune(name, '/'); i >= 0 { - // We infer directories from files in subdirectories. - // If we have x/y, return a directory entry for x. - name = name[0:i] // keep local directory name only - mode = os.ModeDir - // no size or mtime for directories - } else { - mode = 0 - size = int64(e.UncompressedSize) - mtime = e.ModTime() - } - // If we have x/y and x/z, don't return two directory entries for x. - // TODO(gri): It should be possible to do this more efficiently - // by determining the (fs.list) range of local directory entries - // (via two binary searches). - if name != prevname { - list = append(list, &fileInfo{ - name, - mode, - size, - mtime, - }) - prevname = name - count-- - } - } - - if count >= 0 && len(list) == 0 { - return nil, io.EOF - } - - return list, nil -} - -func (f *httpZipFile) Seek(offset int64, whence int) (int64, error) { - return 0, fmt.Errorf("Seek not implemented for zip file entry: %s", f.info.Name()) -} - -// httpZipFS is the zip-file based implementation of http.FileSystem -type httpZipFS struct { - *zip.ReadCloser - list zipList - root string -} - -func (fs *httpZipFS) Open(name string) (http.File, error) { - // fs.root does not start with '/'. - path := path.Join(fs.root, name) // path is clean - index, exact := fs.list.lookup(path) - if index < 0 || !strings.HasPrefix(path, fs.root) { - // file not found or not under root - return nil, fmt.Errorf("file not found: %s", name) - } - - if exact { - // exact match found - must be a file - f := fs.list[index] - rc, err := f.Open() - if err != nil { - return nil, err - } - return &httpZipFile{ - path, - &fileInfo{ - name, - 0, - int64(f.UncompressedSize), - f.ModTime(), - }, - rc, - nil, - }, nil - } - - // not an exact match - must be a directory - return &httpZipFile{ - path, - &fileInfo{ - name, - os.ModeDir, - 0, // no size for directory - time.Time{}, // no mtime for directory - }, - nil, - fs.list[index:], - }, nil -} - -func (fs *httpZipFS) Close() error { - fs.list = nil - return fs.ReadCloser.Close() -} - -// NewHttpZipFS creates a new http.FileSystem based on the contents of -// the zip file rc restricted to the directory tree specified by root; -// root must be an absolute path. -func NewHttpZipFS(rc *zip.ReadCloser, root string) http.FileSystem { - list := make(zipList, len(rc.File)) - copy(list, rc.File) // sort a copy of rc.File - sort.Sort(list) - return &httpZipFS{rc, list, zipPath(root)} -} diff --git a/src/cmd/godoc/index.go b/src/cmd/godoc/index.go index 6c36e6f4f692ee770f512fe31dd6d7f19d974b7c..1bef7969376df1c94dae1ef521cb50c76d3d9090 100644 --- a/src/cmd/godoc/index.go +++ b/src/cmd/godoc/index.go @@ -48,7 +48,7 @@ import ( "index/suffixarray" "io" "os" - "path/filepath" + pathpkg "path" "regexp" "sort" "strings" @@ -248,7 +248,7 @@ type File struct { // Path returns the file path of f. func (f *File) Path() string { - return filepath.Join(f.Pak.Path, f.Name) + return pathpkg.Join(f.Pak.Path, f.Name) } // A Spot describes a single occurrence of a word. @@ -695,7 +695,7 @@ var whitelisted = map[string]bool{ // of "permitted" files for indexing. The filename must // be the directory-local name of the file. func isWhitelisted(filename string) bool { - key := filepath.Ext(filename) + key := pathpkg.Ext(filename) if key == "" { // file has no extension - use entire filename key = filename @@ -708,7 +708,7 @@ func (x *Indexer) visitFile(dirname string, f os.FileInfo, fulltextIndex bool) { return } - filename := filepath.Join(dirname, f.Name()) + filename := pathpkg.Join(dirname, f.Name()) goFile := false switch { diff --git a/src/cmd/godoc/main.go b/src/cmd/godoc/main.go index 5f42105393b2bd5ff6b87f4b055e6fbfb59c9877..f66e78413889db443d237ec885862d04a7ae16cd 100644 --- a/src/cmd/godoc/main.go +++ b/src/cmd/godoc/main.go @@ -39,7 +39,7 @@ import ( "net/http" _ "net/http/pprof" // to serve /debug/pprof/* "os" - "path" + pathpkg "path" "path/filepath" "regexp" "runtime" @@ -239,19 +239,20 @@ func main() { // same is true for the http handlers in initHandlers. if *zipfile == "" { // use file system of underlying OS - *goroot = filepath.Clean(*goroot) // normalize path separator - fs = OS - fsHttp = http.Dir(*goroot) + fs.Bind("/", OS(*goroot), "/", bindReplace) } else { // use file system specified via .zip file (path separator must be '/') rc, err := zip.OpenReader(*zipfile) if err != nil { log.Fatalf("%s: %s\n", *zipfile, err) } - defer rc.Close() // be nice (e.g., -writeIndex mode) - *goroot = path.Join("/", *goroot) // fsHttp paths are relative to '/' - fs = NewZipFS(rc) - fsHttp = NewHttpZipFS(rc, *goroot) + defer rc.Close() // be nice (e.g., -writeIndex mode) + fs.Bind("/", NewZipFS(rc, *zipfile), *goroot, bindReplace) + } + + // Bind $GOPATH trees into Go root. + for _, p := range filepath.SplitList(build.Default.GOPATH) { + fs.Bind("/src/pkg", OS(p), "/src", bindAfter) } readTemplates() @@ -266,7 +267,6 @@ func main() { log.Println("initialize file systems") *verbose = true // want to see what happens initFSTree() - initDirTrees() *indexThrottle = 1 updateIndex() @@ -303,10 +303,7 @@ func main() { default: log.Print("identifier search index enabled") } - if !fsMap.IsEmpty() { - log.Print("user-defined mapping:") - fsMap.Fprint(os.Stderr) - } + fs.Fprint(os.Stderr) handler = loggingHandler(handler) } @@ -319,9 +316,6 @@ func main() { // (Do it in a goroutine so that launch is quick.) go initFSTree() - // Initialize directory trees for user-defined file systems (-path flag). - initDirTrees() - // Start sync goroutine, if enabled. if *syncCmd != "" && *syncMin > 0 { syncDelay.set(*syncMin) // initial sync delay @@ -378,23 +372,27 @@ func main() { const cmdPrefix = "cmd/" path := flag.Arg(0) var forceCmd bool - if strings.HasPrefix(path, ".") { - // assume cwd; don't assume -goroot + var abspath, relpath string + if filepath.IsAbs(path) { + fs.Bind("/target", OS(path), "/", bindReplace) + abspath = "/target" + } else if build.IsLocalImport(path) { cwd, _ := os.Getwd() // ignore errors path = filepath.Join(cwd, path) + fs.Bind("/target", OS(path), "/", bindReplace) + abspath = "/target" } else if strings.HasPrefix(path, cmdPrefix) { - path = path[len(cmdPrefix):] + abspath = path[len(cmdPrefix):] forceCmd = true - } - relpath := path - abspath := path - if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { + } else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { + fs.Bind("/target", OS(bp.Dir), "/", bindReplace) + abspath = "/target" relpath = bp.ImportPath - abspath = bp.Dir - } else if !filepath.IsAbs(path) { - abspath = absolutePath(path, pkgHandler.fsRoot) } else { - relpath = relativeURL(path) + abspath = pathpkg.Join(pkgHandler.fsRoot, path) + } + if relpath == "" { + relpath = abspath } var mode PageInfoMode @@ -422,7 +420,7 @@ func main() { // (the go command invokes godoc w/ absolute paths; don't override) var cinfo PageInfo if !filepath.IsAbs(path) { - abspath = absolutePath(path, cmdHandler.fsRoot) + abspath = pathpkg.Join(cmdHandler.fsRoot, path) cinfo = cmdHandler.getPageInfo(abspath, relpath, "", mode) } @@ -445,6 +443,9 @@ func main() { if info.Err != nil { log.Fatalf("%v", info.Err) } + if info.PDoc.ImportPath == "/target" { + info.PDoc.ImportPath = flag.Arg(0) + } // If we have more than one argument, use the remaining arguments for filtering if flag.NArg() > 1 { diff --git a/src/cmd/godoc/mapping.go b/src/cmd/godoc/mapping.go deleted file mode 100644 index 544dd6f661d7d2cedd0f45f9557174c6e17b5479..0000000000000000000000000000000000000000 --- a/src/cmd/godoc/mapping.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2009 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. - -// This file implements the Mapping data structure. - -package main - -import ( - "fmt" - "io" - "path" - "path/filepath" - "sort" - "strings" -) - -// A Mapping object maps relative paths (e.g. from URLs) -// to absolute paths (of the file system) and vice versa. -// -// A Mapping object consists of a list of individual mappings -// of the form: prefix -> path which are interpreted as follows: -// A relative path of the form prefix/tail is to be mapped to -// the absolute path/tail, if that absolute path exists in the file -// system. Given a Mapping object, a relative path is mapped to an -// absolute path by trying each of the individual mappings in order, -// until a valid mapping is found. For instance, for the mapping: -// -// user -> /home/user -// public -> /home/user/public -// public -> /home/build/public -// -// the relative paths below are mapped to absolute paths as follows: -// -// user/foo -> /home/user/foo -// public/net/rpc/file1.go -> /home/user/public/net/rpc/file1.go -// -// If there is no /home/user/public/net/rpc/file2.go, the next public -// mapping entry is used to map the relative path to: -// -// public/net/rpc/file2.go -> /home/build/public/net/rpc/file2.go -// -// (assuming that file exists). -// -// Each individual mapping also has a RWValue associated with it that -// may be used to store mapping-specific information. See the Iterate -// method. -// -type Mapping struct { - list []mapping - prefixes []string // lazily computed from list -} - -type mapping struct { - prefix, path string - value *RWValue -} - -// Init initializes the Mapping from a list of paths. -// Empty paths are ignored; relative paths are assumed to be relative to -// the current working directory and converted to absolute paths. -// For each path of the form: -// -// dirname/localname -// -// a mapping -// -// localname -> path -// -// is added to the Mapping object, in the order of occurrence. -// For instance, under Unix, the argument: -// -// /home/user:/home/build/public -// -// leads to the following mapping: -// -// user -> /home/user -// public -> /home/build/public -// -func (m *Mapping) Init(paths []string) { - pathlist := canonicalizePaths(paths, nil) - list := make([]mapping, len(pathlist)) - - // create mapping list - for i, path := range pathlist { - _, prefix := filepath.Split(path) - list[i] = mapping{prefix, path, new(RWValue)} - } - - m.list = list -} - -// IsEmpty returns true if there are no mappings specified. -func (m *Mapping) IsEmpty() bool { return len(m.list) == 0 } - -// PrefixList returns a list of all prefixes, with duplicates removed. -// For instance, for the mapping: -// -// user -> /home/user -// public -> /home/user/public -// public -> /home/build/public -// -// the prefix list is: -// -// user, public -// -func (m *Mapping) PrefixList() []string { - // compute the list lazily - if m.prefixes == nil { - list := make([]string, len(m.list)) - - // populate list - for i, e := range m.list { - list[i] = e.prefix - } - - // sort the list and remove duplicate entries - sort.Strings(list) - i := 0 - prev := "" - for _, path := range list { - if path != prev { - list[i] = path - i++ - prev = path - } - } - - m.prefixes = list[0:i] - } - - return m.prefixes -} - -// Fprint prints the mapping. -func (m *Mapping) Fprint(w io.Writer) { - for _, e := range m.list { - fmt.Fprintf(w, "\t%s -> %s\n", e.prefix, e.path) - } -} - -const sep = string(filepath.Separator) - -// splitFirst splits a path at the first path separator and returns -// the path's head (the top-most directory specified by the path) and -// its tail (the rest of the path). If there is no path separator, -// splitFirst returns path as head, and the empty string as tail. -// Specifically, splitFirst("foo") == splitFirst("foo/"). -// -func splitFirst(path string) (head, tail string) { - if i := strings.Index(path, sep); i > 0 { - // 0 < i < len(path) - return path[0:i], path[i+1:] - } - return path, "" -} - -// ToAbsolute maps a slash-separated relative path to an absolute filesystem -// path using the Mapping specified by the receiver. If the path cannot -// be mapped, the empty string is returned. -// -func (m *Mapping) ToAbsolute(spath string) string { - fpath := filepath.FromSlash(spath) - prefix, tail := splitFirst(fpath) - for _, e := range m.list { - if e.prefix == prefix { - // found potential mapping - abspath := filepath.Join(e.path, tail) - if _, err := fs.Stat(abspath); err == nil { - return abspath - } - } - } - return "" // no match -} - -// ToRelative maps an absolute filesystem path to a relative slash-separated -// path using the Mapping specified by the receiver. If the path cannot -// be mapped, the empty string is returned. -// -func (m *Mapping) ToRelative(fpath string) string { - for _, e := range m.list { - // if fpath has prefix e.path, the next character must be a separator (was issue 3096) - if strings.HasPrefix(fpath, e.path+sep) { - spath := filepath.ToSlash(fpath) - // /absolute/prefix/foo -> prefix/foo - return path.Join(e.prefix, spath[len(e.path):]) // Join will remove a trailing '/' - } - } - return "" // no match -} - -// Iterate calls f for each path and RWValue in the mapping (in uspecified order) -// until f returns false. -// -func (m *Mapping) Iterate(f func(path string, value *RWValue) bool) { - for _, e := range m.list { - if !f(e.path, e.value) { - return - } - } -} diff --git a/src/cmd/godoc/parser.go b/src/cmd/godoc/parser.go index d6cc67cb503a44f154197446554ad12bc657fff8..c6b7c2dc8f96f8d09ff5e1cfafc53ab17f825224 100644 --- a/src/cmd/godoc/parser.go +++ b/src/cmd/godoc/parser.go @@ -14,7 +14,7 @@ import ( "go/parser" "go/token" "os" - "path/filepath" + pathpkg "path" ) func parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) { @@ -58,7 +58,7 @@ func parseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool) ( i := 0 for _, d := range list { if filter == nil || filter(d) { - filenames[i] = filepath.Join(path, d.Name()) + filenames[i] = pathpkg.Join(path, d.Name()) i++ } } diff --git a/src/cmd/godoc/utils.go b/src/cmd/godoc/utils.go index be0bdc30670e87372b7441b550fdb0e196fe2c0a..7def015c8a997e5e88f292bcf7bb0d55a7ce7d2f 100644 --- a/src/cmd/godoc/utils.go +++ b/src/cmd/godoc/utils.go @@ -7,12 +7,7 @@ package main import ( - "io" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" + pathpkg "path" "sync" "time" "unicode/utf8" @@ -40,76 +35,6 @@ func (v *RWValue) get() (interface{}, time.Time) { return v.value, v.timestamp } -// TODO(gri) For now, using os.Getwd() is ok here since the functionality -// based on this code is not invoked for the appengine version, -// but this is fragile. Determine what the right thing to do is, -// here (possibly have some Getwd-equivalent in FileSystem). -var cwd, _ = os.Getwd() // ignore errors - -// canonicalizePaths takes a list of (directory/file) paths and returns -// the list of corresponding absolute paths in sorted (increasing) order. -// Relative paths are assumed to be relative to the current directory, -// empty and duplicate paths as well as paths for which filter(path) is -// false are discarded. filter may be nil in which case it is not used. -// -func canonicalizePaths(list []string, filter func(path string) bool) []string { - i := 0 - for _, path := range list { - path = strings.TrimSpace(path) - if len(path) == 0 { - continue // ignore empty paths (don't assume ".") - } - // len(path) > 0: normalize path - if filepath.IsAbs(path) { - path = filepath.Clean(path) - } else { - path = filepath.Join(cwd, path) - } - // we have a non-empty absolute path - if filter != nil && !filter(path) { - continue - } - // keep the path - list[i] = path - i++ - } - list = list[0:i] - - // sort the list and remove duplicate entries - sort.Strings(list) - i = 0 - prev := "" - for _, path := range list { - if path != prev { - list[i] = path - i++ - prev = path - } - } - - return list[0:i] -} - -// writeFileAtomically writes data to a temporary file and then -// atomically renames that file to the file named by filename. -// -func writeFileAtomically(filename string, data []byte) error { - // TODO(gri) this won't work on appengine - f, err := ioutil.TempFile(filepath.Split(filename)) - if err != nil { - return err - } - n, err := f.Write(data) - f.Close() - if err != nil { - return err - } - if n < len(data) { - return io.ErrShortWrite - } - return os.Rename(f.Name(), filename) -} - // isText returns true if a significant prefix of s looks like correct UTF-8; // that is, if it is likely that s is human-readable text. // @@ -146,7 +71,7 @@ var textExt = map[string]bool{ // func isTextFile(filename string) bool { // if the extension is known, use it for decision making - if isText, found := textExt[filepath.Ext(filename)]; found { + if isText, found := textExt[pathpkg.Ext(filename)]; found { return isText } diff --git a/src/cmd/godoc/zip.go b/src/cmd/godoc/zip.go index 8c4b1101b584f20ede215a2cb1290578397c5d01..620eb4f3cc3569fc7db8846d7a35db10df9536de 100644 --- a/src/cmd/godoc/zip.go +++ b/src/cmd/godoc/zip.go @@ -73,6 +73,11 @@ func (fi zipFI) Sys() interface{} { type zipFS struct { *zip.ReadCloser list zipList + name string +} + +func (fs *zipFS) String() string { + return "zip(" + fs.name + ")" } func (fs *zipFS) Close() error { @@ -102,7 +107,7 @@ func (fs *zipFS) stat(abspath string) (int, zipFI, error) { return i, zipFI{name, file}, nil } -func (fs *zipFS) Open(abspath string) (io.ReadCloser, error) { +func (fs *zipFS) Open(abspath string) (readSeekCloser, error) { _, fi, err := fs.stat(zipPath(abspath)) if err != nil { return nil, err @@ -110,7 +115,29 @@ func (fs *zipFS) Open(abspath string) (io.ReadCloser, error) { if fi.IsDir() { return nil, fmt.Errorf("Open: %s is a directory", abspath) } - return fi.file.Open() + r, err := fi.file.Open() + if err != nil { + return nil, err + } + return &zipSeek{fi.file, r}, nil +} + +type zipSeek struct { + file *zip.File + io.ReadCloser +} + +func (f *zipSeek) Seek(offset int64, whence int) (int64, error) { + if whence == 0 && offset == 0 { + r, err := f.file.Open() + if err != nil { + return 0, err + } + f.Close() + f.ReadCloser = r + return 0, nil + } + return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name) } func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) { @@ -161,11 +188,11 @@ func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) { return list, nil } -func NewZipFS(rc *zip.ReadCloser) FileSystem { +func NewZipFS(rc *zip.ReadCloser, name string) FileSystem { list := make(zipList, len(rc.File)) copy(list, rc.File) // sort a copy of rc.File sort.Sort(list) - return &zipFS{rc, list} + return &zipFS{rc, list, name} } type zipList []*zip.File