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")
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to parse remote %q to wrap", remote)
|
return nil, errors.Wrapf(err, "failed to parse remote %q to wrap", remote)
|
||||||
}
|
}
|
||||||
|
baseName, basePath := parsed.ConfigString, parsed.Path
|
||||||
if baseName != "" {
|
if baseName != "" {
|
||||||
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
|
To copy files and directories from `example.com` in the relative
|
||||||
directory `path/to/dir` to `/tmp/dir` using sftp.
|
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
|
### Valid remote names
|
||||||
|
|
||||||
- Remote names may only contain 0-9, A-Z ,a-z ,_ , - and space.
|
- 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 ###
|
### Config file ###
|
||||||
|
|
||||||
You can set defaults for values in the config file on an individual
|
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
|
remote basis. The names of the config items are documented in the page
|
||||||
discover the name of the config items that you want. The easiest way
|
for each backend.
|
||||||
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`).
|
|
||||||
|
|
||||||
To find the name of the environment variable, you need to set, take
|
To find the name of the environment variable, you need to set, take
|
||||||
`RCLONE_CONFIG_` + name of remote + `_` + name of config file option
|
`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
|
Note that if you want to create a remote using environment variables
|
||||||
you must create the `..._TYPE` variable as above.
|
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
|
### Precedence
|
||||||
|
|
||||||
The various different methods of backend configuration are read in
|
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
|
// ParseRemote deconstructs a path into configName, fsPath, looking up
|
||||||
// the fsName in the config file (returning NotFoundInConfigFile if not found)
|
// the fsName in the config file (returning NotFoundInConfigFile if not found)
|
||||||
func ParseRemote(path string) (fsInfo *RegInfo, configName, fsPath string, err error) {
|
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 {
|
if err != nil {
|
||||||
return nil, "", "", err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
|
configName, fsPath = parsed.Name, parsed.Path
|
||||||
|
// FIXME do something with parsed.Config
|
||||||
var fsName string
|
var fsName string
|
||||||
var ok bool
|
var ok bool
|
||||||
if configName != "" {
|
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"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/driveletter"
|
"github.com/rclone/rclone/fs/driveletter"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
configNameRe = `[\w_ -]+`
|
configNameRe = `[\w_ -]+`
|
||||||
remoteNameRe = `^(:?` + configNameRe + `):`
|
remoteNameRe = `^(:?` + configNameRe + `)`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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")
|
errCantBeEmpty = errors.New("can't use empty string as a path")
|
||||||
errCantStartWithDash = errors.New("config name starts with -")
|
errCantStartWithDash = errors.New("config name starts with `-`")
|
||||||
|
errBadConfigParam = errors.New("config parameters may only contain `0-9`, `A-Z`, `a-z` and `_`")
|
||||||
// urlMatcher is a pattern to match an rclone URL
|
errEmptyConfigParam = errors.New("config parameters can't be empty")
|
||||||
// note that this matches invalid remoteNames
|
errConfigNameEmpty = errors.New("config name can't be empty")
|
||||||
urlMatcher = regexp.MustCompile(`^(:?[^\\/:]*):(.*)$`)
|
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 is a pattern to match an rclone config name
|
||||||
configNameMatcher = regexp.MustCompile(`^` + configNameRe + `$`)
|
configNameMatcher = regexp.MustCompile(`^` + configNameRe + `$`)
|
||||||
|
|
||||||
// remoteNameMatcher is a pattern to match an rclone remote name
|
// remoteNameMatcher is a pattern to match an rclone remote name at the start of a config
|
||||||
remoteNameMatcher = regexp.MustCompile(remoteNameRe + `$`)
|
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
|
// CheckConfigName returns an error if configName is invalid
|
||||||
@ -44,41 +53,210 @@ func CheckConfigName(configName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRemoteName returns an error if remoteName is invalid
|
// checkRemoteName returns an error if remoteName is invalid
|
||||||
func CheckRemoteName(remoteName string) error {
|
func checkRemoteName(remoteName string) error {
|
||||||
|
if remoteName == ":" || remoteName == "::" {
|
||||||
|
return errConfigNameEmpty
|
||||||
|
}
|
||||||
if !remoteNameMatcher.MatchString(remoteName) {
|
if !remoteNameMatcher.MatchString(remoteName) {
|
||||||
return errInvalidCharacters
|
return errInvalidCharacters
|
||||||
}
|
}
|
||||||
return nil
|
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"
|
// Note that ConfigString + ":" + Path is equal to the input of Parse except that Path may have had
|
||||||
// and "/path/to/local" will return ("", "/path/to/local")
|
// \ 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
|
// Note that this will turn \ into / in the fsPath on Windows
|
||||||
//
|
//
|
||||||
// An error may be returned if the remote name has invalid characters
|
// An error may be returned if the remote name has invalid characters or the
|
||||||
// in it or if the path is empty.
|
// parameters are invalid or the path is empty.
|
||||||
func Parse(path string) (configName, fsPath string, err error) {
|
func Parse(path string) (parsed Parsed, err error) {
|
||||||
|
parsed.Path = path
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return "", "", errCantBeEmpty
|
return parsed, errCantBeEmpty
|
||||||
}
|
}
|
||||||
parts := urlMatcher.FindStringSubmatch(path)
|
// If path has no `:` in, it must be a local path
|
||||||
configName, fsPath = "", path
|
if strings.IndexRune(path, ':') < 0 {
|
||||||
if parts != nil && !driveletter.IsDriveLetter(parts[1]) {
|
return parsed, nil
|
||||||
configName, fsPath = parts[1], parts[2]
|
}
|
||||||
err = CheckRemoteName(configName + ":")
|
// States for parser
|
||||||
if err != nil {
|
const (
|
||||||
return configName, fsPath, errInvalidCharacters
|
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
|
// change native directory separators to / if there are any
|
||||||
fsPath = filepath.ToSlash(fsPath)
|
parsed.Path = filepath.ToSlash(parsed.Path)
|
||||||
return configName, fsPath, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split splits a remote into a parent and a leaf
|
// 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
|
// The returned values have the property that parent + leaf == remote
|
||||||
// (except under Windows where \ will be translated into /)
|
// (except under Windows where \ will be translated into /)
|
||||||
func Split(remote string) (parent string, leaf string, err error) {
|
func Split(remote string) (parent string, leaf string, err error) {
|
||||||
remoteName, remotePath, err := Parse(remote)
|
parsed, err := Parse(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
remoteName, remotePath := parsed.ConfigString, parsed.Path
|
||||||
if remoteName != "" {
|
if remoteName != "" {
|
||||||
remoteName += ":"
|
remoteName += ":"
|
||||||
}
|
}
|
||||||
@ -130,7 +309,8 @@ func JoinRootPath(remote, filePath string) string {
|
|||||||
if strings.HasPrefix(remote, "//") {
|
if strings.HasPrefix(remote, "//") {
|
||||||
return "/" + path.Join(remote, filePath)
|
return "/" + path.Join(remote, filePath)
|
||||||
}
|
}
|
||||||
remoteName, remotePath, err := Parse(remote)
|
parsed, err := Parse(remote)
|
||||||
|
remoteName, remotePath := parsed.ConfigString, parsed.Path
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Couldn't parse so assume it is a path
|
// Couldn't parse so assume it is a path
|
||||||
remoteName = ""
|
remoteName = ""
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
package fspath
|
package fspath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
|
"github.com/rclone/rclone/fs/driveletter"
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestCheckConfigName(t *testing.T) {
|
||||||
@ -39,6 +49,7 @@ func TestCheckRemoteName(t *testing.T) {
|
|||||||
want error
|
want error
|
||||||
}{
|
}{
|
||||||
{":remote:", nil},
|
{":remote:", nil},
|
||||||
|
{":s3:", nil},
|
||||||
{"remote:", nil},
|
{"remote:", nil},
|
||||||
{"", errInvalidCharacters},
|
{"", errInvalidCharacters},
|
||||||
{"rem:ote", errInvalidCharacters},
|
{"rem:ote", errInvalidCharacters},
|
||||||
@ -49,44 +60,267 @@ func TestCheckRemoteName(t *testing.T) {
|
|||||||
{"[remote:", errInvalidCharacters},
|
{"[remote:", errInvalidCharacters},
|
||||||
{"*:", errInvalidCharacters},
|
{"*:", errInvalidCharacters},
|
||||||
} {
|
} {
|
||||||
got := CheckRemoteName(test.in)
|
got := checkRemoteName(test.in)
|
||||||
assert.Equal(t, test.want, got, test.in)
|
assert.Equal(t, test.want, got, test.in)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
for _, test := range []struct {
|
isDriveLetter = func(name string) bool {
|
||||||
in, wantConfigName, wantFsPath string
|
return name == "C"
|
||||||
wantErr error
|
}
|
||||||
|
defer func() {
|
||||||
|
isDriveLetter = driveletter.IsDriveLetter
|
||||||
|
}()
|
||||||
|
|
||||||
|
for testNumber, test := range []struct {
|
||||||
|
in string
|
||||||
|
wantParsed Parsed
|
||||||
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{"", "", "", errCantBeEmpty},
|
{
|
||||||
{":", "", "", errInvalidCharacters},
|
in: "",
|
||||||
{"::", ":", "", errInvalidCharacters},
|
wantErr: errCantBeEmpty,
|
||||||
{":/:", "", "/:", errInvalidCharacters},
|
}, {
|
||||||
{"/:", "", "/:", nil},
|
in: ":",
|
||||||
{"\\backslash:", "", "\\backslash:", nil},
|
wantErr: errConfigName,
|
||||||
{"/slash:", "", "/slash:", nil},
|
}, {
|
||||||
{"with\\backslash:", "", "with\\backslash:", nil},
|
in: "::",
|
||||||
{"with/slash:", "", "with/slash:", nil},
|
wantErr: errConfigNameEmpty,
|
||||||
{"/path/to/file", "", "/path/to/file", nil},
|
}, {
|
||||||
{"/path:/to/file", "", "/path:/to/file", nil},
|
in: ":/:",
|
||||||
{"./path:/to/file", "", "./path:/to/file", nil},
|
wantErr: errInvalidCharacters,
|
||||||
{"./:colon.txt", "", "./:colon.txt", nil},
|
}, {
|
||||||
{"path/to/file", "", "path/to/file", nil},
|
in: "/:",
|
||||||
{"remote:path/to/file", "remote", "path/to/file", nil},
|
wantParsed: Parsed{
|
||||||
{"rem*ote:path/to/file", "rem*ote", "path/to/file", errInvalidCharacters},
|
ConfigString: "",
|
||||||
{"remote:/path/to/file", "remote", "/path/to/file", nil},
|
Path: "/:",
|
||||||
{"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: "\\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)
|
gotParsed, gotErr := Parse(test.in)
|
||||||
if runtime.GOOS == "windows" {
|
// For non-local paths we convert \ into / on Windows
|
||||||
test.wantFsPath = strings.Replace(test.wantFsPath, `\`, `/`, -1)
|
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.wantErr, gotErr, test.in)
|
||||||
assert.Equal(t, test.wantConfigName, gotConfigName)
|
if test.wantErr == nil {
|
||||||
assert.Equal(t, test.wantFsPath, gotFsPath)
|
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")
|
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)
|
require.NoError(t, err)
|
||||||
|
configName, configLeaf := parsed.ConfigString, parsed.Path
|
||||||
if configName == "" {
|
if configName == "" {
|
||||||
configName, configLeaf = path.Split(subRemoteName)
|
configName, configLeaf = path.Split(subRemoteName)
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,11 +122,11 @@ func Start(remoteName string) (fn func(), err error) {
|
|||||||
// don't start the local backend
|
// don't start the local backend
|
||||||
return func() {}, nil
|
return func() {}, nil
|
||||||
}
|
}
|
||||||
var name string
|
parsed, err := fspath.Parse(remoteName)
|
||||||
name, _, err = fspath.Parse(remoteName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
name := parsed.ConfigString
|
||||||
if name == "" {
|
if name == "" {
|
||||||
// don't start the local backend
|
// don't start the local backend
|
||||||
return func() {}, nil
|
return func() {}, nil
|
||||||
|
Loading…
Reference in New Issue
Block a user