diff --git a/cmd/godoc/README.godoc-app b/cmd/godoc/README.godoc-app new file mode 100644 index 0000000000..cff7d387c1 --- /dev/null +++ b/cmd/godoc/README.godoc-app @@ -0,0 +1,61 @@ +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. + +godoc on appengine +------------------ + +Prerequisites +------------- + +* Go appengine SDK + https://developers.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go + +* Go sources at tip under $GOROOT + + +Directory structure +------------------- + +* Let $APPDIR be the directory containing the app engine files. + (e.g., $APPDIR=$HOME/godoc-app) + +* $APPDIR contains the following entries (this may change depending on + app-engine release and version of godoc): + + app.yaml + godoc.zip + godoc/ + index.split.* + +* The app.yaml file is set up per app engine documentation. + For instance: + + application: godoc-app + version: 1 + runtime: go + api_version: go1 + + handlers: + - url: /.* + script: _go_app + +* The godoc/ directory contains a copy of the files under $GOROOT/src/cmd/godoc + with doc.go excluded (it belongs to pseudo-package "documentation") + + +Configuring and running godoc +----------------------------- + +To configure godoc, run + + bash setup-godoc-app.bash + +to create the godoc.zip, index.split.*, and godoc/appconfig.go files +based on $GOROOT and $APPDIR. See the script for details on usage. + +To run godoc locally, using the app-engine emulator, run + + /dev_appserver.py $APPDIR + +godoc should come up at http://localhost:8080 . diff --git a/cmd/godoc/appinit.go b/cmd/godoc/appinit.go new file mode 100644 index 0000000000..996b2b8504 --- /dev/null +++ b/cmd/godoc/appinit.go @@ -0,0 +1,69 @@ +// 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. + +// +build appengine + +package main + +// This file replaces main.go when running godoc under app-engine. +// See README.godoc-app for details. + +import ( + "archive/zip" + "log" + "net/http" + "path" +) + +func serveError(w http.ResponseWriter, r *http.Request, relpath string, err error) { + w.WriteHeader(http.StatusNotFound) + servePage(w, Page{ + Title: "File " + relpath, + Subtitle: relpath, + Body: applyTemplate(errorHTML, "errorHTML", err), // err may contain an absolute path! + }) +} + +func init() { + log.Println("initializing godoc ...") + log.Printf(".zip file = %s", zipFilename) + log.Printf(".zip GOROOT = %s", zipGoroot) + log.Printf("index files = %s", indexFilenames) + + // initialize flags for app engine + *goroot = path.Join("/", zipGoroot) // fsHttp paths are relative to '/' + *indexEnabled = true + *indexFiles = indexFilenames + *maxResults = 100 // reduce latency by limiting the number of fulltext search results + *indexThrottle = 0.3 // in case *indexFiles is empty (and thus the indexer is run) + *showPlayground = true + + // read .zip file and set up file systems + const zipfile = zipFilename + rc, err := zip.OpenReader(zipfile) + if err != nil { + log.Fatalf("%s: %s\n", zipfile, err) + } + // rc is never closed (app running forever) + fs.Bind("/", NewZipFS(rc, zipFilename), *goroot, bindReplace) + + // initialize http handlers + readTemplates() + initHandlers() + registerPublicHandlers(http.DefaultServeMux) + registerPlaygroundHandlers(http.DefaultServeMux) + + // initialize default directory tree with corresponding timestamp. + initFSTree() + + // Immediately update metadata. + updateMetadata() + + // initialize search index + if *indexEnabled { + go indexer() + } + + log.Println("godoc initialization complete") +} diff --git a/cmd/godoc/codewalk.go b/cmd/godoc/codewalk.go new file mode 100644 index 0000000000..e68c0fa6ba --- /dev/null +++ b/cmd/godoc/codewalk.go @@ -0,0 +1,494 @@ +// Copyright 2010 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. + +// The /doc/codewalk/ tree is synthesized from codewalk descriptions, +// files named $GOROOT/doc/codewalk/*.xml. +// For an example and a description of the format, see +// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060 +// and see http://localhost:6060/doc/codewalk/codewalk . +// That page is itself a codewalk; the source code for it is +// $GOROOT/doc/codewalk/codewalk.xml. + +package main + +import ( + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "regexp" + "sort" + "strconv" + "strings" + "text/template" + "unicode/utf8" +) + +// Handler for /doc/codewalk/ and below. +func codewalk(w http.ResponseWriter, r *http.Request) { + relpath := r.URL.Path[len("/doc/codewalk/"):] + abspath := r.URL.Path + + r.ParseForm() + if f := r.FormValue("fileprint"); f != "" { + codewalkFileprint(w, r, f) + return + } + + // If directory exists, serve list of code walks. + dir, err := fs.Lstat(abspath) + if err == nil && dir.IsDir() { + codewalkDir(w, r, relpath, abspath) + return + } + + // If file exists, serve using standard file server. + if err == nil { + serveFile(w, r) + return + } + + // Otherwise append .xml and hope to find + // a codewalk description, but before trim + // the trailing /. + abspath = strings.TrimRight(abspath, "/") + cw, err := loadCodewalk(abspath + ".xml") + if err != nil { + log.Print(err) + serveError(w, r, relpath, err) + return + } + + // Canonicalize the path and redirect if changed + if redirect(w, r) { + return + } + + servePage(w, Page{ + Title: "Codewalk: " + cw.Title, + Tabtitle: cw.Title, + Body: applyTemplate(codewalkHTML, "codewalk", cw), + }) +} + +// A Codewalk represents a single codewalk read from an XML file. +type Codewalk struct { + Title string `xml:"title,attr"` + File []string `xml:"file"` + Step []*Codestep `xml:"step"` +} + +// A Codestep is a single step in a codewalk. +type Codestep struct { + // Filled in from XML + Src string `xml:"src,attr"` + Title string `xml:"title,attr"` + XML string `xml:",innerxml"` + + // Derived from Src; not in XML. + Err error + File string + Lo int + LoByte int + Hi int + HiByte int + Data []byte +} + +// String method for printing in template. +// Formats file address nicely. +func (st *Codestep) String() string { + s := st.File + if st.Lo != 0 || st.Hi != 0 { + s += fmt.Sprintf(":%d", st.Lo) + if st.Lo != st.Hi { + s += fmt.Sprintf(",%d", st.Hi) + } + } + return s +} + +// loadCodewalk reads a codewalk from the named XML file. +func loadCodewalk(filename string) (*Codewalk, error) { + f, err := fs.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + cw := new(Codewalk) + d := xml.NewDecoder(f) + d.Entity = xml.HTMLEntity + err = d.Decode(cw) + if err != nil { + return nil, &os.PathError{Op: "parsing", Path: filename, Err: err} + } + + // Compute file list, evaluate line numbers for addresses. + m := make(map[string]bool) + for _, st := range cw.Step { + i := strings.Index(st.Src, ":") + if i < 0 { + i = len(st.Src) + } + filename := st.Src[0:i] + data, err := ReadFile(fs, filename) + if err != nil { + st.Err = err + continue + } + if i < len(st.Src) { + lo, hi, err := addrToByteRange(st.Src[i+1:], 0, data) + if err != nil { + st.Err = err + continue + } + // Expand match to line boundaries. + for lo > 0 && data[lo-1] != '\n' { + lo-- + } + for hi < len(data) && (hi == 0 || data[hi-1] != '\n') { + hi++ + } + st.Lo = byteToLine(data, lo) + st.Hi = byteToLine(data, hi-1) + } + st.Data = data + st.File = filename + m[filename] = true + } + + // Make list of files + cw.File = make([]string, len(m)) + i := 0 + for f := range m { + cw.File[i] = f + i++ + } + sort.Strings(cw.File) + + return cw, nil +} + +// codewalkDir serves the codewalk directory listing. +// It scans the directory for subdirectories or files named *.xml +// and prepares a table. +func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string) { + type elem struct { + Name string + Title string + } + + dir, err := fs.ReadDir(abspath) + if err != nil { + log.Print(err) + serveError(w, r, relpath, err) + return + } + var v []interface{} + for _, fi := range dir { + name := fi.Name() + if fi.IsDir() { + v = append(v, &elem{name + "/", ""}) + } else if strings.HasSuffix(name, ".xml") { + cw, err := loadCodewalk(abspath + "/" + name) + if err != nil { + continue + } + v = append(v, &elem{name[0 : len(name)-len(".xml")], cw.Title}) + } + } + + servePage(w, Page{ + Title: "Codewalks", + Body: applyTemplate(codewalkdirHTML, "codewalkdir", v), + }) +} + +// codewalkFileprint serves requests with ?fileprint=f&lo=lo&hi=hi. +// The filename f has already been retrieved and is passed as an argument. +// Lo and hi are the numbers of the first and last line to highlight +// in the response. This format is used for the middle window pane +// 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 := f + data, err := ReadFile(fs, abspath) + if err != nil { + log.Print(err) + serveError(w, r, f, err) + return + } + lo, _ := strconv.Atoi(r.FormValue("lo")) + hi, _ := strconv.Atoi(r.FormValue("hi")) + if hi < lo { + hi = lo + } + lo = lineToByte(data, lo) + hi = lineToByte(data, hi+1) + + // Put the mark 4 lines before lo, so that the iframe + // shows a few lines of context before the highlighted + // section. + n := 4 + mark := lo + for ; mark > 0 && n > 0; mark-- { + if data[mark-1] == '\n' { + if n--; n == 0 { + break + } + } + } + + io.WriteString(w, `
`)
+	template.HTMLEscape(w, data[0:mark])
+	io.WriteString(w, "")
+	template.HTMLEscape(w, data[mark:lo])
+	if lo < hi {
+		io.WriteString(w, "
") + template.HTMLEscape(w, data[lo:hi]) + io.WriteString(w, "
") + } + template.HTMLEscape(w, data[hi:]) + io.WriteString(w, "
") +} + +// addrToByte evaluates the given address starting at offset start in data. +// It returns the lo and hi byte offset of the matched region within data. +// See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II +// for details on the syntax. +func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) { + var ( + dir byte + prevc byte + charOffset bool + ) + lo = start + hi = start + for addr != "" && err == nil { + c := addr[0] + switch c { + default: + err = errors.New("invalid address syntax near " + string(c)) + case ',': + if len(addr) == 1 { + hi = len(data) + } else { + _, hi, err = addrToByteRange(addr[1:], hi, data) + } + return + + case '+', '-': + if prevc == '+' || prevc == '-' { + lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset) + } + dir = c + + case '$': + lo = len(data) + hi = len(data) + if len(addr) > 1 { + dir = '+' + } + + case '#': + charOffset = true + + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + var i int + for i = 1; i < len(addr); i++ { + if addr[i] < '0' || addr[i] > '9' { + break + } + } + var n int + n, err = strconv.Atoi(addr[0:i]) + if err != nil { + break + } + lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset) + dir = 0 + charOffset = false + prevc = c + addr = addr[i:] + continue + + case '/': + var i, j int + Regexp: + for i = 1; i < len(addr); i++ { + switch addr[i] { + case '\\': + i++ + case '/': + j = i + 1 + break Regexp + } + } + if j == 0 { + j = i + } + pattern := addr[1:i] + lo, hi, err = addrRegexp(data, lo, hi, dir, pattern) + prevc = c + addr = addr[j:] + continue + } + prevc = c + addr = addr[1:] + } + + if err == nil && dir != 0 { + lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset) + } + if err != nil { + return 0, 0, err + } + return lo, hi, nil +} + +// addrNumber applies the given dir, n, and charOffset to the address lo, hi. +// dir is '+' or '-', n is the count, and charOffset is true if the syntax +// used was #n. Applying +n (or +#n) means to advance n lines +// (or characters) after hi. Applying -n (or -#n) means to back up n lines +// (or characters) before lo. +// The return value is the new lo, hi. +func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) { + switch dir { + case 0: + lo = 0 + hi = 0 + fallthrough + + case '+': + if charOffset { + pos := hi + for ; n > 0 && pos < len(data); n-- { + _, size := utf8.DecodeRune(data[pos:]) + pos += size + } + if n == 0 { + return pos, pos, nil + } + break + } + // find next beginning of line + if hi > 0 { + for hi < len(data) && data[hi-1] != '\n' { + hi++ + } + } + lo = hi + if n == 0 { + return lo, hi, nil + } + for ; hi < len(data); hi++ { + if data[hi] != '\n' { + continue + } + switch n--; n { + case 1: + lo = hi + 1 + case 0: + return lo, hi + 1, nil + } + } + + case '-': + if charOffset { + // Scan backward for bytes that are not UTF-8 continuation bytes. + pos := lo + for ; pos > 0 && n > 0; pos-- { + if data[pos]&0xc0 != 0x80 { + n-- + } + } + if n == 0 { + return pos, pos, nil + } + break + } + // find earlier beginning of line + for lo > 0 && data[lo-1] != '\n' { + lo-- + } + hi = lo + if n == 0 { + return lo, hi, nil + } + for ; lo >= 0; lo-- { + if lo > 0 && data[lo-1] != '\n' { + continue + } + switch n--; n { + case 1: + hi = lo + case 0: + return lo, hi, nil + } + } + } + + return 0, 0, errors.New("address out of range") +} + +// addrRegexp searches for pattern in the given direction starting at lo, hi. +// The direction dir is '+' (search forward from hi) or '-' (search backward from lo). +// Backward searches are unimplemented. +func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) { + re, err := regexp.Compile(pattern) + if err != nil { + return 0, 0, err + } + if dir == '-' { + // Could implement reverse search using binary search + // through file, but that seems like overkill. + return 0, 0, errors.New("reverse search not implemented") + } + m := re.FindIndex(data[hi:]) + if len(m) > 0 { + m[0] += hi + m[1] += hi + } else if hi > 0 { + // No match. Wrap to beginning of data. + m = re.FindIndex(data) + } + if len(m) == 0 { + return 0, 0, errors.New("no match for " + pattern) + } + return m[0], m[1], nil +} + +// lineToByte returns the byte index of the first byte of line n. +// Line numbers begin at 1. +func lineToByte(data []byte, n int) int { + if n <= 1 { + return 0 + } + n-- + for i, c := range data { + if c == '\n' { + if n--; n == 0 { + return i + 1 + } + } + } + return len(data) +} + +// byteToLine returns the number of the line containing the byte at index i. +func byteToLine(data []byte, i int) int { + l := 1 + for j, c := range data { + if j == i { + return l + } + if c == '\n' { + l++ + } + } + return l +} diff --git a/cmd/godoc/dirtrees.go b/cmd/godoc/dirtrees.go new file mode 100644 index 0000000000..fda7adce52 --- /dev/null +++ b/cmd/godoc/dirtrees.go @@ -0,0 +1,320 @@ +// Copyright 2010 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 contains the code dealing with package directory trees. + +package main + +import ( + "bytes" + "go/doc" + "go/parser" + "go/token" + "log" + "os" + pathpkg "path" + "strings" +) + +// Conventional name for directories containing test data. +// Excluded from directory trees. +// +const testdataDirName = "testdata" + +type Directory struct { + Depth int + Path string // directory path; includes Name + Name string // directory name + HasPkg bool // true if the directory contains at least one package + Synopsis string // package documentation, if any + Dirs []*Directory // subdirectories +} + +func isGoFile(fi os.FileInfo) bool { + name := fi.Name() + return !fi.IsDir() && + len(name) > 0 && name[0] != '.' && // ignore .files + pathpkg.Ext(name) == ".go" +} + +func isPkgFile(fi os.FileInfo) bool { + return isGoFile(fi) && + !strings.HasSuffix(fi.Name(), "_test.go") // ignore test files +} + +func isPkgDir(fi os.FileInfo) bool { + name := fi.Name() + return fi.IsDir() && len(name) > 0 && + name[0] != '_' && name[0] != '.' // ignore _files and .files +} + +type treeBuilder struct { + maxDepth int +} + +func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory { + if name == testdataDirName { + return nil + } + + if depth >= b.maxDepth { + // return a dummy directory so that the parent directory + // doesn't get discarded just because we reached the max + // directory depth + return &Directory{ + Depth: depth, + Path: path, + Name: name, + } + } + + list, _ := fs.ReadDir(path) + + // determine number of subdirectories and if there are package files + ndirs := 0 + hasPkgFiles := false + var synopses [3]string // prioritized package documentation (0 == highest priority) + for _, d := range list { + switch { + case isPkgDir(d): + ndirs++ + case isPkgFile(d): + // looks like a package file, but may just be a file ending in ".go"; + // don't just count it yet (otherwise we may end up with hasPkgFiles even + // 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, pathpkg.Join(path, d.Name()), + parser.ParseComments|parser.PackageClauseOnly) + if err == nil { + hasPkgFiles = true + if file.Doc != nil { + // prioritize documentation + i := -1 + switch file.Name.Name { + case name: + i = 0 // normal case: directory name matches package name + case "main": + i = 1 // directory contains a main package + default: + i = 2 // none of the above + } + if 0 <= i && i < len(synopses) && synopses[i] == "" { + synopses[i] = doc.Synopsis(file.Doc.Text()) + } + } + } + } + } + } + + // create subdirectory tree + var dirs []*Directory + if ndirs > 0 { + dirs = make([]*Directory, ndirs) + i := 0 + for _, d := range list { + if isPkgDir(d) { + name := d.Name() + dd := b.newDirTree(fset, pathpkg.Join(path, name), name, depth+1) + if dd != nil { + dirs[i] = dd + i++ + } + } + } + dirs = dirs[0:i] + } + + // if there are no package files and no subdirectories + // containing package files, ignore the directory + if !hasPkgFiles && len(dirs) == 0 { + return nil + } + + // select the highest-priority synopsis for the directory entry, if any + synopsis := "" + for _, synopsis = range synopses { + if synopsis != "" { + break + } + } + + return &Directory{ + Depth: depth, + Path: path, + Name: name, + HasPkg: hasPkgFiles, + Synopsis: synopsis, + Dirs: dirs, + } +} + +// newDirectory creates a new package directory tree with at most maxDepth +// levels, anchored at root. The result tree is pruned such that it only +// contains directories that contain package files or that contain +// subdirectories containing package files (transitively). If a non-nil +// pathFilter is provided, directory paths additionally must be accepted +// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is +// provided for maxDepth, nodes at larger depths are pruned as well; they +// 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, 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 + // is is hard to see why a directory tree was not built. + switch { + case err != nil: + log.Printf("newDirectory(%s): %s", root, err) + return nil + case !isPkgDir(d): + log.Printf("newDirectory(%s): not a package directory", root) + return nil + } + if maxDepth < 0 { + maxDepth = 1e6 // "infinity" + } + 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) +} + +func (dir *Directory) writeLeafs(buf *bytes.Buffer) { + if dir != nil { + if len(dir.Dirs) == 0 { + buf.WriteString(dir.Path) + buf.WriteByte('\n') + return + } + + for _, d := range dir.Dirs { + d.writeLeafs(buf) + } + } +} + +func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) { + if dir != nil { + if !skipRoot { + c <- dir + } + for _, d := range dir.Dirs { + d.walk(c, false) + } + } +} + +func (dir *Directory) iter(skipRoot bool) <-chan *Directory { + c := make(chan *Directory) + go func() { + dir.walk(c, skipRoot) + close(c) + }() + return c +} + +func (dir *Directory) lookupLocal(name string) *Directory { + for _, d := range dir.Dirs { + if d.Name == name { + return d + } + } + return nil +} + +func splitPath(p string) []string { + p = strings.TrimPrefix(p, "/") + 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 := splitPath(dir.Path) + p := splitPath(path) + i := 0 + for i < len(d) { + if i >= len(p) || d[i] != p[i] { + return nil + } + i++ + } + for dir != nil && i < len(p) { + dir = dir.lookupLocal(p[i]) + i++ + } + return dir +} + +// DirEntry describes a directory entry. The Depth and Height values +// are useful for presenting an entry in an indented fashion. +// +type DirEntry struct { + Depth int // >= 0 + Height int // = DirList.MaxHeight - Depth, > 0 + Path string // directory path; includes Name, relative to DirList root + Name string // directory name + HasPkg bool // true if the directory contains at least one package + Synopsis string // package documentation, if any +} + +type DirList struct { + MaxHeight int // directory tree height, > 0 + List []DirEntry +} + +// listing creates a (linear) directory listing from a directory tree. +// If skipRoot is set, the root directory itself is excluded from the list. +// +func (root *Directory) listing(skipRoot bool) *DirList { + if root == nil { + return nil + } + + // determine number of entries n and maximum height + n := 0 + minDepth := 1 << 30 // infinity + maxDepth := 0 + for d := range root.iter(skipRoot) { + n++ + if minDepth > d.Depth { + minDepth = d.Depth + } + if maxDepth < d.Depth { + maxDepth = d.Depth + } + } + maxHeight := maxDepth - minDepth + 1 + + if n == 0 { + return nil + } + + // create list + list := make([]DirEntry, n) + i := 0 + for d := range root.iter(skipRoot) { + p := &list[i] + p.Depth = d.Depth - minDepth + p.Height = maxHeight - p.Depth + // the path is relative to root.Path - remove the root.Path + // prefix (the prefix should always be present but avoid + // crashes and check) + path := strings.TrimPrefix(d.Path, root.Path) + // remove leading separator if any - path must be relative + path = strings.TrimPrefix(path, "/") + p.Path = path + p.Name = d.Name + p.HasPkg = d.HasPkg + p.Synopsis = d.Synopsis + i++ + } + + return &DirList{maxHeight, list} +} diff --git a/cmd/godoc/doc.go b/cmd/godoc/doc.go new file mode 100644 index 0000000000..1fa57a8b31 --- /dev/null +++ b/cmd/godoc/doc.go @@ -0,0 +1,135 @@ +// 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. + +/* + +Godoc extracts and generates documentation for Go programs. + +It has two modes. + +Without the -http flag, it runs in command-line mode and prints plain text +documentation to standard output and exits. If both a library package and +a command with the same name exists, using the prefix cmd/ will force +documentation on the command rather than the library package. If the -src +flag is specified, godoc prints the exported interface of a package in Go +source form, or the implementation of a specific exported language entity: + + godoc fmt # documentation for package fmt + godoc fmt Printf # documentation for fmt.Printf + godoc cmd/go # force documentation for the go command + godoc -src fmt # fmt package interface in Go source form + godoc -src fmt Printf # implementation of fmt.Printf + +In command-line mode, the -q flag enables search queries against a godoc running +as a webserver. If no explicit server address is specified with the -server flag, +godoc first tries localhost:6060 and then http://golang.org. + + godoc -q Reader + godoc -q math.Sin + godoc -server=:6060 -q sin + +With the -http flag, it runs as a web server and presents the documentation as a +web page. + + godoc -http=:6060 + +Usage: + godoc [flag] package [name ...] + +The flags are: + -v + verbose mode + -q + arguments are considered search queries: a legal query is a + single identifier (such as ToLower) or a qualified identifier + (such as math.Sin). + -src + print (exported) source in command-line mode + -tabwidth=4 + width of tabs in units of spaces + -timestamps=true + show timestamps with directory listings + -index + enable identifier and full text search index + (no search box is shown if -index is not set) + -index_files="" + glob pattern specifying index files; if not empty, + the index is read from these files in sorted order + -index_throttle=0.75 + index throttle value; a value of 0 means no time is allocated + to the indexer (the indexer will never finish), a value of 1.0 + means that index creation is running at full throttle (other + goroutines may get no time while the index is built) + -links=true: + link identifiers to their declarations + -write_index=false + write index to a file; the file name must be specified with + -index_files + -maxresults=10000 + maximum number of full text search results shown + (no full text index is built if maxresults <= 0) + -notes="BUG" + regular expression matching note markers to show + (e.g., "BUG|TODO", ".*") + -html + print HTML in command-line mode + -goroot=$GOROOT + Go root directory + -http=addr + HTTP service address (e.g., '127.0.0.1:6060' or just ':6060') + -server=addr + webserver address for command line searches + -templates="" + directory containing alternate template files; if set, + the directory may provide alternative template files + for the files in $GOROOT/lib/godoc + -url=path + print to standard output the data that would be served by + an HTTP request for path + -zip="" + zip file providing the file system to serve; disabled if empty + +By default, godoc looks at the packages it finds via $GOROOT and $GOPATH (if set). +This behavior can be altered by providing an alternative $GOROOT with the -goroot +flag. + +When godoc runs as a web server and -index is set, a search index is maintained. +The index is created at startup. + +The index contains both identifier and full text search information (searchable +via regular expressions). The maximum number of full text search results shown +can be set with the -maxresults flag; if set to 0, no full text results are +shown, and only an identifier index but no full text search index is created. + +The presentation mode of web pages served by godoc can be controlled with the +"m" URL parameter; it accepts a comma-separated list of flag names as value: + + all show documentation for all declarations, not just the exported ones + methods show all embedded methods, not just those of unexported anonymous fields + src show the original source code rather then the extracted documentation + text present the page in textual (command-line) form rather than HTML + flat present flat (not indented) directory listings using full paths + +For instance, http://golang.org/pkg/math/big/?m=all,text shows the documentation +for all (not just the exported) declarations of package big, in textual form (as +it would appear when using godoc from the command line: "godoc -src math/big .*"). + +By default, godoc serves files from the file system of the underlying OS. +Instead, a .zip file may be provided via the -zip flag, which contains +the file system to serve. The file paths stored in the .zip file must use +slash ('/') as path separator; and they must be unrooted. $GOROOT (or -goroot) +must be set to the .zip file directory path containing the Go root directory. +For instance, for a .zip file created by the command: + + zip go.zip $HOME/go + +one may run godoc as follows: + + godoc -http=:6060 -zip=go.zip -goroot=$HOME/go + +See "Godoc: documenting Go code" for how to write good comments for godoc: +http://golang.org/doc/articles/godoc_documenting_go_code.html + +*/ +package main diff --git a/cmd/godoc/filesystem.go b/cmd/godoc/filesystem.go new file mode 100644 index 0000000000..0309d7cabe --- /dev/null +++ b/cmd/godoc/filesystem.go @@ -0,0 +1,562 @@ +// 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 defines types for abstract file system access and +// provides an implementation accessing the file system of the +// underlying OS. + +package main + +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) (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. +func ReadFile(fs FileSystem, path string) ([]byte, error) { + rc, err := fs.Open(path) + if err != nil { + return nil, err + } + defer rc.Close() + return ioutil.ReadAll(rc) +} + +// 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 + +func (root osFS) String() string { return "os(" + string(root) + ")" } + +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) + + 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 + } + fi, err := f.Stat() + if err != nil { + return nil, err + } + if fi.IsDir() { + return nil, fmt.Errorf("Open: %s is a directory", path) + } + return f, nil +} + +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{Op: "open", Path: path, Err: 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{Op: "stat", Path: path, Err: 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 + first []os.FileInfo + ) + + for _, m := range ns.resolve(path) { + dir, err1 := m.fs.ReadDir(m.translate(path)) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + + if dir == nil { + dir = []os.FileInfo{} + } + + if first == nil { + first = dir + } + + // 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) + } + } + } + + // We didn't find any directories containing Go files. + // If some directory returned successfully, use that. + if !haveGo { + for _, d := range first { + if !haveName[d.Name()] { + haveName[d.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):] + elem = strings.TrimPrefix(elem, "/") + 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 +} + +// httpFile implements http.File for a file (not directory) in a FileSystem. +type httpFile struct { + fs FileSystem + readSeekCloser + name string +} + +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/cmd/godoc/format.go b/cmd/godoc/format.go new file mode 100644 index 0000000000..59a89c5bf9 --- /dev/null +++ b/cmd/godoc/format.go @@ -0,0 +1,372 @@ +// 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 implements FormatSelections and FormatText. +// FormatText is used to HTML-format Go and non-Go source +// text with line numbers and highlighted sections. It is +// built on top of FormatSelections, a generic formatter +// for "selected" text. + +package main + +import ( + "fmt" + "go/scanner" + "go/token" + "io" + "regexp" + "strconv" + "text/template" +) + +// ---------------------------------------------------------------------------- +// Implementation of FormatSelections + +// A Segment describes a text segment [start, end). +// The zero value of a Segment is a ready-to-use empty segment. +// +type Segment struct { + start, end int +} + +func (seg *Segment) isEmpty() bool { return seg.start >= seg.end } + +// A Selection is an "iterator" function returning a text segment. +// Repeated calls to a selection return consecutive, non-overlapping, +// non-empty segments, followed by an infinite sequence of empty +// segments. The first empty segment marks the end of the selection. +// +type Selection func() Segment + +// A LinkWriter writes some start or end "tag" to w for the text offset offs. +// It is called by FormatSelections at the start or end of each link segment. +// +type LinkWriter func(w io.Writer, offs int, start bool) + +// A SegmentWriter formats a text according to selections and writes it to w. +// The selections parameter is a bit set indicating which selections provided +// to FormatSelections overlap with the text segment: If the n'th bit is set +// in selections, the n'th selection provided to FormatSelections is overlapping +// with the text. +// +type SegmentWriter func(w io.Writer, text []byte, selections int) + +// FormatSelections takes a text and writes it to w using link and segment +// writers lw and sw as follows: lw is invoked for consecutive segment starts +// and ends as specified through the links selection, and sw is invoked for +// consecutive segments of text overlapped by the same selections as specified +// by selections. The link writer lw may be nil, in which case the links +// Selection is ignored. +// +func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) { + // If we have a link writer, make the links + // selection the last entry in selections + if lw != nil { + selections = append(selections, links) + } + + // compute the sequence of consecutive segment changes + changes := newMerger(selections) + + // The i'th bit in bitset indicates that the text + // at the current offset is covered by selections[i]. + bitset := 0 + lastOffs := 0 + + // Text segments are written in a delayed fashion + // such that consecutive segments belonging to the + // same selection can be combined (peephole optimization). + // last describes the last segment which has not yet been written. + var last struct { + begin, end int // valid if begin < end + bitset int + } + + // flush writes the last delayed text segment + flush := func() { + if last.begin < last.end { + sw(w, text[last.begin:last.end], last.bitset) + } + last.begin = last.end // invalidate last + } + + // segment runs the segment [lastOffs, end) with the selection + // indicated by bitset through the segment peephole optimizer. + segment := func(end int) { + if lastOffs < end { // ignore empty segments + if last.end != lastOffs || last.bitset != bitset { + // the last segment is not adjacent to or + // differs from the new one + flush() + // start a new segment + last.begin = lastOffs + } + last.end = end + last.bitset = bitset + } + } + + for { + // get the next segment change + index, offs, start := changes.next() + if index < 0 || offs > len(text) { + // no more segment changes or the next change + // is past the end of the text - we're done + break + } + // determine the kind of segment change + if lw != nil && index == len(selections)-1 { + // we have a link segment change (see start of this function): + // format the previous selection segment, write the + // link tag and start a new selection segment + segment(offs) + flush() + lastOffs = offs + lw(w, offs, start) + } else { + // we have a selection change: + // format the previous selection segment, determine + // the new selection bitset and start a new segment + segment(offs) + lastOffs = offs + mask := 1 << uint(index) + if start { + bitset |= mask + } else { + bitset &^= mask + } + } + } + segment(len(text)) + flush() +} + +// A merger merges a slice of Selections and produces a sequence of +// consecutive segment change events through repeated next() calls. +// +type merger struct { + selections []Selection + segments []Segment // segments[i] is the next segment of selections[i] +} + +const infinity int = 2e9 + +func newMerger(selections []Selection) *merger { + segments := make([]Segment, len(selections)) + for i, sel := range selections { + segments[i] = Segment{infinity, infinity} + if sel != nil { + if seg := sel(); !seg.isEmpty() { + segments[i] = seg + } + } + } + return &merger{selections, segments} +} + +// next returns the next segment change: index specifies the Selection +// to which the segment belongs, offs is the segment start or end offset +// as determined by the start value. If there are no more segment changes, +// next returns an index value < 0. +// +func (m *merger) next() (index, offs int, start bool) { + // find the next smallest offset where a segment starts or ends + offs = infinity + index = -1 + for i, seg := range m.segments { + switch { + case seg.start < offs: + offs = seg.start + index = i + start = true + case seg.end < offs: + offs = seg.end + index = i + start = false + } + } + if index < 0 { + // no offset found => all selections merged + return + } + // offset found - it's either the start or end offset but + // either way it is ok to consume the start offset: set it + // to infinity so it won't be considered in the following + // next call + m.segments[index].start = infinity + if start { + return + } + // end offset found - consume it + m.segments[index].end = infinity + // advance to the next segment for that selection + seg := m.selections[index]() + if !seg.isEmpty() { + m.segments[index] = seg + } + return +} + +// ---------------------------------------------------------------------------- +// Implementation of FormatText + +// lineSelection returns the line segments for text as a Selection. +func lineSelection(text []byte) Selection { + i, j := 0, 0 + return func() (seg Segment) { + // find next newline, if any + for j < len(text) { + j++ + if text[j-1] == '\n' { + break + } + } + if i < j { + // text[i:j] constitutes a line + seg = Segment{i, j} + i = j + } + return + } +} + +// tokenSelection returns, as a selection, the sequence of +// consecutive occurrences of token sel in the Go src text. +// +func tokenSelection(src []byte, sel token.Token) Selection { + var s scanner.Scanner + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, src, nil, scanner.ScanComments) + return func() (seg Segment) { + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + offs := file.Offset(pos) + if tok == sel { + seg = Segment{offs, offs + len(lit)} + break + } + } + return + } +} + +// makeSelection is a helper function to make a Selection from a slice of pairs. +// Pairs describing empty segments are ignored. +// +func makeSelection(matches [][]int) Selection { + i := 0 + return func() Segment { + for i < len(matches) { + m := matches[i] + i++ + if m[0] < m[1] { + // non-empty segment + return Segment{m[0], m[1]} + } + } + return Segment{} + } +} + +// regexpSelection computes the Selection for the regular expression expr in text. +func regexpSelection(text []byte, expr string) Selection { + var matches [][]int + if rx, err := regexp.Compile(expr); err == nil { + matches = rx.FindAllIndex(text, -1) + } + return makeSelection(matches) +} + +var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`) + +// rangeSelection computes the Selection for a text range described +// by the argument str; the range description must match the selRx +// regular expression. +// +func rangeSelection(str string) Selection { + m := selRx.FindStringSubmatch(str) + if len(m) >= 2 { + from, _ := strconv.Atoi(m[1]) + to, _ := strconv.Atoi(m[2]) + if from < to { + return makeSelection([][]int{{from, to}}) + } + } + return nil +} + +// Span tags for all the possible selection combinations that may +// be generated by FormatText. Selections are indicated by a bitset, +// and the value of the bitset specifies the tag to be used. +// +// bit 0: comments +// bit 1: highlights +// bit 2: selections +// +var startTags = [][]byte{ + /* 000 */ []byte(``), + /* 001 */ []byte(``), + /* 010 */ []byte(``), + /* 011 */ []byte(``), + /* 100 */ []byte(``), + /* 101 */ []byte(``), + /* 110 */ []byte(``), + /* 111 */ []byte(``), +} + +var endTag = []byte(``) + +func selectionTag(w io.Writer, text []byte, selections int) { + if selections < len(startTags) { + if tag := startTags[selections]; len(tag) > 0 { + w.Write(tag) + template.HTMLEscape(w, text) + w.Write(endTag) + return + } + } + template.HTMLEscape(w, text) +} + +// FormatText HTML-escapes text and writes it to w. +// Consecutive text segments are wrapped in HTML spans (with tags as +// defined by startTags and endTag) as follows: +// +// - if line >= 0, line number (ln) spans are inserted before each line, +// starting with the value of line +// - if the text is Go source, comments get the "comment" span class +// - each occurrence of the regular expression pattern gets the "highlight" +// span class +// - text segments covered by selection get the "selection" span class +// +// Comments, highlights, and selections may overlap arbitrarily; the respective +// HTML span classes are specified in the startTags variable. +// +func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) { + var comments, highlights Selection + if goSource { + comments = tokenSelection(text, token.COMMENT) + } + if pattern != "" { + highlights = regexpSelection(text, pattern) + } + if line >= 0 || comments != nil || highlights != nil || selection != nil { + var lineTag LinkWriter + if line >= 0 { + lineTag = func(w io.Writer, _ int, start bool) { + if start { + fmt.Fprintf(w, "%6d\t", line, line) + line++ + } + } + } + FormatSelections(w, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection) + } else { + template.HTMLEscape(w, text) + } +} diff --git a/cmd/godoc/godoc.go b/cmd/godoc/godoc.go new file mode 100644 index 0000000000..79d485b93d --- /dev/null +++ b/cmd/godoc/godoc.go @@ -0,0 +1,1586 @@ +// 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. + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/build" + "go/doc" + "go/format" + "go/printer" + "go/token" + htmlpkg "html" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + pathpkg "path" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + "text/template" + "time" + "unicode" + "unicode/utf8" +) + +// ---------------------------------------------------------------------------- +// Globals + +type delayTime struct { + RWValue +} + +func (dt *delayTime) backoff(max time.Duration) { + dt.mutex.Lock() + v := dt.value.(time.Duration) * 2 + if v > max { + v = max + } + dt.value = v + // don't change dt.timestamp - calling backoff indicates an error condition + dt.mutex.Unlock() +} + +var ( + verbose = flag.Bool("v", false, "verbose mode") + + // 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)") + + // layout control + tabwidth = flag.Int("tabwidth", 4, "tab width") + showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") + templateDir = flag.String("templates", "", "directory containing alternate template files") + showPlayground = flag.Bool("play", false, "enable playground in web interface") + showExamples = flag.Bool("ex", false, "show examples in command line mode") + declLinks = flag.Bool("links", true, "link identifiers to their declarations") + + // search index + indexEnabled = flag.Bool("index", false, "enable search index") + indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+ + "if not empty, the index is read from these files in sorted order") + 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 information + fsTree RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now) + fsModified RWValue // timestamp of last call to invalidateIndex + docMetadata RWValue // mapping from paths to *Metadata + + // http handlers + fileServer http.Handler // default file server + cmdHandler docServer + pkgHandler docServer + + // source code notes + notes = flag.String("notes", "BUG", "regular expression matching note markers to show") +) + +func initHandlers() { + fileServer = http.FileServer(&httpFS{fs}) + cmdHandler = docServer{"/cmd/", "/src/cmd"} + pkgHandler = docServer{"/pkg/", "/src/pkg"} +} + +func registerPublicHandlers(mux *http.ServeMux) { + mux.Handle(cmdHandler.pattern, &cmdHandler) + mux.Handle(pkgHandler.pattern, &pkgHandler) + mux.HandleFunc("/doc/codewalk/", codewalk) + mux.Handle("/doc/play/", fileServer) + mux.HandleFunc("/search", search) + mux.Handle("/robots.txt", fileServer) + mux.HandleFunc("/opensearch.xml", serveSearchDesc) + mux.HandleFunc("/", serveFile) +} + +func initFSTree() { + dir := newDirectory(pathpkg.Join("/", *testDir), -1) + if dir == nil { + log.Println("Warning: FSTree is nil") + return + } + fsTree.set(dir) + invalidateIndex() +} + +// ---------------------------------------------------------------------------- +// Tab conversion + +var spaces = []byte(" ") // 32 spaces seems like a good number + +const ( + indenting = iota + collecting +) + +// A tconv is an io.Writer filter for converting leading tabs into spaces. +type tconv struct { + output io.Writer + state int // indenting or collecting + indent int // valid if state == indenting +} + +func (p *tconv) writeIndent() (err error) { + i := p.indent + for i >= len(spaces) { + i -= len(spaces) + if _, err = p.output.Write(spaces); err != nil { + return + } + } + // i < len(spaces) + if i > 0 { + _, err = p.output.Write(spaces[0:i]) + } + return +} + +func (p *tconv) Write(data []byte) (n int, err error) { + if len(data) == 0 { + return + } + pos := 0 // valid if p.state == collecting + var b byte + for n, b = range data { + switch p.state { + case indenting: + switch b { + case '\t': + p.indent += *tabwidth + case '\n': + p.indent = 0 + if _, err = p.output.Write(data[n : n+1]); err != nil { + return + } + case ' ': + p.indent++ + default: + p.state = collecting + pos = n + if err = p.writeIndent(); err != nil { + return + } + } + case collecting: + if b == '\n' { + p.state = indenting + p.indent = 0 + if _, err = p.output.Write(data[pos : n+1]); err != nil { + return + } + } + } + } + n = len(data) + if pos < n && p.state == collecting { + _, err = p.output.Write(data[pos:]) + } + return +} + +// ---------------------------------------------------------------------------- +// Templates + +// Write an AST node to w. +func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { + // convert trailing tabs into spaces using a tconv filter + // to ensure a good outcome in most browsers (there may still + // be tabs in comments and strings, but converting those into + // the right number of spaces is much harder) + // + // TODO(gri) rethink printer flags - perhaps tconv can be eliminated + // with an another printer mode (which is more efficiently + // implemented in the printer than here with another layer) + mode := printer.TabIndent | printer.UseSpaces + err := (&printer.Config{Mode: mode, Tabwidth: *tabwidth}).Fprint(&tconv{output: w}, fset, x) + if err != nil { + log.Print(err) + } +} + +func filenameFunc(path string) string { + _, localname := pathpkg.Split(path) + return localname +} + +func fileInfoNameFunc(fi os.FileInfo) string { + name := fi.Name() + if fi.IsDir() { + name += "/" + } + return name +} + +func fileInfoTimeFunc(fi os.FileInfo) string { + if t := fi.ModTime(); t.Unix() != 0 { + return t.Local().String() + } + return "" // don't return epoch if time is obviously not set +} + +// The strings in infoKinds must be properly html-escaped. +var infoKinds = [nKinds]string{ + PackageClause: "package clause", + ImportDecl: "import decl", + ConstDecl: "const decl", + TypeDecl: "type decl", + VarDecl: "var decl", + FuncDecl: "func decl", + MethodDecl: "method decl", + Use: "use", +} + +func infoKind_htmlFunc(info SpotInfo) string { + return infoKinds[info.Kind()] // infoKind entries are html-escaped +} + +func infoLineFunc(info SpotInfo) int { + line := info.Lori() + if info.IsIndex() { + index, _ := searchIndex.get() + if index != nil { + line = index.(*Index).Snippet(line).Line + } else { + // no line information available because + // we don't have an index - this should + // never happen; be conservative and don't + // crash + line = 0 + } + } + return line +} + +func infoSnippet_htmlFunc(info SpotInfo) string { + if info.IsIndex() { + index, _ := searchIndex.get() + // Snippet.Text was HTML-escaped when it was generated + return index.(*Index).Snippet(info.Lori()).Text + } + return `no snippet text available` +} + +func nodeFunc(info *PageInfo, node interface{}) string { + var buf bytes.Buffer + writeNode(&buf, info.FSet, node) + return buf.String() +} + +func node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { + var buf1 bytes.Buffer + writeNode(&buf1, info.FSet, node) + + var buf2 bytes.Buffer + if n, _ := node.(ast.Node); n != nil && linkify && *declLinks { + LinkifyText(&buf2, buf1.Bytes(), n) + } else { + FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) + } + + return buf2.String() +} + +func comment_htmlFunc(comment string) string { + var buf bytes.Buffer + // TODO(gri) Provide list of words (e.g. function parameters) + // to be emphasized by ToHTML. + doc.ToHTML(&buf, comment, nil) // does html-escaping + return buf.String() +} + +// punchCardWidth is the number of columns of fixed-width +// characters to assume when wrapping text. Very few people +// use terminals or cards smaller than 80 characters, so 80 it is. +// We do not try to sniff the environment or the tty to adapt to +// the situation; instead, by using a constant we make sure that +// godoc always produces the same output regardless of context, +// a consistency that is lost otherwise. For example, if we sniffed +// the environment or tty, then http://golang.org/pkg/math/?m=text +// would depend on the width of the terminal where godoc started, +// which is clearly bogus. More generally, the Unix tools that behave +// differently when writing to a tty than when writing to a file have +// a history of causing confusion (compare `ls` and `ls | cat`), and we +// want to avoid that mistake here. +const punchCardWidth = 80 + +func comment_textFunc(comment, indent, preIndent string) string { + var buf bytes.Buffer + doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) + return buf.String() +} + +func startsWithUppercase(s string) bool { + r, _ := utf8.DecodeRuneInString(s) + return unicode.IsUpper(r) +} + +var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) + +// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name +// while keeping uppercase Braz in Foo_Braz. +func stripExampleSuffix(name string) string { + if i := strings.LastIndex(name, "_"); i != -1 { + if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { + name = name[:i] + } + } + return name +} + +func example_textFunc(info *PageInfo, funcName, indent string) string { + if !*showExamples { + return "" + } + + var buf bytes.Buffer + first := true + for _, eg := range info.Examples { + name := stripExampleSuffix(eg.Name) + if name != funcName { + continue + } + + if !first { + buf.WriteString("\n") + } + first = false + + // print code + cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} + var buf1 bytes.Buffer + writeNode(&buf1, info.FSet, cnode) + code := buf1.String() + // Additional formatting if this is a function body. + if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { + // remove surrounding braces + code = code[1 : n-1] + // unindent + code = strings.Replace(code, "\n ", "\n", -1) + } + code = strings.Trim(code, "\n") + code = strings.Replace(code, "\n", "\n\t", -1) + + buf.WriteString(indent) + buf.WriteString("Example:\n\t") + buf.WriteString(code) + buf.WriteString("\n") + } + return buf.String() +} + +func example_htmlFunc(info *PageInfo, funcName string) string { + var buf bytes.Buffer + for _, eg := range info.Examples { + name := stripExampleSuffix(eg.Name) + + if name != funcName { + continue + } + + // print code + cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} + code := node_htmlFunc(info, cnode, true) + out := eg.Output + wholeFile := true + + // Additional formatting if this is a function body. + if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { + wholeFile = false + // remove surrounding braces + code = code[1 : n-1] + // unindent + code = strings.Replace(code, "\n ", "\n", -1) + // remove output comment + if loc := exampleOutputRx.FindStringIndex(code); loc != nil { + code = strings.TrimSpace(code[:loc[0]]) + } + } + + // Write out the playground code in standard Go style + // (use tabs, no comment highlight, etc). + play := "" + if eg.Play != nil && *showPlayground { + var buf bytes.Buffer + if err := format.Node(&buf, info.FSet, eg.Play); err != nil { + log.Print(err) + } else { + play = buf.String() + } + } + + // Drop output, as the output comment will appear in the code. + if wholeFile && play == "" { + out = "" + } + + err := exampleHTML.Execute(&buf, struct { + Name, Doc, Code, Play, Output string + }{eg.Name, eg.Doc, code, play, out}) + if err != nil { + log.Print(err) + } + } + return buf.String() +} + +// example_nameFunc takes an example function name and returns its display +// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". +func example_nameFunc(s string) string { + name, suffix := splitExampleName(s) + // replace _ with . for method names + name = strings.Replace(name, "_", ".", 1) + // use "Package" if no name provided + if name == "" { + name = "Package" + } + return name + suffix +} + +// example_suffixFunc takes an example function name and returns its suffix in +// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". +func example_suffixFunc(name string) string { + _, suffix := splitExampleName(name) + return suffix +} + +func noteTitle(note string) string { + return strings.Title(strings.ToLower(note)) +} + +func splitExampleName(s string) (name, suffix string) { + i := strings.LastIndex(s, "_") + if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { + name = s[:i] + suffix = " (" + strings.Title(s[i+1:]) + ")" + return + } + name = s + return +} + +func pkgLinkFunc(path string) string { + relpath := path[1:] + // because of the irregular mapping under goroot + // we need to correct certain relative paths + relpath = strings.TrimPrefix(relpath, "src/pkg/") + return pkgHandler.pattern[1:] + relpath // remove trailing '/' for relative URL +} + +// n must be an ast.Node or a *doc.Note +func posLink_urlFunc(info *PageInfo, n interface{}) string { + var pos, end token.Pos + + switch n := n.(type) { + case ast.Node: + pos = n.Pos() + end = n.End() + case *doc.Note: + pos = n.Pos + end = n.End + default: + panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) + } + + var relpath string + var line int + var low, high int // selection offset range + + if pos.IsValid() { + p := info.FSet.Position(pos) + relpath = p.Filename + line = p.Line + low = p.Offset + } + if end.IsValid() { + high = info.FSet.Position(end).Offset + } + + var buf bytes.Buffer + template.HTMLEscape(&buf, []byte(relpath)) + // selection ranges are of form "s=low:high" + if low < high { + fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping + // if we have a selection, position the page + // such that the selection is a bit below the top + line -= 10 + if line < 1 { + line = 1 + } + } + // line id's in html-printed source are of the + // form "L%d" where %d stands for the line number + if line > 0 { + fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping + } + + 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 +// require explicit escaping in the template. +var fmap = template.FuncMap{ + // various helpers + "filename": filenameFunc, + "repeat": strings.Repeat, + + // access to FileInfos (directory listings) + "fileInfoName": fileInfoNameFunc, + "fileInfoTime": fileInfoTimeFunc, + + // access to search result information + "infoKind_html": infoKind_htmlFunc, + "infoLine": infoLineFunc, + "infoSnippet_html": infoSnippet_htmlFunc, + + // formatting of AST nodes + "node": nodeFunc, + "node_html": node_htmlFunc, + "comment_html": comment_htmlFunc, + "comment_text": comment_textFunc, + + // support for URL attributes + "pkgLink": pkgLinkFunc, + "srcLink": srcLinkFunc, + "posLink_url": posLink_urlFunc, + + // formatting of Examples + "example_html": example_htmlFunc, + "example_text": example_textFunc, + "example_name": example_nameFunc, + "example_suffix": example_suffixFunc, + + // formatting of Notes + "noteTitle": noteTitle, +} + +func readTemplate(name string) *template.Template { + path := "lib/godoc/" + name + + // use underlying file system fs to read the template file + // (cannot use template ParseFile functions directly) + data, err := ReadFile(fs, path) + if err != nil { + log.Fatal("readTemplate: ", err) + } + // be explicit with errors (for app engine use) + t, err := template.New(name).Funcs(fmap).Parse(string(data)) + if err != nil { + log.Fatal("readTemplate: ", err) + } + return t +} + +var ( + codewalkHTML, + codewalkdirHTML, + dirlistHTML, + errorHTML, + exampleHTML, + godocHTML, + packageHTML, + packageText, + searchHTML, + searchText, + searchDescXML *template.Template +) + +func readTemplates() { + // have to delay until after flags processing since paths depend on goroot + codewalkHTML = readTemplate("codewalk.html") + codewalkdirHTML = readTemplate("codewalkdir.html") + dirlistHTML = readTemplate("dirlist.html") + errorHTML = readTemplate("error.html") + exampleHTML = readTemplate("example.html") + godocHTML = readTemplate("godoc.html") + packageHTML = readTemplate("package.html") + packageText = readTemplate("package.txt") + searchHTML = readTemplate("search.html") + searchText = readTemplate("search.txt") + searchDescXML = readTemplate("opensearch.xml") +} + +// ---------------------------------------------------------------------------- +// Generic HTML wrapper + +// Page describes the contents of the top-level godoc webpage. +type Page struct { + Title string + Tabtitle string + Subtitle string + Query string + Body []byte + + // filled in by servePage + SearchBox bool + Playground bool + Version string +} + +func servePage(w http.ResponseWriter, page Page) { + if page.Tabtitle == "" { + page.Tabtitle = page.Title + } + page.SearchBox = *indexEnabled + page.Playground = *showPlayground + page.Version = runtime.Version() + if err := godocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed { + // Only log if there's an error that's not about writing on HEAD requests. + // See Issues 5451 and 5454. + log.Printf("godocHTML.Execute: %s", err) + } +} + +func serveText(w http.ResponseWriter, text []byte) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write(text) +} + +// ---------------------------------------------------------------------------- +// Files + +var ( + doctype = []byte("") +) + +func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { + // get HTML body contents + src, err := ReadFile(fs, abspath) + if err != nil { + log.Printf("ReadFile: %s", err) + serveError(w, r, relpath, err) + return + } + + // if it begins with "") + FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) + buf.WriteString("") + fmt.Fprintf(&buf, `

