Utility.PS.psm1

#Requires -Version 5.1
#Requires -PSEdition Core,Desktop

<#
.SYNOPSIS
    Utility.PS
.DESCRIPTION
    This module contains cmdlets that extend the basic features of PowerShell.
.NOTES
    ModuleVersion: 2.0.1
    GUID: e6c8b1d2-a261-4a57-80a7-ea8080132c86
    Author: Jason Thompson
    CompanyName: Microsoft Corporation
    Copyright: (c) 2023 Jason Thompson. All rights reserved.
.FUNCTIONALITY
    Compress-Data, ConvertFrom-Base64String, ConvertFrom-ClixmlString, ConvertFrom-HexString, ConvertFrom-HtmlString, ConvertFrom-QueryString, ConvertFrom-SecureStringAsPlainText, ConvertFrom-UrlString, ConvertTo-Base64String, ConvertTo-ClixmlString, ConvertTo-Dictionary, ConvertTo-HexString, ConvertTo-HtmlString, ConvertTo-MarkdownTable, ConvertTo-PsParameterString, ConvertTo-PsString, ConvertTo-QueryString, ConvertTo-UrlString, Expand-Data, Format-DataSize, Format-NumberWithMetricUnit, Format-PropertyValue, Get-ContentEncoding, Get-PropertyValue, Get-RelativePath, Get-StrictModeVersion, Get-X509Certificate, Get-X509CertificateCrlDistributionPoints, Invoke-CommandAsSystem, New-SecureStringKey, Remove-Diacritics, Remove-InvalidFileNameCharacters, Remove-SensitiveData, Select-PsBoundParameters, Skip-NullValue, Test-IpAddressInSubnet, Test-PsElevation, Use-Progress, Write-HostPrompt
.LINK
    https://github.com/jasoth/Utility.PS
#>


#region NestedModules Script(s)

#region Compress-Data.ps1

<#
.SYNOPSIS
    Compress data using DEFLATE (RFC 1951) and optionally GZIP file format (RFC 1952).
 
.EXAMPLE
    PS >Compress-Data 'A string for compression'
 
    Compress string using Deflate.
 
.INPUTS
    System.String
     
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Compress-Data {
    [CmdletBinding()]
    [Alias('Deflate-Data')]
    [OutputType([byte[]])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # Output gzip format
        [Parameter(Mandatory = $false)]
        [switch] $GZip,
        # Level of compression
        [Parameter(Mandatory = $false)]
        [System.IO.Compression.CompressionLevel] $CompressionLevel = ([System.IO.Compression.CompressionLevel]::Optimal),
        # Input encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default',
        # Set gzip OS header byte to unknown
        [Parameter(Mandatory = $false)]
        [switch] $GZipUnknownOS
    )

    begin {
        function Compress ([byte[]]$InputBytes, [bool]$GZip) {
            try {
                $streamInput = New-Object System.IO.MemoryStream -ArgumentList @($InputBytes, $false)
                try {
                    $streamOutput = New-Object System.IO.MemoryStream
                    try {
                        if ($GZip) {
                            $streamCompression = New-Object System.IO.Compression.GZipStream -ArgumentList $streamOutput, $CompressionLevel, $true
                        }
                        else {
                            $streamCompression = New-Object System.IO.Compression.DeflateStream -ArgumentList $streamOutput, $CompressionLevel, $true
                        }
                        $streamInput.CopyTo($streamCompression)
                    }
                    finally { $streamCompression.Dispose() }
                    if ($GZip) {
                        [void] $streamOutput.Seek(8, [System.IO.SeekOrigin]::Begin)
                        switch ($CompressionLevel) {
                            'Optimal' { $streamOutput.WriteByte(2) }
                            'Fastest' { $streamOutput.WriteByte(4) }
                            Default { $streamOutput.WriteByte(0) }
                        }
                        if ($GZipUnknownOS) { $streamOutput.WriteByte(255) }
                        elseif ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) { $streamOutput.WriteByte(11) }
                        elseif ($IsLinux) { $streamOutput.WriteByte(3) }
                        elseif ($IsMacOS) { $streamOutput.WriteByte(7) }
                        else { $streamOutput.WriteByte(255) }
                    }
                    [byte[]] $OutputBytes = $streamOutput.ToArray()
                }
                finally { $streamOutput.Dispose() }
            }
            finally { $streamInput.Dispose() }

            Write-Output $OutputBytes -NoEnumerate
        }

        ## Create list to capture byte stream from piped input.
        [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte]
    }

    process {
        if ($InputObjects -is [byte[]]) {
            Write-Output (Compress $InputObjects -GZip:$GZip) -NoEnumerate
        }
        else {
            foreach ($InputObject in $InputObjects) {
                [byte[]] $InputBytes = $null
                if ($InputObject -is [byte]) {
                    ## Populate list with byte stream from piped input.
                    if ($listBytes.Count -eq 0) {
                        Write-Verbose 'Creating byte array from byte stream.'
                        Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                    }
                    $listBytes.Add($InputObject)
                }
                elseif ($InputObject -is [byte[]]) {
                    $InputBytes = $InputObject
                }
                elseif ($InputObject -is [string]) {
                    $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject)
                }
                elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) {
                    $InputBytes = [System.BitConverter]::GetBytes($InputObject)
                }
                elseif ($InputObject -is [guid]) {
                    $InputBytes = $InputObject.ToByteArray()
                }
                elseif ($InputObject -is [System.IO.FileSystemInfo]) {
                    if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream
                    }
                    else {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte
                    }
                }
                else {
                    ## Non-Terminating Error
                    $Exception = New-Object ArgumentException -ArgumentList ('Cannot compress input of type {0}.' -f $InputObject.GetType())
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'CompressDataFailureTypeNotSupported' -TargetObject $InputObject
                }

                if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) {
                    Write-Output (Compress $InputBytes -GZip:$GZip) -NoEnumerate
                }
            }
        }
    }

    end {
        ## Output captured byte stream from piped input.
        if ($listBytes.Count -gt 0) {
            Write-Output (Compress $listBytes.ToArray() -GZip:$GZip) -NoEnumerate
        }
    }
}

#endregion

#region ConvertFrom-Base64String.ps1

<#
.SYNOPSIS
    Convert Base64 String to Byte Array or Plain Text String.
 
.EXAMPLE
    PS >ConvertFrom-Base64String "QSBzdHJpbmcgd2l0aCBiYXNlNjQgZW5jb2Rpbmc="
 
    Convert Base64 String to String with Default Encoding.
 
.EXAMPLE
    PS >"QVNDSUkgc3RyaW5nIHdpdGggYmFzZTY0dXJsIGVuY29kaW5n" | ConvertFrom-Base64String -Base64Url -Encoding Ascii
 
    Convert Base64Url String to String with Ascii Encoding.
 
.EXAMPLE
    PS >[guid](ConvertFrom-Base64String "5oIhNbCaFUGAe8NsiAKfpA==" -RawBytes)
 
    Convert Base64 String to GUID.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-Base64String {
    [CmdletBinding()]
    [OutputType([byte[]], [string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputObjects,
        # Use base64url variant
        [Parameter (Mandatory = $false)]
        [switch] $Base64Url,
        # Output raw byte array
        [Parameter (Mandatory = $false)]
        [switch] $RawBytes,
        # Encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default'
    )

    process {
        foreach ($InputObject in $InputObjects) {
            [string] $strBase64 = $InputObject
            if (!$PSBoundParameters.ContainsValue('Base64Url') -and ($strBase64.Contains('-') -or $strBase64.Contains('_'))) { $Base64Url = $true }
            if ($Base64Url) { $strBase64 = $strBase64.Replace('-', '+').Replace('_', '/').PadRight($strBase64.Length + (4 - $strBase64.Length % 4) % 4, '=') }
            [byte[]] $outBytes = [System.Convert]::FromBase64String($strBase64)
            if ($RawBytes) {
                Write-Output $outBytes -NoEnumerate
            }
            else {
                [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes))
                Write-Output $outString
            }
        }
    }
}

#endregion

#region ConvertFrom-ClixmlString.ps1

<#
.SYNOPSIS
    Convert Clixml serialized string to object.
     
.EXAMPLE
    PS >ConvertFrom-ClixmlString '<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><S>A clixml serialized string</S></Objs>'
 
    Convert Clixml serialized string to object.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-ClixmlString {
    [CmdletBinding()]
    [OutputType([object])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputString
    )

    process {
        #foreach ($_InputString in $InputString) {
            [System.Management.Automation.PSSerializer]::Deserialize($InputString)
        #}
    }
}

#endregion

#region ConvertFrom-HexString.ps1

<#
.SYNOPSIS
   Convert from Hex String
 
.EXAMPLE
    PS >ConvertFrom-HexString "57 68 61 74 20 69 73 20 61 20 68 65 78 20 73 74 72 69 6E 67 3F"
 
    Convert hex byte string seperated by spaces to string.
 
.EXAMPLE
    PS >"415343494920737472696E6720746F2068657820737472696E67" | ConvertFrom-HexString -Delimiter "" -Encoding Ascii
 
    Convert hex byte string with no seperation to ASCII string.
 
.INPUTS
    System.String
     
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-HexString {
    [CmdletBinding()]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputObject,
        # Delimiter between Hex pairs
        [Parameter (Mandatory = $false)]
        [string] $Delimiter = ' ',
        # Output raw byte array
        [Parameter (Mandatory = $false)]
        [switch] $RawBytes,
        # Encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default'
    )

    process {
        $InputObject = $InputObject -replace '\s', ''
        $listBytes = New-Object object[] $InputObject.Count
        for ($iString = 0; $iString -lt $InputObject.Count; $iString++) {
            [string] $strHex = $InputObject[$iString]

            if ($strHex -notmatch '^[A-Fa-f0-9\r\n]+$') {
                $Exception = New-Object System.Management.Automation.MethodInvocationException 'The input is not a valid hex string as it contains a non-hex character.'
                Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::InvalidData) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertFromHexFailureInvalidData' -TargetObject $InputObject
                return
            }

            if ($strHex.Substring(2, 1) -eq $Delimiter) {
                [string[]] $listHex = $strHex -split $Delimiter
            }
            else {
                [string[]] $listHex = New-Object string[] ($strHex.Length / 2)
                for ($iByte = 0; $iByte -lt $strHex.Length; $iByte += 2) {
                    $listHex[[System.Math]::Truncate($iByte / 2)] = $strHex.Substring($iByte, 2)
                }
            }

            [byte[]] $outBytes = New-Object byte[] $listHex.Count
            for ($iByte = 0; $iByte -lt $listHex.Count; $iByte++) {
                $outBytes[$iByte] = [byte]::Parse($listHex[$iByte], [System.Globalization.NumberStyles]::HexNumber)
            }

            if ($RawBytes) { $listBytes[$iString] = $outBytes }
            else {
                $outString = ([Text.Encoding]::$Encoding.GetString($outBytes))
                Write-Output $outString
            }
        }
        if ($RawBytes) {
            return $listBytes
        }
    }
}

#endregion

#region ConvertFrom-HtmlString.ps1

<#
.SYNOPSIS
    Convert HTML encoded string to string.
     
.EXAMPLE
    PS >ConvertFrom-HtmlString 'A string with &lt;html&gt; encoding'
 
    Convert HTML encoded string to string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-HtmlString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputStrings
    )

    process {
        foreach ($InputString in $InputStrings) {
            Write-Output ([System.Net.WebUtility]::HtmlDecode($InputString))
        }
    }
}

#endregion

#region ConvertFrom-QueryString.ps1

<#
.SYNOPSIS
    Convert Query String to object.
 
.EXAMPLE
    PS >ConvertFrom-QueryString '?name=path/file.json&index=10'
 
    Convert query string to object.
 
.EXAMPLE
    PS >'name=path/file.json&index=10' | ConvertFrom-QueryString -AsHashtable
 
    Convert query string to hashtable.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-QueryString {
    [CmdletBinding()]
    [OutputType([psobject])]
    [OutputType([hashtable])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [AllowEmptyString()]
        [string[]] $InputStrings,
        # URL decode parameter names
        [Parameter(Mandatory = $false)]
        [switch] $DecodeParameterNames,
        # Converts to hash table object
        [Parameter(Mandatory = $false)]
        [switch] $AsHashtable
    )

    process {
        foreach ($InputString in $InputStrings) {
            if ($AsHashtable) { [hashtable] $OutputObject = @{ } }
            else { [psobject] $OutputObject = New-Object psobject }

            if ($InputString) {
                if ($InputString[0] -eq '?') { $InputString = $InputString.Substring(1) }
                [string[]] $QueryParameters = $InputString.Split('&')
                foreach ($QueryParameter in $QueryParameters) {
                    [string[]] $QueryParameterPair = $QueryParameter.Split('=')
                    if ($DecodeParameterNames) { $QueryParameterPair[0] = [System.Net.WebUtility]::UrlDecode($QueryParameterPair[0]) }
                    if ($OutputObject -is [hashtable]) {
                        $OutputObject.Add($QueryParameterPair[0], [System.Net.WebUtility]::UrlDecode($QueryParameterPair[1]))
                    }
                    else {
                        $OutputObject | Add-Member $QueryParameterPair[0] -MemberType NoteProperty -Value ([System.Net.WebUtility]::UrlDecode($QueryParameterPair[1]))
                    }
                }
            }
            Write-Output $OutputObject
        }
    }

}

#endregion

#region ConvertFrom-SecureString.ps1

<#
.SYNOPSIS
    Converts a secure string to an encrypted standard string.
.DESCRIPTION
    The ConvertFrom-SecureString cmdlet converts a secure string (System.Security.SecureString) into an encrypted standard string (System.String). Unlike a secure string, an encrypted standard string can be saved in a file for later use. The encrypted standard string can be converted back to its secure string format by using the ConvertTo-SecureString cmdlet.
 
    If an encryption key is specified by using the Key or SecureKey parameters, the Advanced Encryption Standard (AES) encryption algorithm is used. The specified key must have a length of 128, 192, or 256 bits because those are the key lengths supported by the AES encryption algorithm. If no key is specified, the Windows Data Protection API (DPAPI) is used to encrypt the standard string representation.
.PARAMETER AsPlainText
    When set, ConvertFrom-SecureString will convert secure strings to the decrypted plaintext string as output.
.PARAMETER Key
    Specifies the encryption key as a byte array.
.PARAMETER SecureKey
    Specifies the encryption key as a secure string. The secure string value is converted to a byte array before being used as the key.
.PARAMETER SecureString
    Specifies the secure string to convert to an encrypted standard string.
.EXAMPLE
    PS >$SecureString = Read-Host -AsSecureString
    PS >$StandardString = ConvertFrom-SecureString $SecureString
 
    This command converts the secure string in the $SecureString variable to an encrypted standard string. The resulting encrypted standard string is stored in the $StandardString variable.
.EXAMPLE
    PS >$SecureString = Read-Host -AsSecureString
    PS >$Key = (3,4,2,3,56,34,254,222,1,1,2,23,42,54,33,233,1,34,2,7,6,5,35,43)
    PS >$StandardString = ConvertFrom-SecureString $SecureString -Key $Key
 
    These commands use the Advanced Encryption Standard (AES) algorithm to convert the secure string stored in the $SecureString variable to an encrypted standard string with a 192-bit key. The resulting encrypted standard string is stored in the $StandardString variable.
    The first command stores a key in the $Key variable. The key is an array of 24 decimal numerals, each of which must be less than 256 to fit within a single unsigned byte.
    Because each decimal numeral represents a single byte (8 bits), the key has 24 digits for a total of 192 bits (8 x 24). This is a valid key length for the AES algorithm.
    The second command uses the key in the $Key variable to convert the secure string to an encrypted standard string.
.INPUTS
    System.Security.SecureString
     
    You can pipe a SecureString object to this cmdlet.
.OUTPUTS
    System.String
     
    This cmdlet returns the created plain text string.
.NOTES
    To create a secure string from characters that are typed at the command prompt, use the AsSecureString parameter of the Read-Host cmdlet.
    When you use the Key or SecureKey parameters to specify a key, the key length must be correct. For example, a key of 128 bits can be specified as a byte array of 16 decimal numerals. Similarly, 192-bit and 256-bit keys correspond to byte arrays of 24 and 32 decimal numerals, respectively.
    Some characters, such as emoticons, correspond to several code points in the string that contains them. Avoid using these characters because they may cause problems and misunderstandings when used in a password.
.LINK
    https://go.microsoft.com/fwlink/?LinkID=113287
.LINK
    https://learn.microsoft.com/powershell/module/microsoft.powershell.security/convertfrom-securestring
.LINK
    ConvertTo-SecureString
.LINK
    Read-Host
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-SecureString {
    [CmdletBinding(DefaultParameterSetName = 'Secure', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113287')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [securestring]
        ${SecureString},

        [Parameter(ParameterSetName = 'PlainText')]
        [switch]
        ${AsPlainText},

        [Parameter(ParameterSetName = 'Secure', Position = 1)]
        [securestring]
        ${SecureKey},

        [Parameter(ParameterSetName = 'Open')]
        [byte[]]
        ${Key}
    )

    begin {
        ## Command Extension
        if ($PSBoundParameters.ContainsKey('AsPlainText') -and $PSVersionTable.PSVersion -lt [version]'7.0') {
            if (${AsPlainText}) { return }
            else { [void] $PSBoundParameters.Remove('AsPlainText') }
        }

        ## Resume Command
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Security\ConvertFrom-SecureString', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch {
            throw
        }
    }

    process {
        ## Command Extension
        if (${AsPlainText} -and $PSVersionTable.PSVersion -lt [version]'7.0') {
            try {
                [IntPtr] $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
                Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR))
            }
            finally {
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
            }
            return
        }

        ## Resume Command
        try {
            $steppablePipeline.Process($_)
        }
        catch {
            throw
        }
    }

    end {
        ## Command Extension
        if (${AsPlainText} -and $PSVersionTable.PSVersion -lt [version]'7.0') { return }

        ## Resume Command
        try {
            $steppablePipeline.End()
        }
        catch {
            throw
        }
    }
    <#
 
    .ForwardHelpTargetName Microsoft.PowerShell.Security\ConvertFrom-SecureString
    .ForwardHelpCategory Cmdlet
 
    #>

}

#endregion

#region ConvertFrom-SecureStringAsPlainText.ps1

<#
.SYNOPSIS
    Convert/Decrypt SecureString to Plain Text String.
 
.EXAMPLE
    PS >ConvertFrom-SecureStringAsPlainText (ConvertTo-SecureString 'SuperSecretString' -AsPlainText -Force) -Force
 
    Convert plain text to SecureString and then convert it back.
 
.INPUTS
    System.Security.SecureString
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-SecureStringAsPlainText {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Secure String Value
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [securestring] $SecureString,
        # Confirms that you understand the implications of using the AsPlainText parameter and still want to use it.
        [Parameter(Mandatory = $false)]
        [switch] $Force
    )

    begin {
        if ($PSVersionTable.PSVersion -ge [version]'7.0') {
            Write-Warning 'PowerShell 7 introduced an AsPlainText parameter to the ConvertFrom-SecureString cmdlet.'
        }
        if (!${Force}) {
            ## Terminating Error
            $Exception = New-Object ArgumentException -ArgumentList 'The system cannot protect plain text output. To suppress this warning and convert a SecureString to plain text, reissue the command specifying the Force parameter.'
            Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertSecureStringFailureForceRequired' -TargetObject ${SecureString} -ErrorAction Stop
        }
    }

    process {
        try {
            [IntPtr] $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
            Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR))
        }
        finally {
            [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
        }
    }
}

