diff --git a/backend/chunker/chunker.go b/backend/chunker/chunker.go index 11067ef24..72338c723 100644 --- a/backend/chunker/chunker.go +++ b/backend/chunker/chunker.go @@ -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 += ":" } diff --git a/docs/content/docs.md b/docs/content/docs.md index 5a1b2ff3c..24d60f22b 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -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 diff --git a/fs/fs.go b/fs/fs.go index 154e6b6ef..18f5e27be 100644 --- a/fs/fs.go +++ b/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 != "" { diff --git a/fs/fspath/fuzz.go b/fs/fspath/fuzz.go new file mode 100644 index 000000000..a03bc0ee2 --- /dev/null +++ b/fs/fspath/fuzz.go @@ -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 +} diff --git a/fs/fspath/path.go b/fs/fspath/path.go index a6a4843a4..8c76aeae1 100644 --- a/fs/fspath/path.go +++ b/fs/fspath/path.go @@ -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 = "" diff --git a/fs/fspath/path_test.go b/fs/fspath/path_test.go index 39a3058d6..d9c846153 100644 --- a/fs/fspath/path_test.go +++ b/fs/fspath/path_test.go @@ -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)) + } + } } diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go index 672c2a55b..6ddf1ca54 100644 --- a/fstest/fstests/fstests.go +++ b/fstest/fstests/fstests.go @@ -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 { diff --git a/fstest/testserver/testserver.go b/fstest/testserver/testserver.go index 8026b6d6e..bdcf05131 100644 --- a/fstest/testserver/testserver.go +++ b/fstest/testserver/testserver.go @@ -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