TM-RandomUtility.psm1

using namespace System
using namespace System.IO
using namespace System.Collections.Generic
using namespace Microsoft.PowerShell.Commands

# Ensure we're using the primary write commands from the Microsoft.PowerShell.Utility module.
Set-Alias -Name 'Write-Progress'    -Value 'Microsoft.PowerShell.Utility\Write-Progress'    -Scope Script
Set-Alias -Name 'Write-Debug'       -Value 'Microsoft.PowerShell.Utility\Write-Debug'       -Scope Script
Set-Alias -Name 'Write-Verbose'     -Value 'Microsoft.PowerShell.Utility\Write-Verbose'     -Scope Script
Set-Alias -Name 'Write-Host'        -Value 'Microsoft.PowerShell.Utility\Write-Host'        -Scope Script
Set-Alias -Name 'Write-Information' -Value 'Microsoft.PowerShell.Utility\Write-Information' -Scope Script
Set-Alias -Name 'Write-Warning'     -Value 'Microsoft.PowerShell.Utility\Write-Warning'     -Scope Script
Set-Alias -Name 'Write-Error'       -Value 'Microsoft.PowerShell.Utility\Write-Error'       -Scope Script
Set-Alias -Name 'New-TemporaryFile' -Value 'Microsoft.PowerShell.Utility\New-TemporaryFile' -Scope Script
Set-Alias -Name 'Get-FileHash'      -Value 'Microsoft.PowerShell.Utility\Get-FileHash'      -Scope Script


function Get-ExceptionTypeName {
<#
    .SYNOPSIS
    Returns the exception type name that occurred when executing a script block or command.
 
    .DESCRIPTION
    This function executes the provided command or script block and returns the full name of the exception type
    that occurred during execution.
    Only use this function if you know the exception will not cause further damage by running again.
 
    .PARAMETER Command
    The command to execute
 
    .PARAMETER ScriptBlock
    The script block to execute
 
    .EXAMPLE
    Get-HelpFromType -TypeNames (Get-ExceptionTypeName -Command 'Write-Error "test"')
 
    .OUTPUTS
    If the provided command causes an exception then the exception's typename will be returned.
    If the provided command does NOT cause an exception then nothing is returned.
#>

    [CmdletBinding(DefaultParameterSetName = 'string')]
    [OutputType([Void], [string])]
    param (
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromRemainingArguments,
            ParameterSetName = 'string'
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Command,

        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromRemainingArguments,
            ParameterSetName = 'ScriptBlock'
        )]
        [scriptblock]$ScriptBlock
    )

    [ScriptBlock]$SB = switch ($PSCmdlet.ParameterSetName) {
        'string'      { [ScriptBlock]::Create($Command) }
        'ScriptBlock' { $ScriptBlock }
    }
    $ErrorActionPreference = 'Stop'
    try {
        $SB.Invoke() | Out-Null
    } catch {
        return $_.Exception.GetType().FullName
    }
}


function Get-HelpFromType {
<#
    .SYNOPSIS
    Opens the documentation for .NET types in a web browser.
 
    .DESCRIPTION
    This function opens the official Microsoft documentation for .NET types in the default web browser.
 
    .PARAMETER Objects
    Array of objects to determine the type of and fetch the documentation for.
 
    .EXAMPLE
    Get-HelpFromType -TypeNames @($Object1.GetType().FullName, ($Error[-1].Exception.GetType().FullName))
 
    .EXAMPLE
    Get-HelpFromType -Types $Error[-1].Exception.GetType()
#>

    [CmdletBinding(DefaultParameterSetName = 'TypeNames')]
    [OutputType([Void])]
    param (
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromRemainingArguments,
            ParameterSetName = 'TypeNames'
        )]
        [string[]]$TypeNames,

        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromRemainingArguments,
            ParameterSetName = 'Types'
        )]
        [Type[]]$Types
    )
    process {
        [string[]]$InputTypes = switch ($PSCmdlet.ParameterSetName) {
            'Types' {
                [List[string]]$typeList = [List[string]]::new()
                foreach ($Type in $Types) {
                    $typeList.Add($Type.FullName)
                }
                $typeList.ToArray()
            }
            'TypeNames' { $TypeNames }
        }
        foreach ($typeName in $InputTypes) {
            Start-Process -FilePath "https://docs.microsoft.com/en-us/dotnet/api/$typeName"
        }
    }
}