#endregion

#region ConvertFrom-UrlString.ps1

<#
.SYNOPSIS
    Convert URL encoded string to string.
 
.EXAMPLE
    PS >ConvertFrom-UrlString 'A+string+with+url+encoding'
 
    Convert URL encoded string to string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertFrom-UrlString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputStrings
    )

    process {
        foreach ($InputString in $InputStrings) {
            Write-Output ([System.Net.WebUtility]::UrlDecode($InputString))
        }
    }
}

#endregion

#region ConvertTo-Base64String.ps1

<#
.SYNOPSIS
    Convert Byte Array or Plain Text String to Base64 String.
 
.EXAMPLE
    PS >ConvertTo-Base64String "A string with base64 encoding"
 
    Convert String with Default Encoding to Base64 String.
 
.EXAMPLE
    PS >"ASCII string with base64url encoding" | ConvertTo-Base64String -Base64Url -Encoding Ascii
 
    Convert String with Ascii Encoding to Base64Url String.
 
.EXAMPLE
    PS >ConvertTo-Base64String ([guid]::NewGuid())
 
    Convert GUID to Base64 String.
 
.INPUTS
    System.Object
     
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-Base64String {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # Use base64url variant
        [Parameter (Mandatory = $false)]
        [switch] $Base64Url,
        # Output encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default'
    )

    begin {
        function Transform ([byte[]]$InputBytes) {
            [string] $outBase64String = [System.Convert]::ToBase64String($InputBytes)
            if ($Base64Url) { $outBase64String = $outBase64String.Replace('+', '-').Replace('/', '_').Replace('=', '') }
            return $outBase64String
        }

        ## Create list to capture byte stream from piped input.
        [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte]
    }

    process {
        if ($InputObjects -is [byte[]]) {
            Write-Output (Transform $InputObjects)
        }
        else {
            foreach ($InputObject in $InputObjects) {
                [byte[]] $InputBytes = $null
                if ($InputObject -is [byte]) {
                    ## Populate list with byte stream from piped input.
                    if ($listBytes.Count -eq 0) {
                        Write-Verbose 'Creating byte array from byte stream.'
                        Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                    }
                    $listBytes.Add($InputObject)
                }
                elseif ($InputObject -is [byte[]]) {
                    $InputBytes = $InputObject
                }
                elseif ($InputObject -is [string]) {
                    $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject)
                }
                elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) {
                    $InputBytes = [System.BitConverter]::GetBytes($InputObject)
                }
                elseif ($InputObject -is [guid]) {
                    $InputBytes = $InputObject.ToByteArray()
                }
                elseif ($InputObject -is [System.IO.FileSystemInfo]) {
                    if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream
                    }
                    else {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte
                    }
                }
                else {
                    ## Non-Terminating Error
                    $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to Base64 string.' -f $InputObject.GetType())
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertBase64StringFailureTypeNotSupported' -TargetObject $InputObject
                }

                if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) {
                    Write-Output (Transform $InputBytes)
                }
            }
        }
    }

    end {
        ## Output captured byte stream from piped input.
        if ($listBytes.Count -gt 0) {
            Write-Output (Transform $listBytes.ToArray())
        }
    }
}

#endregion

#region ConvertTo-ClixmlString.ps1

<#
.SYNOPSIS
    Convert string to Clixml serialized string.
 
.EXAMPLE
    PS >ConvertTo-ClixmlString 'A clixml serialized string'
 
    Convert string to Clixml serialized string.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-ClixmlString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObject,
        # Omits white space and indented formatting in the output string.
        [Parameter(Mandatory = $false)]
        [switch] $Compress,
        # Specifies how many levels of nested objects are included.
        [Parameter(Mandatory = $false)]
        [int] $Depth
    )

    process {
        #foreach ($_InputObject in $InputObject) {
            if ($Depth) {
                $OutputString = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth)
            }
            else {
                $OutputString = [System.Management.Automation.PSSerializer]::Serialize($InputObject)
            }

            if ($Compress) { $OutputString = $OutputString -replace '\r?\n\s*', '' }

            return $OutputString
        #}
    }
}

#endregion

#region ConvertTo-Csv.ps1

<#
.SYNOPSIS
    Converts .NET objects into a series of character-separated value (CSV) strings.
.DESCRIPTION
    The `ConvertTo-CSV` cmdlet returns a series of character-separated value (CSV) strings that represent the objects that you submit. You can then use the `ConvertFrom-Csv` cmdlet to recreate objects from the CSV strings. The objects converted from CSV are string values of the original objects that contain property values and no methods.
    You can use the `Export-Csv` cmdlet to convert objects to CSV strings. `Export-CSV` is similar to `ConvertTo-CSV`, except that it saves the CSV strings to a file.
    The `ConvertTo-CSV` cmdlet has parameters to specify a delimiter other than a comma or use the current culture as the delimiter.
.PARAMETER InputObject
    Specifies the objects that are converted to CSV strings. Enter a variable that contains the objects or type a command or expression that gets the objects. You can also pipe objects to `ConvertTo-CSV`.
.PARAMETER Delimiter
    Specifies the delimiter to separate the property values in CSV strings. The default is a comma (`,`). Enter a character, such as a colon (`:`). To specify a semicolon (`;`) enclose it in single quotation marks.
    If you specify a character other than the actual string delimiter in the file, `ConvertFrom-Csv` can't create the objects from the CSV strings and returns the CSV strings.
.PARAMETER UseCulture
    Uses the list separator for the current culture as the item delimiter. To find the list separator for a culture, use the following command: `(Get-Culture).TextInfo.ListSeparator`.
.PARAMETER IncludeTypeInformation
    When this parameter is used the first line of the output contains #TYPE followed by the fully qualified name of the object type. For example, #TYPE System.Diagnostics.Process.
    This parameter was introduced in PowerShell 6.0.
.PARAMETER NoTypeInformation
    Removes the #TYPE information header from the output. This parameter became the default in PowerShell 6.0 and is included for backwards compatibility.
.PARAMETER ArrayDelimiter
    Specifies the delimiter that separates the items in arrays.
.EXAMPLE
    PS >Get-Process -Name pwsh | ConvertTo-Csv -NoTypeInformation
    "Name","SI","Handles","VM","WS","PM","NPM","Path","Parent","Company","CPU","FileVersion", ...
    "pwsh","8","950","2204001161216","100925440","59686912","67104", ...
 
    The `Get-Process` cmdlet gets the Process object and uses the Name parameter to specify the PowerShell process. The process object is sent down the pipeline to the `ConvertTo-CSV` cmdlet. The `ConvertTo-CSV` cmdlet converts the object to CSV strings. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6.
.EXAMPLE
    PS >$Date = Get-Date
    PS >ConvertTo-Csv -InputObject $Date -Delimiter ';' -NoTypeInformation
    "DisplayHint";"DateTime";"Date";"Day";"DayOfWeek";"DayOfYear";"Hour";"Kind";"Millisecond";"Minute";"Month";"Second";"Ticks";"TimeOfDay";"Year"
    "DateTime";"Friday, January 4, 2019 14:40:51";"1/4/2019 00:00:00";"4";"Friday";"4";"14";"Local";"711";"40";"1";"51";"636822096517114991";"14:40:51.7114991";"2019"</dev:code>
 
    The `Get-Date` cmdlet gets the DateTime object and saves it in the `$Date` variable. The `ConvertTo-Csv` cmdlet converts the DateTime object to strings. The InputObject parameter uses the DateTime object stored in the `$Date` variable. The Delimiter parameter specifies a semicolon to separate the string values. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6.
.EXAMPLE
    PS >(Get-Culture).TextInfo.ListSeparator
    PS >Get-WinEvent -LogName 'PowerShellCore/Operational' | ConvertTo-Csv -UseCulture -NoTypeInformation
    ,
    "Message","Id","Version","Qualifiers","Level","Task","Opcode","Keywords","RecordId", ...
    "Error Message = System error""4100","1",,"3","106","19","0","31716","PowerShellCore", ...
 
    The `Get-Culture` cmdlet uses the nested properties TextInfo and ListSeparator and displays the current culture's default list separator. The `Get-WinEvent` cmdlet gets the event log objects and uses the LogName parameter to specify the log file name. The event log objects are sent down the pipeline to the `ConvertTo-Csv` cmdlet. The `ConvertTo-Csv` cmdlet converts the event log objects to a series of CSV strings. The UseCulture parameter uses the current culture's default list separator as the delimiter. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6.
.EXAMPLE
    PS >Get-Date | ConvertTo-Csv -QuoteFields "DateTime","Date"
    DisplayHint,"DateTime","Date",Day,DayOfWeek,DayOfYear,Hour,Kind,Millisecond,Minute,Month,Second,Ticks,TimeOfDay,Year
    DateTime,"Thursday, August 22, 2019 11:27:34 AM","8/22/2019 12:00:00 AM",22,Thursday,234,11,Local,569,27,8,34,637020700545699784,11:27:34.5699784,2019
 
    Convert to CSV with quotes around two columns
.EXAMPLE
    PS >Get-Date | ConvertTo-Csv -UseQuotes AsNeeded
    DisplayHint,DateTime,Date,Day,DayOfWeek,DayOfYear,Hour,Kind,Millisecond,Minute,Month,Second,Ticks,TimeOfDay,Year
    DateTime,"Thursday, August 22, 2019 11:31:00 AM",8/22/2019 12:00:00 AM,22,Thursday,234,11,Local,713,31,8,0,637020702607132640,11:31:00.7132640,2019
 
    Convert to CSV with quotes only when needed
.EXAMPLE
    PS >$person1 = @{
        Name = 'John Smith'
        Number = 1
    }
    PS >$person2 = @{
        Name = 'Jane Smith'
        Number = 2
    }
    PS >$allPeople = $person1, $person2
    PS >$allPeople | ConvertTo-Csv
    "Name","Number"
    "John Smith","1"
    "Jane Smith","2"
 
    Convert hashtables to CSV
.EXAMPLE
    PS >$allPeople | Add-Member -Name ExtraProp -Value 42
    PS >$allPeople | ConvertTo-Csv
    "Name","Number","ExtraProp"
    "John Smith","1","42"
    "Jane Smith","2","42"
 
    Each hashtable has a property named `ExtraProp` added by `Add-Member` and then converted to CSV. You can see `ExtraProp` is now a header in the output.
    If an added property has the same name as a key from the hashtable, the key takes precedence and only the key is converted to CSV.
.INPUTS
    System.Security.SecureString
     
    You can pipe a SecureString object to this cmdlet.
.OUTPUTS
    System.String
     
    This cmdlet returns the created plain text string.
.NOTES
    To create a secure string from characters that are typed at the command prompt, use the AsSecureString parameter of the Read-Host cmdlet.
    When you use the Key or SecureKey parameters to specify a key, the key length must be correct. For example, a key of 128 bits can be specified as a byte array of 16 decimal numerals. Similarly, 192-bit and 256-bit keys correspond to byte arrays of 24 and 32 decimal numerals, respectively.
    Some characters, such as emoticons, correspond to several code points in the string that contains them. Avoid using these characters because they may cause problems and misunderstandings when used in a password.
.LINK
    https://go.microsoft.com/fwlink/?LinkID=135203
.LINK
    ConvertFrom-Csv
.LINK
    Export-Csv
.LINK
    Import-Csv
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-Csv {
    [CmdletBinding(DefaultParameterSetName = 'DelimiterPath', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=135203', RemotingCapability = 'None')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [psobject]
        ${InputObject},

        [Parameter(ParameterSetName = 'Delimiter', Position = 1)]
        [ValidateNotNull()]
        [char]
        ${Delimiter},

        [Parameter(ParameterSetName = 'UseCulture')]
        [switch]
        ${UseCulture},

        [Alias('ITI')]
        [switch]
        ${IncludeTypeInformation},

        [Alias('NTI')]
        [switch]
        ${NoTypeInformation},

        [ValidateNotNull()]
        [string]
        ${ArrayDelimiter} = "`r`n"
    )

    begin {
        function Transform (${InputObject}, ${ArrayDelimiter}) {
            [bool] $ContainsArray = $false
            [System.Collections.Generic.List[object]] $SelectProperties = New-Object System.Collections.Generic.List[object]
            $Properties = ${InputObject} | Select-Object -First 1 | Get-Member -MemberType NoteProperty, Property, ScriptProperty
            foreach ($Property in $Properties) {
                if ($Property.Definition -like ("*``[``] {0}*" -f $Property.Name) -or $Property.Definition -like ("*List``[*``] {0}*" -f $Property.Name)) {
                    $SelectProperties.Add(@{ Name = $Property.Name; Expression = [scriptblock]::Create(('$_.{0} -join "{1}"' -f $Property.Name, ${ArrayDelimiter})) })
                    $ContainsArray = $true
                }
                else {
                    $SelectProperties.Add($Property.Name)
                }
            }
            if ($ContainsArray) { return ${InputObject} | Select-Object -Property $SelectProperties.ToArray() }
            else { return ${InputObject} }
        }

        ## Command Extension
        if ($null -ne ${InputObject}) {
            $PSBoundParameters['InputObject'] = Transform ${InputObject} ${ArrayDelimiter}
        }

        ## Remove extra parameters
        if ($PSBoundParameters.ContainsKey('ArrayDelimiter')) { [void] $PSBoundParameters.Remove('ArrayDelimiter') }

        ## Resume Command
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }

            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Csv', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }

            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch {
            throw
        }
    }

    process {
        ## Command Extension
        if ($null -ne ${InputObject}) {
            $_ = Transform ${InputObject} ${ArrayDelimiter}
        }

        ## Resume Command
        try {
            $steppablePipeline.Process($_)
        }
        catch {
            throw
        }
    }

    end {
        try {
            $steppablePipeline.End()
        }
        catch {
            throw
        }
    }
    <#
 
    .ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Csv
    .ForwardHelpCategory Cmdlet
 
    #>

}

#endregion

#region ConvertTo-Dictionary.ps1

<#
.SYNOPSIS
    Convert hashtable to generic dictionary.
 
.EXAMPLE
    PS >ConvertTo-Dictionary @{ KeyName = 'StringValue' } -ValueType ([string])
 
    Convert hashtable to generic dictionary.
 
.INPUTS
    System.Hashtable
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-Dictionary {
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.Dictionary[object, object]])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [hashtable[]] $InputObjects,
        # Data Type of Key
        [Parameter(Mandatory = $false)]
        [type] $KeyType = [string],
        # Data Type of Value
        [Parameter(Mandatory = $false)]
        [type] $ValueType = [object]
    )

    process {
        foreach ($InputObject in $InputObjects) {
            $OutputObject = New-Object ('System.Collections.Generic.Dictionary[[{0}],[{1}]]' -f $KeyType.FullName, $ValueType.FullName)
            foreach ($KeyPair in $InputObject.GetEnumerator()) {
                $OutputObject.Add($KeyPair.Key, $KeyPair.Value)
            }

            Write-Output $OutputObject
        }
    }
}

#endregion

#region ConvertTo-HexString.ps1

<#
.SYNOPSIS
    Convert to Hex String
 
.EXAMPLE
    PS >ConvertTo-HexString "What is a hex string?"
 
    Convert string to hex byte string seperated by spaces.
 
.EXAMPLE
    PS >"ASCII string to hex string" | ConvertTo-HexString -Delimiter "" -Encoding Ascii
 
    Convert ASCII string to hex byte string with no seperation.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-HexString {
    [CmdletBinding()]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # Delimiter between Hex pairs
        [Parameter (Mandatory = $false)]
        [string] $Delimiter = ' ',
        # Encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default'
    )

    begin {
        function Transform ([byte[]]$InputBytes) {
            [string[]] $outHexString = New-Object string[] $InputBytes.Count
            for ($iByte = 0; $iByte -lt $InputBytes.Count; $iByte++) {
                $outHexString[$iByte] = $InputBytes[$iByte].ToString('X2')
            }
            return $outHexString -join $Delimiter
        }

        ## Create list to capture byte stream from piped input.
        [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte]
    }

    process {
        if ($InputObjects -is [byte[]]) {
            Write-Output (Transform $InputObjects)
        }
        else {
            foreach ($InputObject in $InputObjects) {
                [byte[]] $InputBytes = $null
                if ($InputObject -is [byte]) {
                    ## Populate list with byte stream from piped input.
                    if ($listBytes.Count -eq 0) {
                        Write-Verbose 'Creating byte array from byte stream.'
                        Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                    }
                    $listBytes.Add($InputObject)
                }
                elseif ($InputObject -is [byte[]]) {
                    $InputBytes = $InputObject
                }
                elseif ($InputObject -is [string]) {
                    $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject)
                }
                elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) {
                    $InputBytes = [System.BitConverter]::GetBytes($InputObject)
                }
                elseif ($InputObject -is [guid]) {
                    $InputBytes = $InputObject.ToByteArray()
                }
                elseif ($InputObject -is [System.IO.FileSystemInfo]) {
                    if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream
                    }
                    else {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte
                    }
                }
                else {
                    ## Non-Terminating Error
                    $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to Hex string.' -f $InputObject.GetType())
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertHexFailureTypeNotSupported' -TargetObject $InputObject
                }

                if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) {
                    Write-Output (Transform $InputBytes)
                }
            }
        }
    }

    end {
        ## Output captured byte stream from piped input.
        if ($listBytes.Count -gt 0) {
            Write-Output (Transform $listBytes.ToArray())
        }
    }
}

#endregion

#region ConvertTo-HtmlString.ps1

<#
.SYNOPSIS
    Convert string to HTML encoded string.
 
.EXAMPLE
    PS >ConvertTo-HtmlString 'A string with <html> encoding'
 
    Convert string to HTML encoded string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-HtmlString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputStrings
    )

    process {
        foreach ($InputString in $InputStrings) {
            Write-Output ([System.Net.WebUtility]::HtmlEncode($InputString))
        }
    }
}

#endregion

#region ConvertTo-MarkdownTable.ps1

<#
.SYNOPSIS
    Converts an object to a markdown table.
 
.EXAMPLE
    PS >ConvertTo-MarkdownTable $PsVersionTable
 
    Converts the PsVersionTable variable object to markdown table.
 
