Skip to content

Commit 7799c74

Browse files
authored
Merge pull request #595 from jfly/fix-treefmt-dot-in-symlinked-project-root
fix: `treefmt .` no longer segfaults if `.` is root and symlink
2 parents 64799cc + 109948a commit 7799c74

File tree

4 files changed

+95
-85
lines changed

4 files changed

+95
-85
lines changed

cmd/format/format.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import (
77
"io"
88
"os"
99
"os/signal"
10-
"path/filepath"
1110
"runtime/pprof"
12-
"strings"
1311
"syscall"
1412
"time"
1513

@@ -124,10 +122,6 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
124122
return errors.New("exactly one path should be specified when using the --stdin flag")
125123
}
126124

127-
if err = resolvePaths(paths, walkType, cfg.TreeRoot); err != nil {
128-
return err
129-
}
130-
131125
// create a composite formatter which will handle applying the correct formatters to each file we traverse
132126
formatter, err := format.NewCompositeFormatter(cfg, statz, BatchSize)
133127
if err != nil {
@@ -213,60 +207,3 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
213207

214208
return nil
215209
}
216-
217-
// resolvePaths checks all paths are contained within the tree root and exist
218-
// also "normalize" paths so they're relative to `cfg.TreeRoot`
219-
// Symlinks are allowed in `paths` and we resolve them here, since
220-
// the readers will ignore symlinks.
221-
func resolvePaths(paths []string, walkType walk.Type, treeRoot string) error {
222-
// Note: `treeRoot` may itself be or contain a symlink (e.g. it is in
223-
// `$TMPDIR` on macOS or a user has set a symlink to shorten the repository
224-
// path for path length restrictions), so we resolve it here first.
225-
//
226-
// See: https://github.com/numtide/treefmt/issues/578
227-
treeRoot, err := resolvePath(walkType, treeRoot)
228-
if err != nil {
229-
return fmt.Errorf("error computing absolute path of %s: %w", treeRoot, err)
230-
}
231-
232-
for i, path := range paths {
233-
absolutePath, err := resolvePath(walkType, path)
234-
if err != nil {
235-
return fmt.Errorf("error computing absolute path of %s: %w", path, err)
236-
}
237-
238-
relativePath, err := filepath.Rel(treeRoot, absolutePath)
239-
if err != nil {
240-
return fmt.Errorf("error computing relative path from %s to %s: %w", treeRoot, absolutePath, err)
241-
}
242-
243-
if strings.HasPrefix(relativePath, "..") {
244-
return fmt.Errorf("path %s not inside the tree root %s", path, treeRoot)
245-
}
246-
247-
paths[i] = relativePath
248-
}
249-
250-
return nil
251-
}
252-
253-
// Resolve a path to an absolute path, resolving symlinks if necessary.
254-
func resolvePath(walkType walk.Type, path string) (string, error) {
255-
log.Debugf("Resolving path '%s': %v", path, walkType)
256-
257-
absolutePath, err := filepath.Abs(path)
258-
if err != nil {
259-
return "", fmt.Errorf("error computing absolute path of %s: %w", path, err)
260-
}
261-
262-
if walkType != walk.Stdin {
263-
realPath, err := filepath.EvalSymlinks(absolutePath)
264-
if err != nil {
265-
return "", fmt.Errorf("path %s not found: %w", absolutePath, err)
266-
}
267-
268-
absolutePath = realPath
269-
}
270-
271-
return absolutePath, nil
272-
}

cmd/root_test.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,9 +1439,9 @@ func TestGit(t *testing.T) {
14391439
withConfig(configPath, cfg),
14401440
withNoError(t),
14411441
withStats(t, map[stats.Type]int{
1442-
stats.Traversed: 79,
1443-
stats.Matched: 79,
1444-
stats.Formatted: 51, // the echo formatter should only be applied to the new files
1442+
stats.Traversed: 80,
1443+
stats.Matched: 80,
1444+
stats.Formatted: 52, // the echo formatter should only be applied to the new files
14451445
stats.Changed: 0,
14461446
}),
14471447
)
@@ -1923,7 +1923,7 @@ func TestStdin(t *testing.T) {
19231923
as.ErrorContains(err, "path ../test.nix not inside the tree root "+tempDir)
19241924
}),
19251925
withStderr(func(out []byte) {
1926-
as.Contains(string(out), "Error: path ../test.nix not inside the tree root")
1926+
as.Contains(string(out), "Error: failed to create walker: path ../test.nix not inside the tree root")
19271927
}),
19281928
)
19291929

