ReMime - File type detector from hell.

(I just wanted to reference the first commit message from the git source code.)
This commit is contained in:
H. Utku Maden 2024-05-25 09:01:52 +03:00
commit a89e51f5e7
Signed by: themixedupstuff
GPG Key ID: 25A001B636F17843
24 changed files with 979 additions and 0 deletions

8
.gitignore vendored Normal file

@ -0,0 +1,8 @@
**/.vs
**/.vscode
**/.atom
**/.rider
**/bin
**/obj

22
README.md Normal file

@ -0,0 +1,22 @@
ReMime - Simple Media Type Resolution
=====================================
ReMime is a very humble library that can identify IANA media types of file
from their file extension and its content. While being fully extensible
with your own resolvers, ReMime will also refer to your operating system's
file type database when resolving files.
Platform Caveats
----------------
* On Windows, the default resolver assumes your application has read access to
the registry.
* On Linux, not all `/etc/mime.types` syntax is supported.
* None of this was written with MacOS in mind. But maybe it'll work?
Refer to `ReMime.Cli` as an example of how to use the library. Refer to in line
documentation and the given default resolvers as an example resolver to
implementations.
Contributing
------------
Feel free to contribute your own file type resolvers and bug fixes. The more
file types that can be detected accurately, the better.

194
ReMime.Cli/Program.cs Normal file

@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace ReMime.Cli
{
public static class Program
{
private const string USAGE = "remime [-r] file/directory/-...\n" +
"remime --help for more help.";
private const string HELP =
"ReMime Command Line Tool - Determine file Media Type\n" +
"\n" +
" remime [-r] file/directory/-...\n" +
"\n" +
" file infer a file\n"+
" directory infer files in directory. Requires -r\n"+
" - infer from standard input.\n"+
" -r search files and folders recursively.\n"+
" -a include hidden files.\n" +
" -v verbose mode, use full paths.\n" +
" --list list known mime types. Will ignore files.\n" +
" --help show this help text.";
private static bool HiddenFiles = false;
private static bool FullPaths = false;
private static bool Recursive = false;
private static bool MinorError = false;
private static bool MajorError = false;
[DoesNotReturn]
private static void Usage()
{
Console.WriteLine(USAGE);
Environment.Exit(0);
}
[DoesNotReturn]
private static void Help()
{
Console.WriteLine(HELP);
Environment.Exit(0);
}
[DoesNotReturn]
private static void ListTypes()
{
foreach (MediaType type in MediaTypeResolver.KnownTypes)
{
Console.WriteLine("{0}\t{1}", type.FullTypeNoParameters, string.Join(' ', type.Extensions));
}
Environment.Exit(0);
}
private static string GetPath(FileSystemInfo info)
{
return FullPaths ? info.FullName : Path.GetRelativePath(Environment.CurrentDirectory, info.FullName);
}
private static void PrintInferenceResult(MediaTypeResult result, string file, MediaType type)
{
Console.WriteLine("{0}{1}\t{2}\t{3}",
result.HasFlag(MediaTypeResult.Extension) ? 'e' : '-',
result.HasFlag(MediaTypeResult.Content) ? 'c' : '-',
file,
type.FullTypeNoParameters);
}
private static void PrintInferenceResult(MediaTypeResult result, FileInfo file, MediaType type)
=> PrintInferenceResult(result, GetPath(file), type);
private static void InferStdin()
{
using MemoryStream ms = new MemoryStream(1024);
using Stream stdin = Console.OpenStandardInput();
stdin.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
MediaTypeResult result = MediaTypeResolver.TryResolve(ms, out MediaType mediaType) ? MediaTypeResult.Content : 0;
PrintInferenceResult(result, "<stdin>", mediaType);
}
private static void InferFile(FileInfo file)
{
if (file.Attributes.HasFlag(FileAttributes.Hidden) && !HiddenFiles)
return;
MediaTypeResult result = MediaTypeResolver.TryResolve(file, out MediaType type);
PrintInferenceResult(result, file, type);
}
private static void InferDirectory(DirectoryInfo directory)
{
if (directory.Attributes.HasFlag(FileAttributes.Hidden) && !HiddenFiles)
{
return;
}
foreach (var node in directory.GetFileSystemInfos())
{
if (node.Attributes.HasFlag(FileAttributes.Directory))
{
if (Recursive)
{
InferDirectory((DirectoryInfo)node);
}
else
{
Console.WriteLine("# Skipping directory {0}, set -r to traverse.", GetPath(node));
MinorError = true;
}
}
else
{
InferFile((FileInfo)node);
}
}
}
public static void Main(string[] args)
{
if (args.Length == 0)
{
Usage();
}
List<FileSystemInfo> nodes = new List<FileSystemInfo>();
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--help":
case "/?":
Help();
return;
case "--list":
ListTypes();
return;
case "-a":
HiddenFiles = true;
break;
case "-v":
FullPaths = true;
break;
case "-r":
Recursive = true;
break;
case "-":
InferStdin();
break;
default:
if (Directory.Exists(args[i]))
{
nodes.Add(new DirectoryInfo(args[i]));
}
else if (File.Exists(args[i]))
{
nodes.Add(new FileInfo(args[i]));
}
else
{
Console.WriteLine("# Path {0} does not exist. Skipping...", args[i]);
MajorError = true;
}
break;
}
}
foreach (var node in nodes)
{
try
{
if (node.Attributes.HasFlag(FileAttributes.Directory))
{
InferDirectory((DirectoryInfo)node);
}
else
{
InferFile((FileInfo)node);
}
}
catch (Exception ex)
{
Console.WriteLine("# Error while processing {0}: {1}", node.FullName, ex.Message);
MajorError = true;
}
}
Environment.Exit(MajorError ? 1 : (MinorError ? 2 : 0));
}
}
}

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\ReMime\ReMime.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<AssemblyName>refile</AssemblyName>
</PropertyGroup>
</Project>

