cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.81.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/discovery/glob.go

214lines · modecode

1package discovery
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "log/slog"
9 "os"
10 "path/filepath"
11 "regexp"
12
13 "github.com/cloudflare/pint/internal/git"
14 "github.com/cloudflare/pint/internal/parser"
15)
16
17func NewGlobFinder(patterns []string, filter git.PathFilter, opts parser.Options, allowedOwners []*regexp.Regexp) GlobFinder {
18 return GlobFinder{
19 patterns: patterns,
20 filter: filter,
21 opts: opts,
22 allowedOwners: allowedOwners,
23 }
24}
25
26type GlobFinder struct {
27 filter git.PathFilter
28 patterns []string
29 allowedOwners []*regexp.Regexp
30 opts parser.Options
31}
32
33func (f GlobFinder) Find() (entries []*Entry, err error) {
34 // Collect unique file paths from all glob patterns.
35 paths := filePaths{}
36 for _, p := range f.patterns {
37 matches, err := filepath.Glob(p)
38 if err != nil {
39 return nil, fmt.Errorf("failed to expand file path pattern %s: %w", p, err)
40 }
41
42 for _, path := range matches {
43 if path == ".git" && isDir(path) {
44 slog.LogAttrs(
45 context.Background(), slog.LevelDebug,
46 "Excluding git directory from glob results",
47 slog.String("path", path),
48 slog.String("glob", p),
49 )
50 continue
51 }
52
53 subpaths, err := findFiles(path)
54 if err != nil {
55 return nil, err
56 }
57 for _, subpath := range subpaths {
58 if !paths.hasPath(subpath.path) {
59 paths = append(paths, subpath)
60 }
61 }
62 }
63 }
64
65 if len(paths) == 0 {
66 return nil, errors.New("no matching files")
67 }
68
69 // Parse each allowed file and extract rule entries.
70 for _, fp := range paths {
71 if !f.filter.IsPathAllowed(fp.path) {
72 continue
73 }
74
75 if fp.err != nil {
76 entries = append(entries, &Entry{
77 State: Noop,
78 Path: Path{
79 Name: fp.path,
80 SymlinkTarget: fp.path,
81 },
82 PathError: fp.err,
83 })
84 continue
85 }
86
87 fd, err := os.Open(fp.path)
88 if err != nil {
89 entries = append(entries, &Entry{
90 State: Noop,
91 Path: Path{
92 Name: fp.path,
93 SymlinkTarget: fp.path,
94 },
95 PathError: err,
96 })
97 continue
98 }
99 p := parser.NewParser(f.opts.WithStrict(!f.filter.IsRelaxed(fp.target)))
100 entries = append(entries, readRules(fp.target, fp.path, fd, p, f.allowedOwners, nil)...)
101 fd.Close()
102 }
103
104 slog.LogAttrs(context.Background(), slog.LevelDebug, "Glob finder completed", slog.Int("count", len(entries)))
105 return entries, nil
106}
107
108func isDir(path string) bool {
109 info, err := os.Stat(path)
110 if err != nil {
111 return false
112 }
113 return info.IsDir()
114}
115
116type filePath struct {
117 err error
118 path string
119 target string
120}
121
122type filePaths []filePath
123
124func (fps filePaths) hasPath(p string) bool {
125 for _, fp := range fps {
126 if fp.path == p {
127 return true
128 }
129 }
130 return false
131}
132
133// resolveFileInfo resolves symlinks and stats the given path.
134// Returns the resolved target path and file info.
135// statPath is the path to os.Stat — it can differ from the EvalSymlinks input
136// (e.g. walkDir stats the resolved dest, findFiles stats the original path).
137func resolveFileInfo(evalPath, statPath string) (target string, info os.FileInfo, err error) {
138 target, err = filepath.EvalSymlinks(evalPath)
139 if err != nil {
140 return "", nil, fmt.Errorf("this is a symlink but target file cannot be evaluated: %w", err)
141 }
142
143 info, err = os.Stat(statPath)
144 if err != nil {
145 return "", nil, err
146 }
147
148 return target, info, nil
149}
150
151func findFiles(path string) (filePaths, error) {
152 target, info, err := resolveFileInfo(path, path)
153 if err != nil {
154 return filePaths{{
155 err: err,
156 path: path,
157 target: "",
158 }}, nil
159 }
160
161 var paths filePaths
162
163 // nolint: exhaustive
164 switch {
165 case info.IsDir():
166 subpaths, err := walkDir(path)
167 if err != nil {
168 return nil, err
169 }
170 paths = append(paths, subpaths...)
171 default:
172 paths = append(paths, filePath{err: nil, path: path, target: target})
173 }
174
175 return paths, nil
176}
177
178func walkDir(dirname string) (paths filePaths, err error) {
179 err = filepath.WalkDir(dirname,
180 func(path string, d fs.DirEntry, err error) error {
181 if err != nil {
182 return err
183 }
184
185 // Skip directories - WalkDir recurses into them automatically.
186 if d.Type() == fs.ModeDir {
187 return nil
188 }
189
190 dest, info, err := resolveFileInfo(path, path)
191 if err != nil {
192 paths = append(paths, filePath{
193 err: err,
194 path: path,
195 target: "",
196 })
197 return nil
198 }
199 // Symlink pointing to a directory, recurse manually since WalkDir won't follow symlinks.
200 if info.IsDir() {
201 subpaths, err := findFiles(dest)
202 if err != nil {
203 return err
204 }
205 paths = append(paths, subpaths...)
206 } else {
207 paths = append(paths, filePath{err: nil, path: path, target: dest})
208 }
209
210 return nil
211 })
212
213 return paths, err
214}
215