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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/VbaCompiler/Vba/ModuleStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ public ModuleStream(ModuleUnit module)

public ModuleUnit Module { get; }

public void WriteTo(CFStorage storage)
public void WriteTo(Storage storage)
{
var streamName = this.Module.Name;
var stream = storage.AddStream(streamName);
using var stream = storage.CreateStream(streamName);

var content = this.Module.ToModuleCode();
var contentBytes = VbaEncodings.Default.GetBytes(content);
var compressedBytes = VbaCompression.Compress(contentBytes);

stream.SetData(compressedBytes);
stream.Write(compressedBytes, 0, compressedBytes.Length);
}
}
}
52 changes: 31 additions & 21 deletions src/VbaCompiler/VbaCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,9 @@ public MemoryStream CompileVbaProject()
public void CompileVbaProject(Stream stream)
{
var moduleNames = this.modules.OrderBy(m => m.Type).Select(m => m.Name).ToList();

var storage = new CompoundFile();
var projectId = this.ProjectId.ToString("B").ToUpperInvariant();

// PROJECT stream
var projectStream = storage.RootStorage.AddStream(StreamId.Project);
// Build project record once
var project = new ProjectRecord();
project.Id = projectId;
project.Name = this.ProjectName;
Expand All @@ -83,30 +80,43 @@ public void CompileVbaProject(Stream stream)
project.ProjectPassword = projectPassword.ToEncryptedString();
project.VisibilityState = visibilityState.ToEncryptedString();

var projectContent = project.Generate();
projectStream.SetData(projectContent);
// Create RootStorage directly with the output stream
using var storage = RootStorage.Create(stream, OpenMcdf.Version.V3, StorageModeFlags.LeaveOpen);

// PROJECT stream
using (var projectStream = storage.CreateStream(StreamId.Project))
{
var projectContent = project.Generate();
projectStream.Write(projectContent, 0, projectContent.Length);
}

// PROJECTwm stream
var projectWmStream = storage.RootStorage.AddStream(StreamId.ProjectWm);
var projectWm = new ProjectWmRecord(moduleNames);
var projectWmContent = projectWm.Generate();
projectWmStream.SetData(projectWmContent);
using (var projectWmStream = storage.CreateStream(StreamId.ProjectWm))
{
var projectWm = new ProjectWmRecord(moduleNames);
var projectWmContent = projectWm.Generate();
projectWmStream.Write(projectWmContent, 0, projectWmContent.Length);
}

// VBA storage
var vbaStorage = storage.RootStorage.AddStorage(StorageId.VBA);
var vbaStorage = storage.CreateStorage(StorageId.VBA);

// _VBA_PROJECT stream
var vbaProjectStream = vbaStorage.AddStream(StreamId.VbaProject);
var vbaProject = new VbaProjectStream();
var vbaProjectContent = vbaProject.Generate();
vbaProjectStream.SetData(vbaProjectContent);
using (var vbaProjectStream = vbaStorage.CreateStream(StreamId.VbaProject))
{
var vbaProject = new VbaProjectStream();
var vbaProjectContent = vbaProject.Generate();
vbaProjectStream.Write(vbaProjectContent, 0, vbaProjectContent.Length);
}

// dir stream
var dirStream = vbaStorage.AddStream(StreamId.Dir);
var dir = new DirStream();
var dirContent = dir.GetData(project);
var compressed = VbaCompression.Compress(dirContent);
dirStream.SetData(compressed);
using (var dirStream = vbaStorage.CreateStream(StreamId.Dir))
{
var dir = new DirStream();
var dirContent = dir.GetData(project);
var compressed = VbaCompression.Compress(dirContent);
dirStream.Write(compressed, 0, compressed.Length);
}

// module streams
foreach (var module in this.modules)
Expand All @@ -115,7 +125,7 @@ public void CompileVbaProject(Stream stream)
moduleStream.WriteTo(vbaStorage);
}

storage.Save(stream);
// Auto-flush on storage dispose
}

