diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..9cccfd5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + net8.0 + enable + true + latest + + \ No newline at end of file diff --git a/MagicBytesValidator.CLI/Exceptions/InvalidProgramCallException.cs b/MagicBytesValidator.CLI/Exceptions/InvalidProgramCallException.cs new file mode 100644 index 0000000..b020973 --- /dev/null +++ b/MagicBytesValidator.CLI/Exceptions/InvalidProgramCallException.cs @@ -0,0 +1,5 @@ +namespace MagicBytesValidator.CLI.Exceptions; + +public class InvalidProgramCallException : Exception +{ +} \ No newline at end of file diff --git a/MagicBytesValidator.CLI/Imports.cs b/MagicBytesValidator.CLI/Imports.cs new file mode 100644 index 0000000..8b5be96 --- /dev/null +++ b/MagicBytesValidator.CLI/Imports.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using MagicBytesValidator.CLI.Exceptions; +global using MagicBytesValidator.Services; +global using MagicBytesValidator.Services.Streams; \ No newline at end of file diff --git a/MagicBytesValidator.CLI/MagicBytesValidator.CLI.csproj b/MagicBytesValidator.CLI/MagicBytesValidator.CLI.csproj new file mode 100644 index 0000000..3f345e0 --- /dev/null +++ b/MagicBytesValidator.CLI/MagicBytesValidator.CLI.csproj @@ -0,0 +1,12 @@ + + + + enable + exe + + + + + + + diff --git a/MagicBytesValidator.CLI/Program.cs b/MagicBytesValidator.CLI/Program.cs new file mode 100644 index 0000000..cb632ea --- /dev/null +++ b/MagicBytesValidator.CLI/Program.cs @@ -0,0 +1,83 @@ +namespace MagicBytesValidator.CLI; + +public class Program +{ + public static void Main() + { + Console.WriteLine(); + var streamFileTypeProvider = GetStreamFileTypeProvider(); + + try + { + var path = LoadPathFromArgs(); + var file = File.Open(path, FileMode.Open, FileAccess.Read); + + var matches = streamFileTypeProvider + .FindAllMatchesAsync(file, CancellationToken.None) + .GetAwaiter() + .GetResult() + .ToList(); + + if (matches.Count == 0) + { + Console.WriteLine("No matches."); + return; + } + + Console.WriteLine($"{"FileType",-10}| {"Extensions",-40}| {"MIME Types",-80}"); + Console.WriteLine(new string('-', 130)); + + foreach (var match in matches) + { + var mimeTypeList = string.Join(", ", match.MimeTypes); + var extensionList = string.Join(", ", match.Extensions); + + Console.WriteLine($"{match.GetType().Name,-10}| {extensionList,-40}| {mimeTypeList,-80}"); + } + + Console.WriteLine(); + Console.WriteLine($"Unambiguous match: {(matches.Count == 1 ? "Yes" : "No")}"); + } + catch (InvalidProgramCallException) + { + Console.Error.WriteLine( + """ + Usage: dotnet run -- [PATH] + PATH must point to an existing, readable file + """); + } + catch (FileNotFoundException) + { + Console.Error.WriteLine("Error: File not found"); + } + catch (Exception exception) + { + Console.Error.WriteLine( + """ + Error: Internal Exception. + This should not happen. Please file a GitHub issue with the stack trace attached: + """); + Console.Error.WriteLine(exception); + } + } + + private static string LoadPathFromArgs() + { + var args = Environment.GetCommandLineArgs(); + if ( + args is not { Length: 2 } + || args is not [_, var path] + || string.IsNullOrWhiteSpace(path) + ) + { + throw new InvalidProgramCallException(); + } + + return Path.GetFullPath(path); + } + + private static StreamFileTypeProvider GetStreamFileTypeProvider() + { + return new StreamFileTypeProvider(new Mapping()); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Http/FormFileTypeProviderTests.cs b/MagicBytesValidator.Tests/Http/FindFileTypeForFormFile.cs similarity index 51% rename from MagicBytesValidator.Tests/Http/FormFileTypeProviderTests.cs rename to MagicBytesValidator.Tests/Http/FindFileTypeForFormFile.cs index 20d2e0e..0766263 100644 --- a/MagicBytesValidator.Tests/Http/FormFileTypeProviderTests.cs +++ b/MagicBytesValidator.Tests/Http/FindFileTypeForFormFile.cs @@ -1,70 +1,62 @@ -using System.IO; -using System.Linq; -using FluentAssertions; -using MagicBytesValidator.Exceptions.Http; -using MagicBytesValidator.Formats; -using MagicBytesValidator.Services.Http; -using Microsoft.AspNetCore.Http; -using Xunit; +using MagicBytesValidator.Services.Http; namespace MagicBytesValidator.Tests.Http; -public class FormFileTypeProviderTests +public class FindFileTypeForFormFile { [Fact] - public void Should_find_by_extension() + public async Task Should_find_by_extension() { var formFile = ProvideGifFile("trp.gif", "image/gif"); var sut = new FormFileTypeProvider(); - var result = sut.FindFileTypeForFormFile(formFile); + var result = await sut.FindValidatedTypeAsync(formFile, null, CancellationToken.None); result.Should().BeOfType(); } [Fact] - public void Should_find_by_content_type() + public async Task Should_find_by_content_type() { var formFile = ProvideGifFile(string.Empty, "image/gif"); var sut = new FormFileTypeProvider(); - var result = sut.FindFileTypeForFormFile(formFile); + var result = await sut.FindValidatedTypeAsync(formFile, null, CancellationToken.None); result.Should().BeOfType(); } [Fact] - public void Should_return_null_on_not_found() + public async Task Should_return_null_on_not_found() { var formFile = ProvideGifFile(string.Empty, "trp/nms"); var sut = new FormFileTypeProvider(); - var result = sut.FindFileTypeForFormFile(formFile); + var result = await sut.FindValidatedTypeAsync(formFile, null, CancellationToken.None); result.Should().BeNull(); } [Fact] - public void Should_throw_on_mismatch() + public async Task Should_throw_on_mismatch() { var formFile = ProvideGifFile("trp.gif", "image/png"); var sut = new FormFileTypeProvider(); - Assert.Throws(() => sut.FindFileTypeForFormFile(formFile)); + await Assert.ThrowsAsync(async () => await sut.FindValidatedTypeAsync(formFile, null, CancellationToken.None)); } private static IFormFile ProvideGifFile(string name, string contentType) { - var fileTypeGif = new Gif(); - var fileContents = fileTypeGif.MagicByteSequences.First().Concat(new byte[] { 0x11, 0x12 }).ToArray(); - var fileStream = new MemoryStream(fileContents); + byte[] gifSequence = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x11, 0x12]; + var fileStream = new MemoryStream(gifSequence); return new FormFile( - new MemoryStream(fileContents.ToArray()), + new MemoryStream(gifSequence), 0, fileStream.Length, name, diff --git a/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs b/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs new file mode 100644 index 0000000..3e29d3f --- /dev/null +++ b/MagicBytesValidator.Tests/Http/FindValidatedTypeAsync.cs @@ -0,0 +1,105 @@ +namespace MagicBytesValidator.Tests.Http; + +public class FindValidatedTypeAsync +{ + [Fact] + public async Task Should_find_by_extension() + { + var formFile = ProvideGifFile("trp.gif", "image/gif"); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + result.Should().BeOfType(); + } + + [Fact] + public async Task Should_find_by_content_type() + { + var formFile = ProvideGifFile(string.Empty, "image/gif"); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + result.Should().BeOfType(); + } + + [Fact] + public async Task Should_return_null_on_not_found() + { + var formFile = ProvideGifFile(string.Empty, "trp/crly"); + + var sut = new FormFileTypeProvider(); + + var result = await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ); + + result.Should().BeNull(); + } + + [Fact] + public async Task Should_throw_on_type_vs_name_mismatch() + { + var formFile = ProvideGifFile("trp.gif", "image/png"); + + var sut = new FormFileTypeProvider(); + + await Assert.ThrowsAsync(async () => + await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ) + ); + } + + [Fact] + public async Task Should_throw_on_type_vs_content_mismatch() + { + var formFile = ProvideGifFile("trp.png", "image/png"); + + var sut = new FormFileTypeProvider(); + + await Assert.ThrowsAsync(async () => + await sut.FindValidatedTypeAsync( + formFile, + null, + CancellationToken.None + ) + ); + } + + 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(); + var fileStream = new MemoryStream(fileContents.ToArray()); + + return new FormFile( + new MemoryStream(fileContents.ToArray()), + 0, + fileStream.Length, + name, + name + ) + { + Headers = new HeaderDictionary + { + { "Content-Type", contentType } + } + }; + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Imports.cs b/MagicBytesValidator.Tests/Imports.cs new file mode 100644 index 0000000..9cc0473 --- /dev/null +++ b/MagicBytesValidator.Tests/Imports.cs @@ -0,0 +1,17 @@ +// Global using directives + +global using System; +global using System.IO; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using FluentAssertions; +global using MagicBytesValidator.Exceptions.Http; +global using MagicBytesValidator.Formats; +global using MagicBytesValidator.Models; +global using MagicBytesValidator.Services; +global using MagicBytesValidator.Services.Http; +global using MagicBytesValidator.Services.Streams; +global using Microsoft.AspNetCore.Http; +global using Moq; +global using Xunit; \ No newline at end of file diff --git a/MagicBytesValidator.Tests/IsValidTests.cs b/MagicBytesValidator.Tests/IsValidTests.cs deleted file mode 100644 index 1c73344..0000000 --- a/MagicBytesValidator.Tests/IsValidTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using MagicBytesValidator.Formats; -using MagicBytesValidator.Models; -using MagicBytesValidator.Services; -using Xunit; - -namespace MagicBytesValidator.Tests; - -public class IsValidTests -{ - private readonly Validator _validator; - private readonly FileType _fileTypeGif; - private readonly FileType _fileTypePng; - private readonly FileType _fileTypeZip; - private readonly MemoryStream _gifMemoryStream; - private readonly MemoryStream _zipMemoryStream; - private readonly Gif _gif; - private readonly Png _png; - private readonly Zip _zip; - - public IsValidTests() - { - _gif = new Gif(); - _png = new Png(); - _zip = new Zip(); - _gifMemoryStream = new MemoryStream(); - _zipMemoryStream = new MemoryStream(); - - _validator = new Validator(); - _fileTypeGif = _validator.Mapping.FindByExtension(_gif.Extensions[0]) ?? throw new NullReferenceException(); - _fileTypePng = _validator.Mapping.FindByMimeType(_png.MimeTypes.First()) ?? throw new NullReferenceException(); - _fileTypeZip = _validator.Mapping.FindByMimeType(_zip.MimeTypes.First()) ?? throw new NullReferenceException(); - } - - [Fact] - public async Task Should_validate_gif() - { - await _gifMemoryStream.WriteAsync(_gif.MagicByteSequences[0]); - - // Act - var valid = await _validator.IsValidAsync(_gifMemoryStream, _fileTypeGif, CancellationToken.None); - - // Assert - valid.Should().BeTrue(); - } - - [Fact] - public async Task Should_validate_zip() - { - await _zipMemoryStream.WriteAsync(_zip.MagicByteSequences[0]); - - // Act - var valid = await _validator.IsValidAsync(_zipMemoryStream, _fileTypeZip, CancellationToken.None); - - // Assert - valid.Should().BeTrue(); - } - - [Fact] - public async Task Should_fail_incorrect_extension() - { - await _gifMemoryStream.WriteAsync(_gif.MagicByteSequences[0]); - - // Act - var invalidExtension = await _validator.IsValidAsync(_gifMemoryStream, _fileTypePng, CancellationToken.None); - - // Assert - invalidExtension.Should().BeFalse(); - } - - [Fact] - public async Task Should_fail_incorrect_mimetype() - { - await _gifMemoryStream.WriteAsync(_gif.MagicByteSequences[0]); - - // Act - var inValidMimeType = await _validator.IsValidAsync(_gifMemoryStream, _fileTypePng, CancellationToken.None); - - // Assert - inValidMimeType.Should().BeFalse(); - } - - [Fact] - public async Task Should_work_with_offset() - { - var stream = new MemoryStream(); - await stream.WriteAsync(new byte[] { 0, 0, 0, 32, 102, 116, 121, 112, 109, 112, 52, 50, 0, 0, 0, 0, 109 }); - - // Act - var isValidMimeType = await _validator.IsValidAsync(stream, new Mp4(), CancellationToken.None); - - // Assert - isValidMimeType.Should().BeTrue(); - } - - [Fact] - public async Task Should_fail_incorrect_magicByte_sequence() - { - await _gifMemoryStream.WriteAsync(_png.MagicByteSequences[0]); - - // Act - var invalidMagicByte = await _validator.IsValidAsync(_gifMemoryStream, _fileTypeGif, CancellationToken.None); - - // Assert - invalidMagicByte.Should().BeFalse(); - } -} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/MagicBytesValidator.Tests.csproj b/MagicBytesValidator.Tests/MagicBytesValidator.Tests.csproj index 89723cd..6d7e2c6 100644 --- a/MagicBytesValidator.Tests/MagicBytesValidator.Tests.csproj +++ b/MagicBytesValidator.Tests/MagicBytesValidator.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - latest - enable - true false diff --git a/MagicBytesValidator.Tests/MappingRegister.cs b/MagicBytesValidator.Tests/MappingRegister.cs new file mode 100644 index 0000000..df535ba --- /dev/null +++ b/MagicBytesValidator.Tests/MappingRegister.cs @@ -0,0 +1,55 @@ +namespace MagicBytesValidator.Tests; + +public class MappingRegister +{ + private readonly Mapping _mapping = new(); + + private readonly IFileType _trpFileType = new FileByteFilter( + ["traperto/trp"], + ["trp"] + ).StartsWith([0x74, 0x72, 0x61, 0x70, 0x65, 0x72, 0x74, 0x6f]); + + [Fact] + public void Should_register_single_filetype() + { + _mapping.Register(_trpFileType); + _mapping.FileTypes.Should().Contain(_trpFileType); + } + + [Fact] + public void Should_register_list_filetype() + { + var neonJsFileType = new FileByteFilter( + ["traperto/niklasschmidt"], + ["nms"] + ).StartsWith([0x6e, 0x69, 0x6b, 0x6c, 0x61, 0x73, 0x73, 0x63, 0x68, 0x6d, 0x69, 0x64, 0x74]); + + var kryptobiFileType = new FileByteFilter( + ["traperto/tobiasjanssen"], + ["tjn"] + ).StartsWith([0x74, 0x6f, 0x62, 0x69, 0x61, 0x73, 0x6a, 0x61, 0x6e, 0x73, 0x73, 0x65, 0x6e]); + + _mapping.Register(new[] { neonJsFileType, kryptobiFileType }); + _mapping.FileTypes.Should().Contain(neonJsFileType).And.Contain(kryptobiFileType); + } + + [Fact] + public void Should_register_assembly_fileTypes() + { + var assembly = typeof(AssemblyFacade).Assembly; + _mapping.Register(assembly); + + _mapping.FileTypes.Should().Contain(f => f.MimeTypes.Contains("facade/trp")); + } +} + +public class AssemblyFacade : FileByteFilter +{ + public AssemblyFacade() : base( + ["facade/trp"], + ["trp"] + ) + { + StartsWith([0x74, 0x72, 0x70]); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Models/FileByteFilterMatches.cs b/MagicBytesValidator.Tests/Models/FileByteFilterMatches.cs new file mode 100644 index 0000000..c799e55 --- /dev/null +++ b/MagicBytesValidator.Tests/Models/FileByteFilterMatches.cs @@ -0,0 +1,134 @@ +namespace MagicBytesValidator.Tests.Models; + +public class FileByteFilterMatches +{ + [Fact] + public void Should_match_pdf() + { + var pdf = new Pdf(); + + var pdfTestData = "%PDF-\n%%EOF\n"u8.ToArray(); + + pdf.Matches(pdfTestData).Should().BeTrue(); + } + + [Fact] + public void Should_not_match_pdf() + { + var pdf = new Pdf(); + + var pdfTestData = "%PDDF-\n%%EEOF\n"u8.ToArray(); + + pdf.Matches(pdfTestData).Should().BeFalse(); + } + + [Fact] + public void Should_match_ppt() + { + var pdf = new Ppt(); + + // We need to check for an offset of 512 + var pdfTestData = new byte[520]; + var startingData = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; + var offsetData = new byte[] { 0xFD, 0xFF, 0xFF, 0xFF, 0x53, 0x00, 0x00, 0x00 }; + + // Start of ppt file + for (var startIndex = 0; startIndex < startingData.Length; startIndex++) + { + pdfTestData[startIndex] = startingData[startIndex]; + } + + // Content of ppt file at offset 512 + for (var endIndex = 0; endIndex < offsetData.Length; endIndex++) + { + pdfTestData[endIndex + 512] = offsetData[endIndex]; + } + + pdf.Matches(pdfTestData).Should().BeTrue(); + } + + [Fact] + public void Should_not_match_offset_ppt() + { + // Valid Start but Invalid offset Data + var pdf = new Ppt(); + + // We need to check for an offset of 512 + var pdfTestData = new byte[520]; + var startingData = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; + var offsetData = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // Start of ppt file + for (var startIndex = 0; startIndex < startingData.Length; startIndex++) + { + pdfTestData[startIndex] = startingData[startIndex]; + } + + // Content of ppt file at offset 512 + for (var endIndex = 0; endIndex < offsetData.Length; endIndex++) + { + pdfTestData[endIndex + 512] = offsetData[endIndex]; + } + + pdf.Matches(pdfTestData).Should().BeFalse("Starting data correct but data at offset 512 invalid"); + } + + [Fact] + public void Should_not_match_start_ppt() + { + var pdf = new Ppt(); + + // We need to check for an offset of 512 + var pdfTestData = new byte[520]; + var startingData = new byte[] { 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD }; + var offsetData = new byte[] { 0xFD, 0xFF, 0xFF, 0xFF, 0x53, 0x00, 0x00, 0x00 }; + + // Start of ppt file + for (var startIndex = 0; startIndex < startingData.Length; startIndex++) + { + pdfTestData[startIndex] = startingData[startIndex]; + } + + // Content of ppt file at offset 512 + for (var endIndex = 0; endIndex < offsetData.Length; endIndex++) + { + pdfTestData[endIndex + 512] = offsetData[endIndex]; + } + + pdf.Matches(pdfTestData).Should().BeFalse("Offset data valid but incorrect starting data"); + } + + [Fact] + public void Should_match_xlsx() + { + var xlsx = new Xlsx(); + + // Some random data at start and end, xlsx looks for specific bytes anywhere in the file + // random parts are marked with 0xFF + var xlsxTestData = new byte[] + { + 0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00, 0xFF, 0xFF, 0xFF, 0x78, 0x6c, + 0x2f, 0x5f, 0x72, 0x65, 0x6c, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x6f, + 0x6f, 0x6b, 0x2e, 0x78, 0x6d, 0x6c, 0x2e, 0x72, 0x65, 0x6c, 0x73, 0xFF, 0xFF, + 0x50, 0x4B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00 + }; + + xlsx.Matches(xlsxTestData).Should().BeTrue(); + } + + [Fact] + public void Should_not_match_xlsx() + { + var xlsx = new Xlsx(); + + var xlsxTestData = new byte[] + { + 0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00, 0xFF, 0xFF, 0xFF, 0x78, 0x6c, + 0x2f, 0x5f, 0x72, 0x65, 0x6c, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x6f, + 0x6f, 0x6b, 0x2e, 0x78, 0xFF, 0xFF, 0xFF, 0x72, 0x65, 0x6c, 0x73, 0xFF, 0xFF + }; + + xlsx.Matches(xlsxTestData).Should().BeFalse("specific byte array has invalid bytes"); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Models/FileTypeWithIncompleteStartSequencesMatches.cs b/MagicBytesValidator.Tests/Models/FileTypeWithIncompleteStartSequencesMatches.cs new file mode 100644 index 0000000..0f96897 --- /dev/null +++ b/MagicBytesValidator.Tests/Models/FileTypeWithIncompleteStartSequencesMatches.cs @@ -0,0 +1,26 @@ +namespace MagicBytesValidator.Tests.Models; + +public class FileTypeWithIncompleteStartSequencesMatches +{ + [Fact] + public void Should_match_valid_file() + { + var sut = new Aif(); + + var testDataOne = new byte[] { 0x46, 0x4F, 0x52, 0x4D, 0x11, 0x12, 0x19, 0x98, 0x41, 0x49, 0x46, 0x46, 0x11 }; + var testDataTwo = new byte[] { 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x01, 0x02, 0x03, 0x41, 0x49, 0x46, 0x46, 0x11 }; + + sut.Matches(testDataOne).Should().BeTrue(); + sut.Matches(testDataTwo).Should().BeTrue(); + } + + [Fact] + public void Should_not_match_invalid_file() + { + var sut = new Aif(); + + var testData = new byte[] { 0x00, 0x4F, 0x52, 0x4D, 0x00, 0x12, 0x19, 0x98, 0x41, 0x49, 0x46, 0x46, 0x11 }; + + sut.Matches(testData).Should().BeFalse(); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Models/ZipMatches.cs b/MagicBytesValidator.Tests/Models/ZipMatches.cs new file mode 100644 index 0000000..1a74242 --- /dev/null +++ b/MagicBytesValidator.Tests/Models/ZipMatches.cs @@ -0,0 +1,26 @@ +namespace MagicBytesValidator.Tests.Models; + +public class ZipMatches +{ + private static readonly byte[] EmptyZipFileContent = + { + 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x84, 0x55, 0x57, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x20, 0x00, 0x54, 0x65, 0x73, 0x74, 0x2f, 0x55, + 0x54, 0x0d, 0x00, 0x07, 0xf4, 0xe1, 0x33, 0x65, 0xf4, 0xe1, 0x33, 0x65, 0xf7, 0xe1, 0x33, 0x65, 0x75, 0x78, + 0x0b, 0x00, 0x01, 0x04, 0xf5, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x01, 0x02, 0x14, + 0x03, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x84, 0x55, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xed, + 0x41, 0x00, 0x00, 0x00, 0x00, 0x54, 0x65, 0x73, 0x74, 0x2f, 0x55, 0x54, 0x0d, 0x00, 0x07, 0xf4, 0xe1, 0x33, + 0x65, 0xf4, 0xe1, 0x33, 0x65, 0xf7, 0xe1, 0x33, 0x65, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0xf5, 0x01, 0x00, + 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + [Fact] + public void Should_match_valid_zip() + { + var sut = new Zip(); + + sut.Matches(EmptyZipFileContent).Should().BeTrue(); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/RegisterTests.cs b/MagicBytesValidator.Tests/RegisterTests.cs deleted file mode 100644 index 0ca28ac..0000000 --- a/MagicBytesValidator.Tests/RegisterTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Linq; -using FluentAssertions; -using MagicBytesValidator.Models; -using MagicBytesValidator.Services; -using Xunit; - -namespace MagicBytesValidator.Tests; - -public class RegisterTests -{ - private readonly Mapping _mapping = new(); - - private readonly FileType _trpFileType = new( - new[] { "traperto/trp" }, - new[] { "trp" }, - new[] - { - new byte[] { 0x74, 0x72, 0x61, 0x70, 0x65, 0x72, 0x74, 0x6f } - } - ); - - [Fact] - public void Should_register_single_filetype() - { - _mapping.Register(_trpFileType); - _mapping.FileTypes.Should().Contain(_trpFileType); - } - - [Fact] - public void Should_register_list_filetype() - { - var neonJsFileType = new FileType( - new[] { "traperto/niklasschmidt" }, - new[] { "nms" }, - new[] - { - new byte[] - { - 0x6e, 0x69, 0x6b, 0x6c, 0x61, 0x73, 0x73, 0x63, 0x68, 0x6d, 0x69, - 0x64, 0x74 - } - } - ); - - var kryptobiFileType = new FileType( - new[] { "traperto/tobiasjanssen" }, - new[] { "tjn" }, - new[] - { - new byte[] - { - 0x74, 0x6f, 0x62, 0x69, 0x61, 0x73, 0x6a, 0x61, 0x6e, 0x73, - 0x73, 0x65, 0x6e - } - } - ); - - _mapping.Register(new[] { neonJsFileType, kryptobiFileType }); - _mapping.FileTypes.Should().Contain(neonJsFileType).And.Contain(kryptobiFileType); - } - - [Fact] - public void Should_register_assembly_fileTypes() - { - var assembly = typeof(AssemblyFacade).Assembly; - _mapping.Register(assembly); - - _mapping.FileTypes.Should().Contain(f => f.MimeTypes.Contains("facade/trp")); - } -} - -public class AssemblyFacade : FileType -{ - public AssemblyFacade() : base( - new[] { "facade/trp" }, - new[] { "trp" }, - new[] - { - new byte[] { 0x74, 0x72, 0x70 } - } - ) - { - } -} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Streams/FindAllMatchesAsync.cs b/MagicBytesValidator.Tests/Streams/FindAllMatchesAsync.cs new file mode 100644 index 0000000..b70ed1b --- /dev/null +++ b/MagicBytesValidator.Tests/Streams/FindAllMatchesAsync.cs @@ -0,0 +1,127 @@ +namespace MagicBytesValidator.Tests.Streams; + +public class FindAllMatchesAsync +{ + [Fact] + public async Task Should_find_all_by_magic_byte_sequence() + { + var matchingFileType1 = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); + + var matchingFileType2 = new FileByteFilter( + ["also/matching"], + ["mtch2"] + ).StartsWithAnyOf([ + [0x11, 0x12], + [0x11, 0x22, 0x44, 0x55] + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType1, matchingFileType2 }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x11, 0x12, 0x18 }); + + var result = (await sut.FindAllMatchesAsync(stream, CancellationToken.None)) + .ToList(); + + result.Should().HaveCount(2); + result.Should().Contain(matchingFileType1); + result.Should().Contain(matchingFileType2); + } + + [Fact] + public async Task Should_reset_stream_position() + { + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x11, 0x12, 0x18 }) + { + Position = 1 + }; + + _ = await sut.FindAllMatchesAsync(stream, CancellationToken.None); + + stream.Position.Should().Be(1); + } + + [Fact] + public async Task Should_handle_unknown_file_type() + { + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { mismatchingFileType }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x12, 0x11 }); + + var result = await sut.FindAllMatchesAsync(stream, CancellationToken.None); + + result.Should().BeEmpty(); + } + + [Fact] + public async Task Should_handle_empty_mapping() + { + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x12, 0x11 }); + + var result = await sut.FindAllMatchesAsync(stream, CancellationToken.None); + + result.Should().BeEmpty(); + } + + [Fact] + public async Task Should_throw_on_null_given() + { + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); + + var sut = new StreamFileTypeProvider(mapping.Object); + + await Assert.ThrowsAsync( + async () => await sut.FindAllMatchesAsync(null!, CancellationToken.None) + ); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Streams/StreamFileTypeProviderTests.cs b/MagicBytesValidator.Tests/Streams/FindByMagicByteSequenceAsync.cs similarity index 50% rename from MagicBytesValidator.Tests/Streams/StreamFileTypeProviderTests.cs rename to MagicBytesValidator.Tests/Streams/FindByMagicByteSequenceAsync.cs index 48b5226..5bdff3f 100644 --- a/MagicBytesValidator.Tests/Streams/StreamFileTypeProviderTests.cs +++ b/MagicBytesValidator.Tests/Streams/FindByMagicByteSequenceAsync.cs @@ -1,46 +1,33 @@ -using System.Threading.Tasks; -using MagicBytesValidator.Models; -using MagicBytesValidator.Services; -using Xunit; -using Moq; -using MagicBytesValidator.Services.Streams; -using System.Threading; -using System.IO; -using FluentAssertions; -using System; +#pragma warning disable CS0618 // Type or member is obsolete namespace MagicBytesValidator.Tests.Streams; -public class StreamFileTypeProviderTests +public class FindByMagicByteSequenceAsync { [Fact] public async Task Should_find_by_magic_byte_sequence() { - var matchingFileType = new FileType( - new[] { "matching" }, - new[] { "mtch" }, - new[] - { - new byte[] { 0x11, 0x12, 0x19, 0x20 }, - new byte[] { 0x11, 0x12, 0x18 }, - new byte[] { 0x11, 0x12 }, - } - ); - - var mismatchingFileType = new FileType( - new[] { "mismatching" }, - new[] { "mism" }, - new[] - { - new byte[] { 0x11, 0x22 }, - new byte[] { 0x11, 0x22, 0x44, 0x55 } - } - ); + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); + + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { matchingFileType, mismatchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType, mismatchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -55,21 +42,19 @@ public async Task Should_find_by_magic_byte_sequence() [Fact] public async Task Should_reset_stream_position() { - var matchingFileType = new FileType( - new[] { "matching" }, - new[] { "mtch" }, - new[] - { - new byte[] { 0x11, 0x12, 0x19, 0x20 }, - new byte[] { 0x11, 0x12, 0x18 }, - new byte[] { 0x11, 0x12 }, - } - ); + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { matchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -87,20 +72,18 @@ public async Task Should_reset_stream_position() [Fact] public async Task Should_handle_unknown_file_type() { - var mismatchingFileType = new FileType( - new[] { "mismatching" }, - new[] { "mism" }, - new[] - { - new byte[] { 0x11, 0x22 }, - new byte[] { 0x11, 0x22, 0x44, 0x55 } - } - ); + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { mismatchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { mismatchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -116,8 +99,8 @@ public async Task Should_handle_empty_mapping() { var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(Array.Empty()); + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); var sut = new StreamFileTypeProvider(mapping.Object); @@ -133,8 +116,8 @@ public async Task Should_throw_on_null_given() { var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(Array.Empty()); + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); var sut = new StreamFileTypeProvider(mapping.Object); @@ -143,38 +126,26 @@ await Assert.ThrowsAsync( ); } - - [Fact] public async Task Should_find_by_magic_byte_sequence_with_offset() { - var matchingFileType = new FileType( - new[] { "matching" }, - new[] { "mtch" }, - new[] - { - new byte[] { 0x11, 0x12, 0x19, 0x20 }, - new byte[] { 0x11, 0x12, 0x18 }, - new byte[] { 0x11, 0x12 }, - }, - 2 - ); - - var mismatchingFileType = new FileType( - new[] { "mismatching" }, - new[] { "mism" }, - new[] - { - new byte[] { 0x11, 0x22 }, - new byte[] { 0x11, 0x22, 0x44, 0x55 } - }, - 2 - ); + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).Anywhere([0x11, 0x12, 0x18]); + + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22, 0xFF], + [0x11, 0x22, 0x44, 0x55] + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { matchingFileType, mismatchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType, mismatchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -189,21 +160,17 @@ public async Task Should_find_by_magic_byte_sequence_with_offset() [Fact] public async Task Should_handle_unknown_file_type_by_offset_in_type() { - var mismatchingFileType = new FileType( - new[] { "mismatching" }, - new[] { "mism" }, - new[] - { - new byte[] { 0x11, 0x22 }, - new byte[] { 0x11, 0x22, 0x44, 0x55 } - }, - 2 - ); + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22, 0x44, 0x55] + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { mismatchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { mismatchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -217,20 +184,18 @@ public async Task Should_handle_unknown_file_type_by_offset_in_type() [Fact] public async Task Should_handle_unknown_file_type_by_offset_in_stream() { - var mismatchingFileType = new FileType( - new[] { "mismatching" }, - new[] { "mism" }, - new[] - { - new byte[] { 0x11, 0x22 }, - new byte[] { 0x11, 0x22, 0x44, 0x55 } - } - ); + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); var mapping = new Mock(); mapping - .SetupGet(m => m.FileTypes) - .Returns(new[] { mismatchingFileType }); + .SetupGet(m => m.FileTypes) + .Returns(new[] { mismatchingFileType }); var sut = new StreamFileTypeProvider(mapping.Object); @@ -240,4 +205,5 @@ public async Task Should_handle_unknown_file_type_by_offset_in_stream() result.Should().BeNull(); } -} \ No newline at end of file +} +#pragma warning restore CS0618 // Type or member is obsolete \ No newline at end of file diff --git a/MagicBytesValidator.Tests/Streams/TryFindUnambiguousAsync.cs b/MagicBytesValidator.Tests/Streams/TryFindUnambiguousAsync.cs new file mode 100644 index 0000000..b336160 --- /dev/null +++ b/MagicBytesValidator.Tests/Streams/TryFindUnambiguousAsync.cs @@ -0,0 +1,126 @@ +namespace MagicBytesValidator.Tests.Streams; + +public class TryFindUnambiguousAsync +{ + [Fact] + public async Task Should_find_by_magic_byte_sequence() + { + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); + + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType, mismatchingFileType }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x11, 0x12, 0x18 }); + + var result = await sut.TryFindUnambiguousAsync(stream, CancellationToken.None); + + result.Should().Be(matchingFileType); + result.Should().NotBe(mismatchingFileType); + } + + [Fact] + public async Task Should_reset_stream_position() + { + var matchingFileType = new FileByteFilter( + ["matching"], + ["mtch"] + ).StartsWithAnyOf([ + [0x11, 0x12, 0x19, 0x20], + [0x11, 0x12, 0x18], + [0x11, 0x12], + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { matchingFileType }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x11, 0x12, 0x18 }) + { + Position = 1 + }; + + var result = await sut.TryFindUnambiguousAsync(stream, CancellationToken.None); + + result.Should().Be(matchingFileType); + stream.Position.Should().Be(1); + } + + [Fact] + public async Task Should_handle_unknown_file_type() + { + var mismatchingFileType = new FileByteFilter( + ["mismatching"], + ["mism"] + ).StartsWithAnyOf([ + [0x11, 0x22], + [0x11, 0x22, 0x44, 0x55] + ]); + + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(new[] { mismatchingFileType }); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x12, 0x11 }); + + var result = await sut.TryFindUnambiguousAsync(stream, CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task Should_handle_empty_mapping() + { + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); + + var sut = new StreamFileTypeProvider(mapping.Object); + + var stream = new MemoryStream(new byte[] { 0x12, 0x11 }); + + var result = await sut.TryFindUnambiguousAsync(stream, CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task Should_throw_on_null_given() + { + var mapping = new Mock(); + mapping + .SetupGet(m => m.FileTypes) + .Returns(Array.Empty()); + + var sut = new StreamFileTypeProvider(mapping.Object); + + await Assert.ThrowsAsync( + async () => await sut.TryFindUnambiguousAsync(null!, CancellationToken.None) + ); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.Tests/ValidatorIsValidAsync.cs b/MagicBytesValidator.Tests/ValidatorIsValidAsync.cs new file mode 100644 index 0000000..430ebac --- /dev/null +++ b/MagicBytesValidator.Tests/ValidatorIsValidAsync.cs @@ -0,0 +1,75 @@ +namespace MagicBytesValidator.Tests; + +public class ValidatorIsValidAsync +{ + private readonly Validator _validator; + private readonly IFileType _fileTypeGif; + private readonly IFileType _fileTypePng; + private readonly MemoryStream _gifMemoryStream; + private readonly MemoryStream _pngMemoryStream; + + public ValidatorIsValidAsync() + { + var gif = new Gif(); + var png = new Png(); + _gifMemoryStream = new MemoryStream([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); + _pngMemoryStream = new MemoryStream([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + + _validator = new Validator(); + _fileTypeGif = _validator.Mapping.FindByExtension(gif.Extensions[0]) ?? throw new NullReferenceException(); + _fileTypePng = _validator.Mapping.FindByMimeType(png.MimeTypes.First()) ?? throw new NullReferenceException(); + } + + [Fact] + public async Task Should_validate_gif() + { + // Act + var valid = await _validator.IsValidAsync(_gifMemoryStream, _fileTypeGif, CancellationToken.None); + + // Assert + valid.Should().BeTrue(); + } + + [Fact] + public async Task Should_fail_incorrect_extension() + { + // Act + var invalidExtension = await _validator.IsValidAsync(_gifMemoryStream, _fileTypePng, CancellationToken.None); + + // Assert + invalidExtension.Should().BeFalse(); + } + + [Fact] + public async Task Should_fail_incorrect_mimetype() + { + // Act + var inValidMimeType = await _validator.IsValidAsync(_gifMemoryStream, _fileTypePng, CancellationToken.None); + + // Assert + inValidMimeType.Should().BeFalse(); + } + + [Fact] + public async Task Should_work_with_offset() + { + var stream = new MemoryStream(); + await stream.WriteAsync(new byte[] { 0, 0, 0, 32, 102, 116, 121, 112, 109, 112, 52, 50, 0, 0, 0, 0, 109 }); + + // Act + var isValidMimeType = await _validator.IsValidAsync(stream, new Mp4(), CancellationToken.None); + + // Assert + isValidMimeType.Should().BeTrue(); + } + + [Fact] + public async Task Should_fail_incorrect_magicByte_sequence() + { + // Act + var invalidMagicByte = await _validator.IsValidAsync(_pngMemoryStream, _fileTypeGif, CancellationToken.None); + + // Assert + invalidMagicByte.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/MagicBytesValidator.sln b/MagicBytesValidator.sln index ed76c0b..c0b9902 100644 --- a/MagicBytesValidator.sln +++ b/MagicBytesValidator.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicBytesValidator", "Magi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicBytesValidator.Tests", "MagicBytesValidator.Tests\MagicBytesValidator.Tests.csproj", "{B23AB832-C861-448A-A44F-DC46E1884FB0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicBytesValidator.CLI", "MagicBytesValidator.CLI\MagicBytesValidator.CLI.csproj", "{1B36073D-9033-4E57-82F8-37810880450D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {B23AB832-C861-448A-A44F-DC46E1884FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B23AB832-C861-448A-A44F-DC46E1884FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B23AB832-C861-448A-A44F-DC46E1884FB0}.Release|Any CPU.Build.0 = Release|Any CPU + {1B36073D-9033-4E57-82F8-37810880450D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B36073D-9033-4E57-82F8-37810880450D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B36073D-9033-4E57-82F8-37810880450D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B36073D-9033-4E57-82F8-37810880450D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/MagicBytesValidator.sln.DotSettings b/MagicBytesValidator.sln.DotSettings index c6eba80..394adcb 100644 --- a/MagicBytesValidator.sln.DotSettings +++ b/MagicBytesValidator.sln.DotSettings @@ -1,9 +1,12 @@  True + True + True True True True True + True True True True diff --git a/MagicBytesValidator/Exceptions/ArgumentEmptyException.cs b/MagicBytesValidator/Exceptions/ArgumentEmptyException.cs index 2a99c09..fb370d0 100644 --- a/MagicBytesValidator/Exceptions/ArgumentEmptyException.cs +++ b/MagicBytesValidator/Exceptions/ArgumentEmptyException.cs @@ -1,6 +1,4 @@ -using System; - -namespace MagicBytesValidator.Exceptions; +namespace MagicBytesValidator.Exceptions; /// /// Exception that can be thrown if a given argument is empty or contains an empty value. diff --git a/MagicBytesValidator/Exceptions/DuplicateEntryException.cs b/MagicBytesValidator/Exceptions/DuplicateEntryException.cs deleted file mode 100644 index 2a0d9cf..0000000 --- a/MagicBytesValidator/Exceptions/DuplicateEntryException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace MagicBytesValidator.Exceptions; - -/// -/// Exception that can be thrown if something would result in a duplicate entry if added. -/// -public class DuplicateEntryException : Exception -{ - /// - /// Creates a new DuplicateEntryException. - /// - /// Name of the parameter that would cause a duplicate entry. - public DuplicateEntryException(string parameterName) : base( - $"Value of {parameterName} would result in a duplicate entry." - ) - { - } -} \ No newline at end of file diff --git a/MagicBytesValidator/Exceptions/Http/MimeTypeMismatchException.cs b/MagicBytesValidator/Exceptions/Http/MimeTypeMismatchException.cs index 3d7ba8f..6c354bf 100644 --- a/MagicBytesValidator/Exceptions/Http/MimeTypeMismatchException.cs +++ b/MagicBytesValidator/Exceptions/Http/MimeTypeMismatchException.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace MagicBytesValidator.Exceptions.Http; +namespace MagicBytesValidator.Exceptions.Http; /// /// Exception that can be thrown if two MIME types (that should be equal) are different. @@ -14,4 +11,10 @@ string mimeType2 ) : base($"Mismatch of MIME types ('{mimeType2}' not in ['{string.Join(",", mimeTypes)}'].)") { } + + public MimeTypeMismatchException( + string declaredMimeType + ) : base($"Mismatch of MIME types ('{declaredMimeType}' does not represent content.)") + { + } } \ No newline at end of file diff --git a/MagicBytesValidator/Extensions/EnumerableExtensions.cs b/MagicBytesValidator/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..52b5fbd --- /dev/null +++ b/MagicBytesValidator/Extensions/EnumerableExtensions.cs @@ -0,0 +1,9 @@ +namespace MagicBytesValidator.Extensions; + +public static class EnumerableExtensions +{ + public static IEnumerable<(T, int)> AsIndexed(this IEnumerable enumerable) + { + return enumerable.Select((v, i) => (v, i)); + } +} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Aif.cs b/MagicBytesValidator/Formats/Aif.cs index 01ce824..2e13ef5 100644 --- a/MagicBytesValidator/Formats/Aif.cs +++ b/MagicBytesValidator/Formats/Aif.cs @@ -1,17 +1,13 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Aif : FileType +/// +public class Aif : FileByteFilter { public Aif() : base( - new[] { "audio/x-aiff" }, - new[] { "aif", "aiff", "aifc" }, - new[] - { - new byte[] { 65, 73, 70, 70 } - } + ["audio/x-aiff"], + ["aif", "aiff", "aifc"] ) { + StartsWith([0x46, 0x4F, 0x52, 0x4D, null, null, null, null, 0x41, 0x49, 0x46, 0x46]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Bin.cs b/MagicBytesValidator/Formats/Bin.cs index 982f0f8..b615839 100644 --- a/MagicBytesValidator/Formats/Bin.cs +++ b/MagicBytesValidator/Formats/Bin.cs @@ -1,19 +1,18 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Bin : FileType +// TODO: Check if correct + +public class Bin : FileByteFilter { public Bin() : base( - new[] { "application/octet-stream" }, - new[] { "bin", "file", "com", "class", "ini" }, - new[] - { - new byte[] { 83, 80, 48, 49 }, - new byte[] { 201 }, - new byte[] { 202, 254, 186, 190 } - } + ["application/octet-stream"], + ["bin", "file", "com", "class", "ini"] ) { + StartsWithAnyOf([ + [0x53, 0x50, 0x30, 0x31], + [0xC9], + [0xCA, 0xFE, 0xBA, 0xBE] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Bmp.cs b/MagicBytesValidator/Formats/Bmp.cs index bdef949..bc4e3be 100644 --- a/MagicBytesValidator/Formats/Bmp.cs +++ b/MagicBytesValidator/Formats/Bmp.cs @@ -1,17 +1,14 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Bmp : FileType +/// +/// +public class Bmp : FileByteFilter { public Bmp() : base( - new[] { "image/bmp" }, - new[] { "bmp" }, - new[] - { - new byte[] { 66, 77 } - } + ["image/bmp"], + ["bmp"] ) { + StartsWith([0x42, 0x4D]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Cab.cs b/MagicBytesValidator/Formats/Cab.cs index 664a222..08809ec 100644 --- a/MagicBytesValidator/Formats/Cab.cs +++ b/MagicBytesValidator/Formats/Cab.cs @@ -1,19 +1,19 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Cab : FileType +/// +/// +public class Cab : FileByteFilter { public Cab() : base( - new[] { "application/x-shockwave-flash" }, - new[] { "cab", "swf" }, - new[] - { - new byte[] { 77, 83, 67, 70 }, - new byte[] { 67, 87, 83 }, - new byte[] { 73, 83, 99, 40 } - } + ["application/vnd.ms-cab-compressed", "application/x-cab-compressed"], + ["cab"] ) { + StartsWithAnyOf( + [ + [0x49, 0x53, 0x63, 0x28], + [0x4D, 0x53, 0x43, 0x46] + ] + ); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Doc.cs b/MagicBytesValidator/Formats/Doc.cs index a4e4532..240b93e 100644 --- a/MagicBytesValidator/Formats/Doc.cs +++ b/MagicBytesValidator/Formats/Doc.cs @@ -1,17 +1,16 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Doc : FileType +// TODO: Add sub header check (512 byte offset: EC A5 C1 00) + +/// +/// +public class Doc : FileByteFilter { public Doc() : base( - new[] { "application/msword" }, - new[] { "doc", "dot" }, - new[] - { - new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 } - } + ["application/msword"], + ["doc", "dot"] ) { + StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Docx.cs b/MagicBytesValidator/Formats/Docx.cs index 27312f5..dbdb55e 100644 --- a/MagicBytesValidator/Formats/Docx.cs +++ b/MagicBytesValidator/Formats/Docx.cs @@ -1,19 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Docx : FileType +/// +/// +public class Docx : Zip { public Docx() : base( - new[] { "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, - new[] { "docx" }, - new[] - { - new byte[] { 80, 75, 3, 4 }, - new byte[] { 80, 75, 5, 6 }, - new byte[] { 80, 75, 7, 8 } - } - ) + ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"], + ["docx"]) { + StartsWith([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00]) + .Anywhere([ + 0x77, 0x6F, 0x72, 0x64, 0x2F, 0x5F, 0x72, 0x65, 0x6C, 0x73, 0x2F, 0x64, 0x6F, + 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x78, 0x6D, 0x6C, 0x2E, 0x72, 0x65, 0x6C, 0x73 + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Dxr.cs b/MagicBytesValidator/Formats/Dxr.cs index c7995ac..b007468 100644 --- a/MagicBytesValidator/Formats/Dxr.cs +++ b/MagicBytesValidator/Formats/Dxr.cs @@ -1,17 +1,16 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Dxr : FileType +/// +public class Dxr : FileByteFilter { public Dxr() : base( - new[] { "application/x-director" }, - new[] { "dxr", "dcr", "dir" }, - new[] - { - new byte[] { 77, 86, 57, 51 } - } + ["application/x-director"], + ["dxr", "dcr", "dir"] ) { + StartsWithAnyOf([ + [0x58, 0x46, 0x49, 0x52, null, null, null, null, 0x33, 0x39, 0x56, 0x4D], + [0x52, 0x49, 0x46, 0x58, null, null, null, null, 0x4D, 0x56, 0x39, 0x33] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Exe.cs b/MagicBytesValidator/Formats/Exe.cs new file mode 100644 index 0000000..256906f --- /dev/null +++ b/MagicBytesValidator/Formats/Exe.cs @@ -0,0 +1,17 @@ +namespace MagicBytesValidator.Formats; + +/// +/// +public class Exe : FileByteFilter +{ + public Exe() : base( + ["application/x-dosexec", "application/x-msdos-program"], + [ + "exe", "com", "dll", "drv", "pif", "qts", "qtx ", "sys", "acm", "ax", "cpl", "fon", "ocx", "olb", "scr", + "vbx", "vxd", "mui", "iec", "ime", "rs", "tsp", "efi" + ] + ) + { + StartsWith([0x4D, 0x5A]); + } +} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Gif.cs b/MagicBytesValidator/Formats/Gif.cs index b57bf0d..024a5ab 100644 --- a/MagicBytesValidator/Formats/Gif.cs +++ b/MagicBytesValidator/Formats/Gif.cs @@ -1,18 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Gif : FileType +/// +/// +public class Gif : FileByteFilter { public Gif() : base( - new[] { "image/gif" }, - new[] { "gif" }, - new[] - { - new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, - new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 } - } + ["image/gif"], + ["gif"] ) { + StartsWithAnyOf([ + [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], + [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Gz.cs b/MagicBytesValidator/Formats/Gz.cs index 0919cf1..8b6d425 100644 --- a/MagicBytesValidator/Formats/Gz.cs +++ b/MagicBytesValidator/Formats/Gz.cs @@ -1,17 +1,13 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Gz : FileType +/// +public class Gz : FileByteFilter { public Gz() : base( - new[] { "application/gzip" }, - new[] { "gz" }, - new[] - { - new byte[] { 31, 139 } - } + ["application/gzip"], + ["gz"] ) { + StartsWith([0x1F, 0x8B]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Ico.cs b/MagicBytesValidator/Formats/Ico.cs index 14dbf00..c6e3304 100644 --- a/MagicBytesValidator/Formats/Ico.cs +++ b/MagicBytesValidator/Formats/Ico.cs @@ -1,17 +1,14 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Ico : FileType +/// +/// +public class Ico : FileByteFilter { public Ico() : base( - new[] { "image/x-icon" }, - new[] { "ico" }, - new[] - { - new byte[] { 0, 0, 1, 0 } - } + ["image/x-icon"], + ["ico"] ) { + StartsWith([0x00, 0x00, 0x01, 0x00]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Jpg.cs b/MagicBytesValidator/Formats/Jpg.cs index 04c7efc..9856079 100644 --- a/MagicBytesValidator/Formats/Jpg.cs +++ b/MagicBytesValidator/Formats/Jpg.cs @@ -1,19 +1,15 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Jpg : FileType +/// +/// +public class Jpg : FileByteFilter { public Jpg() : base( - new[] { "image/jpeg" }, - new[] { "jpg", "jpeg", "jpe" }, - new[] - { - new byte[] { 255, 216, 255 }, - new byte[] { 73, 70, 0, 1 }, - new byte[] { 105, 102, 0, 0 } - } + ["image/jpeg"], + ["jpg", "jpeg", "jpe", "jif", "jfif", "jfi"] ) { + StartsWith([0xFF, 0xD8, 0xFF]) + .EndsWith([0xFF, 0xD9]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Midi.cs b/MagicBytesValidator/Formats/Midi.cs index cfc8d3b..7ea9f68 100644 --- a/MagicBytesValidator/Formats/Midi.cs +++ b/MagicBytesValidator/Formats/Midi.cs @@ -1,17 +1,13 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Midi : FileType +/// +public class Midi : FileByteFilter { public Midi() : base( - new[] { "audio/x-midi" }, - new[] { "midi", "mid" }, - new[] - { - new byte[] { 77, 84, 104, 100 } - } + ["audio/x-midi"], + ["midi", "mid"] ) { + StartsWith([0x4D, 0x54, 0x68, 0x64]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Mp3.cs b/MagicBytesValidator/Formats/Mp3.cs index e9b1804..7584a98 100644 --- a/MagicBytesValidator/Formats/Mp3.cs +++ b/MagicBytesValidator/Formats/Mp3.cs @@ -1,17 +1,18 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Mp3 : FileType +/// +public class Mp3 : FileByteFilter { public Mp3() : base( - new[] { "audio/mpeg" }, - new[] { "mp3" }, - new[] - { - new byte[] { 73, 68, 51 } - } + ["audio/mpeg"], + ["mp3"] ) { + StartsWithAnyOf([ + [0x49, 0x44, 0x33], + [0xFF, 0xFB], + [0xFF, 0xF3], + [0xFF, 0xF2] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Mp4.cs b/MagicBytesValidator/Formats/Mp4.cs index e4352bb..7da8ac7 100644 --- a/MagicBytesValidator/Formats/Mp4.cs +++ b/MagicBytesValidator/Formats/Mp4.cs @@ -1,20 +1,19 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Mp4 : FileType +/// +/// +public class Mp4 : FileByteFilter { public Mp4() : base( - new[] { "video/mp4" }, - new[] { "mp4" }, - new[] - { - new byte[] { 102, 116, 121, 112, 105, 115, 111, 109 }, - new byte[] { 102, 116, 121, 112, 109, 112, 52, 50 }, - new byte[] { 102, 116, 121, 112, 77, 83, 62, 86 }, - }, - 4 + ["video/mp4"], + ["mp4"] ) { + StartsWithAnyOf([ + [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D], + [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32], + [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x53, 0x3E, 0x56], + [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x53, 0x4E, 0x56], + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Mpg.cs b/MagicBytesValidator/Formats/Mpg.cs index 670295f..bbd59a2 100644 --- a/MagicBytesValidator/Formats/Mpg.cs +++ b/MagicBytesValidator/Formats/Mpg.cs @@ -1,19 +1,16 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Mpg : FileType +/// +public class Mpg : FileByteFilter { public Mpg() : base( new[] { "video/mpeg" }, - new[] { "mpg", "mpeg", "mpe" }, - new[] - { - new byte[] { 71 }, - new byte[] { 0, 0, 1, 186 }, - new byte[] { 0, 0, 1, 179 } - } + new[] { "mpg", "mpeg", "mpe", "m2p", "vob" } ) { + StartsWithAnyOf([ + [0x00, 0x00, 0x01, 0xB3], + [0x00, 0x00, 0x01, 0xBA] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Odp.cs b/MagicBytesValidator/Formats/Odp.cs index c00cc7f..990bd43 100644 --- a/MagicBytesValidator/Formats/Odp.cs +++ b/MagicBytesValidator/Formats/Odp.cs @@ -1,16 +1,11 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Odp : FileType +/// +public class Odp : Zip { public Odp() : base( - new[] { "application/vnd.oasis.opendocument.presentation" }, - new[] { "odp" }, - new[] - { - new byte[] { 80, 75, 7, 8 } - } + ["application/vnd.oasis.opendocument.presentation"], + ["odp"] ) { } diff --git a/MagicBytesValidator/Formats/Ods.cs b/MagicBytesValidator/Formats/Ods.cs index 85cd056..cce585e 100644 --- a/MagicBytesValidator/Formats/Ods.cs +++ b/MagicBytesValidator/Formats/Ods.cs @@ -1,16 +1,10 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Ods : FileType +public class Ods : Zip { public Ods() : base( - new[] { "application/vnd.oasis.opendocument.spreadsheet" }, - new[] { "ods" }, - new[] - { - new byte[] { 80, 75, 7, 8 } - } + ["application/vnd.oasis.opendocument.spreadsheet"], + ["ods"] ) { } diff --git a/MagicBytesValidator/Formats/Odt.cs b/MagicBytesValidator/Formats/Odt.cs index 9306ee1..ed0c11b 100644 --- a/MagicBytesValidator/Formats/Odt.cs +++ b/MagicBytesValidator/Formats/Odt.cs @@ -1,18 +1,10 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Odt : FileType +public class Odt : Zip { public Odt() : base( - new[] { "application/vnd.oasis.opendocument.text" }, - new[] { "odt" }, - new[] - { - new byte[] { 80, 75, 3, 4 }, - new byte[] { 80, 75, 5, 6 }, - new byte[] { 80, 75, 7, 8 } - } + ["application/vnd.oasis.opendocument.text"], + ["odt"] ) { } diff --git a/MagicBytesValidator/Formats/Ogv.cs b/MagicBytesValidator/Formats/Ogv.cs index 9386725..d7211c2 100644 --- a/MagicBytesValidator/Formats/Ogv.cs +++ b/MagicBytesValidator/Formats/Ogv.cs @@ -1,17 +1,14 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Ogv : FileType +/// +/// +public class Ogv : FileByteFilter { public Ogv() : base( new[] { "video/ogg" }, - new[] { "ogv", "ogg" }, - new[] - { - new byte[] { 79, 103, 103, 83 } - } + new[] { "ogv", "ogg", "oga" } ) { + StartsWith([0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Pbm.cs b/MagicBytesValidator/Formats/Pbm.cs index 92332a4..a832fd4 100644 --- a/MagicBytesValidator/Formats/Pbm.cs +++ b/MagicBytesValidator/Formats/Pbm.cs @@ -1,17 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Pbm : FileType +/// +/// +public class Pbm : FileByteFilter { public Pbm() : base( - new[] { "image/x-portable-bitmap" }, - new[] { "pbm" }, - new[] - { - new byte[] { 80, 49, 10 } - } + ["image/x-portable-bitmap"], + ["pbm"] ) { + StartsWithAnyOf([ + [0x50, 0x31, 0x0A], + [0x50, 0x34, 0x0A], + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Pdf.cs b/MagicBytesValidator/Formats/Pdf.cs index 2cd86c6..392f0d3 100644 --- a/MagicBytesValidator/Formats/Pdf.cs +++ b/MagicBytesValidator/Formats/Pdf.cs @@ -1,17 +1,21 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Pdf : FileType +/// +/// +public class Pdf : FileByteFilter { public Pdf() : base( - new[] { "application/pdf" }, - new[] { "pdf" }, - new[] - { - new byte[] { 0x25, 0x50, 0x44, 0x46 } - } + ["application/pdf"], + ["pdf"] ) { + StartsWith([0x25, 0x50, 0x44, 0x46, 0x2D]) + .EndsWithAnyOf( + [ + [0x0A, 0x25, 0x25, 0x25, 0x45, 0x4F, 0x46], + [0x0A, 0x25, 0x25, 0x45, 0x4F, 0x46, 0x0A], + [0x0D, 0x0A, 0x25, 0x25, 0x45, 0x4F, 0x46, 0x0D, 0x0A], + [0x0D, 0x25, 0x25, 0x45, 0x4F, 0x46, 0x0D] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Pgm.cs b/MagicBytesValidator/Formats/Pgm.cs index a4f33a9..9eaf2da 100644 --- a/MagicBytesValidator/Formats/Pgm.cs +++ b/MagicBytesValidator/Formats/Pgm.cs @@ -1,17 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Pgm : FileType +/// +/// +public class Pgm : FileByteFilter { public Pgm() : base( new[] { "image/x-portable-graymap" }, - new[] { "pgm" }, - new[] - { - new byte[] { 80, 50, 10 } - } + new[] { "pgm" } ) { + StartsWithAnyOf([ + [0x50, 0x32, 0x0A], + [0x50, 0x35, 0x0A] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Png.cs b/MagicBytesValidator/Formats/Png.cs index 16e680c..2ac13e2 100644 --- a/MagicBytesValidator/Formats/Png.cs +++ b/MagicBytesValidator/Formats/Png.cs @@ -1,17 +1,15 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Png : FileType +/// +/// +public class Png : FileByteFilter { public Png() : base( - new[] { "image/png" }, - new[] { "png" }, - new[] - { - new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } - } + ["image/png"], + ["png"] ) { + StartsWith([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + .EndsWith([0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Ppm.cs b/MagicBytesValidator/Formats/Ppm.cs index e3dd1ea..48a03c4 100644 --- a/MagicBytesValidator/Formats/Ppm.cs +++ b/MagicBytesValidator/Formats/Ppm.cs @@ -1,17 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Ppm : FileType +/// +/// +public class Ppm : FileByteFilter { public Ppm() : base( - new[] { "image/x-portable-pixmap" }, - new[] { "ppm" }, - new[] - { - new byte[] { 80, 51, 10 } - } + ["image/x-portable-pixmap"], + ["ppm"] ) { + StartsWithAnyOf([ + [0x50, 0x33, 0x0A], + [0x50, 0x36, 0x0A] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Ppt.cs b/MagicBytesValidator/Formats/Ppt.cs index 3628c1e..fa9acb2 100644 --- a/MagicBytesValidator/Formats/Ppt.cs +++ b/MagicBytesValidator/Formats/Ppt.cs @@ -1,17 +1,20 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Ppt : FileType +/// +/// +public class Ppt : FileByteFilter { public Ppt() : base( - new[] { "application/mspowerpoint" }, - new[] { "ppt", "ppz", "pps", "pot" }, - new[] - { - new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 } - } + ["application/mspowerpoint", "application/vnd.ms-powerpoint"], + ["ppt", "ppz", "pps", "pot"] ) { + StartsWith([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) + .SpecificAnyOf([ + new ByteCheck(512, [0xFD, 0xFF, 0xFF, 0xFF, null, null, 0x00, 0x00]), + new ByteCheck(512, [0xA0, 0x46, 0x1D, 0xF0]), + new ByteCheck(512, [0x00, 0x6E, 0x1E, 0xF0]), + new ByteCheck(512, [0x0F, 0x00, 0xE8, 0x03]) + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Ppt2.cs b/MagicBytesValidator/Formats/Ppt2.cs deleted file mode 100644 index 5705761..0000000 --- a/MagicBytesValidator/Formats/Ppt2.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Formats; - -public class Ppt2 : FileType -{ - public Ppt2() : base( - new[] { "application/vnd.ms-powerpoint" }, - new[] { "ppt", "ppz", "pps", "pot" }, - new[] - { - new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 } - } - ) - { - } -} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Pptx.cs b/MagicBytesValidator/Formats/Pptx.cs index 5fc1ae9..9e88dda 100644 --- a/MagicBytesValidator/Formats/Pptx.cs +++ b/MagicBytesValidator/Formats/Pptx.cs @@ -1,19 +1,18 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Pptx : FileType +/// +/// +public class Pptx : Zip { public Pptx() : base( - new[] { "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, - new[] { "pptx" }, - new[] - { - new byte[] { 80, 75, 3, 4 }, - new byte[] { 80, 75, 5, 6 }, - new byte[] { 80, 75, 7, 8 } - } + ["application/vnd.openxmlformats-officedocument.presentationml.presentation"], + ["pptx"] ) { + StartsWith([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00]) + .Anywhere([ + 0x70, 0x70, 0x74, 0x2f, 0x5f, 0x72, 0x65, 0x6c, 0x73, 0x2f, 0x70, 0x72, 0x65, + 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x6d, 0x6c, 0x2e, 0x72, 0x65, 0x6c, 0x73 + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Rar.cs b/MagicBytesValidator/Formats/Rar.cs index 25de13e..5e79ab9 100644 --- a/MagicBytesValidator/Formats/Rar.cs +++ b/MagicBytesValidator/Formats/Rar.cs @@ -1,17 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Rar : FileType +/// +/// +public class Rar : FileByteFilter { public Rar() : base( - new[] { "application/vnd.rar", "application/x-rar-compressed" }, - new[] { "rar" }, - new[] - { - new byte[] { 82, 97, 114, 33, 26, 7, 1, 0 } - } + ["application/vnd.rar", "application/x-rar-compressed"], + ["rar"] ) { + StartsWithAnyOf([ + [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00], + [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Rpm.cs b/MagicBytesValidator/Formats/Rpm.cs index 808dd7b..6aaf0cb 100644 --- a/MagicBytesValidator/Formats/Rpm.cs +++ b/MagicBytesValidator/Formats/Rpm.cs @@ -1,17 +1,15 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Rpm : FileType +/// +/// +/// +public class Rpm : FileByteFilter { public Rpm() : base( - new[] { "audio/x-pn-realaudio-plugin" }, - new[] { "rpm" }, - new[] - { - new byte[] { 237, 171, 238, 219 } - } + ["application/x-rpm", "application/x-redhat-package-manager"], + ["rpm"] ) { + StartsWith([0xED, 0xAB, 0xEE, 0xDB]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Rtf.cs b/MagicBytesValidator/Formats/Rtf.cs index 66b5f6e..9d35e2a 100644 --- a/MagicBytesValidator/Formats/Rtf.cs +++ b/MagicBytesValidator/Formats/Rtf.cs @@ -1,17 +1,15 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Rtf : FileType +/// +/// +public class Rtf : FileByteFilter { public Rtf() : base( - new[] { "application/rtf" }, - new[] { "rtf" }, - new[] - { - new byte[] { 123, 92, 114, 116, 102, 49 } - } + ["application/rtf"], + ["rtf"] ) { + StartsWith([0x7B, 0x5C, 0x72, 0x74, 0x66, 0x31]) + .EndsWith([0x7D]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Snd.cs b/MagicBytesValidator/Formats/Snd.cs index 679cdcc..7488360 100644 --- a/MagicBytesValidator/Formats/Snd.cs +++ b/MagicBytesValidator/Formats/Snd.cs @@ -1,18 +1,14 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Snd : FileType +/// +/// +public class Snd : FileByteFilter { public Snd() : base( - new[] { "audio/basic" }, - new[] { "snd", "au" }, - new[] - { - new byte[] { 56, 83, 86, 88 }, - new byte[] { 65, 73, 70, 70 } - } + ["audio/basic"], + ["snd", "au"] ) { + StartsWith([0x2E, 0x73, 0x6E, 0x64]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Swf.cs b/MagicBytesValidator/Formats/Swf.cs new file mode 100644 index 0000000..5696376 --- /dev/null +++ b/MagicBytesValidator/Formats/Swf.cs @@ -0,0 +1,18 @@ +namespace MagicBytesValidator.Formats; + +/// +/// +public class Swf : FileByteFilter +{ + public Swf() : base( + ["application/x-shockwave-flash"], + ["swf"] + ) + { + StartsWithAnyOf([ + [0x43, 0x57, 0x53], + [0x46, 0x57, 0x53], + [0x5A, 0x57, 0x53] + ]); + } +} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/ThreeGp.cs b/MagicBytesValidator/Formats/ThreeGp.cs index b1a2749..8f7aea4 100644 --- a/MagicBytesValidator/Formats/ThreeGp.cs +++ b/MagicBytesValidator/Formats/ThreeGp.cs @@ -1,20 +1,17 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; /// /// Definition for 3GP (as C# does not allow leading numbers for class names, we call it "ThreeGP" here. /// -public class ThreeGp : FileType +/// +/// +public class ThreeGp : FileByteFilter { public ThreeGp() : base( - new[] { "video/3gpp" }, - new[] { "3gp" }, - new[] - { - new byte[] { 102, 116, 121, 112, 51, 103 } - } + ["video/3gpp"], + ["3gp"] ) { + StartsWith([null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x33, 0x67, 0x70]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Tif.cs b/MagicBytesValidator/Formats/Tif.cs index 0c889fa..6ae7060 100644 --- a/MagicBytesValidator/Formats/Tif.cs +++ b/MagicBytesValidator/Formats/Tif.cs @@ -1,18 +1,19 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Tif : FileType +/// +/// +public class Tif : FileByteFilter { public Tif() : base( - new[] { "image/tiff" }, - new[] { "tif", "tiff" }, - new[] - { - new byte[] { 73, 73, 42, 0 }, - new byte[] { 77, 77, 0, 42 } - } + ["image/tiff"], + ["tif", "tiff"] ) { + StartsWithAnyOf([ + [0x49, 0x20, 0x49], + [0x49, 0x49, 0x2A, 0x00], + [0x4D, 0x4D, 0x00, 0x2A], + [0x4D, 0x4D, 0x00, 0x2B] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Tsp.cs b/MagicBytesValidator/Formats/Tsp.cs deleted file mode 100644 index add848b..0000000 --- a/MagicBytesValidator/Formats/Tsp.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Formats; - -public class Tsp : FileType -{ - public Tsp() : base( - new[] { "application/dsptype" }, - new[] { "tsp" }, - new[] - { - new byte[] { 77, 90 } - } - ) - { - } -} \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Tsv.cs b/MagicBytesValidator/Formats/Tsv.cs index aa5a006..0585ece 100644 --- a/MagicBytesValidator/Formats/Tsv.cs +++ b/MagicBytesValidator/Formats/Tsv.cs @@ -1,17 +1,15 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Tsv : FileType +/// +/// +/// +public class Tsv : FileByteFilter { public Tsv() : base( - new[] { "text/tab-separated-values" }, - new[] { "tsv" }, - new[] - { - new byte[] { 71 } - } + ["video/mp2t"], + ["ts", "tsv", "tsa", "mpg", "mpeg"] ) { + StartsWith([0x47]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Txt.cs b/MagicBytesValidator/Formats/Txt.cs index 020696b..74a7c90 100644 --- a/MagicBytesValidator/Formats/Txt.cs +++ b/MagicBytesValidator/Formats/Txt.cs @@ -1,23 +1,23 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; /// -/// As plain text is not really defined by magic bytes, handle with care when using this file-type. +/// As plain text is not really defined by magic bytes but often uses BOMs that we can look for. +/// Handle this file-type with care! /// -public class Txt : FileType +/// +public class Txt : FileByteFilter { public Txt() : base( new[] { "text/plain" }, - new[] { "txt" }, - new[] - { - new byte[] { 239, 187, 191 }, - new byte[] { 255, 254 }, - new byte[] { 254, 255 }, - new byte[] { 255, 254, 0, 0 } - } + new[] { "txt" } ) { + StartsWithAnyOf([ + [0xEF, 0xBB, 0xBF], + [0xFF, 0xFE], + [0xFE, 0xFF], + [0xFF, 0xFE, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0xFF] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Webm.cs b/MagicBytesValidator/Formats/Webm.cs index af52c14..94ca714 100644 --- a/MagicBytesValidator/Formats/Webm.cs +++ b/MagicBytesValidator/Formats/Webm.cs @@ -1,17 +1,14 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Webm : FileType +/// +/// +public class Webm : FileByteFilter { public Webm() : base( - new[] { "video/webm" }, - new[] { "webm" }, - new[] - { - new byte[] { 26, 69, 223, 163 } - } + ["video/webm"], + ["mkv", "mka", "mks", "mk3d", "webm"] ) { + StartsWith([0x1A, 0x45, 0xDF, 0xA3]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Xls.cs b/MagicBytesValidator/Formats/Xls.cs index 95cece7..53aa35e 100644 --- a/MagicBytesValidator/Formats/Xls.cs +++ b/MagicBytesValidator/Formats/Xls.cs @@ -1,17 +1,20 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Xls : FileType +/// +/// +public class Xls : FileByteFilter { public Xls() : base( - new[] { "application/msexcel" }, - new[] { "xls", "xla" }, - new[] - { - new byte[] { 208, 207, 17, 224, 161, 177, 26, 225 } - } + ["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]) + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Xlsx.cs b/MagicBytesValidator/Formats/Xlsx.cs index 669a32a..eba69f1 100644 --- a/MagicBytesValidator/Formats/Xlsx.cs +++ b/MagicBytesValidator/Formats/Xlsx.cs @@ -1,19 +1,18 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Xlsx : FileType +/// +/// +public class Xlsx : Zip { public Xlsx() : base( - new[] { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, - new[] { "xlsx" }, - new[] - { - new byte[] { 80, 75, 3, 4 }, - new byte[] { 80, 75, 5, 6 }, - new byte[] { 80, 75, 7, 8 } - } + ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], + ["xlsx"] ) { + StartsWith([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00]) + .Anywhere([ + 0x78, 0x6c, 0x2f, 0x5f, 0x72, 0x65, 0x6c, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, + 0x78, 0x6d, 0x6c, 0x2e, 0x72, 0x65, 0x6c, 0x73 + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Z.cs b/MagicBytesValidator/Formats/Z.cs index 1e874fa..c65f752 100644 --- a/MagicBytesValidator/Formats/Z.cs +++ b/MagicBytesValidator/Formats/Z.cs @@ -1,17 +1,16 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Z : FileType +/// +public class Z : FileByteFilter { public Z() : base( - new[] { "application/x-compress" }, - new[] { "z" }, - new[] - { - new byte[] { 31, 157 } - } + ["application/x-compress"], + ["z"] ) { + StartsWithAnyOf([ + [0x1F, 0x9D], + [0x1F, 0xA0] + ]); } } \ No newline at end of file diff --git a/MagicBytesValidator/Formats/Zip.cs b/MagicBytesValidator/Formats/Zip.cs index d80ea25..0111d09 100644 --- a/MagicBytesValidator/Formats/Zip.cs +++ b/MagicBytesValidator/Formats/Zip.cs @@ -1,17 +1,26 @@ -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Formats; -public class Zip : FileType +/// +/// +public class Zip : FileByteFilter { - public Zip() : base( - new[] { "application/zip", "application/x-zip-compressed" }, - new[] { "zip" }, - new[] - { - new byte[] { 80, 75, 3, 4 } - } + public Zip() : this( + ["application/zip", "application/x-zip-compressed"], + ["zip"] ) { } + + public Zip(string[] mimeTypes, string[] extensions) : base(mimeTypes, extensions) + { + StartsWithAnyOf([ + [0x50, 0x4B, 0x03, 0x04], + [0x50, 0x4B, 0x05, 0x06], + [0x50, 0x4B, 0x07, 0x08] + ]) + .EndsWith([ + 0x50, 0x4B, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, 0x00, 0x00, 0x00 + ]); + } } \ No newline at end of file diff --git a/MagicBytesValidator/Imports.cs b/MagicBytesValidator/Imports.cs new file mode 100644 index 0000000..5bcb03e --- /dev/null +++ b/MagicBytesValidator/Imports.cs @@ -0,0 +1,14 @@ +// Global using directives + +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Reflection; +global using System.Threading; +global using System.Threading.Tasks; +global using MagicBytesValidator.Exceptions; +global using MagicBytesValidator.Exceptions.Http; +global using MagicBytesValidator.Extensions; +global using MagicBytesValidator.Models; +global using Microsoft.AspNetCore.Http; \ No newline at end of file diff --git a/MagicBytesValidator/MagicBytesValidator.csproj b/MagicBytesValidator/MagicBytesValidator.csproj index 44a30a5..a56da15 100644 --- a/MagicBytesValidator/MagicBytesValidator.csproj +++ b/MagicBytesValidator/MagicBytesValidator.csproj @@ -4,8 +4,8 @@ MagicBytesValidator traperto GmbH Validate files based on mimetypes, extensions and magicbytes. - 1.0.17 - Niklas Schmidt, Tobias Janssen, Maximilian Breuker + 2.0.0 + Members of traperto gmbh https://github.com/Traperto/magic-bytes-validator/blob/main/README.md mime mimetype mimetypes magic magicbyte magicbytes extension extensions file fileextension traperto trapertoGmbh @@ -16,16 +16,12 @@ traperto GmbH MIT - - net8.0 - latest - enable - true + true - + diff --git a/MagicBytesValidator/Models/FileByteFilter.cs b/MagicBytesValidator/Models/FileByteFilter.cs new file mode 100644 index 0000000..2fe94b4 --- /dev/null +++ b/MagicBytesValidator/Models/FileByteFilter.cs @@ -0,0 +1,155 @@ +namespace MagicBytesValidator.Models; + +// TODO: currently Anywhere() doesnt support null bytes in Array + +public class FileByteFilter : IFileType +{ + private readonly List _neededByteChecks = []; + private readonly List _oneOfEachByteChecks = []; + private readonly List _anywhereByteChecks = []; + + public string[] MimeTypes { get; } + public string[] Extensions { get; } + + public FileByteFilter(string[] mimeTypes, string[] extensions) + { + if (!mimeTypes.Any() || mimeTypes.Any(string.IsNullOrEmpty)) + { + throw new ArgumentEmptyException($"{nameof(mimeTypes)} cannot be null or empty"); + } + + if (!extensions.Any() || extensions.Any(string.IsNullOrEmpty)) + { + throw new ArgumentEmptyException($"{nameof(extensions)} cannot be null or empty"); + } + + MimeTypes = mimeTypes; + Extensions = extensions; + } + + public class ByteCheck(int offset, byte?[] bytesToCheck) + { + public int Offset = offset; + public readonly byte?[] ByteArray = bytesToCheck; + } + + public bool Matches(byte[] fileByteStream) + { + foreach (var neededByteCheck in _neededByteChecks) + { + if (!CheckBytes(neededByteCheck, fileByteStream)) + return false; + } + + foreach (var oneOf in _oneOfEachByteChecks) + { + if (!oneOf.Any(byteToCheck => CheckBytes(byteToCheck, fileByteStream))) + { + return false; + } + } + + // Then check byteArrays without fixed offsets + // mainly byteArrays from Anywhere() + foreach (var byteCheckWithoutOffset in _anywhereByteChecks) + { + var found = false; + for (var index = 1; index <= fileByteStream.Length; index++) + { + if (byteCheckWithoutOffset.Cast() + .SequenceEqual(fileByteStream.Skip(index).Take(byteCheckWithoutOffset.Length))) + { + found = true; + break; + } + } + + if (!found) + { + return false; + } + } + + return true; + } + + public FileByteFilter StartsWith(byte?[] bytesToCheck) + { + _neededByteChecks.Add(new ByteCheck(0, bytesToCheck)); + return this; + } + + public FileByteFilter StartsWithAnyOf(byte?[][] bytesToCheck) + { + _oneOfEachByteChecks.Add(bytesToCheck.Select(byteArray => new ByteCheck(0, byteArray)).ToArray()); + return this; + } + + public FileByteFilter EndsWith(byte?[] bytesToCheck) + { + _neededByteChecks.Add(new ByteCheck(-bytesToCheck.Length, bytesToCheck)); + return this; + } + + public FileByteFilter EndsWithAnyOf(byte?[][] bytesToCheck) + { + _oneOfEachByteChecks.Add(bytesToCheck.Select(byteArray => new ByteCheck(-byteArray.Length, byteArray)).ToArray()); + return this; + } + + public FileByteFilter Anywhere(byte?[] bytesToCheck) + { + _anywhereByteChecks.Add(bytesToCheck); + return this; + } + + public FileByteFilter Anywhere(byte?[][] bytesToCheck) + { + foreach (var byteArrayToCheck in bytesToCheck) + { + Anywhere(byteArrayToCheck); + } + + return this; + } + + public FileByteFilter Specific(ByteCheck bytesToCheck) + { + _neededByteChecks.Add(bytesToCheck); + return this; + } + + public FileByteFilter SpecificAnyOf(ByteCheck[] bytesToCheck) + { + _oneOfEachByteChecks.Add(bytesToCheck.Select(byteArray => byteArray).ToArray()); + return this; + } + + private bool CheckBytes(ByteCheck byteToCheck, byte[] fileStreamToCheck) + { + // Check ending of file stream + // since in the current format we have the fileStream Length only here calculate the offset + if (byteToCheck.Offset < 0) + byteToCheck.Offset = fileStreamToCheck.Length - byteToCheck.ByteArray.Length; + + if (fileStreamToCheck.Length - Math.Abs(byteToCheck.Offset) < byteToCheck.ByteArray.Length) + { + return false; + } + + foreach (var (sequenceByte, index) in byteToCheck.ByteArray.AsIndexed()) + { + if (sequenceByte == null) + { + continue; + } + + if (sequenceByte != fileStreamToCheck[byteToCheck.Offset + index]) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/MagicBytesValidator/Models/FileType.cs b/MagicBytesValidator/Models/FileType.cs deleted file mode 100644 index f8341ed..0000000 --- a/MagicBytesValidator/Models/FileType.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Linq; -using MagicBytesValidator.Exceptions; - -namespace MagicBytesValidator.Models; - -/// -/// A FileType contains all necessary information to identify and validate the type of a file (based on MIME type, -/// extensions and magic-byte sequences). -/// -public class FileType -{ - /// - /// MIME type of a file - /// "image/gif" - /// - public string[] MimeTypes { get; } - - /// - /// List of file extensions for a type - /// [ "gif" ] - /// - public string[] Extensions { get; } - - /// - /// List of magic-byte sequences to identify a file type based on the file contents - /// [ [47 49 46 38 37 61], [47 49 46 38 39 61] ] for "gif" files - /// - public byte[][] MagicByteSequences { get; } - - public uint MagicByteOffset { get; } - - /// - /// Creates a new FileType - /// - /// MIME types of the new file type - /// File extensions of the new file type - /// Magic byte sequences of the new file type - /// Offset sequences - /// - /// When any property of FileType is empty or contains empty values - /// - public FileType(string[] mimeTypes, string[] extensions, byte[][] magicByteSequences, uint magicByteOffset = 0) - { - if (!mimeTypes.Any() || mimeTypes.Any(string.IsNullOrEmpty)) - { - throw new ArgumentEmptyException(nameof(mimeTypes)); - } - - if (!extensions.Any() || extensions.Any(string.IsNullOrEmpty)) - { - throw new ArgumentEmptyException(nameof(extensions)); - } - - if (!magicByteSequences.Any() || magicByteSequences.Any(mbs => mbs.Length == 0)) - { - throw new ArgumentEmptyException(nameof(magicByteSequences)); - } - - MimeTypes = mimeTypes; - Extensions = extensions; - MagicByteSequences = magicByteSequences; - MagicByteOffset = magicByteOffset; - } -} \ No newline at end of file diff --git a/MagicBytesValidator/Models/IFileType.cs b/MagicBytesValidator/Models/IFileType.cs new file mode 100644 index 0000000..b4aaa9b --- /dev/null +++ b/MagicBytesValidator/Models/IFileType.cs @@ -0,0 +1,25 @@ +namespace MagicBytesValidator.Models; + +/// +/// An IFileType contains all necessary information to identify and validate the type of a file (based on MIME type, +/// extensions and magic-byte sequences). +/// +public interface IFileType +{ + /// + /// MIME types of a file + /// ["image/gif"] + /// + public string[] MimeTypes { get; } + + /// + /// File extensions for a type + /// [ "gif" ] + /// + public string[] Extensions { get; } + + /// + /// Returns whether a given file (as byte array) matches the file type + /// + public bool Matches(byte[] fileByteStream); +} \ No newline at end of file diff --git a/MagicBytesValidator/Services/FileTypeCollector.cs b/MagicBytesValidator/Services/FileTypeCollector.cs index 5962000..bf89dc3 100644 --- a/MagicBytesValidator/Services/FileTypeCollector.cs +++ b/MagicBytesValidator/Services/FileTypeCollector.cs @@ -1,23 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MagicBytesValidator.Models; - namespace MagicBytesValidator.Services; public static class FileTypeCollector { - public static IEnumerable CollectFileTypes(Assembly? assembly = null) + [Obsolete("Use CollectFileTypesForAssembly instead.")] + public static IEnumerable CollectFileTypes(Assembly? assembly = null) + { + assembly ??= typeof(Mapping).GetTypeInfo().Assembly; + return CollectFileTypesForAssembly(assembly); + } + + public static IEnumerable CollectFileTypesForAssembly(Assembly assembly) { - assembly ??= typeof(FileTypeCollector).GetTypeInfo().Assembly; + if (assembly is null) + { + throw new ArgumentEmptyException(nameof(assembly)); + } return assembly.GetTypes() - .Where(t => typeof(FileType).IsAssignableFrom(t)) + .Where(t => typeof(IFileType).IsAssignableFrom(t)) .Where(t => !t.GetTypeInfo().IsAbstract) .Where(t => t.GetConstructors().Any(c => c.GetParameters().Length == 0)) .Select(Activator.CreateInstance) - .OfType() + .OfType() .ToList(); } } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Http/FormFileTypeProvider.cs b/MagicBytesValidator/Services/Http/FormFileTypeProvider.cs index 75725fd..89a05ee 100644 --- a/MagicBytesValidator/Services/Http/FormFileTypeProvider.cs +++ b/MagicBytesValidator/Services/Http/FormFileTypeProvider.cs @@ -1,37 +1,27 @@ -using System.Linq; -using MagicBytesValidator.Exceptions.Http; -using MagicBytesValidator.Models; -using Microsoft.AspNetCore.Http; +namespace MagicBytesValidator.Services.Http; -namespace MagicBytesValidator.Services.Http; - -/// -/// Service that provides file information for given . -/// +/// public class FormFileTypeProvider : IFormFileTypeProvider { private const char _FILE_EXTENSION_SEPARATOR = '.'; - /// - /// Mapping that is used for providing information - /// + /// public Mapping Mapping { get; } - public FormFileTypeProvider(Mapping? mapping = null) + private readonly IValidator _validator; + + public FormFileTypeProvider( + Mapping? mapping = null, + IValidator? validator = null + ) { Mapping = mapping ?? new Mapping(); + _validator = validator ?? new Validator(Mapping); } - /// - /// Tries to find matching FileType for given IFormFile. - /// - /// Given IFormFile - /// Matching FileType (if known) - /// - /// When file-type by extension and given content-type (IFormFile.ContentType) differ. - /// In this case, someone could try to circumvent the validation. - /// - public FileType? FindFileTypeForFormFile(IFormFile formFile) + /// + [Obsolete("Use FindValidatedType instead.")] + public IFileType? FindFileTypeForFormFile(IFormFile formFile) { /* If the form file has a file name with an extension, we'll try to find the fileType by it first. * If not, we'll try loading it by its given content type. */ @@ -55,4 +45,46 @@ public FormFileTypeProvider(Mapping? mapping = null) return fileType; } + + /// + public async Task FindValidatedTypeAsync( + IFormFile formFile, + Stream? formFileStream, + CancellationToken cancellationToken + ) + { + var fileTypeByContentType = Mapping.FindByMimeType(formFile.ContentType); + if (fileTypeByContentType is null) + { + return null; + } + + var fileTypeByExtension = formFile.FileName.Contains(_FILE_EXTENSION_SEPARATOR) + ? Mapping.FindByExtension(formFile.FileName.Split(_FILE_EXTENSION_SEPARATOR).Last()) + : null; + + if ( + fileTypeByExtension is not null + && fileTypeByExtension.GetType() != fileTypeByContentType.GetType() + ) + { + /* This can only occur if the given form file has a file name and its extension indicates a different + * MIME type as (also given) Content-Type. This *can* be an indicator that someone is trying to + * mess with us. As we are a bit paranoid and also the file type is not unambiguous, we'll throw. */ + throw new MimeTypeMismatchException(fileTypeByExtension.MimeTypes, formFile.ContentType); + } + + var contentIsValid = await _validator.IsValidAsync( + formFileStream ?? formFile.OpenReadStream(), + fileTypeByContentType, + cancellationToken + ); + + if (!contentIsValid) + { + throw new MimeTypeMismatchException(formFile.ContentType); + } + + return fileTypeByContentType; + } } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Http/IFormFileTypeProvider.cs b/MagicBytesValidator/Services/Http/IFormFileTypeProvider.cs index d969494..c282920 100644 --- a/MagicBytesValidator/Services/Http/IFormFileTypeProvider.cs +++ b/MagicBytesValidator/Services/Http/IFormFileTypeProvider.cs @@ -1,9 +1,8 @@ -using MagicBytesValidator.Exceptions.Http; -using MagicBytesValidator.Models; -using Microsoft.AspNetCore.Http; - -namespace MagicBytesValidator.Services.Http; +namespace MagicBytesValidator.Services.Http; +/// +/// Service that provides file information for given . +/// public interface IFormFileTypeProvider { /// @@ -14,11 +13,35 @@ public interface IFormFileTypeProvider /// /// Tries to find matching FileType for given IFormFile. /// - /// Given IFormFile - /// Matching FileType (if known) /// /// When file-type by extension and given content-type (IFormFile.ContentType) differ. /// In this case, someone could try to circumvent the validation. /// - FileType? FindFileTypeForFormFile(IFormFile formFile); + [Obsolete("Use FindValidatedType instead.")] + IFileType? FindFileTypeForFormFile(IFormFile formFile); + + /// + /// Tries to find matching for given that also matches + /// the content of the form file. + /// + /// that the should be found for + /// + /// Optional. If the file stream for the form file is already loaded, it can be included here. + /// This prevents opening a read stream for the same file multiple times. + /// However, never include streams of other files than the given form file! Otherwise the validation may be + /// wrong and could be circumvented! + /// + /// CancellationToken + /// + /// that matches by the form files content type, content (and extension, if given) + /// + /// + /// When file-type by extension and given content-type (IFormFile.ContentType) differ. + /// In this case, someone could try to circumvent the validation. + /// + Task FindValidatedTypeAsync( + IFormFile formFile, + Stream? formFileStream, + CancellationToken cancellationToken + ); } \ No newline at end of file diff --git a/MagicBytesValidator/Services/IMapping.cs b/MagicBytesValidator/Services/IMapping.cs index e660ca5..99bea8d 100644 --- a/MagicBytesValidator/Services/IMapping.cs +++ b/MagicBytesValidator/Services/IMapping.cs @@ -1,48 +1,37 @@ -using System.Collections.Generic; -using System.Reflection; -using MagicBytesValidator.Exceptions; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services; +namespace MagicBytesValidator.Services; public interface IMapping { /// - /// Currently registered + /// Currently registered /// - IReadOnlyList FileTypes { get; } + IReadOnlyList FileTypes { get; } /// - /// Tries to find a known by given MIME type. + /// Tries to find a known by given MIME type. /// - /// MIME type that should be searched for - /// FileType that belongs to the given MIME type /// When given MIME type is null or empty - FileType? FindByMimeType(string mimeType); + IFileType? FindByMimeType(string mimeType); /// - /// Tries to find a known by given file extension. + /// Tries to find a known by given file extension. /// - /// File extension that should be searched for - /// FileType that contains the given file extension /// When given file extension is null or empty - FileType? FindByExtension(string extension); + IFileType? FindByExtension(string extension); /// - /// Registers a new in the mapping. + /// Registers a new in the mapping. /// - /// FileType to register - void Register(FileType fileType); + void Register(IFileType fileType); /// - /// Registers a collection of in the mapping. + /// Registers a collection of in the mapping. /// - /// Collection of FileType to register - void Register(IReadOnlyList fileTypes); + void Register(IEnumerable fileTypes); /// - /// Registers all that a part of given assembly + /// Registers all that a part of given assembly /// - /// + /// Assembly that will be searched for s void Register(Assembly assembly); } \ No newline at end of file diff --git a/MagicBytesValidator/Services/IValidator.cs b/MagicBytesValidator/Services/IValidator.cs index 0deae3e..95eed2a 100644 --- a/MagicBytesValidator/Services/IValidator.cs +++ b/MagicBytesValidator/Services/IValidator.cs @@ -1,9 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services; +namespace MagicBytesValidator.Services; public interface IValidator { @@ -15,9 +10,5 @@ public interface IValidator /// /// Validates a given file-Stream against a given FileType and returns if the Stream is valid or not. /// - /// Stream of the file that should be validated - /// FileType that the stream should be validated against - /// - /// Returns whether the Stream matches one of the FileStream's magic-byte sequences. - Task IsValidAsync(Stream fileStream, FileType fileType, CancellationToken cancellationToken); + Task IsValidAsync(Stream fileStream, IFileType fileType, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Mapping.cs b/MagicBytesValidator/Services/Mapping.cs index af95307..5c08488 100644 --- a/MagicBytesValidator/Services/Mapping.cs +++ b/MagicBytesValidator/Services/Mapping.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MagicBytesValidator.Exceptions; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services; +namespace MagicBytesValidator.Services; /// public class Mapping : IMapping { /// - public IReadOnlyList FileTypes => _fileTypes; + public IReadOnlyList FileTypes => _fileTypes; - private readonly List _fileTypes = FileTypeCollector.CollectFileTypes().ToList(); + private readonly List _fileTypes; + + public Mapping() + { + var currentAssembly = typeof(Mapping).GetTypeInfo().Assembly; + _fileTypes = FileTypeCollector.CollectFileTypesForAssembly(currentAssembly).ToList(); + } /// - public FileType? FindByMimeType(string mimeType) + public IFileType? FindByMimeType(string mimeType) { if (string.IsNullOrEmpty(mimeType)) { @@ -29,7 +28,7 @@ public class Mapping : IMapping } /// - public FileType? FindByExtension(string extension) + public IFileType? FindByExtension(string extension) { if (string.IsNullOrEmpty(extension)) { @@ -43,25 +42,18 @@ public class Mapping : IMapping } /// - public void Register(FileType fileType) + public void Register(IFileType fileType) { _fileTypes.Add(fileType); } - public void Register(IReadOnlyList fileTypes) + public void Register(IEnumerable fileTypes) { - if (!fileTypes.Any()) - { - return; - } - _fileTypes.AddRange(fileTypes); } public void Register(Assembly assembly) { - var fileTypes = FileTypeCollector.CollectFileTypes(assembly).ToList(); - - _fileTypes.AddRange(fileTypes); + _fileTypes.AddRange(FileTypeCollector.CollectFileTypesForAssembly(assembly)); } } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Streams/IStreamFileTypeProvider.cs b/MagicBytesValidator/Services/Streams/IStreamFileTypeProvider.cs index f51e0c2..b1f006c 100644 --- a/MagicBytesValidator/Services/Streams/IStreamFileTypeProvider.cs +++ b/MagicBytesValidator/Services/Streams/IStreamFileTypeProvider.cs @@ -1,21 +1,27 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services.Streams; +namespace MagicBytesValidator.Services.Streams; public interface IStreamFileTypeProvider { /// - /// Tries to find a via given magic byte sequence by given file stream. + /// Tries to find a via given magic byte sequence by given file stream. /// Beware that certain file types (e.g. txt files) have no magic bytes sequence and /// could therefore be mismatched. /// - /// Stream that should be identified - /// Cancellation token - /// FileType that belongs to given byte sequence via magic bytes /// When given stream is null - Task FindByMagicByteSequenceAsync(Stream stream, CancellationToken cancellationToken); + [Obsolete("Use TryFindUnambiguousAsync instead.")] + Task FindByMagicByteSequenceAsync(Stream stream, CancellationToken cancellationToken); + + /// + /// Determines all s that match a given file stream. + /// + Task> FindAllMatchesAsync(Stream stream, CancellationToken cancellationToken); + + /// + /// Tries to determine an unambiguous that matches a given file stream. + /// Returns in case it's the only (registered) type that matches. + /// As soon as multiple file types match the file, null will be returned. + /// If no type matches, null will be returned. + /// + /// Only one matching (known) that matches. + Task TryFindUnambiguousAsync(Stream stream, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Streams/StreamFileTypeProvider.cs b/MagicBytesValidator/Services/Streams/StreamFileTypeProvider.cs index 68d4201..b1d351e 100644 --- a/MagicBytesValidator/Services/Streams/StreamFileTypeProvider.cs +++ b/MagicBytesValidator/Services/Streams/StreamFileTypeProvider.cs @@ -1,11 +1,4 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services.Streams; +namespace MagicBytesValidator.Services.Streams; public class StreamFileTypeProvider : IStreamFileTypeProvider { @@ -16,49 +9,36 @@ public StreamFileTypeProvider(IMapping mapping) _mapping = mapping; } - public async Task FindByMagicByteSequenceAsync( - Stream stream, - CancellationToken cancellationToken - ) + [Obsolete("Use TryFindUnambiguousAsync instead")] + public Task FindByMagicByteSequenceAsync(Stream stream, CancellationToken cancellationToken) + { + return TryFindUnambiguousAsync(stream, cancellationToken); + } + + public async Task> FindAllMatchesAsync(Stream stream, CancellationToken cancellationToken) { if (stream is null) { throw new ArgumentNullException(nameof(stream)); } - var sequencesToFileTypes = _mapping.FileTypes - .Select(fileType => - fileType.MagicByteSequences - .Select(sequence => (length: sequence.Length + fileType.MagicByteOffset, sequence, fileType)) - ) - .SelectMany(group => group) - .OrderByDescending(group => group.length); - - if (!sequencesToFileTypes.Any()) - { - return null; - } - - var maxMagicBytesSequenceLength = sequencesToFileTypes - .First() - .length; - var streamBuffer = new byte[maxMagicBytesSequenceLength]; - var previousStreamPosition = stream.Position; stream.Position = 0; + var streamBuffer = new byte[stream.Length]; _ = await stream.ReadAsync(streamBuffer, cancellationToken); stream.Position = previousStreamPosition; - foreach (var (length, sequence, fileType) in sequencesToFileTypes) - { - if (streamBuffer.Skip((int)fileType.MagicByteOffset).Take(sequence.Length).SequenceEqual(sequence)) - { - return fileType; - } - } + return _mapping.FileTypes.Where(fileType => fileType.Matches(streamBuffer)); + } + + public async Task TryFindUnambiguousAsync(Stream stream, CancellationToken cancellationToken) + { + var matches = (await FindAllMatchesAsync(stream, cancellationToken)).ToList(); - return null; + return matches.Count == 1 + ? matches.First() + : null; } } \ No newline at end of file diff --git a/MagicBytesValidator/Services/Validator.cs b/MagicBytesValidator/Services/Validator.cs index cab89a1..d51a8ef 100644 --- a/MagicBytesValidator/Services/Validator.cs +++ b/MagicBytesValidator/Services/Validator.cs @@ -1,17 +1,8 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MagicBytesValidator.Models; - -namespace MagicBytesValidator.Services; +namespace MagicBytesValidator.Services; public class Validator : IValidator { - /// - /// Mapping that is used during validation - /// + /// public Mapping Mapping { get; } public Validator(Mapping? mapping = null) @@ -19,32 +10,17 @@ public Validator(Mapping? mapping = null) Mapping = mapping ?? new Mapping(); } - /// - /// Validates a given file-Stream against a given FileType and returns if the Stream is valid or not. - /// - /// Stream of the file that should be validated - /// FileType that the stream should be validated against - /// - /// Returns if the Stream matches one of the FileStream's magic-byte sequences. - public async Task IsValidAsync( - Stream fileStream, - FileType fileType, - CancellationToken cancellationToken - ) + /// + public async Task IsValidAsync(Stream fileStream, IFileType fileType, CancellationToken cancellationToken) { - var maxLengthFileTypeMagicByteSequences = fileType.MagicByteSequences.Max(mb => mb.Length); - var streamBytes = new byte[maxLengthFileTypeMagicByteSequences]; - - var currentFileStreamPosition = fileStream.Position; - fileStream.Position = fileType.MagicByteOffset; /* Reset the stream to get to the first bytes. */ + var previousStreamPosition = fileStream.Position; + fileStream.Position = 0; - _ = await fileStream.ReadAsync( - streamBytes.AsMemory(0, maxLengthFileTypeMagicByteSequences), - cancellationToken - ); + var streamBuffer = new byte[fileStream.Length]; + _ = await fileStream.ReadAsync(streamBuffer, cancellationToken); - fileStream.Position = currentFileStreamPosition; /* Reset the position */ + fileStream.Position = previousStreamPosition; - return fileType.MagicByteSequences.Any(mb => mb.SequenceEqual(streamBytes.Take(mb.Length))); + return fileType.Matches(streamBuffer); } } \ No newline at end of file diff --git a/README.md b/README.md index 412d1db..b50081e 100644 --- a/README.md +++ b/README.md @@ -7,82 +7,112 @@ The existing `FileTypes` can be expanded in various ways. - Install nuget package into your project: ```powershell -Install-Package MagicBytesValidator -Version 1.0.16 +Install-Package MagicBytesValidator -Version 2.0.0 ``` ```bash -dotnet add package MagicBytesValidator --version 1.0.16 +dotnet add package MagicBytesValidator --version 2.0.0 ``` - Reference in your csproj: ```xml - + ``` ### How to use it? -- Create new instances of the validators: +- Create new instances of the validator & providers: ```c# var validator = new MagicBytesValidator.Services.Validator(); var formFileTypeProvider = new MagicBytesValidator.Services.Http.FormFileTypeProvider(); +var streamFileTypeProvider = new MagicBytesValidator.Services.Streams.StreamFileTypeProvider(); ``` - Find a filetype by extension or mimetype: ```c# var pngFileType = validator.Mapping.FindByExtension("png"); var pdfFileType = validator.Mapping.FindByMimeType("application/pdf"); +``` +- Determine & validate a filetype by uploaded IFormFile: +```c# +var fileType = await formFileTypeProvider.FindValidatedTypeAsync(formFile, null, CancellationToken.None); +``` -// in case of a given IFormFile: -var fileType = formFileTypeProvider.FindFileTypeForFormFile(file); +- Determine the file type of a file by its stream +```c# +var fileType = await streamFileTypeProvider.TryFindUnambiguousAsync(fileStream, CancellationToken.None); ``` - Check a file with its stream and filetype: ```c# -var isValid = await validator.IsValidAsync(memoryStream, fileType); +var isValid = await validator.IsValidAsync(memoryStream, fileType, CancellationToken.None); ``` -#### Expand default the filetype mapping +#### Expand the filetype mapping - Get mapping: ```c# // use the validator: var mapping = validator.Mapping; -// or create an instance of the mapping: + +// use the formFileTypeProvider: +var mapping = formFileTypeProvider.Mapping; + +// or create a new instance of the mapping: var mapping = new MagicBytesValidator.Services.Mapping(); ``` - Register a single Filetype: ```c# mapping.Register( - new FileType( + new FileByteFilter( "traperto/trp", // mime type - new[] { "trp" }, // file extensions - new[] { // magic byte sequences - new byte[] { 0x74, 0x72, 0x61, 0x70, 0x65, 0x72, 0x74, 0x6f } - } - ) + new[] { "trp" } // file extensions + ) { + // magic byte sequences + StartsWith([ + 0x78, 0x6c, 0x2f, 0x5f, 0x72, 0x65 + ]) + .EndsWith([ + 0xFF, 0xFF + ]) + } ) ``` +- FileTypes with specific offset checks: +```c# +mapping.Register( + new FileByteFilter( + "traperto/trp", // mime type + new[] { "trp" } // file extensions + ) { + // magic byte sequences + Specific(new ByteCheck(512, [0xFD])); + } +) +``` +ByteCheck allows for negative offset values to look for a specific offset counting from the end of file + - Register a list of filetypes: ```c# mapping.Register(listOfFileTypes); ``` -You can also create variants of `FileType` and register them by passing the Assembly of the new FileTypes, e.g. +You can also create variants of `IFileType` and register them by passing the Assembly of the new FileTypes, e.g. `mapping.Register(typeof(CustomFileType).Assembly);`. This will register all FileTypes of the given Assembly that are also not abstract and have an empty constructor! ```c# -public class CustomFileType : FileType +public class CustomFileType : FileTypeWithStartSequences { - public CustomFileType() : base( + public CustomFileType() : base( "traperto/trp", // mime type new[] { "trp" }, // file extensions new[] { // magic byte sequences new byte[] { 0x74, 0x72, 0x61, 0x70, 0x65, 0x72, 0x74, 0x6f } } - ) + ) { } } @@ -91,43 +121,52 @@ var assembly = typeof(CustomFileType).Assembly; _mapping.Register(assembly); ``` +### CLI +There's a CLI tool (_MagicBytesValidator.CLI_) which can be used to determine +MIME types for a local file by calling the following command. +```shell +dotnet run --project MagicBytesValidator.CLI -- [PATH] +``` + +This can be useful when debugging or validating newly added FileTypes. + ### List of Filetypes -| Mimetype | Extension | Magicbytes (decimal) | -|-------------------------------------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| -| audio/x-pn-realaudio-plugin | rpm | 237 171 238 219 | -| application/octet-stream | bin
file
com
class
ini |
  • 83 80 48 49
  • 201
  • 202 254 186 190
| -| video/3gpp | 3gp | 102 116 121 112 51 103 | -| image/x-icon | ico | 0 0 1 0 | -| image/gif | gif |
  • 71 73 70 56 55 97
  • 71 73 70 56 57 97
| -| image/tiff | tif
tiff |
  • 73 73 42 0
  • 77 77 0 42
| -| image/jpeg | jpg
jpeg
jpe |
  • 255 216 255 219
  • 255 216 255 224 0 16 74
  • 70 73 70 0 1
  • 255 216 255 238
  • 105 102 0 0
| -| image/png | png | 137 80 78 71 13 10 26 10 | -| video/ogg | ogg
ogv | 79 103 103 83 | -| audio/basic | snd
au |
  • 56 83 86 88
  • 65 73 70 70
| -| application/dsptype | tsp | 77 90 | -| text/plain | txt |
  • 239 187 191
  • 255 254
  • 254 255
  • 255 254 0 0
| -| application/zip | zip | 80 75 3 4 | -| application | docx
xlsx | 80 75 7 8 | -| application/vnd.oasis.opendocument.presentation | odp | 80 75 7 8 | -| application/vnd.oasis.opendocument.spreadsheet | ods | 80 75 7 8 | -| application/vnd.oasis.opendocument.text | odt | 80 75 7 8 | -| audio/mpeg | mp3 | 73 68 51 | -| image/bmp | bmp | 66 77 | -| audio/x-midi | midi
mid | 77 84 104 100 | -| application/msword | doc
dot | 208 207 17 224 161 177 26 255 | -| application/msexcel | xlx
xla | 208 207 17 224 161 177 26 255 | -| application/mspowerpoint | ppt
ppz
pps
pt | 208 207 17 224 161 177 26 225 | -| application/gzip | gz | 31 139 | -| video/webm | webm | 26 69 223 163 | -| application/rtf | rtf | 123 92 114 116 102 49 | -| text/tab-separated-values | tsv | 71 | -| video/mpeg | mpg
mpeg
mpe |
  • 71
  • 0 0 1 186
  • 0 0 1 179
| -| video/mp4 | mp4 |
  • 102 116 121 112 105 115 111 109
  • 102, 116, 121, 112, 109, 112, 52, 50
  • 102, 116, 121, 112, 77, 83, 62, 86
| -| image/x-portable-bitmap | pbm | 80 49 10 | -| image/x-portable-graymap | pgm | 80 50 10 | -| image/x-portable-pixmap | ppm | 80 51 10 | -| application/pdf | pdf | 25 50 44 46 | +| Mimetype | Extension | Magicbytes (decimal) | +|-------------------------------------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| audio/x-pn-realaudio-plugin | rpm | 237 171 238 219 | +| application/octet-stream | bin
file
com
class
ini |
  • 83 80 48 49
  • 201
  • 202 254 186 190
| +| video/3gpp | 3gp | 102 116 121 112 51 103 | +| image/x-icon | ico | 0 0 1 0 | +| image/gif | gif |
  • 71 73 70 56 55 97
  • 71 73 70 56 57 97
| +| image/tiff | tif
tiff |
  • 73 73 42 0
  • 77 77 0 42
| +| image/jpeg | jpg
jpeg
jpe |
  • 255 216 255 219
  • 255 216 255 224 0 16 74
  • 70 73 70 0 1
  • 255 216 255 238
  • 105 102 0 0
| +| image/png | png | 137 80 78 71 13 10 26 10 | +| video/ogg | ogg
ogv | 79 103 103 83 | +| audio/basic | snd
au |
  • 56 83 86 88
  • 65 73 70 70
| +| application/dsptype | tsp | 77 90 | +| text/plain | txt |
  • 239 187 191
  • 255 254
  • 254 255
  • 255 254 0 0
| +| application/zip | zip | 80 75 3 4 | +| application | docx
xlsx | 80 75 7 8 | +| application/vnd.oasis.opendocument.presentation | odp | 80 75 7 8 | +| application/vnd.oasis.opendocument.spreadsheet | ods | 80 75 7 8 | +| application/vnd.oasis.opendocument.text | odt | 80 75 7 8 | +| audio/mpeg | mp3 | 73 68 51 | +| image/bmp | bmp | 66 77 | +| audio/x-midi | midi
mid | 77 84 104 100 | +| application/msword | doc
dot | 208 207 17 224 161 177 26 255 | +| application/msexcel | xlx
xla | 208 207 17 224 161 177 26 255 | +| application/mspowerpoint | ppt
ppz
pps
pt | 208 207 17 224 161 177 26 225 | +| application/gzip | gz | 31 139 | +| video/webm | webm | 26 69 223 163 | +| application/rtf | rtf | 123 92 114 116 102 49 | +| text/tab-separated-values | tsv | 71 | +| video/mpeg | mpg
mpeg
mpe |
  • 71
  • 0 0 1 186
  • 0 0 1 179
| +| video/mp4 | mp4 |
  • 102 116 121 112 105 115 111 109
  • 102 116 121 112 109 112 52 50
  • 102 116 121 112 77 83 62 86
| +| image/x-portable-bitmap | pbm | 80 49 10 | +| image/x-portable-graymap | pgm | 80 50 10 | +| image/x-portable-pixmap | ppm | 80 51 10 | +| application/pdf | pdf | 25 50 44 46 | ### What is the licence?