Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/internal/filepathlite/path_plan9.go
Original file line number Diff line number Diff line change
@@ -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] == '#')
}
8 changes: 8 additions & 0 deletions src/internal/filepathlite/path_unix.go
Original file line number Diff line number Diff line change
@@ -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] == '/'
}
131 changes: 131 additions & 0 deletions src/internal/filepathlite/path_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
107 changes: 107 additions & 0 deletions src/os/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package os

import (
"errors"
"internal/filepathlite"
"io"
"io/fs"
"runtime"
Expand Down Expand Up @@ -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.
Expand Down
Loading