1package util
2
3import (
4	"encoding/xml"
5	"fmt"
6	"go/build"
7	"io"
8	"net/http"
9	"net/url"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"regexp"
14	"strings"
15
16	"github.com/Masterminds/vcs"
17)
18
19// ResolveCurrent selects whether the package should only the dependencies for
20// the current OS/ARCH instead of all possible permutations.
21// This is not concurrently safe which is ok for the current application. If
22// other needs arise it may need to be re-written.
23var ResolveCurrent = false
24
25// goRoot caches the GOROOT variable for build contexts. If $GOROOT is not set in
26// the user's environment, then the context's root path is 'go env GOROOT'.
27var goRoot string
28
29func init() {
30	// Precompile the regular expressions used to check VCS locations.
31	for _, v := range vcsList {
32		v.regex = regexp.MustCompile(v.pattern)
33	}
34	if goRoot = os.Getenv("GOROOT"); len(goRoot) == 0 {
35		goExecutable := os.Getenv("GLIDE_GO_EXECUTABLE")
36		if len(goExecutable) <= 0 {
37			goExecutable = "go"
38		}
39		out, err := exec.Command(goExecutable, "env", "GOROOT").Output()
40		if err == nil {
41			goRoot = strings.TrimSpace(string(out))
42		}
43	}
44}
45
46func toSlash(v string) string {
47	return strings.Replace(v, "\\", "/", -1)
48}
49
50// GetRootFromPackage retrives the top level package from a name.
51//
52// From a package name find the root repo. For example,
53// the package github.com/Masterminds/cookoo/io has a root repo
54// at github.com/Masterminds/cookoo
55func GetRootFromPackage(pkg string) string {
56	pkg = toSlash(pkg)
57	for _, v := range vcsList {
58		m := v.regex.FindStringSubmatch(pkg)
59		if m == nil {
60			continue
61		}
62
63		if m[1] != "" {
64			return m[1]
65		}
66	}
67
68	// There are cases where a package uses the special go get magic for
69	// redirects. If we've not discovered the location already try that.
70	pkg = getRootFromGoGet(pkg)
71
72	return pkg
73}
74
75// Pages like https://golang.org/x/net provide an html document with
76// meta tags containing a location to work with. The go tool uses
77// a meta tag with the name go-import which is what we use here.
78// godoc.org also has one call go-source that we do not need to use.
79// The value of go-import is in the form "prefix vcs repo". The prefix
80// should match the vcsURL and the repo is a location that can be
81// checked out. Note, to get the html document you you need to add
82// ?go-get=1 to the url.
83func getRootFromGoGet(pkg string) string {
84
85	p, found := checkRemotePackageCache(pkg)
86	if found {
87		return p
88	}
89
90	vcsURL := "https://" + pkg
91	u, err := url.Parse(vcsURL)
92	if err != nil {
93		return pkg
94	}
95	if u.RawQuery == "" {
96		u.RawQuery = "go-get=1"
97	} else {
98		u.RawQuery = u.RawQuery + "&go-get=1"
99	}
100	checkURL := u.String()
101	resp, err := http.Get(checkURL)
102	if err != nil {
103		addToRemotePackageCache(pkg, pkg)
104		return pkg
105	}
106	defer resp.Body.Close()
107
108	nu, err := parseImportFromBody(u, resp.Body)
109	if err != nil {
110		addToRemotePackageCache(pkg, pkg)
111		return pkg
112	} else if nu == "" {
113		addToRemotePackageCache(pkg, pkg)
114		return pkg
115	}
116
117	addToRemotePackageCache(pkg, nu)
118	return nu
119}
120
121// The caching is not concurrency safe but should be made to be that way.
122// This implementation is far too much of a hack... rewrite needed.
123var remotePackageCache = make(map[string]string)
124
125func checkRemotePackageCache(pkg string) (string, bool) {
126	for k, v := range remotePackageCache {
127		if pkg == k || strings.HasPrefix(pkg, k+"/") {
128			return v, true
129		}
130	}
131
132	return pkg, false
133}
134
135func addToRemotePackageCache(pkg, v string) {
136	remotePackageCache[pkg] = v
137}
138
139func parseImportFromBody(ur *url.URL, r io.ReadCloser) (u string, err error) {
140	d := xml.NewDecoder(r)
141	d.CharsetReader = charsetReader
142	d.Strict = false
143	var t xml.Token
144	for {
145		t, err = d.Token()
146		if err != nil {
147			if err == io.EOF {
148				// If we hit the end of the markup and don't have anything
149				// we return an error.
150				err = vcs.ErrCannotDetectVCS
151			}
152			return
153		}
154		if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
155			return
156		}
157		if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
158			return
159		}
160		e, ok := t.(xml.StartElement)
161		if !ok || !strings.EqualFold(e.Name.Local, "meta") {
162			continue
163		}
164		if attrValue(e.Attr, "name") != "go-import" {
165			continue
166		}
167		if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
168
169			// If the prefix supplied by the remote system isn't a prefix to the
170			// url we're fetching return continue looking for more go-imports.
171			// This will work for exact matches and prefixes. For example,
172			// golang.org/x/net as a prefix will match for golang.org/x/net and
173			// golang.org/x/net/context.
174			vcsURL := ur.Host + ur.Path
175			if !strings.HasPrefix(vcsURL, f[0]) {
176				continue
177			} else {
178				u = f[0]
179				return
180			}
181
182		}
183	}
184}
185
186func charsetReader(charset string, input io.Reader) (io.Reader, error) {
187	switch strings.ToLower(charset) {
188	case "ascii":
189		return input, nil
190	default:
191		return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
192	}
193}
194
195func attrValue(attrs []xml.Attr, name string) string {
196	for _, a := range attrs {
197		if strings.EqualFold(a.Name.Local, name) {
198			return a.Value
199		}
200	}
201	return ""
202}
203
204type vcsInfo struct {
205	host    string
206	pattern string
207	regex   *regexp.Regexp
208}
209
210var vcsList = []*vcsInfo{
211	{
212		host:    "github.com",
213		pattern: `^(?P<rootpkg>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
214	},
215	{
216		host:    "bitbucket.org",
217		pattern: `^(?P<rootpkg>bitbucket\.org/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
218	},
219	{
220		host:    "launchpad.net",
221		pattern: `^(?P<rootpkg>launchpad\.net/(([A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
222	},
223	{
224		host:    "git.launchpad.net",
225		pattern: `^(?P<rootpkg>git\.launchpad\.net/(([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))$`,
226	},
227	{
228		host:    "hub.jazz.net",
229		pattern: `^(?P<rootpkg>hub\.jazz\.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
230	},
231	{
232		host:    "go.googlesource.com",
233		pattern: `^(?P<rootpkg>go\.googlesource\.com/[A-Za-z0-9_.\-]+/?)$`,
234	},
235	// TODO: Once Google Code becomes fully deprecated this can be removed.
236	{
237		host:    "code.google.com",
238		pattern: `^(?P<rootpkg>code\.google\.com/[pr]/([a-z0-9\-]+)(\.([a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`,
239	},
240	// Alternative Google setup for SVN. This is the previous structure but it still works... until Google Code goes away.
241	{
242		pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/svn(/.*)?)$`,
243	},
244	// Alternative Google setup. This is the previous structure but it still works... until Google Code goes away.
245	{
246		pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/(git|hg))(/.*)?$`,
247	},
248	// If none of the previous detect the type they will fall to this looking for the type in a generic sense
249	// by the extension to the path.
250	{
251		pattern: `^(?P<rootpkg>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(bzr|git|hg|svn))(/[A-Za-z0-9_.\-]+)*$`,
252	},
253}
254
255// BuildCtxt is a convenience wrapper for not having to import go/build
256// anywhere else
257type BuildCtxt struct {
258	build.Context
259}
260
261// PackageName attempts to determine the name of the base package.
262//
263// If resolution fails, this will return "main".
264func (b *BuildCtxt) PackageName(base string) string {
265	cwd, err := os.Getwd()
266	if err != nil {
267		return "main"
268	}
269
270	pkg, err := b.Import(base, cwd, 0)
271	if err != nil {
272		// There may not be any top level Go source files but the project may
273		// still be within the GOPATH.
274		if strings.HasPrefix(base, b.GOPATH) {
275			p := strings.TrimPrefix(base, filepath.Join(b.GOPATH, "src"))
276			return strings.Trim(p, string(os.PathSeparator))
277		}
278	}
279
280	return pkg.ImportPath
281}
282
283// GetBuildContext returns a build context from go/build. When the $GOROOT
284// variable is not set in the users environment it sets the context's root
285// path to the path returned by 'go env GOROOT'.
286//
287// TODO: This should be moved to the `dependency` package.
288func GetBuildContext() (*BuildCtxt, error) {
289	if len(goRoot) == 0 {
290		return nil, fmt.Errorf("GOROOT value not found. Please set the GOROOT " +
291			"environment variable to use this command")
292	}
293
294	buildContext := &BuildCtxt{build.Default}
295
296	// If we aren't resolving for the current system set to look at all
297	// build modes.
298	if !ResolveCurrent {
299		// This tells the context scanning to skip filtering on +build flags or
300		// file names.
301		buildContext.UseAllFiles = true
302	}
303
304	buildContext.GOROOT = goRoot
305	return buildContext, nil
306}
307
308// NormalizeName takes a package name and normalizes it to the top level package.
309//
310// For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is
311// returned as extra data.
312//
313// FIXME: Is this deprecated?
314func NormalizeName(name string) (string, string) {
315	// Fastpath check if a name in the GOROOT. There is an issue when a pkg
316	// is in the GOROOT and GetRootFromPackage tries to look it up because it
317	// expects remote names.
318	b, err := GetBuildContext()
319	if err == nil {
320		p := filepath.Join(b.GOROOT, "src", name)
321		if _, err := os.Stat(p); err == nil {
322			return toSlash(name), ""
323		}
324	}
325
326	name = toSlash(name)
327	root := GetRootFromPackage(name)
328	extra := strings.TrimPrefix(name, root)
329	if len(extra) > 0 && extra != "/" {
330		extra = strings.TrimPrefix(extra, "/")
331	} else {
332		// If extra is / (which is what it would be here) we want to return ""
333		extra = ""
334	}
335
336	return root, extra
337}
338