cuelang.org/go@v0.13.0/internal/golangorgx/gopls/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  	"cuelang.org/go/internal/golangorgx/gopls/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  	// filepath.Dir returns "." in case passed "". So we need to special
    92  	// case a check for uri == "" in case the caller has not done that.
    93  	if uri == "" {
    94  		return ""
    95  	}
    96  	// This function could be more efficiently implemented by avoiding any call
    97  	// to Path(), but at least consolidates URI manipulation.
    98  	return URIFromPath(filepath.Dir(uri.Path()))
    99  }
   100  
   101  // Encloses reports whether uri's path, considered as a sequence of segments,
   102  // is a prefix of file's path.
   103  func (uri DocumentURI) Encloses(file DocumentURI) bool {
   104  	return pathutil.InDir(uri.Path(), file.Path())
   105  }
   106  
   107  func filename(uri DocumentURI) (string, error) {
   108  	if uri == "" {
   109  		return "", nil
   110  	}
   111  
   112  	// This conservative check for the common case
   113  	// of a simple non-empty absolute POSIX filename
   114  	// avoids the allocation of a net.URL.
   115  	if strings.HasPrefix(string(uri), "file:///") {
   116  		rest := string(uri)[len("file://"):] // leave one slash
   117  		for i := 0; i < len(rest); i++ {
   118  			b := rest[i]
   119  			// Reject these cases:
   120  			if b < ' ' || b == 0x7f || // control character
   121  				b == '%' || b == '+' || // URI escape
   122  				b == ':' || // Windows drive letter
   123  				b == '@' || b == '&' || b == '?' { // authority or query
   124  				goto slow
   125  			}
   126  		}
   127  		return rest, nil
   128  	}
   129  slow:
   130  
   131  	u, err := url.ParseRequestURI(string(uri))
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  	if u.Scheme != fileScheme {
   136  		return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
   137  	}
   138  	// If the URI is a Windows URI, we trim the leading "/" and uppercase
   139  	// the drive letter, which will never be case sensitive.
   140  	if isWindowsDriveURIPath(u.Path) {
   141  		u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
   142  	}
   143  
   144  	return u.Path, nil
   145  }
   146  
   147  // ParseDocumentURI interprets a string as a DocumentURI, applying VS
   148  // Code workarounds; see [DocumentURI.UnmarshalText] for details.
   149  func ParseDocumentURI(s string) (DocumentURI, error) {
   150  	if s == "" {
   151  		return "", nil
   152  	}
   153  
   154  	if !strings.HasPrefix(s, "file://") {
   155  		return "", fmt.Errorf("DocumentURI scheme is not 'file': %s", s)
   156  	}
   157  
   158  	// VS Code sends URLs with only two slashes,
   159  	// which are invalid. golang/go#39789.
   160  	if !strings.HasPrefix(s, "file:///") {
   161  		s = "file:///" + s[len("file://"):]
   162  	}
   163  
   164  	// Even though the input is a URI, it may not be in canonical form. VS Code
   165  	// in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
   166  	path, err := url.PathUnescape(s[len("file://"):])
   167  	if err != nil {
   168  		return "", err
   169  	}
   170  
   171  	// File URIs from Windows may have lowercase drive letters.
   172  	// Since drive letters are guaranteed to be case insensitive,
   173  	// we change them to uppercase to remain consistent.
   174  	// For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
   175  	if isWindowsDriveURIPath(path) {
   176  		path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
   177  	}
   178  	u := url.URL{Scheme: fileScheme, Path: path}
   179  	return DocumentURI(u.String()), nil
   180  }
   181  
   182  // URIFromPath returns DocumentURI for the supplied file path.
   183  // Given "", it returns "".
   184  func URIFromPath(path string) DocumentURI {
   185  	if path == "" {
   186  		return ""
   187  	}
   188  	if !isWindowsDrivePath(path) {
   189  		if abs, err := filepath.Abs(path); err == nil {
   190  			path = abs
   191  		}
   192  	}
   193  	// Check the file path again, in case it became absolute.
   194  	if isWindowsDrivePath(path) {
   195  		path = "/" + strings.ToUpper(string(path[0])) + path[1:]
   196  	}
   197  	path = filepath.ToSlash(path)
   198  	u := url.URL{
   199  		Scheme: fileScheme,
   200  		Path:   path,
   201  	}
   202  	return DocumentURI(u.String())
   203  }
   204  
   205  const fileScheme = "file"
   206  
   207  // isWindowsDrivePath returns true if the file path is of the form used by
   208  // Windows. We check if the path begins with a drive letter, followed by a ":".
   209  // For example: C:/x/y/z.
   210  func isWindowsDrivePath(path string) bool {
   211  	if len(path) < 3 {
   212  		return false
   213  	}
   214  	return unicode.IsLetter(rune(path[0])) && path[1] == ':'
   215  }
   216  
   217  // isWindowsDriveURIPath returns true if the file URI is of the format used by
   218  // Windows URIs. The url.Parse package does not specially handle Windows paths
   219  // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
   220  func isWindowsDriveURIPath(uri string) bool {
   221  	if len(uri) < 4 {
   222  		return false
   223  	}
   224  	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
   225  }