返回 DeepSeek-Reasonix
observe.go
根目录 / tools / pathidentityprobe / observe.go
1 package main
2
3 import (
4 "crypto/sha256"
5 "errors"
6 "fmt"
7 "os"
8 "os/user"
9 "path/filepath"
10 "regexp"
11 "strings"
12 "syscall"
13 "unicode/utf16"
14
15 "reasonix/internal/pathidentity"
16 )
17
18 type failure struct {
19 Type string `json:"type"`
20 Text string `json:"text"`
21 Code uint64 `json:"windows_errno,omitempty"`
22 Stage string `json:"stage,omitempty"`
23 Path string `json:"path,omitempty"`
24 }
25
26 type outcome struct {
27 OK bool `json:"ok"`
28 Path string `json:"path,omitempty"`
29 Mode string `json:"mode,omitempty"`
30 Size int64 `json:"size,omitempty"`
31 Volume string `json:"volume_serial,omitempty"`
32 FileID string `json:"file_id,omitempty"`
33 Identity *pathidentity.Identity `json:"identity,omitempty"`
34 Errors []failure `json:"errors,omitempty"`
35 }
36
37 type observation struct {
38 Path string `json:"path"`
39 UTF16Length int `json:"utf16_length"`
40 Lstat outcome `json:"lstat"`
41 Readlink outcome `json:"readlink"`
42 Eval outcome `json:"filepath_eval_symlinks"`
43 Resolve outcome `json:"reasonix_resolve_follow_leaf"`
44 Preserve outcome `json:"reasonix_resolve_preserve_leaf"`
45 Native outcome `json:"windows_native"`
46 }
47
48 // Each row describes exactly one node; errors.As would attribute inner metadata
49 // to its wrappers. The loop explicitly unwraps and reports those nodes next.
50 //
51 //nolint:errorlint // Direct assertions preserve the diagnostic chain's node boundaries.
52 func failureOutcome(err error) outcome {
53 r := outcome{OK: err == nil}
54 for depth := 0; err != nil && depth < 12; depth++ {
55 f := failure{Type: fmt.Sprintf("%T", err), Text: err.Error()}
56 if e, ok := err.(syscall.Errno); ok {
57 f.Code = uint64(e)
58 }
59 if e, ok := err.(*pathidentity.Error); ok {
60 f.Stage, f.Path = e.Stage, e.Path
61 }
62 if e, ok := err.(*os.PathError); ok {
63 f.Stage, f.Path = e.Op, e.Path
64 }
65 r.Errors = append(r.Errors, f)
66 err = errors.Unwrap(err)
67 }
68 return r
69 }
70
71 func inspect(path string) observation {
72 r := observation{Path: path, UTF16Length: len(utf16.Encode([]rune(path)))}
73 info, err := os.Lstat(path)
74 r.Lstat = failureOutcome(err)
75 if err == nil {
76 r.Lstat.Mode, r.Lstat.Size = info.Mode().String(), info.Size()
77 }
78 link, err := os.Readlink(path)
79 r.Readlink = failureOutcome(err)
80 r.Readlink.Path = link
81 resolved, err := filepath.EvalSymlinks(path)
82 r.Eval = failureOutcome(err)
83 r.Eval.Path = resolved
84 for _, follow := range []bool{true, false} {
85 identity, err := pathidentity.Resolve(path, pathidentity.Options{FollowLeaf: follow})
86 result := failureOutcome(err)
87 if err == nil {
88 result.Identity = &identity
89 }
90 if follow {
91 r.Resolve = result
92 } else {
93 r.Preserve = result
94 }
95 }
96 r.Native = nativePath(path)
97 return r
98 }
99
100 func defaultPaths(current *user.User, extra []string) []string {
101 identity := strings.TrimSpace(current.Uid)
102 if identity == "" {
103 identity = strings.TrimSpace(current.Username)
104 }
105 if identity == "" {
106 identity = strings.TrimSpace(current.HomeDir)
107 }
108 digest := sha256.Sum256([]byte(identity))
109 locks := filepath.Join(current.HomeDir, ".reasonix", "locks", fmt.Sprintf("config-edits-%x", digest[:8]))
110 paths := []string{locks}
111 entries, err := os.ReadDir(locks)
112 if err == nil {
113 for _, entry := range entries {
114 if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lock") {
115 paths = append(paths, filepath.Join(locks, entry.Name()))
116 if len(paths) >= 100 {
117 break
118 }
119 }
120 }
121 }
122 // Never create this entry: it exercises the existing-ancestor branch.
123 paths = append(paths, filepath.Join(locks, "reasonix-probe-uncreated-child", "absent.lock"))
124 paths = append(paths, extra...)
125 var result []string
126 seen := map[string]bool{}
127 for _, path := range paths {
128 absolute, err := filepath.Abs(path)
129 if err != nil {
130 continue
131 }
132 for current := absolute; !seen[current]; current = filepath.Dir(current) {
133 seen[current] = true
134 result = append(result, current)
135 if filepath.Dir(current) == current {
136 break
137 }
138 }
139 }
140 return result
141 }
142
143 func redactValue(value any, current *user.User) any {
144 switch value := value.(type) {
145 case string:
146 if current.Uid != "" {
147 value = strings.ReplaceAll(value, current.Uid, "<SID>")
148 }
149 name := filepath.Base(current.HomeDir)
150 if name != "" && name != "." && name != string(filepath.Separator) {
151 // PathError appends ": <message>"; quoted paths can end in quotes.
152 re := regexp.MustCompile(`(?i)([\\/])` + regexp.QuoteMeta(name) + `([\\/:"'\s]|$)`)
153 value = re.ReplaceAllString(value, "${1}<USER>${2}")
154 }
155 return value
156 case []any:
157 for i, item := range value {
158 value[i] = redactValue(item, current)
159 }
160 return value
161 case map[string]any:
162 for key, item := range value {
163 value[key] = redactValue(item, current)
164 }
165 return value
166 default:
167 return value
168 }
169 }
170
170 lines GO