Skip to content

Commit

Permalink
Merge pull request #175 from per1234/handle-symlink-loop
Browse files Browse the repository at this point in the history
Avoid hang when linting projects with symlink loops
  • Loading branch information
per1234 authored May 26, 2021
2 parents 85638b8 + 17c7b7c commit 98ab6a2
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 4 deletions.
24 changes: 20 additions & 4 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package project

import (
"fmt"
"os"

"github.com/arduino/arduino-lint/internal/configuration"
"github.com/arduino/arduino-lint/internal/project/library"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)...)
}
}

Expand Down Expand Up @@ -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)...)
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions internal/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down

0 comments on commit 98ab6a2

Please sign in to comment.