helper/url: add Windows 'safe' URL Parse wrapper

Pull out the urlParse function, which was introduced in config/module,
into a helper package.
This commit is contained in:
Emil Hessman 2015-02-03 20:44:34 +01:00
parent 26156981d7
commit e7bbbfb098
11 changed files with 183 additions and 71 deletions

View File

@ -3,6 +3,8 @@ package module
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"github.com/hashicorp/terraform/helper/url"
) )
// Detector defines the interface that an invalid URL or a URL with a blank // Detector defines the interface that an invalid URL or a URL with a blank
@ -37,7 +39,7 @@ func Detect(src string, pwd string) (string, error) {
// Separate out the subdir if there is one, we don't pass that to detect // Separate out the subdir if there is one, we don't pass that to detect
getSrc, subDir := getDirSubdir(getSrc) getSrc, subDir := getDirSubdir(getSrc)
u, err := urlParse(getSrc) u, err := url.Parse(getSrc)
if err == nil && u.Scheme != "" { if err == nil && u.Scheme != "" {
// Valid URL // Valid URL
return src, nil return src, nil
@ -66,7 +68,7 @@ func Detect(src string, pwd string) (string, error) {
} }
} }
if subDir != "" { if subDir != "" {
u, err := urlParse(result) u, err := url.Parse(result)
if err != nil { if err != nil {
return "", fmt.Errorf("Error parsing URL: %s", err) return "", fmt.Errorf("Error parsing URL: %s", err)
} }

View File

@ -3,6 +3,7 @@ package module
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime"
) )
// FileDetector implements Detector to detect file paths. // FileDetector implements Detector to detect file paths.
@ -23,3 +24,17 @@ func (d *FileDetector) Detect(src, pwd string) (string, bool, error) {
} }
return fmtFileURL(src), true, nil return fmtFileURL(src), true, nil
} }
func fmtFileURL(path string) string {
if runtime.GOOS == "windows" {
// Make sure we're using "/" on Windows. URLs are "/"-based.
path = filepath.ToSlash(path)
return fmt.Sprintf("file://%s", path)
}
// Make sure that we don't start with "/" since we add that below.
if path[0] == '/' {
path = path[1:]
}
return fmt.Sprintf("file:///%s", path)
}

View File

