Skip to content

Commit

Permalink
Fix mime type headers
Browse files Browse the repository at this point in the history
  • Loading branch information
OoLunar committed Nov 18, 2023
1 parent 131f026 commit 32a059b
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 6 deletions.
6 changes: 3 additions & 3 deletions src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/// <summary>
/// Serializes the body to the client as JSON using the <see cref="JsonSerializer.SerializeToUtf8Bytes{TValue}(TValue, JsonSerializerOptions?)"/> method with the <see cref="HyperConfiguration.JsonSerializerOptions"/> options.
Expand All @@ -24,11 +24,11 @@ public static ValueTask<bool> JsonAsync(HyperContext context, HyperStatus status
ArgumentNullException.ThrowIfNull(status);

// Write Content-Type header and beginning of Content-Length header
context.Connection.StreamWriter.Write<byte>(_jsonEncodingHeader);
context.Connection.StreamWriter.Write<byte>(_contentTypeJsonEncodingHeader);
byte[] body = JsonSerializer.SerializeToUtf8Bytes(status.Body, context.Connection.Server.Configuration.JsonSerializerOptions);

// Finish the Content-Length header
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString()));
context.Connection.StreamWriter.Write<byte>(_newLine);

// Write body
Expand Down
149 changes: 149 additions & 0 deletions src/HyperSharp/Protocol/HyperSerializers/MimeTypeMapper.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Holds a collection of static methods implementing <see cref="HyperSerializerDelegate"/> for the most common of Content-Types.
/// </summary>
public static partial class HyperSerializers
{
private static readonly IReadOnlyDictionary<string, IReadOnlyList<string>> _mimeTypes;
private static readonly IReadOnlyDictionary<string, Lazy<HyperSerializerDelegate>> _mimeTypeSerializers;
private static readonly IReadOnlyDictionary<string, Lazy<HyperSerializerDelegate>> _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<string, List<string>> mimeTypes = new(StringComparer.OrdinalIgnoreCase)
{
{ "application/json", new List<string>() { "json" } },
{ "text/plain", new List<string>() { "txt" } },
};

Dictionary<string, Lazy<HyperSerializerDelegate>> mimeTypeSerializers = new(StringComparer.OrdinalIgnoreCase)
{
{ "application/json", new Lazy<HyperSerializerDelegate>(JsonAsync) },
{ "text/plain", new Lazy<HyperSerializerDelegate>(PlainTextAsync) },
};

Dictionary<string, Lazy<HyperSerializerDelegate>> fileExtensionSerializers = new(StringComparer.OrdinalIgnoreCase)
{
{ "json", new Lazy<HyperSerializerDelegate>(JsonAsync) },
{ "txt", new Lazy<HyperSerializerDelegate>(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<HyperSerializerDelegate> 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<byte>(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<byte>(Encoding.ASCII.GetBytes(body.Length.ToString()));
context.Connection.StreamWriter.Write<byte>(_newLine);

// Write body
context.Connection.StreamWriter.Write<byte>(_newLine);
context.Connection.StreamWriter.Write<byte>(body);

return ValueTask.FromResult(true);
});

mimeTypeSerializers[mimeType] = serializer;

List<string> fileExtensions = mimeTypes.TryGetValue(mimeType, out List<string>? extensions) ? extensions : new List<string>();
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
}

/// <summary>
/// Gets the <see cref="HyperSerializerDelegate"/> for the specified MIME type.
/// </summary>
/// <param name="mimeType">The MIME type to get the <see cref="HyperSerializerDelegate"/> for.</param>
/// <param name="context">The <see cref="HyperContext"/> to get the <see cref="HyperSerializerDelegate"/> for.</param>
/// <returns>The <see cref="HyperSerializerDelegate"/> for the specified MIME type. Defaults to <see cref="PlainTextAsync"/> if the MIME type is not found.</returns>
public static HyperSerializerDelegate GetSerializerFromMimeType(string mimeType, HyperContext? context = null)
{
if (context is not null && context.Headers.TryGetValues("Accept", out List<string>? accept) && !accept.Contains(mimeType) && mimeType.StartsWith("text", StringComparison.OrdinalIgnoreCase))
{
mimeType = "text/html";
}

return _mimeTypeSerializers.TryGetValue(mimeType, out Lazy<HyperSerializerDelegate>? serializer) ? serializer.Value : PlainTextAsync;
}

/// <summary>
/// Gets the <see cref="HyperSerializerDelegate"/> associated with the specified file extension.
/// </summary>
/// <param name="fileExtension">The file extension to get the <see cref="HyperSerializerDelegate"/> for.</param>
/// <returns>The <see cref="HyperSerializerDelegate"/> for the specified file extension. Defaults to <see cref="PlainTextAsync"/> if the file extension is not found.</returns>
public static HyperSerializerDelegate GetSerializerFromFileExtension(string fileExtension) => _fileExtensionSerializers.TryGetValue(fileExtension, out Lazy<HyperSerializerDelegate>? serializer)
? serializer.Value
: PlainTextAsync;

public static string GetMimeTypeFromFileExtension(string fileExtension) => _fileExtensionSerializers.TryGetValue(fileExtension, out Lazy<HyperSerializerDelegate>? serializer)
? serializer.Value.Method.Name switch
{
nameof(JsonAsync) => "application/json",
nameof(PlainTextAsync) => "text/plain",
_ => "text/html",
}
: "text/html";
}
}
6 changes: 3 additions & 3 deletions src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/// <summary>
/// Serializes the body to the client as plain text using the <see cref="object.ToString"/> method with the <see cref="Encoding.UTF8"/> encoding.
Expand All @@ -24,12 +24,12 @@ public static ValueTask<bool> PlainTextAsync(HyperContext context, HyperStatus s
ArgumentNullException.ThrowIfNull(status);

// Write Content-Type header and beginning of Content-Length header
context.Connection.StreamWriter.Write<byte>(_contentTypeJsonEncodingHeader);
context.Connection.StreamWriter.Write<byte>(_contentTypeTextEncodingHeader);

byte[] body = Encoding.UTF8.GetBytes(status.Body?.ToString() ?? "");

// Write Content-Length header
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString()));
context.Connection.StreamWriter.Write<byte>(_newLine);

// Write body
Expand Down

0 comments on commit 32a059b

Please sign in to comment.