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