@ -0,0 +1,2 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using ReMime;

@ -0,0 +1,77 @@
using System.Runtime.CompilerServices;
using ReMime.Platform;
namespace ReMime.Tests
{
public abstract class MediaTypesByExtension<T> where T : IMediaTypeResolver, new()
{
private T CIT;
protected MediaTypesByExtension()
{
Unsafe.SkipInit(out CIT);
}
[TestInitialize]
public virtual void Initialize()
{
CIT = new T();
}
readonly (string extension, string type)[] ExampleMimeTypes = new (string, string)[] {
("png", "image/png"),
("gif", "image/gif"),
("jpeg", "image/jpeg"),
("jpg", "image/jpeg"),
("txt", "text/plain"),
("css", "text/css"),
("mp4", "video/mp4"),
("ttf", "font/ttf")
};
[TestMethod]
public void PassKnownTypes()
{
foreach (var(ext, type) in ExampleMimeTypes)
{
Assert.IsTrue(CIT.TryResolve(ext, out MediaType? result));
Assert.AreEqual(result!.FullType, type);
Assert.IsTrue(result.Extensions.Contains(ext));
}
}
}
[TestClass]
public class UnixMediaTypes : MediaTypesByExtension<UnixMediaTypeResolver>
{
[TestInitialize]
public override void Initialize()
{
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD())
{
base.Initialize();
}
else
{
Assert.Inconclusive("Cannot test this in this platform.");
}
}
}
[TestClass]
public class Win32MediaTypes : MediaTypesByExtension<Win32MediaTypeResolver>
{
[TestInitialize]
public override void Initialize()
{
if (OperatingSystem.IsWindows())
{
base.Initialize();
}
else
{
Assert.Inconclusive("Cannot test this in this platform.");
}
}
}
}

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.4" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.4" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReMime\ReMime.csproj" />
</ItemGroup>
</Project>

