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