From e93081e3913806fea52a276d2b19f3903478328a Mon Sep 17 00:00:00 2001 From: Tobias Janssen Date: Thu, 30 Jul 2026 15:56:36 +0200 Subject: [PATCH 1/2] fix: disambiguate legacy OLE DOC vs XLS detection (#178) DOC and XLS both share the generic OLE/CFBF header (D0 CF 11 E0 A1 B1 1A E1). DOC previously matched only this container signature, which could cause XLS files to be classified as DOC and lead to null from validated type lookup. Add a DOC-specific subheader check at offset 512: - EC A5 C1 00 This makes DOC detection stricter and prevents false positives for XLS files that match XLS-specific offset-512 signatures (e.g. FD FF FF FF ?? 00 / ?? 02). Add regression tests to verify: - OLE + XLS subheader matches XLS and does not match DOC - OLE + DOC subheader matches DOC issue: #178 --- .../DocXlsDisambiguationTests.cs | 59 +++++++++++++++++++ MagicBytesValidator/Formats/Doc.cs | 7 ++- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs diff --git a/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs b/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs new file mode 100644 index 0000000..9f63572 --- /dev/null +++ b/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs @@ -0,0 +1,59 @@ +namespace MagicBytesValidator.Tests; + +public class DocXlsDisambiguationTests +{ + private static readonly byte[] OleHeader = + [ + 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 + ]; + + [Fact] + public async Task XlsSignatureAtOffset512_ShouldMatchXls_AndNotDoc() + { + // Arrange + var validator = new Validator(); + var xls = new Xls(); + var doc = new Doc(); + + using var stream = BuildOleLikeStreamWithSubHeaderAt512( + [0xFD, 0xFF, 0xFF, 0xFF, 0x24, 0x00] // matches Xls ByteCheck with wildcard in slot 5 + ); + + // Act + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); + stream.Position = 0; + var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + + // Assert + Assert.True(isXls); + Assert.False(isDoc); + } + + [Fact] + public async Task DocSignatureAtOffset512_ShouldMatchDoc() + { + // Arrange + var validator = new Validator(); + var doc = new Doc(); + + using var stream = BuildOleLikeStreamWithSubHeaderAt512( + [0xEC, 0xA5, 0xC1, 0x00] // classic DOC subheader at offset 512 + ); + + // Act + var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + + // Assert + Assert.True(isDoc); + } + + private static MemoryStream BuildOleLikeStreamWithSubHeaderAt512(byte[] subHeader) + { + var bytes = new byte[1024]; + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(subHeader, 0, bytes, 512, subHeader.Length); + + return new MemoryStream(bytes); + } +} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Doc.cs b/MagicBytesValidator/Formats/Doc.cs index 240b93e..3b54a11 100644 --- a/MagicBytesValidator/Formats/Doc.cs +++ b/MagicBytesValidator/Formats/Doc.cs @@ -1,7 +1,5 @@ namespace MagicBytesValidator.Formats; -// TODO: Add sub header check (512 byte offset: EC A5 C1 00) - /// /// public class Doc : FileByteFilter @@ -11,6 +9,9 @@ public Doc() : base( ["doc", "dot"] ) { - StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]); + StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) + .SpecificAnyOf([ + new ByteCheck(512, [0xEC, 0xA5, 0xC1, 0x00]) + ]); } } \ No newline at end of file From ecc3bdc0f7b9fa0a2b92975a5a1c91dd44106493 Mon Sep 17 00:00:00 2001 From: Tobias Janssen Date: Thu, 10 Sep 2026 15:57:13 +0200 Subject: [PATCH 2/2] test(office): add legacy and OOXML recognition coverage - add cross-family recognition tests for legacy OLE (.doc/.xls/.ppt) and modern OOXML (.docx/.xlsx/.pptx) - extend FormFileTypeProvider.FindValidatedTypeAsync tests for legacy and modern office uploads - add synthetic OLE/OOXML test payload builders to keep tests deterministic --- .../DocXlsDisambiguationTests.cs | 78 +++++-- .../Http/FindValidatedTypeAsync.cs | 198 +++++++++++++++++- .../OfficeLegacyAndModernRecognitionTests.cs | 169 +++++++++++++++ MagicBytesValidator/Formats/Doc.cs | 15 +- MagicBytesValidator/Formats/Xls.cs | 23 +- MagicBytesValidator/Models/FileByteFilter.cs | 18 ++ 6 files changed, 472 insertions(+), 29 deletions(-) create mode 100644 MagicBytesValidator.Tests/OfficeLegacyAndModernRecognitionTests.cs diff --git a/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs b/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs index 9f63572..aa1e2aa 100644 --- a/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs +++ b/MagicBytesValidator.Tests/DocXlsDisambiguationTests.cs @@ -8,51 +8,95 @@ public class DocXlsDisambiguationTests ]; [Fact] - public async Task XlsSignatureAtOffset512_ShouldMatchXls_AndNotDoc() + public async Task XlsWorkbookStreamName_ShouldMatchXls_AndNotDoc() { - // Arrange var validator = new Validator(); var xls = new Xls(); var doc = new Doc(); - using var stream = BuildOleLikeStreamWithSubHeaderAt512( - [0xFD, 0xFF, 0xFF, 0xFF, 0x24, 0x00] // matches Xls ByteCheck with wildcard in slot 5 - ); + using var stream = BuildOleLikeStreamWithUtf16Marker("Workbook"); - // Act var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); stream.Position = 0; var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); - // Assert Assert.True(isXls); Assert.False(isDoc); } [Fact] - public async Task DocSignatureAtOffset512_ShouldMatchDoc() + public async Task DocWordDocumentStreamName_ShouldMatchDoc_AndNotXls() { - // Arrange var validator = new Validator(); var doc = new Doc(); + var xls = new Xls(); - using var stream = BuildOleLikeStreamWithSubHeaderAt512( - [0xEC, 0xA5, 0xC1, 0x00] // classic DOC subheader at offset 512 - ); + using var stream = BuildOleLikeStreamWithUtf16Marker("WordDocument"); - // Act var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + stream.Position = 0; + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); - // Assert Assert.True(isDoc); + Assert.False(isXls); + } + + [Fact] + public async Task LegacyXlsBookStreamName_ShouldMatchXls() + { + var validator = new Validator(); + var xls = new Xls(); + + using var stream = BuildOleLikeStreamWithUtf16Marker("Book"); + + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); + + Assert.True(isXls); + } + + [Fact] + public async Task ClassicDocOffset512Marker_ShouldMatchDoc() + { + var validator = new Validator(); + var doc = new Doc(); + + using var stream = BuildOleLikeStreamWithOffset512Marker([0xEC, 0xA5, 0xC1, 0x00]); + + var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + + Assert.True(isDoc); + } + + [Fact] + public async Task ClassicXlsOffset512Marker_ShouldMatchXls() + { + var validator = new Validator(); + var xls = new Xls(); + + using var stream = BuildOleLikeStreamWithOffset512Marker([0xFD, 0xFF, 0xFF, 0xFF, 0x24, 0x00]); + + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); + + Assert.True(isXls); + } + + private static MemoryStream BuildOleLikeStreamWithUtf16Marker(string marker) + { + var bytes = new byte[4096]; + var markerBytes = System.Text.Encoding.Unicode.GetBytes(marker); + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(markerBytes, 0, bytes, 1536, markerBytes.Length); + + return new MemoryStream(bytes); } - private static MemoryStream BuildOleLikeStreamWithSubHeaderAt512(byte[] subHeader) + private static MemoryStream BuildOleLikeStreamWithOffset512Marker(byte[] marker) { - var bytes = new byte[1024]; + var bytes = new byte[2048]; Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); - Array.Copy(subHeader, 0, bytes, 512, subHeader.Length); + Array.Copy(marker, 0, bytes, 512, marker.Length); return new MemoryStream(bytes); } diff --git a/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs b/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs index c4b3cea..98e360b 100644 --- a/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs +++ b/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs @@ -2,6 +2,11 @@ public class FindValidatedTypeAsync { + private static readonly byte[] OleHeader = + [ + 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 + ]; + [Fact] public async Task Should_find_by_extension() { @@ -82,14 +87,120 @@ await sut.FindValidatedTypeAsync( ); } - private static IFormFile ProvideGifFile(string name, string contentType) + [Fact] + public async Task Should_validate_legacy_doc() { - byte[] gifSequence = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; - var fileContents = gifSequence.Concat(new byte[] { 0x11, 0x12 }).ToArray(); - var fileStream = new MemoryStream(fileContents.ToArray()); + var formFile = ProvideFile("legacy.doc", "application/msword", BuildLegacyDocBytes()); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + [Fact] + public async Task Should_validate_legacy_xls() + { + var formFile = ProvideFile("legacy.xls", "application/msexcel", BuildLegacyXlsBytes()); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + [Fact] + public async Task Should_validate_legacy_ppt() + { + var formFile = ProvideFile("legacy.ppt", "application/vnd.ms-powerpoint", BuildLegacyPptBytes()); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + [Fact] + public async Task Should_validate_modern_docx() + { + var formFile = ProvideFile( + "modern.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + BuildOpenXmlBytes("word/_rels/document.xml.rels") + ); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + [Fact] + public async Task Should_validate_modern_xlsx() + { + var formFile = ProvideFile( + "modern.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + BuildOpenXmlBytes("xl/_rels/workbook.xml.rels") + ); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + [Fact] + public async Task Should_validate_modern_pptx() + { + var formFile = ProvideFile( + "modern.pptx", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + BuildOpenXmlBytes("ppt/_rels/presentation.xml.rels") + ); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + Assert.IsType(result); + } + + private static IFormFile ProvideFile(string name, string contentType, byte[] fileContents) + { + var fileStream = new MemoryStream(fileContents); return new FormFile( - new MemoryStream(fileContents.ToArray()), + new MemoryStream(fileContents), 0, fileStream.Length, name, @@ -102,4 +213,81 @@ private static IFormFile ProvideGifFile(string name, string contentType) } }; } + + private static IFormFile ProvideGifFile(string name, string contentType) + { + byte[] gifSequence = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; + var fileContents = gifSequence.Concat(new byte[] { 0x11, 0x12 }).ToArray(); + + return ProvideFile(name, contentType, fileContents); + } + + private static byte[] BuildLegacyDocBytes() + { + var bytes = new byte[4096]; + var markerBytes = System.Text.Encoding.Unicode.GetBytes("WordDocument"); + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(markerBytes, 0, bytes, 1536, markerBytes.Length); + + return bytes; + } + + private static byte[] BuildLegacyXlsBytes() + { + var bytes = new byte[4096]; + var markerBytes = System.Text.Encoding.Unicode.GetBytes("Workbook"); + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(markerBytes, 0, bytes, 1536, markerBytes.Length); + + return bytes; + } + + private static byte[] BuildLegacyPptBytes() + { + var bytes = new byte[2048]; + var marker = new byte[] { 0xA0, 0x46, 0x1D, 0xF0 }; + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(marker, 0, bytes, 512, marker.Length); + + return bytes; + } + + private static byte[] BuildOpenXmlBytes(string marker) + { + var bytes = BuildZipLikeBytesWithEocd(0x12345678); + var markerBytes = System.Text.Encoding.ASCII.GetBytes(marker); + + Array.Copy(markerBytes, 0, bytes, 64, markerBytes.Length); + + return bytes; + } + + private static byte[] BuildZipLikeBytesWithEocd(uint centralDirectoryOffset) + { + var bytes = new byte[512]; + + bytes[0] = 0x50; + bytes[1] = 0x4B; + bytes[2] = 0x03; + bytes[3] = 0x04; + + var eocdStart = bytes.Length - 22; + bytes[eocdStart + 0] = 0x50; + bytes[eocdStart + 1] = 0x4B; + bytes[eocdStart + 2] = 0x05; + bytes[eocdStart + 3] = 0x06; + + bytes[eocdStart + 16] = (byte)(centralDirectoryOffset & 0xFF); + bytes[eocdStart + 17] = (byte)((centralDirectoryOffset >> 8) & 0xFF); + bytes[eocdStart + 18] = (byte)((centralDirectoryOffset >> 16) & 0xFF); + bytes[eocdStart + 19] = (byte)((centralDirectoryOffset >> 24) & 0xFF); + + bytes[eocdStart + 20] = 0x00; + bytes[eocdStart + 21] = 0x00; + + return bytes; + } } \ No newline at end of file diff --git a/MagicBytesValidator.Tests/OfficeLegacyAndModernRecognitionTests.cs b/MagicBytesValidator.Tests/OfficeLegacyAndModernRecognitionTests.cs new file mode 100644 index 0000000..6cdb4cd --- /dev/null +++ b/MagicBytesValidator.Tests/OfficeLegacyAndModernRecognitionTests.cs @@ -0,0 +1,169 @@ +namespace MagicBytesValidator.Tests; + +public class OfficeLegacyAndModernRecognitionTests +{ + private static readonly byte[] OleHeader = + [ + 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 + ]; + + [Fact] + public async Task LegacyDocOle_ShouldMatchDoc_AndNotDocx() + { + var validator = new Validator(); + var doc = new Doc(); + var docx = new Docx(); + + using var stream = BuildOleLikeStreamWithUtf16Marker("WordDocument"); + + var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + stream.Position = 0; + var isDocx = await validator.IsValidAsync(stream, docx, CancellationToken.None); + + Assert.True(isDoc); + Assert.False(isDocx); + } + + [Fact] + public async Task LegacyXlsOle_ShouldMatchXls_AndNotXlsx() + { + var validator = new Validator(); + var xls = new Xls(); + var xlsx = new Xlsx(); + + using var stream = BuildOleLikeStreamWithUtf16Marker("Workbook"); + + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); + stream.Position = 0; + var isXlsx = await validator.IsValidAsync(stream, xlsx, CancellationToken.None); + + Assert.True(isXls); + Assert.False(isXlsx); + } + + [Fact] + public async Task LegacyPptOle_ShouldMatchPpt_AndNotPptx() + { + var validator = new Validator(); + var ppt = new Ppt(); + var pptx = new Pptx(); + + using var stream = BuildOleLikeStreamWithOffset512Marker([0xA0, 0x46, 0x1D, 0xF0]); + + var isPpt = await validator.IsValidAsync(stream, ppt, CancellationToken.None); + stream.Position = 0; + var isPptx = await validator.IsValidAsync(stream, pptx, CancellationToken.None); + + Assert.True(isPpt); + Assert.False(isPptx); + } + + [Fact] + public async Task ModernDocxZip_ShouldMatchDocx_AndNotDoc() + { + var validator = new Validator(); + var docx = new Docx(); + var doc = new Doc(); + + using var stream = BuildOfficeOpenXmlLikeZipStream("word/_rels/document.xml.rels"); + + var isDocx = await validator.IsValidAsync(stream, docx, CancellationToken.None); + stream.Position = 0; + var isDoc = await validator.IsValidAsync(stream, doc, CancellationToken.None); + + Assert.True(isDocx); + Assert.False(isDoc); + } + + [Fact] + public async Task ModernXlsxZip_ShouldMatchXlsx_AndNotXls() + { + var validator = new Validator(); + var xlsx = new Xlsx(); + var xls = new Xls(); + + using var stream = BuildOfficeOpenXmlLikeZipStream("xl/_rels/workbook.xml.rels"); + + var isXlsx = await validator.IsValidAsync(stream, xlsx, CancellationToken.None); + stream.Position = 0; + var isXls = await validator.IsValidAsync(stream, xls, CancellationToken.None); + + Assert.True(isXlsx); + Assert.False(isXls); + } + + [Fact] + public async Task ModernPptxZip_ShouldMatchPptx_AndNotPpt() + { + var validator = new Validator(); + var pptx = new Pptx(); + var ppt = new Ppt(); + + using var stream = BuildOfficeOpenXmlLikeZipStream("ppt/_rels/presentation.xml.rels"); + + var isPptx = await validator.IsValidAsync(stream, pptx, CancellationToken.None); + stream.Position = 0; + var isPpt = await validator.IsValidAsync(stream, ppt, CancellationToken.None); + + Assert.True(isPptx); + Assert.False(isPpt); + } + + private static MemoryStream BuildOleLikeStreamWithUtf16Marker(string marker) + { + var bytes = new byte[4096]; + var markerBytes = System.Text.Encoding.Unicode.GetBytes(marker); + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(markerBytes, 0, bytes, 1536, markerBytes.Length); + + return new MemoryStream(bytes); + } + + private static MemoryStream BuildOleLikeStreamWithOffset512Marker(byte[] marker) + { + var bytes = new byte[2048]; + + Array.Copy(OleHeader, 0, bytes, 0, OleHeader.Length); + Array.Copy(marker, 0, bytes, 512, marker.Length); + + return new MemoryStream(bytes); + } + + private static MemoryStream BuildOfficeOpenXmlLikeZipStream(string marker) + { + var bytes = BuildZipLikeBytesWithEocd(0x12345678).ToArray(); + var markerBytes = System.Text.Encoding.ASCII.GetBytes(marker); + + Array.Copy(markerBytes, 0, bytes, 64, markerBytes.Length); + + return new MemoryStream(bytes); + } + + private static MemoryStream BuildZipLikeBytesWithEocd(uint centralDirectoryOffset) + { + var bytes = new byte[512]; + + bytes[0] = 0x50; + bytes[1] = 0x4B; + bytes[2] = 0x03; + bytes[3] = 0x04; + + var eocdStart = bytes.Length - 22; + bytes[eocdStart + 0] = 0x50; + bytes[eocdStart + 1] = 0x4B; + bytes[eocdStart + 2] = 0x05; + bytes[eocdStart + 3] = 0x06; + + bytes[eocdStart + 16] = (byte)(centralDirectoryOffset & 0xFF); + bytes[eocdStart + 17] = (byte)((centralDirectoryOffset >> 8) & 0xFF); + bytes[eocdStart + 18] = (byte)((centralDirectoryOffset >> 16) & 0xFF); + bytes[eocdStart + 19] = (byte)((centralDirectoryOffset >> 24) & 0xFF); + + bytes[eocdStart + 20] = 0x00; + bytes[eocdStart + 21] = 0x00; + + return new MemoryStream(bytes); + } +} + diff --git a/MagicBytesValidator/Formats/Doc.cs b/MagicBytesValidator/Formats/Doc.cs index 3b54a11..3257776 100644 --- a/MagicBytesValidator/Formats/Doc.cs +++ b/MagicBytesValidator/Formats/Doc.cs @@ -4,14 +4,25 @@ namespace MagicBytesValidator.Formats; /// public class Doc : FileByteFilter { + private static readonly byte?[] WordDocumentStreamName = ToUtf16LePattern("WordDocument"); + public Doc() : base( ["application/msword"], ["doc", "dot"] ) { StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) - .SpecificAnyOf([ - new ByteCheck(512, [0xEC, 0xA5, 0xC1, 0x00]) + .AnywhereAnyOf([ + [0xEC, 0xA5, 0xC1, 0x00], + WordDocumentStreamName ]); } + + private static byte?[] ToUtf16LePattern(string value) + { + return System.Text.Encoding.Unicode + .GetBytes(value) + .Select(static currentByte => (byte?)currentByte) + .ToArray(); + } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Xls.cs b/MagicBytesValidator/Formats/Xls.cs index 53aa35e..3127aea 100644 --- a/MagicBytesValidator/Formats/Xls.cs +++ b/MagicBytesValidator/Formats/Xls.cs @@ -4,17 +4,30 @@ namespace MagicBytesValidator.Formats; /// public class Xls : FileByteFilter { + private static readonly byte?[] WorkbookStreamName = ToUtf16LePattern("Workbook"); + private static readonly byte?[] BookStreamName = ToUtf16LePattern("Book"); + public Xls() : base( ["application/msexcel"], ["xls", "xla"] ) { StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) - .SpecificAnyOf([ - new ByteCheck(512, [0xFD, 0xFF, 0xFF, 0xFF, null, 0x00]), - new ByteCheck(512, [0xFD, 0xFF, 0xFF, 0xFF, null, 0x02]), - new ByteCheck(512, [0xFD, 0xFF, 0xFF, 0xFF, 0x20, 0x00, 0x00, 0x00]), - new ByteCheck(512, [0x09, 0x08, 0x10, 0x00, 0x00, 0x06, 0x05, 0x00]) + .AnywhereAnyOf([ + [0xFD, 0xFF, 0xFF, 0xFF, null, 0x00], + [0xFD, 0xFF, 0xFF, 0xFF, null, 0x02], + [0xFD, 0xFF, 0xFF, 0xFF, 0x20, 0x00, 0x00, 0x00], + [0x09, 0x08, 0x10, 0x00, 0x00, 0x06, 0x05, 0x00], + WorkbookStreamName, + BookStreamName ]); } + + private static byte?[] ToUtf16LePattern(string value) + { + return System.Text.Encoding.Unicode + .GetBytes(value) + .Select(static currentByte => (byte?)currentByte) + .ToArray(); + } } \ No newline at end of file diff --git a/MagicBytesValidator/Models/FileByteFilter.cs b/MagicBytesValidator/Models/FileByteFilter.cs index f99ace6..1c0bedf 100644 --- a/MagicBytesValidator/Models/FileByteFilter.cs +++ b/MagicBytesValidator/Models/FileByteFilter.cs @@ -44,18 +44,21 @@ private sealed class FileByteCheck public List Needed { get; } = []; public List AnyOf { get; } = []; public List Anywhere { get; } = []; + public List AnywhereAnyOf { get; } = []; public List TailContains { get; } = []; /* A file matches only if: - every Needed check matches at its fixed offset, - for each AnyOf-group at least one alternative matches, - every Anywhere-pattern occurs somewhere in the stream (null bytes act as wildcards), + - for each AnywhereAnyOf-group at least one anywhere-pattern occurs in the stream, - every TailContains check finds its pattern within the last bytes. */ public bool Matches(byte[] fileByteStream) { return Needed.All(check => CheckBytes(check, fileByteStream)) && AnyOf.All(group => group.Any(check => CheckBytes(check, fileByteStream))) && Anywhere.All(pattern => ContainsPatternAnywhere(pattern, fileByteStream)) + && AnywhereAnyOf.All(group => group.Any(pattern => ContainsPatternAnywhere(pattern, fileByteStream))) && TailContains.All(check => CheckTailContains(check, fileByteStream)); } } @@ -141,6 +144,21 @@ public FileByteFilter Anywhere( return this; } + public FileByteFilter AnywhereAnyOf( + byte?[][] bytesToCheck, + FileByteType? type = null) + { + ArgumentNullException.ThrowIfNull(bytesToCheck); + + if (!bytesToCheck.Any()) + { + throw new ArgumentEmptyException($"{nameof(bytesToCheck)} cannot be null or empty"); + } + + GetChecksByType(type).AnywhereAnyOf.Add(bytesToCheck); + return this; + } + public FileByteFilter Specific( ByteCheck bytesToCheck, FileByteType? type = null)