diff --git a/internal/project/project.go b/internal/project/project.go index 2d88a52d..b5728533 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -18,6 +18,7 @@ package project import ( "fmt" + "os" "github.com/arduino/arduino-lint/internal/configuration" "github.com/arduino/arduino-lint/internal/project/library" @@ -87,7 +88,7 @@ func findProjects(targetPath *paths.Path) ([]Type, error) { } else { if configuration.SuperprojectTypeFilter() == projecttype.All || configuration.Recursive() { // Project discovery and/or type detection is required. - foundParentProjects = findProjectsUnderPath(targetPath, configuration.SuperprojectTypeFilter(), configuration.Recursive()) + foundParentProjects = findProjectsUnderPath(targetPath, configuration.SuperprojectTypeFilter(), configuration.Recursive(), 0) } else { // Project was explicitly defined by user. foundParentProjects = append(foundParentProjects, @@ -115,7 +116,7 @@ func findProjects(targetPath *paths.Path) ([]Type, error) { } // findProjectsUnderPath finds projects of the given type under the given path. It returns a slice containing the definitions of all found projects. -func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype.Type, recursive bool) []Type { +func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype.Type, recursive bool, symlinkDepth int) []Type { var foundProjects []Type isProject, foundProjectType := isProject(targetPath, projectTypeFilter) @@ -134,11 +135,26 @@ func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype } if recursive { + if symlinkDepth > 10 { + panic(fmt.Sprintf("symlink depth exceeded maximum while finding projects under %s", targetPath)) + } // targetPath was not a project, so search the subfolders. directoryListing, _ := targetPath.ReadDir() directoryListing.FilterDirs() for _, potentialProjectDirectory := range directoryListing { - foundProjects = append(foundProjects, findProjectsUnderPath(potentialProjectDirectory, projectTypeFilter, recursive)...) + // It is possible for a combination of symlinks to parent paths to cause project discovery to get stuck in + // an endless loop of recursion. This is avoided by keeping count of the depth of symlinks and discontinuing + // recursion when it exceeds reason. + pathStat, err := os.Lstat(potentialProjectDirectory.String()) + if err != nil { + panic(err) + } + depthDelta := 0 + if pathStat.Mode()&os.ModeSymlink != 0 { + depthDelta = 1 + } + + foundProjects = append(foundProjects, findProjectsUnderPath(potentialProjectDirectory, projectTypeFilter, recursive, symlinkDepth+depthDelta)...) } } @@ -184,7 +200,7 @@ func findSubprojects(superproject Type, apexSuperprojectType projecttype.Type) [ directoryListing.FilterDirs() for _, subprojectPath := range directoryListing { - immediateSubprojects = append(immediateSubprojects, findProjectsUnderPath(subprojectPath, subProjectType, searchPathsRecursively)...) + immediateSubprojects = append(immediateSubprojects, findProjectsUnderPath(subprojectPath, subProjectType, searchPathsRecursively, 0)...) } } } diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 825e7ade..25a1a31e 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -26,6 +26,7 @@ import ( "github.com/arduino/arduino-lint/internal/util/test" "github.com/arduino/go-paths-helper" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var testDataPath *paths.Path @@ -38,6 +39,29 @@ func init() { testDataPath = paths.New(workingDirectory, "testdata") } +func TestSymlinkLoop(t *testing.T) { + // Set up directory structure of test library. + libraryPath, err := paths.TempDir().MkTempDir("TestSymlinkLoop") + defer libraryPath.RemoveAll() // Clean up after the test. + require.Nil(t, err) + err = libraryPath.Join("TestSymlinkLoop.h").WriteFile([]byte{}) + require.Nil(t, err) + examplesPath := libraryPath.Join("examples") + err = examplesPath.Mkdir() + require.Nil(t, err) + + // It's probably most friendly for contributors using Windows to create the symlinks needed for the test on demand. + err = os.Symlink(examplesPath.Join("..").String(), examplesPath.Join("UpGoer1").String()) + require.Nil(t, err, "This test must be run as administrator on Windows to have symlink creation privilege.") + // It's necessary to have multiple symlinks to a parent directory to create the loop. + err = os.Symlink(examplesPath.Join("..").String(), examplesPath.Join("UpGoer2").String()) + require.Nil(t, err) + + configuration.Initialize(test.ConfigurationFlags(), []string{libraryPath.String()}) + + assert.Panics(t, func() { FindProjects() }, "Infinite symlink loop encountered during project discovery") +} + func TestFindProjects(t *testing.T) { sketchPath := testDataPath.Join("Sketch") libraryPath := testDataPath.Join("Library")