public void CompilePowerPointMacroFile(Stream outputMacroFileStream, Stream vbaProjectStream, PresentationDocumentType documentType, string? customSourcePath = null)
Expand Down
2 changes: 1 addition & 1 deletion src/VbaCompiler/VbaCompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="OpenMcdf" Version="2.4.1" />
<PackageReference Include="OpenMcdf" Version="3.1.0" />
<PackageReference Include="NetOfficeFw.VbaCompression" Version="3.0.1" />
</ItemGroup>

Expand Down
61 changes: 35 additions & 26 deletions tests/VbaCompiler.Tests/Streams/VbaProjectRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,44 @@ private static VbaCompiler CreateTestCompiler(string projectName)
};
}

private static byte[] ReadStreamData(CfbStream stream)
{
var data = new byte[stream.Length];
stream.ReadExactly(data);
return data;
}

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

// 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();
using var projectStream = root.OpenStream("PROJECT");
var projectData = ReadStreamData(projectStream);
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();
using var projectWmStream = root.OpenStream("PROJECTwm");
var projectWmData = ReadStreamData(projectWmStream);
ClassicAssert.Greater(projectWmData.Length, 0,
"PROJECTwm stream is empty - stream may not have been disposed");

// Verify VBA storage exists
var vbaStorage = root.GetStorage("VBA");
var vbaStorage = root.OpenStorage("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();
using var vbaProjectSubStream = vbaStorage.OpenStream("_VBA_PROJECT");
var vbaProjectData = ReadStreamData(vbaProjectSubStream);
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();
using var dirStream = vbaStorage.OpenStream("dir");
var dirData = ReadStreamData(dirStream);
ClassicAssert.Greater(dirData.Length, 0,
"dir stream is empty - stream may not have been disposed");

Expand All @@ -110,11 +116,12 @@ public void CompileVbaProject_ShouldProduceDecompilableCompoundFile()
[Test]
public void CompileVbaProject_ShouldProduceReadableModules()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");
using var root = RootStorage.Open(new MemoryStream(_compiledVbaProject), StorageModeFlags.LeaveOpen);
var vbaStorage = root.OpenStorage("VBA");

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

Expand All @@ -127,8 +134,8 @@ public void CompileVbaProject_ShouldProduceReadableModules()
{
ClassicAssert.IsNotEmpty(module.Name, "Module name should not be empty");

var moduleStream = vbaStorage.GetStream(module.Name);
var moduleData = moduleStream.GetData();
using var moduleStream = vbaStorage.OpenStream(module.Name!);
var moduleData = ReadStreamData(moduleStream);

// Check raw stream data is not empty before processing
ClassicAssert.Greater(moduleData.Length, 0,
Expand All @@ -151,11 +158,12 @@ public void CompileVbaProject_ShouldProduceReadableModules()
[Test]
public void CompileVbaProject_DecompiledCodeShouldMatchOriginalSource()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");
using var root = RootStorage.Open(new MemoryStream(_compiledVbaProject), StorageModeFlags.LeaveOpen);
var vbaStorage = root.OpenStorage("VBA");

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

Expand All @@ -164,8 +172,8 @@ public void CompileVbaProject_DecompiledCodeShouldMatchOriginalSource()
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();
using var moduleStream = vbaStorage.OpenStream(testModule.Name!);
var moduleData = ReadStreamData(moduleStream);

// Check raw stream data is not empty
ClassicAssert.Greater(moduleData.Length, 0,
Expand All @@ -184,11 +192,12 @@ public void CompileVbaProject_DecompiledCodeShouldMatchOriginalSource()
[Test]
public void CompileVbaProject_WithMultipleModules_AllShouldMatchOriginalSource()
{
using var compoundFile = new CompoundFile(new MemoryStream(_compiledMultiModuleVbaProject));
var vbaStorage = compoundFile.RootStorage.GetStorage("VBA");
using var root = RootStorage.Open(new MemoryStream(_compiledMultiModuleVbaProject), StorageModeFlags.LeaveOpen);
var vbaStorage = root.OpenStorage("VBA");

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

Expand All @@ -206,8 +215,8 @@ public void CompileVbaProject_WithMultipleModules_AllShouldMatchOriginalSource()

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

// Check raw stream data is not empty before processing
ClassicAssert.Greater(moduleData.Length, 0,
Expand Down
2 changes: 1 addition & 1 deletion tests/VbaCompiler.Tests/VbaCompiler.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

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

</Project>
87 changes: 47 additions & 40 deletions utils/vbad/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,58 +15,62 @@

var source = Path.Combine(sourcePath, "vbaProject.bin");

var vba = new CompoundFile(source);
var root = vba.RootStorage;
using var root = RootStorage.OpenRead(source);

root.VisitEntries(item => ProcessFile(root, "", item), false);
ProcessDirAndModules(root.GetStorage("VBA"));
ProcessEntriesRecursive(root, "");
var vbaStorage = root.OpenStorage("VBA");
ProcessDirAndModules(vbaStorage);

Console.WriteLine();
Console.WriteLine($" vbaProject.bin -> {targetPath}");
Console.WriteLine("Project was decompiled.");

void ProcessFile(CFStorage storage, string directory, CFItem item)
void ProcessEntriesRecursive(Storage storage, string directory)
{
if (item.IsStream)
foreach (var entry in storage.EnumerateEntries())
{
var name = item.Name;
var target = Path.Combine(targetPath, directory, name + ".bin");
if (entry.Type == EntryType.Stream)
{
var name = entry.Name;
var target = Path.Combine(targetPath, directory, name + ".bin");

var stream = storage.GetStream(name);
var data = stream.GetData();
using var stream = storage.OpenStream(name);
var data = new byte[stream.Length];
stream.ReadExactly(data);

if (name == "dir")
{
try
{
data = VbaCompression.Decompress(data);
}
catch (Exception ex)
if (name == "dir")
{
Console.WriteLine(ex.Message);
try
{
data = VbaCompression.Decompress(data);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

File.WriteAllBytes(target, data);
}

File.WriteAllBytes(target, data);
}
else if (item.IsStorage)
{
var s2 = (CFStorage)item;
var dir = Path.Combine(targetPath, s2.Name);
if (!Directory.Exists(dir))
else if (entry.Type == EntryType.Storage)
{
Directory.CreateDirectory(dir);
}
var dir = Path.Combine(targetPath, entry.Name);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}

s2.VisitEntries(item => ProcessFile(s2, s2.Name, item), false);
var subStorage = storage.OpenStorage(entry.Name);
ProcessEntriesRecursive(subStorage, entry.Name);
}
}
}


void ProcessDirAndModules(CFStorage vbaStorage)
void ProcessDirAndModules(Storage vbaStorage)
{
var dir = vbaStorage.GetStream("dir");
var data = dir.GetData();
using var dirStream = vbaStorage.OpenStream("dir");
var data = new byte[dirStream.Length];
dirStream.ReadExactly(data);
data = VbaCompression.Decompress(data);
var target = Path.Combine(targetPath, "vba", "dir.bin");

Expand All @@ -76,18 +80,21 @@ void ProcessDirAndModules(CFStorage vbaStorage)
foreach (var module in modules)
{
var name = module.Name;
if (name == null) continue;

var targetVbBin = Path.Combine(targetPath, "vba", name + ".bin");
var targetVbText = Path.Combine(targetPath, "vba", name + ".vb");

var stream = vbaStorage.GetStream(name);
Span<byte> dataVb = stream.GetData();
var dataVbBin = dataVb.Slice((int)module.Offset).ToArray();
using var stream = vbaStorage.OpenStream(name);
var dataVb = new byte[stream.Length];
stream.ReadExactly(dataVb);
var dataVbBin = dataVb.AsSpan().Slice((int)module.Offset).ToArray();
File.WriteAllBytes(targetVbBin, dataVbBin);

dataVb = VbaCompression.Decompress(dataVbBin);

var sourceCode = Encoding.GetEncoding(1252).GetString(dataVb);
var decompressed = VbaCompression.Decompress(dataVbBin);

var sourceCode = Encoding.GetEncoding(1252).GetString(decompressed);

File.WriteAllText(targetVbText, sourceCode);
}
}
}
2 changes: 1 addition & 1 deletion utils/vbad/vbad.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

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

Expand Down