rclone/fs/mount_helper.go

288 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] == '-' {
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
}