// Copyright 2019 The Go Language Server 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 uri import ( "errors" "fmt" "net/url" "os" "path/filepath" "strings" "unicode" ) const ( // FileScheme schema of filesystem path. FileScheme = "file" // HTTPScheme schema of http. HTTPScheme = "http" // HTTPSScheme schema of https. HTTPSScheme = "https" ) const hierPart = "://" // URI Uniform Resource Identifier (URI) https://tools.ietf.org/html/rfc3986. // // This class is a simple parser which creates the basic component parts // (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation // and encoding. // // foo://example.com:8042/over/there?name=ferret#nose // \_/ \______________/\_________/ \_________/ \__/ // | | | | | // scheme authority path query fragment // | _____________________|__ // / \ / \ // urn:example:animal:ferret:nose type URI string // Filename returns the file path for the given URI. // It is an error to call this on a URI that is not a valid filename. func (u URI) Filename() string { filename, err := filename(u) if err != nil { panic(err) } return filepath.FromSlash(filename) } // Returns true if URI has a valid filename func (u URI) HasFilename() bool { return IsFileURI(string(u)) } func IsFileURI(uri string) bool { return strings.HasPrefix(uri, FileScheme+hierPart) } func filename(uri URI) (string, error) { u, err := url.ParseRequestURI(string(uri)) if err != nil { return "", fmt.Errorf("failed to parse request URI: %w", 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 } // New parses and creates a new URI from input. func New(input string) URI { if u, err := url.PathUnescape(input); err == nil { input = u } if IsFileURI(input) { return URI(input) } return File(input) } // File parses and creates a new filesystem URI from path. func File(path string) URI { const goRootPragma = "$GOROOT" if len(path) >= len(goRootPragma) && strings.EqualFold(goRootPragma, path[:len(goRootPragma)]) { if goRoot, found := os.LookupEnv("GOROOT"); found { path = goRoot + path[len(goRootPragma):] } } if isWindowsDrivePath(path) { path = "/" + path } else if absPath, err := filepath.Abs(path); err == nil { path = absPath } path = filepath.ToSlash(path) u := url.URL{ Scheme: FileScheme, Path: path, } return URI(u.String()) } // Parse parses and creates a new URI from input. func Parse(input string) (u URI, err error) { us, err := url.Parse(input) if err != nil { return u, fmt.Errorf("url.Parse: %w", err) } switch us.Scheme { case FileScheme: ut := url.URL{ Scheme: FileScheme, Path: us.Path, RawPath: filepath.FromSlash(us.Path), } u = URI(ut.String()) case HTTPScheme, HTTPSScheme: ut := url.URL{ Scheme: us.Scheme, Host: us.Host, Path: us.Path, RawQuery: us.Query().Encode(), Fragment: us.Fragment, } u = URI(ut.String()) default: return u, errors.New("unknown scheme") } return } // From returns the new URI from args. func From(scheme, authority, path, query, fragment string) URI { switch scheme { case FileScheme: u := url.URL{ Scheme: FileScheme, Path: path, RawPath: filepath.FromSlash(path), } return URI(u.String()) case HTTPScheme, HTTPSScheme: u := url.URL{ Scheme: scheme, Host: authority, Path: path, RawQuery: url.QueryEscape(query), Fragment: fragment, } return URI(u.String()) default: panic(fmt.Sprintf("unknown scheme: %s", scheme)) } } // 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] == ':' }