github.com/divyam234/rclone@v1.64.1/fs/fspath/path.go (about)

     1  // Package fspath contains routines for fspath manipulation
     2  package fspath
     3  
     4  import (
     5  	"errors"
     6  	"path"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/divyam234/rclone/fs/config/configmap"
    12  	"github.com/divyam234/rclone/fs/driveletter"
    13  )
    14  
    15  const (
    16  	configNameRe              = `[\w\p{L}\p{N}.+@]+(?:[ -]+[\w\p{L}\p{N}.+@-]+)*` // May contain Unicode numbers and letters, as well as `_` (covered by \w), `-`, `.`, `+`, `@` and space, but not start with `-` (it complicates usage, see #4261) or space, and not end with space
    17  	illegalPartOfConfigNameRe = `^[ -]+|[^\w\p{L}\p{N}.+@ -]+|[ ]+$`
    18  )
    19  
    20  var (
    21  	errInvalidCharacters = errors.New("config name contains invalid characters - may only contain numbers, letters, `_`, `-`, `.`, `+`, `@` and space, while not start with `-` or space, and not end with space")
    22  	errCantBeEmpty       = errors.New("can't use empty string as a path")
    23  	errBadConfigParam    = errors.New("config parameters may only contain `0-9`, `A-Z`, `a-z` and `_`")
    24  	errEmptyConfigParam  = errors.New("config parameters can't be empty")
    25  	errConfigNameEmpty   = errors.New("config name can't be empty")
    26  	errConfigName        = errors.New("config name needs a trailing `:`")
    27  	errParam             = errors.New("config parameter must end with `,` or `:`")
    28  	errValue             = errors.New("unquoted config value must end with `,` or `:`")
    29  	errQuotedValue       = errors.New("unterminated quoted config value")
    30  	errAfterQuote        = errors.New("expecting `:` or `,` or another quote after a quote")
    31  	errSyntax            = errors.New("syntax error in config string")
    32  
    33  	// configNameMatcher is a pattern to match an rclone config name
    34  	configNameMatcher = regexp.MustCompile(`^` + configNameRe + `$`)
    35  
    36  	// illegalPartOfConfigNameMatcher is a pattern to match a sequence of characters not allowed in an rclone config name
    37  	illegalPartOfConfigNameMatcher = regexp.MustCompile(illegalPartOfConfigNameRe)
    38  
    39  	// remoteNameMatcher is a pattern to match an rclone remote name at the start of a config
    40  	remoteNameMatcher = regexp.MustCompile(`^:?` + configNameRe + `(?::$|,)`)
    41  )
    42  
    43  // CheckConfigName returns an error if configName is invalid
    44  func CheckConfigName(configName string) error {
    45  	if !configNameMatcher.MatchString(configName) {
    46  		return errInvalidCharacters
    47  	}
    48  	return nil
    49  }
    50  
    51  // MakeConfigName makes an input into something legal to be used as a config name.
    52  // Returns a string where any sequences of illegal characters are replaced with
    53  // a single underscore. If the input is already valid as a config name, it is
    54  // returned unchanged. If the input is an empty string, a single underscore is
    55  // returned.
    56  func MakeConfigName(name string) string {
    57  	if name == "" {
    58  		return "_"
    59  	}
    60  	if configNameMatcher.MatchString(name) {
    61  		return name
    62  	}
    63  	return illegalPartOfConfigNameMatcher.ReplaceAllString(name, "_")
    64  }
    65  
    66  // checkRemoteName returns an error if remoteName is invalid
    67  func checkRemoteName(remoteName string) error {
    68  	if remoteName == ":" || remoteName == "::" {
    69  		return errConfigNameEmpty
    70  	}
    71  	if !remoteNameMatcher.MatchString(remoteName) {
    72  		return errInvalidCharacters
    73  	}
    74  	return nil
    75  }
    76  
    77  // Return true if c is a valid character for a config parameter
    78  func isConfigParam(c rune) bool {
    79  	return ((c >= 'a' && c <= 'z') ||
    80  		(c >= 'A' && c <= 'Z') ||
    81  		(c >= '0' && c <= '9') ||
    82  		c == '_')
    83  }
    84  
    85  // Parsed is returned from Parse with the results of the connection string decomposition
    86  //
    87  // If Name is "" then it is a local path in Path
    88  //
    89  // Note that ConfigString + ":" + Path is equal to the input of Parse except that Path may have had
    90  // \ converted to /
    91  type Parsed struct {
    92  	Name         string           // Just the name of the config: "remote" or ":backend"
    93  	ConfigString string           // The whole config string: "remote:" or ":backend,value=6:"
    94  	Path         string           // The file system path, may be empty
    95  	Config       configmap.Simple // key/value config parsed out of ConfigString may be nil
    96  }
    97  
    98  // Parse deconstructs a path into a Parsed structure
    99  //
   100  // If the path is a local path then parsed.Name will be returned as "".
   101  //
   102  // So "remote:path/to/dir" will return Parsed{Name:"remote", Path:"path/to/dir"},
   103  // and "/path/to/local" will return Parsed{Name:"", Path:"/path/to/local"}
   104  //
   105  // Note that this will turn \ into / in the fsPath on Windows
   106  //
   107  // An error may be returned if the remote name has invalid characters or the
   108  // parameters are invalid or the path is empty.
   109  func Parse(path string) (parsed Parsed, err error) {
   110  	parsed.Path = filepath.ToSlash(path)
   111  	if path == "" {
   112  		return parsed, errCantBeEmpty
   113  	}
   114  	// If path has no `:` in, it must be a local path
   115  	if !strings.ContainsRune(path, ':') {
   116  		return parsed, nil
   117  	}
   118  	// States for parser
   119  	const (
   120  		stateConfigName = uint8(iota)
   121  		stateParam
   122  		stateValue
   123  		stateQuotedValue
   124  		stateAfterQuote
   125  		stateDone
   126  	)
   127  	var (
   128  		state   = stateConfigName // current state of parser
   129  		i       int               // position in path
   130  		prev    int               // previous position in path
   131  		c       rune              // current rune under consideration
   132  		quote   rune              // kind of quote to end this quoted string
   133  		param   string            // current parameter value
   134  		doubled bool              // set if had doubled quotes
   135  	)
   136  loop:
   137  	for i, c = range path {
   138  		// Example Parse
   139  		// remote,param=value,param2="qvalue":/path/to/file
   140  		switch state {
   141  		// Parses "remote,"
   142  		case stateConfigName:
   143  			if i == 0 && c == ':' {
   144  				continue
   145  			} else if c == '/' || c == '\\' {
   146  				// `:` or `,` not before a path separator must be a local path,
   147  				// except if the path started with `:` in which case it was intended
   148  				// to be an on the fly remote so return an error.
   149  				if path[0] == ':' {
   150  					return parsed, errInvalidCharacters
   151  				}
   152  				return parsed, nil
   153  			} else if c == ':' || c == ',' {
   154  				parsed.Name = path[:i]
   155  				err := checkRemoteName(parsed.Name + ":")
   156  				if err != nil {
   157  					return parsed, err
   158  				}
   159  				prev = i + 1
   160  				if c == ':' {
   161  					// If we parsed a drive letter, must be a local path
   162  					if driveletter.IsDriveLetter(parsed.Name) {
   163  						parsed.Name = ""
   164  						return parsed, nil
   165  					}
   166  					state = stateDone
   167  					break loop
   168  				}
   169  				state = stateParam
   170  				parsed.Config = make(configmap.Simple)
   171  			}
   172  		// Parses param= and param2=
   173  		case stateParam:
   174  			if c == ':' || c == ',' || c == '=' {
   175  				param = path[prev:i]
   176  				if len(param) == 0 {
   177  					return parsed, errEmptyConfigParam
   178  				}
   179  				prev = i + 1
   180  				if c == '=' {
   181  					state = stateValue
   182  					break
   183  				}
   184  				parsed.Config[param] = "true"
   185  				if c == ':' {
   186  					state = stateDone
   187  					break loop
   188  				}
   189  				state = stateParam
   190  			} else if !isConfigParam(c) {
   191  				return parsed, errBadConfigParam
   192  			}
   193  		// Parses value
   194  		case stateValue:
   195  			if c == '\'' || c == '"' {
   196  				if i == prev {
   197  					quote = c
   198  					state = stateQuotedValue
   199  					prev = i + 1
   200  					doubled = false
   201  					break
   202  				}
   203  			} else if c == ':' || c == ',' {
   204  				value := path[prev:i]
   205  				prev = i + 1
   206  				parsed.Config[param] = value
   207  				if c == ':' {
   208  					state = stateDone
   209  					break loop
   210  				}
   211  				state = stateParam
   212  			}
   213  		// Parses "qvalue"
   214  		case stateQuotedValue:
   215  			if c == quote {
   216  				state = stateAfterQuote
   217  			}
   218  		// Parses : or , or quote after "qvalue"
   219  		case stateAfterQuote:
   220  			if c == ':' || c == ',' {
   221  				value := path[prev : i-1]
   222  				// replace any doubled quotes if there were any
   223  				if doubled {
   224  					value = strings.ReplaceAll(value, string(quote)+string(quote), string(quote))
   225  				}
   226  				prev = i + 1
   227  				parsed.Config[param] = value
   228  				if c == ':' {
   229  					state = stateDone
   230  					break loop
   231  				} else {
   232  					state = stateParam
   233  				}
   234  			} else if c == quote {
   235  				// Here is a doubled quote to indicate a literal quote
   236  				state = stateQuotedValue
   237  				doubled = true
   238  			} else {
   239  				return parsed, errAfterQuote
   240  			}
   241  		}
   242  
   243  	}
   244  
   245  	// Depending on which state we were in when we fell off the
   246  	// end of the state machine we can return a sensible error.
   247  	switch state {
   248  	default:
   249  		return parsed, errSyntax
   250  	case stateConfigName:
   251  		return parsed, errConfigName
   252  	case stateParam:
   253  		return parsed, errParam
   254  	case stateValue:
   255  		return parsed, errValue
   256  	case stateQuotedValue:
   257  		return parsed, errQuotedValue
   258  	case stateAfterQuote:
   259  		return parsed, errAfterQuote
   260  	case stateDone:
   261  		break
   262  	}
   263  
   264  	parsed.ConfigString = path[:i]
   265  	parsed.Path = path[i+1:]
   266  
   267  	// change native directory separators to / if there are any
   268  	parsed.Path = filepath.ToSlash(parsed.Path)
   269  	return parsed, nil
   270  }
   271  
   272  // SplitFs splits a remote a remoteName and an remotePath.
   273  //
   274  // SplitFs("remote:path/to/file") -> ("remote:", "path/to/file")
   275  // SplitFs("/to/file") -> ("", "/to/file")
   276  //
   277  // If it returns remoteName as "" then remotePath is a local path
   278  //
   279  // The returned values have the property that remoteName + remotePath ==
   280  // remote (except under Windows where \ will be translated into /)
   281  func SplitFs(remote string) (remoteName string, remotePath string, err error) {
   282  	parsed, err := Parse(remote)
   283  	if err != nil {
   284  		return "", "", err
   285  	}
   286  	remoteName, remotePath = parsed.ConfigString, parsed.Path
   287  	if remoteName != "" {
   288  		remoteName += ":"
   289  	}
   290  	return remoteName, remotePath, nil
   291  }
   292  
   293  // Split splits a remote into a parent and a leaf
   294  //
   295  // if it returns leaf as an empty string then remote is a directory
   296  //
   297  // if it returns parent as an empty string then that means the current directory
   298  //
   299  // The returned values have the property that parent + leaf == remote
   300  // (except under Windows where \ will be translated into /)
   301  func Split(remote string) (parent string, leaf string, err error) {
   302  	remoteName, remotePath, err := SplitFs(remote)
   303  	if err != nil {
   304  		return "", "", err
   305  	}
   306  	// Construct new remote name without last segment
   307  	parent, leaf = path.Split(remotePath)
   308  	return remoteName + parent, leaf, nil
   309  }
   310  
   311  // Make filePath absolute so it can't read above the root
   312  func makeAbsolute(filePath string) string {
   313  	leadingSlash := strings.HasPrefix(filePath, "/")
   314  	filePath = path.Join("/", filePath)
   315  	if !leadingSlash && strings.HasPrefix(filePath, "/") {
   316  		filePath = filePath[1:]
   317  	}
   318  	return filePath
   319  }
   320  
   321  // JoinRootPath joins filePath onto remote
   322  //
   323  // If the remote has a leading "//" this is preserved to allow Windows
   324  // network paths to be used as remotes.
   325  //
   326  // If filePath is empty then remote will be returned.
   327  //
   328  // If the path contains \ these will be converted to / on Windows.
   329  func JoinRootPath(remote, filePath string) string {
   330  	remote = filepath.ToSlash(remote)
   331  	if filePath == "" {
   332  		return remote
   333  	}
   334  	filePath = filepath.ToSlash(filePath)
   335  	filePath = makeAbsolute(filePath)
   336  	if strings.HasPrefix(remote, "//") {
   337  		return "/" + path.Join(remote, filePath)
   338  	}
   339  	parsed, err := Parse(remote)
   340  	remoteName, remotePath := parsed.ConfigString, parsed.Path
   341  	if err != nil {
   342  		// Couldn't parse so assume it is a path
   343  		remoteName = ""
   344  		remotePath = remote
   345  	}
   346  	remotePath = path.Join(remotePath, filePath)
   347  	if remoteName != "" {
   348  		remoteName += ":"
   349  		// if have remote: then normalise the remotePath
   350  		if remotePath == "." {
   351  			remotePath = ""
   352  		}
   353  	}
   354  	return remoteName + remotePath
   355  }