返回 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 "reasonix/internal/pathidentity"
10 )
11
12 // resolveConfigAccessPath resolves a config file symlink before any content is
13 // read or written. User config links are an explicit user choice and may target
14 // any valid file. Project links are repository-controlled, so their final target
15 // must stay within the project root (the directory containing reasonix.toml).
16 func resolveConfigAccessPath(path string, userConfig bool) (string, error) {
17 if resolved, ok := pinnedConfigEditPath(path); ok {
18 return resolved, nil
19 }
20 return resolveConfigAccessPathUnpinned(path, userConfig)
21 }
22
23 func resolveConfigAccessPathUnpinned(path string, userConfig bool) (string, error) {
24 path = strings.TrimSpace(path)
25 if path == "" {
26 return "", fmt.Errorf("config path is empty")
27 }
28 logical, err := filepath.Abs(filepath.Clean(path))
29 if err != nil {
30 return "", fmt.Errorf("resolve config path %q: %w", path, err)
31 }
32 resolved, err := evalSymlinksAllowMissing(logical)
33 if err != nil && plainConfigEntry(logical) {
34 return logical, nil
35 }
36 if err != nil {
37 scope := "project"
38 if userConfig {
39 scope = "user"
40 }
41 return "", fmt.Errorf("resolve %s config path %q: %w", scope, logical, err)
42 }
43 resolved = filepath.Clean(resolved)
44 if userConfig {
45 return resolved, nil
46 }
47
48 root, err := evalSymlinksAllowMissing(filepath.Dir(logical))
49 if err != nil {
50 return "", fmt.Errorf("resolve project root %q: %w", root, err)
51 }
52 root = filepath.Clean(root)
53 if !pathWithinRoot(root, resolved) {
54 return "", fmt.Errorf("project config path %q resolves outside project root %q: %q", logical, root, resolved)
55 }
56 return resolved, nil
57 }
58
59 // plainConfigEntry reports whether path is absent or a non-link file. Such an
60 // entry lives in its own directory whatever its ancestors resolve to, so a
61 // cloud-drive mount that refuses canonicalization (Box Drive) need not
62 // block it; a link still has to resolve before it is trusted.
63 func plainConfigEntry(path string) bool {
64 info, err := os.Lstat(path)
65 if err != nil {
66 return os.IsNotExist(err)
67 }
68 return info.Mode()&os.ModeSymlink == 0 && !info.IsDir()
69 }
70
71 var resolvePathIdentity = pathidentity.Resolve
72
73 // evalSymlinksAllowMissing canonicalizes every existing path component while
74 // allowing a new file (and missing parent directories) to be created later.
75 // A broken link is not "missing": the shared resolver rejects it, preventing
76 // a write from replacing it. Windows junctions use the same native boundary.
77 func evalSymlinksAllowMissing(path string) (string, error) {
78 absolute, err := filepath.Abs(path)
79 if err != nil {
80 return "", err
81 }
82 identity, err := resolvePathIdentity(absolute, pathidentity.Options{FollowLeaf: true})
83 return identity.PhysicalPath, err
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