| | | 1 | | using System.IO.Compression; |
| | | 2 | | |
| | | 3 | | namespace Kestrun.Forms; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Provides per-part decompression helpers. |
| | | 7 | | /// </summary> |
| | | 8 | | public static class KrPartDecompression |
| | | 9 | | { |
| | | 10 | | /// <summary> |
| | | 11 | | /// Wraps a stream in a decompression stream based on the content encoding. |
| | | 12 | | /// </summary> |
| | | 13 | | /// <param name="source">The source stream.</param> |
| | | 14 | | /// <param name="contentEncoding">The content encoding header value.</param> |
| | | 15 | | /// <returns>The decoded stream and normalized encoding.</returns> |
| | | 16 | | public static (Stream Stream, string Encoding) CreateDecodedStream(Stream source, string? contentEncoding) |
| | | 17 | | { |
| | 9 | 18 | | if (string.IsNullOrWhiteSpace(contentEncoding)) |
| | | 19 | | { |
| | 3 | 20 | | return (source, "identity"); |
| | | 21 | | } |
| | | 22 | | |
| | 6 | 23 | | var normalized = contentEncoding.Trim().ToLowerInvariant(); |
| | 6 | 24 | | return normalized switch |
| | 6 | 25 | | { |
| | 2 | 26 | | "identity" => (source, "identity"), |
| | 1 | 27 | | "gzip" => (new GZipStream(source, CompressionMode.Decompress, leaveOpen: false), "gzip"), |
| | 1 | 28 | | "deflate" => (new DeflateStream(source, CompressionMode.Decompress, leaveOpen: false), "deflate"), |
| | 1 | 29 | | "br" => (new BrotliStream(source, CompressionMode.Decompress, leaveOpen: false), "br"), |
| | 1 | 30 | | _ => (source, normalized) |
| | 6 | 31 | | }; |
| | | 32 | | } |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | /// <summary> |
| | | 36 | | /// Stream wrapper that enforces a maximum number of bytes read. |
| | | 37 | | /// </summary> |
| | | 38 | | /// <remarks> |
| | | 39 | | /// Initializes a new instance of the <see cref="LimitedReadStream"/> class. |
| | | 40 | | /// </remarks> |
| | | 41 | | /// <param name="inner">The inner stream.</param> |
| | | 42 | | /// <param name="maxBytes">The maximum number of bytes allowed.</param> |
| | | 43 | | public sealed class LimitedReadStream(Stream inner, long maxBytes) : Stream |
| | | 44 | | { |
| | | 45 | | private readonly Stream _inner = inner; |
| | | 46 | | private readonly long _maxBytes = maxBytes; |
| | | 47 | | private long _totalRead; |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Gets the total number of bytes read from the stream. |
| | | 51 | | /// </summary> |
| | | 52 | | public long TotalRead => _totalRead; |
| | | 53 | | |
| | | 54 | | /// <inheritdoc /> |
| | | 55 | | public override bool CanRead => _inner.CanRead; |
| | | 56 | | |
| | | 57 | | /// <inheritdoc /> |
| | | 58 | | public override bool CanSeek => false; |
| | | 59 | | |
| | | 60 | | /// <inheritdoc /> |
| | | 61 | | public override bool CanWrite => false; |
| | | 62 | | |
| | | 63 | | /// <inheritdoc /> |
| | | 64 | | public override long Length => _inner.Length; |
| | | 65 | | |
| | | 66 | | /// <inheritdoc /> |
| | | 67 | | public override long Position |
| | | 68 | | { |
| | | 69 | | get => _inner.Position; |
| | | 70 | | set => throw new NotSupportedException(); |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <inheritdoc /> |
| | | 74 | | public override void Flush() => _inner.Flush(); |
| | | 75 | | |
| | | 76 | | /// <inheritdoc /> |
| | | 77 | | public override int Read(byte[] buffer, int offset, int count) |
| | | 78 | | { |
| | | 79 | | var read = _inner.Read(buffer, offset, count); |
| | | 80 | | Track(read); |
| | | 81 | | return read; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <inheritdoc /> |
| | | 85 | | public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) |
| | | 86 | | { |
| | | 87 | | var read = await _inner.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); |
| | | 88 | | Track(read); |
| | | 89 | | return read; |
| | | 90 | | } |
| | | 91 | | |
| | | 92 | | /// <inheritdoc /> |
| | | 93 | | public override int Read(Span<byte> buffer) |
| | | 94 | | { |
| | | 95 | | var read = _inner.Read(buffer); |
| | | 96 | | Track(read); |
| | | 97 | | return read; |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | private void Track(int read) |
| | | 101 | | { |
| | | 102 | | if (read <= 0) |
| | | 103 | | { |
| | | 104 | | return; |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | _totalRead += read; |
| | | 108 | | if (_totalRead > _maxBytes) |
| | | 109 | | { |
| | | 110 | | throw new KrFormLimitExceededException($"Decompressed part size exceeded limit of {_maxBytes} bytes."); |
| | | 111 | | } |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | /// <inheritdoc /> |
| | | 115 | | public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
| | | 116 | | |
| | | 117 | | /// <inheritdoc /> |
| | | 118 | | public override void SetLength(long value) => throw new NotSupportedException(); |
| | | 119 | | |
| | | 120 | | /// <inheritdoc /> |
| | | 121 | | public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
| | | 122 | | } |