34
ReMime.sln Normal file

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReMime", "ReMime\ReMime.csproj", "{05FAB3CF-78AF-4D34-97D1-C3AB24D4C59F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReMime.Tests", "ReMime.Tests\ReMime.Tests.csproj", "{FEEB5BAD-3B18-4A88-A212-32EC9DA93BDE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReMime.Cli", "ReMime.Cli\ReMime.Cli.csproj", "{51AB44A2-D4EB-4CC8-BE4E-EF1912350629}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05FAB3CF-78AF-4D34-97D1-C3AB24D4C59F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05FAB3CF-78AF-4D34-97D1-C3AB24D4C59F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05FAB3CF-78AF-4D34-97D1-C3AB24D4C59F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05FAB3CF-78AF-4D34-97D1-C3AB24D4C59F}.Release|Any CPU.Build.0 = Release|Any CPU
{FEEB5BAD-3B18-4A88-A212-32EC9DA93BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEEB5BAD-3B18-4A88-A212-32EC9DA93BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEEB5BAD-3B18-4A88-A212-32EC9DA93BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEEB5BAD-3B18-4A88-A212-32EC9DA93BDE}.Release|Any CPU.Build.0 = Release|Any CPU
{51AB44A2-D4EB-4CC8-BE4E-EF1912350629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51AB44A2-D4EB-4CC8-BE4E-EF1912350629}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51AB44A2-D4EB-4CC8-BE4E-EF1912350629}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51AB44A2-D4EB-4CC8-BE4E-EF1912350629}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

@ -0,0 +1,3 @@
If you would like to contribute a file type resolver please add them
to the appropriate namespace under this folder. This namespace should
match with the IANA namespaces of the media type in question.

@ -0,0 +1,25 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace ReMime
{
public interface IMediaContentResolver : IMediaTypeResolver
{
/// <summary>
/// Resolve media type via file extension.
/// </summary>
/// <param name="str">The stream to match the content with.</param>
/// <param name="mediaType">The media type for this extension.</param>
/// <returns>True if content matched.</returns>
bool TryResolve(Stream str, [NotNullWhen(true)] out MediaType? mediaType);
/// <summary>
/// Resolve media type via file extension.
/// </summary>
/// <param name="content">The stream to match the content with.</param>
/// <param name="mediaType">The media type for this extension.</param>
/// <returns>True if content matched.</returns>
bool TryResolve(ReadOnlySpan<byte> content, [NotNullWhen(true)] out MediaType? mediaType);
}
}

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace ReMime
{
public interface IMediaTypeResolver
{
/// <summary>
/// Known media types.
/// </summary>
IReadOnlyCollection<MediaType> MediaTypes { get; }
/// <summary>
/// Resolve media type via file extension.
/// </summary>
/// <param name="extension">The file extension of the file without the leading dot.</param>
/// <param name="mediaType">The media type for this extension.</param>
/// <returns>True if a type matched.</returns>
bool TryResolve(string extension, out MediaType? mediaType);
}
}

