Skip to content

Commit 0bfce5b

Browse files
committed
extract module request and path resolution helpers
1 parent f70e4b4 commit 0bfce5b

2 files changed

Lines changed: 168 additions & 159 deletions

File tree

vibes/modules.go

Lines changed: 0 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"io/fs"
77
"os"
88
"path/filepath"
9-
"slices"
10-
"strings"
119
)
1210

1311
type moduleEntry struct {
@@ -25,163 +23,6 @@ type moduleRequest struct {
2523

2624
const moduleKeySeparator = "::"
2725

28-
func parseModuleRequest(name string) (moduleRequest, error) {
29-
trimmed := strings.TrimSpace(name)
30-
if trimmed == "" {
31-
return moduleRequest{}, fmt.Errorf("require: module name must be non-empty")
32-
}
33-
normalizedName := strings.ReplaceAll(trimmed, "\\", string(filepath.Separator))
34-
normalizedName = strings.ReplaceAll(normalizedName, "/", string(filepath.Separator))
35-
36-
request := moduleRequest{
37-
raw: name,
38-
explicitRelative: isExplicitRelativeModulePath(trimmed),
39-
}
40-
if filepath.Ext(normalizedName) == "" {
41-
normalizedName += ".vibe"
42-
}
43-
44-
request.normalized = filepath.Clean(normalizedName)
45-
if request.normalized == "." {
46-
return moduleRequest{}, fmt.Errorf("require: module name %q resolves to current directory", name)
47-
}
48-
if filepath.IsAbs(request.normalized) {
49-
return moduleRequest{}, fmt.Errorf("require: module name %q must be relative", name)
50-
}
51-
if !request.explicitRelative && containsPathTraversal(request.normalized) {
52-
return moduleRequest{}, fmt.Errorf("require: module name %q escapes search paths", name)
53-
}
54-
55-
return request, nil
56-
}
57-
58-
func isExplicitRelativeModulePath(name string) bool {
59-
return strings.HasPrefix(name, "./") ||
60-
strings.HasPrefix(name, "../") ||
61-
strings.HasPrefix(name, ".\\") ||
62-
strings.HasPrefix(name, "..\\")
63-
}
64-
65-
func containsPathTraversal(cleanPath string) bool {
66-
normalized := strings.ReplaceAll(filepath.Clean(cleanPath), "\\", "/")
67-
return slices.Contains(strings.Split(normalized, "/"), "..")
68-
}
69-
70-
func moduleCacheKey(root, relative string) string {
71-
return filepath.Clean(root) + moduleKeySeparator + filepath.Clean(relative)
72-
}
73-
74-
func moduleKeyDisplay(key string) string {
75-
idx := strings.LastIndex(key, moduleKeySeparator)
76-
if idx < 0 {
77-
return key
78-
}
79-
display := key[idx+len(moduleKeySeparator):]
80-
if display == "" {
81-
return key
82-
}
83-
return display
84-
}
85-
86-
func moduleDisplayName(key string) string {
87-
display := filepath.ToSlash(moduleKeyDisplay(key))
88-
return strings.TrimSuffix(display, ".vibe")
89-
}
90-
91-
func moduleRelativePath(root, fullPath string) (string, error) {
92-
rel, err := moduleRelativePathLexical(root, fullPath)
93-
if err != nil {
94-
return "", err
95-
}
96-
cleanRoot := filepath.Clean(root)
97-
cleanPath := filepath.Clean(fullPath)
98-
99-
resolvedRoot, err := resolvedExistingPath(cleanRoot)
100-
if err != nil {
101-
return "", err
102-
}
103-
resolvedPath, err := resolvedPathWithMissing(cleanPath)
104-
if err != nil {
105-
return "", err
106-
}
107-
resolvedRel, err := filepath.Rel(resolvedRoot, resolvedPath)
108-
if err != nil {
109-
return "", err
110-
}
111-
resolvedRel = filepath.Clean(resolvedRel)
112-
sep := string(filepath.Separator)
113-
if resolvedRel == ".." || strings.HasPrefix(resolvedRel, ".."+sep) || filepath.IsAbs(resolvedRel) {
114-
return "", fmt.Errorf("require: module path %q escapes module root %q", cleanPath, cleanRoot)
115-
}
116-
return rel, nil
117-
}
118-
119-
func moduleRelativePathLexical(root, fullPath string) (string, error) {
120-
cleanRoot := filepath.Clean(root)
121-
cleanPath := filepath.Clean(fullPath)
122-
123-
rel, err := filepath.Rel(cleanRoot, cleanPath)
124-
if err != nil {
125-
return "", err
126-
}
127-
rel = filepath.Clean(rel)
128-
sep := string(filepath.Separator)
129-
if rel == ".." || strings.HasPrefix(rel, ".."+sep) || filepath.IsAbs(rel) {
130-
return "", fmt.Errorf("require: module path %q escapes module root %q", cleanPath, cleanRoot)
131-
}
132-
return rel, nil
133-
}
134-
135-
func resolvedExistingPath(path string) (string, error) {
136-
absPath, err := filepath.Abs(path)
137-
if err != nil {
138-
return "", err
139-
}
140-
resolvedPath, err := filepath.EvalSymlinks(absPath)
141-
if err != nil {
142-
return "", err
143-
}
144-
return filepath.Clean(resolvedPath), nil
145-
}
146-
147-
func resolvedPathWithMissing(path string) (string, error) {
148-
absPath, err := filepath.Abs(path)
149-
if err != nil {
150-
return "", err
151-
}
152-
cleanPath := filepath.Clean(absPath)
153-
154-
existing := cleanPath
155-
suffix := make([]string, 0, 4)
156-
157-
for {
158-
_, statErr := os.Lstat(existing)
159-
if statErr == nil {
160-
break
161-
}
162-
if !errors.Is(statErr, fs.ErrNotExist) {
163-
return "", statErr
164-
}
165-
parent := filepath.Dir(existing)
166-
if parent == existing {
167-
return "", statErr
168-
}
169-
suffix = append(suffix, filepath.Base(existing))
170-
existing = parent
171-
}
172-
173-
resolvedExisting, err := filepath.EvalSymlinks(existing)
174-
if err != nil {
175-
return "", err
176-
}
177-
178-
resolved := filepath.Clean(resolvedExisting)
179-
for i := len(suffix) - 1; i >= 0; i-- {
180-
resolved = filepath.Join(resolved, suffix[i])
181-
}
182-
return resolved, nil
183-
}
184-
18526
func (e *Engine) getCachedModule(key string) (moduleEntry, bool) {
18627
e.modMu.RLock()
18728
entry, ok := e.modules[key]

vibes/modules_paths.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package vibes
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"slices"
10+
"strings"
11+
)
12+
13+
func parseModuleRequest(name string) (moduleRequest, error) {
14+
trimmed := strings.TrimSpace(name)
15+
if trimmed == "" {
16+
return moduleRequest{}, fmt.Errorf("require: module name must be non-empty")
17+
}
18+
normalizedName := strings.ReplaceAll(trimmed, "\\", string(filepath.Separator))
19+
normalizedName = strings.ReplaceAll(normalizedName, "/", string(filepath.Separator))
20+
21+
request := moduleRequest{
22+
raw: name,
23+
explicitRelative: isExplicitRelativeModulePath(trimmed),
24+
}
25+
if filepath.Ext(normalizedName) == "" {
26+
normalizedName += ".vibe"
27+
}
28+
29+
request.normalized = filepath.Clean(normalizedName)
30+
if request.normalized == "." {
31+
return moduleRequest{}, fmt.Errorf("require: module name %q resolves to current directory", name)
32+
}
33+
if filepath.IsAbs(request.normalized) {
34+
return moduleRequest{}, fmt.Errorf("require: module name %q must be relative", name)
35+
}
36+
if !request.explicitRelative && containsPathTraversal(request.normalized) {
37+
return moduleRequest{}, fmt.Errorf("require: module name %q escapes search paths", name)
38+
}
39+
40+
return request, nil
41+
}
42+
43+
func isExplicitRelativeModulePath(name string) bool {
44+
return strings.HasPrefix(name, "./") ||
45+
strings.HasPrefix(name, "../") ||
46+
strings.HasPrefix(name, ".\\") ||
47+
strings.HasPrefix(name, "..\\")
48+
}
49+
50+
func containsPathTraversal(cleanPath string) bool {
51+
normalized := strings.ReplaceAll(filepath.Clean(cleanPath), "\\", "/")
52+
return slices.Contains(strings.Split(normalized, "/"), "..")
53+
}
54+
55+
func moduleCacheKey(root, relative string) string {
56+
return filepath.Clean(root) + moduleKeySeparator + filepath.Clean(relative)
57+
}
58+
59+
func moduleKeyDisplay(key string) string {
60+
idx := strings.LastIndex(key, moduleKeySeparator)
61+
if idx < 0 {
62+
return key
63+
}
64+
display := key[idx+len(moduleKeySeparator):]
65+
if display == "" {
66+
return key
67+
}
68+
return display
69+
}
70+
71+
func moduleDisplayName(key string) string {
72+
display := filepath.ToSlash(moduleKeyDisplay(key))
73+
return strings.TrimSuffix(display, ".vibe")
74+
}
75+
76+
func moduleRelativePath(root, fullPath string) (string, error) {
77+
rel, err := moduleRelativePathLexical(root, fullPath)
78+
if err != nil {
79+
return "", err
80+
}
81+
cleanRoot := filepath.Clean(root)
82+
cleanPath := filepath.Clean(fullPath)
83+
84+
resolvedRoot, err := resolvedExistingPath(cleanRoot)
85+
if err != nil {
86+
return "", err
87+
}
88+
resolvedPath, err := resolvedPathWithMissing(cleanPath)
89+
if err != nil {
90+
return "", err
91+
}
92+
resolvedRel, err := filepath.Rel(resolvedRoot, resolvedPath)
93+
if err != nil {
94+
return "", err
95+
}
96+
resolvedRel = filepath.Clean(resolvedRel)
97+
sep := string(filepath.Separator)
98+
if resolvedRel == ".." || strings.HasPrefix(resolvedRel, ".."+sep) || filepath.IsAbs(resolvedRel) {
99+
return "", fmt.Errorf("require: module path %q escapes module root %q", cleanPath, cleanRoot)
100+
}
101+
return rel, nil
102+
}
103+
104+
func moduleRelativePathLexical(root, fullPath string) (string, error) {
105+
cleanRoot := filepath.Clean(root)
106+
cleanPath := filepath.Clean(fullPath)
107+
108+
rel, err := filepath.Rel(cleanRoot, cleanPath)
109+
if err != nil {
110+
return "", err
111+
}
112+
rel = filepath.Clean(rel)
113+
sep := string(filepath.Separator)
114+
if rel == ".." || strings.HasPrefix(rel, ".."+sep) || filepath.IsAbs(rel) {
115+
return "", fmt.Errorf("require: module path %q escapes module root %q", cleanPath, cleanRoot)
116+
}
117+
return rel, nil
118+
}
119+
120+
func resolvedExistingPath(path string) (string, error) {
121+
absPath, err := filepath.Abs(path)
122+
if err != nil {
123+
return "", err
124+
}
125+
resolvedPath, err := filepath.EvalSymlinks(absPath)
126+
if err != nil {
127+
return "", err
128+
}
129+
return filepath.Clean(resolvedPath), nil
130+
}
131+
132+
func resolvedPathWithMissing(path string) (string, error) {
133+
absPath, err := filepath.Abs(path)
134+
if err != nil {
135+
return "", err
136+
}
137+
cleanPath := filepath.Clean(absPath)
138+
139+
existing := cleanPath
140+
suffix := make([]string, 0, 4)
141+
142+
for {
143+
_, statErr := os.Lstat(existing)
144+
if statErr == nil {
145+
break
146+
}
147+
if !errors.Is(statErr, fs.ErrNotExist) {
148+
return "", statErr
149+
}
150+
parent := filepath.Dir(existing)
151+
if parent == existing {
152+
return "", statErr
153+
}
154+
suffix = append(suffix, filepath.Base(existing))
155+
existing = parent
156+
}
157+
158+
resolvedExisting, err := filepath.EvalSymlinks(existing)
159+
if err != nil {
160+
return "", err
161+
}
162+
163+
resolved := filepath.Clean(resolvedExisting)
164+
for i := len(suffix) - 1; i >= 0; i-- {
165+
resolved = filepath.Join(resolved, suffix[i])
166+
}
167+
return resolved, nil
168+
}

0 commit comments

Comments
 (0)