@@ -2136,9 +2136,40 @@ func TestProjectRootIsSymlink(t *testing.T) {
21362136
configPath := filepath.Join(symlinkRoot, "/treefmt.toml")
21372137
test.WriteConfig(t, configPath, cfg)
21382138

2139+
// Verify we can format a specific file.
21392140
treefmt(t,
21402141
withArgs("-c", "go/main.go"),
21412142
withNoError(t),
2143+
withStats(t, map[stats.Type]int{
2144+
stats.Traversed: 1,
2145+
stats.Matched: 1,
2146+
stats.Formatted: 1,
2147+
stats.Changed: 0,
2148+
}),
2149+
)
2150+
2151+
// Verify we can format a specific directory that is a symlink.
2152+
treefmt(t,
2153+
withArgs("-c", "symlink-to-yaml-dir"),
2154+
withNoError(t),
2155+
withStats(t, map[stats.Type]int{
2156+
stats.Traversed: 1,
2157+
stats.Matched: 1,
2158+
stats.Formatted: 1,
2159+
stats.Changed: 0,
2160+
}),
2161+
)
2162+
2163+
// Verify we can format the current directory (which is a symlink!).
2164+
treefmt(t,
2165+
withArgs("-c", "."),
2166+
withNoError(t),
2167+
withStats(t, map[stats.Type]int{
2168+
stats.Traversed: 32,
2169+
stats.Matched: 32,
2170+
stats.Formatted: 32,
2171+
stats.Changed: 0,
2172+
}),
21422173
)
21432174
}
21442175

test/examples/symlink-to-yaml-dir

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

walk/walk.go

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"io/fs"
1010
"os"
1111
"path/filepath"
12+
"strings"
1213

14+
"github.com/charmbracelet/log"
1315
"github.com/numtide/treefmt/v2/stats"
1416
bolt "go.etcd.io/bbolt"
1517
)
@@ -245,7 +247,17 @@ func NewCompositeReader(
245247
db *bolt.DB,
246248
statz *stats.Stats,
247249
) (Reader, error) {
248-
// if not paths are provided we default to processing the tree root
250+
// Note: `root` may itself be or contain a symlink (e.g. it is in
251+
// `$TMPDIR` on macOS or a user has set a symlink to shorten the repository
252+
// path for path length restrictions), so we resolve it here first.
253+
//
254+
// See: https://github.com/numtide/treefmt/issues/578
255+
root, err := resolvePath(root)
256+
if err != nil {
257+
return nil, fmt.Errorf("error resolving path %s: %w", root, err)
258+
}
259+
260+
// if no paths are provided we default to processing the tree root
249261
if len(paths) == 0 {
250262
return NewReader(walkType, root, "", db, statz)
251263
}
@@ -258,44 +270,73 @@ func NewCompositeReader(
258270
return nil, errors.New("stdin walk requires exactly one path")
259271
}
260272

261-
return NewStdinReader(root, paths[0], statz), nil
273+
path := paths[0]
274+
275+
if strings.HasPrefix(path, "..") {
276+
return nil, fmt.Errorf("path %s not inside the tree root %s", path, root)
277+
}
278+
279+
return NewStdinReader(root, path, statz), nil
262280
}
263281

264282
// create a reader for each provided path
265-
for idx, relPath := range paths {
283+
for idx, path := range paths {
266284
var (
267285
err error
268286
info os.FileInfo
269287
)
270288

271-
// create a clean absolute path
272-
path := filepath.Clean(filepath.Join(root, relPath))
289+
resolvedPath, err := resolvePath(path)
290+
if err != nil {
291+
return nil, fmt.Errorf("error resolving path %s: %w", path, err)
292+
}
293+
294+
relativePath, err := filepath.Rel(root, resolvedPath)
295+
if err != nil {
296+
return nil, fmt.Errorf("error computing relative path from %s to %s: %w", root, resolvedPath, err)
297+
}
298+
299+
if strings.HasPrefix(relativePath, "..") {
300+
return nil, fmt.Errorf("path %s not inside the tree root %s", path, root)
301+
}
273302

274-
// check the path exists (don't follow symlinks)
275-
info, err = os.Lstat(path)
303+
// check the path exists
304+
info, err = os.Lstat(resolvedPath)
276305
if err != nil {
277-
return nil, fmt.Errorf("failed to stat %s: %w", path, err)
306+
return nil, fmt.Errorf("failed to stat %s: %w", resolvedPath, err)
278307
}
279308

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():
309+
if info.IsDir() {
286310
// for directories, we honour the walk type as we traverse them
287-
readers[idx], err = NewReader(walkType, root, relPath, db, statz)
288-
default:
311+
readers[idx], err = NewReader(walkType, root, relativePath, db, statz)
312+
} else {
289313
// for files, we enforce a simple filesystem read
290-
readers[idx], err = NewReader(Filesystem, root, relPath, db, statz)
314+
readers[idx], err = NewReader(Filesystem, root, relativePath, db, statz)
291315
}
292316

293317
if err != nil {
294-
return nil, fmt.Errorf("failed to create reader for %s: %w", relPath, err)
318+
return nil, fmt.Errorf("failed to create reader for %s: %w", relativePath, err)
295319
}
296320
}
297321

298322
return &CompositeReader{
299323
readers: readers,
300324
}, nil
301325
}
326+
327+
// Resolve a path to an absolute path, resolving any symlinks along the way.
328+
func resolvePath(path string) (string, error) {
329+
log.Debugf("Resolving path '%s'", path)
330+
331+
absolutePath, err := filepath.Abs(path)
332+
if err != nil {
333+
return "", fmt.Errorf("error computing absolute path of %s: %w", path, err)
334+
}
335+
336+
resolvedPath, err := filepath.EvalSymlinks(absolutePath)
337+
if err != nil {
338+
return "", fmt.Errorf("path %s not found: %w", absolutePath, err)
339+
}
340+
341+
return resolvedPath, nil
342+
}

0 commit comments

Comments
 (0)