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

     1  package fs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  )
    12  
    13  func init() {
    14  	// This block is run super-early, before configuration harness kick in
    15  	if IsMountHelper() {
    16  		if args, err := convertMountHelperArgs(os.Args); err == nil {
    17  			os.Args = args
    18  		} else {
    19  			log.Fatalf("Failed to parse command line: %v", err)
    20  		}
    21  	}
    22  }
    23  
    24  // PassDaemonArgsAsEnviron tells how CLI arguments are passed to the daemon
    25  // When false, arguments are passed as is, visible in the `ps` output.
    26  // When true, arguments are converted into environment variables (more secure).
    27  var PassDaemonArgsAsEnviron bool
    28  
    29  // Comma-separated list of mount options to ignore.
    30  // Leading and trailing commas are required.
    31  const helperIgnoredOpts = ",rw,_netdev,nofail,user,dev,nodev,suid,nosuid,exec,noexec,auto,noauto,"
    32  
    33  // Valid option name characters
    34  const helperValidOptChars = "-_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    35  
    36  // Parser errors
    37  var (
    38  	errHelperBadOption    = errors.New("option names may only contain `0-9`, `A-Z`, `a-z`, `-` and `_`")
    39  	errHelperOptionName   = errors.New("option name can't start with `-` or `_`")
    40  	errHelperEmptyOption  = errors.New("option name can't be empty")
    41  	errHelperQuotedValue  = errors.New("unterminated quoted value")
    42  	errHelperAfterQuote   = errors.New("expecting `,` or another quote after a quote")
    43  	errHelperSyntax       = errors.New("syntax error in option string")
    44  	errHelperEmptyCommand = errors.New("command name can't be empty")
    45  	errHelperEnvSyntax    = errors.New("environment variable must have syntax env.NAME=[VALUE]")
    46  )
    47  
    48  // IsMountHelper returns true if rclone was invoked as mount helper:
    49  // as /sbin/mount.rlone (by /bin/mount)
    50  // or /usr/bin/rclonefs (by fusermount or directly)
    51  func IsMountHelper() bool {
    52  	if runtime.GOOS == "windows" {
    53  		return false
    54  	}
    55  	me := filepath.Base(os.Args[0])
    56  	return me == "mount.rclone" || me == "rclonefs"
    57  }
    58  
    59  // convertMountHelperArgs converts "-o" styled mount helper arguments
    60  // into usual rclone flags
    61  func convertMountHelperArgs(origArgs []string) ([]string, error) {
    62  	if IsDaemon() {
    63  		// The arguments have already been converted by the parent
    64  		return origArgs, nil
    65  	}
    66  
    67  	args := []string{}
    68  	command := "mount"
    69  	parseOpts := false
    70  	gotDaemon := false
    71  	gotVerbose := false
    72  	vCount := 0
    73  
    74  	for _, arg := range origArgs[1:] {
    75  		if !parseOpts {
    76  			switch arg {
    77  			case "-o", "--opt":
    78  				parseOpts = true
    79  			case "-v", "-vv", "-vvv", "-vvvv":
    80  				vCount += len(arg) - 1
    81  			case "-h", "--help":
    82  				args = append(args, "--help")
    83  			default:
    84  				if strings.HasPrefix(arg, "-") {
    85  					return nil, fmt.Errorf("flag %q is not supported in mount mode", arg)
    86  				}
    87  				args = append(args, arg)
    88  			}
    89  			continue
    90  		}
    91  
    92  		opts, err := parseHelperOptionString(arg)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		parseOpts = false
    97  
    98  		for _, opt := range opts {
    99  			if strings.Contains(helperIgnoredOpts, ","+opt+",") || strings.HasPrefix(opt, "x-systemd") {
   100  				continue
   101  			}
   102  
   103  			param, value := opt, ""
   104  			if idx := strings.Index(opt, "="); idx != -1 {
   105  				param, value = opt[:idx], opt[idx+1:]
   106  			}
   107  
   108  			// Set environment variables
   109  			if strings.HasPrefix(param, "env.") {
   110  				if param = param[4:]; param == "" {
   111  					return nil, errHelperEnvSyntax
   112  				}
   113  				_ = os.Setenv(param, value)
   114  				continue
   115  			}
   116  
   117  			switch param {
   118  			// Change command to run
   119  			case "command":
   120  				if value == "" {
   121  					return nil, errHelperEmptyCommand
   122  				}
   123  				command = value
   124  				continue
   125  			// Flag StartDaemon to pass arguments as environment
   126  			case "args2env":
   127  				PassDaemonArgsAsEnviron = true
   128  				continue
   129  			// Handle verbosity options
   130  			case "v", "vv", "vvv", "vvvv":
   131  				vCount += len(param)
   132  				continue
   133  			case "verbose":
   134  				gotVerbose = true
   135  			// Don't add --daemon if it was explicitly included
   136  			case "daemon":
   137  				gotDaemon = true
   138  			// Alias for the standard mount option "ro"
   139  			case "ro":
   140  				param = "read-only"
   141  			}
   142  
   143  			arg = "--" + strings.ToLower(strings.ReplaceAll(param, "_", "-"))
   144  			if value != "" {
   145  				arg += "=" + value
   146  			}
   147  			args = append(args, arg)
   148  		}
   149  	}
   150  	if parseOpts {
   151  		return nil, fmt.Errorf("dangling -o without argument")
   152  	}
   153  
   154  	if vCount > 0 && !gotVerbose {
   155  		args = append(args, fmt.Sprintf("--verbose=%d", vCount))
   156  	}
   157  	if strings.Contains(command, "mount") && !gotDaemon {
   158  		// Default to daemonized mount
   159  		args = append(args, "--daemon")
   160  	}
   161  	if len(args) > 0 && args[0] == command {
   162  		// Remove artefact of repeated conversion
   163  		args = args[1:]
   164  	}
   165  	prepend := []string{origArgs[0], command}
   166  	return append(prepend, args...), nil
   167  }
   168  
   169  // parseHelperOptionString deconstructs the -o value into slice of options
   170  // in a way similar to connection strings.
   171  // Example:
   172  //
   173  //	param1=value,param2="qvalue",param3='item1,item2',param4="a ""b"" 'c'"
   174  //
   175  // An error may be returned if the remote name has invalid characters
   176  // or the parameters are invalid or the path is empty.
   177  //
   178  // The algorithm was adapted from fspath.Parse with some modifications:
   179  // - allow `-` in option names
   180  // - handle special options `x-systemd.X` and `env.X`
   181  // - drop support for :backend: and /path
   182  func parseHelperOptionString(optString string) (opts []string, err error) {
   183  	if optString = strings.TrimSpace(optString); optString == "" {
   184  		return nil, nil
   185  	}
   186  	// States for parser
   187  	const (
   188  		stateParam = uint8(iota)
   189  		stateValue
   190  		stateQuotedValue
   191  		stateAfterQuote
   192  		stateDone
   193  	)
   194  	var (
   195  		state   = stateParam // current state of parser
   196  		i       int          // position in path
   197  		prev    int          // previous position in path
   198  		c       rune         // current rune under consideration
   199  		quote   rune         // kind of quote to end this quoted string
   200  		param   string       // current parameter value
   201  		doubled bool         // set if had doubled quotes
   202  	)
   203  	for i, c = range optString + "," {
   204  		switch state {
   205  		// Parses param= and param2=
   206  		case stateParam:
   207  			switch c {
   208  			case ',', '=':
   209  				param = optString[prev:i]
   210  				if len(param) == 0 {
   211  					return nil, errHelperEmptyOption
   212  				}
   213  				if param[0] == '-' {
   214  					return nil, errHelperOptionName
   215  				}
   216  				prev = i + 1
   217  				if c == '=' {
   218  					state = stateValue
   219  					break
   220  				}
   221  				opts = append(opts, param)
   222  			case '.':
   223  				if pref := optString[prev:i]; pref != "env" && pref != "x-systemd" {
   224  					return nil, errHelperBadOption
   225  				}
   226  			default:
   227  				if !strings.ContainsRune(helperValidOptChars, c) {
   228  					return nil, errHelperBadOption
   229  				}
   230  			}
   231  		case stateValue:
   232  			switch c {
   233  			case '\'', '"':
   234  				if i == prev {
   235  					quote = c
   236  					prev = i + 1
   237  					doubled = false
   238  					state = stateQuotedValue
   239  				}
   240  			case ',':
   241  				value := optString[prev:i]
   242  				prev = i + 1
   243  				opts = append(opts, param+"="+value)
   244  				state = stateParam
   245  			}
   246  		case stateQuotedValue:
   247  			if c == quote {
   248  				state = stateAfterQuote
   249  			}
   250  		case stateAfterQuote:
   251  			switch c {
   252  			case ',':
   253  				value := optString[prev : i-1]
   254  				// replace any doubled quotes if there were any
   255  				if doubled {
   256  					value = strings.ReplaceAll(value, string(quote)+string(quote), string(quote))
   257  				}
   258  				prev = i + 1
   259  				opts = append(opts, param+"="+value)
   260  				state = stateParam
   261  			case quote:
   262  				// Here is a doubled quote to indicate a literal quote
   263  				state = stateQuotedValue
   264  				doubled = true
   265  			default:
   266  				return nil, errHelperAfterQuote
   267  			}
   268  		}
   269  	}
   270  
   271  	// Depending on which state we were in when we fell off the
   272  	// end of the state machine we can return a sensible error.
   273  	if state == stateParam && prev > len(optString) {
   274  		state = stateDone
   275  	}
   276  	switch state {
   277  	case stateQuotedValue:
   278  		return nil, errHelperQuotedValue
   279  	case stateAfterQuote:
   280  		return nil, errHelperAfterQuote
   281  	case stateDone:
   282  		break
   283  	default:
   284  		return nil, errHelperSyntax
   285  	}
   286  	return opts, nil
   287  }