166
ReMime/MediaType.cs Normal file

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ReMime
{
/// <summary>
/// An IANA media type.
/// </summary>
[DebuggerDisplay("{FullType}")]
public class MediaType
{
/// <summary>
/// Full media type string, including parameters.
/// </summary>
public string FullType { get; }
/// <summary>
/// Media type string excluding parameters.
/// </summary>
public string FullTypeNoParameters { get; }
/// <summary>
/// Main media type, e.g. <c>image</c>
/// </summary>
public string Type { get; }
/// <summary>
/// Vendor tree, e.g. <c>vnd.microsoft</c> in <c>application/vnd.microsoft.portable-executable</c>
/// </summary>
public string? Tree { get; }
/// <summary>
/// Subtype, e.g. <c>png</c> in <c>image/png</c>
/// </summary>
public string SubType { get; }
/// <summary>
/// Media type suffix, e.g. <c>json</c> in <c>model/gltf+json</c>
/// </summary>
public string? Suffix { get; }
/// <summary>
/// Media type parameters, e.g. <c>encoding=utf-8</c> in <c>text/plain;encoding=utf-8</c>
/// </summary>
public string? Parameters { get; }
/// <summary>
/// Valid or common file extensions for this media type, excluding the dot. May be empty.
/// </summary>
public IReadOnlyCollection<string> Extensions { get; }
/// <summary>
/// Create a media type descriptor.
/// </summary>
/// <param name="fullType">The full media type string.</param>
/// <param name="extensions">The file extensions if any.</param>
/// <exception cref="Exception">Malformed media type string in <paramref name="fullType"/>.</exception>
public MediaType(string fullType, IEnumerable<string>? extensions = null)
{
FullType = fullType;
ReadOnlySpan<char> str = fullType.AsSpan();
int slash = str.IndexOf('/');
int plus = str.IndexOf('+');
int semicolon = str.IndexOf(';');
if (slash == -1)
{
throw new Exception("Malformed media type string.");
}
Type = new string(str.Slice(0, slash));
ReadOnlySpan<char> typeTree;
if (plus != -1)
{
typeTree = str.Slice(slash+1, plus - slash - 1);
}
else if (semicolon != -1)
{
typeTree = str.Slice(slash+1, semicolon - slash - 1);
}
else
{
typeTree = str.Slice(slash+1);
}
int dot = typeTree.LastIndexOf('.');
if (dot == -1)
{
SubType = new string(typeTree);
}
else
{
SubType = new string(typeTree.Slice(dot+1));
Tree = new string(typeTree.Slice(0, dot));
}
if (plus != -1)
{
if (semicolon != -1)
{
Suffix = new string(str.Slice(plus+1, semicolon - plus - 1));
}
else
{
Suffix = new string(str.Slice(plus + 1));
}
}
if (semicolon != -1)
{
Parameters = new string(str.Slice(semicolon+1));
FullTypeNoParameters = new string(str.Slice(0, semicolon));
}
else
{
FullTypeNoParameters = FullType;
}
Extensions = (extensions ?? Enumerable.Empty<string>()).ToArray();
}
/// <summary>
/// Convert Media type to its string.
/// </summary>
/// <param name="includeParameters">Include media type parameters.</param>
/// <returns>A string.</returns>
public string ToString(bool includeParameters) => includeParameters ? FullType : FullTypeNoParameters;
/// <inheritdoc/>
public override string ToString() => ToString(false);
/// <inheritdoc/>
public override int GetHashCode()
{
return FullType.GetHashCode();
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return FullType == (obj as MediaType)?.FullType;
}
public static bool operator==(MediaType a, MediaType b)
{
return a.FullType == b.FullType;
}
public static bool operator!=(MediaType a, MediaType b)
{
return a.FullType != b.FullType;
}
/// <summary>
/// <c>application/octet-stream</c> is the default for unknown media types.
/// </summary>
public static readonly MediaType OctetStream = new MediaType(
"application/octet-stream",
new[] {"bin", "lha", "lzh", "exe", "class", "so", "dll", "img", "iso"});
}
}

