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 | |
-| 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 | |
-| 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 | |
-| 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 | |
+| 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 | |
+| 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 | |
+| 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?