1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package span
6
7import (
8	"fmt"
9	"net/url"
10	"os"
11	"path"
12	"path/filepath"
13	"runtime"
14	"strings"
15	"unicode"
16)
17
18const fileScheme = "file"
19
20// URI represents the full URI for a file.
21type URI string
22
23// Filename returns the file path for the given URI.
24// It is an error to call this on a URI that is not a valid filename.
25func (uri URI) Filename() string {
26	filename, err := filename(uri)
27	if err != nil {
28		panic(err)
29	}
30	return filepath.FromSlash(filename)
31}
32
33func filename(uri URI) (string, error) {
34	if uri == "" {
35		return "", nil
36	}
37	u, err := url.ParseRequestURI(string(uri))
38	if err != nil {
39		return "", err
40	}
41	if u.Scheme != fileScheme {
42		return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
43	}
44	if isWindowsDriveURI(u.Path) {
45		u.Path = u.Path[1:]
46	}
47	return u.Path, nil
48}
49
50// NewURI returns a span URI for the string.
51// It will attempt to detect if the string is a file path or uri.
52func NewURI(s string) URI {
53	if u, err := url.PathUnescape(s); err == nil {
54		s = u
55	}
56	if strings.HasPrefix(s, fileScheme+"://") {
57		return URI(s)
58	}
59	return FileURI(s)
60}
61
62func CompareURI(a, b URI) int {
63	if equalURI(a, b) {
64		return 0
65	}
66	if a < b {
67		return -1
68	}
69	return 1
70}
71
72func equalURI(a, b URI) bool {
73	if a == b {
74		return true
75	}
76	// If we have the same URI basename, we may still have the same file URIs.
77	if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) {
78		return false
79	}
80	fa, err := filename(a)
81	if err != nil {
82		return false
83	}
84	fb, err := filename(b)
85	if err != nil {
86		return false
87	}
88	// Stat the files to check if they are equal.
89	infoa, err := os.Stat(filepath.FromSlash(fa))
90	if err != nil {
91		return false
92	}
93	infob, err := os.Stat(filepath.FromSlash(fb))
94	if err != nil {
95		return false
96	}
97	return os.SameFile(infoa, infob)
98}
99
100// FileURI returns a span URI for the supplied file path.
101// It will always have the file scheme.
102func FileURI(path string) URI {
103	if path == "" {
104		return ""
105	}
106	// Handle standard library paths that contain the literal "$GOROOT".
107	// TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
108	const prefix = "$GOROOT"
109	if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) {
110		suffix := path[len(prefix):]
111		path = runtime.GOROOT() + suffix
112	}
113	if !isWindowsDrivePath(path) {
114		if abs, err := filepath.Abs(path); err == nil {
115			path = abs
116		}
117	}
118	// Check the file path again, in case it became absolute.
119	if isWindowsDrivePath(path) {
120		path = "/" + path
121	}
122	path = filepath.ToSlash(path)
123	u := url.URL{
124		Scheme: fileScheme,
125		Path:   path,
126	}
127	uri := u.String()
128	if unescaped, err := url.PathUnescape(uri); err == nil {
129		uri = unescaped
130	}
131	return URI(uri)
132}
133
134// isWindowsDrivePath returns true if the file path is of the form used by
135// Windows. We check if the path begins with a drive letter, followed by a ":".
136func isWindowsDrivePath(path string) bool {
137	if len(path) < 4 {
138		return false
139	}
140	return unicode.IsLetter(rune(path[0])) && path[1] == ':'
141}
142
143// isWindowsDriveURI returns true if the file URI is of the format used by
144// Windows URIs. The url.Parse package does not specially handle Windows paths
145// (see https://golang.org/issue/6027). We check if the URI path has
146// a drive prefix (e.g. "/C:"). If so, we trim the leading "/".
147func isWindowsDriveURI(uri string) bool {
148	if len(uri) < 4 {
149		return false
150	}
151	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
152}
153