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:
commit
a89e51f5e7
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.atom
|
||||
**/.rider
|
||||
**/bin
|
||||
**/obj
|
||||
|
||||
|
22
README.md
Normal file
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
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));
|
||||
}
|
||||
}
|
||||
}
|
17
ReMime.Cli/ReMime.Cli.csproj
Normal file
17
ReMime.Cli/ReMime.Cli.csproj
Normal file
@ -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>
|
2
ReMime.Tests/GlobalUsings.cs
Normal file
2
ReMime.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
||||
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
global using ReMime;
|
77
ReMime.Tests/MediaTypesByExtension.cs
Normal file
77
ReMime.Tests/MediaTypesByExtension.cs
Normal file
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
ReMime.Tests/ReMime.Tests.csproj
Normal file
22
ReMime.Tests/ReMime.Tests.csproj
Normal file
@ -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
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
ReMime/ContentResolvers/Audio/.gitkeep
Normal file
0
ReMime/ContentResolvers/Audio/.gitkeep
Normal file
0
ReMime/ContentResolvers/Font/.gitkeep
Normal file
0
ReMime/ContentResolvers/Font/.gitkeep
Normal file
0
ReMime/ContentResolvers/Image/.gitkeep
Normal file
0
ReMime/ContentResolvers/Image/.gitkeep
Normal file
0
ReMime/ContentResolvers/Message/.gitkeep
Normal file
0
ReMime/ContentResolvers/Message/.gitkeep
Normal file
0
ReMime/ContentResolvers/Model/.gitkeep
Normal file
0
ReMime/ContentResolvers/Model/.gitkeep
Normal file
3
ReMime/ContentResolvers/README.md
Normal file
3
ReMime/ContentResolvers/README.md
Normal file
@ -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
ReMime/ContentResolvers/Text/.gitkeep
Normal file
0
ReMime/ContentResolvers/Text/.gitkeep
Normal file
0
ReMime/ContentResolvers/Video/.gitkeep
Normal file
0
ReMime/ContentResolvers/Video/.gitkeep
Normal file
25
ReMime/IMediaContentResolver.cs
Normal file
25
ReMime/IMediaContentResolver.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
21
ReMime/IMediaTypeResolver.cs
Normal file
21
ReMime/IMediaTypeResolver.cs
Normal file
@ -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
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
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
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,
|
||||
}
|
||||
}
|
92
ReMime/Platform/UnixMediaTypeResolver.cs
Normal file
92
ReMime/Platform/UnixMediaTypeResolver.cs
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
ReMime/Platform/Win32MediaTypeResolver.cs
Normal file
66
ReMime/Platform/Win32MediaTypeResolver.cs
Normal file
@ -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
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>
|
Loading…
Reference in New Issue
Block a user