github.com/v2fly/tools@v0.100.0/internal/span/uri.go (about) 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 5 package span 6 7 import ( 8 "fmt" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "unicode" 16 ) 17 18 const fileScheme = "file" 19 20 // URI represents the full URI for a file. 21 type URI string 22 23 func (uri URI) IsFile() bool { 24 return strings.HasPrefix(string(uri), "file://") 25 } 26 27 // Filename returns the file path for the given URI. 28 // It is an error to call this on a URI that is not a valid filename. 29 func (uri URI) Filename() string { 30 filename, err := filename(uri) 31 if err != nil { 32 panic(err) 33 } 34 return filepath.FromSlash(filename) 35 } 36 37 func filename(uri URI) (string, error) { 38 if uri == "" { 39 return "", nil 40 } 41 u, err := url.ParseRequestURI(string(uri)) 42 if err != nil { 43 return "", err 44 } 45 if u.Scheme != fileScheme { 46 return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) 47 } 48 // If the URI is a Windows URI, we trim the leading "/" and lowercase 49 // the drive letter, which will never be case sensitive. 50 if isWindowsDriveURIPath(u.Path) { 51 u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] 52 } 53 return u.Path, nil 54 } 55 56 func URIFromURI(s string) URI { 57 if !strings.HasPrefix(s, "file://") { 58 return URI(s) 59 } 60 61 if !strings.HasPrefix(s, "file:///") { 62 // VS Code sends URLs with only two slashes, which are invalid. golang/go#39789. 63 s = "file:///" + s[len("file://"):] 64 } 65 // Even though the input is a URI, it may not be in canonical form. VS Code 66 // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. 67 path, err := url.PathUnescape(s[len("file://"):]) 68 if err != nil { 69 panic(err) 70 } 71 72 // File URIs from Windows may have lowercase drive letters. 73 // Since drive letters are guaranteed to be case insensitive, 74 // we change them to uppercase to remain consistent. 75 // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. 76 if isWindowsDriveURIPath(path) { 77 path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] 78 } 79 u := url.URL{Scheme: fileScheme, Path: path} 80 return URI(u.String()) 81 } 82 83 func CompareURI(a, b URI) int { 84 if equalURI(a, b) { 85 return 0 86 } 87 if a < b { 88 return -1 89 } 90 return 1 91 } 92 93 func equalURI(a, b URI) bool { 94 if a == b { 95 return true 96 } 97 // If we have the same URI basename, we may still have the same file URIs. 98 if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) { 99 return false 100 } 101 fa, err := filename(a) 102 if err != nil { 103 return false 104 } 105 fb, err := filename(b) 106 if err != nil { 107 return false 108 } 109 // Stat the files to check if they are equal. 110 infoa, err := os.Stat(filepath.FromSlash(fa)) 111 if err != nil { 112 return false 113 } 114 infob, err := os.Stat(filepath.FromSlash(fb)) 115 if err != nil { 116 return false 117 } 118 return os.SameFile(infoa, infob) 119 } 120 121 // URIFromPath returns a span URI for the supplied file path. 122 // It will always have the file scheme. 123 func URIFromPath(path string) URI { 124 if path == "" { 125 return "" 126 } 127 // Handle standard library paths that contain the literal "$GOROOT". 128 // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT. 129 const prefix = "$GOROOT" 130 if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) { 131 suffix := path[len(prefix):] 132 path = runtime.GOROOT() + suffix 133 } 134 if !isWindowsDrivePath(path) { 135 if abs, err := filepath.Abs(path); err == nil { 136 path = abs 137 } 138 } 139 // Check the file path again, in case it became absolute. 140 if isWindowsDrivePath(path) { 141 path = "/" + strings.ToUpper(string(path[0])) + path[1:] 142 } 143 path = filepath.ToSlash(path) 144 u := url.URL{ 145 Scheme: fileScheme, 146 Path: path, 147 } 148 return URI(u.String()) 149 } 150 151 // isWindowsDrivePath returns true if the file path is of the form used by 152 // Windows. We check if the path begins with a drive letter, followed by a ":". 153 // For example: C:/x/y/z. 154 func isWindowsDrivePath(path string) bool { 155 if len(path) < 3 { 156 return false 157 } 158 return unicode.IsLetter(rune(path[0])) && path[1] == ':' 159 } 160 161 // isWindowsDriveURI returns true if the file URI is of the format used by 162 // Windows URIs. The url.Parse package does not specially handle Windows paths 163 // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). 164 func isWindowsDriveURIPath(uri string) bool { 165 if len(uri) < 4 { 166 return false 167 } 168 return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' 169 }