.EXAMPLE
    PS >Get-PSHostProcessInfo | ConvertTo-MarkdownTable -Compact
 
    Converts PSHostProcessInfo objects to markdown table.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-MarkdownTable {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Objects to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object[]] $InputObject,
        # Property names to include in the output.
        [Parameter(Mandatory = $false, Position = 1)]
        [string[]] $Property,
        # Output one row per input object or one keypair list table per input object
        [Parameter(Mandatory = $false)]
        [ValidateSet('Table', 'List')]
        [string] $As,
        # Do not include whitespace padding in table
        [Parameter(Mandatory = $false)]
        [switch] $Compact,
        # String to use as delimiter for array values
        [Parameter(Mandatory = $false)]
        [string] $ArrayDelimiter,
        # Format second level depth objects with the specified format
        [Parameter(Mandatory = $false)]
        [ValidateSet('ToString', 'PsFormat', 'Html')]
        [string] $ObjectFormat = 'PsFormat'
    )

    begin {
        ## Initalize variables
        $NewLineReplacement = '<br>'

        function FormatMarkdownTableHeaderRow ($ColumnWidths) {
            if ($ColumnWidths.Count -gt 0) {
                $InitialColumn = $true
                [string]$TableRow = '| '
                [string]$DelimiterRow = '| '
                foreach ($PropertyName in $ColumnWidths.Keys) {
                    if (!$InitialColumn) { $TableRow += ' | ' }
                    $TableRow += $PropertyName.PadRight($ColumnWidths[$PropertyName], ' ')

                    if (!$InitialColumn) { $DelimiterRow += ' | ' }
                    if ($ColumnWidths[$PropertyName] -gt 0) {
                        $DelimiterRow += '---'.PadRight($ColumnWidths[$PropertyName], '-')
                    }
                    else {
                        $DelimiterRow += '---' #.PadRight($PropertyName.Length, '-')
                    }

                    $InitialColumn = $false
                }
                $TableRow += ' |'
                $DelimiterRow += ' |'

                $TableRow
                $DelimiterRow
            }
        }

        function FormatMarkdownTableRow ($ColumnWidths, $InputObject) {
            $InitialColumn = $true
            [string]$TableRow = '| '
            foreach ($PropertyName in $ColumnWidths.Keys) {
                if (!$InitialColumn) { $TableRow += ' | ' }

                if ($InputObject) {
                    $StringValue = ''

                    $PropertyValue = Get-PropertyValue $InputObject $PropertyName
                    $StringValue = Transform $PropertyValue
                    
                    $TableRow += $StringValue.PadRight($ColumnWidths[$PropertyName], ' ')
                }

                $InitialColumn = $false
            }
            $TableRow += ' |'

            return $TableRow
        }

        function FormatMarkdownKeyPairRows ($ColumnWidths, $InputObject) {
            foreach ($Property in $InputObject.PSObject.Properties) {
                $InitialColumn = $true
                [string]$TableRow = '| '

                foreach ($PropertyName in $ColumnWidths.Keys) {
                    if (!$InitialColumn) { $TableRow += ' | ' }

                    if ($InputObject) {
                        if ($PropertyName -eq 'Name') {
                            $TableRow += $Property.Name.PadRight($ColumnWidths['Name'], ' ')
                        }
                        else {
                            $StringValue = ''

                            $PropertyValue = $Property.Value
                            $StringValue = Transform $PropertyValue
                    
                            $TableRow += $StringValue.PadRight($ColumnWidths['Value'], ' ')
                        }
                    }

                    $InitialColumn = $false
                }
                $TableRow += ' |'

                Write-Output $TableRow
            }
            
        }

        function Transform ($PropertyValue) {
            $StringValue = ''
            if ($null -ne $PropertyValue) {
                if ($ArrayDelimiter -ne '' -and $PropertyValue -is [System.Collections.IList]) {
                    [array]$ArrayObject = New-Object -TypeName object[] -ArgumentList $PropertyValue.Count  # ConstrainedLanguage safe
                    for ($i = 0; $i -lt $PropertyValue.Count; $i++) {
                        $ArrayObject[$i] = $PropertyValue[$i].ToString()
                        if (!$ArrayObject[$i]) { $ArrayObject[$i] = $PropertyValue[$i].psobject.TypeNames[0] }
                    }
                    $StringValue = ($ArrayObject -join $ArrayDelimiter)
                }
                elseif ($PropertyValue -is [System.Collections.IDictionary] -or $PropertyValue -is [psobject]) {
                    if ($PropertyValue -is [System.Collections.IDictionary]) {
                        $PropertyValue = New-Object -TypeName PSObject -Property $PropertyValue  # ConstrainedLanguage safe
                    }
                    
                    if ($ObjectFormat -eq 'PsFormat') {
                        $FormattedObject = $PropertyValue | Format-List | Out-String -Width 2147483647
                        $StringValue = $FormattedObject.Trim("`r", "`n")
                    }
                    elseif ($ObjectFormat -eq 'Html') {
                        $HtmlTable = $PropertyValue | ConvertTo-Html -Fragment -As List
                        $StringValue = $HtmlTable -join ''
                    }
                    else {
                        $StringValue = $PropertyValue.ToString()
                        if (!$StringValue) { $StringValue = $PropertyValue.psobject.TypeNames[0] }
                    }
                }
                else {
                    $StringValue = $PropertyValue.ToString()
                }
            }
            $StringValue = $StringValue.Replace('\', '\\').Replace('|', '\|') # Escape backslash and pipe characters
            $StringValue = $StringValue -replace '(?<=[>])[\r\n]+(?=[<])', '' # Remove newlines between html tags
            $StringValue = $StringValue -replace '[\r\n]+', $NewLineReplacement # Replace newlines

            return $StringValue
        }

        $TableObjects = @()
    }

    process {
        foreach ($_InputObject in $InputObject) {
            ## Convert dictionaries
            if ($_InputObject -is [System.Collections.IDictionary]) {
                $_InputObject = New-Object -TypeName PSObject -Property $_InputObject  # ConstrainedLanguage safe
            }
            
            if ($Property) {
                $OutputObject = Select-Object -InputObject $_InputObject -Property $Property
            }
            else {
                $OutputObject = Select-Object -InputObject $_InputObject -Property "*"
            }

            $TableObjects += $OutputObject
        }
    }

    end {
        
        if (!$As) {
            if ($TableObjects.Count -gt 1) { $As = 'Table' }
            else { $As = 'List' }
        }

        if ($As -eq 'List') {
            foreach ($ObjectTable in $TableObjects) {
                ## Get column names and widths
                $KeyPairWidths = [ordered]@{ Name = 0; Value = 0 }
                foreach ($objProperty in $ObjectTable.PSObject.Properties) {
                    if (!$Compact -and $KeyPairWidths['Name'] -lt $objProperty.Name.Length) {
                        $KeyPairWidths['Name'] = $objProperty.Name.Length
                    }

                    $PropertyValue = Transform $objProperty.Value
                    if (!$Compact -and $null -ne $PropertyValue) {
                        if ($KeyPairWidths['Value'] -lt $PropertyValue.Length) {
                            $KeyPairWidths['Value'] = $PropertyValue.Length
                        }
                    }
                }

                ## Output Header and Separator Rows
                FormatMarkdownTableHeaderRow $KeyPairWidths
                ## Output Object Rows
                FormatMarkdownKeyPairRows $KeyPairWidths $ObjectTable
                ''
            }
        }
        else {
            ## Get column names and widths
            $ColumnWidths = [ordered]@{}
            foreach ($ObjectRow in $TableObjects) {
                foreach ($objProperty in $ObjectRow.PSObject.Properties) {
                    if ($Compact) {
                        $ColumnWidths[$objProperty.Name] = 0
                    }
                    elseif ($null -eq $ColumnWidths[$objProperty.Name]) {
                        $ColumnWidths[$objProperty.Name] = $objProperty.Name.Length
                    }
                    
                    $PropertyValue = Transform $objProperty.Value
                    if (!$Compact -and $null -ne $PropertyValue) {
                        if ($ColumnWidths[$objProperty.Name] -lt $PropertyValue.Length) {
                            $ColumnWidths[$objProperty.Name] = $PropertyValue.Length
                        }
                    }
                }
            }

            ## Output Header and Separator Rows
            FormatMarkdownTableHeaderRow $ColumnWidths

            ## Output Object Rows
            foreach ($ObjectRow in $TableObjects) {
                FormatMarkdownTableRow $ColumnWidths $ObjectRow
            }
        }

    }
}

#endregion

#region ConvertTo-PsParameterString.ps1

<#
.SYNOPSIS
    Convert splatable PowerShell paramters to PowerShell parameter string syntax.
 
.EXAMPLE
    PS >ConvertTo-PsParameterString @{ key1='value1'; key2='value2' }
 
    Convert hashtable to PowerShell parameters string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-PsParameterString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Specifies the parameter object to convert to PowerShell string.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        [object] $InputObjects,
        # Abbrivate types where possible
        [Parameter(Mandatory = $false)]
        [switch] $Compact,
        # Remove types
        [Parameter(Mandatory = $false, Position = 1)]
        [type[]] $RemoveTypes = ([string], [bool], [int], [long]),
        # Do not enumerate output objects
        [Parameter(Mandatory = $false)]
        [switch] $NoEnumerate
    )

    begin {
        function GetPsParameterString ($InputObject) {
            $OutputString = New-Object System.Text.StringBuilder

            ## Add Value
            switch ($InputObject.GetType()) {
                { $_.Equals([Hashtable]) -or $_.Equals([System.Collections.Specialized.OrderedDictionary]) -or $_.FullName.StartsWith('System.Collections.Generic.Dictionary') -or ($_.BaseType -and $_.BaseType.FullName.StartsWith('System.Collections.Generic.Dictionary')) } {
                    foreach ($Parameter in $InputObject.GetEnumerator()) {
                        [string] $ParameterValue = (ConvertTo-PsString $Parameter.Value -Compact:$Compact -NoEnumerate)
                        if ($ParameterValue.StartsWith('[')) { $ParameterValue = '({0})' -f $ParameterValue }
                        [void]$OutputString.AppendFormat(' -{0} {1}', $Parameter.Key, $ParameterValue)
                    }
                    break
                }
                { $_.BaseType.Equals([Array]) -or $_.Equals([System.Collections.ArrayList]) -or $_.FullName.StartsWith('System.Collections.Generic.List') } {
                    foreach ($Parameter in $InputObject) {
                        [string] $ParameterValue = (ConvertTo-PsString $Parameter -Compact:$Compact -NoEnumerate)
                        if ($ParameterValue.StartsWith('[')) { $ParameterValue = '({0})' -f $ParameterValue }
                        [void]$OutputString.AppendFormat(' {0}', $ParameterValue)
                    }
                    break
                }
                Default {
                    $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to PowerShell parameter string. Use -NoEnumerate if providing a single splatable array.' -f $InputObject.GetType())
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertPowerShellParameterStringFailureTypeNotSupported' -TargetObject $InputObject -ErrorAction Stop
                }
            }

            if ($NoEnumerate) {
                $listOutputString.Add($OutputString.ToString())
            }
            else {
                Write-Output $OutputString.ToString()
            }
        }

        if ($NoEnumerate) {
            $listOutputString = New-Object System.Collections.Generic.List[string]
        }
    }

    process {
        if ($PSCmdlet.MyInvocation.ExpectingInput -or $NoEnumerate) {
            GetPsParameterString $InputObjects
        }
        else {
            foreach ($InputObject in $InputObjects) {
                GetPsParameterString $InputObject
            }
        }
    }

    end {
        if ($NoEnumerate) {
            $OutputArray = New-Object System.Text.StringBuilder
            if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                [void]$OutputArray.AppendJoin('', $listOutputString)
            }
            else {
                [void]$OutputArray.Append(($listOutputString -join ''))
            }
            Write-Output $OutputArray.ToString()
        }
    }
}

#endregion

#region ConvertTo-PsString.ps1

<#
.SYNOPSIS
    Convert PowerShell data types to PowerShell string syntax.
 
.EXAMPLE
    PS >ConvertTo-PsString @{ key1='value1'; key2='value2' }
 
    Convert hashtable to PowerShell string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-PsString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Specifies the object to convert to PowerShell string.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        [object] $InputObjects,
        # Abbrivate types where possible
        [Parameter(Mandatory = $false)]
        [switch] $Compact,
        # Remove types
        [Parameter(Mandatory = $false, Position = 1)]
        [type[]] $RemoveTypes = ([string], [bool], [int], [long]),
        # Do not enumerate output objects
        [Parameter(Mandatory = $false)]
        [switch] $NoEnumerate
    )

    begin {
        if ($Compact) {
            [System.Collections.Generic.Dictionary[string, type]] $TypeAccelerators = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::get
            [System.Collections.Generic.Dictionary[type, string]] $TypeAcceleratorsLookup = New-Object 'System.Collections.Generic.Dictionary[type,string]'
            foreach ($TypeAcceleratorKey in $TypeAccelerators.Keys) {
                if (!$TypeAcceleratorsLookup.ContainsKey($TypeAccelerators[$TypeAcceleratorKey])) {
                    $TypeAcceleratorsLookup.Add($TypeAccelerators[$TypeAcceleratorKey], $TypeAcceleratorKey)
                }
            }
        }

        function Resolve-Type {
            param (
                #
                [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
                [type] $ObjectType,
                #
                [Parameter(Mandatory = $false, Position = 1)]
                [switch] $Compact,
                #
                [Parameter(Mandatory = $false, Position = 1)]
                [type[]] $RemoveTypes
            )

            [string] $OutputString = ''
            if ($ObjectType.IsGenericType -or ($ObjectType.BaseType -and $ObjectType.BaseType.IsGenericType)) {
                if (!$ObjectType.IsGenericType) { $ObjectType = $ObjectType.BaseType }
                if ($ObjectType.FullName.StartsWith('System.Collections.Generic.Dictionary')) {
                    #$OutputString += '[hashtable]'
                    if ($Compact) {
                        $OutputString += '(Invoke-Command { $D = New-Object ''Collections.Generic.Dictionary['
                    }
                    else {
                        $OutputString += '(Invoke-Command { $D = New-Object ''System.Collections.Generic.Dictionary['
                    }
                    $iInput = 0
                    foreach ($GenericTypeArgument in $ObjectType.GenericTypeArguments) {
                        if ($iInput -gt 0) { $OutputString += ',' }
                        $OutputString += Resolve-Type $GenericTypeArgument -Compact:$Compact -RemoveTypes @()
                        $iInput++
                    }
                    $OutputString += ']'''
                }
                elseif ($InputObject.GetType().FullName -match '^(System.(Collections.Generic.[a-zA-Z]+))`[0-9]\[(?:\[(.+?), .+?, Version=.+?, Culture=.+?, PublicKeyToken=.+?\],?)+?\]$') {
                    if ($Compact) {
                        $OutputString += '[{0}[' -f $Matches[2]
                    }
                    else {
                        $OutputString += '[{0}[' -f $Matches[1]
                    }
                    $iInput = 0
                    foreach ($GenericTypeArgument in $ObjectType.GenericTypeArguments) {
                        if ($iInput -gt 0) { $OutputString += ',' }
                        $OutputString += Resolve-Type $GenericTypeArgument -Compact:$Compact -RemoveTypes @()
                        $iInput++
                    }
                    $OutputString += ']]'
                }
            }
            elseif ($ObjectType -eq [System.Collections.Specialized.OrderedDictionary]) {
                $OutputString += '[ordered]'  # Explicit cast does not work with full name. Only [ordered] works.
            }
            elseif ($ObjectType -eq [System.Management.Automation.PSCustomObject]) {
                $OutputString += '[pscustomobject]'  # Explicit cast does not work with full name. Only [pscustomobject] works.
            }
            elseif ($Compact) {
                if ($ObjectType -notin $RemoveTypes) {
                    if ($TypeAcceleratorsLookup.ContainsKey($ObjectType)) {
                        $OutputString += '[{0}]' -f $TypeAcceleratorsLookup[$ObjectType]
                    }
                    elseif ($ObjectType.FullName.StartsWith('System.')) {
                        $OutputString += '[{0}]' -f $ObjectType.FullName.Substring(7)
                    }
                    else {
                        $OutputString += '[{0}]' -f $ObjectType.FullName
                    }
                }
            }
            else {
                $OutputString += '[{0}]' -f $ObjectType.FullName
            }
            return $OutputString
        }

        function GetPsString ($InputObject) {
            $OutputString = New-Object System.Text.StringBuilder

            if ($null -eq $InputObject) { [void]$OutputString.Append('$null') }
            else {
                ## Add Casting
                [void]$OutputString.Append((Resolve-Type $InputObject.GetType() -Compact:$Compact -RemoveTypes $RemoveTypes))

                ## Add Value
                switch ($InputObject.GetType()) {
                    { $_.Equals([String]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject.Replace("'", "''")) #.Replace('"','`"')
                        break
                    }
                    { $_.Equals([Char]) } {
                        [void]$OutputString.AppendFormat("'{0}'", ([string]$InputObject).Replace("'", "''"))
                        break
                    }
                    { $_.Equals([Boolean]) -or $_.Equals([switch]) } {
                        [void]$OutputString.AppendFormat('${0}', $InputObject)
                        break
                    }
                    { $_.Equals([DateTime]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject.ToString('O'))
                        break
                    }
                    { $_.Equals([guid]) -or $_.Equals([version]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject)
                        break
                    }
                    { $PSVersionTable.PSVersion -ge [version]'6.0' -and $_.Equals([semver]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject)
                        break
                    }
                    { $_.BaseType -and $_.BaseType.Equals([Enum]) } {
                        [void]$OutputString.AppendFormat('::{0}', $InputObject)
                        break
                    }
                    { $_.BaseType -and $_.BaseType.Equals([ValueType]) } {
                        [void]$OutputString.AppendFormat('{0}', $InputObject)
                        break
                    }
                    { $_.BaseType.Equals([System.IO.FileSystemInfo]) -or $_.Equals([System.Uri]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject.ToString().Replace("'", "''")) #.Replace('"','`"')
                        break
                    }
                    { $_.Equals([System.Xml.XmlDocument]) } {
                        [void]$OutputString.AppendFormat("'{0}'", $InputObject.OuterXml.Replace("'", "''")) #.Replace('"','""')
                        break
                    }
                    { $_.Equals([Hashtable]) -or $_.Equals([System.Collections.Specialized.OrderedDictionary]) } {
                        [void]$OutputString.Append('@{')
                        $iInput = 0
                        foreach ($enumHashtable in $InputObject.GetEnumerator()) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(';') }
                            [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $enumHashtable.Key -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $enumHashtable.Value -Compact:$Compact -NoEnumerate))
                            $iInput++
                        }
                        [void]$OutputString.Append('}')
                        break
                    }
                    { $_.FullName.StartsWith('System.Collections.Generic.Dictionary') -or ($_.BaseType -and $_.BaseType.FullName.StartsWith('System.Collections.Generic.Dictionary')) } {
                        $iInput = 0
                        foreach ($enumHashtable in $InputObject.GetEnumerator()) {
                            [void]$OutputString.AppendFormat('; $D.Add({0},{1})', (ConvertTo-PsString $enumHashtable.Key -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $enumHashtable.Value -Compact:$Compact -NoEnumerate))
                            $iInput++
                        }
                        [void]$OutputString.Append('; $D })')
                        break
                    }
                    { $_.BaseType -and $_.BaseType.Equals([Array]) } {
                        [void]$OutputString.Append('(Write-Output @(')
                        $iInput = 0
                        for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(',') }
                            [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -RemoveTypes $InputObject.GetType().DeclaredMembers.Where( { $_.Name -eq 'Set' })[0].GetParameters()[1].ParameterType -NoEnumerate))
                        }
                        [void]$OutputString.Append(') -NoEnumerate)')
                        break
                    }
                    { $_.Equals([System.Collections.ArrayList]) } {
                        [void]$OutputString.Append('@(')
                        $iInput = 0
                        for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(',') }
                            [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -NoEnumerate))
                        }
                        [void]$OutputString.Append(')')
                        break
                    }
                    { $_.FullName.StartsWith('System.Collections.Generic.List') } {
                        [void]$OutputString.Append('@(')
                        $iInput = 0
                        for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(',') }
                            [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -RemoveTypes $_.GenericTypeArguments -NoEnumerate))
                        }
                        [void]$OutputString.Append(')')
                        break
                    }
                    ## Convert objects with object initializers
                    { $_ -is [object] -and ($_.GetConstructors() | ForEach-Object { if ($_.IsPublic -and !$_.GetParameters()) { $true } }) } {
                        [void]$OutputString.Append('@{')
                        $iInput = 0
                        foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(';') }
                            $PropertyName = $Item.Name
                            [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $PropertyName -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $InputObject.$PropertyName -Compact:$Compact -NoEnumerate))
                            $iInput++
                        }
                        [void]$OutputString.Append('}')
                        break
                    }
                    { $_.Equals([System.Management.Automation.PSCustomObject]) } {
                        [void]$OutputString.Append('@{')
                        $iInput = 0
                        foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) {
                            if ($iInput -gt 0) { [void]$OutputString.Append(';') }
                            $PropertyName = $Item.Name
                            [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $PropertyName -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $InputObject.$PropertyName -Compact:$Compact -NoEnumerate))
                            $iInput++
                        }
                        [void]$OutputString.Append('}')
                        break
                    }
                    Default {
                        $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to PowerShell string.' -f $InputObject.GetType())
                        Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertPowerShellStringFailureTypeNotSupported' -TargetObject $InputObject
                    }
                }
            }

            if ($NoEnumerate) {
                $listOutputString.Add($OutputString.ToString())
            }
            else {
                Write-Output $OutputString.ToString()
            }
        }

        if ($NoEnumerate) {
            $listOutputString = New-Object System.Collections.Generic.List[string]
        }
    }

    process {
        if ($PSCmdlet.MyInvocation.ExpectingInput -or $NoEnumerate -or $null -eq $InputObjects) {
            GetPsString $InputObjects
        }
        else {
            foreach ($InputObject in $InputObjects) {
                GetPsString $InputObject
            }
        }
    }

    end {
        if ($NoEnumerate) {
            if (($null -eq $InputObjects -and $listOutputString.Count -eq 0) -or $listOutputString.Count -gt 1) {
                Write-Warning ('To avoid losing strong type on outermost enumerable type when piping, use "Write-Output $Array -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                $OutputArray = New-Object System.Text.StringBuilder
                [void]$OutputArray.Append('(Write-Output @(')
                if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                    [void]$OutputArray.AppendJoin(',', $listOutputString)
                }
                else {
                    [void]$OutputArray.Append(($listOutputString -join ','))
                }
                [void]$OutputArray.Append(') -NoEnumerate)')
                Write-Output $OutputArray.ToString()
            }
            else {
                Write-Output $listOutputString[0]
            }

        }
    }
}

