golang.org/x/tools/gopls@v0.15.3/internal/protocol/uri.go (about) 1 // Copyright 2023 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 protocol 6 7 // This file declares URI, DocumentURI, and its methods. 8 // 9 // For the LSP definition of these types, see 10 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri 11 12 import ( 13 "fmt" 14 "net/url" 15 "path/filepath" 16 "strings" 17 "unicode" 18 19 "golang.org/x/tools/gopls/internal/util/pathutil" 20 ) 21 22 // A DocumentURI is the URI of a client editor document. 23 // 24 // According to the LSP specification: 25 // 26 // Care should be taken to handle encoding in URIs. For 27 // example, some clients (such as VS Code) may encode colons 28 // in drive letters while others do not. The URIs below are 29 // both valid, but clients and servers should be consistent 30 // with the form they use themselves to ensure the other party 31 // doesn’t interpret them as distinct URIs. Clients and 32 // servers should not assume that each other are encoding the 33 // same way (for example a client encoding colons in drive 34 // letters cannot assume server responses will have encoded 35 // colons). The same applies to casing of drive letters - one 36 // party should not assume the other party will return paths 37 // with drive letters cased the same as it. 38 // 39 // file:///c:/project/readme.md 40 // file:///C%3A/project/readme.md 41 // 42 // This is done during JSON unmarshalling; 43 // see [DocumentURI.UnmarshalText] for details. 44 type DocumentURI string 45 46 // A URI is an arbitrary URL (e.g. https), not necessarily a file. 47 type URI = string 48 49 // UnmarshalText implements decoding of DocumentURI values. 50 // 51 // In particular, it implements a systematic correction of various odd 52 // features of the definition of DocumentURI in the LSP spec that 53 // appear to be workarounds for bugs in VS Code. For example, it may 54 // URI-encode the URI itself, so that colon becomes %3A, and it may 55 // send file://foo.go URIs that have two slashes (not three) and no 56 // hostname. 57 // 58 // We use UnmarshalText, not UnmarshalJSON, because it is called even 59 // for non-addressable values such as keys and values of map[K]V, 60 // where there is no pointer of type *K or *V on which to call 61 // UnmarshalJSON. (See Go issue #28189 for more detail.) 62 // 63 // Non-empty DocumentURIs are valid "file"-scheme URIs. 64 // The empty DocumentURI is valid. 65 func (uri *DocumentURI) UnmarshalText(data []byte) (err error) { 66 *uri, err = ParseDocumentURI(string(data)) 67 return 68 } 69 70 // Path returns the file path for the given URI. 71 // 72 // DocumentURI("").Path() returns the empty string. 73 // 74 // Path panics if called on a URI that is not a valid filename. 75 func (uri DocumentURI) Path() string { 76 filename, err := filename(uri) 77 if err != nil { 78 // e.g. ParseRequestURI failed. 79 // 80 // This can only affect DocumentURIs created by 81 // direct string manipulation; all DocumentURIs 82 // received from the client pass through 83 // ParseRequestURI, which ensures validity. 84 panic(err) 85 } 86 return filepath.FromSlash(filename) 87 } 88 89 // Dir returns the URI for the directory containing the receiver. 90 func (uri DocumentURI) Dir() DocumentURI { 91 // This function could be more efficiently implemented by avoiding any call 92 // to Path(), but at least consolidates URI manipulation. 93 return URIFromPath(filepath.Dir(uri.Path())) 94 } 95 96 // Encloses reports whether uri's path, considered as a sequence of segments, 97 // is a prefix of file's path. 98 func (uri DocumentURI) Encloses(file DocumentURI) bool { 99 return pathutil.InDir(uri.Path(), file.Path()) 100 } 101 102 func filename(uri DocumentURI) (string, error) { 103 if uri == "" { 104 return "", nil 105 } 106 107 // This conservative check for the common case 108 // of a simple non-empty absolute POSIX filename 109 // avoids the allocation of a net.URL. 110 if strings.HasPrefix(string(uri), "file:///") { 111 rest := string(uri)[len("file://"):] // leave one slash 112 for i := 0; i < len(rest); i++ { 113 b := rest[i] 114 // Reject these cases: 115 if b < ' ' || b == 0x7f || // control character 116 b == '%' || b == '+' || // URI escape 117 b == ':' || // Windows drive letter 118 b == '@' || b == '&' || b == '?' { // authority or query 119 goto slow 120 } 121 } 122 return rest, nil 123 } 124 slow: 125 126 u, err := url.ParseRequestURI(string(uri)) 127 if err != nil { 128 return "", err 129 } 130 if u.Scheme != fileScheme { 131 return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) 132 } 133 // If the URI is a Windows URI, we trim the leading "/" and uppercase 134 // the drive letter, which will never be case sensitive. 135 if isWindowsDriveURIPath(u.Path) { 136 u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] 137 } 138 139 return u.Path, nil 140 } 141 142 // ParseDocumentURI interprets a string as a DocumentURI, applying VS 143 // Code workarounds; see [DocumentURI.UnmarshalText] for details. 144 func ParseDocumentURI(s string) (DocumentURI, error) { 145 if s == "" { 146 return "", nil 147 } 148 149 if !strings.HasPrefix(s, "file://") { 150 return "", fmt.Errorf("DocumentURI scheme is not 'file': %s", s) 151 } 152 153 // VS Code sends URLs with only two slashes, 154 // which are invalid. golang/go#39789. 155 if !strings.HasPrefix(s, "file:///") { 156 s = "file:///" + s[len("file://"):] 157 } 158 159 // Even though the input is a URI, it may not be in canonical form. VS Code 160 // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. 161 path, err := url.PathUnescape(s[len("file://"):]) 162 if err != nil { 163 return "", err 164 } 165 166 // File URIs from Windows may have lowercase drive letters. 167 // Since drive letters are guaranteed to be case insensitive, 168 // we change them to uppercase to remain consistent. 169 // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. 170 if isWindowsDriveURIPath(path) { 171 path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] 172 } 173 u := url.URL{Scheme: fileScheme, Path: path} 174 return DocumentURI(u.String()), nil 175 } 176 177 // URIFromPath returns DocumentURI for the supplied file path. 178 // Given "", it returns "". 179 func URIFromPath(path string) DocumentURI { 180 if path == "" { 181 return "" 182 } 183 if !isWindowsDrivePath(path) { 184 if abs, err := filepath.Abs(path); err == nil { 185 path = abs 186 } 187 } 188 // Check the file path again, in case it became absolute. 189 if isWindowsDrivePath(path) { 190 path = "/" + strings.ToUpper(string(path[0])) + path[1:] 191 } 192 path = filepath.ToSlash(path) 193 u := url.URL{ 194 Scheme: fileScheme, 195 Path: path, 196 } 197 return DocumentURI(u.String()) 198 } 199 200 const fileScheme = "file" 201 202 // isWindowsDrivePath returns true if the file path is of the form used by 203 // Windows. We check if the path begins with a drive letter, followed by a ":". 204 // For example: C:/x/y/z. 205 func isWindowsDrivePath(path string) bool { 206 if len(path) < 3 { 207 return false 208 } 209 return unicode.IsLetter(rune(path[0])) && path[1] == ':' 210 } 211 212 // isWindowsDriveURIPath returns true if the file URI is of the format used by 213 // Windows URIs. The url.Parse package does not specially handle Windows paths 214 // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). 215 func isWindowsDriveURIPath(uri string) bool { 216 if len(uri) < 4 { 217 return false 218 } 219 return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' 220 }