diff --git a/list.go b/list.go index 7892fa0..fbab13c 100644 --- a/list.go +++ b/list.go @@ -78,7 +78,7 @@ func (p *PathList) FilterDirs() { func (p *PathList) FilterOutDirs() { res := (*p)[:0] for _, path := range *p { - if path.IsNotDir() { + if !path.IsDir() { res = append(res, path) } } @@ -91,10 +91,12 @@ func (p *PathList) FilterOutHiddenFiles() { p.FilterOutPrefix(".") } -func (p *PathList) filter(filter func(*Path) bool) { +// Filter will remove all the elements of the list that do not match +// the specified acceptor function +func (p *PathList) Filter(acceptorFunc func(*Path) bool) { res := (*p)[:0] for _, path := range *p { - if filter(path) { + if acceptorFunc(path) { res = append(res, path) } } @@ -106,7 +108,7 @@ func (p *PathList) FilterOutPrefix(prefixes ...string) { filterFunction := func(path *Path) bool { return !path.HasPrefix(prefixes...) } - p.filter(filterFunction) + p.Filter(filterFunction) } // FilterPrefix remove all entries not having one of the specified prefixes @@ -114,7 +116,7 @@ func (p *PathList) FilterPrefix(prefixes ...string) { filterFunction := func(path *Path) bool { return path.HasPrefix(prefixes...) } - p.filter(filterFunction) + p.Filter(filterFunction) } // FilterOutSuffix remove all entries having one of the specified suffixes @@ -122,7 +124,7 @@ func (p *PathList) FilterOutSuffix(suffixies ...string) { filterFunction := func(path *Path) bool { return !path.HasSuffix(suffixies...) } - p.filter(filterFunction) + p.Filter(filterFunction) } // FilterSuffix remove all entries not having one of the specified suffixes @@ -130,7 +132,7 @@ func (p *PathList) FilterSuffix(suffixies ...string) { filterFunction := func(path *Path) bool { return path.HasSuffix(suffixies...) } - p.filter(filterFunction) + p.Filter(filterFunction) } // Add adds a Path to the PathList diff --git a/list_test.go b/list_test.go index c7d9354..eaafc82 100644 --- a/list_test.go +++ b/list_test.go @@ -160,4 +160,10 @@ func TestListFilters(t *testing.T) { l16 := list.Clone() l16.FilterPrefix() require.Equal(t, "[]", fmt.Sprintf("%s", l16)) + + l17 := list.Clone() + l17.Filter(func(p *Path) bool { + return p.Base() == "bbbb" + }) + require.Equal(t, "[bbbb aaaa/bbbb]", fmt.Sprintf("%s", l17)) } diff --git a/paths.go b/paths.go index 7fb644d..a734d17 100644 --- a/paths.go +++ b/paths.go @@ -32,7 +32,7 @@ package paths import ( "fmt" "io" - "io/ioutil" + "io/fs" "os" "path/filepath" "strings" @@ -70,10 +70,18 @@ func NewFromFile(file *os.File) *Path { // Stat returns a FileInfo describing the named file. The result is // cached internally for next queries. To ensure that the cached // FileInfo entry is updated just call Stat again. -func (p *Path) Stat() (os.FileInfo, error) { +func (p *Path) Stat() (fs.FileInfo, error) { return os.Stat(p.path) } +// Lstat returns a FileInfo describing the named file. If the file is +// a symbolic link, the returned FileInfo describes the symbolic link. +// Lstat makes no attempt to follow the link. If there is an error, it +// will be of type *PathError. +func (p *Path) Lstat() (fs.FileInfo, error) { + return os.Lstat(p.path) +} + // Clone create a copy of the Path object func (p *Path) Clone() *Path { return New(p.path) @@ -410,6 +418,13 @@ func (p *Path) CopyDirTo(dst *Path) error { return nil } +// Chmod changes the mode of the named file to mode. If the file is a +// symbolic link, it changes the mode of the link's target. If there +// is an error, it will be of type *os.PathError. +func (p *Path) Chmod(mode fs.FileMode) error { + return os.Chmod(p.path, mode) +} + // Chtimes changes the access and modification times of the named file, // similar to the Unix utime() or utimes() functions. func (p *Path) Chtimes(atime, mtime time.Time) error { @@ -418,14 +433,14 @@ func (p *Path) Chtimes(atime, mtime time.Time) error { // ReadFile reads the file named by filename and returns the contents func (p *Path) ReadFile() ([]byte, error) { - return ioutil.ReadFile(p.path) + return os.ReadFile(p.path) } // WriteFile writes data to a file named by filename. If the file // does not exist, WriteFile creates it otherwise WriteFile truncates // it before writing. func (p *Path) WriteFile(data []byte) error { - return ioutil.WriteFile(p.path, data, os.FileMode(0644)) + return os.WriteFile(p.path, data, os.FileMode(0644)) } // WriteToTempFile writes data to a newly generated temporary file. diff --git a/paths_test.go b/paths_test.go index b495273..27fde62 100644 --- a/paths_test.go +++ b/paths_test.go @@ -284,25 +284,49 @@ func TestFilterDirs(t *testing.T) { } func TestFilterOutDirs(t *testing.T) { - testPath := New("testdata", "fileset") + { + testPath := New("testdata", "fileset") - list, err := testPath.ReadDir() - require.NoError(t, err) - require.Len(t, list, 6) + list, err := testPath.ReadDir() + require.NoError(t, err) + require.Len(t, list, 6) + + pathEqualsTo(t, "testdata/fileset/anotherFile", list[0]) + pathEqualsTo(t, "testdata/fileset/file", list[1]) + pathEqualsTo(t, "testdata/fileset/folder", list[2]) + pathEqualsTo(t, "testdata/fileset/symlinktofolder", list[3]) + pathEqualsTo(t, "testdata/fileset/test.txt", list[4]) + pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[5]) + + list.FilterOutDirs() + require.Len(t, list, 4) + pathEqualsTo(t, "testdata/fileset/anotherFile", list[0]) + pathEqualsTo(t, "testdata/fileset/file", list[1]) + pathEqualsTo(t, "testdata/fileset/test.txt", list[2]) + pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[3]) + } - pathEqualsTo(t, "testdata/fileset/anotherFile", list[0]) - pathEqualsTo(t, "testdata/fileset/file", list[1]) - pathEqualsTo(t, "testdata/fileset/folder", list[2]) - pathEqualsTo(t, "testdata/fileset/symlinktofolder", list[3]) - pathEqualsTo(t, "testdata/fileset/test.txt", list[4]) - pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[5]) + { + list, err := New("testdata", "broken_symlink", "dir_1").ReadDirRecursive() + require.NoError(t, err) - list.FilterOutDirs() - require.Len(t, list, 4) - pathEqualsTo(t, "testdata/fileset/anotherFile", list[0]) - pathEqualsTo(t, "testdata/fileset/file", list[1]) - pathEqualsTo(t, "testdata/fileset/test.txt", list[2]) - pathEqualsTo(t, "testdata/fileset/test.txt.gz", list[3]) + require.Len(t, list, 7) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/broken_link", list[0]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/file2", list[1]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir", list[2]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir/file1", list[3]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_file", list[4]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir", list[5]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir/file1", list[6]) + + list.FilterOutDirs() + require.Len(t, list, 5) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/broken_link", list[0]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/file2", list[1]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_dir/file1", list[2]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/linked_file", list[3]) + pathEqualsTo(t, "testdata/broken_symlink/dir_1/real_dir/file1", list[4]) + } } func TestEquivalentPaths(t *testing.T) { diff --git a/readdir.go b/readdir.go index 53341b9..985fed5 100644 --- a/readdir.go +++ b/readdir.go @@ -30,7 +30,8 @@ package paths import ( - "io/ioutil" + "errors" + "os" "strings" ) @@ -41,7 +42,7 @@ type ReadDirFilter func(file *Path) bool // ReadDir returns a PathList containing the content of the directory // pointed by the current Path. The resulting list is filtered by the given filters chained. func (p *Path) ReadDir(filters ...ReadDirFilter) (PathList, error) { - infos, err := ioutil.ReadDir(p.path) + infos, err := os.ReadDir(p.path) if err != nil { return nil, err } @@ -69,27 +70,7 @@ func (p *Path) ReadDir(filters ...ReadDirFilter) (PathList, error) { // ReadDirRecursive returns a PathList containing the content of the directory // and its subdirectories pointed by the current Path func (p *Path) ReadDirRecursive() (PathList, error) { - infos, err := ioutil.ReadDir(p.path) - if err != nil { - return nil, err - } - paths := PathList{} - for _, info := range infos { - path := p.Join(info.Name()) - paths.Add(path) - - if isDir, err := path.IsDirCheck(); err != nil { - return nil, err - } else if isDir { - subPaths, err := path.ReadDirRecursive() - if err != nil { - return nil, err - } - paths.AddAll(subPaths) - } - - } - return paths, nil + return p.ReadDirRecursiveFiltered(nil) } // ReadDirRecursiveFiltered returns a PathList containing the content of the directory @@ -101,41 +82,53 @@ func (p *Path) ReadDirRecursive() (PathList, error) { // - `filters` are the filters that are checked to determine if the entry should be // added to the resulting PathList func (p *Path) ReadDirRecursiveFiltered(recursionFilter ReadDirFilter, filters ...ReadDirFilter) (PathList, error) { - infos, err := ioutil.ReadDir(p.path) - if err != nil { - return nil, err - } + var search func(*Path) (PathList, error) - accept := func(p *Path) bool { - for _, filter := range filters { - if !filter(p) { - return false - } + explored := map[string]bool{} + search = func(currPath *Path) (PathList, error) { + canonical := currPath.Canonical().path + if explored[canonical] { + return nil, errors.New("directories symlink loop detected") } - return true - } + explored[canonical] = true + defer delete(explored, canonical) - paths := PathList{} - for _, info := range infos { - path := p.Join(info.Name()) + infos, err := os.ReadDir(currPath.path) + if err != nil { + return nil, err + } - if accept(path) { - paths.Add(path) + accept := func(p *Path) bool { + for _, filter := range filters { + if !filter(p) { + return false + } + } + return true } - if recursionFilter == nil || recursionFilter(path) { - if isDir, err := path.IsDirCheck(); err != nil { - return nil, err - } else if isDir { - subPaths, err := path.ReadDirRecursiveFiltered(recursionFilter, filters...) - if err != nil { - return nil, err + paths := PathList{} + for _, info := range infos { + path := currPath.Join(info.Name()) + + if accept(path) { + paths.Add(path) + } + + if recursionFilter == nil || recursionFilter(path) { + if path.IsDir() { + subPaths, err := search(path) + if err != nil { + return nil, err + } + paths.AddAll(subPaths) } - paths.AddAll(subPaths) } } + return paths, nil } - return paths, nil + + return search(p) } // FilterDirectories is a ReadDirFilter that accepts only directories diff --git a/readdir_test.go b/readdir_test.go index d0ec927..ae25ede 100644 --- a/readdir_test.go +++ b/readdir_test.go @@ -31,8 +31,11 @@ package paths import ( "fmt" + "io/fs" "os" + "runtime" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -245,3 +248,96 @@ func TestReadDirRecursiveFiltered(t *testing.T) { pathEqualsTo(t, "testdata/fileset/test.txt", l[7]) pathEqualsTo(t, "testdata/fileset/test.txt.gz", l[8]) } + +func TestReadDirRecursiveLoopDetection(t *testing.T) { + loopsPath := New("testdata", "loops") + unbuondedReaddir := func(testdir string) (PathList, error) { + var files PathList + var err error + done := make(chan bool) + go func() { + files, err = loopsPath.Join(testdir).ReadDirRecursive() + done <- true + }() + require.Eventually( + t, + func() bool { + select { + case <-done: + return true + default: + return false + } + }, + 5*time.Second, + 10*time.Millisecond, + "Infinite symlink loop while loading sketch", + ) + return files, err + } + + for _, dir := range []string{"loop_1", "loop_2", "loop_3", "loop_4"} { + l, err := unbuondedReaddir(dir) + require.EqualError(t, err, "directories symlink loop detected", "loop not detected in %s", dir) + require.Nil(t, l) + } + + { + l, err := unbuondedReaddir("regular_1") + require.NoError(t, err) + require.Len(t, l, 4) + l.Sort() + pathEqualsTo(t, "testdata/loops/regular_1/dir1", l[0]) + pathEqualsTo(t, "testdata/loops/regular_1/dir1/file1", l[1]) + pathEqualsTo(t, "testdata/loops/regular_1/dir2", l[2]) + pathEqualsTo(t, "testdata/loops/regular_1/dir2/file1", l[3]) + } + + { + l, err := unbuondedReaddir("regular_2") + require.NoError(t, err) + require.Len(t, l, 6) + l.Sort() + pathEqualsTo(t, "testdata/loops/regular_2/dir1", l[0]) + pathEqualsTo(t, "testdata/loops/regular_2/dir1/file1", l[1]) + pathEqualsTo(t, "testdata/loops/regular_2/dir2", l[2]) + pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1", l[3]) + pathEqualsTo(t, "testdata/loops/regular_2/dir2/dir1/file1", l[4]) + pathEqualsTo(t, "testdata/loops/regular_2/dir2/file2", l[5]) + } + + { + l, err := unbuondedReaddir("regular_3") + require.NoError(t, err) + require.Len(t, l, 7) + l.Sort() + pathEqualsTo(t, "testdata/loops/regular_3/dir1", l[0]) + pathEqualsTo(t, "testdata/loops/regular_3/dir1/file1", l[1]) + pathEqualsTo(t, "testdata/loops/regular_3/dir2", l[2]) + pathEqualsTo(t, "testdata/loops/regular_3/dir2/dir1", l[3]) + pathEqualsTo(t, "testdata/loops/regular_3/dir2/dir1/file1", l[4]) + pathEqualsTo(t, "testdata/loops/regular_3/dir2/file2", l[5]) + pathEqualsTo(t, "testdata/loops/regular_3/link", l[6]) // broken symlink is reported in files + } + + if runtime.GOOS != "windows" { + dir1 := loopsPath.Join("regular_4_with_permission_error", "dir1") + + l, err := unbuondedReaddir("regular_4_with_permission_error") + require.NoError(t, err) + require.NotEmpty(t, l) + + dir1Stat, err := dir1.Stat() + require.NoError(t, err) + err = dir1.Chmod(fs.FileMode(0)) // Enforce permission error + require.NoError(t, err) + t.Cleanup(func() { + // Restore normal permission after the test + dir1.Chmod(dir1Stat.Mode()) + }) + + l, err = unbuondedReaddir("regular_4_with_permission_error") + require.Error(t, err) + require.Nil(t, l) + } +} diff --git a/testdata/broken_symlink/dir_1/broken_link b/testdata/broken_symlink/dir_1/broken_link new file mode 120000 index 0000000..86a410d --- /dev/null +++ b/testdata/broken_symlink/dir_1/broken_link @@ -0,0 +1 @@ +broken \ No newline at end of file diff --git a/testdata/broken_symlink/dir_1/file2 b/testdata/broken_symlink/dir_1/file2 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/broken_symlink/dir_1/linked_dir b/testdata/broken_symlink/dir_1/linked_dir new file mode 120000 index 0000000..4b01904 --- /dev/null +++ b/testdata/broken_symlink/dir_1/linked_dir @@ -0,0 +1 @@ +real_dir \ No newline at end of file diff --git a/testdata/broken_symlink/dir_1/linked_file b/testdata/broken_symlink/dir_1/linked_file new file mode 120000 index 0000000..30d67d4 --- /dev/null +++ b/testdata/broken_symlink/dir_1/linked_file @@ -0,0 +1 @@ +file2 \ No newline at end of file diff --git a/testdata/broken_symlink/dir_1/real_dir/file1 b/testdata/broken_symlink/dir_1/real_dir/file1 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/loop_1/dir1/loop b/testdata/loops/loop_1/dir1/loop new file mode 120000 index 0000000..c9f3ab1 --- /dev/null +++ b/testdata/loops/loop_1/dir1/loop @@ -0,0 +1 @@ +../dir1 \ No newline at end of file diff --git a/testdata/loops/loop_2/dir1/loop2 b/testdata/loops/loop_2/dir1/loop2 new file mode 120000 index 0000000..d014eb4 --- /dev/null +++ b/testdata/loops/loop_2/dir1/loop2 @@ -0,0 +1 @@ +../dir2 \ No newline at end of file diff --git a/testdata/loops/loop_2/dir2/loop1 b/testdata/loops/loop_2/dir2/loop1 new file mode 120000 index 0000000..c9f3ab1 --- /dev/null +++ b/testdata/loops/loop_2/dir2/loop1 @@ -0,0 +1 @@ +../dir1 \ No newline at end of file diff --git a/testdata/loops/loop_3/dir1/loop2 b/testdata/loops/loop_3/dir1/loop2 new file mode 120000 index 0000000..d014eb4 --- /dev/null +++ b/testdata/loops/loop_3/dir1/loop2 @@ -0,0 +1 @@ +../dir2 \ No newline at end of file diff --git a/testdata/loops/loop_3/dir2/dir3/loop2 b/testdata/loops/loop_3/dir2/dir3/loop2 new file mode 120000 index 0000000..85babfd --- /dev/null +++ b/testdata/loops/loop_3/dir2/dir3/loop2 @@ -0,0 +1 @@ +../../dir1/ \ No newline at end of file diff --git a/testdata/loops/loop_4/dir1/dir2/loop2 b/testdata/loops/loop_4/dir1/dir2/loop2 new file mode 120000 index 0000000..3fd50ca --- /dev/null +++ b/testdata/loops/loop_4/dir1/dir2/loop2 @@ -0,0 +1 @@ +../dir3 \ No newline at end of file diff --git a/testdata/loops/loop_4/dir1/dir3/dir4/loop1 b/testdata/loops/loop_4/dir1/dir3/dir4/loop1 new file mode 120000 index 0000000..4f388a6 --- /dev/null +++ b/testdata/loops/loop_4/dir1/dir3/dir4/loop1 @@ -0,0 +1 @@ +../../../dir1 \ No newline at end of file diff --git a/testdata/loops/regular_1/dir1/file1 b/testdata/loops/regular_1/dir1/file1 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_1/dir2 b/testdata/loops/regular_1/dir2 new file mode 120000 index 0000000..df490f8 --- /dev/null +++ b/testdata/loops/regular_1/dir2 @@ -0,0 +1 @@ +dir1 \ No newline at end of file diff --git a/testdata/loops/regular_2/dir1/file1 b/testdata/loops/regular_2/dir1/file1 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_2/dir2/dir1 b/testdata/loops/regular_2/dir2/dir1 new file mode 120000 index 0000000..c9f3ab1 --- /dev/null +++ b/testdata/loops/regular_2/dir2/dir1 @@ -0,0 +1 @@ +../dir1 \ No newline at end of file diff --git a/testdata/loops/regular_2/dir2/file2 b/testdata/loops/regular_2/dir2/file2 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_3/dir1/file1 b/testdata/loops/regular_3/dir1/file1 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_3/dir2/dir1 b/testdata/loops/regular_3/dir2/dir1 new file mode 120000 index 0000000..c9f3ab1 --- /dev/null +++ b/testdata/loops/regular_3/dir2/dir1 @@ -0,0 +1 @@ +../dir1 \ No newline at end of file diff --git a/testdata/loops/regular_3/dir2/file2 b/testdata/loops/regular_3/dir2/file2 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_3/link b/testdata/loops/regular_3/link new file mode 120000 index 0000000..86a410d --- /dev/null +++ b/testdata/loops/regular_3/link @@ -0,0 +1 @@ +broken \ No newline at end of file diff --git a/testdata/loops/regular_4_with_permission_error/dir1/file1 b/testdata/loops/regular_4_with_permission_error/dir1/file1 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_4_with_permission_error/dir2/dir1 b/testdata/loops/regular_4_with_permission_error/dir2/dir1 new file mode 120000 index 0000000..c9f3ab1 --- /dev/null +++ b/testdata/loops/regular_4_with_permission_error/dir2/dir1 @@ -0,0 +1 @@ +../dir1 \ No newline at end of file diff --git a/testdata/loops/regular_4_with_permission_error/dir2/file2 b/testdata/loops/regular_4_with_permission_error/dir2/file2 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/loops/regular_4_with_permission_error/link b/testdata/loops/regular_4_with_permission_error/link new file mode 120000 index 0000000..86a410d --- /dev/null +++ b/testdata/loops/regular_4_with_permission_error/link @@ -0,0 +1 @@ +broken \ No newline at end of file