diff --git a/.github/03-feature.instructions.md b/.github/03-feature.instructions.md index 751157396..2f4d2547e 100644 --- a/.github/03-feature.instructions.md +++ b/.github/03-feature.instructions.md @@ -4,6 +4,6 @@ applyTo: "**" ## Feature Instructions - + ## Testing Configuration diff --git a/src/BinSkim.sln b/src/BinSkim.sln index d43218658..aa6a91ec0 100644 --- a/src/BinSkim.sln +++ b/src/BinSkim.sln @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.UnitTests.BinSkim.Rule EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.UnitTests.BinSkim.Driver", "Test.UnitTests.BinSkim.Driver\Test.UnitTests.BinSkim.Driver.csproj", "{B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.IntegrationTests.BinSkim.Driver", "Test.IntegrationTests.BinSkim.Driver\Test.IntegrationTests.BinSkim.Driver.csproj", "{2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Debug|Any CPU.Build.0 = Debug|AnyCPU {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Release|Any CPU.ActiveCfg = Release|AnyCPU {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Release|Any CPU.Build.0 = Release|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|AnyCPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs b/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs index aaf33c024..99a82c5c1 100644 --- a/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs +++ b/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs @@ -657,7 +657,7 @@ private static DwarfFileInformation ReadFile(DwarfMemoryReader debugLine, List 0 && directoryIndex <= directories.Count - 1) ? directories[directoryIndex - 1] : null; + string directory = (directoryIndex > 0 && directoryIndex <= directories.Count) ? directories[directoryIndex - 1] : null; string path; path = string.IsNullOrEmpty(directory) || Path.IsPathRooted(name) ? name : Path.Combine(directory, name); diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs index 50e99a923..bc6cf1cc8 100644 --- a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs +++ b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs @@ -26,68 +26,91 @@ public RuleTests(ITestOutputHelper output) this.testOutputHelper = output; } + private enum ExpectedOutcome + { + Pass, + Fail + } + private void VerifyPass( BinarySkimmer skimmer, IEnumerable additionalTestFiles = null, - bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + bool useDefaultPolicy = false) { - this.Verify(skimmer, additionalTestFiles, useDefaultPolicy, expectToPass: true, bypassExtensionValidation: bypassExtensionValidation, ignoreNoteTargets: ignoreNoteTargets); + this.Verify( + skimmer, + ExpectedOutcome.Pass, + expectedLevel: FailureLevel.None, + additionalTestFiles: additionalTestFiles, + useDefaultPolicy: useDefaultPolicy); } private void VerifyFail( BinarySkimmer skimmer, IEnumerable additionalTestFiles = null, - bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + bool useDefaultPolicy = false) { - this.Verify(skimmer, additionalTestFiles, useDefaultPolicy, expectToPass: false, bypassExtensionValidation: bypassExtensionValidation, ignoreNoteTargets: ignoreNoteTargets); + this.Verify( + skimmer, + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.None, + additionalTestFiles: additionalTestFiles, + useDefaultPolicy: useDefaultPolicy); } + private static readonly HashSet ExcludedTestFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".pdb", ".ilk", ".txt", ".dsym" }; + private void Verify( BinarySkimmer skimmer, - IEnumerable additionalTestFiles, - bool useDefaultPolicy, - bool expectToPass, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + ExpectedOutcome expectedOutcome, + FailureLevel expectedLevel = FailureLevel.None, + ResultKind expectedKind = ResultKind.None, + IEnumerable additionalTestFiles = null, + bool useDefaultPolicy = false, + string outcomeSubdirectory = null) { - var targets = new List(); string ruleName = skimmer.GetType().Name; - string testFilesDirectory = GetTestDirectoryFor(ruleName); - testFilesDirectory = Path.Combine(Environment.CurrentDirectory, "FunctionalTestData", testFilesDirectory); - testFilesDirectory = Path.Combine(testFilesDirectory, expectToPass ? "Pass" : "Fail"); + string outcomeDir = expectedOutcome == ExpectedOutcome.Pass ? "Pass" : "Fail"; + string testFilesDirectory = Path.Combine( + Environment.CurrentDirectory, + "FunctionalTestData", + GetTestDirectoryFor(ruleName), + outcomeSubdirectory != null ? Path.Combine(outcomeDir, outcomeSubdirectory) : outcomeDir); - Assert.True(Directory.Exists(testFilesDirectory), $"Test directory '{testFilesDirectory}' should exist."); + Assert.True(Directory.Exists(testFilesDirectory), + $"Test directory '{testFilesDirectory}' should exist."); + + string[] allFiles = Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories); + var targets = new List(); - foreach (string target in Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories)) + foreach (string target in allFiles) { - if (bypassExtensionValidation || MultithreadedAnalyzeCommand.ValidAnalysisFileExtensions.Contains(Path.GetExtension(target))) + if (!ExcludedTestFileExtensions.Contains(Path.GetExtension(target))) { targets.Add(target); } } - if (additionalTestFiles != null) + int excludedCount = allFiles.Length - targets.Count; + Assert.True(targets.Count > 0, + $"All {allFiles.Length} file(s) in '{testFilesDirectory}' were excluded by extension filter " + + $"(excluded: {string.Join(", ", ExcludedTestFileExtensions)})."); + + if (excludedCount > 0) { - foreach (string additionalTestFile in additionalTestFiles) - { - targets.Add(additionalTestFile); - } + this.testOutputHelper.WriteLine( + $"[Info] {excludedCount} file(s) excluded by extension filter (e.g. .pdb). " + + $"Analyzing {targets.Count} of {allFiles.Length} files."); } - var context = new BinaryAnalyzerContext(); - var logger = new TestMessageLogger(); - context.Logger = logger; - PropertiesDictionary policy = null; - - if (useDefaultPolicy) + if (additionalTestFiles != null) { - policy = new PropertiesDictionary(); + targets.AddRange(additionalTestFiles); } - context.Policy = policy; + + var logger = new TestMessageLogger(); + PropertiesDictionary policy = useDefaultPolicy ? new PropertiesDictionary() : null; + var context = new BinaryAnalyzerContext { Logger = logger, Policy = policy }; skimmer.Initialize(context); @@ -107,86 +130,109 @@ private void Verify( skimmer.Analyze(context); } - var failTargets = new HashSet(logger.ErrorTargets.Union(logger.WarningTargets)); - var passTargets = new HashSet(logger.PassTargets); + var sb = new StringBuilder(); - if (logger.NoteTargets.Count > 0) + foreach (string target in targets) { - if (ignoreNoteTargets) - { - // If a same target has both warning/error and note result. - passTargets.UnionWith(logger.NoteTargets.Except(failTargets)); - } - else + string fileName = Path.GetFileName(target); + + switch (expectedOutcome) { - failTargets.UnionWith(logger.NoteTargets); + case ExpectedOutcome.Pass: + { + if (!logger.PassTargets.Contains(target)) + { + string actual = GetActualOutcome(logger, target); + sb.AppendLine( + $"Expected '{skimmer.Id}:{ruleName}' Pass (Kind={expectedKind}) " + + $"for '{fileName}' but got: {actual}"); + } + + break; + } + + case ExpectedOutcome.Fail: + { + bool found; + + if (expectedLevel == FailureLevel.None) + { + // Accept any failure level (backwards compat). + found = logger.ErrorTargets.Contains(target) || + logger.WarningTargets.Contains(target) || + logger.NoteTargets.Contains(target); + } + else + { + // Check specific failure level. + found = expectedLevel switch + { + FailureLevel.Error => logger.ErrorTargets.Contains(target), + FailureLevel.Warning => logger.WarningTargets.Contains(target), + FailureLevel.Note => logger.NoteTargets.Contains(target), + _ => false + }; + } + + if (!found) + { + // Allow config errors for PDB-related targets (backwards compat). + if (target.Contains("Pdb") && logger.ConfigurationErrorTargets.Contains(target)) + { + break; + } + + string actual = GetActualOutcome(logger, target); + string levelText = expectedLevel == FailureLevel.None + ? "any failure" + : expectedLevel.ToString(); + + sb.AppendLine( + $"Expected '{skimmer.Id}:{ruleName}' Fail (Level={levelText}) " + + $"for '{fileName}' but got: {actual}"); + } + + break; + } } } - HashSet expected = expectToPass ? passTargets : failTargets; - HashSet other = expectToPass ? failTargets : passTargets; - HashSet configErrors = logger.ConfigurationErrorTargets; + if (sb.Length > 0) + { + this.testOutputHelper.WriteLine(sb.ToString()); + } - string expectedText = expectToPass ? "success" : "failure"; - string actualText = expectToPass ? "failed" : "succeeded"; - var sb = new StringBuilder(); + Assert.Equal(0, sb.Length); + } - foreach (string target in targets) + private static string GetActualOutcome(TestMessageLogger logger, string target) + { + if (logger.PassTargets.Contains(target)) { - if (expected.Contains(target)) - { - expected.Remove(target); - continue; - } - bool missingEntirely = !other.Contains(target); + return "Pass"; + } - if (missingEntirely && - !expectToPass && - target.Contains("Pdb") && - configErrors.Contains(target)) - { - missingEntirely = false; - configErrors.Remove(target); - continue; - } + if (logger.ErrorTargets.Contains(target)) + { + return "Error"; + } - if (missingEntirely) - { - // Generates message such as the following: - // "Expected 'BA2025:EnableShadowStack' success but saw no result at all for file: Native_x64_CETShadowStack_Disabled.exe" - sb.AppendLine( - string.Format( - "Expected '{0}:{1}' {2} but saw no result at all for file: {3}", - skimmer.Id, - ruleName, - expectedText, - Path.GetFileName(target))); - } - else - { - other.Remove(target); + if (logger.WarningTargets.Contains(target)) + { + return "Warning"; + } - // Generates message such as the following: - // "Expected 'BA2025:EnableShadowStack' success but check failed for: Native_x64_CETShadowStack_Disabled.exe" - sb.AppendLine( - string.Format( - "Expected '{0}:{1}' {2} but check {3} for: {4}", - skimmer.Id, - ruleName, - expectedText, - actualText, - Path.GetFileName(target))); - } + if (logger.NoteTargets.Contains(target)) + { + return "Note"; } - if (sb.Length > 0) + if (logger.ConfigurationErrorTargets.Contains(target)) { - this.testOutputHelper.WriteLine(sb.ToString()); + return "ConfigurationError"; } - Assert.Equal(0, sb.Length); - Assert.Empty(expected); - Assert.Empty(other); + return "no result"; } private static string GetTestDirectoryFor(string ruleName) @@ -297,7 +343,6 @@ private void VerifyApplicability( HashSet applicabilityConditions, AnalysisApplicability expectedApplicability = AnalysisApplicability.NotApplicableToSpecifiedTarget, bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, string expectedReasonForNotAnalyzing = null) { string ruleName = skimmer.GetType().Name; @@ -311,7 +356,7 @@ private void VerifyApplicability( { foreach (string target in Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories)) { - if (bypassExtensionValidation || MultithreadedAnalyzeCommand.ValidAnalysisFileExtensions.Contains(Path.GetExtension(target))) + if (!ExcludedTestFileExtensions.Contains(Path.GetExtension(target))) { targets.Add(target); } @@ -548,7 +593,7 @@ public void BA2001_LoadImageAboveFourGigabyteAddress_Pass() [Fact] public void BA2001_LoadImageAboveFourGigabyteAddress_Fail() { - this.VerifyFail(new LoadImageAboveFourGigabyteAddress()); + this.Verify(new LoadImageAboveFourGigabyteAddress(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -582,8 +627,10 @@ public void BA2002_DoNotIncorporateVulnerableDependencies_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new DoNotIncorporateVulnerableDependencies(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -628,11 +675,37 @@ public void BA2004_EnableSecureSourceCodeHashing_Pass() } [Fact] - public void BA2004_EnableSecureSourceCodeHashing_Fail() + public void BA2004_EnableSecureSourceCodeHashing_Fail_Error() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSecureSourceCodeHashing(), useDefaultPolicy: true); + // Managed assemblies with insecure hashing and native binaries with + // directly insecure object files emit Error. + this.Verify( + new EnableSecureSourceCodeHashing(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, + useDefaultPolicy: true, + outcomeSubdirectory: "Error"); + } + else + { + VerifyThrows(new DoNotDisableStackProtectionForFunctions(), useDefaultPolicy: true); + } + } + + [Fact] + public void BA2004_EnableSecureSourceCodeHashing_Fail_Warning() + { + if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) + { + // Native binaries that link insecure static libraries emit Warning. + this.Verify( + new EnableSecureSourceCodeHashing(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, + useDefaultPolicy: true, + outcomeSubdirectory: "Warning"); } else { @@ -645,7 +718,7 @@ public void BA2005_DoNotShipVulnerableBinaries_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new DoNotShipVulnerableBinaries(), useDefaultPolicy: true); + this.Verify(new DoNotShipVulnerableBinaries(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else { @@ -704,8 +777,10 @@ public void BA2006_BuildWithSecureTools_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new BuildWithSecureTools(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -745,8 +820,10 @@ public void BA2007_EnableCriticalCompilerWarnings_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new EnableCriticalCompilerWarnings(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -794,8 +871,10 @@ public void BA2008_EnableControlFlowGuard_Pass() [Fact] public void BA2008_EnableControlFlowGuard_Fail() { - this.VerifyFail( + this.Verify( new EnableControlFlowGuard(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } @@ -824,7 +903,7 @@ public void BA2009_EnableAddressSpaceLayoutRandomization_Pass() [Fact] public void BA2009_EnableAddressSpaceLayoutRandomization_Fail() { - this.VerifyFail(new EnableAddressSpaceLayoutRandomization()); + this.Verify(new EnableAddressSpaceLayoutRandomization(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -849,7 +928,7 @@ public void BA2010_DoNotMarkImportsSectionAsExecutable_Pass() [Fact] public void BA2010_DoNotMarkImportsSectionAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkImportsSectionAsExecutable()); + this.Verify(new DoNotMarkImportsSectionAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -882,7 +961,7 @@ public void BA2011_EnableStackProtection_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableStackProtection()); + this.Verify(new EnableStackProtection(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } else { @@ -907,7 +986,7 @@ public void BA2012_DoNotModifyStackProtectionCookie_Pass() [Fact] public void BA2012_DoNotModifyStackProtectionCookie_Fail() { - this.VerifyFail(new DoNotModifyStackProtectionCookie()); + this.Verify(new DoNotModifyStackProtectionCookie(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -955,8 +1034,10 @@ public void BA2013_InitializeStackProtection_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new InitializeStackProtection(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -1010,9 +1091,11 @@ public void BA2014_DoNotDisableStackProtectionForFunctions_Fail() MetadataConditions.CouldNotLoadPdb, }; - this.VerifyFail( + this.Verify( new DoNotDisableStackProtectionForFunctions(), - GetTestFilesMatchingConditions(failureConditions), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, + additionalTestFiles: GetTestFilesMatchingConditions(failureConditions), useDefaultPolicy: true); } else @@ -1057,7 +1140,7 @@ public void BA2015_EnableHighEntropyVirtualAddresses_Pass() [Fact] public void BA2015_EnableHighEntropyVirtualAddresses_Fail() { - this.VerifyFail(new EnableHighEntropyVirtualAddresses()); + this.Verify(new EnableHighEntropyVirtualAddresses(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1082,7 +1165,7 @@ public void BA2016_MarkImageAsNXCompatible_Pass() [Fact] public void BA2016_MarkImageAsNXCompatible_Fail() { - this.VerifyFail(new MarkImageAsNXCompatible()); + this.Verify(new MarkImageAsNXCompatible(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1109,7 +1192,7 @@ public void BA2018_EnableSafeSEH_Pass() [Fact] public void BA2018_EnableSafeSEH_Fail() { - this.VerifyFail(new EnableSafeSEH()); + this.Verify(new EnableSafeSEH(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1134,7 +1217,7 @@ public void BA2019_DoNotMarkWritableSectionsAsShared_Pass() [Fact] public void BA2019_DoNotMarkWritableSectionsAsShared_Fail() { - this.VerifyFail(new DoNotMarkWritableSectionsAsShared()); + this.Verify(new DoNotMarkWritableSectionsAsShared(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1157,7 +1240,7 @@ public void BA2021_DoNotMarkWritableSectionsAsExecutable_Pass() [Fact] public void BA2021_DoNotMarkWritableSectionsAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkWritableSectionsAsExecutable()); + this.Verify(new DoNotMarkWritableSectionsAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1177,7 +1260,7 @@ public void BA2022_SignSecurely_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new SignSecurely()); + this.Verify(new SignSecurely(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } else { @@ -1218,7 +1301,7 @@ public void BA2024_EnableSpectreMitigations_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSpectreMitigations(), useDefaultPolicy: true); + this.Verify(new EnableSpectreMitigations(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1236,7 +1319,7 @@ public void BA2025_EnableShadowStack_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableShadowStack(), useDefaultPolicy: true); + this.Verify(new EnableShadowStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1254,7 +1337,7 @@ public void BA2025_EnableShadowStack_NotApplicable() this.VerifyApplicabililtyByConditionsOnly( skimmer: new EnableShadowStack(), - applicabilityConditions: notApplicableArm64, + applicabilityConditions: notApplicableArm, expectedReasonForNotAnalyzing: MetadataConditions.ImageIsArmBinary); } @@ -1263,7 +1346,7 @@ public void BA2026_EnableMicrosoftCompilerSdlSwitch_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableMicrosoftCompilerSdlSwitch(), useDefaultPolicy: true); + this.Verify(new EnableMicrosoftCompilerSdlSwitch(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1297,7 +1380,7 @@ public void BA2027_EnableSourceLink_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSourceLink()); + this.Verify(new EnableSourceLink(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning); } } @@ -1340,13 +1423,13 @@ public void BA2029_EnableIntegrityCheck_NotApplicable() [Fact] public void BA3001_EnablePositionIndependentExecutable_Pass() { - this.VerifyPass(new EnablePositionIndependentExecutable(), bypassExtensionValidation: true); + this.VerifyPass(new EnablePositionIndependentExecutable()); } [Fact] public void BA3001_EnablePositionIndependentExecutable_Fail() { - this.VerifyFail(new EnablePositionIndependentExecutable(), bypassExtensionValidation: true); + this.Verify(new EnablePositionIndependentExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1358,13 +1441,13 @@ public void BA3001_EnablePositionIndependentExecutable_NotApplicable() [Fact] public void BA3002_DoNotMarkStackAsExecutable_Pass() { - this.VerifyPass(new DoNotMarkStackAsExecutable(), bypassExtensionValidation: true); + this.VerifyPass(new DoNotMarkStackAsExecutable()); } [Fact] public void BA3002_DoNotMarkStackAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkStackAsExecutable(), bypassExtensionValidation: true); + this.Verify(new DoNotMarkStackAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1376,79 +1459,79 @@ public void BA3002_DoNotMarkStackAsExecutable_NotApplicable() [Fact] public void BA3003_EnableStackProtector_Pass() { - this.VerifyPass(new EnableStackProtector(), bypassExtensionValidation: true); + this.VerifyPass(new EnableStackProtector()); } [Fact] public void BA3003_EnableStackProtector_Fail() { - this.VerifyFail(new EnableStackProtector(), bypassExtensionValidation: true); + this.Verify(new EnableStackProtector(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3003_EnableStackProtector_NotApplicable() { - this.VerifyApplicability(new EnableStackProtector(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableStackProtector(), new HashSet()); } [Fact] public void BA3004_GenerateRequiredSymbolFormat_Pass() { - this.VerifyPass(new GenerateRequiredSymbolFormat(), bypassExtensionValidation: true); + this.VerifyPass(new GenerateRequiredSymbolFormat()); } [Fact] public void BA3004_GenerateRequiredSymbolFormat_Fail() { - this.VerifyFail(new GenerateRequiredSymbolFormat(), bypassExtensionValidation: true); + this.Verify(new GenerateRequiredSymbolFormat(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3005_EnableStackClashProtection_Pass() { - this.VerifyPass(new EnableStackClashProtection(), bypassExtensionValidation: true); + this.VerifyPass(new EnableStackClashProtection()); } [Fact] public void BA3005_EnableStackClashProtection_Fail() { - this.VerifyFail(new EnableStackClashProtection(), bypassExtensionValidation: true); + this.Verify(new EnableStackClashProtection(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3005_EnableStackClashProtection_NotApplicable() { - this.VerifyApplicability(new EnableStackClashProtection(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableStackClashProtection(), new HashSet()); } [Fact] public void BA3006_EnableNonExecutableStack_Pass() { - this.VerifyPass(new EnableNonExecutableStack(), bypassExtensionValidation: true); + this.VerifyPass(new EnableNonExecutableStack()); } [Fact] public void BA3006_EnableNonExecutableStack_Fail() { - this.VerifyFail(new EnableNonExecutableStack(), bypassExtensionValidation: true); + this.Verify(new EnableNonExecutableStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3006_EnableNonExecutableStack_NotApplicable() { - this.VerifyApplicability(new EnableNonExecutableStack(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableNonExecutableStack(), new HashSet()); } [Fact] public void BA3010_EnableReadOnlyRelocations_Pass() { - this.VerifyPass(new EnableReadOnlyRelocations(), bypassExtensionValidation: true); + this.VerifyPass(new EnableReadOnlyRelocations()); } [Fact] public void BA3010_EnableReadOnlyRelocations_Fail() { - this.VerifyFail(new EnableReadOnlyRelocations(), bypassExtensionValidation: true); + this.Verify(new EnableReadOnlyRelocations(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1460,13 +1543,13 @@ public void BA3010_EnableReadOnlyRelocations_NotApplicable() [Fact] public void BA3011_EnableBindNow_Pass() { - this.VerifyPass(new EnableBindNow(), bypassExtensionValidation: true); + this.VerifyPass(new EnableBindNow()); } [Fact] public void BA3011_EnableBindNow_Fail() { - this.VerifyFail(new EnableBindNow(), bypassExtensionValidation: true); + this.Verify(new EnableBindNow(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1478,13 +1561,13 @@ public void BA3011_EnableBindNow_NotApplicable() [Fact] public void BA3030_UseGccCheckedFunctions_Pass() { - this.VerifyPass(new UseGccCheckedFunctions(), bypassExtensionValidation: true); + this.VerifyPass(new UseGccCheckedFunctions()); } [Fact] public void BA3030_UseGccCheckedFunctions_Fail() { - this.VerifyFail(new UseGccCheckedFunctions(), bypassExtensionValidation: true); + this.Verify(new UseGccCheckedFunctions(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1496,13 +1579,13 @@ public void BA3030_UseGccCheckedFunctions_NotApplicable() [Fact] public void BA3031_EnableClangSafeStack_Pass() { - this.VerifyPass(new EnableClangSafeStack(), bypassExtensionValidation: true); + this.VerifyPass(new EnableClangSafeStack()); } [Fact] public void BA3031_EnableClangSafeStack_Fail() { - this.VerifyFail(new EnableClangSafeStack(), bypassExtensionValidation: true); + this.Verify(new EnableClangSafeStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1514,13 +1597,13 @@ public void BA3031_EnableClangSafeStack_NotApplicable() [Fact] public void BA5001_EnablePositionIndependentExecutableMachO_Pass() { - this.VerifyPass(new EnablePositionIndependentExecutableMachO(), bypassExtensionValidation: true); + this.VerifyPass(new EnablePositionIndependentExecutableMachO()); } [Fact] public void BA5001_EnablePositionIndependentExecutableMachO_Fail() { - this.VerifyFail(new EnablePositionIndependentExecutableMachO(), bypassExtensionValidation: true); + this.Verify(new EnablePositionIndependentExecutableMachO(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1532,13 +1615,13 @@ public void BA5001_EnablePositionIndependentExecutableMachO_NotApplicable() [Fact] public void BA5002_DoNotAllowExecutableStack_Pass() { - this.VerifyPass(new DoNotAllowExecutableStack(), bypassExtensionValidation: true); + this.VerifyPass(new DoNotAllowExecutableStack()); } [Fact] public void BA5002_DoNotAllowExecutableStack_Fail() { - this.VerifyFail(new DoNotAllowExecutableStack(), bypassExtensionValidation: true); + this.Verify(new DoNotAllowExecutableStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1552,15 +1635,10 @@ public void BA6001_DisableIncrementalLinkingInReleaseBuilds_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new DisableIncrementalLinkingInReleaseBuilds(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1597,15 +1675,10 @@ public void BA6002_EliminateDuplicateStrings_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EliminateDuplicateStrings(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1642,15 +1715,10 @@ public void BA6004_EnableCOMDATFolding_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableComdatFolding(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1687,15 +1755,10 @@ public void BA6005_EnableOptimizeReferences_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableOptimizeReferences(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1732,15 +1795,10 @@ public void BA6006_EnableLinkTimeCodeGeneration_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableLinkTimeCodeGeneration(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs new file mode 100644 index 000000000..291df678a --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -0,0 +1,558 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.Sarif; + +using Xunit; + +namespace Microsoft.CodeAnalysis.IL +{ + [Trait("Category", "Integration")] + public class AnalyzeCommandIntegrationTests : IDisposable + { + private readonly string _tempDir; + + public AnalyzeCommandIntegrationTests() + { + _tempDir = Path.Join(Path.GetTempPath(), "BinSkimIntegration", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + } + + public void Dispose() + { + try { Directory.Delete(_tempDir, recursive: true); } catch { /* best-effort cleanup */ } + } + + [Fact] + public async Task Analyze_SelfScan_ProducesValidSarif() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Join(_tempDir, "output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + }); + + File.Exists(sarifOutput).Should().BeTrue("SARIF output file should be created"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs.Should().HaveCount(1); + sarifLog.Runs[0].Tool.Driver.Name.Should().Be("BinSkim"); + } + + [Fact] + public async Task Analyze_KnownFailBinary_ProducesErrorResults() + { + string failBinary = GetFunctionalTestDataPath( + "BA2016.MarkImageAsNXCompatible", "Fail", "ManagedFail.dll"); + string sarifOutput = Path.Join(_tempDir, "fail-output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + failBinary, + "-o", sarifOutput, + "--run-only-rules", "BA2016", + "--kind", "Fail", + "--level", "Error;Warning;Note", + }); + + // BinSkim exits 0 even when rules fire errors — exit code reflects tool health, not rule results. + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis successfully.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA2016" && r.Level == FailureLevel.Error, + "BA2016 should fire an error for ManagedFail.dll (not NX compatible)"); + } + + [Fact] + public async Task Analyze_RunOnlyRules_FiltersToSpecifiedRule() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Join(_tempDir, "filtered.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + "--run-only-rules", "BA2016", + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + // Only BA2016 results should appear (plus WRN999 notifications for disabled rules). + sarifLog.Runs[0].Results + .Where(r => !r.RuleId.StartsWith("WRN")) + .Should().OnlyContain( + r => r.RuleId == "BA2016", + "only BA2016 results should be present when --run-only-rules BA2016 is specified"); + } + + [Fact] + public async Task Analyze_NoValidTargets_ExitsWithNonZero() + { + string nonExistentTarget = Path.Join(_tempDir, "does_not_exist.dll"); + string sarifOutput = Path.Join(_tempDir, "output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + nonExistentTarget, + "-o", sarifOutput, + }); + + result.ExitCode.Should().NotBe(0, + "BinSkim should report a non-zero exit code when no valid targets are found."); + } + + [Fact] + public async Task Analyze_InvalidArgument_ExitsWithNonZero() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + "--bogus-flag", + }); + + result.ExitCode.Should().NotBe(0, + "An unrecognized CLI argument should produce a non-zero exit code."); + } + + [Fact] + public async Task Analyze_InvalidVerb_ExitsWithNonZero() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "not-a-real-verb", + }); + + result.ExitCode.Should().NotBe(0, + "An unrecognized verb should produce a non-zero exit code."); + } + + [Fact] + public async Task Analyze_HelpFlag_ExitsCleanly() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "help", + }); + + result.ExitCode.Should().Be(0); + string combinedOutput = result.StdOut + result.StdErr; + combinedOutput.Should().NotBeNullOrWhiteSpace("help output should be printed"); + } + + [Fact] + public async Task Analyze_VersionFlag_ExitsCleanly() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "--version", + }); + + result.ExitCode.Should().Be(0); + string combinedOutput = result.StdOut + result.StdErr; + combinedOutput.Should().NotBeNullOrWhiteSpace("version output should be printed"); + } + + [Fact] + public async Task Dump_SelfScan_ProducesMetadataOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + }); + + result.ExitCode.Should().Be(0, + $"Dump should succeed.\nStdErr: {result.StdErr}"); + result.StdOut.Should().Contain("BinSkim.dll", + "dump output should reference the target binary"); + } + + [Fact] + public async Task Dump_Verbose_ProducesMoreDetailedOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + + BinSkimRunResult normalResult = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + }); + + BinSkimRunResult verboseResult = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + "--verbose", + }); + + normalResult.ExitCode.Should().Be(0); + verboseResult.ExitCode.Should().Be(0); + + verboseResult.StdOut.Length.Should().BeGreaterThanOrEqualTo(normalResult.StdOut.Length, + "verbose dump should produce output at least as detailed as non-verbose"); + } + + [Fact] + public async Task ExportRules_ProducesValidSarifOutput() + { + string outputPath = Path.Join(_tempDir, "rules.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "export-rules", + outputPath, + }); + + result.ExitCode.Should().Be(0, + $"export-rules should succeed.\nStdErr: {result.StdErr}"); + + File.Exists(outputPath).Should().BeTrue("rules SARIF file should be created"); + + string json = File.ReadAllText(outputPath); + json.Should().Contain("BA2016", + "exported rules should include BA2016 (MarkImageAsNXCompatible)"); + } + + [Fact] + public async Task ExportConfig_ProducesValidJsonOutput() + { + string outputPath = Path.Join(_tempDir, "config.json"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "export-config", + outputPath, + }); + + result.ExitCode.Should().Be(0, + $"export-config should succeed.\nStdErr: {result.StdErr}"); + + File.Exists(outputPath).Should().BeTrue("config JSON file should be created"); + + string json = File.ReadAllText(outputPath); + json.Should().NotBeNullOrWhiteSpace("config file should contain content"); + } + + /// + /// Finds the repository root by searching upward for the BinSkim.sln file. + /// + private static string FindRepoRoot() + { + string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + while (dir != null) + { + if (File.Exists(Path.Combine(dir, "src", "BinSkim.sln"))) + { + return dir; + } + + dir = Path.GetDirectoryName(dir); + } + + throw new DirectoryNotFoundException( + "Could not locate the repository root (searched upward for src/BinSkim.sln from assembly location)."); + } + + /// + /// Resolves a path into the BinSkim.Rules functional test data directory. + /// These binaries are curated per-rule with known Pass/Fail outcomes. + /// + private static string GetFunctionalTestDataPath(params string[] relativeParts) + { + string repoRoot = FindRepoRoot(); + string testDataRoot = Path.Join(repoRoot, "src", + "Test.FunctionalTests.BinSkim.Rules", "FunctionalTestData"); + string fullPath = Path.Join(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + + if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) + { + throw new FileNotFoundException( + $"Functional test data not found at: {fullPath}"); + } + + return fullPath; + } + + /// + /// Resolves a path into the BinaryParsers unit test data directory. + /// + private static string GetBinaryParsersTestDataPath(params string[] relativeParts) + { + string repoRoot = FindRepoRoot(); + string testDataRoot = Path.Join(repoRoot, "src", + "Test.UnitTests.BinaryParsers", "TestData"); + string fullPath = Path.Join(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + + if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) + { + throw new FileNotFoundException( + $"BinaryParsers test data not found at: {fullPath}"); + } + + return fullPath; + } + + #region ELF Binary Analysis + + [Fact] + public async Task Analyze_ElfBinary_ProducesValidSarif() + { + string elfBinary = GetFunctionalTestDataPath( + "BA3001.EnablePositionIndependentExecutable", "Pass", "gcc.pie_executable"); + string sarifOutput = Path.Join(_tempDir, "elf-sarif.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + elfBinary, + "-o", sarifOutput, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete ELF analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs.Should().HaveCount(1); + sarifLog.Runs[0].Tool.Driver.Name.Should().Be("BinSkim"); + sarifLog.Runs[0].Results.Should().NotBeEmpty( + "ELF rules should produce results for a valid ELF executable"); + } + + [Fact] + public async Task Analyze_ElfBinary_KnownFail_ReportsElfRule() + { + string failBinary = GetFunctionalTestDataPath( + "BA3006.EnableNonExecutableStack", "Fail", "gcc.helloworld.execstack.5.o"); + string sarifOutput = Path.Combine(_tempDir, "elf-fail.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + failBinary, + "-o", sarifOutput, + "--run-only-rules", "BA3006", + "--kind", "Fail", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA3006" && r.Level == FailureLevel.Error, + "BA3006 should fire an error for an ELF binary with executable stack"); + } + + [Fact] + public async Task Analyze_ElfBinary_KnownPass_ReportsPass() + { + string passBinary = GetFunctionalTestDataPath( + "BA3001.EnablePositionIndependentExecutable", "Pass", "gcc.pie_executable"); + string sarifOutput = Path.Combine(_tempDir, "elf-pass.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + passBinary, + "-o", sarifOutput, + "--run-only-rules", "BA3001", + "--kind", "Fail;Pass", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA3001" && r.Level == FailureLevel.None, + "BA3001 should pass for a PIE executable"); + } + + #endregion + + #region Multi-target and Glob + + [Fact] + public async Task Analyze_MultipleTargets_ScansAll() + { + string target1 = BinSkimRunner.GetBinSkimDllPath(); + string target2 = GetFunctionalTestDataPath( + "BA2016.MarkImageAsNXCompatible", "Fail", "ManagedFail.dll"); + string sarifOutput = Path.Combine(_tempDir, "multi.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + target1, + target2, + "-o", sarifOutput, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should handle multiple targets.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + // Results should reference both target files + var targetUris = sarifLog.Runs[0].Results + .Select(r => r.Locations?.FirstOrDefault()?.PhysicalLocation?.ArtifactLocation?.Uri?.ToString() ?? "") + .Distinct() + .ToList(); + + targetUris.Should().HaveCountGreaterThan(1, + "results should come from multiple target binaries"); + } + + [Fact] + public async Task Analyze_Recurse_FindsNestedBinaries() + { + // Set up a temp directory tree with binaries + string subDir = Path.Combine(_tempDir, "nested"); + Directory.CreateDirectory(subDir); + + string source = BinSkimRunner.GetBinSkimDllPath(); + string copy1 = Path.Combine(_tempDir, "top.dll"); + string copy2 = Path.Combine(subDir, "nested.dll"); + File.Copy(source, copy1); + File.Copy(source, copy2); + + string sarifOutput = Path.Combine(_tempDir, "recurse.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + Path.Join(_tempDir, "*.dll"), + "-o", sarifOutput, + "--recurse", "True", + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should recurse directories.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + var analyzedFiles = sarifLog.Runs[0].Results + .Select(r => r.Locations?.FirstOrDefault()?.PhysicalLocation?.ArtifactLocation?.Uri?.ToString() ?? "") + .Distinct() + .ToList(); + + analyzedFiles.Should().HaveCountGreaterThan(1, + "recurse should find binaries in subdirectories"); + } + + #endregion + + #region Rich Return Code + + [Fact] + public async Task Analyze_RichReturnCode_InvalidTarget_ReturnsRuntimeConditions() + { + string nonExistentTarget = Path.Combine(_tempDir, "does_not_exist.dll"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + nonExistentTarget, + "--rich-return-code", + }); + + // With rich return code, the exit code encodes RuntimeConditions flags + result.ExitCode.Should().NotBe(0, + "rich return code should return non-zero for invalid targets"); + result.ExitCode.Should().NotBe(1, + "rich return code should return a RuntimeConditions bitmask, not simple 1"); + } + + #endregion + + #region Local Symbol Directories + + [Fact] + public async Task Analyze_LocalSymbolDirectories_AcceptsOption() + { + string elfBinary = GetBinaryParsersTestDataPath("Dwarf", "hello-dwarf4-o2"); + string sarifOutput = Path.Join(_tempDir, "symdir.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + elfBinary, + "-o", sarifOutput, + "--local-symbol-directories", _tempDir, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should accept --local-symbol-directories.\nStdErr: {result.StdErr}"); + } + + #endregion + + #region Trace Output + + [Fact] + public async Task Analyze_TraceTargetsScanned_ProducesTraceOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Join(_tempDir, "trace.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + "--trace", "TargetsScanned;ResultsSummary", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should accept trace flags.\nStdErr: {result.StdErr}"); + + string combined = result.StdOut + result.StdErr; + combined.Should().NotBeNullOrWhiteSpace( + "trace output should produce console output"); + } + + #endregion + } +} diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 b/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 new file mode 100644 index 000000000..3cd9563c2 --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 @@ -0,0 +1,395 @@ +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#Requires -Modules Pester + +<# +.SYNOPSIS + Pester integration tests for BinSkim CLI. + Validates end-to-end behavior by invoking BinSkim as an external process. + +.DESCRIPTION + These tests mirror the C# integration tests in AnalyzeCommandIntegrationTests.cs + but are written in PowerShell using the Pester framework for use in pipeline scenarios. + +.EXAMPLE + Invoke-Pester -Path ./BinSkim.Integration.Tests.ps1 +#> + +BeforeAll { + $RepoRoot = (Resolve-Path "$PSScriptRoot/../..").Path + $BinSkimDll = Join-Path $RepoRoot "bld/bin/BinSkim.Driver/release/BinSkim.dll" + + if (-not (Test-Path $BinSkimDll)) { + # Try debug build + $BinSkimDll = Join-Path $RepoRoot "bld/bin/BinSkim.Driver/debug/BinSkim.dll" + } + + if (-not (Test-Path $BinSkimDll)) { + throw "BinSkim.dll not found. Build the BinSkim.Driver project first." + } + + $FunctionalTestData = Join-Path $RepoRoot "src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData" + $BinaryParsersTestData = Join-Path $RepoRoot "src/Test.UnitTests.BinaryParsers/TestData" + + function Invoke-BinSkim { + param( + [string[]]$Arguments + ) + $process = Start-Process -FilePath "dotnet" -ArgumentList (@($BinSkimDll) + $Arguments) ` + -NoNewWindow -Wait -PassThru -RedirectStandardOutput "$env:TEMP\binskim_stdout.txt" ` + -RedirectStandardError "$env:TEMP\binskim_stderr.txt" + + return @{ + ExitCode = $process.ExitCode + StdOut = (Get-Content "$env:TEMP\binskim_stdout.txt" -Raw -ErrorAction SilentlyContinue) ?? "" + StdErr = (Get-Content "$env:TEMP\binskim_stderr.txt" -Raw -ErrorAction SilentlyContinue) ?? "" + } + } + + function Get-SarifLog { + param([string]$Path) + if (Test-Path $Path) { + return Get-Content $Path -Raw | ConvertFrom-Json + } + return $null + } +} + +Describe "BinSkim CLI - Self Scan" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPester_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze self-scan exits with zero" { + $sarifOutput = Join-Path $script:TempDir "selfscan.sarif" + $result = Invoke-BinSkim -Arguments @("analyze", $BinSkimDll, "-o", $sarifOutput) + $result.ExitCode | Should -Be 0 + } + + It "Analyze self-scan produces valid SARIF" { + $sarifOutput = Join-Path $script:TempDir "selfscan-sarif.sarif" + $result = Invoke-BinSkim -Arguments @("analyze", $BinSkimDll, "-o", $sarifOutput) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $sarif.runs | Should -HaveCount 1 + $sarif.runs[0].tool.driver.name | Should -Be "BinSkim" + } +} + +Describe "BinSkim CLI - ELF Binary Analysis" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterElf_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze ELF binary exits with zero" { + $elfBinary = Join-Path $FunctionalTestData "BA3006.EnableNonExecutableStack/Pass/gcc.helloworld.noexecstack.5.o" + $sarifOutput = Join-Path $script:TempDir "elf-exit.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + } + + It "Analyze ELF binary produces valid SARIF with results" { + $elfBinary = Join-Path $FunctionalTestData "BA3001.EnablePositionIndependentExecutable/Pass/gcc.pie_executable" + $sarifOutput = Join-Path $script:TempDir "elf-sarif.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $sarif.runs[0].tool.driver.name | Should -Be "BinSkim" + $sarif.runs[0].results.Count | Should -BeGreaterThan 0 + } + + It "Analyze ELF known-fail binary reports BA3006 error" { + $failBinary = Join-Path $FunctionalTestData "BA3006.EnableNonExecutableStack/Fail/gcc.helloworld.execstack.5.o" + $sarifOutput = Join-Path $script:TempDir "elf-fail.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $failBinary, "-o", $sarifOutput, + "--run-only-rules", "BA3006", "--kind", "Fail", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $errorResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA3006" -and $_.level -eq "error" } + $errorResults | Should -Not -BeNullOrEmpty + } + + It "Analyze ELF known-pass binary reports BA3001 pass" { + $passBinary = Join-Path $FunctionalTestData "BA3001.EnablePositionIndependentExecutable/Pass/gcc.pie_executable" + $sarifOutput = Join-Path $script:TempDir "elf-pass.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $passBinary, "-o", $sarifOutput, + "--run-only-rules", "BA3001", "--kind", "Fail;Pass", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $passResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA3001" -and $_.level -eq "none" } + $passResults | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Multi-target and Glob" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterMulti_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze multiple targets scans all" { + $target1 = $BinSkimDll + $failBinary = Join-Path $FunctionalTestData "BA2016.MarkImageAsNXCompatible/Fail/ManagedFail.dll" + $sarifOutput = Join-Path $script:TempDir "multi.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $target1, $failBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $uris = $sarif.runs[0].results | ForEach-Object { + $_.locations[0].physicalLocation.artifactLocation.uri + } | Sort-Object -Unique + $uris.Count | Should -BeGreaterThan 1 + } + + It "Analyze with recurse finds nested binaries" { + $subDir = Join-Path $script:TempDir "nested" + New-Item -ItemType Directory -Path $subDir -Force | Out-Null + + Copy-Item $BinSkimDll (Join-Path $script:TempDir "top.dll") + Copy-Item $BinSkimDll (Join-Path $subDir "nested.dll") + + $sarifOutput = Join-Path $script:TempDir "recurse.sarif" + $globPattern = Join-Path $script:TempDir "*.dll" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $globPattern, "-o", $sarifOutput, + "--recurse", "True", "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $uris = $sarif.runs[0].results | ForEach-Object { + $_.locations[0].physicalLocation.artifactLocation.uri + } | Sort-Object -Unique + $uris.Count | Should -BeGreaterThan 1 + } +} + +Describe "BinSkim CLI - Rich Return Code" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterRRC_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Rich return code returns non-zero RuntimeConditions for invalid target" { + $nonExistent = Join-Path $script:TempDir "does_not_exist.dll" + + $result = Invoke-BinSkim -Arguments @("analyze", $nonExistent, "--rich-return-code") + + $result.ExitCode | Should -Not -Be 0 + $result.ExitCode | Should -Not -Be 1 + } +} + +Describe "BinSkim CLI - Local Symbol Directories" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterSym_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Accepts --local-symbol-directories option" { + $elfBinary = Join-Path $BinaryParsersTestData "Dwarf/hello-dwarf4-o2" + $sarifOutput = Join-Path $script:TempDir "symdir.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--local-symbol-directories", $script:TempDir, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + } +} + +Describe "BinSkim CLI - Trace Output" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterTrace_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Trace flags produce output" { + $sarifOutput = Join-Path $script:TempDir "trace.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $BinSkimDll, "-o", $sarifOutput, + "--trace", "TargetsScanned;ResultsSummary" + ) + $result.ExitCode | Should -Be 0 + + $combined = $result.StdOut + $result.StdErr + $combined | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Error Handling" { + It "Non-existent target exits with non-zero" { + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterErr_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + $nonExistent = Join-Path $tempDir "does_not_exist.dll" + $sarifOutput = Join-Path $tempDir "output.sarif" + + $result = Invoke-BinSkim -Arguments @("analyze", $nonExistent, "-o", $sarifOutput) + $result.ExitCode | Should -Not -Be 0 + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It "Invalid argument exits with non-zero" { + $result = Invoke-BinSkim -Arguments @("analyze", "--bogus-flag") + $result.ExitCode | Should -Not -Be 0 + } + + It "Invalid verb exits with non-zero" { + $result = Invoke-BinSkim -Arguments @("not-a-real-verb") + $result.ExitCode | Should -Not -Be 0 + } +} + +Describe "BinSkim CLI - Help and Version" { + It "Help flag exits cleanly" { + $result = Invoke-BinSkim -Arguments @("help") + $result.ExitCode | Should -Be 0 + ($result.StdOut + $result.StdErr) | Should -Not -BeNullOrEmpty + } + + It "--version exits cleanly" { + $result = Invoke-BinSkim -Arguments @("--version") + $result.ExitCode | Should -Be 0 + ($result.StdOut + $result.StdErr) | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Known Fail Binary" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterFail_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Known PE fail binary produces error results" { + $failBinary = Join-Path $FunctionalTestData "BA2016.MarkImageAsNXCompatible/Fail/ManagedFail.dll" + $sarifOutput = Join-Path $script:TempDir "pe-fail.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $failBinary, "-o", $sarifOutput, + "--run-only-rules", "BA2016", "--kind", "Fail", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $errorResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA2016" -and $_.level -eq "error" } + $errorResults | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Dump Command" { + It "Dump self-scan produces metadata output" { + $result = Invoke-BinSkim -Arguments @("dump", $BinSkimDll) + $result.ExitCode | Should -Be 0 + $result.StdOut | Should -Match "BinSkim" + } + + It "Dump verbose produces more detailed output" { + $normalResult = Invoke-BinSkim -Arguments @("dump", $BinSkimDll) + $verboseResult = Invoke-BinSkim -Arguments @("dump", $BinSkimDll, "--verbose") + + $normalResult.ExitCode | Should -Be 0 + $verboseResult.ExitCode | Should -Be 0 + $verboseResult.StdOut.Length | Should -BeGreaterOrEqual $normalResult.StdOut.Length + } +} + +Describe "BinSkim CLI - Export Commands" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterExport_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Export-rules produces SARIF with rule metadata" { + $outputPath = Join-Path $script:TempDir "rules.sarif" + + $result = Invoke-BinSkim -Arguments @("export-rules", $outputPath) + $result.ExitCode | Should -Be 0 + + Test-Path $outputPath | Should -BeTrue + $content = Get-Content $outputPath -Raw + $content | Should -Match "BA2016" + } + + It "Export-config produces JSON config" { + $outputPath = Join-Path $script:TempDir "config.json" + + $result = Invoke-BinSkim -Arguments @("export-config", $outputPath) + $result.ExitCode | Should -Be 0 + + Test-Path $outputPath | Should -BeTrue + $content = Get-Content $outputPath -Raw + $content | Should -Not -BeNullOrEmpty + } +} diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs new file mode 100644 index 000000000..9cbd5e123 --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis.Sarif; + +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.IL +{ + /// + /// Result of running BinSkim as an external process. + /// + public sealed class BinSkimRunResult + { + public int ExitCode { get; init; } + public string StdOut { get; init; } + public string StdErr { get; init; } + public string SarifOutputPath { get; init; } + + /// + /// Deserializes the SARIF output file if it exists. + /// Returns null if no output path was specified or the file was not created. + /// + public SarifLog LoadSarifLog() + { + if (string.IsNullOrEmpty(SarifOutputPath) || !File.Exists(SarifOutputPath)) + { + return null; + } + + string json = File.ReadAllText(SarifOutputPath); + return JsonConvert.DeserializeObject(json); + } + } + + /// + /// Launches BinSkim as an external process via "dotnet BinSkim.dll" for cross-platform compatibility. + /// Captures stdout, stderr, exit code, and optionally deserializes produced SARIF output. + /// + public static class BinSkimRunner + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(120); + + /// + /// Resolves the path to the built BinSkim.dll relative to the test assembly location. + /// Layout: bld/bin/Test.IntegrationTests.BinSkim.Driver/release/ → ../../BinSkim.Driver/release/BinSkim.dll + /// + public static string GetBinSkimDllPath() + { + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string binSkimDll = Path.GetFullPath( + Path.Join(assemblyDir, "..", "..", "BinSkim.Driver", "release", "BinSkim.dll")); + + if (!File.Exists(binSkimDll)) + { + // Fall back to debug build. + binSkimDll = Path.GetFullPath( + Path.Join(assemblyDir, "..", "..", "BinSkim.Driver", "debug", "BinSkim.dll")); + } + + if (!File.Exists(binSkimDll)) + { + throw new FileNotFoundException( + $"BinSkim.dll not found at expected path (checked both release and debug): {binSkimDll}. " + + "Ensure the BinSkim.Driver project has been built."); + } + + return binSkimDll; + } + + /// + /// Runs BinSkim as an external process with the specified arguments. + /// + public static async Task RunAsync( + string[] args, + TimeSpan? timeout = null) + { + string binSkimDll = GetBinSkimDllPath(); + timeout ??= DefaultTimeout; + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + startInfo.ArgumentList.Add(binSkimDll); + foreach (string arg in args) + { + startInfo.ArgumentList.Add(arg); + } + + using var process = new Process { StartInfo = startInfo }; + using var cts = new CancellationTokenSource(timeout.Value); + + process.Start(); + + // Read stdout and stderr concurrently to avoid deadlocks from filled OS buffers. + Task stdOutTask = process.StandardOutput.ReadToEndAsync(); + Task stdErrTask = process.StandardError.ReadToEndAsync(); + + try + { + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + try { process.Kill(entireProcessTree: true); } catch { /* best-effort cleanup */ } + throw new TimeoutException( + $"BinSkim process did not exit within {timeout.Value.TotalSeconds}s. " + + $"Args: {string.Join(" ", args)}"); + } + + string stdOut = await stdOutTask; + string stdErr = await stdErrTask; + + string sarifPath = FindOutputPath(args); + + return new BinSkimRunResult + { + ExitCode = process.ExitCode, + StdOut = stdOut, + StdErr = stdErr, + SarifOutputPath = sarifPath, + }; + } + + private static string FindOutputPath(string[] args) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i] is "--output" or "-o") + { + return args[i + 1]; + } + } + + return null; + } + } +} diff --git a/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj new file mode 100644 index 000000000..734ff0f2f --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj @@ -0,0 +1,33 @@ + + + + + $(NetCoreVersion) + Library + True + + Integration + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + false + false + + + diff --git a/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json b/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json new file mode 100644 index 000000000..9c92449ed --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "methodDisplay": "classAndMethod", + "diagnosticMessages": true +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs new file mode 100644 index 000000000..44cd2bf07 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using FluentAssertions; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfAttributeValueTests + { + [Fact] + public void Address_ReturnsUnderlyingUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Address, + Value = 0x12345678UL, + }; + + value.Address.Should().Be(0x12345678UL); + } + + [Fact] + public void Block_ReturnsUnderlyingByteArray() + { + byte[] bytes = { 0x01, 0x02, 0x03 }; + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Block, + Value = bytes, + }; + + value.Block.Should().Equal(0x01, 0x02, 0x03); + } + + [Fact] + public void Constant_ReturnsUnderlyingUlong_WhenValueIsUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 42UL, + }; + + value.Constant.Should().Be(42UL); + } + + [Theory] + [InlineData(new byte[] { 0x2A }, 0x2AUL)] // 1 byte + [InlineData(new byte[] { 0x34, 0x12 }, 0x1234UL)] // 2 bytes (LE) + [InlineData(new byte[] { 0x78, 0x56, 0x34, 0x12 }, 0x12345678UL)] // 4 bytes (LE) + [InlineData(new byte[] { 0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12 }, 0x1234567890ABCDEFUL)] // 8 bytes (LE) + public void Constant_DecodesSupportedByteArrayLengths(byte[] bytes, ulong expected) + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = bytes, + }; + + value.Constant.Should().Be(expected); + } + + [Fact] + public void Constant_ThrowsForUnsupportedByteArrayLength() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = new byte[] { 0x01, 0x02, 0x03 }, // 3 bytes → not implemented + }; + + Action act = () => _ = value.Constant; + + act.Should().Throw(); + } + + [Fact] + public void String_ReturnsUnderlyingString() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "hello", + }; + + value.String.Should().Be("hello"); + } + + [Fact] + public void Flag_ReturnsUnderlyingBool() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Flag, + Value = true, + }; + + value.Flag.Should().BeTrue(); + } + + [Fact] + public void Reference_ReturnsUnderlyingDwarfSymbol() + { + var symbol = new DwarfSymbol(); + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.ResolvedReference, + Value = symbol, + }; + + value.Reference.Should().BeSameAs(symbol); + } + + [Fact] + public void ExpressionLocation_ReturnsUnderlyingByteArray() + { + byte[] bytes = { 0xAA, 0xBB }; + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.ExpressionLocation, + Value = bytes, + }; + + value.ExpressionLocation.Should().Equal(0xAA, 0xBB); + } + + [Fact] + public void SecOffset_ReturnsUnderlyingUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.SecOffset, + Value = 0xCAFEBABEUL, + }; + + value.SecOffset.Should().Be(0xCAFEBABEUL); + } + + // ---- Equality and hash code semantics ---- + + [Fact] + public void Equals_ReturnsTrue_WhenBothNull() + { + DwarfAttributeValue left = null; + DwarfAttributeValue right = null; + + (left == right).Should().BeTrue(); + (left != right).Should().BeFalse(); + } + + [Fact] + public void Equals_ReturnsFalse_WhenOnlyOneNull() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 1UL, + }; + + (value == null).Should().BeFalse(); + (null == value).Should().BeFalse(); + (value != null).Should().BeTrue(); + (null != value).Should().BeTrue(); + } + + [Fact] + public void Equals_ReturnsFalse_WhenTypesDiffer() + { + var value1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 1UL, + }; + + var value2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Address, + Value = 1UL, + }; + + (value1 == value2).Should().BeFalse(); + (value1 != value2).Should().BeTrue(); + } + + [Fact] + public void Equals_HandledScalarTypes_UseUnderlyingUlong() + { + var constant1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 10UL }; + var constant2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 10UL }; + var constant3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 11UL }; + + (constant1 == constant2).Should().BeTrue(); + (constant1 == constant3).Should().BeFalse(); + + var address1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x1000UL }; + var address2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x1000UL }; + var address3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x2000UL }; + + (address1 == address2).Should().BeTrue(); + (address1 == address3).Should().BeFalse(); + + var secOffset1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 5UL }; + var secOffset2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 5UL }; + var secOffset3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 6UL }; + + (secOffset1 == secOffset2).Should().BeTrue(); + (secOffset1 == secOffset3).Should().BeFalse(); + + var reference1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 123UL }; + var reference2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 123UL }; + var reference3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 124UL }; + + (reference1 == reference2).Should().BeTrue(); + (reference1 == reference3).Should().BeFalse(); + } + + [Fact] + public void Equals_HandledArrayAndStringTypes_UseSequenceAndStringComparison() + { + var block1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x02 } }; + var block2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x02 } }; + var block3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x03 } }; + + (block1 == block2).Should().BeTrue(); + (block1 == block3).Should().BeFalse(); + + var expr1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xAA } }; + var expr2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xAA } }; + var expr3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xBB } }; + + (expr1 == expr2).Should().BeTrue(); + (expr1 == expr3).Should().BeFalse(); + + var flag1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = true }; + var flag2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = true }; + var flag3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = false }; + + (flag1 == flag2).Should().BeTrue(); + (flag1 == flag3).Should().BeFalse(); + + var string1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "name" }; + var string2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = new string("name".ToCharArray()) }; + var string3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "other" }; + + (string1 == string2).Should().BeTrue(); + (string1 == string3).Should().BeFalse(); + } + + [Fact] + public void Equals_UnhandledTypes_IgnoreValueAndTreatSameTypeAsEqual() + { + var invalid1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Invalid, Value = 1UL }; + var invalid2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Invalid, Value = 2UL }; + + (invalid1 == invalid2).Should().BeTrue(); + + var resolved1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ResolvedReference, Value = new DwarfSymbol() }; + var resolved2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ResolvedReference, Value = new DwarfSymbol() }; + + (resolved1 == resolved2).Should().BeTrue(); + + var loclistx1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Loclistx, Value = 10UL }; + var loclistx2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Loclistx, Value = 20UL }; + + (loclistx1 == loclistx2).Should().BeTrue(); + } + + [Fact] + public void GetHashCode_MatchesForEqualValues_OnHandledType() + { + var v1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 123UL, + }; + + var v2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 123UL, + }; + + v1.Equals(v2).Should().BeTrue(); + v1.GetHashCode().Should().Be(v2.GetHashCode()); + } + + [Fact] + public void GetHashCode_MayDifferForEqualValues_OnUnhandledType() + { + var v1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Loclistx, + Value = 1UL, + }; + + var v2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Loclistx, + Value = 2UL, + }; + + // Current implementation considers these equal but uses Value in the hash code. + v1.Equals(v2).Should().BeTrue(); + v1.GetHashCode().Should().NotBe(v2.GetHashCode()); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs new file mode 100644 index 000000000..33e9015c1 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for and its interaction with + /// when parsing .debug_frame style data. + /// + public class DwarfCommonInformationEntryTests + { + [Fact] + public void ParseAll_WithNonEmptyAugmentation_UsesFixedDefaultsAndReadsInstructions() + { + // Layout for a single Common Information Entry (CIE): + // length (4 bytes) + // offset (4 bytes) -- unused for CIE + // second offset = -1 (4 bytes) -- sentinel indicating CIE + // version (1 byte) + // augmentation string "x" + null terminator (2 bytes) + // initial instructions (2 bytes) + + byte[] data = new byte[] + { + // length = 13 bytes following this field (4 + 4 + 1 + 2 + 2) + 0x0D, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (0xFFFFFFFF) marks this as CIE + 0xFF, 0xFF, 0xFF, 0xFF, + + // version + 0x03, + + // augmentation string "x" (ASCII 0x78) + null terminator + 0x78, 0x00, + + // initial instructions + 0xAA, 0xBB, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + DwarfCommonInformationEntry entry = entries[0]; + + entry.Version.Should().Be(3); + entry.Augmentation.Should().Be("x"); + + // Non-empty augmentation path uses fixed defaults. + entry.AddressSize.Should().Be(4); + entry.SegmentSelectorSize.Should().Be(0); + entry.CodeAlignmentFactor.Should().Be(0); + entry.DataAlignmentFactor.Should().Be(0); + entry.ReturnAddressRegister.Should().Be(0); + + entry.InitialInstructions.Should().Equal(0xAA, 0xBB); + entry.FrameDescriptionEntries.Should().BeEmpty(); + } + + [Fact] + public void ParseAll_WithEmptyAugmentationAndVersionLessThanFour_UsesDefaultAddressSize() + { + // Version < 4 and empty augmentation string should use the provided + // defaultAddressSize and read alignment factors and return address + // via ULEB128. + + // Body layout after the two offsets: + // version (1 byte) + // augmentation string "" (null terminator only, 1 byte) + // code alignment factor (ULEB128, 1 byte) + // data alignment factor (ULEB128, 1 byte) + // return address register (ULEB128, 1 byte) + // initial instructions (1 byte) + // Total body = 6 bytes, plus two 4-byte offsets = 14. + + byte[] data = new byte[] + { + // length = 14 bytes following this field + 0x0E, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 3 (< 4) + 0x03, + + // empty augmentation string (just null terminator) + 0x00, + + // CodeAlignmentFactor = 1, DataAlignmentFactor = 2, ReturnAddressRegister = 3 + 0x01, 0x02, 0x03, + + // initial instructions + 0xCC, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + DwarfCommonInformationEntry entry = entries[0]; + + entry.Version.Should().Be(3); + entry.Augmentation.Should().BeEmpty(); + + entry.AddressSize.Should().Be(8); // defaultAddressSize + entry.SegmentSelectorSize.Should().Be(0); + entry.CodeAlignmentFactor.Should().Be(1); + entry.DataAlignmentFactor.Should().Be(2); + entry.ReturnAddressRegister.Should().Be(3); + + entry.InitialInstructions.Should().Equal(0xCC); + } + + [Fact] + public void ParseAll_WithEmptyAugmentationAndVersionFour_ReadsExplicitAddressAndSegmentSizes() + { + // Version >= 4 with empty augmentation string should read AddressSize + // and SegmentSelectorSize explicitly from the stream before the + // alignment factors and return address register. + + // Body layout after the two offsets: + // version (1 byte) + // augmentation string "" (1 byte) + // address size (1 byte) + // segment selector size (1 byte) + // code alignment factor (ULEB128, 1 byte) + // data alignment factor (ULEB128, 1 byte) + // return address register (ULEB128, 1 byte) + // initial instructions (1 byte) + // Total body = 8 bytes, plus two 4-byte offsets = 16. + + byte[] data = new byte[] + { + // length = 16 bytes following this field + 0x10, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 4 + 0x04, + + // empty augmentation string + 0x00, + + // explicit address and segment selector sizes + 0x08, // AddressSize + 0x01, // SegmentSelectorSize + + // CodeAlignmentFactor = 2, DataAlignmentFactor = 3, ReturnAddressRegister = 4 + 0x02, 0x03, 0x04, + + // initial instructions + 0xDD, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 4); + + entries.Should().HaveCount(1); + DwarfCommonInformationEntry entry = entries[0]; + + entry.Version.Should().Be(4); + entry.Augmentation.Should().BeEmpty(); + + entry.AddressSize.Should().Be(8); + entry.SegmentSelectorSize.Should().Be(1); + entry.CodeAlignmentFactor.Should().Be(2); + entry.DataAlignmentFactor.Should().Be(3); + entry.ReturnAddressRegister.Should().Be(4); + + entry.InitialInstructions.Should().Equal(0xDD); + } + + [Fact] + public void ParseAll_WithFdeFollowingCie_AttachesFrameDescriptionEntryAndParsesAddresses() + { + // This test crafts a stream with one Common Information Entry (CIE) + // followed by a Frame Description Entry (FDE) that references it. + // It verifies that: + // - the FDE is associated with the CIE via FrameDescriptionEntries + // - InitialLocation, AddressRange, and Instructions are parsed + // according to the CIE's AddressSize and the record length. + + // First record: CIE with version 3, empty augmentation, defaultAddressSize = 8. + // Body after two offsets: + // version (1) + // augmentation "" (1) + // CodeAlignmentFactor (ULEB128 0) + // DataAlignmentFactor (ULEB128 0) + // ReturnAddressRegister (ULEB128 0) + // InitialInstructions (0 bytes) + // Body length = 5, plus 8 bytes of offsets = 13. + + byte[] cie = new byte[] + { + // length = 13 bytes following this field + 0x0D, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 3 (< 4) + 0x03, + + // empty augmentation + 0x00, + + // CodeAlignmentFactor = 0, DataAlignmentFactor = 0, ReturnAddressRegister = 0 + 0x00, 0x00, 0x00, + }; + + // Second record: FDE that references the CIE at offset 0. + // Body after two offsets: + // InitialLocation (8 bytes, LE) + // AddressRange (8 bytes, LE) + // Instructions (2 bytes) + // Body length = 18, plus 8 bytes of offsets = 26. + + ulong initialLocation = 0x1122334455667788UL; + ulong addressRange = 0x8877665544332211UL; + + byte[] fde = new byte[4 + 4 + 4 + 8 + 8 + 2]; + + int index = 0; + + // length = 26 + fde[index++] = 0x1A; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // first offset = 0 (reference to CIE start position) + fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // second offset != -1 so this is treated as FDE + fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // InitialLocation (8 bytes LE) + Array.Copy(BitConverter.GetBytes(initialLocation), 0, fde, index, 8); + index += 8; + + // AddressRange (8 bytes LE) + Array.Copy(BitConverter.GetBytes(addressRange), 0, fde, index, 8); + index += 8; + + // Instructions (2 bytes) + fde[index++] = 0xAA; + fde[index++] = 0xBB; + + // Combined stream: CIE followed by FDE. + byte[] data = new byte[cie.Length + fde.Length]; + Array.Copy(cie, 0, data, 0, cie.Length); + Array.Copy(fde, 0, data, cie.Length, fde.Length); + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + DwarfCommonInformationEntry entry = entries[0]; + + entry.AddressSize.Should().Be(8); + entry.FrameDescriptionEntries.Should().HaveCount(1); + + DwarfFrameDescriptionEntry description = entry.FrameDescriptionEntries[0]; + + description.CommonInformationEntry.Should().BeSameAs(entry); + description.InitialLocation.Should().Be(initialLocation); + description.AddressRange.Should().Be(addressRange); + description.Instructions.Should().Equal(0xAA, 0xBB); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index 13fa00470..1971d9a56 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -46,25 +46,7 @@ private class StubDwarfBinary : IDwarfBinary public void Dispose() { } } - /// - /// Helper to encode an unsigned LEB128 value into bytes. - /// - private static byte[] EncodeULEB128(ulong value) - { - var bytes = new List(); - do - { - byte b = (byte)(value & 0x7F); - value >>= 7; - if (value != 0) - { - b |= 0x80; - } - - bytes.Add(b); - } while (value != 0); - return bytes.ToArray(); - } + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); /// /// Builds a DWARF5 32-bit compilation unit header for .debug_info. @@ -168,11 +150,19 @@ private static byte[] BuildAbbrevMultipleAttributes(DwarfTag tag, params (DwarfA /// Parses a DWARF compilation unit from crafted binary data and returns the first symbol. /// private static DwarfSymbol ParseSingleSymbol(byte[] debugInfoData, byte[] debugAbbrevData, byte[] debugStrData = null) + { + return ParseSingleSymbolWithLineStrings(debugInfoData, debugAbbrevData, debugStrData, new byte[] { 0x00 }); + } + + /// + /// Parses a DWARF compilation unit with explicit .debug_str and .debug_line_str data. + /// + private static DwarfSymbol ParseSingleSymbolWithLineStrings(byte[] debugInfoData, byte[] debugAbbrevData, byte[] debugStrData, byte[] debugLineStrData) { using var debugData = new DwarfMemoryReader(debugInfoData); using var debugAbbrev = new DwarfMemoryReader(debugAbbrevData); using var debugStrings = new DwarfMemoryReader(debugStrData ?? new byte[] { 0x00 }); - using var debugLineStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + using var debugLineStrings = new DwarfMemoryReader(debugLineStrData ?? new byte[] { 0x00 }); var stub = new StubDwarfBinary(); var stringOffsets = new List(); @@ -408,6 +398,332 @@ public void ReadData_GNUAddrIndex_MultiByte_FollowedByData1() ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x99); } + // ---- DWARF4 classic forms: Data*, SData, UData, Address, Block*, String/Strp, Flag*, Ref* ---- + + [Fact] + public void ReadData_Dwarf4_DataForms_MapToConstant() + { + // Data1 = 0x11, Data2 = 0x2222, Data4 = 0x33333333, Data8 = 0x4444444444444444 + byte[] dieData = new byte[] + { + 0x01, // abbreviation code + 0x11, // Data1 + 0x22, 0x22, // Data2 + 0x33, 0x33, 0x33, 0x33, // Data4 + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, // Data8 + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Location, DwarfFormat.Data1), + (DwarfAttribute.ByteSize, DwarfFormat.Data2), + (DwarfAttribute.DeclFile, DwarfFormat.Data4), + (DwarfAttribute.DeclLine, DwarfFormat.Data8)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.Location].Value).Should().Be(0x11); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x2222); + + symbol.Attributes[DwarfAttribute.DeclFile].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.DeclFile].Value).Should().Be(0x33333333); + + symbol.Attributes[DwarfAttribute.DeclLine].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.DeclLine].Value).Should().Be(0x4444444444444444UL); + } + + [Fact] + public void ReadData_Dwarf4_SDataAndUData_MapToConstant() + { + // SData = -2, UData = 300 + byte[] sdata = EncodeULEB128(0x7E); // SLEB128(-2) payload read via SData + byte[] udata = EncodeULEB128(300); // ULEB128(300) + + byte[] dieData = new byte[] { 0x01 } + .Concat(sdata) // SData + .Concat(udata) // UData + .ToArray(); + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.LowPc, DwarfFormat.SData), + (DwarfAttribute.HighPc, DwarfFormat.UData)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Constant); + // Current implementation stores SData as a 32-bit signed value widened to ulong. + ((ulong)symbol.Attributes[DwarfAttribute.LowPc].Value).Should().Be(4294967294UL); + + symbol.Attributes[DwarfAttribute.HighPc].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.HighPc].Value).Should().Be(300UL); + } + + [Fact] + public void ReadData_Dwarf4_AddressAndRefForms_MapToAddressAndReference() + { + // Address: 8-byte value, Ref4: 4-byte offset from CU begin + byte[] dieData = new byte[] + { + 0x01, + // Address (8 bytes LE) + 0x78, 0x56, 0x34, 0x12, 0xEF, 0xCD, 0xAB, 0x90, + // Ref4 (4 bytes LE) = 0x20 from CU begin (we don't need a real target DIE for this test) + 0x20, 0x00, 0x00, 0x00, + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length, addressSize: 8); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.LowPc, DwarfFormat.Address), + (DwarfAttribute.Type, DwarfFormat.Ref4)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + ((ulong)symbol.Attributes[DwarfAttribute.LowPc].Value).Should().Be(0x90ABCDEF12345678UL); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Reference); + ((ulong)symbol.Attributes[DwarfAttribute.Type].Value).Should().BeGreaterThan(0UL); // offset from CU begin + } + + [Fact] + public void ReadData_Dwarf4_BlockAndStringForms_MapToBlockAndString() + { + // Block: length-prefixed via ULEB128; String: null-terminated inline; Strp: offset into .debug_str + byte[] blockPayload = new byte[] { 0xAA, 0xBB }; + byte[] blockLen = EncodeULEB128((ulong)blockPayload.Length); + byte[] inlineString = new byte[] { (byte)'H', (byte)'i', 0x00 }; + + byte[] dieData = new byte[] { 0x01 } + .Concat(blockLen) // Block length + .Concat(blockPayload) // Block bytes + .Concat(inlineString) // String + .Concat(new byte[] { 0x02, 0x00, 0x00, 0x00 }) // Strp: 4-byte offset 2 into debug_str + .ToArray(); + + // .debug_str: two bytes of padding, then "World\0" + byte[] debugStr = new byte[] { 0x00, 0x00, (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d', 0x00 }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Location, DwarfFormat.Block), + (DwarfAttribute.Name, DwarfFormat.String), + (DwarfAttribute.CompDir, DwarfFormat.Strp)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev, debugStr); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Block); + symbol.Attributes[DwarfAttribute.Location].Block.Should().Equal(0xAA, 0xBB); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("Hi"); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].String.Should().Be("World"); + } + + [Fact] + public void ReadData_Dwarf4_FlagForms_MapToFlag() + { + byte[] dieData = new byte[] + { + 0x01, + 0x01, // Flag = true + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.External, DwarfFormat.Flag), + (DwarfAttribute.Declaration, DwarfFormat.FlagPresent)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.External].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.External].Flag.Should().BeTrue(); + + symbol.Attributes[DwarfAttribute.Declaration].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.Declaration].Flag.Should().BeTrue(); + } + + // ---- DWARF5 newer forms: LineStrp, StrpSup, Strx/Strx1/2/4, GNUStrIndex, Addrx, RefSup*, RefSig8 ---- + + [Fact] + public void ReadData_LineStrp_ResolvesFromDebugLineStrings() + { + // .debug_line_str: two bytes padding, then "Line\0" + byte[] debugLineStr = new byte[] { 0x00, 0x00, (byte)'L', (byte)'i', (byte)'n', (byte)'e', 0x00 }; + + // DIE: abbrev code 1, then 32-bit offset 2 into .debug_line_str + byte[] dieData = new byte[] + { + 0x01, + 0x02, 0x00, 0x00, 0x00, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.Name, DwarfFormat.LineStrp); + + DwarfSymbol symbol = ParseSingleSymbolWithLineStrings(debugInfo, abbrev, new byte[] { 0x00 }, debugLineStr); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("Line"); + } + + [Fact] + public void ReadData_StrpSup_SetsOffsetAndLeavesValueNull() + { + byte[] dieData = new byte[] + { + 0x01, + 0x04, 0x03, 0x02, 0x01, // offset = 0x01020304 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.Name, DwarfFormat.StrpSup); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(0x01020304UL); + symbol.Attributes[DwarfAttribute.Name].Value.Should().BeNull(); + } + + [Fact] + public void ReadData_StrxAndGnuStrIndex_SetStringOffsets() + { + // ULEB128(5) then ULEB128(10) + byte[] dieData = new byte[] + { + 0x01, + 0x05, // Strx index + 0x0A, // GNUStrIndex index + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.Strx), + (DwarfAttribute.CompDir, DwarfFormat.GNUStrIndex)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(5UL); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(10UL); + } + + [Fact] + public void ReadData_StrxFixedWidthForms_SetExpectedOffsets() + { + byte[] dieData = new byte[] + { + 0x01, + 0x0A, // Strx1 = 10 + 0x22, 0x11, // Strx2 = 0x1122 + 0x33, 0x22, 0x11, // Strx3 = 0x112233 + 0x44, 0x33, 0x22, 0x11, // Strx4 = 0x11223344 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.Strx1), + (DwarfAttribute.CompDir, DwarfFormat.Strx2), + (DwarfAttribute.DeclFile, DwarfFormat.Strx3), + (DwarfAttribute.DeclLine, DwarfFormat.Strx4)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(10UL); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(0x1122UL); + symbol.Attributes[DwarfAttribute.DeclFile].Offset.Should().Be(0x112233UL); + symbol.Attributes[DwarfAttribute.DeclLine].Offset.Should().Be(0x11223344UL); + } + + [Fact] + public void ReadData_Addrx_MapsToAddress() + { + // ULEB128(7) index; beginPosition offset is added internally, but we only + // need to verify that the resulting attribute type is Address. + byte[] dieData = new byte[] + { + 0x01, + 0x07, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.LowPc, DwarfFormat.Addrx); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + } + + [Fact] + public void ReadData_RefSupForms_SetReferenceOffsets() + { + byte[] dieData = new byte[] + { + 0x01, + 0x10, 0x00, 0x00, 0x00, // RefSup4 = 16 + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // RefSup8 = 32 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Specification, DwarfFormat.RefSup4), + (DwarfAttribute.Type, DwarfFormat.RefSup8)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Specification].Type.Should().Be(DwarfAttributeValueType.Reference); + symbol.Attributes[DwarfAttribute.Specification].Offset.Should().Be(16UL); + symbol.Attributes[DwarfAttribute.Specification].Value.Should().BeNull(); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Reference); + symbol.Attributes[DwarfAttribute.Type].Offset.Should().Be(32UL); + symbol.Attributes[DwarfAttribute.Type].Value.Should().BeNull(); + } + + [Fact] + public void ReadData_RefSig8_SkipsEightBytesAndMarksInvalid() + { + byte[] dieData = new byte[] + { + 0x01, + // RefSig8 payload (8 bytes) + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + // Following Data1 attribute + 0x42, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Type, DwarfFormat.RefSig8), + (DwarfAttribute.ByteSize, DwarfFormat.Data1)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Invalid); + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x42UL); + } + // ---- DWARF5 header parsing for DW_UT_type ---- [Fact] @@ -488,6 +804,95 @@ public void ReadData_MultipleFormsInSequence_AllConsumeCorrectBytes() ((ulong)symbol.Attributes[DwarfAttribute.DeclFile].Value).Should().Be(0x12345678); } + [Fact] + public void EndToEnd_Dwarf4_VariableSymbol_ParsesRepresentativeAttributes() + { + byte[] nameBytes = new byte[] { (byte)'v', (byte)'a', (byte)'r', 0x00 }; + byte[] addressBytes = new byte[] { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }; // 0x1122334455667788 + + byte[] dieData = new byte[] { 0x01 } + .Concat(nameBytes) // Name: "var" + .Concat(addressBytes) // LowPc: DW_FORM_addr + .Concat(new byte[] { 0x10 })// ByteSize: DW_FORM_data1 = 16 + .Concat(new byte[] { 0x01 })// External: DW_FORM_flag = true + .ToArray(); + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.String), + (DwarfAttribute.LowPc, DwarfFormat.Address), + (DwarfAttribute.ByteSize, DwarfFormat.Data1), + (DwarfAttribute.External, DwarfFormat.Flag)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Tag.Should().Be(DwarfTag.Variable); + symbol.Name.Should().Be("var"); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("var"); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + symbol.Attributes[DwarfAttribute.LowPc].Address.Should().Be(0x1122334455667788UL); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x10UL); + + symbol.Attributes[DwarfAttribute.External].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.External].Flag.Should().BeTrue(); + } + + [Fact] + public void EndToEnd_Dwarf5_VariableSymbol_ParsesRepresentativeAttributes() + { + // .debug_line_str: two bytes padding, then "foo\0" + byte[] debugLineStr = new byte[] { 0x00, 0x00, (byte)'f', (byte)'o', (byte)'o', 0x00 }; + + byte[] dieData = new byte[] + { + 0x01, // abbreviation code + 0x02, 0x00, 0x00, 0x00, // Name: LineStrp offset = 2 + 0x03, // CompDir: Strx1 index = 3 + 0x05, // Location: Loclistx = ULEB128(5) + 0x07, // Ranges: Rnglistx = ULEB128(7) + 0x34, 0x12, // ByteSize: Data2 = 0x1234 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.LineStrp), + (DwarfAttribute.CompDir, DwarfFormat.Strx1), + (DwarfAttribute.Location, DwarfFormat.Loclistx), + (DwarfAttribute.Ranges, DwarfFormat.Rnglistx), + (DwarfAttribute.ByteSize, DwarfFormat.Data2)); + + DwarfSymbol symbol = ParseSingleSymbolWithLineStrings( + debugInfo, + abbrev, + new byte[] { 0x00 }, // .debug_str (unused in this test) + debugLineStr); + + symbol.Tag.Should().Be(DwarfTag.Variable); + symbol.Name.Should().Be("foo"); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("foo"); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(3UL); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Loclistx); + ((ulong)symbol.Attributes[DwarfAttribute.Location].Value).Should().Be(5UL); + + symbol.Attributes[DwarfAttribute.Ranges].Type.Should().Be(DwarfAttributeValueType.SecOffset); + ((ulong)symbol.Attributes[DwarfAttribute.Ranges].Value).Should().Be(7UL); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x1234UL); + } + // Abbreviation table terminator handling (DWARF5 spec 7.5.3) [Fact] @@ -700,5 +1105,74 @@ public void ReadData_BailsOutOnUnknownAbbrevCode_PreservingValidEntries() cu.SymbolsTree[0].Tag.Should().Be(DwarfTag.CompileUnit); ((ulong)cu.SymbolsTree[0].Attributes[DwarfAttribute.Name].Value).Should().Be(0xAA); } + + [Fact] + public void ReadData_InsertsVoidBaseTypeAndDefaultsPointerTypeToVoid() + { + // Single DIE using a PointerType abbreviation with only a ByteSize attribute. + // DwarfCompilationUnit should: + // - Insert a synthetic "void" base type as a child of the first root symbol. + // - Add a Type attribute to the PointerType symbol pointing to that void type. + + byte[] dieData = new byte[] + { + 0x01, // abbreviation code = 1 + 0x08, // ByteSize = DW_FORM_data1 = 8 + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute( + DwarfTag.PointerType, + DwarfAttribute.ByteSize, + DwarfFormat.Data1); + + using var debugData = new DwarfMemoryReader(debugInfo); + using var debugAbbrev = new DwarfMemoryReader(abbrev); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + using var debugLineStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var stub = new StubDwarfBinary(); + var cu = new DwarfCompilationUnit( + stub, + debugData, + debugAbbrev, + debugStrings, + debugLineStrings, + new List(), + stub.NormalizeAddress); + + cu.SymbolsTree.Should().HaveCount(1); + DwarfSymbol pointer = cu.SymbolsTree[0]; + pointer.Tag.Should().Be(DwarfTag.PointerType); + + // Synthetic void base type is inserted as first child. + pointer.Children.Should().NotBeNull(); + pointer.Children.Should().NotBeEmpty(); + DwarfSymbol voidSymbol = pointer.Children[0]; + voidSymbol.Tag.Should().Be(DwarfTag.BaseType); + + voidSymbol.Attributes.Should().ContainKey(DwarfAttribute.Name); + voidSymbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + voidSymbol.Attributes[DwarfAttribute.Name].String.Should().Be("void"); + + voidSymbol.Attributes.Should().ContainKey(DwarfAttribute.ByteSize); + voidSymbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)voidSymbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0UL); + + // Pointer keeps its explicit ByteSize attribute. + pointer.Attributes.Should().ContainKey(DwarfAttribute.ByteSize); + pointer.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)pointer.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(8UL); + + // And receives a Type attribute that resolves to the synthetic void symbol. + pointer.Attributes.Should().ContainKey(DwarfAttribute.Type); + DwarfAttributeValue typeAttr = pointer.Attributes[DwarfAttribute.Type]; + typeAttr.Type.Should().Be(DwarfAttributeValueType.ResolvedReference); + typeAttr.Reference.Should().BeSameAs(voidSymbol); + + // NextOffset should advance to the end of the single CU. + cu.NextOffset.Should().Be((uint)debugInfo.Length); + } } } diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs new file mode 100644 index 000000000..510f7adff --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfFrameDescriptionEntryTests + { + /// + /// Minimal test-only CIE that lets us control AddressSize. + /// + private sealed class TestCommonInformationEntry : DwarfCommonInformationEntry + { + public TestCommonInformationEntry(byte addressSize) + { + AddressSize = addressSize; + } + } + + [Fact] + public void Constructor_WithFourByteAddressSize_ReadsTwoUintsAndRemainingInstructions() + { + // Layout for AddressSize = 4: + // InitialLocation (4 bytes LE) + // AddressRange (4 bytes LE) + // Instructions (3 bytes) + byte[] data = new byte[] + { + 0x78, 0x56, 0x34, 0x12, // InitialLocation = 0x12345678 + 0xEF, 0xCD, 0xAB, 0x90, // AddressRange = 0x90ABCDEF + 0xAA, 0xBB, 0xCC, // Instructions + }; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 4); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.InitialLocation.Should().Be(0x12345678UL); + entry.AddressRange.Should().Be(0x90ABCDEFUL); + entry.Instructions.Should().Equal(0xAA, 0xBB, 0xCC); + entry.CommonInformationEntry.Should().BeSameAs(cie); + } + + [Fact] + public void Constructor_WithEightByteAddressSize_ReadsTwoUlongsAndRemainingInstructions() + { + // Layout for AddressSize = 8: + // InitialLocation (8 bytes LE) + // AddressRange (8 bytes LE) + // Instructions (2 bytes) + ulong initialLocation = 0x1122334455667788UL; + ulong addressRange = 0x8877665544332211UL; + + byte[] data = new byte[8 + 8 + 2]; + int index = 0; + + // InitialLocation + System.Array.Copy(System.BitConverter.GetBytes(initialLocation), 0, data, index, 8); + index += 8; + + // AddressRange + System.Array.Copy(System.BitConverter.GetBytes(addressRange), 0, data, index, 8); + index += 8; + + // Instructions + data[index++] = 0x01; + data[index++] = 0x02; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 8); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.InitialLocation.Should().Be(initialLocation); + entry.AddressRange.Should().Be(addressRange); + entry.Instructions.Should().Equal(0x01, 0x02); + entry.CommonInformationEntry.Should().BeSameAs(cie); + } + + [Fact] + public void Constructor_WhenEndPositionEqualsCurrentPosition_SetsEmptyInstructions() + { + // No instruction bytes after InitialLocation and AddressRange; the + // ReadBlock call should return an empty array. + byte[] data = new byte[] + { + 0x11, 0x22, 0x33, 0x44, // InitialLocation (4 bytes) + 0x55, 0x66, 0x77, 0x88, // AddressRange (4 bytes) + }; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 4); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.Instructions.Should().NotBeNull(); + entry.Instructions.Should().BeEmpty(); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs new file mode 100644 index 000000000..6fec6001d --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs @@ -0,0 +1,809 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfLineNumberProgramTests + { + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); + + [Fact] + public void Constructor_WithUnitLengthLessOrEqualToOne_ReturnsNullFiles() + { + // unit_length = 1 triggers the early-null path in ReadData. + byte[] debugLineData = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram( + dwarfVersion: 4, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr); + + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_Dwarf4_SingleFileSingleCopy_ProducesOneNormalizedLine() + { + // Build a minimal DWARF4 .debug_line section with: + // - one file entry + // - a single COPY opcode to emit one line record + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 4 + bw.Write((ushort)4); + + // header_length (ignored by implementation, but required field) + bw.Write(0); // 32-bit offset + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 (true) + bw.Write((byte)1); + + // line_base (sbyte) and line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths (1..12) + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + // All standard opcodes take 0 extra operands for this test + bw.Write((byte)0); + } + + // Directories: none — write terminator 0 + bw.Write((byte)0x00); + + // Files: one entry "file.c" with directory index 0, timestamp 0, length 0 + bw.Write(System.Text.Encoding.UTF8.GetBytes("file.c")); + bw.Write((byte)0x00); // null terminator for name + bw.Write((byte)0x00); // directory index (ULEB128 0) + bw.Write((byte)0x00); // lastModified (ULEB128 0) + bw.Write((byte)0x00); // length (ULEB128 0) + bw.Write((byte)0x00); // files terminator + + // Instructions: a single COPY opcode (standard opcode 1) + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) // extra padding byte so endPosition stays within bounds + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + // Normalize addresses by adding a fixed offset to prove post-processing ran. + uint normalizeDelta = 0x10; + var program = new DwarfLineNumberProgram( + dwarfVersion: 4, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr + normalizeDelta); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + DwarfFileInformation file = program.Files[0]; + file.Name.Should().Be("file.c"); + file.Lines.Should().HaveCount(1); + + DwarfLineInformation line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); // initial line + line.Column.Should().Be(0ul); // default column + line.Address.Should().Be(normalizeDelta); // 0 + normalizeDelta + } + + [Fact] + public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() + { + // Build DWARF5 .debug_line section exercising the DWARF5-specific + // directory and file tables using Strp and DirectoryIndex entries. + + // .debug_line/.debug_line_str (or shared debugStrings) data: + // offset 0: "dir\0" + // offset 4: "foo.c\0" + byte[] debugStringsData = + { + (byte)'d', (byte)'i', (byte)'r', 0x00, + (byte)'f', (byte)'o', (byte)'o', (byte)'.', (byte)'c', 0x00, + }; + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 5 + bw.Write((ushort)5); + + // address_size and segment_selector_size (ignored by tests) + bw.Write((byte)8); // address_size + bw.Write((byte)0); // segmentSelectorSize + + // header_length (offset; implementation does not use it for skipping) + bw.Write(0); // 32-bit + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 + bw.Write((byte)1); + + // line_base, line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + bw.Write((byte)0); + } + + // ---- DWARF5 directory table ---- + + // directoryEntryFormatCount = 1 + bw.Write((byte)1); + + // Entry 0: Path (Strp) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.Path)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Strp)); + + // directoriesCount = 1 + bw.Write(EncodeULEB128(1)); + + // Directory 0: path at offset 0 in debugStringsData + bw.Write(BitConverter.GetBytes(0)); + + // ---- DWARF5 file table ---- + + // fileEntryFormatCount = 2 + bw.Write((byte)2); + + // Entry 0: Path (Strp) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.Path)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Strp)); + + // Entry 1: DirectoryIndex (Data1) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.DirectoryIndex)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Data1)); + + // filesCount = 1 + bw.Write(EncodeULEB128(1)); + + // File 0: name at offset 4, directory index = 0 (implementation treats this as first directory) + bw.Write(BitConverter.GetBytes(4)); // Strp offset to "foo.c" + bw.Write((byte)0); // directory index (Data1) + + // ---- Opcodes ---- + + // Single COPY to emit one line record. + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) // extra padding byte so endPosition stays within bounds + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(debugStringsData); + + var program = new DwarfLineNumberProgram( + dwarfVersion: 5, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + DwarfFileInformation file = program.Files[0]; + file.Name.Should().Be("foo.c"); + file.Directory.Should().Be("dir"); + // Path combines directory and name; we only check that both segments appear. + file.Path.Should().Contain("foo.c"); + file.Path.Should().Contain("dir"); + + file.Lines.Should().HaveCount(1); + DwarfLineInformation line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); + line.Address.Should().Be(0u); + } + + #region Helpers + + private static byte[] EncodeSLEB128(int value) => DwarfTestHelpers.EncodeSLEB128(value); + + /// + /// Builds a minimal DWARF4 .debug_line section and returns the parsed program. + /// + private static DwarfLineNumberProgram BuildDwarf4Program( + Action writeOpcodes, + byte minimumInstructionLength = 1, + sbyte lineBase = 0, + byte lineRange = 1, + byte operationCodeBase = 13, + string[] fileNames = null, + string[] directories = null, + NormalizeAddressDelegate addressNormalizer = null, + int dwarfVersion = 4) + { + fileNames ??= new[] { "file.c" }; + addressNormalizer ??= addr => addr; + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)dwarfVersion); + + if (dwarfVersion > 4) + { + bw.Write((byte)8); // addressSize + bw.Write((byte)0); // segmentSelectorSize + } + + bw.Write(0); // header_length (32-bit) + bw.Write(minimumInstructionLength); + + if (dwarfVersion > 3) + { + bw.Write((byte)1); // maximumOperationsPerInstruction + } + + bw.Write((byte)1); // default_is_stmt + bw.Write(unchecked((byte)lineBase)); + bw.Write(lineRange); + bw.Write(operationCodeBase); + + for (int i = 1; i < operationCodeBase; i++) + { + bw.Write((byte)0); + } + + // Directories + if (directories != null) + { + foreach (string dir in directories) + { + bw.Write(System.Text.Encoding.UTF8.GetBytes(dir)); + bw.Write((byte)0x00); + } + } + bw.Write((byte)0x00); // directory terminator + + // Files + foreach (string fileName in fileNames) + { + bw.Write(System.Text.Encoding.UTF8.GetBytes(fileName)); + bw.Write((byte)0x00); // null terminator + bw.Write((byte)0x00); // directory index + bw.Write((byte)0x00); // timestamp + bw.Write((byte)0x00); // length + } + bw.Write((byte)0x00); // file terminator + + writeOpcodes(bw); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + return new DwarfLineNumberProgram( + dwarfVersion: dwarfVersion, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addressNormalizer); + } + + /// + /// Writes a SetAddress extended opcode. + /// + private static void WriteSetAddress(BinaryWriter bw, uint address) + { + bw.Write((byte)0x00); // extended marker + bw.Write(EncodeULEB128(5)); // length: 1 opcode + 4 address + bw.Write((byte)DwarfLineNumberExtendedOpcode.SetAddress); + bw.Write(address); + } + + /// + /// Writes an EndSequence extended opcode. + /// + private static void WriteEndSequence(BinaryWriter bw) + { + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(1)); + bw.Write((byte)DwarfLineNumberExtendedOpcode.EndSequence); + } + + #endregion + + #region Error / Early-Return Paths + + [Fact] + public void ReadData_WithVersionLessThanTwo_ReturnsNull() + { + // Manually build data with version=1 so ReadData returns null. + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)1); // version < 2 + bw.Write(0); // header_length + bw.Write((byte)1); // minInstrLen + bw.Write((byte)1); // maxOpsPerInstr + bw.Write((byte)1); // default_is_stmt + bw.Write((byte)0); // lineBase + bw.Write((byte)1); // lineRange + bw.Write((byte)13); // opcodeBase + for (int i = 1; i < 13; i++) + { + bw.Write((byte)0); + } + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_WithOperationCodeBaseZero_ReturnsNull() + { + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)4); + bw.Write(0); + bw.Write((byte)1); + bw.Write((byte)1); // maxOpsPerInstr + bw.Write((byte)1); + bw.Write((byte)0); + bw.Write((byte)1); + bw.Write((byte)0); // operationCodeBase = 0 + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_WithEndPositionBeyondBuffer_ReturnsNull() + { + // unitLength points past the end of data. + byte[] debugLineData = BitConverter.GetBytes((uint)9999) + .Concat(new byte[] { 0x04, 0x00 }) // version=4 stub + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + #endregion + + #region Standard Opcodes + + [Fact] + public void ReadData_AdvancePc_AdvancesAddressByOperandTimesMinInstructionLength() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvancePc); + bw.Write(EncodeULEB128(5)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, minimumInstructionLength: 4); + + program.Files.Should().HaveCount(1); + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Address.Should().Be(20u); + } + + [Fact] + public void ReadData_AdvanceLine_PositiveValue_IncrementsLine() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(10)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Line.Should().Be(11u); // initial 1 + 10 + } + + [Fact] + public void ReadData_AdvanceLine_NegativeAfterPositive_DecrementsLine() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(20)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(-5)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[0].Line.Should().Be(21u); + program.Files[0].Lines[1].Line.Should().Be(16u); + } + + [Fact] + public void ReadData_SetFile_SwitchesToSecondFile() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + // SetFile to file 2 (1-based index) + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: new[] { "first.c", "second.c" }); + + program.Files.Should().HaveCount(2); + program.Files[0].Lines.Should().BeEmpty(); + program.Files[1].Lines.Should().HaveCount(1); + program.Files[1].Name.Should().Be("second.c"); + } + + [Fact] + public void ReadData_SetColumn_SetsColumnOnEmittedLine() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.SetColumn); + bw.Write(EncodeULEB128(42)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Column.Should().Be(42ul); + } + + [Fact] + public void ReadData_ConstAddPc_AdvancesAddressByFormula() + { + // With opcodeBase=13, lineRange=14, minInstrLen=1: + // advance = (255 - 13) / 14 = 17 + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.ConstAddPc); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, lineRange: 14, operationCodeBase: 13); + + program.Files[0].Lines[0].Address.Should().Be(17u); + } + + [Fact] + public void ReadData_FixedAdvancePc_AddsUshortToAddress() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.FixedAdvancePc); + bw.Write((ushort)256); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Address.Should().Be(256u); + } + + #endregion + + #region Extended Opcodes + + [Fact] + public void ReadData_SetAddress_SetsAddressToValue() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x2000); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Address.Should().Be(0x2000u); + } + + [Fact] + public void ReadData_EndSequence_EmitsLineAndResetsState() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(5)); + WriteSetAddress(bw, 0x1000); + WriteEndSequence(bw); + // After reset: address=0, line=1 + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + // EndSequence emission + program.Files[0].Lines[0].Address.Should().Be(0x1000u); + program.Files[0].Lines[0].Line.Should().Be(6u); // 1 + 5 + // After reset + program.Files[0].Lines[1].Address.Should().Be(0u); + program.Files[0].Lines[1].Line.Should().Be(1u); + } + + [Fact] + public void ReadData_SetAddress_WhenZero_UsesLastAddressFromPreviousEndSequence() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x5000); + WriteEndSequence(bw); + // SetAddress(0) should fall back to lastAddress = 0x5000 + WriteSetAddress(bw, 0); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[1].Address.Should().Be(0x5000u); + } + + [Fact] + public void ReadData_DefineFile_AddsAndSwitchesToNewFile() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + // Extended: DefineFile + bw.Write((byte)0x00); // extended marker + byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes("new.c"); + // length = 1 (opcode) + name.Length + 1 (null) + 3 (ULEB128 zeros) + int extLen = 1 + nameBytes.Length + 1 + 3; + bw.Write(EncodeULEB128((ulong)extLen)); + bw.Write((byte)DwarfLineNumberExtendedOpcode.DefineFile); + bw.Write(nameBytes); + bw.Write((byte)0x00); // null terminator + bw.Write((byte)0x00); // directory index + bw.Write((byte)0x00); // timestamp + bw.Write((byte)0x00); // length + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files.Should().HaveCount(2); + program.Files[1].Name.Should().Be("new.c"); + program.Files[1].Lines.Should().HaveCount(1); + } + + #endregion + + #region Special Opcodes + + [Fact] + public void ReadData_SpecialOpcode_AdvancesAddressAndLine() + { + // lineBase=-3, lineRange=12, opcodeBase=13, minInstrLen=1 + // Opcode 20: adjusted = 20 - 13 = 7 + // operationAdvance = 7 / 12 = 0 + // lineAdvance = -3 + (7 % 12) = -3 + 7 = 4 + // Result: address=0, line=1+4=5 + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)20); // special opcode + }, lineBase: -3, lineRange: 12); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Address.Should().Be(0u); + program.Files[0].Lines[0].Line.Should().Be(5u); + } + + [Fact] + public void ReadData_SpecialOpcode_WithAddressAdvance() + { + // lineBase=0, lineRange=4, opcodeBase=13, minInstrLen=2 + // Opcode 21: adjusted = 21 - 13 = 8 + // operationAdvance = 8 / 4 = 2 + // lineAdvance = 0 + (8 % 4) = 0 + // address = 2 * 2 = 4, line = 1 + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)21); + }, lineBase: 0, lineRange: 4, minimumInstructionLength: 2); + + program.Files[0].Lines[0].Address.Should().Be(4u); + program.Files[0].Lines[0].Line.Should().Be(1u); + } + + [Fact] + public void ReadData_MultipleSpecialOpcodes_AccumulateState() + { + // lineBase=0, lineRange=4, opcodeBase=13, minInstrLen=1 + // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=1, line=1. + // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=2, line=1. + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)17); + bw.Write((byte)17); + }, lineBase: 0, lineRange: 4); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[0].Address.Should().Be(1u); + program.Files[0].Lines[1].Address.Should().Be(2u); + } + + #endregion + + #region Multiple Files, Directories, Normalization + + [Fact] + public void ReadData_Dwarf4_MultipleDirectories_ResolvesFilePaths() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: null, directories: new[] { "src", "lib" }); + + // Override: need custom file entries with directory indices. + // Since BuildDwarf4Program doesn't support dir indices on files, + // build manually. + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)4); + bw.Write(0); + bw.Write((byte)1); + bw.Write((byte)1); + bw.Write((byte)1); + bw.Write((byte)0); + bw.Write((byte)1); + bw.Write((byte)13); + for (int i = 1; i < 13; i++) + { + bw.Write((byte)0); + } + + // Directories + bw.Write(System.Text.Encoding.UTF8.GetBytes("src")); + bw.Write((byte)0x00); + bw.Write(System.Text.Encoding.UTF8.GetBytes("lib")); + bw.Write((byte)0x00); + bw.Write((byte)0x00); // terminator + + // File in dir 1 (src) + bw.Write(System.Text.Encoding.UTF8.GetBytes("main.c")); + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(1)); // directory index 1 = "src" + bw.Write((byte)0x00); + bw.Write((byte)0x00); + + // File in dir 2 (lib) + bw.Write(System.Text.Encoding.UTF8.GetBytes("util.c")); + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(2)); // directory index 2 = "lib" + bw.Write((byte)0x00); + bw.Write((byte)0x00); + + bw.Write((byte)0x00); // file terminator + + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var prog = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + + prog.Files.Should().HaveCount(2); + prog.Files[0].Path.Should().Contain("src"); + prog.Files[0].Path.Should().Contain("main.c"); + prog.Files[1].Path.Should().Contain("lib"); + prog.Files[1].Path.Should().Contain("util.c"); + } + + [Fact] + public void ReadData_Dwarf4_MultipleFiles_LinesAssignedToCorrectFile() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); // line on file 1 + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); // switch to file 2 + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); // line on file 2 + }, fileNames: new[] { "a.c", "b.c" }); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[1].Lines.Should().HaveCount(1); + } + + [Fact] + public void ReadData_NormalizationAppliedToAllLinesAcrossFiles() + { + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x100); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); + WriteSetAddress(bw, 0x200); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: new[] { "a.c", "b.c" }, + addressNormalizer: addr => addr + 0x1000); + + program.Files[0].Lines[0].Address.Should().Be(0x1100u); + program.Files[1].Lines[0].Address.Should().Be(0x1200u); + } + + [Fact] + public void ReadData_Dwarf3_ParsesWithoutMaxOperationsPerInstruction() + { + // DWARF3: no maxOperationsPerInstruction field in header. + DwarfLineNumberProgram program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, dwarfVersion: 3); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + program.Files[0].Lines.Should().HaveCount(1); + } + + #endregion + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs new file mode 100644 index 000000000..cbdaafa5e --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers; +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfSymbolProviderTests + { + /// + /// Minimal IDwarfBinary stub needed by ParseLineNumberPrograms. + /// + private class StubDwarfBinary : IDwarfBinary + { + public int DwarfVersion { get; set; } + public DwarfUnitType DwarfUnitType { get; set; } + public byte[] DebugData { get; } = Array.Empty(); + public byte[] DebugDataDescription { get; } = Array.Empty(); + public byte[] DebugDataStrings { get; } = Array.Empty(); + public byte[] DebugLine { get; } = Array.Empty(); + public byte[] DebugFrame { get; } = Array.Empty(); + public byte[] EhFrame { get; } = Array.Empty(); + public ulong CodeSegmentOffset { get; } = 0; + public ulong EhFrameAddress { get; } = 0; + public ulong TextSectionAddress { get; } = 0; + public ulong DataSectionAddress { get; } = 0; + public IReadOnlyList PublicSymbols { get; } = Array.Empty(); + public bool Is64bit { get; } = false; + public ICompiler[] Compilers { get; } = Array.Empty(); + public List CommandLineInfos { get; } = new List(); + public ulong NormalizeAddress(ulong address) => address; + public DwarfLanguage GetLanguage() => DwarfLanguage.C; + public void Dispose() { } + } + + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); + + // ---- ParseDebugStringOffsets ---- + + [Fact] + public void ParseDebugStringOffsets_Reads32BitOffsets() + { + byte[] data = BitConverter.GetBytes(1u) + .Concat(BitConverter.GetBytes(2u)) + .ToArray(); + + List offsets = DwarfSymbolProvider.ParseDebugStringOffsets(data, is64bit: false); + + offsets.Should().Equal(1, 2); + } + + [Fact] + public void ParseDebugStringOffsets_Reads64BitOffsets() + { + byte[] data = BitConverter.GetBytes(1ul) + .Concat(BitConverter.GetBytes(2ul)) + .ToArray(); + + List offsets = DwarfSymbolProvider.ParseDebugStringOffsets(data, is64bit: true); + + offsets.Should().Equal(1, 2); + } + + // ---- ParseLineNumberPrograms ---- + + [Fact] + public void ParseLineNumberPrograms_WithEmptyDebugLine_ReturnsEmptyList() + { + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: Array.Empty(), + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr); + + programs.Should().NotBeNull(); + programs.Should().BeEmpty(); + } + + [Fact] + public void ParseLineNumberPrograms_StopsWhenProgramFilesIsNull() + { + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + // unit_length = 1 triggers the early-null path in DwarfLineNumberProgram.ReadData. + byte[] debugLineData = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: debugLineData, + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr); + + programs.Should().NotBeNull(); + programs.Should().BeEmpty(); + } + + [Fact] + public void ParseLineNumberPrograms_Dwarf4SingleProgram_ProducesOneNormalizedLine() + { + // Build a minimal DWARF4 .debug_line section with one file and a single COPY opcode, + // mirroring DwarfLineNumberProgramTests but exercising the aggregator in DwarfSymbolProvider. + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 4 + bw.Write((ushort)4); + + // header_length (ignored by implementation, but required field) + bw.Write(0); // 32-bit offset + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 (true) + bw.Write((byte)1); + + // line_base (sbyte) and line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths (1..12) + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + // All standard opcodes take 0 extra operands for this test + bw.Write((byte)0); + } + + // Directories: none — write terminator 0 + bw.Write((byte)0x00); + + // Files: one entry "file.c" with directory index 0, timestamp 0, length 0 + bw.Write(System.Text.Encoding.UTF8.GetBytes("file.c")); + bw.Write((byte)0x00); // null terminator for name + bw.Write((byte)0x00); // directory index (ULEB128 0) + bw.Write((byte)0x00); // lastModified (ULEB128 0) + bw.Write((byte)0x00); // length (ULEB128 0) + bw.Write((byte)0x00); // files terminator + + // Instructions: a single COPY opcode (standard opcode 1) + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + // Append 4 padding bytes so that the aggregator's loop terminates cleanly + // after the valid program. + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00, 0x00, 0x00, 0x00 }) + .ToArray(); + + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + uint normalizeDelta = 0x10; + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: debugLineData, + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr + normalizeDelta); + + programs.Should().HaveCount(1); + + DwarfLineNumberProgram program = programs[0]; + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + DwarfFileInformation file = program.Files[0]; + file.Name.Should().Be("file.c"); + file.Lines.Should().HaveCount(1); + + DwarfLineInformation line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); + line.Column.Should().Be(0ul); + line.Address.Should().Be(normalizeDelta); + } + + // ---- ParseAllCommandLineInfos ---- + + [Fact] + public void ParseAllCommandLineInfos_CompileUnitWithSupportedLanguage_ProducesInfo() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "/src/main.c" } }, + { DwarfAttribute.CompDir, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "/src" } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gcc -o main main.c" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.CPlusPlus11 } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.Type.Should().Be(DwarfTag.CompileUnit); + info.FullName.Should().Be("/src/main.c"); + info.CompileDirectory.Should().Be("/src"); + info.FileName.Should().Be("main.c"); + info.CommandLine.Should().Be("gcc -o main main.c"); + info.Language.Should().Be(DwarfLanguage.CPlusPlus11); + } + + [Fact] + public void ParseAllCommandLineInfos_CompileUnitWithUnsupportedLanguage_IsSkipped() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "mod.f" } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gfortran mod.f" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.Fortran77 } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().BeEmpty(); + } + + [Fact] + public void ParseAllCommandLineInfos_SubprogramUsesLinkageName() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "foo" } }, + { DwarfAttribute.LinkageName, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "_Z3foov" } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.Type.Should().Be(DwarfTag.Subprogram); + info.FullName.Should().Be("foo"); + info.FileName.Should().Be("foo"); + info.CommandLine.Should().Be("_Z3foov"); + info.Language.Should().Be(DwarfLanguage.Unknown); + } + + [Fact] + public void ParseAllCommandLineInfos_StripsNumericAndLongUnsignedIntNames() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "1234" } }, + { DwarfAttribute.CompDir, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = ElfUtility.LongUnsignedInt } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gcc 1234" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.C } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.FullName.Should().BeEmpty(); + info.CompileDirectory.Should().BeEmpty(); + info.CommandLine.Should().Be("gcc 1234"); + } + + private static DwarfCompilationUnit CreateCompilationUnitWithSymbols(params DwarfSymbol[] symbols) + { + // Create a real DwarfCompilationUnit instance with a minimal, version-0 header + // so that ReadData bails out early, then inject our own symbol table via reflection. + var dwarfBinary = new StubDwarfBinary(); + + // 4-byte unit_length (0) + 2-byte version (0) is enough for ReadData + // to hit the "version == 0" early-return path without reading further. + using var debugData = new DwarfMemoryReader(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + using var debugDataDescription = new DwarfMemoryReader(Array.Empty()); + using var debugStrings = new DwarfMemoryReader(Array.Empty()); + using var debugLineStrings = new DwarfMemoryReader(Array.Empty()); + IList debugStringOffsets = Array.Empty(); + + var cu = new DwarfCompilationUnit( + dwarfBinary, + debugData, + debugDataDescription, + debugStrings, + debugLineStrings, + debugStringOffsets, + address => address); + + FieldInfo field = typeof(DwarfCompilationUnit).GetField("symbolsByOffset", BindingFlags.Instance | BindingFlags.NonPublic); + field.Should().NotBeNull("DwarfCompilationUnit must have a symbolsByOffset field for tests to seed symbols"); + + var map = new Dictionary(); + uint offset = 0; + foreach (DwarfSymbol symbol in symbols) + { + map[offset++] = symbol; + } + + field.SetValue(cu, map); + + return cu; + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs new file mode 100644 index 000000000..24d6d4908 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfSymbolTests + { + [Fact] + public void Name_ReturnsNull_WhenNameAttributeMissing() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary() + }; + + symbol.Name.Should().BeNull(); + } + + [Fact] + public void Name_ReturnsStringAttribute_WhenPresent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "foo" + } + } + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = attributes + }; + + symbol.Name.Should().Be("foo"); + } + + [Fact] + public void FullName_ReturnsSimpleName_WhenNoParentOrCompileUnitParent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Top" + } + } + }; + + var compileUnit = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = new Dictionary() + }; + + var child = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = attributes, + Parent = compileUnit + }; + + child.FullName.Should().Be("Top"); + } + + [Fact] + public void FullName_BuildsQualifiedName_ThroughNonCompileUnitParents() + { + // Build hierarchy: ns::Class::Method + var ns = new DwarfSymbol + { + Tag = DwarfTag.Namespace, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "ns" + } + } + } + }; + + var cls = new DwarfSymbol + { + Tag = DwarfTag.ClassType, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Class" + } + } + }, + Parent = ns + }; + + var method = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Method" + } + } + }, + Parent = cls + }; + + method.FullName.Should().Be("ns::Class::Method"); + } + + [Fact] + public void GetConstantAttribute_ReturnsDefault_WhenAttributeMissing() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary() + }; + + symbol.GetConstantAttribute(DwarfAttribute.ByteSize, defaultValue: 42).Should().Be(42UL); + } + + [Fact] + public void GetConstantAttribute_ReturnsUnderlyingConstant_WhenPresent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.ByteSize, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 8UL + } + } + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = attributes + }; + + symbol.GetConstantAttribute(DwarfAttribute.ByteSize, defaultValue: 0).Should().Be(8UL); + } + + [Fact] + public void ToString_IncludesTagOffsetAttributeAndChildCounts() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary(), + Children = new List(), + }; + + // Offset is internal but defaults to 0. + string text = symbol.ToString(); + + text.Should().Contain("Variable"); + text.Should().Contain("Offset = 0"); + text.Should().Contain("Attributes = 0"); + text.Should().Contain("Children = 0"); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs new file mode 100644 index 000000000..2f034e13a --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Shared encoding helpers for DWARF unit tests. + /// + internal static class DwarfTestHelpers + { + /// + /// Encodes an unsigned LEB128 value into bytes. + /// + internal static byte[] EncodeULEB128(ulong value) + { + var bytes = new List(); + do + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if (value != 0) + { + b |= 0x80; + } + + bytes.Add(b); + } while (value != 0); + return bytes.ToArray(); + } + + /// + /// Encodes a signed LEB128 value into bytes. + /// + internal static byte[] EncodeSLEB128(int value) + { + var bytes = new List(); + bool more = true; + while (more) + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if ((value == 0 && (b & 0x40) == 0) || (value == -1 && (b & 0x40) != 0)) + { + more = false; + } + else + { + b |= 0x80; + } + + bytes.Add(b); + } + return bytes.ToArray(); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs b/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs index 59d3e3fe9..85b4ad980 100644 --- a/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Reflection; +using ELFSharp.ELF.Segments; + using FluentAssertions; using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; @@ -294,5 +296,261 @@ public void ValidateDwarfV4_WithO2_Split_DebugFileInAnotherDirectory() binaryNotFound.DebugFileType.Should().Be(DebugFileType.FromDwo); binaryNotFound.GetLanguage().Should().Be(DwarfLanguage.Unknown); } + + #region CanLoadBinary + + [Fact] + public void CanLoadBinary_ValidElfFile_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeTrue(); + } + + [Fact] + public void CanLoadBinary_PeFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "PE/DeterministicBuild/clangcl.14.pe.c.codeview.pdbpagesize_16384.exe"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + [Fact] + public void CanLoadBinary_NonExistentFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "NonExistent/does_not_exist.bin"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + [Fact] + public void CanLoadBinary_ZeroByteFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "Invalid/ZeroByte/WORKSPACE"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + #endregion + + #region Error Handling + + [Fact] + public void Constructor_InvalidElfFile_SetsValidFalse() + { + // A zero-byte file cannot be a valid ELF + string fileName = Path.Combine(TestData, "Invalid/ZeroByte/WORKSPACE"); + var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeFalse(); + binary.LoadException.Should().NotBeNull(); + } + + [Fact] + public void Constructor_PeFile_SetsValidFalse() + { + string fileName = Path.Combine(TestData, "PE/DeterministicBuild/clangcl.14.pe.c.codeview.pdbpagesize_16384.exe"); + var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeFalse(); + binary.LoadException.Should().NotBeNull(); + } + + #endregion + + #region GetSegmentFlags + + [Fact] + public void GetSegmentFlags_GnuStack_ReturnsFlags() + { + // A full binary with debug should have a GNU_STACK segment + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + SegmentFlags? flags = binary.GetSegmentFlags(ElfSegmentType.PT_GNU_STACK); + flags.Should().NotBeNull(); + } + + [Fact] + public void GetSegmentFlags_NonExistentSegment_ReturnsNull() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // PT_SHLIB is reserved and not used in practice + SegmentFlags? flags = binary.GetSegmentFlags(ElfSegmentType.PT_SHLIB); + flags.Should().BeNull(); + } + + #endregion + + #region GetSymbolTableFiles + + [Fact] + public void GetSymbolTableFiles_BinaryWithSymbols_ReturnsEntries() + { + // The full debug-included binary should have a symbol table with file entries + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/BinaryDirectory/gcc.objcopy.stripall.addgnudebuglink.full"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + List> files = binary.GetSymbolTableFiles(); + files.Should().NotBeNull(); + files.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void GetSymbolTableFiles_StrippedBinary_ReturnsEmptyList() + { + // A stripped binary has no symbol table + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/BinaryDirectory/gcc.objcopy.stripall.addgnudebuglink.nolink"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + List> files = binary.GetSymbolTableFiles(); + files.Should().NotBeNull(); + files.Should().BeEmpty(); + } + + #endregion + + #region NormalizeAddress + + [Fact] + public void NormalizeAddress_AddressInAllocatableSection_NormalizesCorrectly() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // The text section address should be valid + ulong textAddr = binary.TextSectionAddress; + textAddr.Should().NotBe(ulong.MaxValue); + + // NormalizeAddress should not throw and should return a value + ulong normalized = binary.NormalizeAddress(binary.ELF.Sections + .First(s => s.Name == ".text").LoadAddress); + normalized.Should().BeGreaterThan(0UL); + } + + [Fact] + public void NormalizeAddress_ZeroAddress_ReturnsOffsetFromCodeSegment() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // Address 0 is not in any section, so falls through to default path + ulong normalized = binary.NormalizeAddress(0); + // Should subtract CodeSegmentOffset (which wraps around for 0) + normalized.Should().Be(0UL - binary.CodeSegmentOffset); + } + + #endregion + + #region Properties + + [Fact] + public void PublicSymbols_ValidBinary_ContainsSymbols() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.PublicSymbols.Should().NotBeNull(); + binary.PublicSymbols.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void Is64bit_64BitBinary_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.Is64bit.Should().BeTrue(); + } + + [Fact] + public void TextSectionAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.TextSectionAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void DataSectionAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.DataSectionAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void EhFrameAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.EhFrameAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void CodeSegmentOffset_ValidBinary_IsAccessible() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + // CodeSegmentOffset is derived from ProgramHeader segment; value depends on binary layout + // Just verify it's accessible without throwing + binary.CodeSegmentOffset.Should().BeGreaterThanOrEqualTo(0UL); + } + + [Fact] + public void IsDebugOnlyFile_FullBinary_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.IsDebugOnlyFile.Should().BeFalse(); + } + + [Fact] + public void IsDebugOnlyFile_DebugOnlyFile_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/AnotherLocalSymbolDirectory/gcc.objcopy.stripall.addgnudebuglink.dbg"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.IsDebugOnlyFile.Should().BeTrue(); + } + + #endregion + + #region ForceComprehensiveParsing + + [Fact] + public void Constructor_ForceComprehensiveParsing_LoadsAllLazyProperties() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName), forceComprehensiveParsing: true); + binary.Valid.Should().BeTrue(); + + // When forceComprehensiveParsing is true, all lazy properties should already be evaluated + binary.CompilationUnits.IsValueCreated.Should().BeTrue(); + binary.LineNumberPrograms.IsValueCreated.Should().BeTrue(); + binary.CommonInformationEntries.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public void Constructor_WithoutForceComprehensiveParsing_LazyPropertiesNotLoaded() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName), forceComprehensiveParsing: false); + binary.Valid.Should().BeTrue(); + + // Without forcing, lazy properties should not be evaluated yet + binary.CompilationUnits.IsValueCreated.Should().BeFalse(); + binary.LineNumberPrograms.IsValueCreated.Should().BeFalse(); + binary.CommonInformationEntries.IsValueCreated.Should().BeFalse(); + } + + #endregion } }