diff --git a/cmd/cmount/mountpoint_other.go b/cmd/cmount/mountpoint_other.go index 0e69487d1..3666f9272 100644 --- a/cmd/cmount/mountpoint_other.go +++ b/cmd/cmount/mountpoint_other.go @@ -26,5 +26,6 @@ func getMountpoint(f fs.Fs, mountPath string, opt *mountlib.Options) (string, er if err = mountlib.CheckAllowNonEmpty(mountPath, opt); err != nil { return "", err } + opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName) return mountPath, nil } diff --git a/cmd/cmount/mountpoint_windows.go b/cmd/cmount/mountpoint_windows.go index 050afd6b4..715b4dd1d 100644 --- a/cmd/cmount/mountpoint_windows.go +++ b/cmd/cmount/mountpoint_windows.go @@ -9,9 +9,11 @@ import ( "os" "path/filepath" "regexp" + "strings" "github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/lib/encoder" "github.com/rclone/rclone/lib/file" ) @@ -19,10 +21,13 @@ var isDriveRegex = regexp.MustCompile(`^[a-zA-Z]\:$`) var isDriveRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\$`) var isDriveOrRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\?$`) var isNetworkSharePathRegex = regexp.MustCompile(`^\\\\[^\\\?]+\\[^\\]`) +var isAnyPathSeparatorRegex = regexp.MustCompile(`[/\\]+`) // Matches any path separators, slash or backslash, or sequences of them -// isNetworkSharePath returns true if the given string is a valid network share path, -// in the basic UNC format "\\Server\Share\Path", where the first two path components -// are required ("\\Server\Share", which represents the volume). +// isNetworkSharePath returns true if the given string is a network share path, +// in the basic UNC format "\\Server\Share\Path". The first two path components +// are required ("\\Server\Share"), and represents the volume. The rest of the +// string can be anything, i.e. can be a nested path ("\\Server\Share\Path\Path\Path"). +// Actual validity of the path, e.g. if it contains invalid characters, is not considered. // Extended-length UNC format "\\?\UNC\Server\Share\Path" is not considered, as it is // not supported by cgofuse/winfsp, so returns false for any paths with prefix "\\?\". // Note: There is a UNCPath function in lib/file, but it refers to any extended-length @@ -132,30 +137,47 @@ func handleLocalMountpath(f fs.Fs, mountpath string, opt *mountlib.Options) (str return mountpath, nil } +// networkSharePathEncoder is an encoder used to make strings valid as (part of) Windows network share UNC paths +const networkSharePathEncoder = (encoder.EncodeZero | // NUL(0x00) + encoder.EncodeCtl | // CTRL(0x01-0x1F) + encoder.EncodeDel | // DEL(0x7F) + encoder.EncodeWin | // :?"*<>| + encoder.EncodeInvalidUtf8) // Also encode invalid UTF-8 bytes as Go can't convert them to UTF-16. + +// encodeNetworkSharePath makes a string valid to use as (part of) a Windows network share UNC path. +// Using backslash as path separator here, but forward slashes would also be treated as +// path separators by the library, and therefore does not encode either of them. For convenience, +// normalizes to backslashes-only. UNC paths always start with two path separators, but WinFsp +// requires volume prefix as UNC-like path but with only a single backslash prefix, and multiple +// separators are not valid in any other parts of network share paths, so therefore (unlike what +// filepath.FromSlash would do) replaces multiple separators with a single one (like filpath.Clean +// would do, but it does also more). A trailing path separator would just be ignored, but we +// remove it here as well for convenience. +func encodeNetworkSharePath(volumeName string) string { + return networkSharePathEncoder.Encode(strings.TrimRight(isAnyPathSeparatorRegex.ReplaceAllString(volumeName, `\`), `\`)) +} + // handleVolumeName handles the volume name option. -func handleVolumeName(opt *mountlib.Options, volumeName string) { - // If volumeName parameter is set, then just set that into options replacing any existing value. - // Else, ensure the volume name option is a valid network share UNC path if network mode, +func handleVolumeName(opt *mountlib.Options) { + // Ensure the volume name option is a valid network share UNC path if network mode, // and ensure network mode if configured volume name is already UNC path. - if volumeName != "" { - opt.VolumeName = volumeName - } else if opt.VolumeName != "" { // Should always be true due to code in mountlib caller + if opt.VolumeName != "" { // Should always be true due to code in mountlib caller // Use value of given volume name option, but check if it is disk volume name or network volume prefix if isNetworkSharePath(opt.VolumeName) { // Specified volume name is network share UNC path, assume network mode and use it as volume prefix - opt.VolumeName = opt.VolumeName[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash + opt.VolumeName = encodeNetworkSharePath(opt.VolumeName[1:]) // We know from isNetworkSharePath it has a duplicate path separator prefix, so removes that right away (but encodeNetworkSharePath would remove it also) if !opt.NetworkMode { // Specified volume name is network share UNC path, force network mode and use it as volume prefix fs.Debugf(nil, "Forcing network mode due to network share (UNC) volume name") opt.NetworkMode = true } } else if opt.NetworkMode { - // Plain volume name treated as share name in network mode, append to hard coded "\\server" prefix to get full volume prefix. - opt.VolumeName = "\\server\\" + opt.VolumeName + // Specified volume name is not a valid network share UNC path, but network mode is enabled, so append to a hard coded server prefix and use it as volume prefix + opt.VolumeName = `\server\` + strings.TrimLeft(encodeNetworkSharePath(opt.VolumeName), `\`) } } else if opt.NetworkMode { - // Hard coded default - opt.VolumeName = "\\server\\share" + // Use hard coded default + opt.VolumeName = `\server\share` } } @@ -174,22 +196,27 @@ func getMountpoint(f fs.Fs, mountpath string, opt *mountlib.Options) (mountpoint } // Handle mountpath - var volumeName string if isDefaultPath(mountpath) { // Mount path indicates defaults, which will automatically pick an unused drive letter. - mountpoint, err = handleDefaultMountpath() + if mountpoint, err = handleDefaultMountpath(); err != nil { + return + } } else if isNetworkSharePath(mountpath) { // Mount path is a valid network share path (UNC format, "\\Server\Share" prefix). - mountpoint, err = handleNetworkShareMountpath(mountpath, opt) - // In this case the volume name is taken from the mount path, will replace any existing volume name option. - volumeName = mountpath[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash + if mountpoint, err = handleNetworkShareMountpath(mountpath, opt); err != nil { + return + } + // In this case the volume name is taken from the mount path, it replaces any existing volume name option. + opt.VolumeName = mountpath } else { // Mount path is drive letter or directory path. - mountpoint, err = handleLocalMountpath(f, mountpath, opt) + if mountpoint, err = handleLocalMountpath(f, mountpath, opt); err != nil { + return + } } // Handle volume name - handleVolumeName(opt, volumeName) + handleVolumeName(opt) // Done, return mountpoint to be used, together with updated mount options. if opt.NetworkMode { diff --git a/cmd/mount/mount.go b/cmd/mount/mount.go index 9be432c5e..25dcbb44d 100644 --- a/cmd/mount/mount.go +++ b/cmd/mount/mount.go @@ -79,6 +79,7 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil { return nil, nil, err } + opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName) fs.Debugf(f, "Mounting on %q", mountpoint) if opt.DebugFUSE { diff --git a/cmd/mount2/mount.go b/cmd/mount2/mount.go index 152f3a89e..09dc06db9 100644 --- a/cmd/mount2/mount.go +++ b/cmd/mount2/mount.go @@ -151,6 +151,7 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil { return nil, nil, err } + opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName) fs.Debugf(f, "Mounting on %q", mountpoint) fsys := NewFS(VFS, opt) diff --git a/cmd/mountlib/mount.go b/cmd/mountlib/mount.go index 5e5f4da20..5807e56ff 100644 --- a/cmd/mountlib/mount.go +++ b/cmd/mountlib/mount.go @@ -240,8 +240,12 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm func (m *MountPoint) Mount() (daemon *os.Process, err error) { // Ensure sensible defaults - m.SetVolumeName(m.MountOpt.VolumeName) - m.SetDeviceName(m.MountOpt.DeviceName) + if m.MountOpt.VolumeName == "" { + m.MountOpt.VolumeName = fs.ConfigString(m.Fs) + } + if m.MountOpt.DeviceName == "" { + m.MountOpt.DeviceName = fs.ConfigString(m.Fs) + } // Start background task if --daemon is specified if m.MountOpt.Daemon { diff --git a/cmd/mountlib/utils.go b/cmd/mountlib/utils.go index bf3a7edf9..b3c065aac 100644 --- a/cmd/mountlib/utils.go +++ b/cmd/mountlib/utils.go @@ -97,29 +97,10 @@ func checkMountEmpty(mountpoint string) error { return fmt.Errorf(msg+": %w", mountpoint, err) } -// SetVolumeName with sensible default -func (m *MountPoint) SetVolumeName(vol string) { - if vol == "" { - vol = fs.ConfigString(m.Fs) - } - m.MountOpt.SetVolumeName(vol) -} - -// SetVolumeName removes special characters from volume name if necessary -func (o *Options) SetVolumeName(vol string) { - vol = strings.ReplaceAll(vol, ":", " ") - vol = strings.ReplaceAll(vol, "/", " ") - vol = strings.TrimSpace(vol) - if runtime.GOOS == "windows" && len(vol) > 32 { - vol = vol[:32] - } - o.VolumeName = vol -} - -// SetDeviceName with sensible default -func (m *MountPoint) SetDeviceName(dev string) { - if dev == "" { - dev = fs.ConfigString(m.Fs) - } - m.MountOpt.DeviceName = dev +// MakeVolumeNameValidOnUnix takes a volume name and returns a variant that is valid on unix systems. +func MakeVolumeNameValidOnUnix(volumeName string) string { + volumeName = strings.ReplaceAll(volumeName, ":", " ") + volumeName = strings.ReplaceAll(volumeName, "/", " ") + volumeName = strings.TrimSpace(volumeName) + return volumeName }