#endregion

#region ConvertTo-QueryString.ps1

<#
.SYNOPSIS
    Convert Hashtable to Query String.
 
.EXAMPLE
    PS >ConvertTo-QueryString @{ name = 'path/file.json'; index = 10 }
 
    Convert hashtable to query string.
 
.EXAMPLE
    PS >[ordered]@{ title = 'convert&prosper'; id = [guid]'352182e6-9ab0-4115-807b-c36c88029fa4' } | ConvertTo-QueryString
 
    Convert ordered dictionary to query string.
 
.INPUTS
    System.Collections.Hashtable
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-QueryString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # URL encode parameter names
        [Parameter(Mandatory = $false)]
        [switch] $EncodeParameterNames
    )

    process {
        foreach ($InputObject in $InputObjects) {
            $QueryString = New-Object System.Text.StringBuilder
            if ($InputObject -is [System.Collections.IDictionary]) {
                foreach ($Item in $InputObject.GetEnumerator()) {
                    if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') }
                    [string] $ParameterName = $Item.Key
                    if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) }
                    [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($Item.Value))
                }
            }
            elseif ($InputObject -is [object] -and $InputObject -isnot [ValueType]) {
                foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) {
                    if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') }
                    [string] $ParameterName = $Item.Name
                    if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) }
                    [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($InputObject.($Item.Name)))
                }
            }
            else {
                ## Non-Terminating Error
                $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to query string.' -f $InputObject.GetType())
                Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertQueryStringFailureTypeNotSupported' -TargetObject $InputObject
                continue
            }

            Write-Output $QueryString.ToString()
        }
    }
}

#endregion

#region ConvertTo-UrlString.ps1

<#
.SYNOPSIS
    Convert string to URL encoded string.
 
.EXAMPLE
    PS >ConvertTo-UrlString 'A string with url encoding'
 
    Convert string to URL encoded string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function ConvertTo-UrlString {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string[]] $InputStrings
    )

    process {
        foreach ($InputString in $InputStrings) {
            Write-Output ([System.Net.WebUtility]::UrlEncode($InputString))
        }
    }
}

#endregion

#region Expand-Data.ps1

<#
.SYNOPSIS
    Decompress data using DEFLATE (RFC 1951) or GZIP file format (RFC 1952).
 
.EXAMPLE
    PS >[byte[]] $byteArray = @(115,84,40,46,41,202,204,75,87,72,203,47,82,72,206,207,45,40,74,45,46,206,204,207,3,0)
    PS >Expand-Data $byteArray
 
    Decompress string using Deflate.
 
.INPUTS
    System.String
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Expand-Data {
    [CmdletBinding()]
    [Alias('Decompress-Data')]
    [Alias('Inflate-Data')]
    [OutputType([string], [byte[]])]
    param (
        # Value to convert
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # Input is gzip file format
        [Parameter(Mandatory = $false)]
        [switch] $GZip,
        # Output raw byte array
        [Parameter (Mandatory = $false)]
        [switch] $RawBytes,
        # Encoding to use for text strings
        [Parameter (Mandatory = $false)]
        [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')]
        [string] $Encoding = 'Default'
    )

    begin {
        function Expand ([byte[]]$InputBytes) {
            try {
                $streamOutput = New-Object System.IO.MemoryStream
                try {
                    $streamInput = New-Object System.IO.MemoryStream -ArgumentList @($InputBytes, $false)
                    try {
                        if ($GZip) {
                            $streamCompression = New-Object System.IO.Compression.GZipStream -ArgumentList $streamInput, ([System.IO.Compression.CompressionMode]::Decompress)
                        }
                        else {
                            $streamCompression = New-Object System.IO.Compression.DeflateStream -ArgumentList $streamInput, ([System.IO.Compression.CompressionMode]::Decompress)
                        }
                        $streamCompression.CopyTo($streamOutput)
                    }
                    catch {
                        Write-Error -Exception $_.Exception.InnerException -Category ([System.Management.Automation.ErrorCategory]::InvalidData) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ExpandDataFailureInvalidData' -TargetObject $InputBytes -ErrorAction Stop
                    }
                    finally { $streamCompression.Dispose() }
                    [byte[]] $OutputBytes = $streamOutput.ToArray()
                }
                finally { $streamInput.Dispose() }
            }
            finally { $streamOutput.Dispose() }

            Write-Output $OutputBytes
        }

        ## Create list to capture byte stream from piped input.
        [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte]
    }

    process {
        if ($InputObjects -is [byte[]]) {
            [byte[]] $outBytes = Expand $InputObjects
            if ($RawBytes) {
                Write-Output $outBytes -NoEnumerate
            }
            else {
                [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes))
                Write-Output $outString
            }
        }
        else {
            foreach ($InputObject in $InputObjects) {
                [byte[]] $InputBytes = $null
                if ($InputObject -is [byte]) {
                    ## Populate list with byte stream from piped input.
                    if ($listBytes.Count -eq 0) {
                        Write-Verbose 'Creating byte array from byte stream.'
                        Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                    }
                    $listBytes.Add($InputObject)
                }
                elseif ($InputObject -is [byte[]]) {
                    $InputBytes = $InputObject
                }
                elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) {
                    $InputBytes = [System.BitConverter]::GetBytes($InputObject)
                }
                elseif ($InputObject -is [System.IO.FileSystemInfo]) {
                    if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream
                    }
                    else {
                        $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte
                    }
                }
                else {
                    ## Non-Terminating Error
                    $Exception = New-Object ArgumentException -ArgumentList ('Cannot compress input of type {0}.' -f $InputObject.GetType())
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'CompressDataFailureTypeNotSupported' -TargetObject $InputObject
                }

                if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) {
                    [byte[]] $outBytes = Expand $InputBytes
                    if ($RawBytes) {
                        Write-Output $outBytes -NoEnumerate
                    }
                    else {
                        [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes))
                        Write-Output $outString
                    }
                }
            }
        }
    }

    end {
        ## Output captured byte stream from piped input.
        if ($listBytes.Count -gt 0) {
            [byte[]] $outBytes = Expand $listBytes.ToArray()
            if ($RawBytes) {
                Write-Output $outBytes -NoEnumerate
            }
            else {
                [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes))
                Write-Output $outString
            }
        }
    }
}

#endregion

#region Format-DataSize.ps1

<#
.SYNOPSIS
    Format data size in bytes to human readable format.
 
.EXAMPLE
    PS >Format-DataSize 123
 
    Format 123 bytes to "123.0 Bytes".
 
.EXAMPLE
    PS >Format-DataSize 1234567890
 
    Format 1234567890 bytes to "1.150 GB".
 
.INPUTS
    System.Int64
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Format-DataSize {
    [CmdletBinding()]
    [Alias('Format-FileSize')]
    [OutputType([string])]
    param (
        # Specifies the number of bytes to auto scale and format.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [long] $Bytes
    )

    begin {
        ## Adapted From:
        ## https://github.com/PowerShell/PowerShell/blob/80b5df4b7f6e749e34a2363e1ef6cc09f2761c89/src/System.Management.Automation/engine/Utils.cs#L1489
        function DisplayHumanReadableFileSize([long] $bytes) {
            switch ($bytes) {
                { $_ -lt 1024 -and $_ -ge 0 } { return "{0:0.0} Bytes" -f $bytes }
                { $_ -lt 1048576 -and $_ -ge 1024 } { return "{0:0.0} KB" -f ($bytes / 1024) }
                { $_ -lt 1073741824 -and $_ -ge 1048576 } { return "{0:0.0} MB" -f ($bytes / 1048576) }
                { $_ -lt 1099511627776 -and $_ -ge 1073741824 } { return "{0:0.000} GB" -f ($bytes / 1073741824) }
                { $_ -lt 1125899906842624 -and $_ -ge 1099511627776 } { return "{0:0.00000} TB" -f ($bytes / 1099511627776) }
                { $_ -lt 1152921504606847000 -and $_ -ge 1125899906842624 } { return "{0:0.0000000} PB" -f ($bytes / 1125899906842624) }
                { $_ -ge 1152921504606847000 } { return "{0:0.000000000} EB" -f ($bytes / 1152921504606847000 ) }
                Default { return "0 Bytes" }
            }
        }
    }

    process {
        foreach ($Byte in $Bytes) {
            DisplayHumanReadableFileSize $Byte
        }
    }
}

#endregion

#region Format-NumberWithMetricUnit.ps1

<#
.SYNOPSIS
    Format number in different metric unit of measure.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 1234 -Unit 'byte(s)'
 
    Format number in kilobytes.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 12345678 -Unit 'B'
 
    Format number in megabytes.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 1234 'kilobyte(s)'
 
    Format number in megabytes.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 12345678 'KB' -TargetUnit 'MB'
 
    Format number in megabytes.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 1234 'bit(s)'
 
    Format number in kilobits.
 
.EXAMPLE
    PS >Format-NumberWithMetricUnit 1234 'm'
 
    Format number in kilometers.
 
.INPUTS
    System.Double
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Format-NumberWithMetricUnit {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Number to scale
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [double] $Number,
        # Unit of Number
        [Parameter(Mandatory = $true, Position = 2)]
        [string] $Unit,
        # Target Unit of Number
        [Parameter(Mandatory = $false)]
        [string] $TargetUnit
    )

    begin {
        $mapMetricSymbol = @{
            -8 = 'y'
            -7 = 'z'
            -6 = 'a'
            -5 = 'f'
            -4 = 'p'
            -3 = 'n'
            -2 = 'µ'
            -1 = 'm'
            0  = ''
            1  = 'k'
            2  = 'M'
            3  = 'G'
            4  = 'T'
            5  = 'P'
            6  = 'E'
            7  = 'Z'
            8  = 'Y'
        }

        $mapMetricPrefix = New-Object hashtable @{
            -8 = 'yocto'
            -7 = 'zepto'
            -6 = 'atto'
            -5 = 'femto'
            -4 = 'pico'
            -3 = 'nano'
            -2 = 'micro'
            -1 = 'milli'
            0  = ''
            1  = 'kilo'
            2  = 'mega'
            3  = 'giga'
            4  = 'tera'
            5  = 'peta'
            6  = 'exa'
            7  = 'zetta'
            8  = 'yotta'
        }

        # $mapMetricToExponent = @{
        # #'y' = -8
        # #'z' = -7
        # 'a' = -6
        # 'f' = -5
        # #'p' = -4
        # 'n' = -3
        # 'µ' = -2
        # #'m' = -1
        # 'yocto' = -8
        # 'zepto' = -7
        # 'atto' = -6
        # 'femto' = -5
        # 'pico' = -4
        # 'nano' = -3
        # 'micro' = -2
        # 'milli' = -1
        # '' = 0
        # 'kilo' = 1
        # 'mega' = 2
        # 'giga' = 3
        # 'tera' = 4
        # 'peta' = 5
        # 'exa' = 6
        # 'zetta' = 7
        # 'yotta' = 8
        # 'k' = 1
        # 'M' = 2
        # 'G' = 3
        # 'T' = 4
        # 'P' = 5
        # 'E' = 6
        # 'Z' = 7
        # 'Y' = 8
        # }

        # This method of adding hashtable method uses case-sensitive lookups
        $mapMetricToExponent = New-Object hashtable
        $mapMetricToExponent.Add('y', -8)
        $mapMetricToExponent.Add('z', -7)
        $mapMetricToExponent.Add('a', -6)
        $mapMetricToExponent.Add('f', -5)
        $mapMetricToExponent.Add('p', -4)
        $mapMetricToExponent.Add('n', -3)
        $mapMetricToExponent.Add('µ', -2)
        $mapMetricToExponent.Add('m', -1)
        $mapMetricToExponent.Add('yocto', -8)
        $mapMetricToExponent.Add('zepto', -7)
        $mapMetricToExponent.Add('atto', -6)
        $mapMetricToExponent.Add('femto', -5)
        $mapMetricToExponent.Add('pico', -4)
        $mapMetricToExponent.Add('nano', -3)
        $mapMetricToExponent.Add('micro', -2)
        $mapMetricToExponent.Add('milli', -1)
        $mapMetricToExponent.Add('', 0)
        $mapMetricToExponent.Add('kilo', 1)
        $mapMetricToExponent.Add('mega', 2)
        $mapMetricToExponent.Add('giga', 3)
        $mapMetricToExponent.Add('tera', 4)
        $mapMetricToExponent.Add('peta', 5)
        $mapMetricToExponent.Add('exa', 6)
        $mapMetricToExponent.Add('zetta', 7)
        $mapMetricToExponent.Add('yotta', 8)
        $mapMetricToExponent.Add('k', 1)
        $mapMetricToExponent.Add('M', 2)
        $mapMetricToExponent.Add('G', 3)
        $mapMetricToExponent.Add('T', 4)
        $mapMetricToExponent.Add('P', 5)
        $mapMetricToExponent.Add('E', 6)
        $mapMetricToExponent.Add('Z', 7)
        $mapMetricToExponent.Add('Y', 8)
    }

    process {
        if ($Unit -match '^(yocto|zepto|atto|femto|pico|nano|micro|milli|centi|deci|deca|hecto|kilo|mega|giga|tera|peta|exa|zetta|yotta|[yzafpnµmcdhkMGTPEZY](?=[A-Z]$|(?-i:[A-Z])))?(.*)$') {
            $UnitPrefix = if ($Matches[1] -in 'M', 'P', 'Z', 'Y') { $Matches[1] } elseif ($Matches[1] -in 'G', 'T', 'E') { $Matches[1].ToUpper() } elseif ($Matches[1]) { $Matches[1].ToLower() } else { '' }
            $UnitName = $Matches[2]
        }

        if ($UnitName.StartsWith('Byte', [System.StringComparison]::OrdinalIgnoreCase) -or $UnitName -ceq 'B') {
            $Base = 1024
        }
        else {
            $Base = 1000
        }

        [int] $SourceExponent = $mapMetricToExponent[$UnitPrefix]
        [int] $AutoExponent = 0
        if ($TargetUnit) {
            if ($TargetUnit -match '^(yocto|zepto|atto|femto|pico|nano|micro|milli|centi|deci|deca|hecto|kilo|mega|giga|tera|peta|exa|zetta|yotta|[yzafpnµmcdhkMGTPEZY](?=[A-Z]$|(?-i:[A-Z])))?(.*)$') {
                [string] $TargetUnitPrefix = $null
                $TargetUnitPrefix = if ($Matches[1] -in 'M', 'P', 'Z', 'Y') { $Matches[1] } elseif ($Matches[1] -in 'G', 'T', 'E') { $Matches[1].ToUpper() } elseif ($Matches[1]) { $Matches[1].ToLower() } else { '' }
                #$TargetUnitName = $Matches[2]

                $AutoExponent = $mapMetricToExponent[$TargetUnitPrefix] - $SourceExponent
            }
        }
        else {
            $AutoExponent = [System.Math]::Floor([System.Math]::Log($Number) / [System.Math]::Log($Base)) # Fails when number is negative

            if ($UnitName.Length -le 2) {
                $TargetUnit = $mapMetricSymbol[($SourceExponent + $AutoExponent)] + $UnitName
            }
            else {
                $TargetUnit = $mapMetricPrefix[($SourceExponent + $AutoExponent)] + $UnitName
            }
        }
        [double] $ScaledNumber = $Number / [System.Math]::Pow($Base, $AutoExponent)

        Write-Output ('{0:0.##} {1}' -f $ScaledNumber, $TargetUnit)
    }
}

#endregion

#region Format-PropertyValue.ps1

<#
.SYNOPSIS
    Format objects as readable strings for CSV output.
 