@ -23,8 +23,8 @@ var unixFileTests = []fileTest{
var winFileTests = []fileTest{ var winFileTests = []fileTest{
{"/foo", "/pwd", "file:///pwd/foo", false}, {"/foo", "/pwd", "file:///pwd/foo", false},
{`C:\`, `/pwd`, `file:///C:/`, false}, {`C:\`, `/pwd`, `file://C:/`, false},
{`C:\?bar=baz`, `/pwd`, `file:///C:/?bar=baz`, false}, {`C:\?bar=baz`, `/pwd`, `file://C:/?bar=baz`, false},
} }
func TestFileDetector(t *testing.T) { func TestFileDetector(t *testing.T) {
@ -61,7 +61,7 @@ var noPwdUnixFileTests = []fileTest{
var noPwdWinFileTests = []fileTest{ var noPwdWinFileTests = []fileTest{
{in: "/foo", pwd: "", out: "", err: true}, {in: "/foo", pwd: "", out: "", err: true},
{in: `C:\`, pwd: ``, out: `file:///C:/`, err: false}, {in: `C:\`, pwd: ``, out: `file://C:/`, err: false},
} }
func TestFileDetector_noPwd(t *testing.T) { func TestFileDetector_noPwd(t *testing.T) {

View File

@ -11,6 +11,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"syscall" "syscall"
urlhelper "github.com/hashicorp/terraform/helper/url"
) )
// Getter defines the interface that schemes must implement to download // Getter defines the interface that schemes must implement to download
@ -72,7 +74,7 @@ func Get(dst, src string) error {
dst = tmpDir dst = tmpDir
} }
u, err := urlParse(src) u, err := urlhelper.Parse(src)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,6 +6,8 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
urlhelper "github.com/hashicorp/terraform/helper/url"
) )
// HgGetter is a Getter implementation that will download a module from // HgGetter is a Getter implementation that will download a module from
@ -17,7 +19,7 @@ func (g *HgGetter) Get(dst string, u *url.URL) error {
return fmt.Errorf("hg must be available and on the PATH") return fmt.Errorf("hg must be available and on the PATH")
} }
newURL, err := urlParse(u.String()) newURL, err := urlhelper.Parse(u.String())
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
urlhelper "github.com/hashicorp/terraform/helper/url"
) )
const fixtureDir = "./test-fixtures" const fixtureDir = "./test-fixtures"
@ -43,7 +44,7 @@ func testModule(n string) string {
} }
func testModuleURL(n string) *url.URL { func testModuleURL(n string) *url.URL {
u, err := urlParse(testModule(n)) u, err := urlhelper.Parse(testModule(n))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -1,63 +0,0 @@
package module
import (
"fmt"
"net/url"
"path/filepath"
"runtime"
"strings"
)
func urlParse(rawURL string) (*url.URL, error) {
if runtime.GOOS == "windows" {
// Make sure we're using "/" on Windows. URLs are "/"-based.
rawURL = filepath.ToSlash(rawURL)
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if runtime.GOOS != "windows" {
return u, err
}
if len(rawURL) > 1 && rawURL[1] == ':' {
// Assume we're dealing with a drive letter file path on Windows.
// We need to adjust the URL Path for drive letter file paths
// because url.Parse("c:/users/user") yields URL Scheme = "c"
// and URL path = "/users/user".
u.Path = fmt.Sprintf("%s:%s", u.Scheme, u.Path)
u.Scheme = ""
}
if len(u.Host) > 1 && u.Host[1] == ':' && strings.HasPrefix(rawURL, "file://") {
// Assume we're dealing with a drive letter file path on Windows
// where the drive letter has been parsed into the URL Host.
u.Path = fmt.Sprintf("%s%s", u.Host, u.Path)
u.Host = ""
}
// Remove leading slash for absolute file paths on Windows.
// For example, url.Parse yields u.Path = "/C:/Users/user" for
// rawURL = "file:///C:/Users/user", which is an incorrect syntax.
if len(u.Path) > 2 && u.Path[0] == '/' && u.Path[2] == ':' {
u.Path = u.Path[1:]
}
return u, err
}
func fmtFileURL(path string) string {
if runtime.GOOS == "windows" {
// Make sure we're using "/" on Windows. URLs are "/"-based.
path = filepath.ToSlash(path)
}
// Make sure that we don't start with "/" since we add that below.
if path[0] == '/' {
path = path[1:]
}
return fmt.Sprintf("file:///%s", path)
}

14
helper/url/url.go Normal file
View File

@ -0,0 +1,14 @@
package url
import (
"net/url"
)
// Parse parses rawURL into a URL structure.
// The rawURL may be relative or absolute.
//
// Parse is a wrapper for the Go stdlib net/url Parse function, but returns
// Windows "safe" URLs on Windows platforms.
func Parse(rawURL string) (*url.URL, error) {
return parse(rawURL)
}

88
helper/url/url_test.go Normal file
View File

@ -0,0 +1,88 @@
package url
import (
"runtime"
"testing"
)
type parseTest struct {
rawURL string
scheme string
host string
path string
str string
err bool
}
var parseTests = []parseTest{
{
rawURL: "/foo/bar",
scheme: "",
host: "",
path: "/foo/bar",
str: "/foo/bar",
err: false,
},
{
rawURL: "file:///dir/",
scheme: "file",
host: "",
path: "/dir/",
str: "file:///dir/",
err: false,
},
}
var winParseTests = []parseTest{
{
rawURL: `C:\`,
scheme: ``,
host: ``,
path: `C:/`,
str: `C:/`,
err: false,
},
{
rawURL: `file://C:\`,
scheme: `file`,
host: ``,
path: `C:/`,
str: `file://C:/`,
err: false,
},
{
rawURL: `file:///C:\`,
scheme: `file`,
host: ``,
path: `C:/`,
str: `file://C:/`,
err: false,
},
}
func TestParse(t *testing.T) {
if runtime.GOOS == "windows" {
parseTests = append(parseTests, winParseTests...)
}
for i, pt := range parseTests {
url, err := Parse(pt.rawURL)
if err != nil && !pt.err {
t.Errorf("test %d: unexpected error: %s", i, err)
}
if err == nil && pt.err {
t.Errorf("test %d: expected an error", i)
}
if url.Scheme != pt.scheme {
t.Errorf("test %d: expected Scheme = %q, got %q", i, pt.scheme, url.Scheme)
}
if url.Host != pt.host {
t.Errorf("test %d: expected Host = %q, got %q", i, pt.host, url.Host)
}
if url.Path != pt.path {
t.Errorf("test %d: expected Path = %q, got %q", i, pt.path, url.Path)
}
if url.String() != pt.str {
t.Errorf("test %d: expected url.String() = %q, got %q", i, pt.str, url.String())
}
}
}

11
helper/url/url_unix.go Normal file
View File

@ -0,0 +1,11 @@
// +build !windows
package url
import (
"net/url"
)
func parse(rawURL string) (*url.URL, error) {
return url.Parse(rawURL)
}

40
helper/url/url_windows.go Normal file
View File

@ -0,0 +1,40 @@
package url
import (
"fmt"
"net/url"
"path/filepath"
"strings"
)
func parse(rawURL string) (*url.URL, error) {
// Make sure we're using "/" since URLs are "/"-based.
rawURL = filepath.ToSlash(rawURL)
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if len(rawURL) > 1 && rawURL[1] == ':' {
// Assume we're dealing with a drive letter file path where the drive
// letter has been parsed into the URL Scheme, and the rest of the path
// has been parsed into the URL Path without the leading ':' character.
u.Path = fmt.Sprintf("%s:%s", string(rawURL[0]), u.Path)
u.Scheme = ""
}
if len(u.Host) > 1 && u.Host[1] == ':' && strings.HasPrefix(rawURL, "file://") {
// Assume we're dealing with a drive letter file path where the drive
// letter has been parsed into the URL Host.
u.Path = fmt.Sprintf("%s%s", u.Host, u.Path)
u.Host = ""
}
// Remove leading slash for absolute file paths.
if len(u.Path) > 2 && u.Path[0] == '/' && u.Path[2] == ':' {
u.Path = u.Path[1:]
}
return u, err
}