Skip to content

Commit ce65379

Browse files
gabyxbrianmcgee
andauthored
fix: ignoring symlinks (#528)
Co-authored-by: Brian McGee <brian@bmcgee.ie>
1 parent 05e7a02 commit ce65379

File tree

5 files changed

+70
-39
lines changed

5 files changed

+70
-39
lines changed

cmd/format/format.go

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
8888

8989
// ensure db is closed after we're finished
9090
defer func() {
91-
if err := db.Close(); err != nil {
92-
log.Errorf("failed to close cache: %v", err)
91+
if closeErr := db.Close(); closeErr != nil {
92+
log.Errorf("failed to close cache: %v", closeErr)
9393
}
9494
}()
9595
}
@@ -126,30 +126,8 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
126126
return errors.New("exactly one path should be specified when using the --stdin flag")
127127
}
128128

129-
// checks all paths are contained within the tree root and exist
130-
// also "normalize" paths so they're relative to cfg.TreeRoot
131-
for i, path := range paths {
132-
absolutePath, err := filepath.Abs(path)
133-
if err != nil {
134-
return fmt.Errorf("error computing absolute path of %s: %w", path, err)
135-
}
136-
137-
relativePath, err := filepath.Rel(cfg.TreeRoot, absolutePath)
138-
if err != nil {
139-
return fmt.Errorf("error computing relative path from %s to %s: %w", cfg.TreeRoot, absolutePath, err)
140-
}
141-
142-
if strings.HasPrefix(relativePath, "..") {
143-
return fmt.Errorf("path %s not inside the tree root %s", path, cfg.TreeRoot)
144-
}
145-
146-
paths[i] = relativePath
147-
148-
if walkType != walk.Stdin {
149-
if _, err = os.Stat(absolutePath); err != nil {
150-
return fmt.Errorf("path %s not found", path)
151-
}
152-
}
129+
if err = resolvePaths(paths, walkType, cfg.TreeRoot); err != nil {
130+
return err
153131
}
154132

155133
// create a composite formatter which will handle applying the correct formatters to each file we traverse
@@ -237,3 +215,40 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
237215

238216
return nil
239217
}
218+
219+
// resolvePaths checks all paths are contained within the tree root and exist
220+
// also "normalize" paths so they're relative to `cfg.TreeRoot`
221+
// Symlinks are allowed in `paths` and we resolve them here, since
222+
// the readers will ignore symlinks.
223+
func resolvePaths(paths []string, walkType walk.Type, treeRoot string) error {
224+
for i, path := range paths {
225+
log.Errorf("Resolving path '%s': %v", path, walkType)
226+
227+
absolutePath, err := filepath.Abs(path)
228+
if err != nil {
229+
return fmt.Errorf("error computing absolute path of %s: %w", path, err)
230+
}
231+
232+
if walkType != walk.Stdin {
233+
realPath, err := filepath.EvalSymlinks(absolutePath)
234+
if err != nil {
235+
return fmt.Errorf("path %s not found: %w", absolutePath, err)
236+
}
237+
238+
absolutePath = realPath
239+
}
240+
241+
relativePath, err := filepath.Rel(treeRoot, absolutePath)
242+
if err != nil {
243+
return fmt.Errorf("error computing relative path from %s to %s: %w", treeRoot, absolutePath, err)
244+
}
245+
246+
if strings.HasPrefix(relativePath, "..") {
247+
return fmt.Errorf("path %s not inside the tree root %s", path, treeRoot)
248+
}
249+
250+
paths[i] = relativePath
251+
}
252+
253+
return nil
254+
}

cmd/root_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,9 +1411,9 @@ func TestGit(t *testing.T) {
14111411
withConfig(configPath, cfg),
14121412
withNoError(t),
14131413
withStats(t, map[stats.Type]int{
1414-
stats.Traversed: 78,
1415-
stats.Matched: 78,
1416-
stats.Formatted: 48, // the echo formatter should only be applied to the new files
1414+
stats.Traversed: 79,
1415+
stats.Matched: 79,
1416+
stats.Formatted: 49, // the echo formatter should only be applied to the new files
14171417
stats.Changed: 0,
14181418
}),
14191419
)
@@ -1463,7 +1463,7 @@ func TestGit(t *testing.T) {
14631463
withArgs("-C", tempDir, "haskell", "foo"),
14641464
withConfig(configPath, cfg),
14651465
withError(func(as *require.Assertions, err error) {
1466-
as.ErrorContains(err, "path foo not found")
1466+
as.ErrorContains(err, "foo not found")
14671467
}),
14681468
)
14691469

