using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
///
/// Unit tests for PeReader full PE parsing including CodeView GUID, Rich header, and version resources.
///
public class PeReaderTests : NativeTestBase
{
#region Basic Parsing
[Fact]
public void TryExtractIdentity_InvalidData_ReturnsFalse()
{
// Arrange
var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 };
// Act
var result = PeReader.TryExtractIdentity(invalidData, out var identity);
// Assert
result.Should().BeFalse();
identity.Should().BeNull();
}
[Fact]
public void TryExtractIdentity_TooShort_ReturnsFalse()
{
// Arrange
var shortData = new byte[0x20];
// Act
var result = PeReader.TryExtractIdentity(shortData, out var identity);
// Assert
result.Should().BeFalse();
}
[Fact]
public void TryExtractIdentity_MissingMzSignature_ReturnsFalse()
{
// Arrange
var data = new byte[0x100];
data[0] = (byte)'X';
data[1] = (byte)'Y';
// Act
var result = PeReader.TryExtractIdentity(data, out var identity);
// Assert
result.Should().BeFalse();
}
[Fact]
public void TryExtractIdentity_ValidMinimalPe64_ReturnsTrue()
{
// Arrange
var pe = PeBuilder.Console64().Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.Is64Bit.Should().BeTrue();
identity.Machine.Should().Be("x86_64");
identity.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
}
[Fact]
public void TryExtractIdentity_ValidMinimalPe32_ReturnsTrue()
{
// Arrange
var pe = new PeBuilder()
.Is64Bit(false)
.WithSubsystem(PeSubsystem.WindowsConsole)
.Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.Is64Bit.Should().BeFalse();
identity.Machine.Should().Be("x86");
}
[Fact]
public void TryExtractIdentity_GuiSubsystem_ParsesCorrectly()
{
// Arrange
var pe = new PeBuilder()
.Is64Bit(true)
.WithSubsystem(PeSubsystem.WindowsGui)
.Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.Subsystem.Should().Be(PeSubsystem.WindowsGui);
}
#endregion
#region Parse Method
[Fact]
public void Parse_ValidPeStream_ReturnsPeParseResult()
{
// Arrange
var pe = PeBuilder.Console64().Build();
using var stream = new MemoryStream(pe);
// Act
var result = PeReader.Parse(stream, "test.exe");
// Assert
result.Should().NotBeNull();
result!.Identity.Should().NotBeNull();
result.Identity.Is64Bit.Should().BeTrue();
}
[Fact]
public void Parse_InvalidStream_ReturnsNull()
{
// Arrange
var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 };
using var stream = new MemoryStream(invalidData);
// Act
var result = PeReader.Parse(stream, "invalid.exe");
// Assert
result.Should().BeNull();
}
[Fact]
public void Parse_ThrowsOnNullStream()
{
// Act & Assert
var action = () => PeReader.Parse(null!, "test.exe");
action.Should().Throw();
}
#endregion
#region Machine Architecture
[Theory]
[InlineData(PeMachine.I386, "x86", false)]
[InlineData(PeMachine.Amd64, "x86_64", true)]
[InlineData(PeMachine.Arm64, "arm64", true)]
public void TryExtractIdentity_MachineTypes_MapCorrectly(PeMachine machine, string expectedArch, bool is64Bit)
{
// Arrange
var pe = new PeBuilder()
.Is64Bit(is64Bit)
.WithMachine(machine)
.Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.Machine.Should().Be(expectedArch);
}
#endregion
#region Exports
[Fact]
public void TryExtractIdentity_NoExports_ReturnsEmptyList()
{
// Arrange - standard console app has no exports
var pe = PeBuilder.Console64().Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.Exports.Should().BeEmpty();
}
#endregion
#region Compiler Hints (Rich Header)
[Fact]
public void TryExtractIdentity_NoRichHeader_ReturnsEmptyHints()
{
// Arrange - builder-generated PEs don't have rich header
var pe = PeBuilder.Console64().Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.CompilerHints.Should().BeEmpty();
identity.RichHeaderHash.Should().BeNull();
}
#endregion
#region CodeView Debug Info
[Fact]
public void TryExtractIdentity_NoDebugDirectory_ReturnsNullCodeView()
{
// Arrange - builder-generated PEs don't have debug directory
var pe = PeBuilder.Console64().Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.CodeViewGuid.Should().BeNull();
identity.CodeViewAge.Should().BeNull();
identity.PdbPath.Should().BeNull();
}
#endregion
#region Version Resources
[Fact]
public void TryExtractIdentity_NoVersionResource_ReturnsNullVersions()
{
// Arrange - builder-generated PEs don't have version resources
var pe = PeBuilder.Console64().Build();
// Act
var result = PeReader.TryExtractIdentity(pe, out var identity);
// Assert
result.Should().BeTrue();
identity.Should().NotBeNull();
identity!.ProductVersion.Should().BeNull();
identity.FileVersion.Should().BeNull();
identity.CompanyName.Should().BeNull();
identity.ProductName.Should().BeNull();
identity.OriginalFilename.Should().BeNull();
}
#endregion
#region Determinism
[Fact]
public void TryExtractIdentity_SameInput_ReturnsSameOutput()
{
// Arrange
var pe = PeBuilder.Console64().Build();
// Act
PeReader.TryExtractIdentity(pe, out var identity1);
PeReader.TryExtractIdentity(pe, out var identity2);
// Assert
identity1.Should().BeEquivalentTo(identity2);
}
[Fact]
public void TryExtractIdentity_DifferentInputs_ReturnsDifferentOutput()
{
// Arrange
var pe64 = PeBuilder.Console64().Build();
var pe32 = new PeBuilder().Is64Bit(false).Build();
// Act
PeReader.TryExtractIdentity(pe64, out var identity64);
PeReader.TryExtractIdentity(pe32, out var identity32);
// Assert
identity64!.Is64Bit.Should().NotBe(identity32!.Is64Bit);
}
#endregion
#region Edge Cases
[Fact]
public void TryExtractIdentity_InvalidPeOffset_ReturnsFalse()
{
// Arrange - Create data with MZ signature but invalid PE offset
var data = new byte[0x100];
data[0] = (byte)'M';
data[1] = (byte)'Z';
// Set PE offset beyond file bounds
data[0x3C] = 0xFF;
data[0x3D] = 0xFF;
data[0x3E] = 0x00;
data[0x3F] = 0x00;
// Act
var result = PeReader.TryExtractIdentity(data, out var identity);
// Assert
result.Should().BeFalse();
}
[Fact]
public void TryExtractIdentity_MissingPeSignature_ReturnsFalse()
{
// Arrange - Create data with MZ but missing PE signature
var data = new byte[0x100];
data[0] = (byte)'M';
data[1] = (byte)'Z';
data[0x3C] = 0x80; // PE offset at 0x80
// No PE signature at offset 0x80
// Act
var result = PeReader.TryExtractIdentity(data, out var identity);
// Assert
result.Should().BeFalse();
}
[Fact]
public void TryExtractIdentity_InvalidMagic_ReturnsFalse()
{
// Arrange - Create data with PE signature but invalid magic
var data = new byte[0x200];
data[0] = (byte)'M';
data[1] = (byte)'Z';
data[0x3C] = 0x80; // PE offset at 0x80
// PE signature
data[0x80] = (byte)'P';
data[0x81] = (byte)'E';
data[0x82] = 0;
data[0x83] = 0;
// Invalid COFF header with size 0
data[0x80 + 16] = 0; // SizeOfOptionalHeader = 0
// Act
var result = PeReader.TryExtractIdentity(data, out var identity);
// Assert
result.Should().BeFalse();
}
#endregion
}