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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions tests/VbaCompiler.Tests/Streams/VbaProjectRoundTripTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2026 Cisco Systems, Inc.
// Licensed under MIT-style license (see LICENSE.txt file).

using System;
using System.IO;
using System.Linq;
using System.Text;
using Kavod.Vba.Compression;
using OpenMcdf;
using VbadDecompiler = vbad;

namespace vbamc.Tests.Streams
{
/// <summary>
/// Integration tests that validate VBA project compilation produces
/// a valid CompoundFile that can be decompiled using vbad logic.
///
/// These tests ensure the compiled VBA project binary has correct
/// structure and stream mappings. With broken OpenMcdf versions,
/// the compilation produces files where streams are not correctly
/// mapped to folders, making the VBA project unusable.
/// </summary>
class VbaProjectRoundTripTests
{
private byte[] _compiledVbaProject = null!;
private byte[] _compiledMultiModuleVbaProject = null!;

[SetUp]
public void Setup()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "data");
var modulePath = Path.Combine(sourcePath, "TestModule.vb");
var classPath = Path.Combine(sourcePath, "Class.vb");

// Compile single-module project
var compiler = CreateTestCompiler("RoundTripTest");
compiler.AddModule(modulePath);
using var stream = compiler.CompileVbaProject();
_compiledVbaProject = stream.ToArray();

// Compile multi-module project
var multiCompiler = CreateTestCompiler("MultiModuleTest");
multiCompiler.AddModule(modulePath);
multiCompiler.AddClass(classPath);
using var multiStream = multiCompiler.CompileVbaProject();
_compiledMultiModuleVbaProject = multiStream.ToArray();
}

private static VbaCompiler CreateTestCompiler(string projectName)
{
return new VbaCompiler
{
ProjectId = Guid.NewGuid(),
ProjectName = projectName,
ProjectVersion = "1.0.0",
CompanyName = "TestCompany"
};
}

[Test]
public void CompileVbaProject_ShouldProduceDecompilableCompoundFile()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledVbaProject));
var root = compoundFile.RootStorage;

// Verify PROJECT stream exists and has content
// Empty stream data indicates directory metadata wasn't flushed (broken OpenMcdf)
var projectStream = root.GetStream("PROJECT");
var projectData = projectStream.GetData();
ClassicAssert.Greater(projectData.Length, 0,
"PROJECT stream is empty - stream may not have been disposed");

// Verify PROJECTwm stream exists and has content
var projectWmStream = root.GetStream("PROJECTwm");
var projectWmData = projectWmStream.GetData();
ClassicAssert.Greater(projectWmData.Length, 0,
"PROJECTwm stream is empty - stream may not have been disposed");

// Verify VBA storage exists
var vbaStorage = root.GetStorage("VBA");
ClassicAssert.IsNotNull(vbaStorage, "VBA storage should exist");

// Verify _VBA_PROJECT stream exists and has content
var vbaProjectSubStream = vbaStorage.GetStream("_VBA_PROJECT");
var vbaProjectData = vbaProjectSubStream.GetData();
ClassicAssert.Greater(vbaProjectData.Length, 0,
"_VBA_PROJECT stream is empty - stream may not have been disposed");

// Verify dir stream exists, has content, and can be decompressed
var dirStream = vbaStorage.GetStream("dir");
var dirData = dirStream.GetData();
ClassicAssert.Greater(dirData.Length, 0,
"dir stream is empty - stream may not have been disposed");

var decompressedDir = VbaCompression.Decompress(dirData);
ClassicAssert.Greater(decompressedDir.Length, 0, "Decompressed dir should have content");
}

[Test]
public void CompileVbaProject_ShouldProduceReadableModules()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");

// Verify dir stream has content before decompression
var dirStreamData = vbaStorage.GetStream("dir").GetData();
ClassicAssert.Greater(dirStreamData.Length, 0,
"dir stream is empty - stream may not have been disposed");

var dirData = VbaCompression.Decompress(dirStreamData);

var modules = VbadDecompiler.DirStream.GetModules(dirData).ToList();
ClassicAssert.Greater(modules.Count, 0, "Should have at least one module");

