Skip to content

fix: ignoring symlinks #528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 41 additions & 26 deletions cmd/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)

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

// checks all paths are contained within the tree root and exist
// also "normalize" paths so they're relative to cfg.TreeRoot
for i, path := range paths {
absolutePath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("error computing absolute path of %s: %w", path, err)
}

relativePath, err := filepath.Rel(cfg.TreeRoot, absolutePath)
if err != nil {
return fmt.Errorf("error computing relative path from %s to %s: %w", cfg.TreeRoot, absolutePath, err)
}

if strings.HasPrefix(relativePath, "..") {
return fmt.Errorf("path %s not inside the tree root %s", path, cfg.TreeRoot)
}

paths[i] = relativePath

if walkType != walk.Stdin {
if _, err = os.Stat(absolutePath); err != nil {
return fmt.Errorf("path %s not found", path)
}
}
if err = resolvePaths(paths, walkType, cfg.TreeRoot); err != nil {
return err
}

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

return nil
}

// resolvePaths checks all paths are contained within the tree root and exist
// also "normalize" paths so they're relative to `cfg.TreeRoot`
// Symlinks are allowed in `paths` and we resolve them here, since
// the readers will ignore symlinks.
func resolvePaths(paths []string, walkType walk.Type, treeRoot string) error {
for i, path := range paths {
log.Errorf("Resolving path '%s': %v", path, walkType)

absolutePath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("error computing absolute path of %s: %w", path, err)
}

if walkType != walk.Stdin {
realPath, err := filepath.EvalSymlinks(absolutePath)
if err != nil {
return fmt.Errorf("path %s not found: %w", absolutePath, err)
}

absolutePath = realPath
}

relativePath, err := filepath.Rel(treeRoot, absolutePath)
if err != nil {
return fmt.Errorf("error computing relative path from %s to %s: %w", treeRoot, absolutePath, err)
}

if strings.HasPrefix(relativePath, "..") {
return fmt.Errorf("path %s not inside the tree root %s", path, treeRoot)
}

paths[i] = relativePath
}

return nil
}
14 changes: 7 additions & 7 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1411,9 +1411,9 @@ func TestGit(t *testing.T) {
withConfig(configPath, cfg),
withNoError(t),
withStats(t, map[stats.Type]int{
stats.Traversed: 78,
stats.Matched: 78,
stats.Formatted: 48, // the echo formatter should only be applied to the new files
stats.Traversed: 79,
stats.Matched: 79,
stats.Formatted: 49, // the echo formatter should only be applied to the new files
stats.Changed: 0,
}),
)
Expand Down Expand Up @@ -1463,7 +1463,7 @@ func TestGit(t *testing.T) {
withArgs("-C", tempDir, "haskell", "foo"),
withConfig(configPath, cfg),
withError(func(as *require.Assertions, err error) {
as.ErrorContains(err, "path foo not found")
as.ErrorContains(err, "foo not found")
}),
)

Expand Down Expand Up @@ -1592,7 +1592,7 @@ func TestPathsArg(t *testing.T) {
treefmt(t,
withArgs("rust/src/main.rs", "haskell/Nested/Bar.hs"),
withError(func(as *require.Assertions, err error) {
as.ErrorContains(err, "path haskell/Nested/Bar.hs not found")
as.ErrorContains(err, "Bar.hs not found")
}),
)

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

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

treefmt(t,
withArgs(relativeExternalPath),
Expand Down Expand Up @@ -1842,7 +1842,7 @@ func TestRunInSubdir(t *testing.T) {
treefmt(t,
withArgs("-c", "go/main.go", "haskell/Nested/Foo.hs"),
withError(func(as *require.Assertions, err error) {
as.ErrorContains(err, "path go/main.go not found")
as.ErrorContains(err, "go/main.go not found")
}),
)

Expand Down
1 change: 1 addition & 0 deletions test/examples/yaml/test-rel-symlink.yaml
13 changes: 10 additions & 3 deletions walk/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,30 @@ LOOP:

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

info, err := os.Stat(path)
if os.IsNotExist(err) {
info, err := os.Lstat(path)

switch {
case os.IsNotExist(err):
// the underlying file might have been removed
g.log.Warnf(
"Path %s is in the worktree but appears to have been removed from the filesystem", path,
)

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

files[n] = &File{
Path: path,
RelPath: filepath.Join(g.path, entry),
Info: info,
}

n++
} else {
// nothing more to read
Expand Down
14 changes: 11 additions & 3 deletions walk/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ func NewReader(
return reader, err
}

// NewCompositeReader returns a composite reader for the `root` and all `paths`. It
// never follows symlinks.
//
//nolint:ireturn
func NewCompositeReader(
walkType Type,
Expand Down Expand Up @@ -268,16 +271,21 @@ func NewCompositeReader(
// create a clean absolute path
path := filepath.Clean(filepath.Join(root, relPath))

// check the path exists
// check the path exists (don't follow symlinks)
info, err = os.Lstat(path)
if err != nil {
return nil, fmt.Errorf("failed to stat %s: %w", path, err)
}

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