-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlinks.go
More file actions
147 lines (137 loc) · 4.9 KB
/
Copy pathlinks.go
File metadata and controls
147 lines (137 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package fs
import (
"errors"
stdfs "io/fs"
"os"
"path/filepath"
"strings"
"syscall"
)
const (
opSymlink = "symlink"
opReadLink = "readlink"
opHardlink = "hardlink"
)
// Symlink creates a symbolic link at linkPath pointing to target.
// Idempotent: if linkPath already exists as a symlink with the same
// target, returns nil. If linkPath exists but points elsewhere or is
// not a symlink, returns [ErrAlreadyExists].
//
// Concurrent callers: the idempotency check (Readlink to Symlink)
// is NOT atomic. Between the two syscalls another process can
// create the link with a different target; the loser of that race
// sees [ErrAlreadyExists]. POSIX `symlink(2)` is atomic for the
// create-if-not-exists case but Go's stdlib does not expose the
// flag needed to thread that through. For strict
// "exactly one creator wins" semantics, accept [ErrAlreadyExists]
// from one of the concurrent callers as a successful outcome.
//
// The target is stored verbatim in the link; it is not validated,
// resolved, or required to exist (a "dangling" symlink is allowed,
// matching [os.Symlink]).
func Symlink(target, linkPath string) error {
if existing, err := os.Readlink(linkPath); err == nil {
if existing == target {
return nil
}
return wrapPathError(opSymlink, linkPath, ErrAlreadyExists)
} else if _, lerr := os.Lstat(linkPath); lerr == nil {
// linkPath exists but isn't a symlink.
return wrapPathError(opSymlink, linkPath, ErrAlreadyExists)
}
if err := os.Symlink(target, linkPath); err != nil {
return wrapPathError(opSymlink, linkPath, err)
}
return nil
}
// ReadLink returns the target stored in the symlink at linkPath.
// Wraps [os.Readlink].
func ReadLink(linkPath string) (string, error) {
target, err := os.Readlink(linkPath)
if err != nil {
return "", wrapPathError(opReadLink, linkPath, err)
}
return target, nil
}
// EvalSymlinks resolves all symlinks in path and returns the
// canonical absolute path. Wraps [filepath.EvalSymlinks]. A symlink
// loop or excessive hop count returns [ErrSymlinkLoop].
//
// For constrained resolution that won't escape a parent directory,
// use [EvalSymlinksWithin].
func EvalSymlinks(path string) (string, error) {
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
// stdlib uses "too many links" message for loop detection.
// Map to the package sentinel.
if isSymlinkLoop(err) {
return "", wrapPathError(opEvalSymlinks, path, ErrSymlinkLoop)
}
return "", wrapPathError(opEvalSymlinks, path, err)
}
return resolved, nil
}
// isSymlinkLoop reports whether err looks like a symlink-loop error
// from filepath.EvalSymlinks. POSIX surfaces ELOOP; filepath itself
// has an internal hop-counter that returns the unexported sentinel
// `errors.New("EvalSymlinks: too many links")`.
//
// The string-match below is brittle: if the Go team ever renames the
// internal message (it's a private implementation detail and they
// are allowed to), this function silently fails to detect loops and
// the caller sees the raw error rather than [ErrSymlinkLoop]. The
// tripwire is [TestConformanceSymlinkLoopDetection] in
// conformance_test.go; it constructs a real `a to b to a` loop and
// asserts that EvalSymlinks to isSymlinkLoop returns
// [ErrSymlinkLoop]. CI runs this on every supported Go version on
// every supported platform; a stdlib message rename will surface
// there before any user-visible regression.
//
// Once a public sentinel exists upstream (track:
// https://github.com/golang/go; search "EvalSymlinks too many links"
// in proposals), this function should switch to errors.Is against it
// and drop the string match.
func isSymlinkLoop(err error) bool {
if err == nil {
return false
}
if errors.Is(err, syscall.ELOOP) {
return true
}
// filepath's internal sentinel is unexported; match by message.
if strings.Contains(err.Error(), "too many links") {
return true
}
// Some platforms wrap ELOOP inside a *fs.PathError without
// errors.Is matching directly. Walk the chain.
var pe *stdfs.PathError
if errors.As(err, &pe) && pe.Err != nil {
if errors.Is(pe.Err, syscall.ELOOP) {
return true
}
}
return false
}
// Hardlink creates a hard link at linkPath pointing to target.
// Idempotent: if linkPath already refers to the same inode as
// target, returns nil. If linkPath exists but is a different file,
// returns [ErrAlreadyExists]. Cross-device links surface as
// [ErrCrossDevice].
//
// target must exist and be a regular file (most filesystems forbid
// hard-linking directories).
func Hardlink(target, linkPath string) error {
if same, err := SameFile(target, linkPath); err == nil && same {
return nil
}
if _, lerr := os.Lstat(linkPath); lerr == nil {
return wrapPathError(opHardlink, linkPath, ErrAlreadyExists)
}
if err := os.Link(target, linkPath); err != nil {
if errors.Is(err, syscall.EXDEV) {
return wrapPathError(opHardlink, linkPath, ErrCrossDevice)
}
return wrapPathError(opHardlink, linkPath, err)
}
return nil
}