From c948542c05b4da03f8206058464bb8002f788604 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 22 Oct 2024 22:55:13 +0100 Subject: [PATCH] #1426: migrates a couple stream funcs to .NET, and removes the Streams.ps1 file --- src/Listener/PodeCompressionType.cs | 8 + src/Listener/PodeHelpers.cs | 92 +++++++++ src/Private/Helpers.ps1 | 9 +- src/Private/Streams.ps1 | 287 ---------------------------- src/Public/Responses.ps1 | 21 +- 5 files changed, 105 insertions(+), 312 deletions(-) create mode 100644 src/Listener/PodeCompressionType.cs delete mode 100644 src/Private/Streams.ps1 diff --git a/src/Listener/PodeCompressionType.cs b/src/Listener/PodeCompressionType.cs new file mode 100644 index 000000000..459470bc8 --- /dev/null +++ b/src/Listener/PodeCompressionType.cs @@ -0,0 +1,8 @@ +namespace Pode +{ + public enum PodeCompressionType + { + Gzip, + Deflate + } +} \ No newline at end of file diff --git a/src/Listener/PodeHelpers.cs b/src/Listener/PodeHelpers.cs index 44a7e8f95..2458dfad0 100644 --- a/src/Listener/PodeHelpers.cs +++ b/src/Listener/PodeHelpers.cs @@ -7,6 +7,8 @@ using System.Runtime.Versioning; using System.Threading.Tasks; using System.Threading; +using System.Text; +using System.IO.Compression; namespace Pode { @@ -212,5 +214,95 @@ public static List Subset(List list, int startIndex, int endIndex) { return Subset(list.ToArray(), startIndex, endIndex).ToList(); } + + public static byte[] ConvertStreamToBytes(Stream stream) + { + // we need to copy the stream to a memory stream and then return the bytes + using (var memory = new MemoryStream()) + { + stream.CopyTo(memory); + return memory.ToArray(); + } + } + + public static string ConvertBytesToString(byte[] bytes, bool removeNewLines = false) + { + // return empty string if no bytes + if (bytes == default(byte[]) || bytes.Length == 0) + { + return string.Empty; + } + + // convert the bytes to a string + var str = Encoding.UTF8.GetString(bytes); + + // remove new lines if needed + if (removeNewLines) + { + return str.Trim(NEW_LINE_ARRAY); + } + + return str; + } + + public static string ReadStreamToEnd(Stream stream, Encoding encoding = default) + { + // return empty string if no stream + if (stream == default(Stream)) + { + return string.Empty; + } + + // set the encoding if not provided + if (encoding == default(Encoding)) + { + encoding = Encoding.UTF8; + } + + // read the stream to the end + using (var reader = new StreamReader(stream, encoding)) + { + return reader.ReadToEnd(); + } + } + + // decompress bytes into either a gzip or deflate stream, and return the string + public static string DecompressBytes(byte[] bytes, PodeCompressionType type, Encoding encoding = default) + { + var stream = CompressStream(new MemoryStream(bytes), type, CompressionMode.Decompress); + return ReadStreamToEnd(stream, encoding); + } + + // compress bytes into either a gzip or deflate stream, and return the bytes + public static byte[] CompressBytes(byte[] bytes, PodeCompressionType type) + { + var ms = new MemoryStream(); + + using (var stream = CompressStream(ms, type, CompressionMode.Compress)) + { + stream.Write(bytes, 0, bytes.Length); + } + + ms.Position = 0; + return ms.ToArray(); + } + + // compress stream into either a gzip or deflate stream + public static Stream CompressStream(Stream stream, PodeCompressionType type, CompressionMode mode) + { + var leaveOpen = mode == CompressionMode.Compress; + + switch (type) + { + case PodeCompressionType.Gzip: + return new GZipStream(stream, mode, leaveOpen); + + case PodeCompressionType.Deflate: + return new DeflateStream(stream, mode, leaveOpen); + + default: + return stream; + } + } } } \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 0b6c5ad59..ccd86f48b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1535,14 +1535,7 @@ function ConvertFrom-PodeRequestContent { else { # if the request is compressed, attempt to uncompress it if (![string]::IsNullOrWhiteSpace($TransferEncoding)) { - # create a compressed stream to decompress the req bytes - $ms = [System.IO.MemoryStream]::new() - $ms.Write($Request.RawBody, 0, $Request.RawBody.Length) - $null = $ms.Seek(0, 0) - $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $TransferEncoding -Mode Decompress - - # read the decompressed bytes - $Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding + $Content = [PodeHelpers]::DecompressBytes($Request.RawBody, $TransferEncoding, $Request.ContentEncoding) } else { $Content = $Request.Body diff --git a/src/Private/Streams.ps1 b/src/Private/Streams.ps1 deleted file mode 100644 index 8e37ff1b5..000000000 --- a/src/Private/Streams.ps1 +++ /dev/null @@ -1,287 +0,0 @@ -function Read-PodeStreamToEnd { - param( - [Parameter()] - $Stream, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8 - ) - - if ($null -eq $Stream) { - return [string]::Empty - } - - return (Use-PodeStream -Stream ([System.IO.StreamReader]::new($Stream, $Encoding)) { - return $args[0].ReadToEnd() - }) -} - -function Read-PodeByteLineFromByteArray { - param( - [Parameter(Mandatory = $true)] - [byte[]] - $Bytes, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8, - - [Parameter()] - [int] - $StartIndex = 0, - - [switch] - $IncludeNewLine - ) - - $nlBytes = Get-PodeNewLineByte -Encoding $Encoding - - # attempt to find \n - $index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex) - $fIndex = $index - - # if not including new line, remove any trailing \r and \n - if (!$IncludeNewLine) { - $fIndex-- - - if ($Bytes[$fIndex] -eq $nlBytes.Return) { - $fIndex-- - } - } - - # grab the portion of the bytes array - which is our line - return @{ - Bytes = $Bytes[$StartIndex..$fIndex] - StartIndex = $StartIndex - EndIndex = $index - } -} - -function Get-PodeByteLinesFromByteArray { - param( - [Parameter(Mandatory = $true)] - [byte[]] - $Bytes, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8, - - [switch] - $IncludeNewLine - ) - - # lines - $lines = @() - $nlBytes = Get-PodeNewLineByte -Encoding $Encoding - - # attempt to find \n - $index = 0 - while (($nextIndex = [array]::IndexOf($Bytes, $nlBytes.NewLine, $index)) -gt 0) { - $fIndex = $nextIndex - - # if not including new line, remove any trailing \r and \n - if (!$IncludeNewLine) { - $fIndex-- - if ($Bytes[$fIndex] -eq $nlBytes.Return) { - $fIndex-- - } - } - - # add the line, and get the next one - $lines += , $Bytes[$index..$fIndex] - $index = $nextIndex + 1 - } - - return $lines -} -<# -.SYNOPSIS - Converts a stream to a byte array. - -.DESCRIPTION - The `ConvertFrom-PodeStreamToByteArray` function reads data from a stream and converts it to a byte array. - It's useful for scenarios where you need to work with binary data from a stream. - -.PARAMETER Stream - Specifies the input stream to convert. This parameter is mandatory. - -.OUTPUTS - Returns a byte array containing the data read from the input stream. - -.EXAMPLE - # Example usage: - # Read data from a file stream and convert it to a byte array - $stream = [System.IO.File]::OpenRead("C:\path\to\file.bin") - $byteArray = ConvertFrom-PodeStreamToByteArray -Stream $stream - $stream.Close() - -.NOTES - This is an internal function and may change in future releases of Pode. -#> -function ConvertFrom-PodeStreamToByteArray { - param( - [Parameter(Mandatory = $true)] - $Stream - ) - - # Initialize a buffer to read data in chunks - $buffer = [byte[]]::new(64 * 1024) - $ms = [System.IO.MemoryStream]::new() - $read = 0 - - # Read data from the stream and write it to the memory stream - while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) { - $ms.Write($buffer, 0, $read) - } - - # Close the memory stream and return the byte array - $ms.Close() - return $ms.ToArray() -} - -function ConvertFrom-PodeBytesToString { - param( - [Parameter()] - [byte[]] - $Bytes, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8, - - [switch] - $RemoveNewLine - ) - - if (($null -eq $Bytes) -or ($Bytes.Length -eq 0)) { - return $Bytes - } - - $value = $Encoding.GetString($Bytes) - if ($RemoveNewLine) { - $value = $value.Trim("`r`n") - } - - return $value -} - -<# -.SYNOPSIS - Retrieves information about newline characters in different encodings. - -.DESCRIPTION - The `Get-PodeNewLineByte` function returns a hashtable containing information about newline characters. - It calculates the byte values for newline (`n`) and carriage return (`r`) based on the specified encoding (default is UTF-8). - -.PARAMETER Encoding - Specifies the encoding to use when calculating newline and carriage return byte values. - Default value is UTF-8. - -.OUTPUTS - Returns a hashtable with the following keys: - - `NewLine`: Byte value for newline character (`n`). - - `Return`: Byte value for carriage return character (`r`). - -.EXAMPLE - Get-PodeNewLineByte -Encoding [System.Text.Encoding]::ASCII - # Returns the byte values for newline and carriage return in ASCII encoding. - -.NOTES - This is an internal function and may change in future releases of Pode. -#> -function Get-PodeNewLineByte { - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8 - ) - - return @{ - NewLine = @($Encoding.GetBytes("`n"))[0] - Return = @($Encoding.GetBytes("`r"))[0] - } -} - -function Test-PodeByteArrayIsBoundary { - param( - [Parameter()] - [byte[]] - $Bytes, - - [Parameter()] - [string] - $Boundary, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8 - ) - - # if no bytes, return - if ($Bytes.Length -eq 0) { - return $false - } - - # if length difference >3, return (ie, 2 offset for `r`n) - if (($Bytes.Length - $Boundary.Length) -gt 3) { - return $false - } - - # check if bytes starts with the boundary - return (ConvertFrom-PodeBytesToString $Bytes $Encoding).StartsWith($Boundary) -} - -function Remove-PodeNewLineBytesFromArray { - param( - [Parameter()] - $Bytes, - - [Parameter()] - $Encoding = [System.Text.Encoding]::UTF8 - ) - - $nlBytes = Get-PodeNewLineByte -Encoding $Encoding - $length = $Bytes.Length - 1 - - if ($Bytes[$length] -eq $nlBytes.NewLine) { - $length-- - } - - if ($Bytes[$length] -eq $nlBytes.Return) { - $length-- - } - - return $Bytes[0..$length] -} - -function Get-PodeCompressionStream { - param ( - [Parameter(Mandatory = $true)] - [System.IO.Stream] - $InputStream, - - [Parameter(Mandatory = $true)] - [ValidateSet('gzip', 'deflate')] - [string] - $Encoding, - - [Parameter(Mandatory = $true)] - [System.IO.Compression.CompressionMode] - $Mode - ) - - $leaveOpen = $Mode -eq [System.IO.Compression.CompressionMode]::Compress - - switch ($Encoding.ToLower()) { - 'gzip' { - return [System.IO.Compression.GZipStream]::new($InputStream, $Mode, $leaveOpen) - } - - 'deflate' { - return [System.IO.Compression.DeflateStream]::new($InputStream, $Mode, $leaveOpen) - } - - default { - # Unsupported stream compression encoding: $Encoding - throw ($PodeLocale.unsupportedStreamCompressionEncodingExceptionMessage -f $Encoding) - } - } -} diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index b01c6ccda..9a9f82e3c 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -1,3 +1,5 @@ +using namespace Pode + <# .SYNOPSIS Attaches a file onto the Response for downloading. @@ -272,23 +274,8 @@ function Write-PodeTextResponse { # check if we need to compress the response if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) { - try { - $ms = [System.IO.MemoryStream]::new() - $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $WebEvent.AcceptEncoding -Mode Compress - $stream.Write($Bytes, 0, $Bytes.Length) - $stream.Close() - $ms.Position = 0 - $Bytes = $ms.ToArray() - } - finally { - if ($null -ne $stream) { - $stream.Close() - } - - if ($null -ne $ms) { - $ms.Close() - } - } + # compress the bytes + $Bytes = [PodeHelpers]::CompressBytes($Bytes, $WebEvent.AcceptEncoding) # set content encoding header Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding