mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 16:34:30 +01:00
fspath: Implement a connection string parser #4996
This is implemented as a state machine parser so it can emit sensible error messages. It does not use the connection strings elsewhere in rclone yet - see subsequent commits. An optional fuzzer is implemented for the Parse function.
This commit is contained in:
parent
3b21857097
commit
8a46dd1b57
@ -277,10 +277,11 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
||||
return nil, errors.New("can't point remote at itself - check the value of the remote setting")
|
||||
}
|
||||
|
||||
baseName, basePath, err := fspath.Parse(remote)
|
||||
parsed, err := fspath.Parse(remote)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse remote %q to wrap", remote)
|
||||
}
|
||||
baseName, basePath := parsed.ConfigString, parsed.Path
|
||||
if baseName != "" {
|
||||
baseName += ":"
|
||||
}
|
||||
|
@ -208,6 +208,83 @@ To copy files and directories in `https://example.com/path/to/dir` to `/tmp/dir`
|
||||
To copy files and directories from `example.com` in the relative
|
||||
directory `path/to/dir` to `/tmp/dir` using sftp.
|
||||
|
||||
### Connection strings {#connection-strings}
|
||||
|
||||
The above examples can also be written using a connection string
|
||||
syntax, so instead of providing the arguments as command line
|
||||
parameters `--http-url https://pub.rclone.org` they are provided as
|
||||
part of the remote specification as a kind of connection string.
|
||||
|
||||
rclone lsd ":http,url='https://pub.rclone.org':"
|
||||
rclone lsf ":http,url='https://example.com':path/to/dir"
|
||||
rclone copy ":http,url='https://example.com':path/to/dir" /tmp/dir
|
||||
rclone copy :sftp,host=example.com:path/to/dir /tmp/dir
|
||||
|
||||
These can apply to modify existing remotes as well as create new
|
||||
remotes with the on the fly syntax. This example is equivalent to
|
||||
adding the `--drive-shared-with-me` parameter to the remote `gdrive:`.
|
||||
|
||||
rclone lsf "gdrive,shared_with_me:path/to/dir"
|
||||
|
||||
The major advantage to using the connection string style syntax is
|
||||
that it only applies the the remote, not to all the remotes of that
|
||||
type of the command line. A common confusion is this attempt to copy a
|
||||
file shared on google drive to the normal drive which **does not
|
||||
work** because the `--drive-shared-with-me` flag applies to both the
|
||||
source and the destination.
|
||||
|
||||
rclone copy --drive-shared-with-me gdrive:shared-file.txt gdrive:
|
||||
|
||||
However using the connection string syntax, this does work.
|
||||
|
||||
rclone copy "gdrive,shared_with_me:shared-file.txt" gdrive:
|
||||
|
||||
The connection strings have the following syntax
|
||||
|
||||
remote,parameter=value,parameter2=value2:path/to/dir
|
||||
:backend,parameter=value,parameter2=value2:path/to/dir
|
||||
|
||||
If the `parameter` has a `:` or `,` then it must be placed in quotes `"` or
|
||||
`'`, so
|
||||
|
||||
remote,parameter="colon:value",parameter2="comma,value":path/to/dir
|
||||
:backend,parameter='colon:value',parameter2='comma,value':path/to/dir
|
||||
|
||||
If a quoted value needs to include that quote, then it should be
|
||||
doubled, so
|
||||
|
||||
remote,parameter="with""quote",parameter2='with''quote':path/to/dir
|
||||
|
||||
This will make `parameter` be `with"quote` and `parameter2` be
|
||||
`with'quote`.
|
||||
|
||||
If you leave off the `=parameter` then rclone will substitute `=true`
|
||||
which works very well with flags. For example to use s3 configured in
|
||||
the environment you could use:
|
||||
|
||||
rclone lsd :s3,env_auth:
|
||||
|
||||
Which is equivalent to
|
||||
|
||||
rclone lsd :s3,env_auth=true:
|
||||
|
||||
Note that on the command line you might need to surround these
|
||||
connection strings with `"` or `'` to stop the shell interpreting any
|
||||
special characters within them.
|
||||
|
||||
If you are a shell master then you'll know which strings are OK and
|
||||
which aren't, but if you aren't sure then enclose them in `"` and use
|
||||
`'` as the inside quote. This syntax works on all OSes.
|
||||
|
||||
rclone copy ":http,url='https://example.com':path/to/dir" /tmp/dir
|
||||
|
||||
On Linux/macOS some characters are still interpreted inside `"`
|
||||
strings in the shell (notably `\` and `$` and `"`) so if your strings
|
||||
contain those you can swap the roles of `"` and `'` thus. (This syntax
|
||||
does not work on Windows.)
|
||||
|
||||
rclone copy ':http,url="https://example.com":path/to/dir' /tmp/dir
|
||||
|
||||
### Valid remote names
|
||||
|
||||
- Remote names may only contain 0-9, A-Z ,a-z ,_ , - and space.
|
||||
@ -1927,11 +2004,8 @@ so they take exactly the same form.
|
||||
### Config file ###
|
||||
|
||||
You can set defaults for values in the config file on an individual
|
||||
remote basis. If you want to use this feature, you will need to
|
||||
discover the name of the config items that you want. The easiest way
|
||||
is to run through `rclone config` by hand, then look in the config
|
||||
file to see what the values are (the config file can be found by
|
||||
looking at the help for `--config` in `rclone help`).
|
||||
remote basis. The names of the config items are documented in the page
|
||||
for each backend.
|
||||
|
||||
To find the name of the environment variable, you need to set, take
|
||||
`RCLONE_CONFIG_` + name of remote + `_` + name of config file option
|
||||
@ -1953,6 +2027,11 @@ mys3:
|
||||
Note that if you want to create a remote using environment variables
|
||||
you must create the `..._TYPE` variable as above.
|
||||
|
||||
Note also that now rclone has [connectionstrings](#connection-strings),
|
||||
it is probably easier to use those instead which makes the above example
|
||||
|
||||
rclone lsd :s3,access_key_id=XXX,secret_access_key=XXX:
|
||||
|
||||
### Precedence
|
||||
|
||||
The various different methods of backend configuration are read in
|
||||
|
4
fs/fs.go
4
fs/fs.go
@ -1206,10 +1206,12 @@ func MustFind(name string) *RegInfo {
|
||||
// ParseRemote deconstructs a path into configName, fsPath, looking up
|
||||
// the fsName in the config file (returning NotFoundInConfigFile if not found)
|
||||
func ParseRemote(path string) (fsInfo *RegInfo, configName, fsPath string, err error) {
|
||||
configName, fsPath, err = fspath.Parse(path)
|
||||
parsed, err := fspath.Parse(path)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
configName, fsPath = parsed.Name, parsed.Path
|
||||
// FIXME do something with parsed.Config
|
||||
var fsName string
|
||||
var ok bool
|
||||
if configName != "" {
|
||||
|
46
fs/fspath/fuzz.go
Normal file
46
fs/fspath/fuzz.go
Normal file
@ -0,0 +1,46 @@
|
||||
//+build gofuzz
|
||||
|
||||
/*
|
||||
Fuzz test the Parse function
|
||||
|
||||
Generate corpus
|
||||
|
||||
go test -v -make-corpus
|
||||
|
||||
Install go fuzz
|
||||
|
||||
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
|
||||
Compile and fuzz
|
||||
|
||||
go-fuzz-build
|
||||
go-fuzz
|
||||
|
||||
Tidy up
|
||||
|
||||
rm -rf corpus/ crashers/ suppressions/
|
||||
git co ../../go.mod ../../go.sum
|
||||
*/
|
||||
|
||||
package fspath
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
path := string(data)
|
||||
parsed, err := Parse(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if parsed.Name == "" {
|
||||
if parsed.ConfigString != "" {
|
||||
panic("bad ConfigString")
|
||||
}
|
||||
if parsed.Path != path {
|
||||
panic("local path not preserved")
|
||||
}
|
||||
} else {
|
||||
if parsed.ConfigString+":"+parsed.Path != path {
|
||||
panic("didn't split properly")
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
@ -8,28 +8,37 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
"github.com/rclone/rclone/fs/driveletter"
|
||||
)
|
||||
|
||||
const (
|
||||
configNameRe = `[\w_ -]+`
|
||||
remoteNameRe = `^(:?` + configNameRe + `):`
|
||||
remoteNameRe = `^(:?` + configNameRe + `)`
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCharacters = errors.New("config name contains invalid characters - may only contain 0-9, A-Z ,a-z ,_ , - and space")
|
||||
errInvalidCharacters = errors.New("config name contains invalid characters - may only contain `0-9`, `A-Z`, `a-z`, `_`, `-` and space")
|
||||
errCantBeEmpty = errors.New("can't use empty string as a path")
|
||||
errCantStartWithDash = errors.New("config name starts with -")
|
||||
|
||||
// urlMatcher is a pattern to match an rclone URL
|
||||
// note that this matches invalid remoteNames
|
||||
urlMatcher = regexp.MustCompile(`^(:?[^\\/:]*):(.*)$`)
|
||||
errCantStartWithDash = errors.New("config name starts with `-`")
|
||||
errBadConfigParam = errors.New("config parameters may only contain `0-9`, `A-Z`, `a-z` and `_`")
|
||||
errEmptyConfigParam = errors.New("config parameters can't be empty")
|
||||
errConfigNameEmpty = errors.New("config name can't be empty")
|
||||
errConfigName = errors.New("config name needs a trailing `:`")
|
||||
errParam = errors.New("config parameter must end with `,` or `:`")
|
||||
errValue = errors.New("unquoted config value must end with `,` or `:`")
|
||||
errQuotedValue = errors.New("unterminated quoted config value")
|
||||
errAfterQuote = errors.New("expecting `:` or `,` or another quote after a quote")
|
||||
errSyntax = errors.New("syntax error in config string")
|
||||
|
||||
// configNameMatcher is a pattern to match an rclone config name
|
||||
configNameMatcher = regexp.MustCompile(`^` + configNameRe + `$`)
|
||||
|
||||
// remoteNameMatcher is a pattern to match an rclone remote name
|
||||
remoteNameMatcher = regexp.MustCompile(remoteNameRe + `$`)
|
||||
// remoteNameMatcher is a pattern to match an rclone remote name at the start of a config
|
||||
remoteNameMatcher = regexp.MustCompile(`^` + remoteNameRe + `(:$|,)`)
|
||||
|
||||
// Function to check if string is a drive letter to be overriden in the tests
|
||||
isDriveLetter = driveletter.IsDriveLetter
|
||||
)
|
||||
|
||||
// CheckConfigName returns an error if configName is invalid
|
||||
@ -44,41 +53,210 @@ func CheckConfigName(configName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRemoteName returns an error if remoteName is invalid
|
||||
func CheckRemoteName(remoteName string) error {
|
||||
// checkRemoteName returns an error if remoteName is invalid
|
||||
func checkRemoteName(remoteName string) error {
|
||||
if remoteName == ":" || remoteName == "::" {
|
||||
return errConfigNameEmpty
|
||||
}
|
||||
if !remoteNameMatcher.MatchString(remoteName) {
|
||||
return errInvalidCharacters
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse deconstructs a remote path into configName and fsPath
|
||||
// Return true if c is a valid character for a config parameter
|
||||
func isConfigParam(c rune) bool {
|
||||
return ((c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_')
|
||||
}
|
||||
|
||||
// Parsed is returned from Parse with the results of the connection string decomposition
|
||||
//
|
||||
// If the path is a local path then configName will be returned as "".
|
||||
// If Name is "" then it is a local path in Path
|
||||
//
|
||||
// So "remote:path/to/dir" will return "remote", "path/to/dir"
|
||||
// and "/path/to/local" will return ("", "/path/to/local")
|
||||
// Note that ConfigString + ":" + Path is equal to the input of Parse except that Path may have had
|
||||
// \ converted to /
|
||||
type Parsed struct {
|
||||
Name string // Just the name of the config: "remote" or ":backend"
|
||||
ConfigString string // The whole config string: "remote:" or ":backend,value=6:"
|
||||
Path string // The file system path, may be empty
|
||||
Config configmap.Simple // key/value config parsed out of ConfigString may be nil
|
||||
}
|
||||
|
||||
// Parse deconstructs a path into a Parsed structure
|
||||
//
|
||||
// If the path is a local path then parsed.Name will be returned as "".
|
||||
//
|
||||
// So "remote:path/to/dir" will return Parsed{Name:"remote", Path:"path/to/dir"},
|
||||
// and "/path/to/local" will return Parsed{Name:"", Path:"/path/to/local"}
|
||||
//
|
||||
// Note that this will turn \ into / in the fsPath on Windows
|
||||
//
|
||||
// An error may be returned if the remote name has invalid characters
|
||||
// in it or if the path is empty.
|
||||
func Parse(path string) (configName, fsPath string, err error) {
|
||||
// An error may be returned if the remote name has invalid characters or the
|
||||
// parameters are invalid or the path is empty.
|
||||
func Parse(path string) (parsed Parsed, err error) {
|
||||
parsed.Path = path
|
||||
if path == "" {
|
||||
return "", "", errCantBeEmpty
|
||||
return parsed, errCantBeEmpty
|
||||
}
|
||||
parts := urlMatcher.FindStringSubmatch(path)
|
||||
configName, fsPath = "", path
|
||||
if parts != nil && !driveletter.IsDriveLetter(parts[1]) {
|
||||
configName, fsPath = parts[1], parts[2]
|
||||
err = CheckRemoteName(configName + ":")
|
||||
if err != nil {
|
||||
return configName, fsPath, errInvalidCharacters
|
||||
// If path has no `:` in, it must be a local path
|
||||
if strings.IndexRune(path, ':') < 0 {
|
||||
return parsed, nil
|
||||
}
|
||||
// States for parser
|
||||
const (
|
||||
stateConfigName = uint8(iota)
|
||||
stateParam
|
||||
stateValue
|
||||
stateQuotedValue
|
||||
stateAfterQuote
|
||||
stateDone
|
||||
)
|
||||
var (
|
||||
state = stateConfigName // current state of parser
|
||||
i int // position in path
|
||||
prev int // previous position in path
|
||||
c rune // current rune under consideration
|
||||
quote rune // kind of quote to end this quoted string
|
||||
param string // current parameter value
|
||||
doubled bool // set if had doubled quotes
|
||||
)
|
||||
loop:
|
||||
for i, c = range path {
|
||||
// Example Parse
|
||||
// remote,param=value,param2="qvalue":/path/to/file
|
||||
switch state {
|
||||
// Parses "remote,"
|
||||
case stateConfigName:
|
||||
if i == 0 && c == ':' {
|
||||
continue
|
||||
} else if c == '/' || c == '\\' {
|
||||
// `:` or `,` not before a path separator must be a local path,
|
||||
// except if the path started with `:` in which case it was intended
|
||||
// to be an on the fly remote so return an error.
|
||||
if path[0] == ':' {
|
||||
return parsed, errInvalidCharacters
|
||||
}
|
||||
return parsed, nil
|
||||
} else if c == ':' || c == ',' {
|
||||
parsed.Name = path[:i]
|
||||
err := checkRemoteName(parsed.Name + ":")
|
||||
if err != nil {
|
||||
return parsed, err
|
||||
}
|
||||
prev = i + 1
|
||||
if c == ':' {
|
||||
// If we parsed a drive letter, must be a local path
|
||||
if isDriveLetter(parsed.Name) {
|
||||
parsed.Name = ""
|
||||
return parsed, nil
|
||||
}
|
||||
state = stateDone
|
||||
break loop
|
||||
}
|
||||
state = stateParam
|
||||
parsed.Config = make(configmap.Simple)
|
||||
}
|
||||
// Parses param= and param2=
|
||||
case stateParam:
|
||||
if c == ':' || c == ',' || c == '=' {
|
||||
param = path[prev:i]
|
||||
if len(param) == 0 {
|
||||
return parsed, errEmptyConfigParam
|
||||
}
|
||||
prev = i + 1
|
||||
if c == '=' {
|
||||
state = stateValue
|
||||
break
|
||||
}
|
||||
parsed.Config[param] = "true"
|
||||
if c == ':' {
|
||||
state = stateDone
|
||||
break loop
|
||||
}
|
||||
state = stateParam
|
||||
} else if !isConfigParam(c) {
|
||||
return parsed, errBadConfigParam
|
||||
}
|
||||
// Parses value
|
||||
case stateValue:
|
||||
if c == '\'' || c == '"' {
|
||||
if i == prev {
|
||||
quote = c
|
||||
state = stateQuotedValue
|
||||
prev = i + 1
|
||||
doubled = false
|
||||
break
|
||||
}
|
||||
} else if c == ':' || c == ',' {
|
||||
value := path[prev:i]
|
||||
prev = i + 1
|
||||
parsed.Config[param] = value
|
||||
if c == ':' {
|
||||
state = stateDone
|
||||
break loop
|
||||
}
|
||||
state = stateParam
|
||||
}
|
||||
// Parses "qvalue"
|
||||
case stateQuotedValue:
|
||||
if c == quote {
|
||||
state = stateAfterQuote
|
||||
}
|
||||
// Parses : or , or quote after "qvalue"
|
||||
case stateAfterQuote:
|
||||
if c == ':' || c == ',' {
|
||||
value := path[prev : i-1]
|
||||
// replace any doubled quotes if there were any
|
||||
if doubled {
|
||||
value = strings.Replace(value, string(quote)+string(quote), string(quote), -1)
|
||||
}
|
||||
prev = i + 1
|
||||
parsed.Config[param] = value
|
||||
if c == ':' {
|
||||
state = stateDone
|
||||
break loop
|
||||
} else {
|
||||
state = stateParam
|
||||
}
|
||||
} else if c == quote {
|
||||
// Here is a doubled quote to indicate a literal quote
|
||||
state = stateQuotedValue
|
||||
doubled = true
|
||||
} else {
|
||||
return parsed, errAfterQuote
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Depending on which state we were in when we fell off the
|
||||
// end of the state machine we can return a sensible error.
|
||||
switch state {
|
||||
default:
|
||||
return parsed, errSyntax
|
||||
case stateConfigName:
|
||||
return parsed, errConfigName
|
||||
case stateParam:
|
||||
return parsed, errParam
|
||||
case stateValue:
|
||||
return parsed, errValue
|
||||
case stateQuotedValue:
|
||||
return parsed, errQuotedValue
|
||||
case stateAfterQuote:
|
||||
return parsed, errAfterQuote
|
||||
case stateDone:
|
||||
break
|
||||
}
|
||||
|
||||
parsed.ConfigString = path[:i]
|
||||
parsed.Path = path[i+1:]
|
||||
|
||||
// change native directory separators to / if there are any
|
||||
fsPath = filepath.ToSlash(fsPath)
|
||||
return configName, fsPath, nil
|
||||
parsed.Path = filepath.ToSlash(parsed.Path)
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// Split splits a remote into a parent and a leaf
|
||||
@ -90,10 +268,11 @@ func Parse(path string) (configName, fsPath string, err error) {
|
||||
// The returned values have the property that parent + leaf == remote
|
||||
// (except under Windows where \ will be translated into /)
|
||||
func Split(remote string) (parent string, leaf string, err error) {
|
||||
remoteName, remotePath, err := Parse(remote)
|
||||
parsed, err := Parse(remote)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
remoteName, remotePath := parsed.ConfigString, parsed.Path
|
||||
if remoteName != "" {
|
||||
remoteName += ":"
|
||||
}
|
||||
@ -130,7 +309,8 @@ func JoinRootPath(remote, filePath string) string {
|
||||
if strings.HasPrefix(remote, "//") {
|
||||
return "/" + path.Join(remote, filePath)
|
||||
}
|
||||
remoteName, remotePath, err := Parse(remote)
|
||||
parsed, err := Parse(remote)
|
||||
remoteName, remotePath := parsed.ConfigString, parsed.Path
|
||||
if err != nil {
|
||||
// Couldn't parse so assume it is a path
|
||||
remoteName = ""
|
||||
|
@ -1,13 +1,23 @@
|
||||
package fspath
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
"github.com/rclone/rclone/fs/driveletter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
makeCorpus = flag.Bool("make-corpus", false, "Set to make the fuzzing corpus")
|
||||
)
|
||||
|
||||
func TestCheckConfigName(t *testing.T) {
|
||||
@ -39,6 +49,7 @@ func TestCheckRemoteName(t *testing.T) {
|
||||
want error
|
||||
}{
|
||||
{":remote:", nil},
|
||||
{":s3:", nil},
|
||||
{"remote:", nil},
|
||||
{"", errInvalidCharacters},
|
||||
{"rem:ote", errInvalidCharacters},
|
||||
@ -49,44 +60,267 @@ func TestCheckRemoteName(t *testing.T) {
|
||||
{"[remote:", errInvalidCharacters},
|
||||
{"*:", errInvalidCharacters},
|
||||
} {
|
||||
got := CheckRemoteName(test.in)
|
||||
got := checkRemoteName(test.in)
|
||||
assert.Equal(t, test.want, got, test.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in, wantConfigName, wantFsPath string
|
||||
wantErr error
|
||||
isDriveLetter = func(name string) bool {
|
||||
return name == "C"
|
||||
}
|
||||
defer func() {
|
||||
isDriveLetter = driveletter.IsDriveLetter
|
||||
}()
|
||||
|
||||
for testNumber, test := range []struct {
|
||||
in string
|
||||
wantParsed Parsed
|
||||
wantErr error
|
||||
}{
|
||||
{"", "", "", errCantBeEmpty},
|
||||
{":", "", "", errInvalidCharacters},
|
||||
{"::", ":", "", errInvalidCharacters},
|
||||
{":/:", "", "/:", errInvalidCharacters},
|
||||
{"/:", "", "/:", nil},
|
||||
{"\\backslash:", "", "\\backslash:", nil},
|
||||
{"/slash:", "", "/slash:", nil},
|
||||
{"with\\backslash:", "", "with\\backslash:", nil},
|
||||
{"with/slash:", "", "with/slash:", nil},
|
||||
{"/path/to/file", "", "/path/to/file", nil},
|
||||
{"/path:/to/file", "", "/path:/to/file", nil},
|
||||
{"./path:/to/file", "", "./path:/to/file", nil},
|
||||
{"./:colon.txt", "", "./:colon.txt", nil},
|
||||
{"path/to/file", "", "path/to/file", nil},
|
||||
{"remote:path/to/file", "remote", "path/to/file", nil},
|
||||
{"rem*ote:path/to/file", "rem*ote", "path/to/file", errInvalidCharacters},
|
||||
{"remote:/path/to/file", "remote", "/path/to/file", nil},
|
||||
{"rem.ote:/path/to/file", "rem.ote", "/path/to/file", errInvalidCharacters},
|
||||
{":backend:/path/to/file", ":backend", "/path/to/file", nil},
|
||||
{":bac*kend:/path/to/file", ":bac*kend", "/path/to/file", errInvalidCharacters},
|
||||
{
|
||||
in: "",
|
||||
wantErr: errCantBeEmpty,
|
||||
}, {
|
||||
in: ":",
|
||||
wantErr: errConfigName,
|
||||
}, {
|
||||
in: "::",
|
||||
wantErr: errConfigNameEmpty,
|
||||
}, {
|
||||
in: ":/:",
|
||||
wantErr: errInvalidCharacters,
|
||||
}, {
|
||||
in: "/:",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "/:",
|
||||
},
|
||||
}, {
|
||||
in: "\\backslash:",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "\\backslash:",
|
||||
},
|
||||
}, {
|
||||
in: "/slash:",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "/slash:",
|
||||
},
|
||||
}, {
|
||||
in: "with\\backslash:",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "with\\backslash:",
|
||||
},
|
||||
}, {
|
||||
in: "with/slash:",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "with/slash:",
|
||||
},
|
||||
}, {
|
||||
in: "/path/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "/path/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "/path:/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "/path:/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "./path:/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "./path:/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "./:colon.txt",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "./:colon.txt",
|
||||
},
|
||||
}, {
|
||||
in: "path/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "",
|
||||
Path: "path/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "remote:path/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "remote",
|
||||
Name: "remote",
|
||||
Path: "path/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "rem*ote:path/to/file",
|
||||
wantErr: errInvalidCharacters,
|
||||
}, {
|
||||
in: "remote:/path/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: "remote",
|
||||
Name: "remote",
|
||||
Path: "/path/to/file",
|
||||
},
|
||||
}, {
|
||||
in: "rem.ote:/path/to/file",
|
||||
wantErr: errInvalidCharacters,
|
||||
}, {
|
||||
in: ":backend:/path/to/file",
|
||||
wantParsed: Parsed{
|
||||
ConfigString: ":backend",
|
||||
Name: ":backend",
|
||||
Path: "/path/to/file",
|
||||
},
|
||||
}, {
|
||||
in: ":bac*kend:/path/to/file",
|
||||
wantErr: errInvalidCharacters,
|
||||
}, {
|
||||
in: `C:\path\to\file`,
|
||||
wantParsed: Parsed{
|
||||
Name: "",
|
||||
Path: `C:\path\to\file`,
|
||||
},
|
||||
}, {
|
||||
in: `remote:\path\to\file`,
|
||||
wantParsed: Parsed{
|
||||
Name: "remote",
|
||||
ConfigString: "remote",
|
||||
Path: `\path\to\file`,
|
||||
},
|
||||
}, {
|
||||
in: `D:/path/to/file`,
|
||||
wantParsed: Parsed{
|
||||
Name: "D",
|
||||
ConfigString: "D",
|
||||
Path: `/path/to/file`,
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param1:/path/to/file`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param1`,
|
||||
Name: ":backend",
|
||||
Path: "/path/to/file",
|
||||
Config: configmap.Simple{
|
||||
"param1": "true",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param1=value:/path/to/file`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param1=value`,
|
||||
Name: ":backend",
|
||||
Path: "/path/to/file",
|
||||
Config: configmap.Simple{
|
||||
"param1": "value",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param1=value1,param2,param3=value3:/path/to/file`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param1=value1,param2,param3=value3`,
|
||||
Name: ":backend",
|
||||
Path: "/path/to/file",
|
||||
Config: configmap.Simple{
|
||||
"param1": "value1",
|
||||
"param2": "true",
|
||||
"param3": "value3",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param1=value1,param2="value2",param3='value3':/path/to/file`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param1=value1,param2="value2",param3='value3'`,
|
||||
Name: ":backend",
|
||||
Path: "/path/to/file",
|
||||
Config: configmap.Simple{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
"param3": "value3",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param-1=value:/path/to/file`,
|
||||
wantErr: errBadConfigParam,
|
||||
}, {
|
||||
in: `:backend,param1="value"x:/path/to/file`,
|
||||
wantErr: errAfterQuote,
|
||||
}, {
|
||||
in: `:backend,`,
|
||||
wantErr: errParam,
|
||||
}, {
|
||||
in: `:backend,param=value`,
|
||||
wantErr: errValue,
|
||||
}, {
|
||||
in: `:backend,param="value'`,
|
||||
wantErr: errQuotedValue,
|
||||
}, {
|
||||
in: `:backend,param1="value"`,
|
||||
wantErr: errAfterQuote,
|
||||
}, {
|
||||
in: `:backend,=value:`,
|
||||
wantErr: errEmptyConfigParam,
|
||||
}, {
|
||||
in: `:backend,:`,
|
||||
wantErr: errEmptyConfigParam,
|
||||
}, {
|
||||
in: `:backend,,:`,
|
||||
wantErr: errEmptyConfigParam,
|
||||
}, {
|
||||
in: `:backend,param=:path`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param=`,
|
||||
Name: ":backend",
|
||||
Path: "path",
|
||||
Config: configmap.Simple{
|
||||
"param": "",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param="with""quote":path`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param="with""quote"`,
|
||||
Name: ":backend",
|
||||
Path: "path",
|
||||
Config: configmap.Simple{
|
||||
"param": `with"quote`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param='''''':`,
|
||||
wantParsed: Parsed{
|
||||
ConfigString: `:backend,param=''''''`,
|
||||
Name: ":backend",
|
||||
Path: "",
|
||||
Config: configmap.Simple{
|
||||
"param": `''`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
in: `:backend,param=''bad'':`,
|
||||
wantErr: errAfterQuote,
|
||||
},
|
||||
} {
|
||||
gotConfigName, gotFsPath, gotErr := Parse(test.in)
|
||||
if runtime.GOOS == "windows" {
|
||||
test.wantFsPath = strings.Replace(test.wantFsPath, `\`, `/`, -1)
|
||||
gotParsed, gotErr := Parse(test.in)
|
||||
// For non-local paths we convert \ into / on Windows
|
||||
if runtime.GOOS == "windows" && test.wantParsed.Name != "" {
|
||||
test.wantParsed.Path = strings.Replace(test.wantParsed.Path, `\`, `/`, -1)
|
||||
}
|
||||
assert.Equal(t, test.wantErr, gotErr)
|
||||
assert.Equal(t, test.wantConfigName, gotConfigName)
|
||||
assert.Equal(t, test.wantFsPath, gotFsPath)
|
||||
assert.Equal(t, test.wantErr, gotErr, test.in)
|
||||
if test.wantErr == nil {
|
||||
assert.Equal(t, test.wantParsed, gotParsed, test.in)
|
||||
}
|
||||
if *makeCorpus {
|
||||
// write the test corpus for fuzzing
|
||||
require.NoError(t, os.MkdirAll("corpus", 0777))
|
||||
require.NoError(t, ioutil.WriteFile(fmt.Sprintf("corpus/%02d", testNumber), []byte(test.in), 0666))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1474,8 +1474,9 @@ func Run(t *testing.T, opt *Opt) {
|
||||
t.Skip("Can't list from root on this remote")
|
||||
}
|
||||
|
||||
configName, configLeaf, err := fspath.Parse(subRemoteName)
|
||||
parsed, err := fspath.Parse(subRemoteName)
|
||||
require.NoError(t, err)
|
||||
configName, configLeaf := parsed.ConfigString, parsed.Path
|
||||
if configName == "" {
|
||||
configName, configLeaf = path.Split(subRemoteName)
|
||||
} else {
|
||||
|
@ -122,11 +122,11 @@ func Start(remoteName string) (fn func(), err error) {
|
||||
// don't start the local backend
|
||||
return func() {}, nil
|
||||
}
|
||||
var name string
|
||||
name, _, err = fspath.Parse(remoteName)
|
||||
parsed, err := fspath.Parse(remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := parsed.ConfigString
|
||||
if name == "" {
|
||||
// don't start the local backend
|
||||
return func() {}, nil
|
||||
|
Loading…
Reference in New Issue
Block a user