| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package span |
| |
| import ( |
| "fmt" |
| "net/url" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "unicode" |
| ) |
| |
| const fileScheme = "file" |
| |
| // URI represents the full URI for a file. |
| type URI string |
| |
| // Filename returns the file path for the given URI. It will return an error if |
| // the URI is invalid, or if the URI does not have the file scheme. |
| func (uri URI) Filename() (string, error) { |
| filename, err := filename(uri) |
| if err != nil { |
| return "", err |
| } |
| return filepath.FromSlash(filename), nil |
| } |
| |
| func filename(uri URI) (string, error) { |
| u, err := url.ParseRequestURI(string(uri)) |
| if err != nil { |
| return "", err |
| } |
| if u.Scheme != fileScheme { |
| return "", fmt.Errorf("only file URIs are supported, got %v", u.Scheme) |
| } |
| if isWindowsDriveURI(u.Path) { |
| u.Path = u.Path[1:] |
| } |
| return u.Path, nil |
| } |
| |
| // NewURI returns a span URI for the string. |
| // It will attempt to detect if the string is a file path or uri. |
| func NewURI(s string) URI { |
| if u, err := url.PathUnescape(s); err == nil { |
| s = u |
| } |
| if strings.HasPrefix(s, fileScheme+"://") { |
| return URI(s) |
| } |
| return FileURI(s) |
| } |
| |
| func CompareURI(a, b URI) int { |
| if a == b { |
| return 0 |
| } |
| // If we have the same URI basename, we may still have the same file URIs. |
| if fa, err := a.Filename(); err == nil { |
| if fb, err := b.Filename(); err == nil { |
| if strings.EqualFold(filepath.Base(fa), filepath.Base(fb)) { |
| // Stat the files to check if they are equal. |
| if infoa, err := os.Stat(fa); err == nil { |
| if infob, err := os.Stat(fb); err == nil { |
| if os.SameFile(infoa, infob) { |
| return 0 |
| } |
| } |
| } |
| } |
| return strings.Compare(fa, fb) |
| } |
| } |
| return strings.Compare(string(a), string(b)) |
| } |
| |
| // FileURI returns a span URI for the supplied file path. |
| // It will always have the file scheme. |
| func FileURI(path string) URI { |
| // Handle standard library paths that contain the literal "$GOROOT". |
| // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT. |
| const prefix = "$GOROOT" |
| if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) { |
| suffix := path[len(prefix):] |
| path = runtime.GOROOT() + suffix |
| } |
| if !isWindowsDrivePath(path) { |
| if abs, err := filepath.Abs(path); err == nil { |
| path = abs |
| } |
| } |
| // Check the file path again, in case it became absolute. |
| if isWindowsDrivePath(path) { |
| path = "/" + path |
| } |
| path = filepath.ToSlash(path) |
| u := url.URL{ |
| Scheme: fileScheme, |
| Path: path, |
| } |
| return URI(u.String()) |
| } |
| |
| // isWindowsDrivePath returns true if the file path is of the form used by |
| // Windows. We check if the path begins with a drive letter, followed by a ":". |
| func isWindowsDrivePath(path string) bool { |
| if len(path) < 4 { |
| return false |
| } |
| return unicode.IsLetter(rune(path[0])) && path[1] == ':' |
| } |
| |
| // isWindowsDriveURI returns true if the file URI is of the format used by |
| // Windows URIs. The url.Parse package does not specially handle Windows paths |
| // (see https://golang.org/issue/6027). We check if the URI path has |
| // a drive prefix (e.g. "/C:"). If so, we trim the leading "/". |
| func isWindowsDriveURI(uri string) bool { |
| if len(uri) < 4 { |
| return false |
| } |
| return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' |
| } |