blob: aa13f46c900e772b6bd4f94c6e88d0f78c4b1bed [file] [log] [blame]
// Copyright 2019 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 source
import (
"context"
"encoding/xml"
"fmt"
"io"
"strings"
"golang.org/x/pkgsite/internal/derrors"
)
// This code adapted from https://go.googlesource.com/gddo/+/refs/heads/master/gosrc/gosrc.go.
// sourceMeta represents the values in a go-source meta tag, or as a fallback,
// values from a go-import meta tag.
// The go-source spec is at https://github.com/golang/gddo/wiki/Source-Code-Links.
// The go-import spec is in "go help importpath".
type sourceMeta struct {
repoRootPrefix string // import path prefix corresponding to repo root
repoURL string // URL of the repo root
// The next two are only present in a go-source tag.
dirTemplate string // URL template for a directory
fileTemplate string // URL template for a file and line
}
// fetchMeta retrieves go-import and go-source meta tag information, using the import path to construct
// a URL as described in "go help importpath".
//
// The importPath argument, as the name suggests, could be any package import
// path. But we only pass module paths.
//
// The discovery site only cares about linking to source, not fetching it (we
// already have it in the module zip file). So we merge the go-import and
// go-source meta tag information, preferring the latter.
func fetchMeta(ctx context.Context, client *Client, importPath string) (_ *sourceMeta, err error) {
defer derrors.Wrap(&err, "fetchMeta(ctx, client, %q)", importPath)
uri := importPath
if !strings.Contains(uri, "/") {
// Add slash for root of domain.
uri = uri + "/"
}
uri = uri + "?go-get=1"
resp, err := client.doURL(ctx, "GET", "https://"+uri, true)
if err != nil {
resp, err = client.doURL(ctx, "GET", "http://"+uri, false)
if err != nil {
return nil, err
}
}
defer resp.Body.Close()
return parseMeta(importPath, resp.Body)
}
func parseMeta(importPath string, r io.Reader) (sm *sourceMeta, err error) {
errorMessage := "go-import and go-source meta tags not found"
// gddo uses an xml parser, and this code is adapted from it.
d := xml.NewDecoder(r)
d.Strict = false
metaScan:
for {
t, tokenErr := d.Token()
if tokenErr != nil {
break metaScan
}
switch t := t.(type) {
case xml.EndElement:
if strings.EqualFold(t.Name.Local, "head") {
break metaScan
}
case xml.StartElement:
if strings.EqualFold(t.Name.Local, "body") {
break metaScan
}
if !strings.EqualFold(t.Name.Local, "meta") {
continue metaScan
}
nameAttr := attrValue(t.Attr, "name")
if nameAttr != "go-import" && nameAttr != "go-source" {
continue metaScan
}
fields := strings.Fields(attrValue(t.Attr, "content"))
if len(fields) < 1 {
continue metaScan
}
repoRootPrefix := fields[0]
if !strings.HasPrefix(importPath, repoRootPrefix) ||
!(len(importPath) == len(repoRootPrefix) || importPath[len(repoRootPrefix)] == '/') {
// Ignore if root is not a prefix of the path. This allows a
// site to use a single error page for multiple repositories.
continue metaScan
}
switch nameAttr {
case "go-import":
if len(fields) != 3 {
errorMessage = "go-import meta tag content attribute does not have three fields"
continue metaScan
}
if fields[1] == "mod" {
// We can't make source links from a "mod" vcs type, so skip it.
continue
}
if sm != nil {
sm = nil
errorMessage = "more than one go-import meta tag found"
break metaScan
}
sm = &sourceMeta{
repoRootPrefix: repoRootPrefix,
repoURL: fields[2],
}
// Keep going in the hope of finding a go-source tag.
case "go-source":
if len(fields) != 4 {
errorMessage = "go-source meta tag content attribute does not have four fields"
continue metaScan
}
if sm != nil && sm.repoRootPrefix != repoRootPrefix {
errorMessage = fmt.Sprintf("import path prefixes %q for go-import and %q for go-source disagree", sm.repoRootPrefix, repoRootPrefix)
sm = nil
break metaScan
}
// If go-source repo is "_", then default to the go-import repo.
repoURL := fields[1]
if repoURL == "_" {
if sm == nil {
errorMessage = `go-source repo is "_", but no previous go-import tag`
break metaScan
}
repoURL = sm.repoURL
}
sm = &sourceMeta{
repoRootPrefix: repoRootPrefix,
repoURL: repoURL,
dirTemplate: fields[2],
fileTemplate: fields[3],
}
break metaScan
}
}
}
if sm == nil {
return nil, fmt.Errorf("%s: %w", errorMessage, derrors.NotFound)
}
return sm, nil
}
func attrValue(attrs []xml.Attr, name string) string {
for _, a := range attrs {
if strings.EqualFold(a.Name.Local, name) {
return a.Value
}
}
return ""
}