198
ReMime/MediaTypeResolver.cs Normal file

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using ReMime.Platform;
namespace ReMime
{
public static class MediaTypeResolver
{
private static readonly SortedList<int, IMediaTypeResolver> s_resolvers = new SortedList<int, IMediaTypeResolver>();
private static IReadOnlyList<MediaType>? s_mediaTypes = null;
public static IEnumerable<IMediaTypeResolver> Resolvers => s_resolvers.Values;
public static IEnumerable<MediaType> KnownTypes
{
get
{
if (s_mediaTypes != null)
{
return s_mediaTypes;
}
IEnumerable<MediaType>? x = null;
foreach (IEnumerable<MediaType> types in s_resolvers.Values.Select(x => x.MediaTypes))
{
x = (x == null) ? types : x.Concat(types);
}
if (x == null)
{
return Enumerable.Empty<MediaType>();
}
return s_mediaTypes = x.DistinctBy(x => x.FullTypeNoParameters).ToImmutableList();
}
}
static MediaTypeResolver()
{
if (OperatingSystem.IsWindows())
{
AddResolver(new Win32MediaTypeResolver());
}
else if (OperatingSystem.IsLinux())
{
AddResolver(new UnixMediaTypeResolver());
// TODO: add freedesktop mime type database.
}
else if (OperatingSystem.IsMacOS())
{
AddResolver(new UnixMediaTypeResolver()); //?
}
}
public static void AddResolver(IMediaTypeResolver resolver, int priority = 9999)
{
s_resolvers.Add(priority, resolver);
}
public static bool TryResolve(ReadOnlySpan<char> path, out MediaType mediaType)
{
path = Path.GetFileName(path);
if (path.Length == 0)
{
throw new ArgumentException("Path is not a file path.");
}
ReadOnlySpan<char> span = path;
Stack<int> indices = new Stack<int>();
for (int i = 0; i < span.Length; i++)
{
if (span[i] == '.')
indices.Push(i);
}
while (indices.TryPop(out int dot))
{
string value = new string(path.Slice(dot+1)).ToLowerInvariant();
foreach (IMediaTypeResolver resolver in Resolvers)
{
if (resolver.TryResolve(value, out mediaType!))
{
return true;
}
}
}
// Could not resolve media type, oh well.
mediaType = MediaType.OctetStream;
return false;
}
public static bool TryResolve(Stream stream, out MediaType mediaType)
{
if (!stream.CanSeek)
{
throw new Exception("This stream is not seekable, cannot resolve unseekable streams.");
}
foreach (IMediaTypeResolver resolver in Resolvers)
{
if (resolver is not IMediaContentResolver contentResolver)
{
continue;
}
stream.Seek(0, SeekOrigin.Begin);
if (contentResolver.TryResolve(stream, out mediaType!))
{
return true;
}
}
mediaType = MediaType.OctetStream;
return false;
}
public static bool TryResolve(ReadOnlySpan<byte> bytes, out MediaType mediaType)
{
foreach (IMediaTypeResolver resolver in Resolvers)
{
if (resolver is not IMediaContentResolver contentResolver)
{
continue;
}
if (contentResolver.TryResolve(bytes, out mediaType!))
{
return true;
}
}
mediaType = MediaType.OctetStream;
return false;
}
public static MediaTypeResult TryResolve(ReadOnlySpan<char> path, ReadOnlySpan<byte> bytes, out MediaType mediaType)
{
if (TryResolve(bytes, out mediaType))
{
// Only return both matched if the media types agree.
return
(!TryResolve(path, out MediaType mt2) || mt2.FullTypeNoParameters != mediaType.FullTypeNoParameters)
? MediaTypeResult.Content
: MediaTypeResult.Extension | MediaTypeResult.Content;
}
else if (TryResolve(path, out mediaType))
{
return MediaTypeResult.Extension;
}
else
{
mediaType = MediaType.OctetStream;
return MediaTypeResult.None;
}
}
public static MediaTypeResult TryResolve(ReadOnlySpan<char> path, Stream stream, out MediaType mediaType)
{
if (TryResolve(stream, out mediaType))
{
// Only return both matched if the media types agree.
return
(!TryResolve(path, out MediaType mt2) || mt2.FullTypeNoParameters != mediaType.FullTypeNoParameters)
? MediaTypeResult.Content
: MediaTypeResult.Extension | MediaTypeResult.Content;
}
else if (TryResolve(path, out mediaType))
{
return MediaTypeResult.Extension;
}
else
{
mediaType = MediaType.OctetStream;
return MediaTypeResult.None;
}
}
public static MediaTypeResult TryResolve(FileInfo fileInfo, out MediaType mediaType, bool open = true)
{
if (open)
{
using Stream str = fileInfo.OpenRead();
return TryResolve(fileInfo.Name, str, out mediaType);
}
else
{
return TryResolve(fileInfo.Name, out mediaType) ? MediaTypeResult.Extension : 0;
}
}
}
}

