diff --git a/cmd/eval.go b/cmd/eval.go index 043113626a..d29d5f3360 100644 --- a/cmd/eval.go +++ b/cmd/eval.go @@ -15,6 +15,7 @@ import ( "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/cover" + fileurl "github.com/open-policy-agent/opa/internal/file/url" pr "github.com/open-policy-agent/opa/internal/presentation" "github.com/open-policy-agent/opa/internal/runtime" "github.com/open-policy-agent/opa/metrics" @@ -109,8 +110,8 @@ To evaluate a query against JSON data: To evaluate a query against JSON data supplied with a file:// URL: $ opa eval --data file:///path/to/file.json 'data' - - + + File & Bundle Loading --------------------- @@ -137,12 +138,12 @@ The JSON file 'foo/bar/data.json' would be loaded and rooted under package path contained inside the file. Only data files named data.json or data.yaml will be loaded. In the example above the manifest.yaml would be ignored. - + See https://www.openpolicyagent.org/docs/latest/bundles/ for more details on bundle directory structures. The --data flag can be used to recursively load ALL *.rego, *.json, and -*.yaml files under the specified directory. +*.yaml files under the specified directory. Output Formats -------------- @@ -421,7 +422,11 @@ func readInputBytes(params evalCommandParams) ([]byte, error) { if params.stdinInput { return ioutil.ReadAll(os.Stdin) } else if params.inputPath != "" { - return ioutil.ReadFile(params.inputPath) + path, err := fileurl.Clean(params.inputPath) + if err != nil { + return nil, err + } + return ioutil.ReadFile(path) } return nil, nil } diff --git a/cmd/fmt.go b/cmd/fmt.go index b19e4703c7..83556c45ab 100644 --- a/cmd/fmt.go +++ b/cmd/fmt.go @@ -14,7 +14,7 @@ import ( "path/filepath" "github.com/open-policy-agent/opa/format" - + fileurl "github.com/open-policy-agent/opa/internal/file/url" "github.com/spf13/cobra" ) @@ -59,6 +59,14 @@ func opaFmt(args []string) int { } for _, filename := range args { + + var err error + filename, err = fileurl.Clean(filename) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + if err := filepath.Walk(filename, formatFile); err != nil { switch err := err.(type) { case fmtError: diff --git a/internal/file/url/url.go b/internal/file/url/url.go new file mode 100644 index 0000000000..aacc571837 --- /dev/null +++ b/internal/file/url/url.go @@ -0,0 +1,42 @@ +// Copyright 2019 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +// Package url contains helpers for dealing with file paths and URLs. +package url + +import ( + "fmt" + "net/url" + "runtime" + "strings" +) + +var goos = runtime.GOOS + +// Clean returns a cleaned file path that may or may not be a URL. +func Clean(path string) (string, error) { + + if strings.Contains(path, "://") { + + url, err := url.Parse(path) + if err != nil { + return "", err + } + + if url.Scheme != "file" { + return "", fmt.Errorf("unsupported URL scheme: %v", path) + } + + path = url.Path + + // Trim leading slash on Windows if present. The url.Path field returned + // by url.Parse has leading slash that causes CreateFile() calls to fail + // on Windows. See https://github.com/golang/go/issues/6027 for details. + if goos == "windows" && len(path) >= 1 && path[0] == '/' { + path = path[1:] + } + } + + return path, nil +} diff --git a/internal/file/url/url_test.go b/internal/file/url/url_test.go new file mode 100644 index 0000000000..4b56fd4477 --- /dev/null +++ b/internal/file/url/url_test.go @@ -0,0 +1,50 @@ +package url + +import "testing" + +func TestClean(t *testing.T) { + + cases := []struct { + input string + goos string + exp string + err error + }{ + { + input: "c:/foo", + exp: "c:/foo", + goos: "windows", + }, + { + input: "file:///c:/a/b", + exp: "c:/a/b", + goos: "windows", + }, + { + input: "foo", + exp: "foo", + }, + { + input: "/a/b/c", + exp: "/a/b/c", + }, + { + input: "file:///a/b/c", + exp: "/a/b/c", + }, + } + + for _, tc := range cases { + t.Run(tc.input, func(t *testing.T) { + goos = tc.goos + path, err := Clean(tc.input) + if tc.err != nil { + if err == nil || err == tc.err { + t.Fatalf("Want err: %v but got: %v, err: %v", tc.err, path, err) + } + } else if err != nil || path != tc.exp { + t.Fatalf("Want %v but got: %v, err: %v", tc.exp, path, err) + } + }) + } +} diff --git a/loader/loader.go b/loader/loader.go index c29888b795..e956a220b6 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -9,17 +9,15 @@ import ( "bytes" "fmt" "io/ioutil" - "net/url" "os" "path/filepath" - "runtime" "strings" - "github.com/open-policy-agent/opa/internal/file" - "github.com/ghodss/yaml" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/bundle" + "github.com/open-policy-agent/opa/internal/file" + fileurl "github.com/open-policy-agent/opa/internal/file/url" "github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/storage/inmem" "github.com/open-policy-agent/opa/util" @@ -122,7 +120,7 @@ func Filtered(paths []string, filter Filter) (*Result, error) { // Rego returns a RegoFile object loaded from the given path. func Rego(path string) (*RegoFile, error) { - path, err := cleanFileURL(path) + path, err := fileurl.Clean(path) if err != nil { return nil, err } @@ -137,7 +135,7 @@ func Rego(path string) (*RegoFile, error) { // it will be treated as a normal tarball bundle. If a directory // is supplied it will be loaded as an unzipped bundle tree. func AsBundle(path string) (*bundle.Bundle, error) { - path, err := cleanFileURL(path) + path, err := fileurl.Clean(path) if err != nil { return nil, err } @@ -173,7 +171,7 @@ func CleanPath(path string) string { // and path is a directory, then Paths will walk the directory structure // recursively and list files at each level. func Paths(path string, recurse bool) (paths []string, err error) { - path, err = cleanFileURL(path) + path, err = fileurl.Clean(path) if err != nil { return nil, err } @@ -276,7 +274,7 @@ func all(paths []string, filter Filter, f func(*Result, string, int) error) (*Re func allRec(path string, filter Filter, errors *loaderErrors, loaded *Result, depth int, f func(*Result, string, int) error) { - path, err := cleanFileURL(path) + path, err := fileurl.Clean(path) if err != nil { errors.Add(err) return @@ -316,32 +314,6 @@ func allRec(path string, filter Filter, errors *loaderErrors, loaded *Result, de } } -func cleanFileURL(path string) (string, error) { - - if strings.Contains(path, "://") { - - url, err := url.Parse(path) - if err != nil { - return "", err - } - - if url.Scheme != "file" { - return "", fmt.Errorf("unsupported URL scheme: %v", path) - } - - path = url.Path - - // Trim leading slash on Windows if present. The url.Path field returned - // by url.Parse has leading slash that causes CreateFile() calls to fail - // on Windows. See https://github.com/golang/go/issues/6027 for details. - if runtime.GOOS == "windows" && len(path) >= 1 && path[0] == '/' { - path = path[1:] - } - } - - return path, nil -} - func exclude(filters []Filter, path string, info os.FileInfo, depth int) bool { for _, f := range filters { if f(path, info, depth) {