diff --git a/src/internal/filepathlite/path_plan9.go b/src/internal/filepathlite/path_plan9.go new file mode 100644 index 0000000000..ef13a1b57f --- /dev/null +++ b/src/internal/filepathlite/path_plan9.go @@ -0,0 +1,6 @@ +package filepathlite + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) bool { + return len(path) > 0 && (path[0] == '/' || path[0] == '#') +} diff --git a/src/internal/filepathlite/path_unix.go b/src/internal/filepathlite/path_unix.go new file mode 100644 index 0000000000..9194c04ef3 --- /dev/null +++ b/src/internal/filepathlite/path_unix.go @@ -0,0 +1,8 @@ +//go:build unix || (js && wasm) || wasip1 + +package filepathlite + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) bool { + return len(path) > 0 && path[0] == '/' +} diff --git a/src/internal/filepathlite/path_windows.go b/src/internal/filepathlite/path_windows.go new file mode 100644 index 0000000000..65a250e553 --- /dev/null +++ b/src/internal/filepathlite/path_windows.go @@ -0,0 +1,131 @@ +package filepathlite + +func IsPathSeparator(c uint8) bool { + return c == '\\' || c == '/' +} + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) (b bool) { + l := volumeNameLen(path) + if l == 0 { + return false + } + // If the volume name starts with a double slash, this is an absolute path. + if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) { + return true + } + path = path[l:] + if path == "" { + return false + } + return IsPathSeparator(path[0]) +} + +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +// +// See: +// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats +// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html +func volumeNameLen(path string) int { + switch { + case len(path) >= 2 && path[1] == ':': + // Path starts with a drive letter. + // + // Not all Windows functions necessarily enforce the requirement that + // drive letters be in the set A-Z, and we don't try to here. + // + // We don't handle the case of a path starting with a non-ASCII character, + // in which case the "drive letter" might be multiple bytes long. + return 2 + + case len(path) == 0 || !IsPathSeparator(path[0]): + // Path does not have a volume component. + return 0 + + case pathHasPrefixFold(path, `\\.\UNC`): + // We're going to treat the UNC host and share as part of the volume + // prefix for historical reasons, but this isn't really principled; + // Windows's own GetFullPathName will happily remove the first + // component of the path in this space, converting + // \\.\unc\a\b\..\c into \\.\unc\a\c. + return uncLen(path, len(`\\.\UNC\`)) + + case pathHasPrefixFold(path, `\\.`) || + pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`): + // Path starts with \\.\, and is a Local Device path; or + // path starts with \\?\ or \??\ and is a Root Local Device path. + // + // We treat the next component after the \\.\ prefix as + // part of the volume name, which means Clean(`\\?\c:\`) + // won't remove the trailing \. (See #64028.) + if len(path) == 3 { + return 3 // exactly \\. + } + _, rest, ok := cutPath(path[4:]) + if !ok { + return len(path) + } + return len(path) - len(rest) - 1 + + case len(path) >= 2 && IsPathSeparator(path[1]): + // Path starts with \\, and is a UNC path. + return uncLen(path, 2) + } + return 0 +} + +// pathHasPrefixFold tests whether the path s begins with prefix, +// ignoring case and treating all path separators as equivalent. +// If s is longer than prefix, then s[len(prefix)] must be a path separator. +func pathHasPrefixFold(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if IsPathSeparator(prefix[i]) { + if !IsPathSeparator(s[i]) { + return false + } + } else if toUpper(prefix[i]) != toUpper(s[i]) { + return false + } + } + if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) { + return false + } + return true +} + +// uncLen returns the length of the volume prefix of a UNC path. +// prefixLen is the prefix prior to the start of the UNC host; +// for example, for "//host/share", the prefixLen is len("//")==2. +func uncLen(path string, prefixLen int) int { + count := 0 + for i := prefixLen; i < len(path); i++ { + if IsPathSeparator(path[i]) { + count++ + if count == 2 { + return i + } + } + } + return len(path) +} + +// cutPath slices path around the first path separator. +func cutPath(path string) (before, after string, found bool) { + for i := range path { + if IsPathSeparator(path[i]) { + return path[:i], path[i+1:], true + } + } + return path, "", false +} + +func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') + } + return c +} diff --git a/src/os/file.go b/src/os/file.go index e7dd214b73..a9a08af8b9 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -19,6 +19,7 @@ package os import ( "errors" + "internal/filepathlite" "io" "io/fs" "runtime" @@ -374,6 +375,112 @@ func TempDir() string { return tempDir() } +// UserCacheDir returns the default root directory to use for user-specific +// cached data. Users should create their own application-specific subdirectory +// within this one and use that. +// +// On Unix systems, it returns $XDG_CACHE_HOME as specified by +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if +// non-empty, else $HOME/.cache. +// On Darwin, it returns $HOME/Library/Caches. +// On Windows, it returns %LocalAppData%. +// On Plan 9, it returns $home/lib/cache. +// +// If the location cannot be determined (for example, $HOME is not defined) or +// the path in $XDG_CACHE_HOME is relative, then it will return an error. +func UserCacheDir() (string, error) { + var dir string + + switch runtime.GOOS { + case "windows": + dir = Getenv("LocalAppData") + if dir == "" { + return "", errors.New("%LocalAppData% is not defined") + } + + case "darwin", "ios": + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("$HOME is not defined") + } + dir += "/Library/Caches" + + case "plan9": + dir = Getenv("home") + if dir == "" { + return "", errors.New("$home is not defined") + } + dir += "/lib/cache" + + default: // Unix + dir = Getenv("XDG_CACHE_HOME") + if dir == "" { + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("neither $XDG_CACHE_HOME nor $HOME are defined") + } + dir += "/.cache" + } else if !filepathlite.IsAbs(dir) { + return "", errors.New("path in $XDG_CACHE_HOME is relative") + } + } + + return dir, nil +} + +// UserConfigDir returns the default root directory to use for user-specific +// configuration data. Users should create their own application-specific +// subdirectory within this one and use that. +// +// On Unix systems, it returns $XDG_CONFIG_HOME as specified by +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if +// non-empty, else $HOME/.config. +// On Darwin, it returns $HOME/Library/Application Support. +// On Windows, it returns %AppData%. +// On Plan 9, it returns $home/lib. +// +// If the location cannot be determined (for example, $HOME is not defined) or +// the path in $XDG_CONFIG_HOME is relative, then it will return an error. +func UserConfigDir() (string, error) { + var dir string + + switch runtime.GOOS { + case "windows": + dir = Getenv("AppData") + if dir == "" { + return "", errors.New("%AppData% is not defined") + } + + case "darwin", "ios": + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("$HOME is not defined") + } + dir += "/Library/Application Support" + + case "plan9": + dir = Getenv("home") + if dir == "" { + return "", errors.New("$home is not defined") + } + dir += "/lib" + + default: // Unix + dir = Getenv("XDG_CONFIG_HOME") + if dir == "" { + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") + } + dir += "/.config" + } else if !filepathlite.IsAbs(dir) { + return "", errors.New("path in $XDG_CONFIG_HOME is relative") + } + } + + return dir, nil +} + // UserHomeDir returns the current user's home directory. // // On Unix, including macOS, it returns the $HOME environment variable.