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  }