.EXAMPLE
    PS > Format-PropertyValue @{ NestedHashtables = @{ lvl1 = @{ lvl2 = @('value1','value2') } }; Array = @('value1','value2') } -SingleLineOutput
 
    Format property values to a single line string for CSV output.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Format-PropertyValue {
    [CmdletBinding()]
    [OutputType([psobject])]
    param (
        # Specifies the objects that are converted to CSV strings.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [psobject[]] $InputObject,
        # Property names to include in the output.
        [Parameter(Mandatory = $false, Position = 1)]
        [string[]] $Property,
        # Specify how objects are represented in the output.
        [Parameter(Mandatory = $false)]
        [ValidateSet('PsFormat', 'PsFormatExpression', 'ToString', 'Json', 'Html')]
        [string] $ObjectFormat,
        # Determines how many items are enumerated. Set to 0 to enumerate all items. Default is $global:FormatEnumerationLimit.
        [Parameter(Mandatory = $false)]
        [int] $EnumerationLimit = $global:FormatEnumerationLimit,
        # Specifies how many levels of nested objects are included. This does not apply to ToString object representation.
        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 100)]
        [int] $Depth = 1,
        # Formatted output is a single line.
        [Parameter(Mandatory = $false)]
        [switch] $SingleLineOutput,
        # Format root level object properties in addition to nested properties.
        [Parameter(Mandatory = $false)]
        [switch] $FormatRootLevelProperties
    )

    begin {
        ## Initalize variables
        $TopLevelTypes = [string], [ValueType], [version]
        if ($PSVersionTable.PSVersion -ge '6.0') { $TopLevelTypes += [semver] }
        $PrevFormatEnumerationLimit = $global:FormatEnumerationLimit
        $NewLineReplacement = '; '
        $ArrayDelimiter = "`r`n"
        $PsFormatWidth = 2147483646

        ## Set default values
        if (!$ObjectFormat) {
            if ($SingleLineOutput) {
                $ObjectFormat = 'ToString'
            }
            else {
                $ObjectFormat = 'PsFormat'
            }
        }

        ## ToDo: Find a way to replicate SmartToString function to replace current PsFormatExpression/ToString object representation implementation.
        # https://github.com/PowerShell/PowerShell/blob/08baf27b80e604d1685c065ea75761508634de12/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs#L213

        function IsTopLevelType ($InputObject) {
            foreach ($TopLevelType in $TopLevelTypes) {
                if ($InputObject -is $TopLevelType) { return $true }
            }
            return $false
        }

        function TransformObject ($InputObject, [string]$ObjectFormat) {
            if ($InputObject -is [System.Collections.IList]) {
                $Truncated = $false
                if ($EnumerationLimit -gt 0 -and $InputObject.Count -gt $EnumerationLimit) { $InputObject = $InputObject[0..($EnumerationLimit - 1)] + '...'; $Truncated = $true }
                # if (IsTopLevelType $InputObject[0]) {
                # $OutputObject = $InputObject -join $ArrayDelimiter
                # }
            }

            if ($ObjectFormat -eq 'Json') {
                $JsonDepth = if ($FormatRootLevelProperties -or $Depth -eq 0) { $Depth } else { $Depth - 1 }
                $OutputObject = $InputObject | ConvertTo-Json -Depth $JsonDepth -Compress:$SingleLineOutput
            }
            elseif ($ObjectFormat -eq 'Html') {
                if ($InputObject -is [System.Collections.IList]) {
                    if (IsTopLevelType $InputObject[0]) {
                        $OutputObject = $InputObject -join $ArrayDelimiter
                    }
                    else {
                        if ($Truncated) { $InputObject = $InputObject[0..($InputObject.Count - 2)] }
                        $OutputObject = $InputObject | ConvertTo-Html -Fragment -As Table
                        if ($Truncated) { $OutputObject[-1] += '...' }
                    }
                }
                else {
                    if (IsTopLevelType $InputObject) { $OutputObject = $InputObject }
                    else { $OutputObject = $InputObject | ConvertTo-Html -Fragment -As List }
                }
                if ($SingleLineOutput) { $OutputObject = $OutputObject -join "" }
                else { $OutputObject = $OutputObject -join "`r`n" }

                ## Decode inner HTML table tags
                #$OutputObject = [System.Net.WebUtility]::HtmlDecode($OutputObject) # Not ConstrainedLanguage safe
                $OutputObject = $OutputObject -replace '&lt;(/?(?:table|th|tr|td|colgroup|col)/?)&gt;', '<$1>'  # Escape nested table tags
                $OutputObject = $OutputObject -replace '&amp;(?=[a-zA-Z0-9]+;)', '&'  # Decode HTML ampersands due to double encoding
            }
            elseif ($ObjectFormat -eq 'PsFormat') {
                try {
                    if ($EnumerationLimit) { $global:FormatEnumerationLimit = $EnumerationLimit }
                    if ($InputObject -is [System.Collections.IList]) {
                        if (IsTopLevelType $InputObject[0]) {
                            $OutputObject = $InputObject -join $ArrayDelimiter
                        }
                        else {
                            $OutputObject = $InputObject | Format-Table -AutoSize | Out-String -Width $PsFormatWidth
                        }
                    }
                    else {
                        if (IsTopLevelType $InputObject) { $OutputObject = $InputObject }
                        else { $OutputObject = $InputObject | Format-List | Out-String -Width $PsFormatWidth }
                    }
                }
                finally {
                    if ($EnumerationLimit) { $global:FormatEnumerationLimit = $PrevFormatEnumerationLimit }
                }
                ## Remove trailing new line from Out-String
                $OutputObject = $OutputObject.Trim("`r", "`n")
            }
            elseif ($ObjectFormat -eq 'ToString') {
                if ($InputObject -is [System.Collections.IList]) {
                    ## Expand array items
                    [array]$ArrayObject = New-Object -TypeName object[] -ArgumentList $InputObject.Count  # ConstrainedLanguage safe
                    for ($i = 0; $i -lt $InputObject.Count; $i++) {
                        $ArrayObject[$i] = $InputObject[$i].ToString()
                        if (!$ArrayObject[$i]) { $ArrayObject[$i] = $InputObject[$i].psobject.TypeNames[0] }
                    }

                    $OutputObject = $ArrayObject -join $ArrayDelimiter
                }
                else {
                    $OutputObject = $InputObject.ToString()
                    if (!$OutputObject) { $OutputObject = $InputObject.psobject.TypeNames[0] }
                }
            }
            else {
                $OutputObject = $InputObject
            }
            return $OutputObject
        }

        function Transform ($InputObject, [int]$CurrentDepth = 0) {
            if ($InputObject) {
                if ($InputObject -is [string]) {
                    $OutputObject = $InputObject.ToString()
                }
                elseif ($InputObject -is [DateTime]) {
                    $OutputObject = $InputObject.ToString("o")
                }
                elseif (IsTopLevelType $InputObject) {
                    $OutputObject = $InputObject.ToString()
                }
                elseif ($InputObject -is [System.Collections.IDictionary]) {
                    ## Convert hashtables to PSObjects and shallow copy object
                    $OutputObject = New-Object -TypeName PSObject -Property $InputObject  # ConstrainedLanguage safe

                    if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) {
                        foreach ($Key in $InputObject.Keys) {
                            $OutputObject.$Key = Transform $InputObject[$Key] ($CurrentDepth + 1)
                        }
                    }

                    $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat
                }
                elseif ($InputObject -is [System.Collections.ICollection]) {
                    ## Shallow copy array
                    [array]$OutputObject = $InputObject | ForEach-Object { $_ }  # ConstrainedLanguage safe

                    if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) {
                        # foreach ($_OutputObject in $OutputObject) {
                        # $_OutputObject = Transform $_OutputObject ($CurrentDepth + 1)
                        # }
                        # for ($i = 0; $i -lt $InputObject.Count; $i++) {
                        # $OutputObject[$i] = Transform $InputObject[$i] ($CurrentDepth + 1)
                        # }
                        for ($i = 0; $i -lt $OutputObject.Count; $i++) {
                            $OutputObject[$i] = Transform $OutputObject[$i] ($CurrentDepth + 1)
                        }
                    }
                    
                    $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat
                }
                elseif ($InputObject -is [psobject]) {
                    ## Shallow copy PSObject
                    $OutputObject = Select-Object -InputObject $InputObject -Property "*"  # ConstrainedLanguage safe

                    if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) {
                        foreach ($objProperty in $InputObject.psobject.Properties) {
                            $PropertyName = $objProperty.Name
                            $OutputObject.$PropertyName = Transform $objProperty.Value ($CurrentDepth + 1)
                        }
                    }

                    $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat
                }
                else {
                    $OutputObject = $InputObject.ToString()
                }

                if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement }

                return $OutputObject
            }
            return $InputObject
        }

    }

    process {
        foreach ($_InputObject in $InputObject) {

            if ($_InputObject -is [string]) {
                $OutputObject = $_InputObject
                if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement }
            }
            elseif ($_InputObject -is [System.ValueType]) {
                $OutputObject = Transform $_InputObject
                if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement }
            }
            elseif ($_InputObject -is [System.Collections.IDictionary] -or $_InputObject -is [psobject]) {
                if ($_InputObject -is [System.Collections.IDictionary]) {
                    ## Convert IDictionary to PSObject
                    $_InputObject = New-Object -TypeName PSObject -Property $_InputObject  # ConstrainedLanguage safe
                }

                if ($Property) {
                    $OutputObject = Select-Object -InputObject $_InputObject -Property $Property
                }
                else {
                    $OutputObject = Select-Object -InputObject $_InputObject -Property "*"
                }
                
                if ($FormatRootLevelProperties) {
                    $OutputObject = Transform $OutputObject -CurrentDepth 0
                }
                else {
                    foreach ($objProperty in $OutputObject.psobject.Properties) {
                        $PropertyName = $objProperty.Name
                        if (!$Property -or $objProperty.Name -in $Property) {
                            $OutputObject.$PropertyName = Transform $objProperty.Value -CurrentDepth 1
                        }
                    }
                }
            }
            else {
                $OutputObject = Transform $_InputObject
                if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement }
            }

            try {
                if ($EnumerationLimit) { $global:FormatEnumerationLimit = $EnumerationLimit }
                Write-Output $OutputObject
            }
            finally {
                if ($EnumerationLimit) { $global:FormatEnumerationLimit = $PrevFormatEnumerationLimit }
            }

        }
    }
}

#endregion

#region Get-ContentEncoding.ps1

<#
.SYNOPSIS
    Get content encoding of byte array for file
 
.EXAMPLE
    PS >Get-ContentEncoding ([byte[]](0xFE, 0xFF, 0x00, 0x00))
 
    Get content encoding of byte array.
 
.EXAMPLE
    PS >Get-ContentEncoding 'file.txt'
     
    Get content encoding of file.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-ContentEncoding {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param (
        # Content represented as byte array
        [Parameter(Mandatory = $true, ParameterSetName = 'ByteArray', Position = 0, ValueFromPipeline = $true)]
        [byte[]] $InputBytes,
        # Content file path
        [Parameter(Mandatory = $true, ParameterSetName = 'File', Position = 0)]
        [string] $Path,
        # Number of bytes to read from beginning of file
        [Parameter(Mandatory = $false, ParameterSetName = 'File')]
        [int] $NumberOfBytesToRead = 8000
    )

    begin {
        function New-ContentEncodingOutput ($Encoding) {
            $Output = [pscustomobject][ordered]@{
                CodePage     = $null
                Name         = $null
                DisplayName  = $null
                TextEncoding = $null
            }
            if ($Encoding -is [System.Text.Encoding]) {
                $Output.CodePage = $Encoding.CodePage
                $Output.Name = $Encoding.WebName
                $Output.DisplayName = $Encoding.EncodingName
                $Output.TextEncoding = $Encoding
            }
            else {
                $Output.Name = $Output.DisplayName = $Encoding
            }
            return $Output
        }

        $CriticalError = $null
        if ($PSCmdlet.ParameterSetName -eq 'File') {
            [byte[]] $InputBytes = $null
            if (Resolve-Path $Path -ErrorVariable CriticalError) {
                #Get-Content $Path -AsByteStream -ReadCount $NumberOfBytesToRead
                $FileStream = [System.IO.File]::OpenRead($Path)
                try {
                    $BinaryReader = New-Object System.IO.BinaryReader -ArgumentList $FileStream
                    $InputBytes = $BinaryReader.ReadBytes($NumberOfBytesToRead)
                }
                finally {
                    $FileStream.Close()
                }
            }
        }
        
        ## Intialize
        $EncodingInfo = [System.Text.Encoding]::GetEncodings()
        [System.Collections.Generic.List[System.Text.Encoding]] $listEncodings = $EncodingInfo.GetEncoding() | Where-Object { $_.GetPreamble() } | Sort-Object { $_.GetPreamble().Count } -Descending
        [int] $MaxPreambleCount = $listEncodings[0].GetPreamble().Count

        Set-Variable NullByte -Option Constant -Value ([byte]0x00)
        [bool] $ContainsNull = $false
        [int] $Position = 0
    }

    process {
        if ($CriticalError) { return }

        foreach ($byte in $InputBytes) {
            ## Break out of loop if null was found and any potential preambles are complete
            if ($ContainsNull -eq $true -and $Position -ge $MaxPreambleCount) { return }

            ## Check for BOM preamble to determine text encoding
            if ($Position -lt $MaxPreambleCount) {
                for ($i = 0; $i -lt $listEncodings.Count; $i++) {
                    [byte[]]$Preamble = $listEncodings[$i].GetPreamble()
                    if ($Position -lt $Preamble.Count -and $byte -ne $Preamble[$Position]) {
                        $listEncodings.RemoveAt($i)
                        $i--
                    }
                }
            }

            ## Check for null byte as it could mean binary data
            if ($byte.Equals($NullByte)) {
                $ContainsNull = $true
            }
            
            ## Advance position
            $Position++
        }
    }

    end {
        if ($CriticalError) { return }

        ## Produce output object
        if ($listEncodings.Count -gt 0) {
            New-ContentEncodingOutput $listEncodings[0]
        }
        elseif ($ContainsNull) {
            New-ContentEncodingOutput 'Binary'
        }
        else {
            New-ContentEncodingOutput 'Unknown'
        }
    }
}

#endregion

#region Get-PropertyValue.ps1

<#
.SYNOPSIS
    Get object property value in a manner that satifies strict mode.
 
.EXAMPLE
    PS >$object = New-Object psobject -Property @{ title = 'title value' }
    PS >$object | Get-PropertyValue -Property 'title'
 
    Get value of object property named title.
 
.EXAMPLE
    PS >$object = New-Object psobject -Property @{ lvl1 = (New-Object psobject -Property @{ nextLevel = 'lvl2 data' }) }
    PS >Get-PropertyValue $object -Property 'lvl1', 'nextLevel'
 
    Get value of nested object property named nextLevel.
 
.INPUTS
    System.Collections.IDictionary
    System.Management.Automation.PSObject
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-PropertyValue {
    [CmdletBinding()]
    [OutputType([psobject])]
    param (
        # Object containing property values
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [AllowNull()]
        [psobject] $InputObjects,
        # Name of property. Specify an array of property names to tranverse nested objects.
        [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
        [string[]] $Property
    )

    process {
        foreach ($InputObject in $InputObjects) {
            for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) {
                ## Get property value
                if ($InputObject -is [System.Collections.IDictionary]) {
                    if ($InputObject.Contains($Property[$iProperty])) {
                        $PropertyValue = $InputObject[$Property[$iProperty]]
                    }
                    else { $PropertyValue = $null }
                }
                else {
                    $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore
                }
                ## Check for more nested properties
                if ($iProperty -lt $Property.Count - 1) {
                    $InputObject = $PropertyValue
                    if ($null -eq $InputObject) { break }
                }
                else {
                    Write-Output $PropertyValue
                }
            }
        }
    }
}

#endregion

#region Get-RelativePath.ps1

<#
.SYNOPSIS
    Get path relative to working directory.
 
.EXAMPLE
    PS >Get-RelativePath 'C:\DirectoryA\File1.txt'
 
    Get path relative to current directory.
 
.EXAMPLE
    PS >Get-RelativePath 'C:\DirectoryA\File1.txt' -WorkingDirectory 'C:\DirectoryB' -CompareCase
 
    Get path relative to specified working directory with case-sensitive directory comparison.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-RelativePath {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # Input paths
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string[]] $InputObjects,
        # Working directory for relative paths. Default is current directory.
        [Parameter(Mandatory = $false, Position = 2)]
        [string] $WorkingDirectory = (Get-Location).ProviderPath,
        # Compare directory names as case-sensitive.
        [Parameter(Mandatory = $false)]
        [switch] $CompareCase,
        # Directory separator used in paths.
        [Parameter(Mandatory = $false)]
        [char] $DirectorySeparator = [System.IO.Path]::DirectorySeparatorChar
    )

    begin {
        ## Adapted From:
        ## https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.Uri/src/System/Uri.cs#L5037
        function PathDifference([string] $path1, [string] $path2, [bool] $compareCase, [char] $directorySeparator = [System.IO.Path]::DirectorySeparatorChar) {
            [int] $i = 0
            [int] $si = -1

            for ($i = 0; ($i -lt $path1.Length) -and ($i -lt $path2.Length); $i++) {
                if (($path1[$i] -cne $path2[$i]) -and ($compareCase -or ([char]::ToLowerInvariant($path1[$i]) -cne [char]::ToLowerInvariant($path2[$i])))) {
                    break
                }
                elseif ($path1[$i] -ceq $directorySeparator) {
                    $si = $i
                }
            }

            if ($i -ceq 0) {
                return $path2
            }
            if (($i -ceq $path1.Length) -and ($i -ceq $path2.Length)) {
                return [string]::Empty
            }

            [System.Text.StringBuilder] $relPath = New-Object System.Text.StringBuilder
            ## Walk down several dirs
            for (; $i -lt $path1.Length; $i++) {
                if ($path1[$i] -ceq $directorySeparator) {
                    [void] $relPath.Append("..$directorySeparator")
                }
            }
            ## Same path except that path1 ended with a file name and path2 didn't
            if ($relPath.Length -ceq 0 -and $path2.Length - 1 -ceq $si) {
                return ".$directorySeparator" ## Truncate the file name
            }
            return $relPath.Append($path2.Substring($si + 1)).ToString()
        }
    }

    process {
        foreach ($InputObject in $InputObjects) {
            if (!$WorkingDirectory.EndsWith($DirectorySeparator)) { $WorkingDirectory += $DirectorySeparator }
            [string] $RelativePath = '.{0}{1}' -f $DirectorySeparator, (PathDifference $WorkingDirectory $InputObject $CompareCase $DirectorySeparator)
            Write-Output $RelativePath
        }
    }
}

#endregion

#region Get-StrictModeVersion.ps1

<#
.SYNOPSIS
    Get the strict mode version of the current session scope.
     
.DESCRIPTION
    Get the strict mode version of the current session scope.
    1.0
        Prohibits references to uninitialized variables, except for uninitialized variables in strings.
    2.0
        Prohibits references to uninitialized variables. This includes uninitialized variables in strings.
        Prohibits references to non-existent properties of an object.
        Prohibits function calls that use the syntax for calling methods.
    3.0
        Prohibits references to uninitialized variables. This includes uninitialized variables in strings.
        Prohibits references to non-existent properties of an object.
        Prohibits function calls that use the syntax for calling methods.
        Prohibit out of bounds or unresolvable array indexes.
 
.EXAMPLE
    PS >Get-StrictModeVersion
 
    Get the strict mode version of the current session scope.
 
.INPUTS
    None
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-StrictModeVersion {
    [CmdletBinding()]
    [OutputType([version])]
    param ()

    try { $null = @()[0] }
    catch { return [version]'3.0' }

    try { $null = $null.NonExistentProperty }
    catch { return [version]'2.0' }

    try { $null = $UninitializedVariable }
    catch { return [version]'1.0' }

    return [version]'0.0'
}

#endregion

#region Get-X509Certificate.ps1

<#
.SYNOPSIS
    Get certificate object for X509 certificate.
 
