diff --git a/pkg/crosspath/crosspath.go b/pkg/crosspath/crosspath.go index 11a96e75..e8f57948 100644 --- a/pkg/crosspath/crosspath.go +++ b/pkg/crosspath/crosspath.go @@ -75,3 +75,32 @@ func ToNative(path string) string { 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]) +} diff --git a/pkg/crosspath/crosspath_test.go b/pkg/crosspath/crosspath_test.go index e872eb36..9a692a12 100644 --- a/pkg/crosspath/crosspath_test.go +++ b/pkg/crosspath/crosspath_test.go @@ -169,3 +169,34 @@ func TestToNative_unsupported(t *testing.T) { t.Fatalf("ToNative not supported on this platform %q with path separator %q", runtime.GOOS, filepath.Separator) } + +func TestIsRoot(t *testing.T) { + tests := []struct { + name string + path string + want bool + }{ + {"empty", "", false}, + + {"UNIX", "/", true}, + {"Drive only", "C:", true}, + {"Drive and slash", "C:/", true}, + {"Drive and backslash", `C:\`, true}, + + {"backslash", `\`, false}, + {"just letters", "just letters", false}, + {"subdir of root", "/subdir", false}, + {"subdir of drive", "C:\\subdir", false}, + + {"indirectly root", "/subdir/..", false}, + {"UNC notation", `\\NAS\Share\`, false}, + {"Slashed UNC notation", `//NAS/Share/`, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsRoot(tt.path); got != tt.want { + t.Errorf("IsRoot() = %v, want %v", got, tt.want) + } + }) + } +}