From 044e99c0a76dfd2a6d5a42d88f1494d04b4c894e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Fri, 29 May 2026 07:00:04 +0200 Subject: [PATCH 1/3] Add support for pre-compressed static files --- API/Protocol/IRequestTarget.cs | 6 +- Engine/Internal/Protocol/ClientHandler.cs | 1 - Engine/Shared/Types/RequestTarget.cs | 40 +++++++- Modules/Compression/PreCompressedResources.cs | 17 ++++ .../PreCompressedResourceHandler.cs | 98 +++++++++++++++++++ .../PreCompressedResourceHandlerBuilder.cs | 36 +++++++ Modules/Compression/Providers/AcceptHeader.cs | 55 +++++++++++ .../Providers/CompressionConcern.cs | 53 +--------- .../Provider/ListingRouter.cs | 2 +- Modules/IO/FileSystem/DirectoryContainer.cs | 18 ++-- Modules/IO/Providers/ResourceHandler.cs | 13 +-- Modules/IO/ResourceTreeExtensions.cs | 4 +- Modules/IO/Streaming/ResourceContent.cs | 9 +- .../Routing/Segments/RegexSegment.cs | 2 +- .../Routing/Segments/SimpleVariableSegment.cs | 3 +- .../Routing/Segments/StringSegment.cs | 2 +- .../Provider/StaticWebsiteHandler.cs | 2 +- .../Modules/Archives/FunctionalTests.cs | 4 +- .../Acceptance/Modules/IO/VirtualTreeTests.cs | 4 +- .../Modules/Websockets/ProviderTests.cs | 2 - 20 files changed, 278 insertions(+), 93 deletions(-) create mode 100644 Modules/Compression/PreCompressedResources.cs create mode 100644 Modules/Compression/PreCompression/PreCompressedResourceHandler.cs create mode 100644 Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs create mode 100644 Modules/Compression/Providers/AcceptHeader.cs diff --git a/API/Protocol/IRequestTarget.cs b/API/Protocol/IRequestTarget.cs index 983ed531..4adbd27b 100644 --- a/API/Protocol/IRequestTarget.cs +++ b/API/Protocol/IRequestTarget.cs @@ -6,12 +6,14 @@ public interface IRequestTarget PathSegment? Current { get; } bool IsLast { get; } - + bool HasTrailingSlash { get; } void Advance(int segments = 1); - ReadOnlyMemory? Next(int offset); + PathSegment? Next(int offset); + + IRequestTarget CopyAndAppend(ReadOnlyMemory suffix); string AsString(bool decode = true, bool remainingOnly = false); diff --git a/Engine/Internal/Protocol/ClientHandler.cs b/Engine/Internal/Protocol/ClientHandler.cs index 41591ce2..bbeab919 100644 --- a/Engine/Internal/Protocol/ClientHandler.cs +++ b/Engine/Internal/Protocol/ClientHandler.cs @@ -9,7 +9,6 @@ using GenHTTP.Engine.Shared.Types; using Glyph11; using Glyph11.Parser; -using Glyph11.Parser.Hardened; using Glyph11.Parser.UltraHardened; using Glyph11.Protocol; using StringContent = GenHTTP.Modules.IO.Strings.StringContent; diff --git a/Engine/Shared/Types/RequestTarget.cs b/Engine/Shared/Types/RequestTarget.cs index 32f288cb..96496eb5 100644 --- a/Engine/Shared/Types/RequestTarget.cs +++ b/Engine/Shared/Types/RequestTarget.cs @@ -53,7 +53,7 @@ public void Apply(ReadOnlyMemory path) MoveNext(); } - public ReadOnlyMemory? Next(int offset) + public PathSegment? Next(int offset) { if (Current == null) { @@ -62,7 +62,7 @@ public void Apply(ReadOnlyMemory path) if (offset == 0) { - return Current.Value.Value; + return Current; } var span = _path.Span; @@ -86,7 +86,7 @@ public void Apply(ReadOnlyMemory path) if (skip == offset) { - return idx < 0 ? _path.Slice(start, length - start) : _path.Slice(start, idx); + return new(idx < 0 ? _path.Slice(start, length - start) : _path.Slice(start, idx)); } i += idx < 0 ? (length - start) : idx; @@ -143,6 +143,38 @@ private void MoveNext() } } + public IRequestTarget CopyAndAppend(ReadOnlyMemory suffix) + { + var original = _path; + + var buffer = new byte[original.Length + suffix.Length]; + + original.Span.CopyTo(buffer); + suffix.Span.CopyTo(buffer.AsSpan(original.Length)); + + var combined = new ReadOnlyMemory(buffer); + + var copy = new RequestTarget + { + _path = combined, + _offset = _offset, + _segmentStart = _segmentStart + }; + + if (Current != null) + { + var span = combined.Span; + + var idx = span[_segmentStart..].IndexOf((byte)'/'); + + copy.Current = idx < 0 + ? new PathSegment(combined[_segmentStart..]) + : new PathSegment(combined.Slice(_segmentStart, idx)); + } + + return copy; + } + public string AsString(bool decode = true, bool remainingOnly = false) { ReadOnlyMemory slice; @@ -162,7 +194,7 @@ public string AsString(bool decode = true, bool remainingOnly = false) } var stringPath = Encoding.ASCII.GetString(slice.Span); - + if (decode && stringPath.Contains('%')) { return Uri.UnescapeDataString(stringPath); diff --git a/Modules/Compression/PreCompressedResources.cs b/Modules/Compression/PreCompressedResources.cs new file mode 100644 index 00000000..92d948d2 --- /dev/null +++ b/Modules/Compression/PreCompressedResources.cs @@ -0,0 +1,17 @@ +using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.Compression.PreCompression; + +namespace GenHTTP.Modules.Compression; + +public class PreCompressedResources +{ + + public static PreCompressedResourceHandlerBuilder From(IBuilder tree) + => From(tree.Build()); + + public static PreCompressedResourceHandlerBuilder From(IResourceTree tree) + => new(tree); + +} diff --git a/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs new file mode 100644 index 00000000..fad73ff8 --- /dev/null +++ b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs @@ -0,0 +1,98 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Compression.Providers; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.IO.Streaming; + +namespace GenHTTP.Modules.Compression.PreCompression; + +public sealed class PreCompressedResourceHandler : IHandler +{ + private readonly IResourceTree _tree; + + private readonly IHandler _regular; + + private readonly List _algorithms; + + public PreCompressedResourceHandler(IResourceTree tree, List algorithms) + { + _tree = tree; + _algorithms = algorithms; + + _regular = Resources.From(tree).Build(); + } + + public ValueTask PrepareAsync() => _regular.PrepareAsync(); + + public async ValueTask HandleAsync(IRequest request) + { + var target = request.Header.Target; + + var file = GetFileName(target); + + if (file is not null) + { + var acceptHeader = request.Header.Headers.GetEntry(KnownHeaders.Accept); + + if (acceptHeader != null) + { + var supported = AcceptHeader.ParseSupported(acceptHeader.Value.Span); + + foreach (var algorithm in _algorithms.OrderByDescending(a => (int)a.Priority)) + { + if (supported.Contains(algorithm.Name)) + { + var extension = new byte[algorithm.Name.Value.Length + 1]; + extension[0] = (byte)'.'; + algorithm.Name.Value.Span.CopyTo(extension.AsSpan(1)); + + var newTarget = target.CopyAndAppend(extension); + + var (_, resource) = await _tree.FindAsync(newTarget); + + if (resource is not null) + { + var contentType = file.GuessContentType() ?? ContentType.ApplicationOctetStream; + var content = new ResourceContent(resource, contentType, algorithm.Name.Value); + + return request.Respond() + .Content(content) + .Build(); + } + } + } + } + } + + return await _regular.HandleAsync(request); + } + + private static string? GetFileName(IRequestTarget target) + { + if (target.HasTrailingSlash) + { + return null; + } + + var offset = 0; + + PathSegment? current = null; + + while (true) + { + var next = target.Next(offset++); + + if (next == null) + { + break; + } + + current = next.Value; + } + + return current?.Decode(); + } + +} diff --git a/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs b/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs new file mode 100644 index 00000000..311ab124 --- /dev/null +++ b/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs @@ -0,0 +1,36 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Content.IO; + +namespace GenHTTP.Modules.Compression.PreCompression; + +public sealed class PreCompressedResourceHandlerBuilder : IHandlerBuilder +{ + private readonly List _concerns = []; + + private readonly List _algorithms = []; + + private readonly IResourceTree _source; + + public PreCompressedResourceHandlerBuilder(IResourceTree source) + { + _source = source; + } + + public PreCompressedResourceHandlerBuilder Add(ICompressionAlgorithm algorithm) + { + _algorithms.Add(algorithm); + return this; + } + + public PreCompressedResourceHandlerBuilder Add(IConcernBuilder concern) + { + _concerns.Add(concern); + return this; + } + + public IHandler Build() + { + return Concerns.Chain(_concerns, new PreCompressedResourceHandler(_source, _algorithms)); + } + +} diff --git a/Modules/Compression/Providers/AcceptHeader.cs b/Modules/Compression/Providers/AcceptHeader.cs new file mode 100644 index 00000000..b23177e7 --- /dev/null +++ b/Modules/Compression/Providers/AcceptHeader.cs @@ -0,0 +1,55 @@ +using GenHTTP.Api.Content.IO; + +namespace GenHTTP.Modules.Compression.Providers; + +public static class AcceptHeader +{ + + public static HashSet ParseSupported(ReadOnlySpan acceptHeader) + { + var result = new HashSet(); + var start = 0; + + while (start < acceptHeader.Length) + { + var comma = acceptHeader[start..].IndexOf((byte)','); + var end = comma >= 0 ? start + comma : acceptHeader.Length; + + var token = acceptHeader.Slice(start, end - start); + + var semicolon = token.IndexOf((byte)';'); + var nameSpan = semicolon >= 0 ? token[..semicolon] : token; + + var part = TrimAscii(nameSpan); + + if (!part.IsEmpty) + { + result.Add(new(part.ToArray())); + } + + start = end + 1; + } + + return result; + } + + private static ReadOnlySpan TrimAscii(ReadOnlySpan span) + { + var start = 0; + var end = span.Length - 1; + + while (start <= end && IsAsciiWhiteSpace(span[start])) + start++; + + while (end >= start && IsAsciiWhiteSpace(span[end])) + end--; + + return span.Slice(start, end - start + 1); + } + + private static bool IsAsciiWhiteSpace(byte b) + { + return b == (byte)' ' || b == (byte)'\t'; + } + +} diff --git a/Modules/Compression/Providers/CompressionConcern.cs b/Modules/Compression/Providers/CompressionConcern.cs index eb25fd86..925854f4 100644 --- a/Modules/Compression/Providers/CompressionConcern.cs +++ b/Modules/Compression/Providers/CompressionConcern.cs @@ -60,7 +60,7 @@ public CompressionConcern(IHandler content, IReadOnlyDictionary HandleAsync(IRequest request) { var acceptEncoding = request.Header.Headers.GetEntry(KnownHeaders.AcceptEncoding); - + var response = await Content.HandleAsync(request); if (response == null) @@ -78,7 +78,7 @@ public CompressionConcern(IHandler content, IReadOnlyDictionary= MinimumSize; } - - private static HashSet ParseSupported(ReadOnlySpan acceptHeader) - { - var result = new HashSet(); - var start = 0; - - while (start < acceptHeader.Length) - { - var comma = acceptHeader[start..].IndexOf((byte)','); - var end = comma >= 0 ? start + comma : acceptHeader.Length; - - var token = acceptHeader.Slice(start, end - start); - - var semicolon = token.IndexOf((byte)';'); - var nameSpan = semicolon >= 0 ? token[..semicolon] : token; - - var part = TrimAscii(nameSpan); - - if (!part.IsEmpty) - { - result.Add(new(part.ToArray())); - } - - start = end + 1; - } - - return result; - } - - private static ReadOnlySpan TrimAscii(ReadOnlySpan span) - { - var start = 0; - var end = span.Length - 1; - - while (start <= end && IsAsciiWhiteSpace(span[start])) - start++; - - while (end >= start && IsAsciiWhiteSpace(span[end])) - end--; - - return span.Slice(start, end - start + 1); - } - - private static bool IsAsciiWhiteSpace(byte b) - { - return b == (byte)' ' || b == (byte)'\t'; - } public ValueTask PrepareAsync() => Content.PrepareAsync(); diff --git a/Modules/DirectoryBrowsing/Provider/ListingRouter.cs b/Modules/DirectoryBrowsing/Provider/ListingRouter.cs index 161ae1cf..07462f45 100644 --- a/Modules/DirectoryBrowsing/Provider/ListingRouter.cs +++ b/Modules/DirectoryBrowsing/Provider/ListingRouter.cs @@ -28,7 +28,7 @@ public ListingRouter(IResourceTree tree) public async ValueTask HandleAsync(IRequest request) { - var (node, resource) = await Tree.Find(request.Header.Target); + var (node, resource) = await Tree.FindAsync(request.Header.Target); if (resource is not null) { diff --git a/Modules/IO/FileSystem/DirectoryContainer.cs b/Modules/IO/FileSystem/DirectoryContainer.cs index 05be8041..976adae0 100644 --- a/Modules/IO/FileSystem/DirectoryContainer.cs +++ b/Modules/IO/FileSystem/DirectoryContainer.cs @@ -1,13 +1,10 @@ -using System.Collections.Concurrent; - -using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.IO.FileSystem; internal class DirectoryContainer : IResourceContainer { - private readonly ConcurrentDictionary _resourceCache = new(); #region Initialization @@ -68,12 +65,15 @@ public ValueTask> GetResources() public ValueTask TryGetResourceAsync(PathSegment segment) { - return new ValueTask(_resourceCache.GetOrAdd(segment, static (key, dir) => + var path = Path.Combine(Directory.FullName, segment.Decode()); + var file = new FileInfo(path); + + if (File.Exists(file.FullName)) { - var path = Path.Combine(dir.FullName, key.Decode()); - var file = new FileInfo(path); - return file.Exists ? Resource.FromFile(file).Build() : null; - }, Directory)); + return new(Resource.FromFile(file).Build()); + } + + return default; } #endregion diff --git a/Modules/IO/Providers/ResourceHandler.cs b/Modules/IO/Providers/ResourceHandler.cs index f08ace9d..4d2f3eec 100644 --- a/Modules/IO/Providers/ResourceHandler.cs +++ b/Modules/IO/Providers/ResourceHandler.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; - -using GenHTTP.Api.Content; +using GenHTTP.Api.Content; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; @@ -10,8 +8,7 @@ namespace GenHTTP.Modules.IO.Providers; public sealed class ResourceHandler : IHandler { - private readonly ConcurrentDictionary _contentCache = new(); - + #region Get-/Setters private IResourceTree Tree { get; } @@ -33,14 +30,12 @@ public ResourceHandler(IResourceTree tree) public async ValueTask HandleAsync(IRequest request) { - var (_, resource) = await Tree.Find(request.Header.Target); + var (_, resource) = await Tree.FindAsync(request.Header.Target); if (resource is not null) { - var content = _contentCache.GetOrAdd(resource, static r => new ResourceContent(r)); - return request.Respond() - .Content(content) + .Content(new ResourceContent(resource)) .Build(); } diff --git a/Modules/IO/ResourceTreeExtensions.cs b/Modules/IO/ResourceTreeExtensions.cs index 6180fa42..95d79b06 100644 --- a/Modules/IO/ResourceTreeExtensions.cs +++ b/Modules/IO/ResourceTreeExtensions.cs @@ -13,7 +13,7 @@ public static class ResourceTreeExtensions /// The node used to resolve the target /// The target to be resolved /// A tuple of the node and resource resolved from the container (or both null, if they could not be resolved) - public static async ValueTask<(IResourceContainer? node, IResource? resource)> Find(this IResourceContainer node, IRequestTarget target) + public static async ValueTask<(IResourceContainer? node, IResource? resource)> FindAsync(this IResourceContainer node, IRequestTarget target) { var current = target.Current; @@ -40,7 +40,7 @@ public static class ResourceTreeExtensions if ((childNode = await node.TryGetNodeAsync(current.Value)) != null) { target.Advance(); - return await childNode.Find(target); + return await childNode.FindAsync(target); } } diff --git a/Modules/IO/Streaming/ResourceContent.cs b/Modules/IO/Streaming/ResourceContent.cs index 1f80e162..a5d755a4 100644 --- a/Modules/IO/Streaming/ResourceContent.cs +++ b/Modules/IO/Streaming/ResourceContent.cs @@ -5,7 +5,7 @@ namespace GenHTTP.Modules.IO.Streaming; public sealed class ResourceContent : IResponseContent { - + #region Get-/Setters public ulong? Length => Resource.Length; @@ -14,16 +14,17 @@ public sealed class ResourceContent : IResponseContent private IResource Resource { get; } - public ReadOnlyMemory? Encoding => null; + public ReadOnlyMemory? Encoding { get; } #endregion - + #region Initialization - public ResourceContent(IResource resource, ContentType? contentType = null) + public ResourceContent(IResource resource, ContentType? contentType = null, ReadOnlyMemory? encoding = null) { Resource = resource; Type = contentType ?? resource.GuessContentType(); + Encoding = encoding; } #endregion diff --git a/Modules/Reflection/Routing/Segments/RegexSegment.cs b/Modules/Reflection/Routing/Segments/RegexSegment.cs index 67bc19bd..cfde66ab 100644 --- a/Modules/Reflection/Routing/Segments/RegexSegment.cs +++ b/Modules/Reflection/Routing/Segments/RegexSegment.cs @@ -56,7 +56,7 @@ public RegexSegment(string definition) return (false, 0); } - var stringPart = Encoding.ASCII.GetString(part.Value.Span); + var stringPart = part.Value.ToString(); var match = _matcher.Match(stringPart); diff --git a/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs b/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs index aee99a37..97a7cd34 100644 --- a/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs +++ b/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs @@ -1,4 +1,3 @@ -using System.Text; using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.Reflection.Routing.Segments; @@ -25,7 +24,7 @@ internal sealed class SimpleVariableSegment(string variableName) : IRoutingSegme return (false, 0); } - argumentSink.Add(variableName, Encoding.ASCII.GetString(part.Value.Span)); // todo: optimize? + argumentSink.Add(variableName, part.Value.ToString()); // todo: optimize? return (true, 1); } diff --git a/Modules/Reflection/Routing/Segments/StringSegment.cs b/Modules/Reflection/Routing/Segments/StringSegment.cs index a181a966..24c3790f 100644 --- a/Modules/Reflection/Routing/Segments/StringSegment.cs +++ b/Modules/Reflection/Routing/Segments/StringSegment.cs @@ -17,7 +17,7 @@ internal sealed class StringSegment(string segment) : IRoutingSegment { var next = target.Next(offset); - if (next?.Span.SequenceEqual(_segmentBytes.Span) ?? false) + if (next?.Value.Span.SequenceEqual(_segmentBytes.Span) ?? false) { return (true, 1); } diff --git a/Modules/StaticWebsites/Provider/StaticWebsiteHandler.cs b/Modules/StaticWebsites/Provider/StaticWebsiteHandler.cs index 7b22aab0..3dafc5bd 100644 --- a/Modules/StaticWebsites/Provider/StaticWebsiteHandler.cs +++ b/Modules/StaticWebsites/Provider/StaticWebsiteHandler.cs @@ -38,7 +38,7 @@ public StaticWebsiteHandler(IResourceTree tree) if (target.HasTrailingSlash) { - var (node, _) = await Tree.Find(target); + var (node, _) = await Tree.FindAsync(target); if (node != null) { diff --git a/Testing/Acceptance/Modules/Archives/FunctionalTests.cs b/Testing/Acceptance/Modules/Archives/FunctionalTests.cs index 0f9426ce..fef512e1 100644 --- a/Testing/Acceptance/Modules/Archives/FunctionalTests.cs +++ b/Testing/Acceptance/Modules/Archives/FunctionalTests.cs @@ -30,7 +30,7 @@ public async Task TestFormats() var target = new RequestTarget(); target.Apply("/SubDir/SubDir/SubFile.txt"u8.ToArray()); - var (foundNode, foundFile) = await tree.Find(target); + var (foundNode, foundFile) = await tree.FindAsync(target); Assert.IsNotNull(foundNode); Assert.IsNotNull(foundFile); @@ -54,7 +54,7 @@ public async Task TestNonSeekableResource() var target = new RequestTarget(); target.Apply("/SubDir/SubDir/SubFile.txt"u8.ToArray()); - var (_, foundFile) = await tree.Find(target); + var (_, foundFile) = await tree.FindAsync(target); Assert.AreEqual("3", await GetContentAsync(foundFile!)); } diff --git a/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs b/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs index 9baf6e8a..b4f97853 100644 --- a/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs +++ b/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs @@ -24,7 +24,7 @@ public async Task TestNestedTree() .Add("r", tree) .Build(); - var (node, file) = await virt.Find(GetTarget("/r/File.txt")); + var (node, file) = await virt.FindAsync(GetTarget("/r/File.txt")); Assert.IsNotNull(node); Assert.IsNotNull(file); @@ -41,7 +41,7 @@ public async Task TestResource() .Add("res.txt", Resource.FromString("Blubb")) .Build(); - var (node, file) = await virt.Find(GetTarget("/res.txt")); + var (node, file) = await virt.FindAsync(GetTarget("/res.txt")); Assert.IsNotNull(node); Assert.IsNotNull(file); diff --git a/Testing/Acceptance/Modules/Websockets/ProviderTests.cs b/Testing/Acceptance/Modules/Websockets/ProviderTests.cs index b2bca600..67bcd7d9 100644 --- a/Testing/Acceptance/Modules/Websockets/ProviderTests.cs +++ b/Testing/Acceptance/Modules/Websockets/ProviderTests.cs @@ -1,7 +1,5 @@ -using System.Buffers; using System.Net; using GenHTTP.Api.Protocol; -using GenHTTP.Engine.Internal.Protocol; using GenHTTP.Modules.IO; using GenHTTP.Modules.IO.Streaming; using GenHTTP.Modules.Websockets.Provider; From 78c02b6ffe755c4734da83f7c3f2dace2aedc2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Fri, 29 May 2026 07:24:22 +0200 Subject: [PATCH 2/3] Add tests (still failing) --- Modules/Compression/PreCompressedResources.cs | 2 +- .../PreCompressedResourceHandler.cs | 45 ++++++------- .../GenHTTP.Testing.Acceptance.csproj | 2 + .../Compression/PreCompressionTests.cs | 61 ++++++++++++++++++ .../Modules/IO/ResourceTreeTests.cs | 2 +- .../Resources/LargeTemplate.html.br | Bin 0 -> 700 bytes 6 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 Testing/Acceptance/Modules/Compression/PreCompressionTests.cs create mode 100644 Testing/Acceptance/Resources/LargeTemplate.html.br diff --git a/Modules/Compression/PreCompressedResources.cs b/Modules/Compression/PreCompressedResources.cs index 92d948d2..1bcbc567 100644 --- a/Modules/Compression/PreCompressedResources.cs +++ b/Modules/Compression/PreCompressedResources.cs @@ -5,7 +5,7 @@ namespace GenHTTP.Modules.Compression; -public class PreCompressedResources +public static class PreCompressedResources { public static PreCompressedResourceHandlerBuilder From(IBuilder tree) diff --git a/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs index fad73ff8..449df033 100644 --- a/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs +++ b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs @@ -1,7 +1,6 @@ using GenHTTP.Api.Content; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; - using GenHTTP.Modules.Compression.Providers; using GenHTTP.Modules.IO; using GenHTTP.Modules.IO.Streaming; @@ -32,35 +31,37 @@ public PreCompressedResourceHandler(IResourceTree tree, List (int)a.Priority)) + if (acceptHeader != null) + { + var supported = AcceptHeader.ParseSupported(acceptHeader.Value.Span); + + foreach (var algorithm in _algorithms.OrderByDescending(a => (int)a.Priority)) + { + if (supported.Contains(algorithm.Name)) { - if (supported.Contains(algorithm.Name)) - { - var extension = new byte[algorithm.Name.Value.Length + 1]; - extension[0] = (byte)'.'; - algorithm.Name.Value.Span.CopyTo(extension.AsSpan(1)); + var extension = new byte[algorithm.Name.Value.Length + 1]; + extension[0] = (byte)'.'; + algorithm.Name.Value.Span.CopyTo(extension.AsSpan(1)); - var newTarget = target.CopyAndAppend(extension); + var newTarget = target.CopyAndAppend(extension); - var (_, resource) = await _tree.FindAsync(newTarget); + var (_, resource) = await _tree.FindAsync(newTarget); - if (resource is not null) - { - var contentType = file.GuessContentType() ?? ContentType.ApplicationOctetStream; - var content = new ResourceContent(resource, contentType, algorithm.Name.Value); + if (resource is not null) + { + var contentType = file.GuessContentType() ?? ContentType.ApplicationOctetStream; + var content = new ResourceContent(resource, contentType, algorithm.Name.Value); - return request.Respond() - .Content(content) - .Build(); - } + return request.Respond() + .Content(content) + .Build(); } } } diff --git a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj index 7fe2faa7..b9790f4c 100644 --- a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj +++ b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj @@ -31,6 +31,8 @@ + + diff --git a/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs b/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs new file mode 100644 index 00000000..d239c5a9 --- /dev/null +++ b/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Http.Headers; +using GenHTTP.Modules.Compression; +using GenHTTP.Modules.Compression.Algorithms; +using GenHTTP.Modules.IO; + +namespace GenHTTP.Testing.Acceptance.Modules.Compression; + +[TestClass] +public class PreCompressionTests +{ + + [TestMethod] + [MultiEngineTest] + public async Task TestRegular(TestEngine engine) + { + await using var runner = await RunAsync(engine); + + var request = runner.GetRequest("/Resources/LargeTemplate.html"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br")); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.AreEqual("br", response.Content.Headers.ContentEncoding.First()); + + Assert.AreEqual("text/html", response.Content.Headers.ContentType?.ToString()); + + var content = await response.GetContentAsync(); + + Assert.DoesNotContain("precompressed", content); // not automatically decompressed + } + + [TestMethod] + [MultiEngineTest] + public async Task TestNonSupportedAlgorithm(TestEngine engine) + { + await using var runner = await RunAsync(engine); + + var request = runner.GetRequest("/Resources/LargeTemplate.html"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.IsEmpty(response.Content.Headers.ContentEncoding); + } + + private static async ValueTask RunAsync(TestEngine engine) + { + var handler = PreCompressedResources.From(ResourceTree.FromAssembly()) + .Add(new BrotliAlgorithm()); + + return await TestHost.RunAsync(handler, defaults: false, engine: engine); + } + +} diff --git a/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs index 51e050fd..553ff1e0 100644 --- a/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs +++ b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs @@ -17,7 +17,7 @@ public async Task TestAssemblyByName() Assert.IsNotNull(await tree.TryGetResourceAsync(new("File.txt"))); - Assert.HasCount(2, await tree.GetNodes()); + Assert.HasCount(3, await tree.GetNodes()); Assert.HasCount(6, await tree.GetResources()); } diff --git a/Testing/Acceptance/Resources/LargeTemplate.html.br b/Testing/Acceptance/Resources/LargeTemplate.html.br new file mode 100644 index 0000000000000000000000000000000000000000..bff4cf059ffef6f1135913a60a01fa84318921ab GIT binary patch literal 700 zcmV;t0z>^9k_7<7x1VO*1ie}8I5byLtLV6_ul4HYx2X8~(cw%3;k)~9&TWN7WdW8= z+StZamg~%3)3&4m&XD!;CcyS`SzrT{mR?+>Z|8uj`<|uR;G}LVa)L$y;CXQmIWmFz z7nh9d-NWU6KAAT0_is~W(ruyyQHCBJ911qc=IX80)w9T`M0p1Zg6~5Nu-OY94XH8R z6#e+A!!{{80Wx#69|h>RL7+maV{3GDs`WP-sDr0RezEMeH<8HjYuBlc+X3W`qKB{E4r{xCtDj!E-OkUs51)_qy|lD%$7wvm`*OLd>`2`TacG zik{b2fO*(v{{LULB(Vk3gP(>;gHF$eRDKU!1Joo|5hqDNUFC{Ry4&%*KPZyEgepW#?J05Hia(B zAx3f8YZ;aeWSpy}Oq^L~20qo4M+(FSDc;mwY;5>U!qUlcYLFmIJ@hfrmNgoLze%lf zl=z&sZM59j18vx@tH`@G{y>)zNck!~sSvKzyeMPG{gyV>A%}R7Z+JDjFixpxlt!vm zE7{f%pzWD1Wd&8O+CXtNGIQVSP--a3k%M=RuI}1UiPKj{Wn$M|dO--$Xql9=;Y~X8 zbZiD0)ByHOe0wY1(}kQ$iy<$()3k*^=uDJaIE)oF5wc{BEbC#%9G+qV&Rwe#IF00) z`y<+>Sf+RcPE)0$3xq_!OdkEDT6v%+7Hm^-vJiwET`a0LQ#8=Y(jzOaBFXu6=cpNz zA~@pP(PCog0f#Mm67{kvIfkKRA&{8}Y`uyVdjyYZwM%X3iLJyFAB%CuX-%j_Jam?; ic7VWKWGO_H7Pf=cnAmVydmfWokex?L?Cp>dXqE;1fL+)C literal 0 HcmV?d00001 From f7b34dcc68cea349302d1177510ee6f8a7d93021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Fri, 29 May 2026 08:45:29 +0200 Subject: [PATCH 3/3] Add tests and documentation --- .../Compression/PreCompressedBenchmark.cs | 33 ++++++++++ Benchmarks/GenHTTP.Benchmarks.csproj | 9 ++- Benchmarks/Program.cs | 6 +- Benchmarks/Resources/file.js.br | Bin 0 -> 52037 bytes Engine/Shared/Types/RequestTarget.cs | 15 +++-- Modules/Compression/PreCompressedResources.cs | 26 ++++++++ .../PreCompressedResourceHandler.cs | 35 ++++++----- .../PreCompressedResourceHandlerBuilder.cs | 28 +++++++-- .../PreCompression/SupportedCompression.cs | 12 ++++ ...cceptHeader.cs => AcceptEncodingHeader.cs} | 2 +- .../Providers/CompressionConcern.cs | 59 +++++++++--------- .../Providers/CompressionConcernBuilder.cs | 7 +-- .../GenHTTP.Testing.Acceptance.csproj | 3 +- .../Compression/PreCompressionTests.cs | 43 ++++++++++++- .../Modules/IO/ResourceTreeTests.cs | 4 +- ...Template.html.br => LargeTemplate.html+br} | Bin 16 files changed, 214 insertions(+), 68 deletions(-) create mode 100644 Benchmarks/Benchmarks/Compression/PreCompressedBenchmark.cs create mode 100644 Benchmarks/Resources/file.js.br create mode 100644 Modules/Compression/PreCompression/SupportedCompression.cs rename Modules/Compression/Providers/{AcceptHeader.cs => AcceptEncodingHeader.cs} (93%) rename Testing/Acceptance/Resources/{LargeTemplate.html.br => LargeTemplate.html+br} (100%) diff --git a/Benchmarks/Benchmarks/Compression/PreCompressedBenchmark.cs b/Benchmarks/Benchmarks/Compression/PreCompressedBenchmark.cs new file mode 100644 index 00000000..4e93a951 --- /dev/null +++ b/Benchmarks/Benchmarks/Compression/PreCompressedBenchmark.cs @@ -0,0 +1,33 @@ +using BenchmarkDotNet.Attributes; + +using GenHTTP.Benchmarks.Infrastructure; + +using GenHTTP.Modules.Compression; +using GenHTTP.Modules.Compression.Algorithms; +using GenHTTP.Modules.IO; + +namespace GenHTTP.Benchmarks.Benchmarks.Compression; + +[MemoryDiagnoser] +public class PreCompressedBenchmark +{ + private readonly BenchmarkContext _context = CreateContext(); + + [Benchmark] + public ValueTask BenchmarkPreCompressedStaticFiles() => _context.Execute(); + + private static BenchmarkContext CreateContext() + { + var tree = ResourceTree.FromDirectory("./Resources/"); + + var handler = PreCompressedResources.From(tree) + .Add(new BrotliAlgorithm()) + .Add(CompressedContent.Default()); + + var request = "GET /file.js HTTP/1.1\r\nHost: localhost:8080\r\nAccept-Encoding: br;q=1, gzip;q=0.8\r\n\r\n"; + + return new(request, handler.Build()); + } + +} + diff --git a/Benchmarks/GenHTTP.Benchmarks.csproj b/Benchmarks/GenHTTP.Benchmarks.csproj index 475eb430..dd787cae 100644 --- a/Benchmarks/GenHTTP.Benchmarks.csproj +++ b/Benchmarks/GenHTTP.Benchmarks.csproj @@ -26,9 +26,14 @@ - + + Always - + + + + Always + diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs index a0a054bb..9e114da4 100644 --- a/Benchmarks/Program.cs +++ b/Benchmarks/Program.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Running; -using GenHTTP.Benchmarks.Benchmarks.IO; +using GenHTTP.Benchmarks.Benchmarks.Compression; -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); -// await new StaticFileBenchmark().BenchmarkStaticFile(); \ No newline at end of file +// await new PreCompressedBenchmark().BenchmarkPreCompressedStaticFiles(); \ No newline at end of file diff --git a/Benchmarks/Resources/file.js.br b/Benchmarks/Resources/file.js.br new file mode 100644 index 0000000000000000000000000000000000000000..e6c1acc6f25fde4c699c6962582179e43e67e9f2 GIT binary patch literal 52037 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 KEkkoFDS;Znw0<=J literal 0 HcmV?d00001 diff --git a/Engine/Shared/Types/RequestTarget.cs b/Engine/Shared/Types/RequestTarget.cs index 96496eb5..3a2136c9 100644 --- a/Engine/Shared/Types/RequestTarget.cs +++ b/Engine/Shared/Types/RequestTarget.cs @@ -165,11 +165,18 @@ public IRequestTarget CopyAndAppend(ReadOnlyMemory suffix) { var span = combined.Span; - var idx = span[_segmentStart..].IndexOf((byte)'/'); + var relative = span[_segmentStart..].IndexOf((byte)'/'); - copy.Current = idx < 0 - ? new PathSegment(combined[_segmentStart..]) - : new PathSegment(combined.Slice(_segmentStart, idx)); + if (relative < 0) + { + copy._offset = combined.Length; + copy.Current = new PathSegment(combined[_segmentStart..]); + } + else + { + copy._offset = _segmentStart + relative; + copy.Current = new PathSegment(combined.Slice(_segmentStart, relative)); + } } return copy; diff --git a/Modules/Compression/PreCompressedResources.cs b/Modules/Compression/PreCompressedResources.cs index 1bcbc567..b36ce42b 100644 --- a/Modules/Compression/PreCompressedResources.cs +++ b/Modules/Compression/PreCompressedResources.cs @@ -5,12 +5,38 @@ namespace GenHTTP.Modules.Compression; +/// +/// Allows to serve static resources that already are pre-compressed by +/// an external system. +/// +/// +/// Expects compressed files to be stored side-by-side with the non-compressed +/// files (so for example "file.js" and "file.br.js"). +/// +/// By default, no algorithms are configured for a newly created handler, so +/// you need to register them by yourself. As the supported algorithms are +/// controlled by the external system pre-compressing the files, this is +/// more efficient than looping through all compression algorithms supported +/// by GenHTTP. +/// public static class PreCompressedResources { + /// + /// Creates a new handler that will look for existing, pre-compressed files + /// based on the client preferences and serve them if suitable. + /// + /// The tree to be served + /// The newly created handler builder public static PreCompressedResourceHandlerBuilder From(IBuilder tree) => From(tree.Build()); + /// + /// Creates a new handler that will look for existing, pre-compressed files + /// based on the client preferences and serve them if suitable. + /// + /// The tree to be served + /// The newly created handler builder public static PreCompressedResourceHandlerBuilder From(IResourceTree tree) => new(tree); diff --git a/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs index 449df033..6a9fc873 100644 --- a/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs +++ b/Modules/Compression/PreCompression/PreCompressedResourceHandler.cs @@ -1,6 +1,7 @@ using GenHTTP.Api.Content; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Compression.Providers; using GenHTTP.Modules.IO; using GenHTTP.Modules.IO.Streaming; @@ -13,14 +14,24 @@ public sealed class PreCompressedResourceHandler : IHandler private readonly IHandler _regular; - private readonly List _algorithms; + private readonly List _algorithms; - public PreCompressedResourceHandler(IResourceTree tree, List algorithms) + public PreCompressedResourceHandler(IResourceTree tree, List algorithms, char separator) { _tree = tree; - _algorithms = algorithms; _regular = Resources.From(tree).Build(); + + _algorithms = algorithms.Select(a => + { + var extension = new byte[a.Name.Value.Length + 1]; + extension[0] = (byte)separator; + a.Name.Value.Span.CopyTo(extension.AsSpan(1)); + + return new SupportedCompression(a, extension); + }) + .OrderByDescending(a => (int)a.Algorithm.Priority) + .ToList(); } public ValueTask PrepareAsync() => _regular.PrepareAsync(); @@ -36,28 +47,24 @@ public PreCompressedResourceHandler(IResourceTree tree, List (int)a.Priority)) + foreach (var supported in _algorithms) { - if (supported.Contains(algorithm.Name)) + if (requested.Contains(supported.Algorithm.Name)) { - var extension = new byte[algorithm.Name.Value.Length + 1]; - extension[0] = (byte)'.'; - algorithm.Name.Value.Span.CopyTo(extension.AsSpan(1)); - - var newTarget = target.CopyAndAppend(extension); + var newTarget = target.CopyAndAppend(supported.Extension); var (_, resource) = await _tree.FindAsync(newTarget); if (resource is not null) { var contentType = file.GuessContentType() ?? ContentType.ApplicationOctetStream; - var content = new ResourceContent(resource, contentType, algorithm.Name.Value); + var content = new ResourceContent(resource, contentType, supported.Algorithm.Name.Value); return request.Respond() .Content(content) diff --git a/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs b/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs index 311ab124..f0729742 100644 --- a/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs +++ b/Modules/Compression/PreCompression/PreCompressedResourceHandlerBuilder.cs @@ -11,11 +11,34 @@ public sealed class PreCompressedResourceHandlerBuilder : IHandlerBuilder + /// Sets the separator used to build the paths to the compressed files. Defaults to '.'. + /// + /// The separator used to built file paths + /// + /// If your files are stored like "file.js+br", you can set this to '+'. + /// + public PreCompressedResourceHandlerBuilder Separator(char separator) + { + _separator = separator; + return this; + } + + /// + /// Registers a algorithm used to search for file names. + /// + /// The algorithm to use for searching + /// + /// The handler will not use the compression or decompression features of the algorithm, + /// but use the priority field to prefer algorithms with better compression ratios. + /// public PreCompressedResourceHandlerBuilder Add(ICompressionAlgorithm algorithm) { _algorithms.Add(algorithm); @@ -28,9 +51,6 @@ public PreCompressedResourceHandlerBuilder Add(IConcernBuilder concern) return this; } - public IHandler Build() - { - return Concerns.Chain(_concerns, new PreCompressedResourceHandler(_source, _algorithms)); - } + public IHandler Build() => Concerns.Chain(_concerns, new PreCompressedResourceHandler(_source, _algorithms, _separator)); } diff --git a/Modules/Compression/PreCompression/SupportedCompression.cs b/Modules/Compression/PreCompression/SupportedCompression.cs new file mode 100644 index 00000000..a113d5a6 --- /dev/null +++ b/Modules/Compression/PreCompression/SupportedCompression.cs @@ -0,0 +1,12 @@ +using GenHTTP.Api.Content.IO; + +namespace GenHTTP.Modules.Compression.PreCompression; + +public readonly struct SupportedCompression(ICompressionAlgorithm algorithm, ReadOnlyMemory extension) +{ + + public ICompressionAlgorithm Algorithm => algorithm; + + public ReadOnlyMemory Extension => extension; + +} diff --git a/Modules/Compression/Providers/AcceptHeader.cs b/Modules/Compression/Providers/AcceptEncodingHeader.cs similarity index 93% rename from Modules/Compression/Providers/AcceptHeader.cs rename to Modules/Compression/Providers/AcceptEncodingHeader.cs index b23177e7..64160444 100644 --- a/Modules/Compression/Providers/AcceptHeader.cs +++ b/Modules/Compression/Providers/AcceptEncodingHeader.cs @@ -2,7 +2,7 @@ namespace GenHTTP.Modules.Compression.Providers; -public static class AcceptHeader +public static class AcceptEncodingHeader { public static HashSet ParseSupported(ReadOnlySpan acceptHeader) diff --git a/Modules/Compression/Providers/CompressionConcern.cs b/Modules/Compression/Providers/CompressionConcern.cs index 925854f4..5042266c 100644 --- a/Modules/Compression/Providers/CompressionConcern.cs +++ b/Modules/Compression/Providers/CompressionConcern.cs @@ -1,5 +1,4 @@ using System.IO.Compression; - using GenHTTP.Api.Content; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; @@ -34,7 +33,7 @@ public sealed class CompressionConcern : IConcern public IHandler Content { get; } - private IReadOnlyDictionary Algorithms { get; } + private List Algorithms { get; } private CompressionLevel Level { get; } @@ -44,13 +43,15 @@ public sealed class CompressionConcern : IConcern #region Initialization - public CompressionConcern(IHandler content, IReadOnlyDictionary algorithms, CompressionLevel level, ulong? minimumSize) + public CompressionConcern(IHandler content, List algorithms, CompressionLevel level, ulong? minimumSize) { Content = content; Algorithms = algorithms; Level = level; MinimumSize = minimumSize; + + Algorithms.Sort((a, b) => b.Priority.CompareTo(a.Priority)); } #endregion @@ -76,46 +77,44 @@ public CompressionConcern(IHandler content, IReadOnlyDictionary (int)a.Priority)) + foreach (var algorithm in Algorithms) { - if (supported.Contains(algorithm.Name)) + if (!supported.Contains(algorithm.Name)) { - var builder = response.Rebuild(); + continue; + } - builder.Content(algorithm.Compress(content, Level)); + var builder = response.Rebuild(); - var vary = response.Headers.GetEntry(KnownHeaders.Vary); + builder.Content(algorithm.Compress(content, Level)); - if (vary != null) - { - var combined = new byte[vary.Value.Length + KnownHeaders.AcceptEncoding.Length + 2]; + var vary = response.Headers.GetEntry(KnownHeaders.Vary); - var span = combined.AsSpan(); - var offset = 0; + if (vary != null) + { + var combined = new byte[vary.Value.Length + KnownHeaders.AcceptEncoding.Length + 2]; - vary.Value.Span.CopyTo(span); - offset += vary.Value.Length; + var span = combined.AsSpan(); + var offset = 0; - span[offset++] = (byte)','; - span[offset++] = (byte)' '; + vary.Value.Span.CopyTo(span); + offset += vary.Value.Length; - KnownHeaders.AcceptEncoding.Span.CopyTo(span[offset..]); + span[offset++] = (byte)','; + span[offset++] = (byte)' '; - builder.Header(KnownHeaders.Vary, combined); - } - else - { - builder.Header(KnownHeaders.Vary, KnownHeaders.AcceptEncoding); - } + KnownHeaders.AcceptEncoding.Span.CopyTo(span[offset..]); - return builder.Build(); + builder.Header(KnownHeaders.Vary, combined); + } + else + { + builder.Header(KnownHeaders.Vary, KnownHeaders.AcceptEncoding); } + + return builder.Build(); } } } diff --git a/Modules/Compression/Providers/CompressionConcernBuilder.cs b/Modules/Compression/Providers/CompressionConcernBuilder.cs index ca5e5de3..48e43b2d 100644 --- a/Modules/Compression/Providers/CompressionConcernBuilder.cs +++ b/Modules/Compression/Providers/CompressionConcernBuilder.cs @@ -42,12 +42,7 @@ public CompressionConcernBuilder MinimumSize(ulong? minimumSize) return this; } - public IConcern Build(IHandler content) - { - var algorithms = _algorithms.ToDictionary(a => a.Name); - - return new CompressionConcern(content, algorithms, _level, _minimumSize); - } + public IConcern Build(IHandler content) => new CompressionConcern(content, _algorithms, _level, _minimumSize); #endregion diff --git a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj index b9790f4c..dcb9f250 100644 --- a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj +++ b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj @@ -32,7 +32,8 @@ - + + diff --git a/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs b/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs index d239c5a9..7525f019 100644 --- a/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs +++ b/Testing/Acceptance/Modules/Compression/PreCompressionTests.cs @@ -1,9 +1,12 @@ using System.Net; using System.Net.Http.Headers; + using GenHTTP.Modules.Compression; using GenHTTP.Modules.Compression.Algorithms; using GenHTTP.Modules.IO; +using GenHTTP.Testing.Acceptance.Utilities; + namespace GenHTTP.Testing.Acceptance.Modules.Compression; [TestClass] @@ -49,11 +52,49 @@ public async Task TestNonSupportedAlgorithm(TestEngine engine) Assert.IsEmpty(response.Content.Headers.ContentEncoding); } + + [TestMethod] + [MultiEngineTest] + public async Task TestNoCompressionRequested(TestEngine engine) + { + await using var runner = await RunAsync(engine); - private static async ValueTask RunAsync(TestEngine engine) + using var response = await runner.GetResponseAsync("/Resources/LargeTemplate.html"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.IsEmpty(response.Content.Headers.ContentEncoding); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestFolderNotFound(TestEngine engine) + { + await using var runner = await RunAsync(engine); + + var request = runner.GetRequest("/Resources/SubDirectory/"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br")); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public void TestChaining() { var handler = PreCompressedResources.From(ResourceTree.FromAssembly()) .Add(new BrotliAlgorithm()); + + Chain.Works(handler); + } + + private static async ValueTask RunAsync(TestEngine engine) + { + var handler = PreCompressedResources.From(ResourceTree.FromAssembly()) + .Add(new BrotliAlgorithm()) + .Separator('+'); return await TestHost.RunAsync(handler, defaults: false, engine: engine); } diff --git a/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs index 553ff1e0..c671a161 100644 --- a/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs +++ b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs @@ -17,9 +17,9 @@ public async Task TestAssemblyByName() Assert.IsNotNull(await tree.TryGetResourceAsync(new("File.txt"))); - Assert.HasCount(3, await tree.GetNodes()); + Assert.HasCount(2, await tree.GetNodes()); - Assert.HasCount(6, await tree.GetResources()); + Assert.HasCount(7, await tree.GetResources()); } [TestMethod] diff --git a/Testing/Acceptance/Resources/LargeTemplate.html.br b/Testing/Acceptance/Resources/LargeTemplate.html+br similarity index 100% rename from Testing/Acceptance/Resources/LargeTemplate.html.br rename to Testing/Acceptance/Resources/LargeTemplate.html+br