.EXAMPLE
    PS >[byte[]] $DERCert = @(48,130,4,18,48,130,2,250,160,3,2,1,2,2,15,0,193,0,139,60,60,136,17,209,62,246,99,236,223,64,48,13,6,9,42,134,72,134,247,13,1,1,4,5,0,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,48,30,23,13,57,55,48,49,49,48,48,55,48,48,48,48,90,23,13,50,48,49,50,51,49,48,55,48,48,48,48,90,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,3,130,1,15,0,48,130,1,10,2,130,1,1,0,169,2,189,193,112,230,59,242,78,27,40,159,151,120,94,48,234,162,169,141,37,95,248,254,149,76,163,183,254,157,162,32,62,124,81,162,155,162,143,96,50,107,209,66,100,121,238,172,118,201,84,218,242,235,156,134,28,143,159,132,102,179,197,107,122,98,35,214,29,60,222,15,1,146,232,150,196,191,45,102,154,154,104,38,153,208,58,44,191,12,181,88,38,193,70,231,10,62,56,150,44,169,40,57,168,236,73,131,66,227,132,15,187,154,108,85,97,172,130,124,161,96,45,119,76,233,153,180,100,59,154,80,28,49,8,36,20,159,169,231,145,43,24,230,61,152,99,20,96,88,5,101,159,29,55,82,135,247,167,239,148,2,198,27,211,191,85,69,179,137,128,191,58,236,84,148,78,174,253,167,122,109,116,78,175,24,204,150,9,40,33,0,87,144,96,105,55,187,75,18,7,60,86,255,91,251,164,102,10,8,166,210,129,86,87,239,182,59,94,22,129,119,4,218,246,190,174,128,149,254,176,205,127,214,167,26,114,92,60,202,188,240,8,163,34,48,179,6,133,201,179,32,119,19,133,223,2,3,1,0,1,163,129,168,48,129,165,48,129,162,6,3,85,29,1,4,129,154,48,129,151,128,16,91,208,112,239,105,114,158,35,81,126,20,178,77,142,255,203,161,114,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,130,15,0,193,0,139,60,60,136,17,209,62,246,99,236,223,64,48,13,6,9,42,134,72,134,247,13,1,1,4,5,0,3,130,1,1,0,149,232,11,192,141,243,151,24,53,237,184,1,36,216,119,17,243,92,96,50,159,158,11,203,62,5,145,136,143,201,58,230,33,242,240,87,147,44,181,160,71,200,98,239,252,215,204,59,59,90,169,54,84,105,254,36,109,63,201,204,170,222,5,124,221,49,141,61,159,16,112,106,187,254,18,79,24,105,192,252,208,67,227,17,90,32,79,234,98,123,175,170,25,200,43,55,37,45,190,101,161,18,138,37,15,99,163,247,84,28,249,33,201,214,21,243,82,172,110,67,50,7,253,130,23,248,229,103,108,13,81,246,189,241,82,199,189,231,196,48,252,32,49,9,136,29,149,41,26,77,213,29,2,165,241,128,224,3,180,91,244,177,221,200,87,238,101,73,199,82,84,182,180,3,40,18,255,144,214,240,8,143,126,184,151,197,171,55,44,228,122,228,168,119,227,118,160,0,208,106,63,193,210,54,138,224,65,18,168,53,106,27,106,219,53,225,212,28,4,228,168,69,4,200,90,51,56,110,77,28,13,98,183,10,162,140,211,213,84,63,70,205,28,85,166,112,219,18,58,135,147,117,159,167,210,160)
    PS >Get-X509Certificate $DERCert -Verbose
 
    Get certificate details from binary (DER) encoded X509 certificate.
 
.EXAMPLE
    PS >[string] $Base64Cert = 'MIIEEjCCAvqgAwIBAgIPAMEAizw8iBHRPvZj7N9AMA0GCSqGSIb3DQEBBAUAMHAxKzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5MB4XDTk3MDExMDA3MDAwMFoXDTIwMTIzMTA3MDAwMFowcDErMCkGA1UECxMiQ29weXJpZ2h0IChjKSAxOTk3IE1pY3Jvc29mdCBDb3JwLjEeMBwGA1UECxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEwHwYDVQQDExhNaWNyb3NvZnQgUm9vdCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpAr3BcOY78k4bKJ+XeF4w6qKpjSVf+P6VTKO3/p2iID58UaKboo9gMmvRQmR57qx2yVTa8uuchhyPn4Rms8VremIj1h083g8BkuiWxL8tZpqaaCaZ0Dosvwy1WCbBRucKPjiWLKkoOajsSYNC44QPu5psVWGsgnyhYC13TOmZtGQ7mlAcMQgkFJ+p55ErGOY9mGMUYFgFZZ8dN1KH96fvlALGG9O/VUWziYC/OuxUlE6u/ad6bXROrxjMlgkoIQBXkGBpN7tLEgc8Vv9b+6RmCgim0oFWV++2O14WgXcE2va+roCV/rDNf9anGnJcPMq88AijIjCzBoXJsyB3E4XfAgMBAAGjgagwgaUwgaIGA1UdAQSBmjCBl4AQW9Bw72lyniNRfhSyTY7/y6FyMHAxKzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5gg8AwQCLPDyIEdE+9mPs30AwDQYJKoZIhvcNAQEEBQADggEBAJXoC8CN85cYNe24ASTYdxHzXGAyn54Lyz4FkYiPyTrmIfLwV5MstaBHyGLv/NfMOztaqTZUaf4kbT/JzKreBXzdMY09nxBwarv+Ek8YacD80EPjEVogT+pie6+qGcgrNyUtvmWhEoolD2Oj91Qc+SHJ1hXzUqxuQzIH/YIX+OVnbA1R9r3xUse958Qw/CAxCYgdlSkaTdUdAqXxgOADtFv0sd3IV+5lScdSVLa0AygS/5DW8AiPfriXxas3LOR65Kh343agANBqP8HSNorgQRKoNWobats14dQcBOSoRQTIWjM4bk0cDWK3CqKM09VUP0bNHFWmcNsSOoeTdZ+n0qA='
    PS >$Base64Cert | Get-X509Certificate -Verbose
 
    Get certificate details from Base64 encoded X509 certificate.
 
.EXAMPLE
    PS >Get-Item "certificateFile.cer" | Get-X509Certificate
 
    Get certificate details from .cer file.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-X509Certificate {
    [CmdletBinding()]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2], [System.Security.Cryptography.X509Certificates.X509Certificate2Collection])]
    param (
        # X.509 certificate that is binary (DER) encoded or Base64-encoded
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [object] $InputObjects,
        # Only return the end-entity certificate
        [Parameter(Mandatory = $false)]
        [switch] $EndEntityCertificateOnly
    )

    begin {
        ## Create list to capture byte stream from piped input.
        [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte]

        function Transform ([byte[]]$InputBytes) {
            $X509CertificateCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
            $X509CertificateCollection.Import($InputBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet)
            Write-Output $X509CertificateCollection -NoEnumerate
        }
    }

    process {
        if ($InputObjects -is [byte[]]) {
            $X509CertificateCollection = Transform $InputObjects
            if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] }
            else { Write-Output $X509CertificateCollection }
        }
        else {
            foreach ($InputObject in $InputObjects) {
                [byte[]] $inputBytes = $null
                if ($InputObject -is [byte]) {
                    ## Populate list with byte stream from piped input.
                    if ($listBytes.Count -eq 0) {
                        Write-Verbose 'Creating byte array from byte stream.'
                        Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand)
                    }
                    $listBytes.Add($InputObject)
                }
                elseif ($InputObject -is [byte[]]) {
                    $inputBytes = $InputObject
                }
                elseif ($InputObject -is [string] -or $InputObject -is [SecureString]) {
                    if ($InputObject -is [SecureString]) {
                        Write-Verbose 'Decrypting SecureString and decoding Base64 string to byte array.'
                        if ($PSVersionTable.PSVersion -ge [version]'7.0') {
                            $inputString = ConvertFrom-SecureString $InputObject -AsPlainText
                        }
                        else {
                            $inputString = ConvertFrom-SecureStringAsPlainText $InputObject -Force
                        }
                    }
                    else {
                        $inputString = $InputObject
                    }
                    
                    switch -Regex ($inputString) {
                        '^[A-Fa-f0-9\r\n]+$' {
                            Write-Verbose 'Decoding hex string to byte array.'
                            $inputBytes = ConvertFrom-HexString $inputString -RawBytes
                        }
                        '^(?:[A-Za-z0-9+/\r\n]{4})*(?:[A-Za-z0-9+/\r\n]{2}==|[A-Za-z0-9+/\r\n]{3}=)?$' {
                            Write-Verbose 'Decoding Base64 string to byte array.'
                            $inputBytes = [System.Convert]::FromBase64String($inputString)
                        }
                        '.*\.(cer)$' {
                            Write-Verbose 'Decoding hex string to byte array.'
                            if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                                $inputBytes = Get-Content $inputString.FullName -Raw -AsByteStream
                            }
                            else {
                                $inputBytes = Get-Content $inputString.FullName -Raw -Encoding Byte
                            }
                        }
                        default {
                            $inputBytes = [Text.Encoding]::Default.GetBytes($inputString)
                        }
                    }
                }
                elseif ($InputObject -is [System.IO.FileSystemInfo]) {
                    Write-Verbose 'Decoding file content to byte array.'
                    if ($PSVersionTable.PSVersion -ge [version]'6.0') {
                        $inputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream
                    }
                    else {
                        $inputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte
                    }
                }
                else {
                    # Otherwise, write a terminating error message indicating that input object type is not supported.
                    $errorMessage = 'Cannot convert input of type {0} to X.509 certificate.' -f $InputObject.GetType()
                    Write-Error -Message $errorMessage -Category ([System.Management.Automation.ErrorCategory]::ParserError) -ErrorId 'GetX509CertificateFailureTypeNotSupported' -ErrorAction Stop
                }

                ## Only write output if the input is not a byte stream.
                if ($listBytes.Count -eq 0) {
                    $X509CertificateCollection = Transform $inputBytes
                    if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] }
                    else { Write-Output $X509CertificateCollection }
                }
            }
        }
    }

    end {
        ## Output captured byte stream from piped input.
        if ($listBytes.Count -gt 0) {
            $X509CertificateCollection = Transform $listBytes
            if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] }
            else { Write-Output $X509CertificateCollection }
        }
    }
}

#endregion

#region Get-X509CertificateCrlDistributionPoints.ps1

<#
.SYNOPSIS
    Get X509 certificate extension 2.5.29.31 for CRL Distribution Points.
 
.EXAMPLE
    PS >Get-X509CertificateCrlDistributionPoints $Certificate
 
    Get certificate CRL Distribution Points extension.
 
.INPUTS
    System.Security.Cryptography.X509Certificates.X509Certificate2
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Get-X509CertificateCrlDistributionPoints {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # X.509 Certificate
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2[]] $X509Certificate
    )

    process {
        foreach ($Certificate in $X509Certificate) {

            $ExtCrlDistributionPoints = $Certificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.31' }

            if ($null -eq $ExtCrlDistributionPoints -or $null -eq $ExtCrlDistributionPoints.RawData -or $ExtCrlDistributionPoints.RawData.Length -lt 11) {
                continue
            }

            [int] $prev = -2
            [System.Collections.Generic.List[string]] $items = New-Object 'System.Collections.Generic.List[string]'
            while ($prev -ne -1 -and $ExtCrlDistributionPoints.RawData.Length -gt $prev + 1) {
                [int] $startIndex = if ($prev -eq -2) { 8 } else { $prev + 1 }
                [int] $next = [System.Array]::IndexOf($ExtCrlDistributionPoints.RawData, [byte]0x86, $startIndex)
                if ($next -eq -1) {
                    if ($prev -ge 0) {
                        [string] $item = [System.Text.Encoding]::UTF8.GetString($ExtCrlDistributionPoints.RawData, $prev + 2, $ExtCrlDistributionPoints.RawData.Length - ($prev + 2))
                        $items.Add($item)
                    }
                    break
                }

                if ($prev -ge 0 -and $next -gt $prev) {
                    [string] $item = [System.Text.Encoding]::UTF8.GetString($ExtCrlDistributionPoints.RawData, $prev + 2, $next - ($prev + 2))
                    $items.Add($item)
                }

                $prev = $next
            }

            Write-Output $items.ToArray()
        }
    }
}

#endregion

#region Invoke-CommandAsSystem.ps1

<#
.SYNOPSIS
    Run PowerShell commands under system context.
 
.EXAMPLE
    PS >Invoke-CommandAsSystem { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name }
 
    Run the ScriptBlock under the system context.
 
.INPUTS
    System.Management.Automation.ScriptBlock
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Invoke-CommandAsSystem {
    [CmdletBinding()]
    param (
        # Specifies the ScriptBlock to run under the system context.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)]
        [ScriptBlock] $ScriptBlock,
        # Specifies the arguments to pass to the ScriptBlock.
        [Parameter(Mandatory = $false, Position = 2)]
        [string[]] $ArgumentList
    )

    begin {
        ## Initialize Critical Dependencies
        $CriticalError = $null
        try {
            Import-Module PSScheduledJob, ScheduledTasks -ErrorAction Stop
        }
        catch { Write-Error -ErrorRecord $_ -ErrorVariable CriticalError; return }
    }

    process {
        ## Return Immediately On Critical Error
        if ($CriticalError) { return }

        ## Process
        [guid] $GUID = New-Guid

        try {
            ## Register ScheduleJob
            if ($ArgumentList) {
                $ScheduledJob = Register-ScheduledJob -Name $GUID -ScheduledJobOption (New-ScheduledJobOption -RunElevated) -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
            }
            else {
                $ScheduledJob = Register-ScheduledJob -Name $GUID -ScheduledJobOption (New-ScheduledJobOption -RunElevated) -ScriptBlock $ScriptBlock -ErrorAction Stop
            }
            try {
                ## Register ScheduledTask for ScheduledJob
                $ScheduledTask = Register-ScheduledTask -TaskName $GUID -Action (New-ScheduledTaskAction -Execute $ScheduledJob.PSExecutionPath -Argument $ScheduledJob.PSExecutionArgs) -Principal (New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount -RunLevel Highest) -ErrorAction Stop

                try {
                    ## Execute ScheduledTask Job to Run ScheduledJob Job
                    $ScheduledTask | Start-ScheduledTask -AsJob -ErrorAction Stop | Wait-Job | Remove-Job -Force -Confirm:$False

                    ## Wait for ScheduledTask to finish
                    While (($ScheduledTask | Get-ScheduledTaskInfo).LastTaskResult -eq 267009) { Start-Sleep -Milliseconds 150 }

                    ## Find ScheduledJob and get the result
                    $Job = Get-Job -Name $GUID -ErrorAction SilentlyContinue | Wait-Job
                    $Result = $Job | Receive-Job -Wait -AutoRemoveJob
                }
                finally {
                    ## Unregister ScheduledTask for ScheduledJob
                    $ScheduledTask | Unregister-ScheduledTask -Confirm:$false
                }
            }
            finally {
                ## Unregister ScheduleJob
                $ScheduledJob | Unregister-ScheduledJob -Force -Confirm:$False
            }
        }
        catch { Write-Error -ErrorRecord $_; return }

        return $Result
    }
}

#endregion

#region New-SecureStringKey.ps1

<#
.SYNOPSIS
    Generate random key for securestring encryption.
 
.EXAMPLE
    PS >New-SecureStringKey
 
    Generate random 16 byte (128-bit) key.
 
.EXAMPLE
    PS >$SecureKey = New-SecureStringKey -Length 32
    PS >$SecureString = ConvertTo-SecureString "Super Secret String" -AsPlainText -Force
    PS >$EncryptedSecureString = ConvertFrom-SecureString $SecureString -SecureKey $SecureKey
    PS >$DecryptedSecureString = ConvertTo-SecureString $EncryptedSecureString -SecureKey $SecureKey
    PS >ConvertFrom-SecureStringAsPlainText $DecryptedSecureString
 
    Generate random 32 byte (256-bit) key and use it to encrypt another string.
 
.INPUTS
    System.Int32
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function New-SecureStringKey {
    [CmdletBinding()]
    param (
        # Key length
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [ValidateSet(16, 24, 32)]
        [int] $Length = 16
    )

    [byte[]] $Key = Get-Random -InputObject ((0..255)*$Length) -Count $Length
    [securestring] $SecureKey = ConvertTo-SecureString -String ([System.Text.Encoding]::ASCII.GetString($Key)) -AsPlainText -Force

    return $SecureKey
}

#endregion

#region Remove-Diacritics.ps1

<#
.SYNOPSIS
    Decompose characters to their base character equivilents and remove diacritics.
 
.EXAMPLE
    PS >Remove-Diacritics 'àáâãäåÀÁÂÃÄÅfi⁵ẛ'
 
    Decompose characters to their base character equivilents and remove diacritics.
 
.EXAMPLE
    PS >Remove-Diacritics 'àáâãäåÀÁÂÃÄÅfi⁵ẛ' -CompatibilityDecomposition
 
    Decompose composite characters to their base character equivilents and remove diacritics.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Remove-Diacritics {
    [CmdletBinding()]
    param (
        # String value to transform.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [string[]] $InputStrings,
        # Use compatibility decomposition instead of canonical decomposition which further decomposes composite characters and many formatting distinctions are removed.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [switch] $CompatibilityDecomposition
    )

    process {
        [System.Text.NormalizationForm] $NormalizationForm = [System.Text.NormalizationForm]::FormD
        if ($CompatibilityDecomposition) { $NormalizationForm = [System.Text.NormalizationForm]::FormKD }
        foreach ($InputString in $InputStrings) {
            $NormalizedString = $InputString.Normalize($NormalizationForm)
            $OutputString = New-Object System.Text.StringBuilder

            foreach ($char in $NormalizedString.ToCharArray()) {
                if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($char) -ne [Globalization.UnicodeCategory]::NonSpacingMark) {
                    [void] $OutputString.Append($char)
                }
            }

            Write-Output $OutputString.ToString()
        }
    }
}

#endregion

#region Remove-InvalidFileNameCharacters.ps1

<#
.SYNOPSIS
    Remove invalid filename characters from string.
 
.EXAMPLE
    PS >Remove-InvalidFileNameCharacters 'à/1\b?2|ć*3<đ>4 ē'
 
    Remove invalid filename characters from string.
 
.EXAMPLE
    PS >Remove-InvalidFileNameCharacters 'à/1\b?2|ć*3<đ>4 ē' -RemoveDiacritics
 
    Remove invalid filename characters and diacritics from string.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Remove-InvalidFileNameCharacters {
    [CmdletBinding()]
    param (
        # String value to transform.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [string[]] $InputStrings,
        # Character used as replacement for invalid characters. Use '' to simply remove.
        [Parameter(Mandatory = $false)]
        [string] $ReplacementCharacter = '-',
        # Replace characters with diacritics to their non-diacritic equivilent.
        [Parameter(Mandatory = $false)]
        [switch] $RemoveDiacritics
    )

    process {
        foreach ($InputString in $InputStrings) {
            [string] $OutputString = $InputString
            if ($RemoveDiacritics) { $OutputString = Remove-Diacritics $OutputString -CompatibilityDecomposition }
            $OutputString = [regex]::Replace($OutputString, ('[{0}]' -f [regex]::Escape([System.IO.Path]::GetInvalidFileNameChars() -join '')), $ReplacementCharacter)
            Write-Output $OutputString
        }
    }
}

