flamenco/pkg/crosspath/crosspath.go
Sybren A. Stüvel dae5b1a571 Fix #104237: fix issue with drive-only paths on Windows
Fix an issue where a shared storage path on Linux, that maps via two-way
variables to a drive root on Windows, caused problems with the path
translation system.

Windows paths that consist only of a drive letter (`F:`) cannot just be
concatenated to a relative path, as that will result in `F:path\to\file`,
which is still a relative path of sorts. This is now handled correctly,
and should result in `F:\path\to\file`.

This fixes #104237.
2023-07-31 15:28:07 +02:00

194 lines
5.1 KiB
Go

// Package crosspath deals with file/directory paths in a cross-platform way.
//
// This package tries to understand Windows paths on UNIX and vice versa.
// Returned paths may be using forward slashes as separators.
package crosspath
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"fmt"
path_module "path" // import under other name so that parameters can be called 'path'
"path/filepath"
"strings"
)
// Base returns the last element of path. Trailing slashes are removed before
// extracting the last element. If the path is empty, Base returns ".". If the
// path consists entirely of slashes, Base returns "/".
func Base(path string) string {
slashed := ToSlash(path)
return path_module.Base(slashed)
}
// Dir returns all but the last element of path, typically the path's directory.
// If the path is empty, Dir returns ".".
func Dir(path string) string {
if path == "" {
return "."
}
slashed := ToSlash(path)
// Don't use path.Dir(), as that cleans up the path and removes double
// slashes. However, Windows UNC paths start with double blackslashes, which
// will translate to double slashes and should not be removed.
dir, _ := path_module.Split(slashed)
switch {
case dir == "":
return "."
case len(dir) > 1:
// Remove trailing slash.
return dir[:len(dir)-1]
default:
return dir
}
}
func Join(elem ...string) string {
return ToSlash(path_module.Join(elem...))
}
// Stem returns the filename without extension.
func Stem(path string) string {
base := Base(path)
ext := path_module.Ext(base)
return base[:len(base)-len(ext)]
}
// ToSlash replaces all backslashes with forward slashes.
// Contrary to filepath.ToSlash(), this also happens on Linux; it does not
// expect `path` to be in platform-native notation.
func ToSlash(path string) string {
return strings.ReplaceAll(path, "\\", "/")
}
// ToNative replaces all path separators (forward and backward slashes) with the
// platform-native separator.
func ToNative(path string) string {
switch filepath.Separator {
case '/':
return ToSlash(path)
case '\\':
return strings.ReplaceAll(path, "/", "\\")
default:
panic(fmt.Sprintf("this platform has an unknown path separator: %q", filepath.Separator))
}
}
func validDriveLetter(r rune) bool {
return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}
// IsRoot returns whether the given path is a root path or not.
// Paths "/", "C:", "C:\", and "C:/" are considered root, for all latin drive
// letters A-Z.
//
// NOTE: this does NOT resolve symlinks or `..` entries.
func IsRoot(path string) bool {
switch {
case path == "":
return false
case path == "/":
return true
// From here on, it can only be a DOS root, so must have a drive letter and a colon.
case len(path) < 2, len(path) > 3:
return false
case path[1] != ':':
return false
// C:\ and C:/ are both considered roots.
case len(path) == 3 && path[2] != '/' && path[2] != '\\':
return false
}
runes := []rune(path)
return validDriveLetter(runes[0])
}
// ToPlatform returns the path, with path separators adjusted for the given platform.
// It is assumed that all forward and backward slashes in the path are path
// separators, and that apart from the style of separators the path makes sense
// on the target platform.
func ToPlatform(path, platform string) string {
if path == "" {
return ""
}
components := strings.FieldsFunc(path, isPathSep)
// FieldsFunc() removes leading path separators, turning an absolute path on
// Linux to a relative path, and turning `\\NAS\share` on Windows into
// `NAS\share`.
extraComponents := []string{}
for _, r := range path {
if !isPathSep(r) {
break
}
extraComponents = append(extraComponents, "")
}
components = append(extraComponents, components...)
pathsep := pathSepForPlatform(platform)
translated := strings.Join(components, pathsep)
if platform == "windows" {
return EnsureDriveAbsolute(translated)
}
return translated
}
// pathSepForPlatform returns the path separator for the given platform.
// This is rather simple, and just returns `\` on Windows and `/` on all other
// platforms.
func pathSepForPlatform(platform string) string {
switch platform {
case "windows":
return `\`
default:
return "/"
}
}
func isPathSep(r rune) bool {
return r == '/' || r == '\\'
}
// TrimTrailingSep removes any trailling path separator.
func TrimTrailingSep(path string) string {
if path == "" {
return ""
}
trimmed := strings.TrimRightFunc(path, isPathSep)
if trimmed == "" {
return string([]rune(path)[0])
}
return trimmed
}
// EnsureDriveAbsolute ensures that a Windows path that starts with a drive
// letter also has an absolute path on that drive. For example, turns
// `F:path\to\file` into `F:\path\to\file`.
func EnsureDriveAbsolute(windowsPath string) string {
runes := []rune(windowsPath)
numRunes := len(runes)
if numRunes < 2 {
return windowsPath
}
if !validDriveLetter(runes[0]) || runes[1] != ':' {
return windowsPath
}
pathSep := pathSepForPlatform("windows")
if numRunes == 2 {
return windowsPath + pathSep
}
if string(runes[2]) == pathSep {
return windowsPath // Already F:\blabla
}
return string(runes[:2]) + pathSep + string(runes[2:])
}