function Get-StringHash {
<#
    .SYNOPSIS
    This function generates the hash value for a given string using a specified algorithm and text encoding.
 
    .DESCRIPTION
    Get-StringHash is a function that takes an input string, converts it to a byte array in a specified encoding
    (defaulting to UTF8), and then computes the hash of these bytes using the specified algorithm (defaulting to
    SHA256). The function uses PowerShell's Get-FileHash cmdlet on a MemoryStream object to calculate the hash value.
    The function returns a FileHashInfo object.
 
    .PARAMETER InputString
    The string to be hashed.
 
    .PARAMETER Algorithm
    The hashing algorithm to use.
    The options are SHA1, SHA256, SHA384, SHA512, and MD5. The default is SHA256.
 
    .PARAMETER Encoding
    The encoding to be used for transforming the input string into bytes.
    The options are ASCII, BigEndianUnicode, Default, Latin1, Unicode, UTF7, UTF8, and UTF32.
    The "Default" option uses the dotnet system default encoding.
    If no argument is provided the function will default to using UTF8.
 
    .EXAMPLE
    # This example hashes the string 'TestString' using the SHA512 algorithm, and using the default encoding (UTF8).
    'TestString' | Get-StringHash -Algorithm sha512
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [string]$InputString,

        [Parameter(Position = 1, Mandatory = $false)]
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')]
        [string]$Algorithm = 'SHA256',

        [Parameter(Position = 2, Mandatory = $false)]
        [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Latin1', 'Unicode', 'UTF32', 'UTF8', 'UTF7')]
        [string]$Encoding = 'UTF8'
    )

    # Retrieve the bytearray for the string in the specified encoding..
    [byte[]]$byteArray = switch ($Encoding) {
        'ASCII'            { [System.Text.Encoding]::ASCII.GetBytes($InputString) }
        'BigEndianUnicode' { [System.Text.Encoding]::BigEndianUnicode.GetBytes($InputString) }
        'Default'          { [System.Text.Encoding]::Default.GetBytes($InputString) }
        'Latin1'           { [System.Text.Encoding]::Latin1.GetBytes($InputString) }
        'Unicode'          { [System.Text.Encoding]::Unicode.GetBytes($InputString) }
        'UTF32'            { [System.Text.Encoding]::UTF32.GetBytes($InputString) }
        'UTF8'             { [System.Text.Encoding]::UTF8.GetBytes($InputString) }
        'UTF7'             { [System.Text.Encoding]::UTF7.GetBytes($InputString) }
    }

    # Turn the bytes into a memorystream and then get the hash of the stream.
    try {
        [MemoryStream]$stream = [MemoryStream]::new($byteArray)
        [FileHashInfo]$hash = Get-FileHash -InputStream $Stream -Algorithm $Algorithm
    } catch {
        throw # Re-throw the error after finally.
    } finally {
        # Ensure we close the stream if it exists.
        if ($null -ne $stream) {
            $stream.Close()
        }
    }

    return $hash
}


