Skip to content

Commit a746e6f

Browse files
committed
feat(os): add UserConfigDir and UserCacheDir
1 parent dfbb133 commit a746e6f

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package filepathlite
2+
3+
// IsAbs reports whether the path is absolute.
4+
func IsAbs(path string) bool {
5+
return len(path) > 0 && (path[0] == '/' || path[0] == '#')
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build unix || (js && wasm) || wasip1
2+
3+
package filepathlite
4+
5+
// IsAbs reports whether the path is absolute.
6+
func IsAbs(path string) bool {
7+
return len(path) > 0 && path[0] == '/'
8+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package filepathlite
2+
3+
func IsPathSeparator(c uint8) bool {
4+
return c == '\\' || c == '/'
5+
}
6+
7+
// IsAbs reports whether the path is absolute.
8+
func IsAbs(path string) (b bool) {
9+
l := volumeNameLen(path)
10+
if l == 0 {
11+
return false
12+
}
13+
// If the volume name starts with a double slash, this is an absolute path.
14+
if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) {
15+
return true
16+
}
17+
path = path[l:]
18+
if path == "" {
19+
return false
20+
}
21+
return IsPathSeparator(path[0])
22+
}
23+
24+
// volumeNameLen returns length of the leading volume name on Windows.
25+
// It returns 0 elsewhere.
26+
//
27+
// See:
28+
// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
29+
// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
30+
func volumeNameLen(path string) int {
31+
switch {
32+
case len(path) >= 2 && path[1] == ':':
33+
// Path starts with a drive letter.
34+
//
35+
// Not all Windows functions necessarily enforce the requirement that
36+
// drive letters be in the set A-Z, and we don't try to here.
37+
//
38+
// We don't handle the case of a path starting with a non-ASCII character,
39+
// in which case the "drive letter" might be multiple bytes long.
40+
return 2
41+
42+
case len(path) == 0 || !IsPathSeparator(path[0]):
43+
// Path does not have a volume component.
44+
return 0
45+
46+
case pathHasPrefixFold(path, `\\.\UNC`):
47+
// We're going to treat the UNC host and share as part of the volume
48+
// prefix for historical reasons, but this isn't really principled;
49+
// Windows's own GetFullPathName will happily remove the first
50+
// component of the path in this space, converting
51+
// \\.\unc\a\b\..\c into \\.\unc\a\c.
52+
return uncLen(path, len(`\\.\UNC\`))
53+
54+
case pathHasPrefixFold(path, `\\.`) ||
55+
pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
56+
// Path starts with \\.\, and is a Local Device path; or
57+
// path starts with \\?\ or \??\ and is a Root Local Device path.
58+
//
59+
// We treat the next component after the \\.\ prefix as
60+
// part of the volume name, which means Clean(`\\?\c:\`)
61+
// won't remove the trailing \. (See #64028.)
62+
if len(path) == 3 {
63+
return 3 // exactly \\.
64+
}
65+
_, rest, ok := cutPath(path[4:])
66+
if !ok {
67+
return len(path)
68+
}
69+
return len(path) - len(rest) - 1
70+
71+
case len(path) >= 2 && IsPathSeparator(path[1]):
72+
// Path starts with \\, and is a UNC path.
73+
return uncLen(path, 2)
74+
}
75+
return 0
76+
}
77+
78+
// pathHasPrefixFold tests whether the path s begins with prefix,
79+
// ignoring case and treating all path separators as equivalent.
80+
// If s is longer than prefix, then s[len(prefix)] must be a path separator.
81+
func pathHasPrefixFold(s, prefix string) bool {
82+
if len(s) < len(prefix) {
83+
return false
84+
}
85+
for i := 0; i < len(prefix); i++ {
86+
if IsPathSeparator(prefix[i]) {
87+
if !IsPathSeparator(s[i]) {
88+
return false
89+
}
90+
} else if toUpper(prefix[i]) != toUpper(s[i]) {
91+
return false
92+
}
93+
}
94+
if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
95+
return false
96+
}
97+
return true
98+
}
99+
100+
// uncLen returns the length of the volume prefix of a UNC path.
101+
// prefixLen is the prefix prior to the start of the UNC host;
102+
// for example, for "//host/share", the prefixLen is len("//")==2.
103+
func uncLen(path string, prefixLen int) int {
104+
count := 0
105+
for i := prefixLen; i < len(path); i++ {
106+
if IsPathSeparator(path[i]) {
107+
count++
108+
if count == 2 {
109+
return i
110+
}
111+
}
112+
}
113+
return len(path)
114+
}
115+
116+
// cutPath slices path around the first path separator.
117+
func cutPath(path string) (before, after string, found bool) {
118+
for i := range path {
119+
if IsPathSeparator(path[i]) {
120+
return path[:i], path[i+1:], true
121+
}
122+
}
123+
return path, "", false
124+
}
125+
126+
func toUpper(c byte) byte {
127+
if 'a' <= c && c <= 'z' {
128+
return c - ('a' - 'A')
129+
}
130+
return c
131+
}

src/os/file.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package os
1919

2020
import (
2121
"errors"
22+
"internal/filepathlite"
2223
"io"
2324
"io/fs"
2425
"runtime"
@@ -374,6 +375,112 @@ func TempDir() string {
374375
return tempDir()
375376
}
376377

378+
// UserCacheDir returns the default root directory to use for user-specific
379+
// cached data. Users should create their own application-specific subdirectory
380+
// within this one and use that.
381+
//
382+
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
383+
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
384+
// non-empty, else $HOME/.cache.
385+
// On Darwin, it returns $HOME/Library/Caches.
386+
// On Windows, it returns %LocalAppData%.
387+
// On Plan 9, it returns $home/lib/cache.
388+
//
389+
// If the location cannot be determined (for example, $HOME is not defined) or
390+
// the path in $XDG_CACHE_HOME is relative, then it will return an error.
391+
func UserCacheDir() (string, error) {
392+
var dir string
393+
394+
switch runtime.GOOS {
395+
case "windows":
396+
dir = Getenv("LocalAppData")
397+
if dir == "" {
398+
return "", errors.New("%LocalAppData% is not defined")
399+
}
400+
401+
case "darwin", "ios":
402+
dir = Getenv("HOME")
403+
if dir == "" {
404+
return "", errors.New("$HOME is not defined")
405+
}
406+
dir += "/Library/Caches"
407+
408+
case "plan9":
409+
dir = Getenv("home")
410+
if dir == "" {
411+
return "", errors.New("$home is not defined")
412+
}
413+
dir += "/lib/cache"
414+
415+
default: // Unix
416+
dir = Getenv("XDG_CACHE_HOME")
417+
if dir == "" {
418+
dir = Getenv("HOME")
419+
if dir == "" {
420+
return "", errors.New("neither $XDG_CACHE_HOME nor $HOME are defined")
421+
}
422+
dir += "/.cache"
423+
} else if !filepathlite.IsAbs(dir) {
424+
return "", errors.New("path in $XDG_CACHE_HOME is relative")
425+
}
426+
}
427+
428+
return dir, nil
429+
}
430+
431+
// UserConfigDir returns the default root directory to use for user-specific
432+
// configuration data. Users should create their own application-specific
433+
// subdirectory within this one and use that.
434+
//
435+
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
436+
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
437+
// non-empty, else $HOME/.config.
438+
// On Darwin, it returns $HOME/Library/Application Support.
439+
// On Windows, it returns %AppData%.
440+
// On Plan 9, it returns $home/lib.
441+
//
442+
// If the location cannot be determined (for example, $HOME is not defined) or
443+
// the path in $XDG_CONFIG_HOME is relative, then it will return an error.
444+
func UserConfigDir() (string, error) {
445+
var dir string
446+
447+
switch runtime.GOOS {
448+
case "windows":
449+
dir = Getenv("AppData")
450+
if dir == "" {
451+
return "", errors.New("%AppData% is not defined")
452+
}
453+
454+
case "darwin", "ios":
455+
dir = Getenv("HOME")
456+
if dir == "" {
457+
return "", errors.New("$HOME is not defined")
458+
}
459+
dir += "/Library/Application Support"
460+
461+
case "plan9":
462+
dir = Getenv("home")
463+
if dir == "" {
464+
return "", errors.New("$home is not defined")
465+
}
466+
dir += "/lib"
467+
468+
default: // Unix
469+
dir = Getenv("XDG_CONFIG_HOME")
470+
if dir == "" {
471+
dir = Getenv("HOME")
472+
if dir == "" {
473+
return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined")
474+
}
475+
dir += "/.config"
476+
} else if !filepathlite.IsAbs(dir) {
477+
return "", errors.New("path in $XDG_CONFIG_HOME is relative")
478+
}
479+
}
480+
481+
return dir, nil
482+
}
483+
377484
// UserHomeDir returns the current user's home directory.
378485
//
379486
// On Unix, including macOS, it returns the $HOME environment variable.

0 commit comments

Comments
 (0)