foreach (var module in modules)
{
ClassicAssert.IsNotEmpty(module.Name, "Module name should not be empty");

var moduleStream = vbaStorage.GetStream(module.Name);
var moduleData = moduleStream.GetData();

// Check raw stream data is not empty before processing
ClassicAssert.Greater(moduleData.Length, 0,
$"Module '{module.Name}' stream is empty - stream may not have been disposed");

ClassicAssert.Greater(moduleData.Length, (int)module.Offset,
$"Module '{module.Name}' data length ({moduleData.Length}) should be greater than offset ({module.Offset})");

var moduleCode = moduleData.AsSpan().Slice((int)module.Offset).ToArray();
ClassicAssert.Greater(moduleCode.Length, 0,
$"Module '{module.Name}' code after offset is empty");

// Decompress module code - this is the critical test
// If the CompoundFile structure is broken, decompression will fail
var decompressedCode = VbaCompression.Decompress(moduleCode);
ClassicAssert.Greater(decompressedCode.Length, 0, "Decompressed code should have content");
}
}

[Test]
public void CompileVbaProject_DecompiledCodeShouldContainOriginalSource()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");

// Verify dir stream has content before decompression
var dirStreamData = vbaStorage.GetStream("dir").GetData();
ClassicAssert.Greater(dirStreamData.Length, 0,
"dir stream is empty - stream may not have been disposed");

var dirData = VbaCompression.Decompress(dirStreamData);

var modules = VbadDecompiler.DirStream.GetModules(dirData).ToList();
var testModule = modules.First(m => m.Name == "TestModule");

var moduleStream = vbaStorage.GetStream(testModule.Name!);
var moduleData = moduleStream.GetData();

// Check raw stream data is not empty
ClassicAssert.Greater(moduleData.Length, 0,
"TestModule stream is empty - stream may not have been disposed");

var moduleCode = moduleData.AsSpan().Slice((int)testModule.Offset).ToArray();
ClassicAssert.Greater(moduleCode.Length, 0,
"TestModule code after offset is empty");

var decompressedSource = Encoding.GetEncoding(1252).GetString(VbaCompression.Decompress(moduleCode));

ClassicAssert.IsTrue(decompressedSource.Contains("HelloWorld"),
"Decompiled source should contain 'HelloWorld' function");
ClassicAssert.IsTrue(decompressedSource.Contains("AddNumbers"),
"Decompiled source should contain 'AddNumbers' function");
ClassicAssert.IsTrue(decompressedSource.Contains("MsgBox"),
"Decompiled source should contain 'MsgBox' call");
}

[Test]
public void CompileVbaProject_WithMultipleModules_AllShouldBeDecompilable()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledMultiModuleVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");

// Verify dir stream has content before decompression
var dirStreamData = vbaStorage.GetStream("dir").GetData();
ClassicAssert.Greater(dirStreamData.Length, 0,
"dir stream is empty - stream may not have been disposed");

var dirData = VbaCompression.Decompress(dirStreamData);

var modules = VbadDecompiler.DirStream.GetModules(dirData).ToList();
ClassicAssert.AreEqual(2, modules.Count, "Should have exactly 2 modules (TestModule and Class)");

foreach (var module in modules)
{
var moduleStream = vbaStorage.GetStream(module.Name);
var moduleData = moduleStream.GetData();

// Check raw stream data is not empty before processing
ClassicAssert.Greater(moduleData.Length, 0,
$"Module '{module.Name}' stream is empty - stream may not have been disposed");

var moduleCode = moduleData.AsSpan().Slice((int)module.Offset).ToArray();
ClassicAssert.Greater(moduleCode.Length, 0,
$"Module '{module.Name}' code after offset is empty");

var decompressedCode = VbaCompression.Decompress(moduleCode);
ClassicAssert.IsNotNull(decompressedCode, $"Module '{module.Name}' should decompress successfully");
}
}
}
}
12 changes: 11 additions & 1 deletion tests/VbaCompiler.Tests/VbaCompiler.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
Expand All @@ -10,6 +10,7 @@
<ItemGroup>
<None Remove="data\Class.vb" />
<None Remove="data\Module.vb" />
<None Remove="data\TestModule.vb" />
</ItemGroup>

<ItemGroup>
Expand All @@ -19,6 +20,9 @@
<Content Include="data\Module.vb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\TestModule.vb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
Expand All @@ -34,6 +38,12 @@

<ItemGroup>
<ProjectReference Include="..\..\src\vbamc\vbamc.csproj" />
<ProjectReference Include="..\..\utils\vbad\vbad.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NetOfficeFw.VbaCompression" Version="3.0.1" />
<PackageReference Include="OpenMcdf" Version="2.4.1" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions tests/VbaCompiler.Tests/data/TestModule.vb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Public Sub HelloWorld()
MsgBox "Hello from VBA!"
End Sub

Public Function AddNumbers(a As Integer, b As Integer) As Integer
AddNumbers = a + b
End Function
4 changes: 2 additions & 2 deletions utils/vbad/vbad.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down