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 }