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  }