diff --git a/src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs b/src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs index e5325a6..3390e82 100644 --- a/src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs +++ b/src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs @@ -9,7 +9,7 @@ namespace HyperSharp.Protocol { public static partial class HyperSerializers { - private static readonly byte[] _jsonEncodingHeader = "Content-Type: application/json; charset=utf-8\r\nContent-Length: "u8.ToArray(); + private static readonly byte[] _contentTypeJsonEncodingHeader = "Content-Type: application/json; charset=utf-8\r\nContent-Length: "u8.ToArray(); /// /// Serializes the body to the client as JSON using the method with the options. @@ -24,11 +24,11 @@ public static ValueTask JsonAsync(HyperContext context, HyperStatus status ArgumentNullException.ThrowIfNull(status); // Write Content-Type header and beginning of Content-Length header - context.Connection.StreamWriter.Write(_jsonEncodingHeader); + context.Connection.StreamWriter.Write(_contentTypeJsonEncodingHeader); byte[] body = JsonSerializer.SerializeToUtf8Bytes(status.Body, context.Connection.Server.Configuration.JsonSerializerOptions); // Finish the Content-Length header - context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string + context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes(body.Length.ToString())); context.Connection.StreamWriter.Write(_newLine); // Write body diff --git a/src/HyperSharp/Protocol/HyperSerializers/MimeTypeMapper.cs b/src/HyperSharp/Protocol/HyperSerializers/MimeTypeMapper.cs new file mode 100644 index 0000000..d48e3f1 --- /dev/null +++ b/src/HyperSharp/Protocol/HyperSerializers/MimeTypeMapper.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#else +using System.Collections.Immutable; +#endif +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance; + +namespace HyperSharp.Protocol +{ + /// + /// Holds a collection of static methods implementing for the most common of Content-Types. + /// + public static partial class HyperSerializers + { + private static readonly IReadOnlyDictionary> _mimeTypes; + private static readonly IReadOnlyDictionary> _mimeTypeSerializers; + private static readonly IReadOnlyDictionary> _fileExtensionSerializers; + + static HyperSerializers() + { + string? mimeFile = null; + if (Path.Exists("/etc/mime.types")) + { + mimeFile = "/etc/mime.types"; + } + else + { + string? home = Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "HOMEPATH" : "HOME"); + if (!string.IsNullOrWhiteSpace(home) && Path.Exists(Path.Combine(home, ".mime.types"))) + { + mimeFile = Path.Combine(home, ".mime.types"); + } + } + + Dictionary> mimeTypes = new(StringComparer.OrdinalIgnoreCase) + { + { "application/json", new List() { "json" } }, + { "text/plain", new List() { "txt" } }, + }; + + Dictionary> mimeTypeSerializers = new(StringComparer.OrdinalIgnoreCase) + { + { "application/json", new Lazy(JsonAsync) }, + { "text/plain", new Lazy(PlainTextAsync) }, + }; + + Dictionary> fileExtensionSerializers = new(StringComparer.OrdinalIgnoreCase) + { + { "json", new Lazy(JsonAsync) }, + { "txt", new Lazy(PlainTextAsync) }, + }; + + if (!string.IsNullOrWhiteSpace(mimeFile)) + { + FileStream fileStream = File.OpenRead(mimeFile); + StreamReader streamReader = new(fileStream); + + string? line; + while ((line = streamReader.ReadLine()) is not null) + { + if (line.StartsWith('#') || string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = line.Replace('\t', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries); + string mimeType = parts[0]; + Lazy serializer = new(() => (context, status, cancellationToken) => + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(status); + + // Write Content-Type header and beginning of Content-Length header + context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes($"Content-Type: {mimeType}\r\nContent-Length: ")); + + byte[] body = Encoding.UTF8.GetBytes(status.Body?.ToString() ?? ""); + + // Write Content-Length header + context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes(body.Length.ToString())); + context.Connection.StreamWriter.Write(_newLine); + + // Write body + context.Connection.StreamWriter.Write(_newLine); + context.Connection.StreamWriter.Write(body); + + return ValueTask.FromResult(true); + }); + + mimeTypeSerializers[mimeType] = serializer; + + List fileExtensions = mimeTypes.TryGetValue(mimeType, out List? extensions) ? extensions : new List(); + foreach (string fileExtension in parts[1..]) + { + fileExtensionSerializers[fileExtension] = serializer; + fileExtensions.Add(fileExtension); + } + } + } + +#if NET8_0_OR_GREATER + _fileExtensionSerializers = fileExtensionSerializers.ToFrozenDictionary(); + _mimeTypeSerializers = mimeTypeSerializers.ToFrozenDictionary(); +#else + _fileExtensionSerializers = fileExtensionSerializers.ToImmutableDictionary(); + _mimeTypeSerializers = mimeTypeSerializers.ToImmutableDictionary(); +#endif + } + + /// + /// Gets the for the specified MIME type. + /// + /// The MIME type to get the for. + /// The to get the for. + /// The for the specified MIME type. Defaults to if the MIME type is not found. + public static HyperSerializerDelegate GetSerializerFromMimeType(string mimeType, HyperContext? context = null) + { + if (context is not null && context.Headers.TryGetValues("Accept", out List? accept) && !accept.Contains(mimeType) && mimeType.StartsWith("text", StringComparison.OrdinalIgnoreCase)) + { + mimeType = "text/html"; + } + + return _mimeTypeSerializers.TryGetValue(mimeType, out Lazy? serializer) ? serializer.Value : PlainTextAsync; + } + + /// + /// Gets the associated with the specified file extension. + /// + /// The file extension to get the for. + /// The for the specified file extension. Defaults to if the file extension is not found. + public static HyperSerializerDelegate GetSerializerFromFileExtension(string fileExtension) => _fileExtensionSerializers.TryGetValue(fileExtension, out Lazy? serializer) + ? serializer.Value + : PlainTextAsync; + + public static string GetMimeTypeFromFileExtension(string fileExtension) => _fileExtensionSerializers.TryGetValue(fileExtension, out Lazy? serializer) + ? serializer.Value.Method.Name switch + { + nameof(JsonAsync) => "application/json", + nameof(PlainTextAsync) => "text/plain", + _ => "text/html", + } + : "text/html"; + } +} diff --git a/src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs b/src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs index e697e8e..4fdd977 100644 --- a/src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs +++ b/src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs @@ -12,7 +12,7 @@ namespace HyperSharp.Protocol public static partial class HyperSerializers { private static readonly byte[] _newLine = "\r\n"u8.ToArray(); - private static readonly byte[] _contentTypeJsonEncodingHeader = "Content-Type: application/json; charset=utf-8\r\nContent-Length: "u8.ToArray(); + private static readonly byte[] _contentTypeTextEncodingHeader = "Content-Type: text/plain; charset=utf-8\r\nContent-Length: "u8.ToArray(); /// /// Serializes the body to the client as plain text using the method with the encoding. @@ -24,12 +24,12 @@ public static ValueTask PlainTextAsync(HyperContext context, HyperStatus s ArgumentNullException.ThrowIfNull(status); // Write Content-Type header and beginning of Content-Length header - context.Connection.StreamWriter.Write(_contentTypeJsonEncodingHeader); + context.Connection.StreamWriter.Write(_contentTypeTextEncodingHeader); byte[] body = Encoding.UTF8.GetBytes(status.Body?.ToString() ?? ""); // Write Content-Length header - context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string + context.Connection.StreamWriter.Write(Encoding.ASCII.GetBytes(body.Length.ToString())); context.Connection.StreamWriter.Write(_newLine); // Write body