#endregion

#region Remove-SensitiveData.ps1

<#
.SYNOPSIS
    Remove sensitive data from object or string.
 
.EXAMPLE
    PS >$MyString = 'My password is: "SuperSecretString"'
    PS >Remove-SensitiveData ([ref]$MyString) -FilterValues "Super","String"
 
    This removes the word "Super" and "String" from the input string with no output.
 
.EXAMPLE
    PS >Remove-SensitiveData 'My password is: "SuperSecretString"' -FilterValues "Super","String" -PassThru
 
    This removes the word "Super" and "String" from the input string and return the result.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Remove-SensitiveData {
    [CmdletBinding()]
    [OutputType([object])]
    param (
        # Object from which to remove sensitive data.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObjects,
        # Sensitive string values to remove from input object.
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string[]] $FilterValues,
        # Replacement value for senstive data.
        [Parameter(Mandatory = $false)]
        [string] $ReplacementValue = '********',
        # Copy the input object rather than remove data directly from input.
        [Parameter(Mandatory = $false)]
        [switch] $Clone,
        # Output object with sensitive data removed.
        [Parameter(Mandatory = $false)]
        [switch] $PassThru
    )

    process {
        if ($InputObjects.GetType().FullName.StartsWith('System.Management.Automation.PSReference')) {
            if ($Clone) { $OutputObjects = $InputObjects.Value.Clone() }
            else { $OutputObjects = $InputObjects }
        }
        else {
            if ($Clone) { $OutputObjects = [ref]$InputObjects.Clone() }
            else {
                if ($InputObjects -is [System.ValueType] -or $InputObjects -is [string]) { Write-Warning ('The input of type [{0}] was not passed by reference. Senstive data will not be removed from the original input.' -f $InputObjects.GetType()) }
                $OutputObjects = [ref]$InputObjects
            }
        }

        if ($OutputObjects.Value -is [string]) {
            foreach ($FilterValue in $FilterValues) {
                if ($OutputObjects.Value -and $FilterValue) { $OutputObjects.Value = $OutputObjects.Value.Replace($FilterValue, $ReplacementValue) }
            }
        }
        elseif ($OutputObjects.Value -is [System.Collections.IList]) {
            for ($ii = 0; $ii -lt $OutputObjects.Value.Count; $ii++) {
                if ($null -ne $OutputObjects.Value[$ii] -and $OutputObjects.Value[$ii] -isnot [ValueType]) {
                    $OutputObjects.Value[$ii] = Remove-SensitiveData ([ref]$OutputObjects.Value[$ii]) -FilterValues $FilterValues -PassThru
                }
            }
        }
        elseif ($OutputObjects.Value -is [System.Collections.IDictionary]) {
            [array] $KeyNames = $OutputObjects.Value.Keys
            for ($ii = 0; $ii -lt $KeyNames.Count; $ii++) {
                if ($null -ne $OutputObjects.Value[$KeyNames[$ii]] -and $OutputObjects.Value[$KeyNames[$ii]] -isnot [ValueType]) {
                    $OutputObjects.Value[$KeyNames[$ii]] = Remove-SensitiveData ([ref]$OutputObjects.Value[$KeyNames[$ii]]) -FilterValues $FilterValues -PassThru
                }
            }
        }
        elseif ($OutputObjects.Value -is [object] -and $OutputObjects.Value -isnot [ValueType]) {
            [array] $PropertyNames = $OutputObjects.Value | Get-Member -MemberType Property, NoteProperty
            for ($ii = 0; $ii -lt $PropertyNames.Count; $ii++) {
                $PropertyName = $PropertyNames[$ii].Name
                if ($null -ne $OutputObjects.Value.$PropertyName -and $OutputObjects.Value.$PropertyName -isnot [ValueType]) {
                    $OutputObjects.Value.$PropertyName = Remove-SensitiveData ([ref]$OutputObjects.Value.$PropertyName) -FilterValues $FilterValues -PassThru
                }
            }
        }
        else {
            ## Non-Terminating Error
            $Exception = New-Object ArgumentException -ArgumentList ('Cannot remove senstive data from input of type {0}.' -f $OutputObjects.Value.GetType())
            Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'RemoveSensitiveDataFailureTypeNotSupported' -TargetObject $OutputObjects.Value
            continue
        }

        if ($PassThru -or $Clone) {
            ## Return the object with sensitive data removed.
            if ($OutputObjects.Value -is [System.Collections.IList]) {
                Write-Output $OutputObjects.Value -NoEnumerate
            }
            else {
                Write-Output $OutputObjects.Value
            }
        }
    }
}

#endregion

#region Select-PsBoundParameters.ps1

<#
.SYNOPSIS
    Filters a hashtable or PSBoundParameters containing PowerShell command parameters to only those valid for specified command.
 
.EXAMPLE
    PS >Select-PsBoundParameters @{Name='Valid'; Verbose=$true; NotAParameter='Remove'} -CommandName Get-Process -ExcludeParameters 'Verbose'
 
    Filters the parameter hashtable to only include valid parameters for the Get-Process command and exclude the Verbose parameter.
 
.EXAMPLE
    PS >Select-PsBoundParameters @{Name='Valid'; Verbose=$true; NotAParameter='Remove'} -CommandName Get-Process -CommandParameterSets NameWithUserName
 
    Filters the parameter hashtable to only include valid parameters for the Get-Process command in the "NameWithUserName" ParameterSet.
 
.INPUTS
    System.String
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Select-PsBoundParameters {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        # Specifies the parameter key pairs to be filtered.
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [hashtable] $NamedParameters,

        # Specifies the parameter names to remove from the output.
        [Parameter(Mandatory = $false)]
        [ArgumentCompleter( {
                param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
                if ($fakeBoundParameters.ContainsKey('NamedParameters')) {
                    [string[]]$fakeBoundParameters.NamedParameters.Keys | Where-Object { $_ -Like "$wordToComplete*" }
                }
            })]
        [string[]] $ExcludeParameters,

        # Specifies the name of a PowerShell command to further filter valid parameters.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter( {
                param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
                [array] $CommandInfo = Get-Command "$wordToComplete*"
                if ($CommandInfo) {
                    $CommandInfo.Name #| ForEach-Object {$_}
                }
            })]
        [Alias('Name')]
        [string] $CommandName,

        # Specifies parameter sets of the PowerShell command to further filter valid parameters.
        [Parameter(Mandatory = $false)]
        [ArgumentCompleter( {
                param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
                if ($fakeBoundParameters.ContainsKey('CommandName')) {
                    [array] $CommandInfo = Get-Command $fakeBoundParameters.CommandName
                    if ($CommandInfo) {
                        $CommandInfo[0].ParameterSets.Name | Where-Object { $_ -Like "$wordToComplete*" }
                    }
                }
            })]
        [string[]] $CommandParameterSets
    )

    process {
        [hashtable] $SelectedParameters = $NamedParameters.Clone()

        [string[]] $CommandParameters = $null
        if ($CommandName) {
            $CommandInfo = Get-Command $CommandName
            if ($CommandParameterSets) {
                [System.Collections.Generic.List[string]] $listCommandParameters = New-Object System.Collections.Generic.List[string]
                foreach ($CommandParameterSet in $CommandParameterSets) {
                    $listCommandParameters.AddRange([string[]]($CommandInfo.ParameterSets | Where-Object Name -eq $CommandParameterSet | Select-Object -ExpandProperty Parameters | Select-Object -ExpandProperty Name))
                }
                $CommandParameters = $listCommandParameters | Select-Object -Unique
            }
            else {
                $CommandParameters = $CommandInfo.Parameters.Keys
            }
        }

        [string[]] $ParameterKeys = $SelectedParameters.Keys
        foreach ($ParameterKey in $ParameterKeys) {
            if ($ExcludeParameters -contains $ParameterKey -or ($CommandParameters -and $CommandParameters -notcontains $ParameterKey)) {
                $SelectedParameters.Remove($ParameterKey)
            }
        }

        return $SelectedParameters
    }
}

#endregion

#region Skip-NullValue.ps1

<#
.SYNOPSIS
    Output the first non-null value from list of input values.
 
.EXAMPLE
    PS >Skip-NullValue $null, 'winner', 'loser'
 
    Return the first non-null value which is 'winner'.
 
.EXAMPLE
    PS >Get-Module 'NonExistentModuleName' | Skip-NullValue -DefaultValue @()
 
    Return the first non-null value which is 'winner'.
 
.EXAMPLE
    PS >Skip-NullValue $null, '', ([guid]::Empty), @(), 0, ([int]-1), 'winner', 'loser' -SkipEmpty -SkipZero -SkipNegative
 
    Return the first non-null, non-empty, non-zero, and non-negative value which is 'winner'.
 
.INPUTS
    System.Object
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Skip-NullValue {
    [CmdletBinding()]
    [Alias('Coalesce')]
    param (
        # Values to coalesce
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [AllowNull()]
        [object] $InputObject,
        # Skip over empty values
        [Parameter(Mandatory = $false)]
        [switch] $SkipEmpty,
        # Skip over zero values
        [Parameter(Mandatory = $false)]
        [switch] $SkipZero,
        # Skip over negative values
        [Parameter(Mandatory = $false)]
        [switch] $SkipNegative,
        # Default value when no other values
        [Parameter(Mandatory = $false)]
        [object] $DefaultValue = $null,
        # Enumerate pipeline input rather than treat it as a single input
        [Parameter(Mandatory = $false)]
        [switch] $EnumeratePipelineInput
    )

    begin {
        function HasValue ($Object) {
            if ($null -ne $Object) {
                Write-Debug "ObjectType: $($Object.psobject.TypeNames[0]) | Object: $Object"

                ## Additional Tests (these could leak errors into $Error variable)
                [bool]$TestEmptyArray = try { $SkipEmpty -and $Object -is [array] -and $Object.Count -eq 0 } catch { $false }
                [bool]$TestEmpty = try { $SkipEmpty -and $Object -eq ($Object.GetType())::Empty } catch { $false }
                [bool]$TestZero = try { $SkipZero -and $Object -eq 0 } catch { $false }
                [bool]$TestNegative = try { $SkipNegative -and $Object -lt 0 } catch { $false }

                Write-Debug "TestEmptyArray: $TestEmptyArray | TestEmpty: $TestEmpty | TestZero: $TestZero | TestNegative: $TestNegative"

                if (!($TestEmptyArray -or $TestEmpty -or $TestZero -or $TestNegative)) {
                    return $true
                }
            }
            return $false
        }

        ## Initialize
        $InputObjects = @()
        $OutputObject = $null
        [bool]$IsPipelineInput = $false
        if ($null -eq $InputObject) { $IsPipelineInput = $true }
    }

    process {
        ## Save pipeline input to process at end if not enumerating pipeline input.
        $InputObjects += $InputObject
        if ($IsPipelineInput -and !$EnumeratePipelineInput) { return }

        ## Skip enumerated input if previous input already satisfied condition.
        if ($null -ne $OutputObject) { return }
        
        ## Loop through input objects and return the first value.
        foreach ($Object in $InputObject) {
            if (HasValue $Object) {
                $OutputObject = $Object
                return
            }
        }
    }

    end {
        ## Remove array if count is 1 or less.
        if ($InputObjects.Count -eq 0) { $InputObjects = $null }
        elseif ($InputObjects.Count -eq 1) { $InputObjects = $InputObjects[0] }

        ## If pipeline input was detected, treat the input array as one value like ?? operator.
        if ($IsPipelineInput -and !$EnumeratePipelineInput) {
            if (HasValue $InputObjects) {
                $OutputObject = $InputObjects
            }
        }

        ## If no acceptable values were found, use default value.
        if ($null -eq $OutputObject) { $OutputObject = $DefaultValue }

        Write-Output $OutputObject -NoEnumerate
    }
}

#endregion

#region Test-IpAddressInSubnet.ps1

<#
.SYNOPSIS
    Determine if an IP address exists in the specified subnet.
 
.EXAMPLE
    PS >Test-IpAddressInSubnet 192.168.1.10 -Subnet '192.168.1.1/32','192.168.1.0/24'
 
    Determine if the IPv4 address exists in the specified subnet.
 
.EXAMPLE
    PS >Test-IpAddressInSubnet 2001:db8:1234::1 -Subnet '2001:db8:a::123/64','2001:db8:1234::/48'
 
    Determine if the IPv6 address exists in the specified subnet.
 
.INPUTS
    System.Net.IPAddress
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Test-IpAddressInSubnet {
    [CmdletBinding()]
    [OutputType([bool], [string[]])]
    param (
        # IP Address to test against provided subnets.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)]
        [ipaddress[]] $IpAddresses,
        # List of subnets in CIDR notation. For example, "192.168.1.0/24" or "2001:db8:1234::/48".
        [Parameter(Mandatory = $true)]
        [string[]] $Subnets,
        # Return list of matching subnets rather than a boolean result.
        [Parameter(Mandatory = $false)]
        [switch] $ReturnMatchingSubnets
    )

    begin {
        function ConvertBitArrayToByteArray([System.Collections.BitArray] $BitArray)
        {
            [byte[]] $ByteArray = New-Object byte[] ([System.Math]::Ceiling($BitArray.Length / 8))
            $BitArray.CopyTo($ByteArray, 0)
            return $ByteArray
        }

        function ConvertBitArrayToBigInt([System.Collections.BitArray] $BitArray) {
            return [bigint][byte[]](ConvertBitArrayToByteArray $BitArray)
        }
    }

    process {
        foreach ($IpAddress in $IpAddresses) {
            if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
                [int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0)
            }
            else {
                [System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes()
            }

            [System.Collections.Generic.List[string]] $listSubnets = New-Object System.Collections.Generic.List[string]
            [bool] $Result = $false
            foreach ($Subnet in $Subnets) {
                [string[]] $SubnetComponents = $Subnet.Split('/')
                [ipaddress] $SubnetAddress = $SubnetComponents[0]
                [int] $SubnetMaskLength = $SubnetComponents[1]

                if ($IpAddress.AddressFamily -eq $SubnetAddress.AddressFamily) {
                    if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
                        ## Supports IPv4 (32 bit) only but more performant than BitArray?
                        #[int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0)
                        [int32] $bitSubnetAddress = [BitConverter]::ToInt32($SubnetAddress.GetAddressBytes(), 0)
                        [int32] $bitSubnetMaskHostOrder = 0
                        if ($SubnetMaskLength -gt 0) {
                            $bitSubnetMaskHostOrder = -1 -shl (32 - $SubnetMaskLength)
                        }
                        [int32] $bitSubnetMask = [ipaddress]::HostToNetworkOrder($bitSubnetMaskHostOrder)

                        ## Check IP
                        if (($bitIpAddress -band $bitSubnetMask) -eq ($bitSubnetAddress -band $bitSubnetMask)) {
                            if ($ReturnMatchingSubnets) {
                                $listSubnets.Add($Subnet)
                            }
                            else {
                                $Result = $true
                                continue
                            }
                        }
                    }
                    else {
                        ## BitArray supports IPv4 (32 bits) and IPv6 (128 bits). Would Int128 type in .NET 7 improve performance?
                        #[System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes()
                        [System.Collections.BitArray] $bitSubnetAddress = $SubnetAddress.GetAddressBytes()
                        [System.Collections.BitArray] $bitSubnetMask = New-Object System.Collections.BitArray -ArgumentList ($bitSubnetAddress.Length - $SubnetMaskLength), $true
                        $bitSubnetMask.Length = $bitSubnetAddress.Length
                        [void]$bitSubnetMask.Not()
                        [byte[]] $ByteArray = ConvertBitArrayToByteArray $bitSubnetMask
                        [array]::Reverse($ByteArray)  # Convert to Network byte order
                        [System.Collections.BitArray] $bitSubnetMask = $ByteArray
                
                        ## Check IP
                        if ((ConvertBitArrayToBigInt $bitIpAddress.And($bitSubnetMask)) -eq (ConvertBitArrayToBigInt $bitSubnetAddress.And($bitSubnetMask))) {
                            if ($ReturnMatchingSubnets) {
                                $listSubnets.Add($Subnet)
                            }
                            else {
                                $Result = $true
                                continue
                            }
                        }
                    }
                }
            }

            ## Return list of matches or boolean result
            if ($ReturnMatchingSubnets) {
                if ($listSubnets.Count -gt 1) { Write-Output $listSubnets.ToArray() -NoEnumerate }
                elseif ($listSubnets.Count -eq 1) { Write-Output $listSubnets.ToArray() }
                else {
                    $Exception = New-Object ArgumentException -ArgumentList ('The IP address {0} does not belong to any of the provided subnets.' -f $IpAddress)
                    Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ObjectNotFound) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'TestIpAddressInSubnetNoMatch' -TargetObject $IpAddress
                }
            }
            else {
                Write-Output $Result
            }
        }
    }
}

#endregion

#region Test-PsElevation.ps1

<#
.SYNOPSIS
    Test if current PowerShell process is elevated to local administrator privileges.
 
.EXAMPLE
    PS >Test-PsElevation
 
    Test is current PowerShell process is elevated.
 
.INPUTS
    None
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Test-PsElevation {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    try {
        $WindowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $WindowsPrincipal = New-Object 'System.Security.Principal.WindowsPrincipal' $WindowsIdentity
        $LocalAdministrator = [System.Security.Principal.WindowsBuiltInRole]::Administrator
        return $WindowsPrincipal.IsInRole($LocalAdministrator)
    }
    catch {
        if ($_.Exception.InnerException) {
            Write-Error -Exception $_.Exception.InnerException -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -ErrorId $_.FullyQualifiedErrorId -TargetObject $_.TargetObject
        }
        else { Write-Error -ErrorRecord $_ }
    }
}

#endregion

#region Use-Progress.ps1

<#
.SYNOPSIS
    Display progress bar for processing array of objects.
 
.EXAMPLE
    PS >Use-Progress -InputObjects @(1..10) -Activity "Processing Parent Objects" -ScriptBlock {
        $Parent = $args[0]
        Use-Progress -InputObjects @(1..200) -Activity "Processing Child Objects" -ScriptBlock {
            $Child = $args[0]
            Write-Host "Child $Child of Parent $Parent."
            Start-Sleep -Milliseconds 50
        }
    }
 
    Display progress bar for processing array of objects.
 
