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