23
ReMime/MediaTypeResult.cs Normal file

@ -0,0 +1,23 @@
using System;
namespace ReMime
{
[Flags]
public enum MediaTypeResult
{
/// <summary>
/// No match was found.
/// </summary>
None = 0,
/// <summary>
/// Matched via file extension.
/// </summary>
Extension = 1 << 0,
/// <summary>
/// Matched via both file extension and file contents.
/// </summary>
Content = 1 << 1,
}
}

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
namespace ReMime.Platform
{
public class UnixMediaTypeResolver : IMediaTypeResolver
{
private readonly Dictionary<string, MediaType> _extensionsMap = new Dictionary<string, MediaType>();
public IReadOnlyCollection<MediaType> MediaTypes { get; }
public UnixMediaTypeResolver()
{
{
bool valid = OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD();
if (!valid)
throw new PlatformNotSupportedException("This media type resolver is only for *nix systems.");
}
List<MediaType> mediaTypes = new List<MediaType>();
{
using Stream str = File.OpenRead("/etc/mime.types");
StreamReader reader = new StreamReader(str);
DigestMimeDatabase(reader, mediaTypes);
}
string localPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".mime.types");
if (File.Exists(localPath))
{
using Stream str = File.OpenRead(localPath);
StreamReader reader = new StreamReader(str);
DigestMimeDatabase(reader, mediaTypes);
}
foreach (MediaType type in mediaTypes)
{
foreach (string extension in type.Extensions)
{
_extensionsMap[extension] = type;
}
}
MediaTypes = mediaTypes.ToImmutableList();
}
public bool TryResolve(string extension, out MediaType? mediaType)
{
if (_extensionsMap.TryGetValue(extension, out mediaType))
return true;
else
return false;
}
private static readonly char[] s_delimeters = new char[] { '\t', ' ' };
private static void DigestMimeDatabase(TextReader reader, List<MediaType> types)
{
string? line;
string type = string.Empty;
List<string> extensions = new List<string>();
while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line))
continue;
line = line.Trim();
if (line.StartsWith('#'))
continue;
extensions.Clear();
string[] parts = line.Split(s_delimeters, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < parts.Length; i++)
{
if (i == 0)
{
type = parts[0];
}
else
{
extensions.Add(parts[i]);
}
}
types.Add(new MediaType(type!, extensions));
}
}
}
}

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Win32;
namespace ReMime.Platform
{
public class Win32MediaTypeResolver : IMediaTypeResolver
{
private readonly Dictionary<string, MediaType> _extensionsMap = new Dictionary<string, MediaType>();
public IReadOnlyCollection<MediaType> MediaTypes { get; }
public Win32MediaTypeResolver()
{
if (!OperatingSystem.IsWindows())
throw new PlatformNotSupportedException();
Dictionary<string, List<string>> map = new Dictionary<string, List<string>>();
List<MediaType> list = new List<MediaType>();
foreach (var name in Registry.ClassesRoot.GetSubKeyNames().Where(x => x.StartsWith('.')))
{
RegistryKey? key = Registry.ClassesRoot.OpenSubKey(name);
string? value = key?.GetValue("Content Type") as string;
if (value == null)
continue;
if (!map.TryGetValue(value, out List<string>? extensions))
{
extensions = new List<string>();
map[value] = extensions;
}
extensions.Add(name.Substring(1));
key!.Dispose();
}
foreach (var(type, extensions) in map)
{
MediaType mediaType = new MediaType(type, extensions);
list.Add(mediaType);
foreach (string extension in extensions)
{
_extensionsMap[extension] = mediaType;
}
}
MediaTypes = list.AsReadOnly();
}
public bool TryResolve(string extension, out MediaType? mediaType)
{
if (_extensionsMap.TryGetValue(extension, out mediaType))
{
return true;
}
else
{
mediaType = null;
return true;
}
}
}
}

9
ReMime/ReMime.csproj Normal file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>