function Get-SubResourceIntegrityHash {
<#
    .SYNOPSIS
    This function generates a Subresource Integrity (SRI) hash, using openssl, for a URI or local file using the
    specified hashing algorithm.
 
    .DESCRIPTION
    Get-SubResourceIntegrityHash is a function that generates a Subresource Integrity (SRI) hash.
    The function can operate on a file located at a given URI (by downloading it to a temporary file) or a local file.
    It uses openssl (which must be installed on the system and accessible from the system path) to generate the hash.
    The hash algorithm defaults to SHA256, but can be specified.
 
    .PARAMETER Uri
    The URI of the file to generate an SRI hash for. Either this parameter or 'Path' must be specified.
 
    .PARAMETER Path
    The local file path of the file to generate an SRI hash for. Either this parameter or 'Uri' must be specified.
 
    .PARAMETER Algorithm
    The hashing algorithm to use. The options are sha1, sha256, sha384, sha512, and MD5. The default is sha256.
 
    .EXAMPLE
    # This example generates an SRI hash for Uri 'https://code.jquery.com/jquery-3.7.0.js' using the SHA512 algorithm.
    Get-SubResourceIntegrityHash -Uri 'https://code.jquery.com/jquery-3.7.0.js' -Algorithm sha512
 
    .EXAMPLE
    # This example generates an SRI hash for the file located at 'C:\Temp\jquery-3.7.0.js' using the SHA384 algorithm.
    Get-SubResourceIntegrityHash -Path 'C:\Temp\jquery-3.7.0.js' -Algorithm sha384
#>


    [CmdletBinding()]
    [OutputType([string],[Void])]
    param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'Uri'
        )]
        [string]$Uri,

        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'Path'
        )]
        [Validation.ValidatePathExists('File')]
        [string]$Path,

        [Parameter(Position = 1, Mandatory = $false)]
        [ValidateSet('sha1', 'sha256', 'sha384', 'sha512', 'MD5')]
        [string]$Algorithm = 'sha256'
    )

    if ((Test-ApplicationExistsInPath -ApplicationName 'openssl') -eq $false) {
        Write-Verbose 'openssl does not exist in the Path. Cannot retrieve subresource integrity hash.'
        return
    }

    try {
        if ($PSCmdlet.ParameterSetName -eq 'Uri') {
            $TempFile = New-TemporaryFile
            Invoke-RestMethod -Uri $Uri -Method Get -OutFile $TempFile.FullName
            $FilePath = $TempFile.FullName
        } else {
            $FilePath = $Path
        }

        $OpensslCmd = "openssl dgst -$Algorithm -binary $FilePath | openssl base64 -A"
        $Result = switch ($true) {
            $IsLinux { bash -c $OpensslCmd }
            $IsMacOS { zsh -c $OpensslCmd }
            default  { & cmd.exe /c $OpensslCmd }
        }
    } catch {
        throw # Re-throw the error after finally.
    } finally {
        if ($TempFile -ne [string]::Empty) {
            Remove-Item -Path $TempFile.FullName -Force
        }
    }
    return $Result
}


function New-Timer {
<#
    .SYNOPSIS
    Starts a new timer.
 
    .DESCRIPTION
    This function creates and starts a new stopwatch timer.
#>

    [CmdletBinding()]
    [OutputType([Diagnostics.Stopwatch])]
    param ()
    return [Diagnostics.Stopwatch]::StartNew()
}


function Test-ApplicationExistsInPath {
<#
    .SYNOPSIS
    Uses Get-Command to see whether the $ApplicationName exists in the path.
#>

    [CmdletBinding()]
    [OutputType([boolean])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationName
    )

    return ($null -ne (Get-Command $ApplicationName -CommandType Application -ErrorAction Ignore))
}


function Write-RandomBytesToFile {
<#
    .SYNOPSIS
    Writes random bytes to a file.
 
    .DESCRIPTION
    This function writes random bytes to a file until that file has reached the specified fileByteSize.
    If the file is already greater than fileByteSize then the function exits early.
 
    .PARAMETER fileByteSize
    The desired file size in bytes
 
    .PARAMETER fileName
    The name of the file to write to, default is 'temp.rnd'
 
    .PARAMETER directoryPath
    The directory path where the file will be created or updated, default is the current directory
#>

    [CmdletBinding()]
    [OutputType([Void])]
    param (
        [Parameter(Mandatory)]
        [long]$fileByteSize,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$fileName = 'temp.rnd',

        [Parameter(Mandatory = $false)]
        [Validation.ValidatePathExists('Folder')]
        [string]$directoryPath = [Environment]::CurrentDirectory
    )

    [string]$filePath = Join-Path -Path $directoryPath -ChildPath $fileName
    [Random]$rnd = [Random]::New()
    [IO.FileInfo]$file = [IO.FileInfo]::New($filePath);
    if ($file.Length -ge $fileByteSize) {
        Write-Host "File size for '$($file.FullName)' already meets or exceeds $fileByteSize"
        return
    }
    [IO.FileStream]$outputStream
    try {
        $outputStream = [IO.File]::Open($file.FullName, [IO.FileMode]::OpenOrCreate)
        $outputStream.Seek(0, [IO.SeekOrigin]::End) | Out-Null
        [long]$bytesWritten = $file.Length
        do {
            [byte[]]$bytes = [byte[]]::New(
                (. {
                    if ($fileByteSize -gt ($bytesWritten + 1024)) {
                        1024
                    } else {
                        [int]($fileByteSize - $bytesWritten)
                    }
                })
            )
            $rnd.NextBytes($bytes)

            $outputStream.Write($bytes, 0, $bytes.Length)
            $bytesWritten += $bytes.Length
        } while ($bytesWritten -lt $fileByteSize)

        Write-Host "Wrote $bytesWritten bytes to '$($file.FullName)'. FileSize: $fileByteSize"
    } finally {
        if ($null -ne $outputStream) { $outputStream.Dispose() }
    }
}