diff --git a/ReMime/ContentResolvers/Image/ImageMagicValues.cs b/ReMime/ContentResolvers/Image/ImageMagicValues.cs new file mode 100644 index 0000000..ee2b0d6 --- /dev/null +++ b/ReMime/ContentResolvers/Image/ImageMagicValues.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace ReMime.ContentResolvers.Image +{ + public static class ImageMagicValues + { + private static readonly MediaType Tiff = new MediaType("image/tiff", new string[] { "nif", "tif", "tiff"}); + private static readonly MediaType Jpeg = new MediaType("image/jpeg", new string[] { "jpg", "jpeg" }); + + public static readonly IReadOnlyList List = new List() { + new MagicValueMediaType(new MagicValue("BM"), new MediaType("image/bmp")), + new MagicValueMediaType(new MagicValue("GIF8"), new MediaType("image/gif")), + new MagicValueMediaType(new MagicValue("IIN1"), Tiff), + new MagicValueMediaType(new MagicValue(new byte[] { 0x4d, 0x4d, 0x00, 0x2a }), Tiff), + new MagicValueMediaType(new MagicValue(new byte[] { 0x49, 0x49, 0x2a, 0x00 }), Tiff), + new MagicValueMediaType(new MagicValue(new byte[] { 0x89, 0x50, 0x4e, 0x47 }), new MediaType("image/png")), + + /* Yes this is how we are doing JPEG, I don't want to modify my thing to allow for magic values to be defined in terms of bits. */ + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe0 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe1 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe2 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe3 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe4 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe5 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe6 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe7 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe8 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xe9 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xea }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xeb }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xec }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xed }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xee }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xef }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf0 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf1 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf2 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf3 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf4 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf5 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf6 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf7 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf8 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xf9 }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xfa }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xfb }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xfc }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xfd }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xfe }), Jpeg), + new MagicValueMediaType(new MagicValue(new byte[] { 0xff, 0xd8, 0xff, 0xff }), Jpeg), + }.AsReadOnly(); + + public static void AddToMagicResolver(MagicContentResolver resolver) + { + resolver.AddMagicValues(List); + } + } +} \ No newline at end of file diff --git a/ReMime/ContentResolvers/MagicResolver.cs b/ReMime/ContentResolvers/MagicResolver.cs new file mode 100644 index 0000000..770d689 --- /dev/null +++ b/ReMime/ContentResolvers/MagicResolver.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace ReMime.ContentResolvers +{ + public record MagicValueMediaType(MagicValue Magic, MediaType MediaType); + + public class MagicContentResolver : IMediaContentResolver + { + private readonly List _mediaTypes = new List(); + private readonly Dictionary _extensions = new Dictionary(); + private readonly Tree _tree = new Tree(); + private int _maxBytes = 0; + + public MagicContentResolver(IEnumerable values) : this() + { + AddMagicValues(values); + } + + public MagicContentResolver() + { + Image.ImageMagicValues.AddToMagicResolver(this); + } + + public IReadOnlyCollection MediaTypes => _mediaTypes.AsReadOnly(); + + public void AddMagicValue(MagicValueMediaType value) + { + _maxBytes = Math.Max(_maxBytes, value.Magic.Value.Length); + _mediaTypes.Add(value.MediaType); + _tree.Add(value); + + foreach (string extension in value.MediaType.Extensions) + { + _extensions[extension] = value.MediaType; + } + } + + public void AddMagicValues(IEnumerable values) + { + foreach (MagicValueMediaType value in values) + { + AddMagicValue(value); + } + } + + public bool TryResolve(Stream str, [NotNullWhen(true)] out MediaType? mediaType) + { + Span bytes = stackalloc byte[_maxBytes]; + str.Read(bytes); + return TryResolve(bytes, out mediaType); + } + + public bool TryResolve(ReadOnlySpan content, [NotNullWhen(true)] out MediaType? mediaType) + { + MagicValueMediaType? type = _tree[content]; + + if (type == null) + { + mediaType = null; + return false; + } + else + { + mediaType = type.MediaType; + return true; + } + } + + public bool TryResolve(string extension, out MediaType? mediaType) + { + return _extensions.TryGetValue(extension, out mediaType); + } + + private class Tree + { + public MagicValueMediaType? Node { get; private set; } + public Dictionary? Children { get; private set; } + + public MagicValueMediaType? this[ReadOnlySpan bytes] + { + get + { + if (bytes.Length == 0) + return Node; + + if (Children == null) + return null; + + byte b = bytes[0]; + + if (!Children.TryGetValue(b, out Tree? subtree)) + { + return null; + } + + return subtree[bytes.Slice(1)]; + } + } + + private void AddInternal(MagicValueMediaType magic, ReadOnlySpan bytes) + { + if (bytes.Length == 0) + { + Node = magic; + return; + } + + if (Children == null) + { + Children = new Dictionary(); + } + + if (!Children.TryGetValue(bytes[0], out Tree? tree)) + { + tree = new Tree(); + Children[bytes[0]] = tree; + } + + tree.AddInternal(magic, bytes.Slice(1)); + } + + public void Add(MagicValueMediaType magic) + { + ReadOnlySpan bytes = magic.Magic.Value; + AddInternal(magic, bytes); + } + } + } +} \ No newline at end of file diff --git a/ReMime/ContentResolvers/MagicValue.cs b/ReMime/ContentResolvers/MagicValue.cs new file mode 100644 index 0000000..f18ecfb --- /dev/null +++ b/ReMime/ContentResolvers/MagicValue.cs @@ -0,0 +1,41 @@ +using System; +using System.Text; + +namespace ReMime.ContentResolvers +{ + public record struct MagicValue(byte[] Value) + { + public MagicValue(int value) : this(BitConverter.GetBytes(value)) { } + public MagicValue(short value) : this(BitConverter.GetBytes(value)) { } + public MagicValue(string value, Encoding? encoding = null) + : this((encoding ?? Encoding.ASCII).GetBytes(value)) { } + public MagicValue(ReadOnlySpan bytes) : this(bytes.ToArray()) { } + + public bool Matches(ReadOnlySpan haystack) + { + for (int i = 0; i < haystack.Length && i < Value.Length; i++) + { + if (haystack[i] != Value[i]) + return false; + } + + return true; + } + + public override int GetHashCode() + { + // Uses the FVN-1A algorithm in 32-bit mode. + const int PRIME = 0x01000193; + const int BASIS = unchecked((int)0x811c9dc5); + + int hash = BASIS; + for (int i = 0; i < Value.Length; i++) + { + hash ^= Value[i]; + hash *= PRIME; + } + + return hash; + } + } +} \ No newline at end of file diff --git a/ReMime/MediaTypeResolver.cs b/ReMime/MediaTypeResolver.cs index a84329f..1b2a208 100644 --- a/ReMime/MediaTypeResolver.cs +++ b/ReMime/MediaTypeResolver.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; +using ReMime.ContentResolvers; using ReMime.Platform; namespace ReMime @@ -40,6 +41,8 @@ namespace ReMime static MediaTypeResolver() { + AddResolver(new MagicContentResolver(), 9998); + if (OperatingSystem.IsWindows()) { AddResolver(new Win32MediaTypeResolver());