|
5 | 5 | using System.Collections.Generic; |
6 | 6 | using System.IO; |
7 | 7 | using System.Linq; |
| 8 | +using System.Security.Cryptography; |
8 | 9 | using System.Text; |
9 | 10 | using Kavod.Vba.Compression; |
10 | 11 | using OpenMcdf; |
| 12 | +using vbamc.Vba; |
11 | 13 | using VbadDecompiler = vbad; |
12 | 14 |
|
13 | 15 | namespace vbamc.Tests.Streams |
@@ -225,5 +227,131 @@ public void CompileVbaProject_WithMultipleModules_AllShouldMatchOriginalSource() |
225 | 227 | $"Decompiled source for module '{module.Name}' should exactly match the original source with VBA headers"); |
226 | 228 | } |
227 | 229 | } |
| 230 | + |
| 231 | + [Test] |
| 232 | + public void CompileVbaProject_ShouldMatchGoldenFile() |
| 233 | + { |
| 234 | + // Use fixed values to ensure deterministic output |
| 235 | + var fixedGuid = new Guid("12345678-1234-1234-1234-123456789ABC"); |
| 236 | + var originalRandomGenerator = VbaEncryption.CreateRandomGenerator; |
| 237 | + |
| 238 | + try |
| 239 | + { |
| 240 | + // Replace random generator with deterministic one |
| 241 | + VbaEncryption.CreateRandomGenerator = () => new DeterministicRandomNumberGenerator(); |
| 242 | + |
| 243 | + var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "data"); |
| 244 | + var modulePath = Path.Combine(sourcePath, "TestModule.vb"); |
| 245 | + |
| 246 | + var compiler = new VbaCompiler |
| 247 | + { |
| 248 | + ProjectId = fixedGuid, |
| 249 | + ProjectName = "GoldenTest", |
| 250 | + ProjectVersion = "1.0.0", |
| 251 | + CompanyName = "TestCompany" |
| 252 | + }; |
| 253 | + compiler.AddModule(modulePath); |
| 254 | + |
| 255 | + using var stream = compiler.CompileVbaProject(); |
| 256 | + var compiledBytes = stream.ToArray(); |
| 257 | + |
| 258 | + // Mask out compound file timestamps that vary between runs |
| 259 | + // These are at offsets in the directory entries (varies by file structure) |
| 260 | + MaskCompoundFileTimestamps(compiledBytes); |
| 261 | + |
| 262 | + var goldenFilePath = Path.Combine(sourcePath, "vbaProject.bin"); |
| 263 | + |
| 264 | + if (!File.Exists(goldenFilePath)) |
| 265 | + { |
| 266 | + // Generate golden file if it doesn't exist |
| 267 | + File.WriteAllBytes(goldenFilePath, compiledBytes); |
| 268 | + Assert.Inconclusive($"Golden file created at {goldenFilePath}. Re-run the test to validate."); |
| 269 | + } |
| 270 | + |
| 271 | + var goldenBytes = File.ReadAllBytes(goldenFilePath); |
| 272 | + MaskCompoundFileTimestamps(goldenBytes); |
| 273 | + |
| 274 | + ClassicAssert.AreEqual(goldenBytes, compiledBytes, |
| 275 | + "Compiled VBA project should match the golden file byte-for-byte (excluding timestamps)"); |
| 276 | + } |
| 277 | + finally |
| 278 | + { |
| 279 | + // Restore original random generator |
| 280 | + VbaEncryption.CreateRandomGenerator = originalRandomGenerator; |
| 281 | + } |
| 282 | + } |
| 283 | + |
| 284 | + /// <summary> |
| 285 | + /// Masks out timestamp fields in compound file directory entries. |
| 286 | + /// Compound files have 128-byte directory entries with timestamps at specific offsets. |
| 287 | + /// </summary> |
| 288 | + private static void MaskCompoundFileTimestamps(byte[] data) |
| 289 | + { |
| 290 | + // Compound file directory entries start at sector 0 (offset 512) or later |
| 291 | + // Each entry is 128 bytes with creation time at offset 100 and modification time at offset 108 |
| 292 | + // The directory sector location is specified in the header at offset 48 |
| 293 | + |
| 294 | + if (data.Length < 512) |
| 295 | + return; |
| 296 | + |
| 297 | + // Read directory sector location from header (offset 48, 4 bytes, little-endian) |
| 298 | + int directorySectorIndex = BitConverter.ToInt32(data, 48); |
| 299 | + if (directorySectorIndex < 0) |
| 300 | + return; |
| 301 | + |
| 302 | + // Sector size is typically 512 bytes for compound files with version 3 |
| 303 | + const int sectorSize = 512; |
| 304 | + const int headerSize = 512; |
| 305 | + int directoryOffset = headerSize + (directorySectorIndex * sectorSize); |
| 306 | + |
| 307 | + // Mask timestamps in all directory entries |
| 308 | + const int entrySize = 128; |
| 309 | + const int creationTimeOffset = 100; |
| 310 | + const int modificationTimeOffset = 108; |
| 311 | + const int timestampSize = 8; |
| 312 | + |
| 313 | + for (int entryStart = directoryOffset; |
| 314 | + entryStart + entrySize <= data.Length; |
| 315 | + entryStart += entrySize) |
| 316 | + { |
| 317 | + // Check if this looks like a valid entry (first byte of name should be non-zero for used entries) |
| 318 | + if (data[entryStart] == 0 && data[entryStart + 1] == 0) |
| 319 | + break; // End of entries |
| 320 | + |
| 321 | + // Mask creation time |
| 322 | + for (int i = 0; i < timestampSize; i++) |
| 323 | + data[entryStart + creationTimeOffset + i] = 0; |
| 324 | + |
| 325 | + // Mask modification time |
| 326 | + for (int i = 0; i < timestampSize; i++) |
| 327 | + data[entryStart + modificationTimeOffset + i] = 0; |
| 328 | + } |
| 329 | + } |
| 330 | + |
| 331 | + /// <summary> |
| 332 | + /// A deterministic random number generator for testing purposes. |
| 333 | + /// Produces a fixed repeatable sequence independent of .NET version. |
| 334 | + /// </summary> |
| 335 | + private sealed class DeterministicRandomNumberGenerator : RandomNumberGenerator |
| 336 | + { |
| 337 | + private int _index; |
| 338 | + |
| 339 | + public override void GetBytes(byte[] data) |
| 340 | + { |
| 341 | + for (int i = 0; i < data.Length; i++) |
| 342 | + { |
| 343 | + data[i] = (byte)((_index++ * 17 + 31) % 256); |
| 344 | + } |
| 345 | + } |
| 346 | + |
| 347 | + public override void GetNonZeroBytes(byte[] data) |
| 348 | + { |
| 349 | + for (int i = 0; i < data.Length; i++) |
| 350 | + { |
| 351 | + // Generate values 1-255 (never zero) |
| 352 | + data[i] = (byte)((_index++ * 17 + 31) % 255 + 1); |
| 353 | + } |
| 354 | + } |
| 355 | + } |
228 | 356 | } |
229 | 357 | } |
0 commit comments