rclone/fs/mount_helper.go
Nick Craig-Wood e43b5ce5e5 Remove github.com/pkg/errors and replace with std library version
This is possible now that we no longer support go1.12 and brings
rclone into line with standard practices in the Go world.

This also removes errors.New and errors.Errorf from lib/errors and
prefers the stdlib errors package over lib/errors.
2021-11-07 11:53:30 +00:00

285 lines
7.5 KiB
Go

package fs
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
)
func init() {
// This block is run super-early, before configuration harness kick in
if IsMountHelper() {
if args, err := convertMountHelperArgs(os.Args); err == nil {
os.Args = args
} else {
log.Fatalf("Failed to parse command line: %v", err)
}
}
}
// PassDaemonArgsAsEnviron tells how CLI arguments are passed to the daemon
// When false, arguments are passed as is, visible in the `ps` output.
// When true, arguments are converted into environment variables (more secure).
var PassDaemonArgsAsEnviron bool
// Comma-separated list of mount options to ignore.
// Leading and trailing commas are required.
const helperIgnoredOpts = ",rw,_netdev,nofail,user,dev,nodev,suid,nosuid,exec,noexec,auto,noauto,"
// Valid option name characters
const helperValidOptChars = "-_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// Parser errors
var (
errHelperBadOption = errors.New("option names may only contain `0-9`, `A-Z`, `a-z`, `-` and `_`")
errHelperOptionName = errors.New("option name can't start with `-` or `_`")
errHelperEmptyOption = errors.New("option name can't be empty")
errHelperQuotedValue = errors.New("unterminated quoted value")
errHelperAfterQuote = errors.New("expecting `,` or another quote after a quote")
errHelperSyntax = errors.New("syntax error in option string")
errHelperEmptyCommand = errors.New("command name can't be empty")
errHelperEnvSyntax = errors.New("environment variable must have syntax env.NAME=[VALUE]")
)
// IsMountHelper returns true if rclone was invoked as mount helper:
// as /sbin/mount.rlone (by /bin/mount)
// or /usr/bin/rclonefs (by fusermount or directly)
func IsMountHelper() bool {
if runtime.GOOS == "windows" {
return false
}
me := filepath.Base(os.Args[0])
return me == "mount.rclone" || me == "rclonefs"
}
// convertMountHelperArgs converts "-o" styled mount helper arguments
// into usual rclone flags
func convertMountHelperArgs(origArgs []string) ([]string, error) {
if IsDaemon() {
// The arguments have already been converted by the parent
return origArgs, nil
}
args := []string{}
command := "mount"
parseOpts := false
gotDaemon := false
gotVerbose := false
vCount := 0
for _, arg := range origArgs[1:] {
if !parseOpts {
switch arg {
case "-o", "--opt":
parseOpts = true
case "-v", "-vv", "-vvv", "-vvvv":
vCount += len(arg) - 1
case "-h", "--help":
args = append(args, "--help")
default:
if strings.HasPrefix(arg, "-") {
return nil, fmt.Errorf("flag %q is not supported in mount mode", arg)
}
args = append(args, arg)
}
continue
}
opts, err := parseHelperOptionString(arg)
if err != nil {
return nil, err
}
parseOpts = false
for _, opt := range opts {
if strings.Contains(helperIgnoredOpts, ","+opt+",") || strings.HasPrefix(opt, "x-systemd") {
continue
}
param, value := opt, ""
if idx := strings.Index(opt, "="); idx != -1 {
param, value = opt[:idx], opt[idx+1:]
}
// Set environment variables
if strings.HasPrefix(param, "env.") {
if param = param[4:]; param == "" {
return nil, errHelperEnvSyntax
}
_ = os.Setenv(param, value)
continue
}
switch param {
// Change command to run
case "command":
if value == "" {
return nil, errHelperEmptyCommand
}
command = value
continue
// Flag StartDaemon to pass arguments as environment
case "args2env":
PassDaemonArgsAsEnviron = true
continue
// Handle verbosity options
case "v", "vv", "vvv", "vvvv":
vCount += len(param)
continue
case "verbose":
gotVerbose = true
// Don't add --daemon if it was explicitly included
case "daemon":
gotDaemon = true
// Alias for the standard mount option "ro"
case "ro":
param = "read-only"
}
arg = "--" + strings.ToLower(strings.ReplaceAll(param, "_", "-"))
if value != "" {
arg += "=" + value
}
args = append(args, arg)
}
}
if parseOpts {
return nil, fmt.Errorf("dangling -o without argument")
}
if vCount > 0 && !gotVerbose {
args = append(args, fmt.Sprintf("--verbose=%d", vCount))
}
if strings.Contains(command, "mount") && !gotDaemon {
// Default to daemonized mount
args = append(args, "--daemon")
}
if len(args) > 0 && args[0] == command {
// Remove artefact of repeated conversion
args = args[1:]
}
prepend := []string{origArgs[0], command}
return append(prepend, args...), nil
}
// parseHelperOptionString deconstructs the -o value into slice of options
// in a way similar to connection strings.
// Example:
// param1=value,param2="qvalue",param3='item1,item2',param4="a ""b"" 'c'"
// An error may be returned if the remote name has invalid characters
// or the parameters are invalid or the path is empty.
//
// The algorithm was adapted from fspath.Parse with some modifications:
// - allow `-` in option names
// - handle special options `x-systemd.X` and `env.X`
// - drop support for :backend: and /path
func parseHelperOptionString(optString string) (opts []string, err error) {
if optString = strings.TrimSpace(optString); optString == "" {
return nil, nil
}
// States for parser
const (
stateParam = uint8(iota)
stateValue
stateQuotedValue
stateAfterQuote
stateDone
)
var (
state = stateParam // 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
)
for i, c = range optString + "," {
switch state {
// Parses param= and param2=
case stateParam:
switch c {
case ',', '=':
param = optString[prev:i]
if len(param) == 0 {
return nil, errHelperEmptyOption
}
if param[0] == '-' || param[0] == '_' {
return nil, errHelperOptionName
}
prev = i + 1
if c == '=' {
state = stateValue
break
}
opts = append(opts, param)
case '.':
if pref := optString[prev:i]; pref != "env" && pref != "x-systemd" {
return nil, errHelperBadOption
}
default:
if !strings.ContainsRune(helperValidOptChars, c) {
return nil, errHelperBadOption
}
}
case stateValue:
switch c {
case '\'', '"':
if i == prev {
quote = c
prev = i + 1
doubled = false
state = stateQuotedValue
}
case ',':
value := optString[prev:i]
prev = i + 1
opts = append(opts, param+"="+value)
state = stateParam
}
case stateQuotedValue:
if c == quote {
state = stateAfterQuote
}
case stateAfterQuote:
switch c {
case ',':
value := optString[prev : i-1]
// replace any doubled quotes if there were any
if doubled {
value = strings.ReplaceAll(value, string(quote)+string(quote), string(quote))
}
prev = i + 1
opts = append(opts, param+"="+value)
state = stateParam
case quote:
// Here is a doubled quote to indicate a literal quote
state = stateQuotedValue
doubled = true
default:
return nil, errHelperAfterQuote
}
}
}
// Depending on which state we were in when we fell off the
// end of the state machine we can return a sensible error.
if state == stateParam && prev > len(optString) {
state = stateDone
}
switch state {
case stateQuotedValue:
return nil, errHelperQuotedValue
case stateAfterQuote:
return nil, errHelperAfterQuote
case stateDone:
break
default:
return nil, errHelperSyntax
}
return opts, nil
}