返回 DeepSeek-Reasonix
path_access.go
根目录 / internal / config / path_access.go
1 package config
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "slices"
8 "strings"
9 )
10
11 // resolveConfigAccessPath resolves a config file symlink before any content is
12 // read or written. User config links are an explicit user choice and may target
13 // any valid file. Project links are repository-controlled, so their final target
14 // must stay within the project root (the directory containing reasonix.toml).
15 func resolveConfigAccessPath(path string, userConfig bool) (string, error) {
16 if resolved, ok := pinnedConfigEditPath(path); ok {
17 return resolved, nil
18 }
19 return resolveConfigAccessPathUnpinned(path, userConfig)
20 }
21
22 func resolveConfigAccessPathUnpinned(path string, userConfig bool) (string, error) {
23 path = strings.TrimSpace(path)
24 if path == "" {
25 return "", fmt.Errorf("config path is empty")
26 }
27 logical, err := filepath.Abs(filepath.Clean(path))
28 if err != nil {
29 return "", fmt.Errorf("resolve config path %q: %w", path, err)
30 }
31 resolved, err := evalSymlinksAllowMissing(logical)
32 if err != nil {
33 scope := "project"
34 if userConfig {
35 scope = "user"
36 }
37 return "", fmt.Errorf("resolve %s config path %q: %w", scope, logical, err)
38 }
39 resolved = filepath.Clean(resolved)
40 if userConfig {
41 return resolved, nil
42 }
43
44 root, err := evalSymlinksAllowMissing(filepath.Dir(logical))
45 if err != nil {
46 return "", fmt.Errorf("resolve project root %q: %w", root, err)
47 }
48 root = filepath.Clean(root)
49 if !pathWithinRoot(root, resolved) {
50 return "", fmt.Errorf("project config path %q resolves outside project root %q: %q", logical, root, resolved)
51 }
52 return resolved, nil
53 }
54
55 // evalSymlinksAllowMissing canonicalizes every existing path component while
56 // allowing a new file (and missing parent directories) to be created later.
57 // A broken final symlink is not "missing": EvalSymlinks sees the link and
58 // returns an error, which prevents a write from replacing it.
59 func evalSymlinksAllowMissing(path string) (string, error) {
60 path = filepath.Clean(path)
61 current := path
62 var suffix []string
63 for {
64 if _, err := os.Lstat(current); err == nil {
65 resolved, err := filepath.EvalSymlinks(current)
66 if err != nil {
67 return "", err
68 }
69 for _, v := range slices.Backward(suffix) {
70 resolved = filepath.Join(resolved, v)
71 }
72 return filepath.Clean(resolved), nil
73 } else if !os.IsNotExist(err) {
74 return "", err
75 }
76
77 parent := filepath.Dir(current)
78 if parent == current {
79 return path, nil
80 }
81 suffix = append(suffix, filepath.Base(current))
82 current = parent
83 }
84 }
85
86 func resolveConfigReadPath(path string) (string, error) {
87 return resolveConfigAccessPath(path, isUserConfigPath(path))
88 }
89
90 func statConfigPath(path string) (resolved string, exists bool, err error) {
91 resolved, err = resolveConfigReadPath(path)
92 if err != nil {
93 return "", false, err
94 }
95 if _, err = os.Stat(resolved); err != nil {
96 if os.IsNotExist(err) {
97 return resolved, false, nil
98 }
99 return "", false, err
100 }
101 return resolved, true, nil
102 }
103
104 func pathWithinRoot(root, path string) bool {
105 rel, err := filepath.Rel(root, path)
106 if err != nil || filepath.IsAbs(rel) {
107 return false
108 }
109 return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
110 }
111
111 lines GO