Skip to content

Commit dcc3db3

Browse files
authored
Merge pull request #29 from arduino/recursion_loops
Detection of recursion loops in ReadDirRecursive* methods
2 parents b3ef88d + b49819c commit dcc3db3

File tree

15 files changed

+122
-50
lines changed

15 files changed

+122
-50
lines changed

Diff for: paths.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ package paths
3232
import (
3333
"fmt"
3434
"io"
35-
"io/ioutil"
3635
"os"
3736
"path/filepath"
3837
"strings"
@@ -418,14 +417,14 @@ func (p *Path) Chtimes(atime, mtime time.Time) error {
418417

419418
// ReadFile reads the file named by filename and returns the contents
420419
func (p *Path) ReadFile() ([]byte, error) {
421-
return ioutil.ReadFile(p.path)
420+
return os.ReadFile(p.path)
422421
}
423422

424423
// WriteFile writes data to a file named by filename. If the file
425424
// does not exist, WriteFile creates it otherwise WriteFile truncates
426425
// it before writing.
427426
func (p *Path) WriteFile(data []byte) error {
428-
return ioutil.WriteFile(p.path, data, os.FileMode(0644))
427+
return os.WriteFile(p.path, data, os.FileMode(0644))
429428
}
430429

431430
// WriteToTempFile writes data to a newly generated temporary file.

Diff for: readdir.go

+42-47
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
package paths
3131

3232
import (
33-
"io/ioutil"
33+
"errors"
34+
"os"
3435
"strings"
3536
)
3637

@@ -41,7 +42,7 @@ type ReadDirFilter func(file *Path) bool
4142
// ReadDir returns a PathList containing the content of the directory
4243
// pointed by the current Path. The resulting list is filtered by the given filters chained.
4344
func (p *Path) ReadDir(filters ...ReadDirFilter) (PathList, error) {
44-
infos, err := ioutil.ReadDir(p.path)
45+
infos, err := os.ReadDir(p.path)
4546
if err != nil {
4647
return nil, err
4748
}
@@ -69,27 +70,7 @@ func (p *Path) ReadDir(filters ...ReadDirFilter) (PathList, error) {
6970
// ReadDirRecursive returns a PathList containing the content of the directory
7071
// and its subdirectories pointed by the current Path
7172
func (p *Path) ReadDirRecursive() (PathList, error) {
72-
infos, err := ioutil.ReadDir(p.path)
73-
if err != nil {
74-
return nil, err
75-
}
76-
paths := PathList{}
77-
for _, info := range infos {
78-
path := p.Join(info.Name())
79-
paths.Add(path)
80-
81-
if isDir, err := path.IsDirCheck(); err != nil {
82-
return nil, err
83-
} else if isDir {
84-
subPaths, err := path.ReadDirRecursive()
85-
if err != nil {
86-
return nil, err
87-
}
88-
paths.AddAll(subPaths)
89-
}
90-
91-
}
92-
return paths, nil
73+
return p.ReadDirRecursiveFiltered(nil)
9374
}
9475

9576
// ReadDirRecursiveFiltered returns a PathList containing the content of the directory
@@ -101,41 +82,55 @@ func (p *Path) ReadDirRecursive() (PathList, error) {
10182
// - `filters` are the filters that are checked to determine if the entry should be
10283
// added to the resulting PathList
10384
func (p *Path) ReadDirRecursiveFiltered(recursionFilter ReadDirFilter, filters ...ReadDirFilter) (PathList, error) {
104-
infos, err := ioutil.ReadDir(p.path)
105-
if err != nil {
106-
return nil, err
107-
}
85+
var search func(*Path) (PathList, error)
10886

109-
accept := func(p *Path) bool {
110-
for _, filter := range filters {
111-
if !filter(p) {
112-
return false
113-
}
87+
explored := map[string]bool{}
88+
search = func(currPath *Path) (PathList, error) {
89+
canonical := currPath.Canonical().path
90+
if explored[canonical] {
91+
return nil, errors.New("directories symlink loop detected")
11492
}
115-
return true
116-
}
93+
explored[canonical] = true
94+
defer delete(explored, canonical)
11795

118-
paths := PathList{}
119-
for _, info := range infos {
120-
path := p.Join(info.Name())
96+
infos, err := os.ReadDir(currPath.path)
97+
if err != nil {
98+
return nil, err
99+
}
121100

122-
if accept(path) {
123-
paths.Add(path)
101+
accept := func(p *Path) bool {
102+
for _, filter := range filters {
103+
if !filter(p) {
104+
return false
105+
}
106+
}
107+
return true
124108
}
125109

126-
if recursionFilter == nil || recursionFilter(path) {
127-
if isDir, err := path.IsDirCheck(); err != nil {
128-
return nil, err
129-
} else if isDir {
130-
subPaths, err := path.ReadDirRecursiveFiltered(recursionFilter, filters...)
131-
if err != nil {
110+
paths := PathList{}
111+
for _, info := range infos {
112+
path := currPath.Join(info.Name())
113+
114+
if accept(path) {
115+
paths.Add(path)
116+
}
117+
118+
if recursionFilter == nil || recursionFilter(path) {
119+
if isDir, err := path.IsDirCheck(); err != nil {
132120
return nil, err
121+
} else if isDir {
122+
subPaths, err := search(path)
123+
if err != nil {
124+
return nil, err
125+
}
126+
paths.AddAll(subPaths)
133127
}
134-
paths.AddAll(subPaths)
135128
}
136129
}
130+
return paths, nil
137131
}
138-
return paths, nil
132+
133+
return search(p)
139134
}
140135

141136
// FilterDirectories is a ReadDirFilter that accepts only directories

Diff for: readdir_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"fmt"
3434
"os"
3535
"testing"
36+
"time"
3637

3738
"github.com/stretchr/testify/require"
3839
)
@@ -245,3 +246,71 @@ func TestReadDirRecursiveFiltered(t *testing.T) {
245246
pathEqualsTo(t, "testdata/fileset/test.txt", l[7])
246247
pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[8])
247248
}
249+
250+
func TestReadDirRecursiveLoopDetection(t *testing.T) {
251+
loopsPath := New("testdata", "loops")
252+
unbuondedReaddir := func(testdir string) (PathList, error) {
253+
// This is required to unbound the recursion, otherwise it will stop
254+
// when the paths becomes too long due to the symlink loop: this is not
255+
// what we want, we are looking for an early detection of the loop.
256+
skipBrokenLinks := func(p *Path) bool {
257+
_, err := p.Stat()
258+
return err == nil
259+
}
260+
261+
var files PathList
262+
var err error
263+
done := make(chan bool)
264+
go func() {
265+
files, err = loopsPath.Join(testdir).ReadDirRecursiveFiltered(
266+
skipBrokenLinks,
267+
)
268+
done <- true
269+
}()
270+
require.Eventually(
271+
t,
272+
func() bool {
273+
select {
274+
case <-done:
275+
return true
276+
default:
277+
return false
278+
}
279+
},
280+
5*time.Second,
281+
10*time.Millisecond,
282+
"Infinite symlink loop while loading sketch",
283+
)
284+
return files, err
285+
}
286+
287+
for _, dir := range []string{"loop_1", "loop_2", "loop_3", "loop_4"} {
288+
l, err := unbuondedReaddir(dir)
289+
require.EqualError(t, err, "directories symlink loop detected", "loop not detected in %s", dir)
290+
require.Nil(t, l)
291+
}
292+
293+
{
294+
l, err := unbuondedReaddir("regular_1")
295+
require.NoError(t, err)
296+
require.Len(t, l, 4)
297+
l.Sort()
298+
pathEqualsTo(t, "testdata/loops/regular_1/dir1", l[0])
299+
pathEqualsTo(t, "testdata/loops/regular_1/dir1/file1", l[1])
300+
pathEqualsTo(t, "testdata/loops/regular_1/dir2", l[2])
301+
pathEqualsTo(t, "testdata/loops/regular_1/dir2/file1", l[3])
302+
}
303+
304+
{
305+
l, err := unbuondedReaddir("regular_2")
306+
require.NoError(t, err)
307+
require.Len(t, l, 6)
308+
l.Sort()
309+
pathEqualsTo(t, "testdata/loops/regular_2/dir1", l[0])
310+
pathEqualsTo(t, "testdata/loops/regular_2/dir1/file1", l[1])
311+
pathEqualsTo(t, "testdata/loops/regular_2/dir2", l[2])
312+
pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1", l[3])
313+
pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1/file1", l[4])
314+
pathEqualsTo(t, "testdata/loops/regular_2/dir2/file2", l[5])
315+
}
316+
}

Diff for: testdata/loops/loop_1/dir1/loop

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir1

Diff for: testdata/loops/loop_2/dir1/loop2

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir2

Diff for: testdata/loops/loop_2/dir2/loop1

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir1

Diff for: testdata/loops/loop_3/dir1/loop2

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir2

Diff for: testdata/loops/loop_3/dir2/dir3/loop2

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../dir1/

Diff for: testdata/loops/loop_4/dir1/dir2/loop2

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir3

Diff for: testdata/loops/loop_4/dir1/dir3/dir4/loop1

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../dir1

Diff for: testdata/loops/regular_1/dir1/file1

Whitespace-only changes.

Diff for: testdata/loops/regular_1/dir2

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dir1

Diff for: testdata/loops/regular_2/dir1/file1

Whitespace-only changes.

Diff for: testdata/loops/regular_2/dir2/dir1

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../dir1

Diff for: testdata/loops/regular_2/dir2/file2

Whitespace-only changes.

0 commit comments

Comments
 (0)