Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AppInspector.CLI/CLICmdOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ public record CLIAnalysisSharedCommandOptions : CLICustomRulesCommandOptions
[Option('u', "scan-unknown-filetypes", Required = false, HelpText = "Scan files of unknown types.")]
public bool ScanUnknownTypes { get; set; }

[Option("follow-symlinks", Required = false,
HelpText = "Follow symbolic links while enumerating source files.")]
public bool FollowSymlinks { get; set; }

[Option('c', "confidence-filters", Required = false, Separator = ',',
HelpText =
"Output only matches with specified confidence <value>,<value>. Default: Medium,High. [High|Medium|Low]",
Expand Down
2 changes: 2 additions & 0 deletions AppInspector.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ private static int RunAnalyzeCommand(CLIAnalyzeCmdOptions cliOptions)
ProcessingTimeOut = cliOptions.ProcessingTimeOut,
ContextLines = numContextLines,
ScanUnknownTypes = cliOptions.ScanUnknownTypes,
FollowSymlinks = cliOptions.FollowSymlinks,
TagsOnly = tagsOnly,
NoFileMetadata = cliOptions.NoFileMetadata,
AllowAllTagsInBuildFiles = cliOptions.AllowAllTagsInBuildFiles,
Expand Down Expand Up @@ -372,6 +373,7 @@ private static int RunTagDiffCommand(CLITagDiffCmdOptions cliOptions)
FileTimeOut = cliOptions.FileTimeOut,
ProcessingTimeOut = cliOptions.ProcessingTimeOut,
ScanUnknownTypes = cliOptions.ScanUnknownTypes,
FollowSymlinks = cliOptions.FollowSymlinks,
SingleThread = cliOptions.SingleThread,
DisableCustomRuleValidation = cliOptions.DisableCustomRuleValidation,
DisableRequireUniqueIds = cliOptions.DisableRequireUniqueIds,
Expand Down
138 changes: 138 additions & 0 deletions AppInspector.Tests/Commands/TestAnalyzeCmd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,65 @@

namespace AppInspector.Tests.Commands;

internal static class SymlinkTestSupport
{
public static string? SkipReason { get; } = GetSkipReason();

private static string? GetSkipReason()
{
var testRoot = Path.Combine(Path.GetTempPath(), $"ApplicationInspector-SymlinkTest-{Guid.NewGuid()}");

try
{
Directory.CreateDirectory(testRoot);
var fileTarget = Path.Combine(testRoot, "target.txt");
var directoryTarget = Path.Combine(testRoot, "target-directory");
File.WriteAllText(fileTarget, string.Empty);
Directory.CreateDirectory(directoryTarget);
File.CreateSymbolicLink(Path.Combine(testRoot, "file-link"), fileTarget);
Directory.CreateSymbolicLink(Path.Combine(testRoot, "directory-link"), directoryTarget);
return null;
}
catch (UnauthorizedAccessException)
{
return "Symlink creation is not permitted on this machine.";
}
catch (PlatformNotSupportedException)
{
return "Symlinks are not supported on this platform.";
}
catch (IOException ex)
{
return $"Symlink creation is not supported by the test file system: {ex.Message}";
}
finally
{
if (Directory.Exists(testRoot))
{
Directory.Delete(testRoot, true);
}
}
}
}

[AttributeUsage(AttributeTargets.Method)]
internal sealed class SymlinkFactAttribute : FactAttribute
{
public SymlinkFactAttribute()
{
Skip = SymlinkTestSupport.SkipReason;
}
}

[AttributeUsage(AttributeTargets.Method)]
internal sealed class SymlinkTheoryAttribute : TheoryAttribute
{
public SymlinkTheoryAttribute()
{
Skip = SymlinkTestSupport.SkipReason;
}
}

public class TestAnalyzeCmdFixture : IDisposable
{
public TestAnalyzeCmdFixture()
Expand Down Expand Up @@ -134,6 +193,85 @@ public void Overrides()
Assert.Equal(2, result.Metadata.UniqueMatchesCount);
}

[SymlinkTheory]
[InlineData(false, 4)]
[InlineData(true, 6)]
public void FollowSymlinks(bool followSymlinks, int expectedFiles)
{
var testRoot = Path.Combine("TestOutput", $"SymlinkTest-{Guid.NewGuid()}");
var sourcePath = Path.Combine(testRoot, "source");
var linkedDirectoryTarget = Path.Combine(testRoot, "linked-directory-target");
Directory.CreateDirectory(sourcePath);
Directory.CreateDirectory(linkedDirectoryTarget);

try
{
File.WriteAllText(Path.Combine(sourcePath, "regular.js"), "windows");
var linkedFileTarget = Path.Combine(testRoot, "linked-file-target.js");
File.WriteAllText(linkedFileTarget, "windows");
File.WriteAllText(Path.Combine(linkedDirectoryTarget, "linked-directory-file.js"), "windows");
File.CreateSymbolicLink(Path.Combine(sourcePath, "linked-file.js"), Path.GetFullPath(linkedFileTarget));
Directory.CreateSymbolicLink(Path.Combine(sourcePath, "linked-directory"),
Path.GetFullPath(linkedDirectoryTarget));
var directLinkedFile = Path.Combine(testRoot, "direct-linked-file.js");
var directLinkedDirectory = Path.Combine(testRoot, "direct-linked-directory");
File.CreateSymbolicLink(directLinkedFile, Path.GetFullPath(linkedFileTarget));
Directory.CreateSymbolicLink(directLinkedDirectory, Path.GetFullPath(linkedDirectoryTarget));
var linkedAncestorTarget = Path.Combine(testRoot, "linked-ancestor-target");
var sourceThroughLinkedAncestor = Path.Combine(testRoot, "linked-ancestor", "source");
Directory.CreateDirectory(Path.Combine(linkedAncestorTarget, "source"));
File.WriteAllText(Path.Combine(linkedAncestorTarget, "source", "linked-ancestor-file.js"), "windows");
Directory.CreateSymbolicLink(Path.Combine(testRoot, "linked-ancestor"),
Path.GetFullPath(linkedAncestorTarget));

AnalyzeCommand command = new(new AnalyzeOptions
{
SourcePath = new[] { sourcePath, directLinkedFile, directLinkedDirectory, sourceThroughLinkedAncestor },
CustomRulesPath = testRulesPath,
IgnoreDefaultRules = true,
FollowSymlinks = followSymlinks
}, factory);

var result = command.GetResult();

Assert.Equal(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.Equal(expectedFiles, result.Metadata.TotalFiles);
}
finally
{
Directory.Delete(testRoot, true);
}
}

[SymlinkFact]
public void SkipsDanglingSymlinksByDefault()
{
var testRoot = Path.Combine("TestOutput", $"DanglingSymlinkTest-{Guid.NewGuid()}");
Directory.CreateDirectory(testRoot);

try
{
File.WriteAllText(Path.Combine(testRoot, "regular.js"), "windows");
var danglingLink = Path.Combine(testRoot, "dangling.js");
File.CreateSymbolicLink(danglingLink, Path.Combine(testRoot, "missing.js"));
AnalyzeCommand command = new(new AnalyzeOptions
{
SourcePath = new[] { testRoot },
CustomRulesPath = testRulesPath,
IgnoreDefaultRules = true
}, factory);

var result = command.GetResult();

Assert.Equal(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.Equal(1, result.Metadata.TotalFiles);
}
finally
{
Directory.Delete(testRoot, true);
}
}

[Fact]
public async Task OverridesAsync()
{
Expand Down
22 changes: 19 additions & 3 deletions AppInspector/Commands/AnalyzeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public class AnalyzeOptions
/// </summary>
public IEnumerable<string> FilePathExclusions { get; set; } = Array.Empty<string>();
/// <summary>
/// Follow symbolic links while enumerating source files.
/// </summary>
public bool FollowSymlinks { get; set; }
/// <summary>
/// If enabled, processing will be performed on one file at a time.
/// </summary>
public bool SingleThread { get; set; }
Expand Down Expand Up @@ -238,11 +242,23 @@ public AnalyzeCommand(AnalyzeOptions opt, ILoggerFactory? loggerFactory = null)
{
// Turn any relative paths into absolute paths for consistent behavior with file name regexes in rules
var entryFullPath = Path.GetFullPath(entry);
if (Directory.Exists(entryFullPath))
var directoryExists = Directory.Exists(entryFullPath);
var fileExists = File.Exists(entryFullPath);
if (directoryExists)
{
_srcfileList.AddRange(Directory.EnumerateFiles(entryFullPath, "*.*", SearchOption.AllDirectories));
_srcfileList.AddRange(Directory.EnumerateFiles(entryFullPath, "*.*", new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = false,
MatchType = MatchType.Win32,
#if NETSTANDARD2_1
AttributesToSkip = _options.FollowSymlinks ? 0 : FileAttributes.ReparsePoint
#else
AttributesToSkip = _options.FollowSymlinks ? FileAttributes.None : FileAttributes.ReparsePoint
#endif
}));
Comment thread
Copilot marked this conversation as resolved.
}
else if (File.Exists(entryFullPath))
else if (fileExists)
{
_srcfileList.Add(entryFullPath);
}
Expand Down
3 changes: 3 additions & 0 deletions AppInspector/Commands/TagDiffCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class TagDiffOptions
public int ProcessingTimeOut { get; set; }
public bool ScanUnknownTypes { get; set; }
public bool SingleThread { get; set; }
public bool FollowSymlinks { get; set; }
public IEnumerable<Confidence> ConfidenceFilters { get; set; } = new[] { Confidence.High, Confidence.Medium };

public IEnumerable<Severity> SeverityFilters { get; set; } = new[]
Expand Down Expand Up @@ -173,6 +174,7 @@ public TagDiffResult GetResult()
NoShowProgress = true,
ScanUnknownTypes = _options.ScanUnknownTypes,
SingleThread = _options.SingleThread,
FollowSymlinks = _options.FollowSymlinks,
CustomCommentsPath = _options.CustomCommentsPath,
CustomLanguagesPath = _options.CustomLanguagesPath,
DisableCustomRuleVerification = _options.DisableCustomRuleValidation,
Expand All @@ -197,6 +199,7 @@ public TagDiffResult GetResult()
NoShowProgress = true,
ScanUnknownTypes = _options.ScanUnknownTypes,
SingleThread = _options.SingleThread,
FollowSymlinks = _options.FollowSymlinks,
CustomCommentsPath = _options.CustomCommentsPath,
CustomLanguagesPath = _options.CustomLanguagesPath,
DisableCustomRuleVerification = true, // Rules are already validated by the first command
Expand Down
Loading