.INPUTS
    System.Object[]
 
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Use-Progress {
    [CmdletBinding()]
    param (
        # Array of objects to loop through.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object[]] $InputObjects,
        # Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported.
        [Parameter(Mandatory = $true)]
        [string] $Activity,
        # Script block to execute for each object in array.
        [Parameter(Mandatory = $true)]
        [scriptblock] $ScriptBlock,
        # Property name to use for current operation
        [Parameter(Mandatory = $false)]
        [string] $Property,
        # Minimum timespan between each progress update.
        [Parameter(Mandatory = $false)]
        [timespan] $MinimumUpdateFrequency = (New-Timespan -Seconds 1)
    )

    begin {
        [System.Collections.Generic.List[object]] $listObjects = New-Object System.Collections.Generic.List[object]
    }

    process {
        $listObjects.AddRange($InputObjects)
    }

    end {
        if ($listObjects.Count -gt 0) { [object[]] $InputObjects = $listObjects.ToArray() }
        [int] $Id = 0
        if (!(Get-Variable stackProgressId -ErrorAction SilentlyContinue)) { New-Variable -Name stackProgressId -Scope Script -Value (New-Object System.Collections.Generic.Stack[int]) }
        while ($stackProgressId.Contains($Id)) { $Id += 1 }
        [hashtable] $paramWriteProgress = @{
            Id       = $Id
            Activity = $Activity
        }
        if ($stackProgressId.Count -gt 0) { $paramWriteProgress['ParentId'] = $stackProgressId.Peek() }
        [int] $SecondsRemaining = -1
        [int] $total = $InputObjects.Count

        try {
            $stackProgressId.Push($Id)
            [System.Diagnostics.Stopwatch] $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
            for ($iObject = 0; $iObject -lt $total; $iObject++) {
                if ($iObject -eq 0 -or ($stopwatch.Elapsed - $TimeElapsed) -gt $MinimumUpdateFrequency) {
                    [timespan] $TimeElapsed = $stopwatch.Elapsed
                    $PercentComplete = $iObject / $total
                    if ($PercentComplete -gt 0) { $SecondsRemaining = $TimeElapsed.TotalSeconds / $PercentComplete - $TimeElapsed.TotalSeconds }
                    if ($Property) { $CurrentOperation = $InputObjects[$iObject].$Property }
                    else { $CurrentOperation = $InputObjects[$iObject] }
                    Write-Progress -CurrentOperation $CurrentOperation -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f $PercentComplete, $iObject, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete ($PercentComplete * 100) -SecondsRemaining $SecondsRemaining @paramWriteProgress
                }
                Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $InputObjects[$iObject]
            }
            $stopwatch.Stop()
            Write-Progress -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f 1, $total, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete 100 -SecondsRemaining 0 @paramWriteProgress
        }
        finally {
            [void] $stackProgressId.Pop()
            #Start-Sleep -Seconds 1
            Write-Progress -Id $Id -Activity $Activity -Completed
        }
    }
}

#endregion

#region Write-HostPrompt.ps1

<#
.SYNOPSIS
    Displays a PowerShell prompt for multiple fields or multiple choices.
 
.EXAMPLE
    PS >Write-HostPrompt "Prompt Caption" -Fields "Field 1", "Field 2"
 
    Display simple prompt for 2 fields.
 
.EXAMPLE
    PS >$IntegerField = New-Object System.Management.Automation.Host.FieldDescription -ArgumentList "Integer Field" -Property @{ HelpMessage = "Help Message for Integer Field" }
    PS >$IntegerField.SetParameterType([int[]])
    PS >$DateTimeField = New-Object System.Management.Automation.Host.FieldDescription -ArgumentList "DateTime Field" -Property @{ HelpMessage = "Help Message for DateTime Field" }
    PS >$DateTimeField.SetParameterType([datetime])
    PS >Write-HostPrompt "Prompt Caption" "Prompt Message" -Fields $IntegerField, $DateTimeField
 
    Display prompt for 2 type-specific fields, with int field being an array.
 
.EXAMPLE
    PS >Write-HostPrompt "Prompt Caption" -Choices "Choice &1", "Choice &2"
 
    Display simple prompt with 2 choices.
 
.EXAMPLE
    PS >Write-HostPrompt "Prompt Caption" "Prompt Message" -DefaultChoice 2 -Choices @(
        New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&1`bChoice one" -Property @{ HelpMessage = "Help Message for Choice 1" }
        New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&2`bChoice two" -Property @{ HelpMessage = "Help Message for Choice 2" }
    )
 
    Display prompt with 2 choices and help messages that defaults to the second choice.
 
.EXAMPLE
    PS >Write-HostPrompt "Prompt Caption" "Choose a number" -Choices "Menu Item A", "Menu Item B", "Menu Item C" -HelpMessages "Menu Item A Needs Help", "Menu Item B Needs More Help",, "Menu Item C Needs Crazy Help" -NumberedHotKeys
 
    Display prompt with 3 choices and help message that are automatically numbered.
 
.INPUTS
    System.Management.Automation.Host.FieldDescription
    System.Management.Automation.Host.ChoiceDescription
 
.OUTPUTS
    System.Collections.Generic.Dictionary[System.String,System.Management.Automation.PSObject]
    System.Int32
     
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Write-HostPrompt {
    [CmdletBinding()]
    param (
        # Caption to preceed or title the prompt.
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Caption,
        # A message that describes the prompt.
        [Parameter(Mandatory = $false, Position = 2)]
        [string] $Message,
        # The fields in the prompt.
        [Parameter(Mandatory = $true, ParameterSetName = 'Fields', Position = 3, ValueFromPipeline = $true)]
        [System.Management.Automation.Host.FieldDescription[]] $Fields,
        # The choices the shown in the prompt.
        [Parameter(Mandatory = $true, ParameterSetName = 'Choices', Position = 3, ValueFromPipeline = $true)]
        [System.Management.Automation.Host.ChoiceDescription[]] $Choices,
        # Specifies a help message for each field or choice.
        [Parameter(Mandatory = $false, Position = 4)]
        [string[]] $HelpMessages = @(),
        # The index of the label in the choices to make default.
        [Parameter(Mandatory = $false, ParameterSetName = 'Choices', Position = 5)]
        [int] $DefaultChoice,
        # Use numbered hot keys (aka "keyboard accelerator") for each choice.
        [Parameter(Mandatory = $false, ParameterSetName = 'Choices', Position = 6)]
        [switch] $NumberedHotKeys
    )

    begin {
        ## Create list to capture multiple fields or multiple choices.
        [System.Collections.Generic.List[System.Management.Automation.Host.FieldDescription]] $listFields = New-Object System.Collections.Generic.List[System.Management.Automation.Host.FieldDescription]
        [System.Collections.Generic.List[System.Management.Automation.Host.ChoiceDescription]] $listChoices = New-Object System.Collections.Generic.List[System.Management.Automation.Host.ChoiceDescription]
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'Fields' {
                for ($iField = 0; $iField -lt $Fields.Count; $iField++) {
                    if ($iField -lt $HelpMessages.Count -and $HelpMessages[$iField]) { $Fields[$iField].HelpMessage = $HelpMessages[$iField] }
                    $listFields.Add($Fields[$iField])
                }
            }
            'Choices' {
                for ($iChoice = 0; $iChoice -lt $Choices.Count; $iChoice++) {
                    if ($NumberedHotKeys) { $Choices[$iChoice] = New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($iChoice+1)`b$($Choices[$iChoice].Label)" -Property @{ HelpMessage = $Choices[$iChoice].HelpMessage } }
                    #elseif (!$Choices[$iChoice].Label.Contains('&')) { $Choices[$iChoice] = New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($Choices[$iChoice].Label)" -Property @{ HelpMessage = $Choices[$iChoice].HelpMessage } }
                    if ($iChoice -lt $HelpMessages.Count -and $HelpMessages[$iChoice]) { $Choices[$iChoice].HelpMessage = $HelpMessages[$iChoice] }
                    $listChoices.Add($Choices[$iChoice])
                }
            }
        }
    }

    end {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                'Fields' { return $Host.UI.Prompt($Caption, $Message, $listFields.ToArray()) }
                'Choices' { return $Host.UI.PromptForChoice($Caption, $Message, $listChoices.ToArray(), $DefaultChoice - 1) + 1 }
            }
        }
        catch [System.Management.Automation.PSInvalidOperationException] {
            ## Write Non-Terminating Error When In Non-Interactive Mode.
            Write-Error -ErrorRecord $_ -CategoryActivity $MyInvocation.MyCommand
        }
    }
}

#endregion

#endregion

## Set Strict Mode for Module. https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode
Set-StrictMode -Version 3.0

## PowerShell Desktop 5.1 does not dot-source ScriptsToProcess when a specific version is specified on import. This is a bug.
# if ($PSEdition -eq 'Desktop') {
# $ModuleManifest = Import-PowershellDataFile (Join-Path $PSScriptRoot $MyInvocation.MyCommand.Name.Replace('.psm1','.psd1'))
# if ($ModuleManifest.ContainsKey('ScriptsToProcess')) {
# foreach ($Path in $ModuleManifest.ScriptsToProcess) {
# . (Join-Path $PSScriptRoot $Path)
# }
# }
# }

Export-ModuleMember -Function @('Compress-Data','ConvertFrom-Base64String','ConvertFrom-ClixmlString','ConvertFrom-HexString','ConvertFrom-HtmlString','ConvertFrom-QueryString','ConvertFrom-SecureStringAsPlainText','ConvertFrom-UrlString','ConvertTo-Base64String','ConvertTo-ClixmlString','ConvertTo-Dictionary','ConvertTo-HexString','ConvertTo-HtmlString','ConvertTo-MarkdownTable','ConvertTo-PsParameterString','ConvertTo-PsString','ConvertTo-QueryString','ConvertTo-UrlString','Expand-Data','Format-DataSize','Format-NumberWithMetricUnit','Format-PropertyValue','Get-ContentEncoding','Get-PropertyValue','Get-RelativePath','Get-StrictModeVersion','Get-X509Certificate','Get-X509CertificateCrlDistributionPoints','Invoke-CommandAsSystem','New-SecureStringKey','Remove-Diacritics','Remove-InvalidFileNameCharacters','Remove-SensitiveData','Select-PsBoundParameters','Skip-NullValue','Test-IpAddressInSubnet','Test-PsElevation','Use-Progress','Write-HostPrompt') -Cmdlet @() -Variable @() -Alias @('Deflate-Data','Decompress-Data','Inflate-Data','Format-FileSize')


# SIG # Begin signature block
# MIIjbAYJKoZIhvcNAQcCoIIjXTCCI1kCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA8LgGqFXfoBD9w
# gBo6zLuRvJxiwTUUcZA0c7z0G+qfpaCCHWUwggUmMIIEDqADAgECAhAKbwamSf02
# TrzqY8wkoMRzMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcN
# MjAwMzMxMDAwMDAwWhcNMjMwNDA1MTIwMDAwWjBjMQswCQYDVQQGEwJVUzENMAsG
# A1UECBMET2hpbzETMBEGA1UEBxMKQ2luY2lubmF0aTEXMBUGA1UEChMOSmFzb24g
# VGhvbXBzb24xFzAVBgNVBAMTDkphc29uIFRob21wc29uMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEAxWfKBk7TC+lDc2MakRESqnSv8U3kLRfQafofGuE9
# cDIZloGUSNXR47pvPw0FUXDIexDQEXFPsKsa8ILC96Sbtuohlogl72QVgC85UEMr
# 5LTjZ0ZpPxxRLFTpAiSBcvYhkpm7xHwfT7bqt6Ealp2P6idurMWyFpLwLXz/WgW/
# btb/cV47ACRdsTwxum5z2e1H/o9RXhuLDcBhQhNWmzQ+Z9MHV/ToOattZreisdUM
# 7XIQv8TWGh7SOlc8AfO+02Usy1mDkt5GsZ2R9qyrxX3heJw1ZTxcXLoPlwWUiDRE
# 9xLMwlElvvyd+lAieukMBqC+IMJRVHlnAuy8OTT3qHyQJQIDAQABo4IBxTCCAcEw
# HwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFL7nzjkk
# +8NZ6eNdqEujhdQJxOcyMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEF
# BQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20v
# c2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNl
# cnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgB
# hv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
# UzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDov
# L29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5j
# cnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEARH2swe77D6omtCaH
# pq3oasA9i4eLbO5TTid1FHNNKYdQq/NLUO8RjEunpw7//eAcSoFXVLRhXnxGfmJ0
# yKLt+YA1J87U6DjHvv8KaaenAHxqhIKltHGpwgET6lSbuvskFPjE0QpPcWSBylXK
# YThW4ixwGCd6QSaZpV8OiHVebhxD6G+3Jnz7f5s1D857TTxFKTnOaJaJL754Z4HU
# Pm/rIuzZscAeV0ooKnwyDfbZWpEHYL1sWVBLFL3sUH+zgniMbGNJKXoyZxgvOTD4
# Kilzn/1zVATMF772tkxoA/Bvp73vu2QW0U4J+J6QRICOS7Y0+qOPzcS0s46WWu/e
# vzWhZzCCBTAwggQYoAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQEL
# BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ
# RCBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENv
# ZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjT
# sxx/DhGvZ3cH0wsxSRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3
# Gde2qvCchqXYJawOeSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEo
# JrskacLCUvIUZ4qJRdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCi
# q85/6XzLkqHlOzEcz+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xn
# TrXE94zRICUj6whkPlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAP
# ZHM8np+mM6n9Gd8lk9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAw
# DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEB
# BG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsG
# AQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1
# cmVkSURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5k
# aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu
# Y3JsME8GA1UdIARIMEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRw
# czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRa
# xLl7KgqjpepxA8Bg+S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd
# 823IDzANBgkqhkiG9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE
# 7zBh134LYP3DPQ/Er4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4T
# S63XX0R58zYUBor3nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwu
# kaPAJRHinBRHoXpoaK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2
# iiQC/i9yfhzXSUWW6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/G
# kxAG/AeB+ova+YJJ92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBY0wggR1
# oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UE
# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj
# ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X
# DTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTAT
# BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEh
# MB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLh
# Kac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+
# vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMp
# Lc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+n
# MNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1Dek
# LgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmk
# wuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0
# yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP
# 9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHh
# D5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnf
# fEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId
# 5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LS
# cV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgP
# MA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0
# dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2Vy
# dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNV
# HR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRB
# c3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0B
# AQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlU
# Iu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqa
# i7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eH
# qNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01
# YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ
# 8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4wggSWoAMCAQICEAc2N7ckVHzY
# R6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UE
# AxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3
# MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2
# IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjz
# aPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3E
# F3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYnc
# fGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8O
# pWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROp
# VymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4i
# FNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmif
# tkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0
# UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9Ne
# S3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCj
# WAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTAS
# BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57I
# bzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMC
# AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0
# MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCG
# SAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAY
# LhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQx
# Z822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf
# 7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDV
# inF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7
# +6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJ
# D5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvk
# OHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJG
# nXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimG
# sJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38A
# C+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d
# 2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWlyS5T6PCpKPSkHgD1aMA0GCSqG
# SIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j
# LjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBU
# aW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAwWhcNMzMxMTIxMjM1OTU5WjBG
# MQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxJDAiBgNVBAMTG0RpZ2lD
# ZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0MxomrNAcVR4eNm28klUMYfSdCXc9
# FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK6aYo25BjXL2JU+A6LYyHQq4m
# pOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7gL307scpTjUCDHufLckkoHkyA
# HoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo44DLannR0hCRRinrPibytIzN
# TLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5PgxeZowaCiS+nKrSnLb3T254
# xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h3cKtpX74LRsf7CtGGKMZ9jn3
# 9cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn88JSxOYWe1p+pSVz28BqmSEt
# Y+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g9ArmFG1keLuY/ZTDcyHzL8Iu
# INeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQprdhZPrZIGwYUWC6poEPCSVT
# 8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcTB5rBeO3GiMiwbjJ5xwtZg43G
# 7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVzHIR+187i1Dp3AgMBAAGjggGL
# MIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAK
# BggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwHwYD
# VR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFGKK3tBh/I8x
# FO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBp
# bmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3Rh
# bXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAFWqKhrzRvN4Vzcw/HXjT9aF
# I/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJRjkA/GnUypsp+6M/wMkAmxMd
# sJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1nggwCfrkLdcJiXn5CeaIzn0b
# uGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Qp+sAul9Kjxo6UrTqvwlJFTU2
# WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4GYhEFOUKWaJr5yI+RCHSPxzA
# m+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC6Vp0dQ094XmIvxwBl8kZI4DX
# NlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNfarXH4PMFw1nfJ2Ir3kHJU7n/
# NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA4CPe+AOk9kVH5c64A0JH6EE2
# cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92ByaUcQvmvZfpyeXupYuhVfAYOd4
# Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqYyJ+/jbsYXEP10Cro4mLueATb
# vdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl9uab3H4szP8XTE0AotjWAQ64
# i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTAT
# BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx
# MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD
# QQIQCm8Gpkn9Nk686mPMJKDEczANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3
# AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG
# AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDIlqRKJM49
# LNKFcGAbLdJkVgNSyXPFDr5KoxVrld+h6TANBgkqhkiG9w0BAQEFAASCAQBUsOzn
# STnZxtk16rAMIWKCsvOjBfTGtEjZuN/qQ2QhpSIi0uUO+NEIR7m+4ajOT0naFZGj
# XCL61R7qWgwh8LwfMOtoCr99umyidBrQvalc3yl4fG470PUyyEjbSdcMZ1emilwT
# FW9cORE+uZlgs3ASC6mMmbqx58/a9JAsWz9aaWSECdE482PkjEXUaZsSaUcHG55o
# AjMkTnh3GIf2XEmARMTCzV3bQ0gdyD4tibyuF+l6HYDOUiVrfAUnp44uzgY06wgI
# Eb61Op3c3iSWuRMQodJEjMmQdV5x2NHQOk9zyEqRlg6uaG6CEnESO38nWtPCHwyY
# 3kPCAJliJuzQ8AupoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0wggMJAgEBMHcwYzEL
# MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE
# aWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBD
# QQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJ
# AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMDMxOTIyMTExMFowLwYJ
# KoZIhvcNAQkEMSIEIOUF3lKakngQ2MMFm2CCJvF+cyvz4ok4NilU9iXcyNxwMA0G
# CSqGSIb3DQEBAQUABIICADyvryUuSV3uPTqg47QP4B4VOX1mKUXHFlijgvGW5Xox
# o1PLEzHfoPsp00fa4ayGCIFzOzkUgEnn8+kNoUEEOkr3oX7IRiPVEqqtjEfYdodt
# m3frIlp+QaFA/Cc8xq3pRzcQOnr0BHDEgXlYrdEpXvydEdiDTr2pVJL+YdXO5jRM
# CKJsIB8/YrqrQzV/TXS55rJ96d+xj0ceFUyKqePhmMUMkmRc8pN1en42hoR2kfJP
# XhTIELnbLcK7b9ysH2mcbprv7JZINeLUI1x4dq09W5VDbPaRJ9YVczDT1Ugpdkzm
# +tChaQi2UAkww5ZHCMT94Rt+IBPgKyjK0u+5wrB4cqAGIyo7LK5ne3H0FDbk5tAq
# JHaZk2AZirNW61juvXyQMh4m8reIH5yOcuLik/0Q90s05OS7hul/4pz4Kv0KMnmR
# aCnvN/cpe8ZCwH6bA/WmjtO8OMbxTdKdZET+XBdPbhb/9YKDzffrxfiGcf682Lvl
# e/9ERZpTh9/IuYrUrDJblIOswrJhX/wYj9nJ4qQ6ySxggso4q882kEimd1tuf5iE
# 40KzBqt7cvqAA49FZ4TY0F71cmEYu8ZCXOu3jl4OflB9W+UcCCKiK/7De9o6EY0O
# 1H1bgb9nQ8t+lPYRFTl30iCoa4EiPBcYWeCBAaWjE/5Mb5U+TPyQYKDLB/UozFBv
# SIG # End signature block