返回 DeepSeek-Reasonix
path.go
1 // Package pathidentity separates paths used for filesystem access from keys
2 // used to compare filesystem identity.
3 package pathidentity
4
5 import (
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "runtime"
11 "slices"
12 "strings"
13 "syscall"
14 )
15
16 const Version = 2
17
18 type ErrorKind string
19
20 const (
21 ErrorInvalid ErrorKind = "invalid"
22 ErrorPermission ErrorKind = "permission"
23 ErrorLinkLoop ErrorKind = "link_loop"
24 ErrorUnavailable ErrorKind = "unavailable"
25 ErrorUnknown ErrorKind = "unknown"
26 )
27
28 type Error struct {
29 Kind ErrorKind
30 Stage string
31 Path string
32 Err error
33 }
34
35 func (e *Error) Error() string {
36 if e == nil {
37 return "<nil>"
38 }
39 return fmt.Sprintf("resolve path identity (%s, %s): %v", e.Stage, e.Kind, e.Err)
40 }
41
42 func (e *Error) Unwrap() error { return e.Err }
43
44 type Options struct {
45 // BaseDir is required when path is relative. It must itself be absolute.
46 BaseDir string
47 // FollowLeaf resolves the final directory entry. Leave it false for
48 // rename/delete operations that act on the entry rather than its target.
49 FollowLeaf bool
50 }
51
52 type Identity struct {
53 AccessPath string
54 PhysicalPath string
55 Key string
56 }
57
58 func Resolve(path string, options Options) (Identity, error) {
59 access, err := absoluteAccessPath(path, options.BaseDir)
60 if err != nil {
61 return Identity{}, err
62 }
63 physical, err := resolvePhysicalPath(access, options.FollowLeaf)
64 if err != nil {
65 return Identity{}, err
66 }
67 key, err := platformIdentityKey(physical)
68 if err != nil {
69 return Identity{}, classify("compare", physical, err)
70 }
71 return Identity{AccessPath: access, PhysicalPath: physical, Key: filepath.Clean(key)}, nil
72 }
73
74 func Same(a, b string, options Options) (bool, error) {
75 left, err := Resolve(a, options)
76 if err != nil {
77 return false, err
78 }
79 right, err := Resolve(b, options)
80 if err != nil {
81 return false, err
82 }
83 leftInfo, leftErr := statForMode(left.AccessPath, options.FollowLeaf)
84 rightInfo, rightErr := statForMode(right.AccessPath, options.FollowLeaf)
85 if leftErr == nil && rightErr == nil && os.SameFile(leftInfo, rightInfo) {
86 return true, nil
87 }
88 for _, statErr := range []error{leftErr, rightErr} {
89 if statErr != nil && !os.IsNotExist(statErr) {
90 return false, classify("verify", "", statErr)
91 }
92 }
93 return left.Key == right.Key, nil
94 }
95
96 func statForMode(path string, followLeaf bool) (os.FileInfo, error) {
97 if followLeaf {
98 return os.Stat(path)
99 }
100 return os.Lstat(path)
101 }
102
103 func absoluteAccessPath(path, baseDir string) (string, error) {
104 path = strings.TrimSpace(path)
105 if path == "" {
106 return "", &Error{Kind: ErrorInvalid, Stage: "input", Path: path, Err: errors.New("path is empty")}
107 }
108 path = filepath.Clean(path)
109 if !filepath.IsAbs(path) {
110 baseDir = strings.TrimSpace(baseDir)
111 if baseDir == "" || !filepath.IsAbs(baseDir) {
112 return "", &Error{Kind: ErrorInvalid, Stage: "input", Path: path, Err: errors.New("relative path requires an absolute base directory")}
113 }
114 path = filepath.Join(filepath.Clean(baseDir), path)
115 }
116 return filepath.Clean(path), nil
117 }
118
119 func resolvePhysicalPath(access string, followLeaf bool) (string, error) {
120 target, leaf := access, ""
121 if !followLeaf {
122 parent := filepath.Dir(access)
123 if parent != access {
124 target, leaf = parent, filepath.Base(access)
125 }
126 }
127 resolved, err := resolveThroughExistingAncestor(target)
128 if err != nil {
129 return "", err
130 }
131 if leaf != "" {
132 resolved = filepath.Join(resolved, leaf)
133 }
134 return filepath.Clean(resolved), nil
135 }
136
137 func resolveThroughExistingAncestor(path string) (string, error) {
138 return resolveThroughExistingAncestorWith(path, filepath.EvalSymlinks)
139 }
140
141 func resolveThroughExistingAncestorWith(path string, evalSymlinks func(string) (string, error)) (string, error) {
142 current := filepath.Clean(path)
143 missing := make([]string, 0, 4)
144 for {
145 // Inspect existence before resolving links. A missing entry may be
146 // created by another lock contender; that is not a dangling link.
147 _, err := os.Lstat(current)
148 if err == nil {
149 resolved, resolveErr := evalSymlinks(current)
150 if resolveErr != nil {
151 return "", classify("physical", current, resolveErr)
152 }
153 for _, part := range slices.Backward(missing) {
154 resolved = filepath.Join(resolved, part)
155 }
156 return filepath.Clean(resolved), nil
157 }
158 if !os.IsNotExist(err) {
159 return "", classify("physical", current, err)
160 }
161 parent := filepath.Dir(current)
162 if parent == current {
163 return "", &Error{Kind: ErrorUnavailable, Stage: "physical", Path: path, Err: err}
164 }
165 missing = append(missing, filepath.Base(current))
166 current = parent
167 }
168 }
169
170 func classify(stage, path string, err error) error {
171 kind := ErrorUnknown
172 switch {
173 case errors.Is(err, os.ErrPermission):
174 kind = ErrorPermission
175 case errors.Is(err, syscall.ELOOP) || strings.Contains(strings.ToLower(err.Error()), "too many links"):
176 kind = ErrorLinkLoop
177 case os.IsNotExist(err):
178 kind = ErrorUnavailable
179 }
180 return &Error{Kind: kind, Stage: stage, Path: path, Err: err}
181 }
182
183 // Canonical is the frozen v1 compatibility key used by historical locks.
184 // New identity-sensitive code must use Resolve so errors are not discarded.
185 func Canonical(path string) string {
186 key := filepath.Clean(strings.TrimSpace(path))
187 if abs, err := filepath.Abs(key); err == nil {
188 key = abs
189 }
190 key = legacyResolvePathThroughExistingAncestor(key)
191 if runtime.GOOS == "windows" {
192 if strings.HasPrefix(strings.ToUpper(key), `\\?\UNC\`) {
193 key = `\\` + key[len(`\\?\UNC\`):]
194 } else {
195 key = strings.TrimPrefix(key, `\\?\`)
196 }
197 key = strings.ToLower(key)
198 }
199 return key
200 }
201
202 func legacyResolvePathThroughExistingAncestor(path string) string {
203 current := filepath.Clean(path)
204 missing := make([]string, 0, 4)
205 for {
206 if resolved, err := filepath.EvalSymlinks(current); err == nil {
207 for _, part := range slices.Backward(missing) {
208 resolved = filepath.Join(resolved, part)
209 }
210 return resolved
211 }
212 parent := filepath.Dir(current)
213 if parent == current {
214 return path
215 }
216 missing = append(missing, filepath.Base(current))
217 current = parent
218 }
219 }
220
220 lines GO