@@ -1592,7 +1592,7 @@ func TestPathsArg(t *testing.T) {
15921592
treefmt(t,
15931593
withArgs("rust/src/main.rs", "haskell/Nested/Bar.hs"),
15941594
withError(func(as *require.Assertions, err error) {
1595-
as.ErrorContains(err, "path haskell/Nested/Bar.hs not found")
1595+
as.ErrorContains(err, "Bar.hs not found")
15961596
}),
15971597
)
15981598

@@ -1610,7 +1610,7 @@ func TestPathsArg(t *testing.T) {
16101610

16111611
// specify a relative path outside the tree root
16121612
relativeExternalPath := "../outside_tree.go"
1613-
as.FileExists(relativeExternalPath, "exernal file must exist")
1613+
as.FileExists(relativeExternalPath, "external file must exist")
16141614

16151615
treefmt(t,
16161616
withArgs(relativeExternalPath),
@@ -1842,7 +1842,7 @@ func TestRunInSubdir(t *testing.T) {
18421842
treefmt(t,
18431843
withArgs("-c", "go/main.go", "haskell/Nested/Foo.hs"),
18441844
withError(func(as *require.Assertions, err error) {
1845-
as.ErrorContains(err, "path go/main.go not found")
1845+
as.ErrorContains(err, "go/main.go not found")
18461846
}),
18471847
)
18481848

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../yaml/test.yaml

walk/git.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,30 @@ LOOP:
9191

9292
g.log.Debugf("processing file: %s", path)
9393

94-
info, err := os.Stat(path)
95-
if os.IsNotExist(err) {
94+
info, err := os.Lstat(path)
95+
96+
switch {
97+
case os.IsNotExist(err):
9698
// the underlying file might have been removed
9799
g.log.Warnf(
98100
"Path %s is in the worktree but appears to have been removed from the filesystem", path,
99101
)
100102

101103
continue
102-
} else if err != nil {
104+
case err != nil:
103105
return n, fmt.Errorf("failed to stat %s: %w", path, err)
106+
case info.Mode()&os.ModeSymlink == os.ModeSymlink:
107+
// we skip reporting symlinks stored in Git, they should
108+
// point to local files which we would list anyway.
109+
continue
104110
}
105111

106112
files[n] = &File{
107113
Path: path,
108114
RelPath: filepath.Join(g.path, entry),
109115
Info: info,
110116
}
117+
111118
n++
112119
} else {
113120
// nothing more to read

walk/walk.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ func NewReader(
234234
return reader, err
235235
}
236236

237+
// NewCompositeReader returns a composite reader for the `root` and all `paths`. It
238+
// never follows symlinks.
239+
//
237240
//nolint:ireturn
238241
func NewCompositeReader(
239242
walkType Type,
@@ -268,16 +271,21 @@ func NewCompositeReader(
268271
// create a clean absolute path
269272
path := filepath.Clean(filepath.Join(root, relPath))
270273

271-
// check the path exists
274+
// check the path exists (don't follow symlinks)
272275
info, err = os.Lstat(path)
273276
if err != nil {
274277
return nil, fmt.Errorf("failed to stat %s: %w", path, err)
275278
}
276279

277-
if info.IsDir() {
280+
switch {
281+
case info.Mode()&os.ModeSymlink == os.ModeSymlink:
282+
// for symlinks -> we ignore them since it does not make sense to follow them
283+
// as normal files in the `root` will be picked up nevertheless.
284+
continue
285+
case info.IsDir():
278286
// for directories, we honour the walk type as we traverse them
279287
readers[idx], err = NewReader(walkType, root, relPath, db, statz)
280-
} else {
288+
default:
281289
// for files, we enforce a simple filesystem read
282290
readers[idx], err = NewReader(Filesystem, root, relPath, db, statz)
283291
}

0 commit comments

Comments
 (0)