返回 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 = 3
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 return NewMatcher(options, nil).Same(a, b)
76 }
77
78 func statForMode(path string, followLeaf bool) (os.FileInfo, error) {
79 if followLeaf {
80 return os.Stat(path)
81 }
82 return os.Lstat(path)
83 }
84
85 func absoluteAccessPath(path, baseDir string) (string, error) {
86 path = strings.TrimSpace(path)
87 if path == "" {
88 return "", &Error{Kind: ErrorInvalid, Stage: "input", Path: path, Err: errors.New("path is empty")}
89 }
90 path = filepath.Clean(path)
91 if !filepath.IsAbs(path) {
92 baseDir = strings.TrimSpace(baseDir)
93 if baseDir == "" || !filepath.IsAbs(baseDir) {
94 return "", &Error{Kind: ErrorInvalid, Stage: "input", Path: path, Err: errors.New("relative path requires an absolute base directory")}
95 }
96 path = filepath.Join(filepath.Clean(baseDir), path)
97 }
98 return filepath.Clean(path), nil
99 }
100
101 func resolvePhysicalPath(access string, followLeaf bool) (string, error) {
102 target, leaf := access, ""
103 if !followLeaf {
104 parent := filepath.Dir(access)
105 if parent != access {
106 target, leaf = parent, filepath.Base(access)
107 }
108 }
109 resolved, err := resolveThroughExistingAncestor(target)
110 if err != nil {
111 return "", err
112 }
113 if leaf != "" {
114 resolved = filepath.Join(resolved, leaf)
115 }
116 return filepath.Clean(resolved), nil
117 }
118
119 func resolveThroughExistingAncestor(path string) (string, error) {
120 return resolveThroughExistingAncestorWith(path, resolveExistingPath)
121 }
122
123 func resolveThroughExistingAncestorWith(path string, evalSymlinks func(string) (string, error)) (string, error) {
124 current := filepath.Clean(path)
125 missing := make([]string, 0, 4)
126 for {
127 // Inspect existence before resolving links. A missing entry may be
128 // created by another lock contender; that is not a dangling link.
129 _, err := os.Lstat(current)
130 if err == nil {
131 resolved, resolveErr := evalSymlinks(current)
132 if resolveErr != nil {
133 return "", classify("physical", current, resolveErr)
134 }
135 for _, part := range slices.Backward(missing) {
136 resolved = filepath.Join(resolved, part)
137 }
138 return filepath.Clean(resolved), nil
139 }
140 if !os.IsNotExist(err) {
141 return "", classify("physical", current, err)
142 }
143 parent := filepath.Dir(current)
144 if parent == current {
145 return "", &Error{Kind: ErrorUnavailable, Stage: "physical", Path: path, Err: err}
146 }
147 missing = append(missing, filepath.Base(current))
148 current = parent
149 }
150 }
151
152 func classify(stage, path string, err error) error {
153 kind := ErrorUnknown
154 switch {
155 case errors.Is(err, os.ErrPermission):
156 kind = ErrorPermission
157 case errors.Is(err, syscall.ELOOP) || platformLinkLoop(err) || strings.Contains(strings.ToLower(err.Error()), "too many links"):
158 kind = ErrorLinkLoop
159 case errors.Is(err, os.ErrNotExist):
160 kind = ErrorUnavailable
161 }
162 return &Error{Kind: kind, Stage: stage, Path: path, Err: err}
163 }
164
165 // Canonical is the frozen v1 compatibility key used by historical locks.
166 // New identity-sensitive code must use Resolve so errors are not discarded.
167 func Canonical(path string) string {
168 key := filepath.Clean(strings.TrimSpace(path))
169 if abs, err := filepath.Abs(key); err == nil {
170 key = abs
171 }
172 key = legacyResolvePathThroughExistingAncestor(key)
173 if runtime.GOOS == "windows" {
174 if strings.HasPrefix(strings.ToUpper(key), `\\?\UNC\`) {
175 key = `\\` + key[len(`\\?\UNC\`):]
176 } else {
177 key = strings.TrimPrefix(key, `\\?\`)
178 }
179 key = strings.ToLower(key)
180 }
181 return key
182 }
183
184 func legacyResolvePathThroughExistingAncestor(path string) string {
185 current := filepath.Clean(path)
186 missing := make([]string, 0, 4)
187 for {
188 if resolved, err := filepath.EvalSymlinks(current); err == nil {
189 for _, part := range slices.Backward(missing) {
190 resolved = filepath.Join(resolved, part)
191 }
192 return resolved
193 }
194 parent := filepath.Dir(current)
195 if parent == current {
196 return path
197 }
198 missing = append(missing, filepath.Base(current))
199 current = parent
200 }
201 }
202
202 lines GO