Files
rvbox/internal/config/path.go
T

139 lines
4.3 KiB
Go

package config
import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
func validateAbsolutePath(name, value string, platform Platform, allowEmpty bool) (string, error) {
if value == "" && allowEmpty {
return "", nil
}
if value == "" || strings.IndexByte(value, 0) >= 0 {
return "", fmt.Errorf("%s must be a nonempty absolute path", name)
}
if platform == PlatformWindows {
if !isWindowsAbsolute(value) {
return "", fmt.Errorf("%s must be an absolute Windows path", name)
}
return cleanWindowsPath(value), nil
}
// Config parsing can validate an inactive Unix shell path while running on
// Windows. filepath follows the host OS, whereas the config field's stated
// platform is Unix, so use slash-path semantics here.
if !path.IsAbs(value) {
return "", fmt.Errorf("%s must be an absolute Unix path", name)
}
return path.Clean(value), nil
}
func isWindowsAbsolute(value string) bool {
if strings.HasPrefix(value, `\\`) {
parts := strings.Split(strings.TrimPrefix(value, `\\`), `\`)
return len(parts) >= 2 && parts[0] != "" && parts[1] != ""
}
return len(value) >= 3 && ((value[0] >= 'A' && value[0] <= 'Z') || (value[0] >= 'a' && value[0] <= 'z')) &&
value[1] == ':' && (value[2] == '\\' || value[2] == '/')
}
func cleanWindowsPath(value string) string {
value = strings.ReplaceAll(value, "/", `\`)
prefix := value[:3]
rest := strings.TrimPrefix(value[3:], `\`)
if strings.HasPrefix(value, `\\`) {
components := strings.Split(strings.TrimPrefix(value, `\\`), `\`)
prefix = `\\` + components[0] + `\` + components[1] + `\`
rest = strings.Join(components[2:], `\`)
}
parts := make([]string, 0)
for _, part := range strings.Split(rest, `\`) {
switch part {
case "", ".":
continue
case "..":
if len(parts) > 0 {
parts = parts[:len(parts)-1]
}
default:
parts = append(parts, part)
}
}
return prefix + strings.Join(parts, `\`)
}
func checkNativeExecutable(name, value string, platform Platform) (string, error) {
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
return "", fmt.Errorf("%s filesystem validation requires a native %s process", name, map[bool]string{true: "Windows", false: "Unix"}[platform == PlatformWindows])
}
canonical, err := filepath.Abs(value)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
canonical = evaluated
}
info, err := os.Stat(canonical)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("%s must name a regular file", name)
}
if platform == PlatformUnix && info.Mode().Perm()&0o111 == 0 {
return "", fmt.Errorf("%s is not executable", name)
}
if platform == PlatformWindows {
extension := strings.ToLower(filepath.Ext(canonical))
if extension != ".exe" && extension != ".com" && extension != ".cmd" && extension != ".bat" {
return "", fmt.Errorf("%s is not a recognized Windows executable", name)
}
}
return canonical, nil
}
func checkNativeDirectory(name, value string, platform Platform) (string, error) {
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
return "", fmt.Errorf("%s filesystem validation requires a native target", name)
}
canonical, err := filepath.Abs(value)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
canonical = evaluated
}
info, err := os.Stat(canonical)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if !info.IsDir() {
return "", fmt.Errorf("%s must name a directory", name)
}
return canonical, nil
}
func checkNativeRegularFile(name, value string, platform Platform) (string, error) {
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
return "", fmt.Errorf("%s filesystem validation requires a native target", name)
}
canonical, err := filepath.Abs(value)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
canonical = evaluated
}
info, err := os.Stat(canonical)
if err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("%s must name a regular file", name)
}
return canonical, nil
}