View as plain text

`, htmlpkg.EscapeString(relpath)) + + servePage(w, Page{ + Title: title + " " + relpath, + Tabtitle: relpath, + Body: buf.Bytes(), + }) +} + +func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { + if redirect(w, r) { + return + } + + list, err := fs.ReadDir(abspath) + if err != nil { + serveError(w, r, relpath, err) + return + } + + servePage(w, Page{ + Title: "Directory " + relpath, + Tabtitle: relpath, + Body: applyTemplate(dirlistHTML, "dirlistHTML", list), + }) +} + +func serveFile(w http.ResponseWriter, r *http.Request) { + relpath := r.URL.Path + + // Check to see if we need to redirect or serve another file. + if m := metadataFor(relpath); m != nil { + if m.Path != relpath { + // Redirect to canonical path. + http.Redirect(w, r, m.Path, http.StatusMovedPermanently) + return + } + // Serve from the actual filesystem path. + relpath = m.filePath + } + + abspath := relpath + relpath = relpath[1:] // strip leading slash + + switch pathpkg.Ext(relpath) { + case ".html": + if strings.HasSuffix(relpath, "/index.html") { + // We'll show index.html for the directory. + // Use the dir/ version as canonical instead of dir/index.html. + http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) + return + } + serveHTMLDoc(w, r, abspath, relpath) + return + + case ".go": + serveTextFile(w, r, abspath, relpath, "Source file") + return + } + + dir, err := fs.Lstat(abspath) + if err != nil { + log.Print(err) + serveError(w, r, relpath, err) + return + } + + if dir != nil && dir.IsDir() { + if redirect(w, r) { + return + } + if index := pathpkg.Join(abspath, "index.html"); isTextFile(index) { + serveHTMLDoc(w, r, index, index) + return + } + serveDirectory(w, r, abspath, relpath) + return + } + + if isTextFile(abspath) { + if redirectFile(w, r) { + return + } + serveTextFile(w, r, abspath, relpath, "Text file") + return + } + + fileServer.ServeHTTP(w, r) +} + +func serveSearchDesc(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/opensearchdescription+xml") + data := map[string]interface{}{ + "BaseURL": fmt.Sprintf("http://%s", r.Host), + } + if err := searchDescXML.Execute(w, &data); err != nil && err != http.ErrBodyNotAllowed { + // Only log if there's an error that's not about writing on HEAD requests. + // See Issues 5451 and 5454. + log.Printf("searchDescXML.Execute: %s", err) + } +} + +// ---------------------------------------------------------------------------- +// Packages + +// Fake relative package path for built-ins. Documentation for all globals +// (not just exported ones) will be shown for packages in this directory. +const builtinPkgPath = "builtin" + +type PageInfoMode uint + +const ( + noFiltering PageInfoMode = 1 << iota // do not filter exports + allMethods // show all embedded methods + showSource // show source code, do not extract documentation + noHtml // show result in textual form, do not generate HTML + flatDir // show directory in a flat (non-indented) manner +) + +// modeNames defines names for each PageInfoMode flag. +var modeNames = map[string]PageInfoMode{ + "all": noFiltering, + "methods": allMethods, + "src": showSource, + "text": noHtml, + "flat": flatDir, +} + +// getPageInfoMode computes the PageInfoMode flags by analyzing the request +// URL form value "m". It is value is a comma-separated list of mode names +// as defined by modeNames (e.g.: m=src,text). +func getPageInfoMode(r *http.Request) PageInfoMode { + var mode PageInfoMode + for _, k := range strings.Split(r.FormValue("m"), ",") { + if m, found := modeNames[strings.TrimSpace(k)]; found { + mode |= m + } + } + return adjustPageInfoMode(r, mode) +} + +// Specialized versions of godoc may adjust the PageInfoMode by overriding +// this variable. +var adjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode { + return mode +} + +// remoteSearchURL returns the search URL for a given query as needed by +// remoteSearch. If html is set, an html result is requested; otherwise +// the result is in textual form. +// Adjust this function as necessary if modeNames or FormValue parameters +// change. +func remoteSearchURL(query string, html bool) string { + s := "/search?m=text&q=" + if html { + s = "/search?q=" + } + return s + url.QueryEscape(query) +} + +type PageInfo struct { + Dirname string // directory containing the package + Err error // error or nil + + // package info + FSet *token.FileSet // nil if no package documentation + PDoc *doc.Package // nil if no package documentation + Examples []*doc.Example // nil if no example code + Notes map[string][]*doc.Note // nil if no package Notes + PAst *ast.File // nil if no AST with package exports + IsMain bool // true for package main + + // directory info + Dirs *DirList // nil if no directory information + DirTime time.Time // directory time stamp + DirFlat bool // if set, show directory in a flat (non-indented) manner +} + +func (info *PageInfo) IsEmpty() bool { + return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil +} + +type docServer struct { + pattern string // url pattern; e.g. "/pkg/" + fsRoot string // file system root to which the pattern is mapped +} + +// fsReadDir implements ReadDir for the go/build package. +func fsReadDir(dir string) ([]os.FileInfo, error) { + return fs.ReadDir(filepath.ToSlash(dir)) +} + +// fsOpenFile implements OpenFile for the go/build package. +func fsOpenFile(name string) (r io.ReadCloser, err error) { + data, err := ReadFile(fs, filepath.ToSlash(name)) + if err != nil { + return nil, err + } + return ioutil.NopCloser(bytes.NewReader(data)), nil +} + +// packageExports is a local implementation of ast.PackageExports +// which correctly updates each package file's comment list. +// (The ast.PackageExports signature is frozen, hence the local +// implementation). +// +func packageExports(fset *token.FileSet, pkg *ast.Package) { + for _, src := range pkg.Files { + cmap := ast.NewCommentMap(fset, src, src.Comments) + ast.FileExports(src) + src.Comments = cmap.Filter(src).Comments() + } +} + +// addNames adds the names declared by decl to the names set. +// Method names are added in the form ReceiverTypeName_Method. +func addNames(names map[string]bool, decl ast.Decl) { + switch d := decl.(type) { + case *ast.FuncDecl: + name := d.Name.Name + if d.Recv != nil { + var typeName string + switch r := d.Recv.List[0].Type.(type) { + case *ast.StarExpr: + typeName = r.X.(*ast.Ident).Name + case *ast.Ident: + typeName = r.Name + } + name = typeName + "_" + name + } + names[name] = true + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + names[s.Name.Name] = true + case *ast.ValueSpec: + for _, id := range s.Names { + names[id.Name] = true + } + } + } + } +} + +// globalNames returns a set of the names declared by all package-level +// declarations. Method names are returned in the form Receiver_Method. +func globalNames(pkg *ast.Package) map[string]bool { + names := make(map[string]bool) + for _, file := range pkg.Files { + for _, decl := range file.Decls { + addNames(names, decl) + } + } + return names +} + +// collectExamples collects examples for pkg from testfiles. +func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example { + var files []*ast.File + for _, f := range testfiles { + files = append(files, f) + } + + var examples []*doc.Example + globals := globalNames(pkg) + for _, e := range doc.Examples(files...) { + name := stripExampleSuffix(e.Name) + if name == "" || globals[name] { + examples = append(examples, e) + } else { + log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name) + } + } + + return examples +} + +// poorMansImporter returns a (dummy) package object named +// by the last path component of the provided package path +// (as is the convention for packages). This is sufficient +// to resolve package identifiers without doing an actual +// import. It never returns an error. +// +func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { + pkg := imports[path] + if pkg == nil { + // note that strings.LastIndex returns -1 if there is no "/" + pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) + pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import + imports[path] = pkg + } + return pkg, nil +} + +// getPageInfo returns the PageInfo for a package directory abspath. If the +// parameter genAST is set, an AST containing only the package exports is +// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc) +// is extracted from the AST. If there is no corresponding package in the +// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub- +// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is +// set to the respective error but the error is not logged. +// +func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo { + info := &PageInfo{Dirname: abspath} + + // Restrict to the package files that would be used when building + // the package on this system. This makes sure that if there are + // separate implementations for, say, Windows vs Unix, we don't + // jumble them all together. + // Note: Uses current binary's GOOS/GOARCH. + // To use different pair, such as if we allowed the user to choose, + // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir. + ctxt := build.Default + ctxt.IsAbsPath = pathpkg.IsAbs + ctxt.ReadDir = fsReadDir + ctxt.OpenFile = fsOpenFile + pkginfo, err := ctxt.ImportDir(abspath, 0) + // continue if there are no Go source files; we still want the directory info + if _, nogo := err.(*build.NoGoError); err != nil && !nogo { + info.Err = err + return info + } + + // collect package files + pkgname := pkginfo.Name + pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...) + if len(pkgfiles) == 0 { + // Commands written in C have no .go files in the build. + // Instead, documentation may be found in an ignored file. + // The file may be ignored via an explicit +build ignore + // constraint (recommended), or by defining the package + // documentation (historic). + pkgname = "main" // assume package main since pkginfo.Name == "" + pkgfiles = pkginfo.IgnoredGoFiles + } + + // get package information, if any + if len(pkgfiles) > 0 { + // build package AST + fset := token.NewFileSet() + files, err := parseFiles(fset, abspath, pkgfiles) + if err != nil { + info.Err = err + return info + } + + // ignore any errors - they are due to unresolved identifiers + pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) + + // extract package documentation + info.FSet = fset + if mode&showSource == 0 { + // show extracted documentation + var m doc.Mode + if mode&noFiltering != 0 { + m = doc.AllDecls + } + if mode&allMethods != 0 { + m |= doc.AllMethods + } + info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath + + // collect examples + testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...) + files, err = parseFiles(fset, abspath, testfiles) + if err != nil { + log.Println("parsing examples:", err) + } + info.Examples = collectExamples(pkg, files) + + // collect any notes that we want to show + if info.PDoc.Notes != nil { + // could regexp.Compile only once per godoc, but probably not worth it + if rx, err := regexp.Compile(*notes); err == nil { + for m, n := range info.PDoc.Notes { + if rx.MatchString(m) { + if info.Notes == nil { + info.Notes = make(map[string][]*doc.Note) + } + info.Notes[m] = n + } + } + } + } + + } else { + // show source code + // TODO(gri) Consider eliminating export filtering in this mode, + // or perhaps eliminating the mode altogether. + if mode&noFiltering == 0 { + packageExports(fset, pkg) + } + info.PAst = ast.MergePackageFiles(pkg, 0) + } + info.IsMain = pkgname == "main" + } + + // get directory information, if any + var dir *Directory + var timestamp time.Time + if tree, ts := fsTree.get(); tree != nil && tree.(*Directory) != nil { + // directory tree is present; lookup respective directory + // (may still fail if the file system was updated and the + // new directory tree has not yet been computed) + 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, 1) + timestamp = time.Now() + } + info.Dirs = dir.listing(true) + info.DirTime = timestamp + info.DirFlat = mode&flatDir != 0 + + return info +} + +func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if redirect(w, r) { + return + } + + relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) + abspath := pathpkg.Join(h.fsRoot, relpath) + mode := getPageInfoMode(r) + if relpath == builtinPkgPath { + mode = noFiltering + } + info := h.getPageInfo(abspath, relpath, mode) + if info.Err != nil { + log.Print(info.Err) + serveError(w, r, relpath, info.Err) + return + } + + if mode&noHtml != 0 { + serveText(w, applyTemplate(packageText, "packageText", info)) + return + } + + var tabtitle, title, subtitle string + switch { + case info.PAst != nil: + tabtitle = info.PAst.Name.Name + case info.PDoc != nil: + tabtitle = info.PDoc.Name + default: + tabtitle = info.Dirname + title = "Directory " + if *showTimestamps { + subtitle = "Last update: " + info.DirTime.String() + } + } + if title == "" { + if info.IsMain { + // assume that the directory name is the command name + _, tabtitle = pathpkg.Split(relpath) + title = "Command " + } else { + title = "Package " + } + } + title += tabtitle + + // special cases for top-level package/command directories + switch tabtitle { + case "/src/pkg": + tabtitle = "Packages" + case "/src/cmd": + tabtitle = "Commands" + } + + servePage(w, Page{ + Title: title, + Tabtitle: tabtitle, + Subtitle: subtitle, + Body: applyTemplate(packageHTML, "packageHTML", info), + }) +} + +// ---------------------------------------------------------------------------- +// Search + +var searchIndex RWValue + +type SearchResult struct { + Query string + Alert string // error or warning message + + // identifier matches + Pak HitList // packages matching Query + Hit *LookupResult // identifier matches of Query + Alt *AltWords // alternative identifiers to look for + + // textual matches + Found int // number of textual occurrences found + Textual []FileLines // textual matches of Query + Complete bool // true if all textual occurrences of Query are reported +} + +func lookup(query string) (result SearchResult) { + result.Query = query + + index, timestamp := searchIndex.get() + if index != nil { + index := index.(*Index) + + // identifier search + var err error + result.Pak, result.Hit, result.Alt, err = index.Lookup(query) + if err != nil && *maxResults <= 0 { + // ignore the error if full text search is enabled + // since the query may be a valid regular expression + result.Alert = "Error in query string: " + err.Error() + return + } + + // full text search + if *maxResults > 0 && query != "" { + rx, err := regexp.Compile(query) + if err != nil { + result.Alert = "Error in query regular expression: " + err.Error() + return + } + // If we get maxResults+1 results we know that there are more than + // maxResults results and thus the result may be incomplete (to be + // precise, we should remove one result from the result set, but + // nobody is going to count the results on the result page). + result.Found, result.Textual = index.LookupRegexp(rx, *maxResults+1) + result.Complete = result.Found <= *maxResults + if !result.Complete { + result.Found-- // since we looked for maxResults+1 + } + } + } + + // is the result accurate? + if *indexEnabled { + if _, ts := fsModified.get(); timestamp.Before(ts) { + // The index is older than the latest file system change under godoc's observation. + result.Alert = "Indexing in progress: result may be inaccurate" + } + } else { + result.Alert = "Search index disabled: no results available" + } + + return +} + +func search(w http.ResponseWriter, r *http.Request) { + query := strings.TrimSpace(r.FormValue("q")) + result := lookup(query) + + if getPageInfoMode(r)&noHtml != 0 { + serveText(w, applyTemplate(searchText, "searchText", result)) + return + } + + var title string + if result.Hit != nil || len(result.Textual) > 0 { + title = fmt.Sprintf(`Results for query %q`, query) + } else { + title = fmt.Sprintf(`No results found for query %q`, query) + } + + servePage(w, Page{ + Title: title, + Tabtitle: query, + Query: query, + Body: applyTemplate(searchHTML, "searchHTML", result), + }) +} + +// ---------------------------------------------------------------------------- +// Documentation Metadata + +type Metadata struct { + Title string + Subtitle string + Template bool // execute as template + Path string // canonical path for this page + filePath string // filesystem path relative to goroot +} + +// extractMetadata extracts the Metadata from a byte slice. +// It returns the Metadata value and the remaining data. +// If no metadata is present the original byte slice is returned. +// +func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) { + tail = b + if !bytes.HasPrefix(b, jsonStart) { + return + } + end := bytes.Index(b, jsonEnd) + if end < 0 { + return + } + b = b[len(jsonStart)-1 : end+1] // drop leading %s", command, buf.Bytes()) + return text, nil +} + +// parseArg returns the integer or string value of the argument and tells which it is. +func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) { + switch n := arg.(type) { + case int: + if n <= 0 || n > max { + log.Panicf("%q:%d is out of range", file, n) + } + return n, "", true + case string: + return 0, n, false + } + log.Panicf("unrecognized argument %v type %T", arg, arg) + return +} + +// oneLine returns the single line generated by a two-argument code invocation. +func oneLine(file, text string, arg interface{}) string { + lines := strings.SplitAfter(contents(file), "\n") + line, pattern, isInt := parseArg(arg, file, len(lines)) + if isInt { + return lines[line-1] + } + return lines[match(file, 0, lines, pattern)-1] +} + +// multipleLines returns the text generated by a three-argument code invocation. +func multipleLines(file, text string, arg1, arg2 interface{}) string { + lines := strings.SplitAfter(contents(file), "\n") + line1, pattern1, isInt1 := parseArg(arg1, file, len(lines)) + line2, pattern2, isInt2 := parseArg(arg2, file, len(lines)) + if !isInt1 { + line1 = match(file, 0, lines, pattern1) + } + if !isInt2 { + line2 = match(file, line1, lines, pattern2) + } else if line2 < line1 { + log.Panicf("lines out of order for %q: %d %d", text, line1, line2) + } + for k := line1 - 1; k < line2; k++ { + if strings.HasSuffix(lines[k], "OMIT\n") { + lines[k] = "" + } + } + return strings.Join(lines[line1-1:line2], "") +} + +// match identifies the input line that matches the pattern in a code invocation. +// If start>0, match lines starting there rather than at the beginning. +// The return value is 1-indexed. +func match(file string, start int, lines []string, pattern string) int { + // $ matches the end of the file. + if pattern == "$" { + if len(lines) == 0 { + log.Panicf("%q: empty file", file) + } + return len(lines) + } + // /regexp/ matches the line that matches the regexp. + if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { + re, err := regexp.Compile(pattern[1 : len(pattern)-1]) + if err != nil { + log.Panic(err) + } + for i := start; i < len(lines); i++ { + if re.MatchString(lines[i]) { + return i + 1 + } + } + log.Panicf("%s: no match for %#q", file, pattern) + } + log.Panicf("unrecognized pattern: %q", pattern) + return 0 +} diff --git a/cmd/godoc/throttle.go b/cmd/godoc/throttle.go new file mode 100644 index 0000000000..ac18b44e0e --- /dev/null +++ b/cmd/godoc/throttle.go @@ -0,0 +1,88 @@ +// 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. + +package main + +import "time" + +// A Throttle permits throttling of a goroutine by +// calling the Throttle method repeatedly. +// +type Throttle struct { + f float64 // f = (1-r)/r for 0 < r < 1 + dt time.Duration // minimum run time slice; >= 0 + tr time.Duration // accumulated time running + ts time.Duration // accumulated time stopped + tt time.Time // earliest throttle time (= time Throttle returned + tm) +} + +// NewThrottle creates a new Throttle with a throttle value r and +// a minimum allocated run time slice of dt: +// +// r == 0: "empty" throttle; the goroutine is always sleeping +// r == 1: full throttle; the goroutine is never sleeping +// +// A value of r == 0.6 throttles a goroutine such that it runs +// approx. 60% of the time, and sleeps approx. 40% of the time. +// Values of r < 0 or r > 1 are clamped down to values between 0 and 1. +// Values of dt < 0 are set to 0. +// +func NewThrottle(r float64, dt time.Duration) *Throttle { + var f float64 + switch { + case r <= 0: + f = -1 // indicates always sleep + case r >= 1: + f = 0 // assume r == 1 (never sleep) + default: + // 0 < r < 1 + f = (1 - r) / r + } + if dt < 0 { + dt = 0 + } + return &Throttle{f: f, dt: dt, tt: time.Now().Add(dt)} +} + +// Throttle calls time.Sleep such that over time the ratio tr/ts between +// accumulated run (tr) and sleep times (ts) approximates the value 1/(1-r) +// where r is the throttle value. Throttle returns immediately (w/o sleeping) +// if less than tm ns have passed since the last call to Throttle. +// +func (p *Throttle) Throttle() { + if p.f < 0 { + select {} // always sleep + } + + t0 := time.Now() + if t0.Before(p.tt) { + return // keep running (minimum time slice not exhausted yet) + } + + // accumulate running time + p.tr += t0.Sub(p.tt) + p.dt + + // compute sleep time + // Over time we want: + // + // tr/ts = r/(1-r) + // + // Thus: + // + // ts = tr*f with f = (1-r)/r + // + // After some incremental run time δr added to the total run time + // tr, the incremental sleep-time δs to get to the same ratio again + // after waking up from time.Sleep is: + if δs := time.Duration(float64(p.tr)*p.f) - p.ts; δs > 0 { + time.Sleep(δs) + } + + // accumulate (actual) sleep time + t1 := time.Now() + p.ts += t1.Sub(t0) + + // set earliest next throttle time + p.tt = t1.Add(p.dt) +} diff --git a/cmd/godoc/utils.go b/cmd/godoc/utils.go new file mode 100644 index 0000000000..0cdb7ff7af --- /dev/null +++ b/cmd/godoc/utils.go @@ -0,0 +1,91 @@ +// Copyright 2010 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 contains support functionality for godoc. + +package main + +import ( + pathpkg "path" + "sync" + "time" + "unicode/utf8" +) + +// An RWValue wraps a value and permits mutually exclusive +// access to it and records the time the value was last set. +// +type RWValue struct { + mutex sync.RWMutex + value interface{} + timestamp time.Time // time of last set() +} + +func (v *RWValue) set(value interface{}) { + v.mutex.Lock() + v.value = value + v.timestamp = time.Now() + v.mutex.Unlock() +} + +func (v *RWValue) get() (interface{}, time.Time) { + v.mutex.RLock() + defer v.mutex.RUnlock() + return v.value, v.timestamp +} + +// 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. +// +func isText(s []byte) bool { + const max = 1024 // at least utf8.UTFMax + if len(s) > max { + s = s[0:max] + } + for i, c := range string(s) { + if i+utf8.UTFMax > len(s) { + // last char may be incomplete - ignore + break + } + if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' { + // decoding error or control character - not a text file + return false + } + } + return true +} + +// textExt[x] is true if the extension x indicates a text file, and false otherwise. +var textExt = map[string]bool{ + ".css": false, // must be served raw + ".js": false, // must be served raw +} + +// isTextFile returns true if the file has a known extension indicating +// a text file, or if a significant chunk of the specified file looks like +// correct UTF-8; that is, if it is likely that the file contains human- +// readable text. +// +func isTextFile(filename string) bool { + // if the extension is known, use it for decision making + if isText, found := textExt[pathpkg.Ext(filename)]; found { + return isText + } + + // the extension is not known; read an initial chunk + // of the file and check if it looks like text + f, err := fs.Open(filename) + if err != nil { + return false + } + defer f.Close() + + var buf [1024]byte + n, err := f.Read(buf[0:]) + if err != nil { + return false + } + + return isText(buf[0:n]) +} diff --git a/cmd/godoc/zip.go b/cmd/godoc/zip.go new file mode 100644 index 0000000000..620eb4f3cc --- /dev/null +++ b/cmd/godoc/zip.go @@ -0,0 +1,236 @@ +// 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 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 must be absolute paths. + +package main + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path" + "sort" + "strings" + "time" +) + +// zipFI is the zip-file based implementation of FileInfo +type zipFI struct { + name string // directory-local name + file *zip.File // nil for a directory +} + +func (fi zipFI) Name() string { + return fi.name +} + +func (fi zipFI) Size() int64 { + if f := fi.file; f != nil { + return int64(f.UncompressedSize) + } + return 0 // directory +} + +func (fi zipFI) ModTime() time.Time { + if f := fi.file; f != nil { + return f.ModTime() + } + return time.Time{} // directory has no modified time entry +} + +func (fi zipFI) Mode() os.FileMode { + if fi.file == nil { + // Unix directories typically are executable, hence 555. + return os.ModeDir | 0555 + } + return 0444 +} + +func (fi zipFI) IsDir() bool { + return fi.file == nil +} + +func (fi zipFI) Sys() interface{} { + return nil +} + +// zipFS is the zip-file based implementation of FileSystem +type zipFS struct { + *zip.ReadCloser + list zipList + name string +} + +func (fs *zipFS) String() string { + return "zip(" + fs.name + ")" +} + +func (fs *zipFS) Close() error { + fs.list = nil + return fs.ReadCloser.Close() +} + +func zipPath(name string) string { + name = path.Clean(name) + if !path.IsAbs(name) { + panic(fmt.Sprintf("stat: not an absolute path: %s", name)) + } + return name[1:] // strip leading '/' +} + +func (fs *zipFS) stat(abspath string) (int, zipFI, error) { + i, exact := fs.list.lookup(abspath) + if i < 0 { + // abspath has leading '/' stripped - print it explicitly + return -1, zipFI{}, fmt.Errorf("file not found: /%s", abspath) + } + _, name := path.Split(abspath) + var file *zip.File + if exact { + file = fs.list[i] // exact match found - must be a file + } + return i, zipFI{name, file}, nil +} + +func (fs *zipFS) Open(abspath string) (readSeekCloser, error) { + _, fi, err := fs.stat(zipPath(abspath)) + if err != nil { + return nil, err + } + if fi.IsDir() { + return nil, fmt.Errorf("Open: %s is a directory", abspath) + } + 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) { + _, fi, err := fs.stat(zipPath(abspath)) + return fi, err +} + +func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) { + _, fi, err := fs.stat(zipPath(abspath)) + return fi, err +} + +func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) { + path := zipPath(abspath) + i, fi, err := fs.stat(path) + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath) + } + + var list []os.FileInfo + dirname := path + "/" + prevname := "" + for _, e := range fs.list[i:] { + if !strings.HasPrefix(e.Name, dirname) { + break // not in the same directory anymore + } + name := e.Name[len(dirname):] // local name + file := e + 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 + file = nil + } + // 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, zipFI{name, file}) + prevname = name + } + } + + return list, nil +} + +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, name} +} + +type zipList []*zip.File + +// zipList implements sort.Interface +func (z zipList) Len() int { return len(z) } +func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name } +func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] } + +// lookup returns the smallest index of an entry with an exact match +// for name, or an inexact match starting with name/. If there is no +// such entry, the result is -1, false. +func (z zipList) lookup(name string) (index int, exact bool) { + // look for exact match first (name comes before name/ in z) + i := sort.Search(len(z), func(i int) bool { + return name <= z[i].Name + }) + if i >= len(z) { + return -1, false + } + // 0 <= i < len(z) + if z[i].Name == name { + return i, true + } + + // look for inexact match (must be in z[i:], if present) + z = z[i:] + name += "/" + j := sort.Search(len(z), func(i int) bool { + return name <= z[i].Name + }) + if j >= len(z) { + return -1, false + } + // 0 <= j < len(z) + if strings.HasPrefix(z[j].Name, name) { + return i + j, false + } + + return -1, false +}