class/RegCli.psm1

#Requires -Version 7.0
#Requires -RunAsAdministrator

Enum MachineType { x64; x86 }

Class RegCli {
    # RegCli is not meant to be instantiated
    # and only declares static functions
    # It is a singleton

    #Region Hidden Members
    Static Hidden [MachineType] $OSArchitecture = $(
        # Get the OS architecture string

        If ([Environment]::Is64BitOperatingSystem) { [MachineType]::x64 } Else { [MachineType]::x86 }
    )

    Static Hidden [void] Get7zip() {
        # Download and install 7zip if it is not yet installed

        $PSScriptRoot |
        ForEach-Object {
            Set-Variable -Name '7Z_EXE' -Value '7z.exe' -Option Constant
            Set-Variable -Name '7Z_DLL' -Value '7z.dll' -Option Constant
            If ((Test-Path "$_\$7Z_EXE","$_\$7Z_DLL").Where({ $_ }).Count -lt 2) {
                Set-Variable -Name 'CHECKSUM' -Value @{
                    '7z' = '8C8FBCF80F0484B48A07BD20E512B103969992DBF81B6588832B08205E3A1B43'
                    '7z_x64' = 'B055FEE85472921575071464A97A79540E489C1C3A14B9BDFBDBAB60E17F36E4'
                    '7zr' = '5E47D0900FB0AB13059E0642C1FFF974C8340C0029DECC3CE7470F9AA78869AB'
                } -Option Constant
                Set-Variable -Name 'CURRENT_DIRECTORY' -Value $PWD -Option Constant
                Set-Location $_
                Try {
                    Set-Variable -Name '7Z_URL' -Value (
                        'https://www.7-zip.org/a/7z2201{0}.exe' -f ([Environment]::Is64BitOperatingSystem ? '-x64':'')
                    ) -Option Constant
                    Set-Variable -Name '7ZR_URL' -Value 'https://www.7-zip.org/a/7zr.exe' -Option Constant
                    Start-BitsTransfer $7ZR_URL,$7Z_URL
                    Set-Variable -Name '7Z_SETUP' -Value ([uri] $7Z_URL).Segments?[-1] -Option Constant
                    Set-Variable -Name '7ZR_SETUP' -Value ([uri] $7ZR_URL).Segments?[-1] -Option Constant
                    Set-Variable -Name 'REMOVE_SETUP' -Value { 
                        Remove-Item $7Z_SETUP,$7ZR_SETUP -Force -ErrorAction SilentlyContinue 
                    } -Option Constant
                    Set-Variable -Name 'COMPARE_SHA' -Value {
                        Param($File, $Hash)
                        If ((Get-FileHash $File -Algorithm SHA256).Hash -ine $Hash) { 
                            & $REMOVE_SETUP
                            Throw
                        }
                    } -Option Constant
                    Switch ($7Z_SETUP) {
                        { $_ -like '*-x64.exe' } { & $COMPARE_SHA $_ $CHECKSUM.'7z_x64' }
                        Default { & $COMPARE_SHA $_ $CHECKSUM.'7z' }
                    }
                    & $COMPARE_SHA $7ZR_SETUP $CHECKSUM.'7zr'
                    . ".\$7ZR_SETUP" x -aoa -o"$_" $7Z_SETUP '7z.exe' '7z.dll' | Out-Null
                    & $REMOVE_SETUP
                }
                Catch { }
                Finally { Set-Location $CURRENT_DIRECTORY }
            }
        }
    }

    Static Hidden [psobject] GetMsiDBRecord(
        [string] $InstallerPath,
        [string] $PropertyName
    ) {
        # Get the value of a record defined by a property name in an MSI installer.

        Try {
            Return $(
                CScript.exe //NoLogo "$PSScriptRoot\GetMsiDBRecord.vbs" `
                /Path:"$((Resolve-Path $InstallerPath).Path)" `
                /Property:"$PropertyName"
            )
        }
        Catch { Return $Null }
    }

    Static Hidden [string] GetInstallerDescription([psobject] $InstallerItem) {
        # Get installer description

        Return (
            $InstallerItem | ForEach-Object {
                (
                    $(
                        Switch (@($_.Extension,$_)) {
                            '.msi'  {
                                [void] $Switch.MoveNext()
                                [RegCli]::GetMsiDBRecord($Switch.Current.FullName, 'ProductName')
                            }
                            Default {
                                [void] $Switch.MoveNext()
                                $Switch.Current.VersionInfo.FileDescription
                            }
                        }
                    ) | ForEach-Object { $_ ? ${_}:$Null }
                ) ?? (Get-AuthenticodeSignature $_.FullName).SignerCertificate.Subject ?? $_.BaseName
            }
        )
    }

    Static Hidden [scriptblock] GetSavedInstallerFilter() {
        # Get the scriptblock that runs the steppable pipeline that filters saved installers

        Return {
            [CmdletBinding()]
            Param(
                [Parameter(Mandatory, ValueFromPipeline)]
                [ValidateNotNullOrEmpty()]
                [pscustomobject] $Item,
                [ValidateNotNullOrEmpty()]
                [array] $AllowedExtensions = @('.exe','.msi'),
                [string] $Description
            )

            Process {
                Switch (
                    {
                        Where-Object {
                            $Item | ForEach-Object {
                                $_ -isnot [System.IO.DirectoryInfo] -and
                                $_.LinkType -ine 'SymbolicLink' -and
                                $_.Extension -iin $AllowedExtensions -and
                                [RegCli]::GetInstallerDescription($_) -like "$Description*"
                            }
                        }
                    }.GetSteppablePipeline()
                ) {
                    Default {
                        $_.Begin($true)
                        $_.Process($Item)
                        $_.End()
                        $_.Dispose()
                    } 
                }
            }
        }
    }

    Static Hidden [scriptblock] GetSavedInstallerScript() {
        # Get the scriptblock that runs the steppable pipeline that filters saved installers

        Return {
            [CmdletBinding()]
            Param(
                [Parameter(Mandatory, ValueFromPipeline)]
                [ValidateNotNullOrEmpty()]
                [pscustomobject] $Item,
                [ValidateNotNullOrEmpty()]
                [string] $Type = 'Version'
            )

            Begin { If ($Type -ieq 'SigningTime') { Import-Module "$PSScriptRoot\SigningTimeGetter.psm1" } }
            Process {
                Switch (@($Type,$Item)) {
                    'Version'  {
                        [void] $Switch.MoveNext()
                        Switch ($Switch.Current) {
                            { $_.Extension -ieq '.msi' }  { [version] [RegCli]::GetMsiDBRecord($_.FullName, 'ProductVersion') }
                            Default { $_.VersionInfo.FileVersionRaw }
                        }
                    }
                    'DateTime' { 
                        [void] $Switch.MoveNext()
                        $Switch.Current.LastWriteTime
                    }
                    'SigningTime' {
                        [void] $Switch.MoveNext()
                        Get-AuthenticodeSigningTime $Switch.Current.FullName
                    }
                }
            }
            End { Remove-Module SigningTimeGetter -ErrorAction SilentlyContinue }
        }
    }
    #EndRegion

    Static [void] ExpandInstaller([string] $Path) {
        [RegCli]::ExpandInstaller($Path, $Null)
    }

    Static [void] ExpandInstaller(
        [string] $InstallerPath,
        [string] $DestinationPath
    ) {
        # Extract files from a specified self-extracting executable
        # installer $InstallerPath to $DestinationPath directory.
        # Precondition :
        # 1. $InstallerPath exists.
        # 2. $DestinationPath may or may not exist and may be $Null.
        # 3. 7zip is installed.

        Get-Item -LiteralPath $InstallerPath |
        ForEach-Object {
            If ([string]::IsNullOrEmpty($DestinationPath)) { $DestinationPath = "$($_.Directory)\$($_.BaseName)" }
            $InstallerPath = $_.FullName
            [RegCli]::Get7zip()
            . "$PSScriptRoot\7z.exe" x -aoa -o"$DestinationPath" "$InstallerPath" 2> $Null
        }
    }

    Static [void] ExpandTypeInstaller(
        [string] $InstallerPath,
        [string] $ExecutablePath,
        [string] $ArchivePattern,
        [bool] $ForceReinstall
    ) {
        [RegCli]::ExpandTypeInstaller($InstallerPath, $ExecutablePath, $ArchivePattern, $ForceReinstall, $False)
    }

    Static [void] ExpandTypeInstaller(
        [string] $InstallerPath,
        [string] $ExecutablePath,
        [string] $ArchivePattern,
        [bool] $ForceReinstall,
        [bool] $MoveAll
    ) {
        # Extracts files from a specified Type installer $InstallerPath
        # to the directory in which the application $ExecutablePath is located.
        # Precondition :
        # 1. $InstallerPath exists.
        # 2. $ExecutablePath may or may not exist.

        $ExeName = $ExeBaseName = $ExeDir = $Null
        ,@(($ExecutablePath -split '\\').Where({ $_ })) |
        ForEach-Object {
            $ExeName = $_[-1]
            $ExeBaseName = & {
                [void] ($ExeName -match '(?<BaseName>[^\\/]+)\.exe$')
                $Matches.BaseName
            }
            $Count = $_.Count
            $Depth = ([int] $MoveAll) + 1
            $ExeDir = $(If ($Count -gt $Depth) { $_[0..($Count - 1 - $Depth)] -join '\' } Else { $PWD })
            Switch ($(Try { Get-Item -LiteralPath $InstallerPath } Catch { })) {
                { $Null -ne $_ } {
                    [RegCli]::ExpandInstaller($_.FullName)
                    New-Item $ExeDir -ItemType Directory -ErrorAction SilentlyContinue
                    $ExeDir = (Get-Item -LiteralPath $ExeDir).FullName
                    $UnzipPath = "$($_.Directory)\$($_.BaseName)"
                    Try {
                        (Get-Item -LiteralPath $UnzipPath).FullName |
                        ForEach-Object { Push-Location $_ }
                        (Get-Item ".\$ArchivePattern" | Select-Object -First 1).FullName |
                        Where-Object { ![string]::IsNullOrEmpty($_) } |
                        ForEach-Object {
                            [RegCli]::ExpandInstaller($_)
                            Remove-Item $_
                        }
                        $UnzippedExeName =
                            Get-ChildItem $ExeName -Recurse -ErrorAction SilentlyContinue |
                            Select-Object -First 1
                        $Executable = Get-Item -LiteralPath $ExecutablePath -ErrorAction SilentlyContinue
                        If (
                            $ForceReinstall ?
                            $($UnzippedExeName.VersionInfo.FileVersionRaw -ge $Executable.VersionInfo.FileVersionRaw):
                            $($UnzippedExeName.VersionInfo.FileVersionRaw -gt $Executable.VersionInfo.FileVersionRaw)
                        ) {
                            Write-Verbose 'Current install is outdated or not installed...'
                            $ExeArchive = "${Env:TEMP}\$($ExeBaseName)_$(Get-Date -Format 'yyMMddHHmm').zip"
                            Try { Compress-Archive $ExeDir -DestinationPath $ExeArchive 2>&1 | Out-Null }
                            Catch { . "$PSScriptRoot\7z.exe" a -tzip $ExeArchive "$ExeDir\*" 2>&1 | Out-Null }
                            Stop-Process -Name $($ExeBaseName) -Force -ErrorAction SilentlyContinue
                            Get-ChildItem $ExeDir -ErrorAction SilentlyContinue |
                            Remove-Item -Recurse
                            If ($MoveAll) { Move-Item "$UnzipPath\*" $ExeDir -Exclude '$*' -ErrorAction SilentlyContinue }
                            Else { Move-Item "$($UnzippedExeName.Directory)\*" $ExeDir -Exclude '$*' -ErrorAction SilentlyContinue }
                        } Else { Write-Verbose 'A newer or the same version is already installed.' }
                        Pop-Location
                        Remove-Item $UnzipPath -Recurse
                    } Catch { }
                }
            }
        }
    }

    Static [void] SetChromiumVisualElementsManifest(
        [string] $VisualElementsManifest,
        [string] $BackgroundColor
    ) {
        # Create the VisualElementManifest.xml in chromium app directory

        $InstallLocation = $VisualElementsManifest -replace ($VisualElementsManifest -split '\\')[-1] -replace '\\$'
        $ErrorActionPreference = 'SilentlyContinue'
        ,@(Get-ChildItem "$InstallLocation\*Logo.png" -Recurse) |
        ForEach-Object {
            $Pattern = "$InstallLocation\" -replace '\\','\\'
            @{
                BigLogo   = $_[0] -replace $Pattern
                SmallLogo = $_[1] -replace $Pattern
            }
        } |
        ForEach-Object {
            Set-Content $VisualElementsManifest -Value @"
<Application xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
    <VisualElements
        ShowNameOnSquare150x150Logo='on'
        Square150x150Logo='$($_.BigLogo)'
        Square70x70Logo='$($_.SmallLogo)'
        Square44x44Logo='$($_.SmallLogo)'
        ForegroundText='light'
        BackgroundColor='$BackgroundColor'/>
</Application>
"@

        }
    }

    Static [void] SetChromiumShortcut(
        [string] $ExecutablePath,
        [string] $ShortcutName
    ) {
        # Create shortcut link to chromium app and save it to Start Menu

        $ExeItem = Get-Item -LiteralPath $ExecutablePath -ErrorAction SilentlyContinue
        (New-Object -ComObject 'WScript.Shell').
        CreateShortcut("${Env:ProgramData}\Microsoft\Windows\Start Menu\Programs\$(
            [string]::IsNullOrEmpty($ShortcutName) ? (Get-Culture).TextInfo.ToTitleCase($(
                $ExeItem.VersionInfo |
                ForEach-Object {
                    $Description = $_.FileDescription
                    $Description ? ($Description):($_.ProductName)
                }
            )):$ShortcutName
        ).lnk"
) |
        ForEach-Object {
            $_.TargetPath = $ExeItem.FullName
            $_.WorkingDirectory = $ExeItem.Directory.FullName
            $_.Save()
        }
    }

    Static [void] ResetTaskbarShortcutTargetPath([string] $ExecutablePath) {
        # Reset target path of a shortcut link only if it exists

        $ExeItem = Get-Item -LiteralPath $ExecutablePath -ErrorAction SilentlyContinue
        $WsShell = New-Object -ComObject 'WScript.Shell'
        Get-ChildItem "${Env:APPDATA}\Microsoft\Internet Explorer\Quick Launch\*" -Recurse -Force |
        Where-Object Name -Like '*.lnk' |
        ForEach-Object { $WsShell.CreateShortcut($_.FullName) } |
        ForEach-Object {
            If ($_.TargetPath -like "*\$($ExeItem.Name)") {
                $_.TargetPath = $ExeItem.FullName
                $_.WorkingDirectory = $ExeItem.Directory.FullName
                $_.Save()
            }
        }
    }

    Static [string] DownloadInstaller([uri] $InstallerUrl) {
        # Download resource and save it to %TEMP% directory

        Return [RegCli]::DownloadInstaller($InstallerUrl, $InstallerUrl.Segments[-1])
    }

    Static [string] DownloadInstaller(
        [uri] $InstallerUrl,
        [string] $InstallerName
    ) {
        # Download resource and save it to %TEMP% directory

        Try {
            $InstallerName -match '((?<BaseName>^.+)(?<Extension>\.[^\.]+$))'
            $Result = "${Env:TEMP}\$($Matches.BaseName)_$(Get-Date -Format 'yyMMddHHmm')$($Matches.Extension)"
            Start-BitsTransfer -Source "$InstallerUrl" -Destination $Result
            If (!$?) { Invoke-WebRequest -Uri "$InstallerUrl" -OutFile $Result }
            Return $Result
        }
        Catch { Return $Null }
    }

    Static [MachineType] GetExeMachineType([string] $ExecutablePath) {
        # Get the machine type of an application

        Switch (
            (Get-Item $ExecutablePath -ErrorAction SilentlyContinue).
            Where({ $_.LinkType -ieq 'SymbolicLink' }) |
            ForEach-Object { $_.LinkTarget }
        ) { Default { $ExecutablePath = $_ } }
        Switch ($(Try { (Get-Item -LiteralPath $ExecutablePath).FullName } Catch { })) {
            { ![string]::IsNullOrEmpty($_) } {
                $PEHeaderOffset = [Byte[]]::New(2)
                $PESignature = [Byte[]]::New(4)
                $MachineType = [Byte[]]::New(2)
                $FileStream = [System.IO.FileStream]::New($_, 'Open', 'Read', 'ReadWrite')
                $FileStream.Position = 0x3c
                [void] $FileStream.Read($PEHeaderOffset, 0, 2)
                $FileStream.Position = [System.BitConverter]::ToUInt16($PEHeaderOffset, 0)
                [void] $FileStream.Read($PESignature, 0, 4)
                [void] $FileStream.Read($MachineType, 0, 2)
                $FileStream.Close()
                Switch ([System.BitConverter]::ToUInt16($MachineType, 0)){
                    0x8664  { Return [MachineType]::x64 }
                    0x14c   { Return [MachineType]::x86 }
                }
            }
        }
        Return [RegCli]::OSArchitecture
    }

    Static [psobject] GetSavedInstallerInfo(
        [string] $Type,
        [string] $InstallerDirectory,
        [string] $InstallerDescription
    ) {
        # Get the most recent date or signing time, or the latest version
        # of a pool of saved installers of particular software.

        ${Function:Select-SavedInstaller} = [RegCli]::GetSavedInstallerFilter()
        ${Function:Get-SavedInstallerInfo} = [RegCli]::GetSavedInstallerScript()
        Return Get-ChildItem $InstallerDirectory |
        Select-SavedInstaller -Description $InstallerDescription |
        Get-SavedInstallerInfo -Type $Type |
        Sort-Object -Descending |
        Select-Object -First 1
    }

    Static [System.Management.Automation.PSModuleInfo] NewUpdate(
        [string] $ExecutablePath,
        [string] $InstallerDirectory,
        [psobject] $VersionString,
        [string] $InstallerDescription,
        [switch] $UseSigningTime,
        [string] $InstallerChecksum,
        [string] $SoftwareName,
        [string] $InstallerExtension = '.exe'
    ) {
        # Load a dynamic module of helper functions for non-software specific tasks

        Return New-Module {
            Param (
                [string] $InstallPath,
                [string] $SaveTo,
                [psobject] $VersionString,
                [string] $InstallerDescription,
                [switch] $UseSigningTime,
                [string] $InstallerChecksum,
                [string] $SoftwareName,
                [string] $InstallerExtension
            )
            
            ${Function:Select-SavedInstaller} = [RegCli]::GetSavedInstallerFilter()
            ${Function:Get-SavedInstallerInfo} = [RegCli]::GetSavedInstallerScript()

            Function Get-ExecutableVersion {
                [CmdletBinding()]
                [OutputType([version])]
                Param ()

                (Get-Item -LiteralPath $InstallPath -ErrorAction SilentlyContinue).VersionInfo.FileVersionRaw
            }

            Set-Variable -Name 'VERSION_PREINSTALL' -Value (Get-ExecutableVersion) -Option ReadOnly

            $Version =
                Switch ($VersionString) {
                    { $_ -is [string] } { 
                        Try {
                            [version] (
                                (
                                    & {
                                        Param ($VerStr)
                                        Switch ($VerStr -replace '\.\.','.') {
                                            { $_ -eq $VerStr } { Return $_ }
                                            Default { & $MyInvocation.MyCommand.ScriptBlock $_ }
                                        }
                                    } ($_ -replace '[^0-9\.]','.')
                                ) -replace '^\.' -replace '\.$'
                            )
                        } Catch { }
                    }
                    Default { $_ }
                }
            
            If ($Version -is [version]) {
                $Version = $Version | ForEach-Object {
                    [version] (
                        ($_.Major,$_.Minor,$_.Build,$_.Revision |
                        ForEach-Object {
                            Switch ($_) { 
                                { $_ -lt 0 } { 0 }
                                Default { $_ }
                            }
                        }) -join '.'
                    )
                }
            }

            $GetVersionInfo = & {
                Switch ($InstallerChecksum.Length) {
                    32 {
                        Return {
                            Param($Item)
                            $InstallerChecksum -ieq (Get-FileHash $Item.FullName 'MD5').Hash 
                        }
                    }
                    40 {
                        Return {
                            Param($Item)
                            $InstallerChecksum -ieq (Get-FileHash $Item.FullName 'SHA1').Hash 
                        }
                    }
                    64 {
                        Return {
                            Param($Item)
                            $InstallerChecksum -ieq (Get-FileHash $Item.FullName 'SHA256').Hash 
                        }
                    }
                    128 {
                        Return {
                            Param($Item)
                            $InstallerChecksum -ieq (Get-FileHash $Item.FullName 'SHA512').Hash
                        }
                    }
                }
                Switch (${Version}?.GetType()) {
                    'version'  {
                        Switch ($InstallerExtension) {
                            '.msi'  {
                                Return {
                                    Param($Item)
                                    ([version] [RegCli]::GetMsiDBRecord($Item.FullName, 'ProductVersion')) -eq $Version
                                }
                            }
                            Default {
                                Return {
                                    Param($Item)
                                    $Item.VersionInfo.FileVersionRaw -eq $Version
                                }
                            }
                        }
                    }
                    'datetime' {
                        If ($UseSigningTime) {
                            Return {
                                Param($Item)
                                (Get-AuthenticodeSigningTime $Item.FullName) -eq $Version
                            }
                        }
                        Else {
                            Return {
                                Param($Item)
                                $Item.LastWriteTime -eq $Version
                            }
                        }
                    }
                }
            }

            $InstallerPrefix = "$(${SoftwareName}?.ToLower().Trim() -replace ' ','_')$(If(!!$SoftwareName){'_'})"

            $InstallerPath = $(
                If ($UseSigningTime) { Import-Module "$PSScriptRoot\SigningTimeGetter.psm1" }
                Get-ChildItem $SaveTo |
                Select-SavedInstaller -AllowedExtensions @($InstallerExtension) -Description $InstallerDescription |
                Where-Object { & $GetVersionInfo $_ } |
                Select-Object -First 1
                Remove-Module SigningTimeGetter -ErrorAction SilentlyContinue
            ).FullName ??
            "$SaveTo\$InstallerPrefix$(
                $VersionString | ForEach-Object {
                    $_ -is [datetime] ? ('{0}.{1}.{2}' -f $_.Year,$_.DayOfYear,$_.TimeOfDay.TotalMinutes.ToString('#.##')):$_
                }
            )$InstallerExtension"


            <#
            .SYNOPSIS
                Gets the installer path.
            #>

            Function Get-InstallerPath {
                [CmdletBinding()]
                [OutputType([string])]
                Param ()
                
                Return $Script:InstallerPath
            }
            
            <#
            .SYNOPSIS
                Gets the installer version.
            #>

            Function Get-InstallerVersion {
                [CmdletBinding()]
                [OutputType([psobject])]
                Param ([switch] $UseInstaller)
                DynamicParam {
                    If ($UseInstaller) {
                        $AttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::New()
                        $AttributeCollection.Add([System.Management.Automation.ParameterAttribute] @{ Mandatory = $False })
                        $AttributeCollection.Add([System.Management.Automation.ValidateSetAttribute]::New('Version','DateTime','SigningTime'))
                        $ParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::New()
                        $ParamDictionary.Add('Type',[System.Management.Automation.RuntimeDefinedParameter]::New('Type',[string],$AttributeCollection))
                        $PSBoundParameters.Type = 'Version'
                        $ParamDictionary
                    }
                }

                Process {
                    If ($UseInstaller) {
                        Return Get-SavedInstallerInfo -Type $Type -Item (Get-Item -LiteralPath (Get-InstallerPath) -ErrorAction SilentlyContinue)
                    }
                    Return $Script:Version
                }
                End { }
            }
            
            <#
            .SYNOPSIS
                Saves the installer to the installation path.
            #>

            Filter Start-InstallerDownload {
                [CmdletBinding()]
                [OutputType([System.Void])]
                Param (
                    [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
                    [ValidateNotNullOrEmpty()]
                    [Alias('Url','Link')]
                    [string] $InstallerUrl,
                    [Parameter(ValueFromPipelineByPropertyName)]
                    [ValidateNotNullOrEmpty()]
                    [ValidateScript({ $_.Length -in @(32, 40, 64, 128) })]
                    [Alias('Checksum')]
                    [string] $InstallerChecksum,
                    [Parameter(ValueFromPipelineByPropertyName)]
                    [AllowNull()]
                    [AllowEmptyString()]
                    [Alias('Name')]
                    [string] $InstallerName,
                    [switch] $Force
                )
    
                If (!(Test-Path (Get-InstallerPath))) {
                    Write-Verbose 'Download installer...'
                    $IsChecksumPresent = $PSBoundParameters.ContainsKey('InstallerChecksum')
                    $SaveInstallerArgs = @{ Url = [uri] $InstallerUrl }
                    If ($PSBoundParameters.ContainsKey('InstallerName') -and
                        ![string]::IsNullOrEmpty($InstallerName)) {
                        $SaveInstallerArgs.FileName = $InstallerName
                    }
                    Switch ($PSBoundParameters) {
                        {
                            $_.ContainsKey('InstallerName') -and
                            ![string]::IsNullOrEmpty($InstallerName)
                        } { $SaveInstallerArgs.FileName = $InstallerName }
                        { $Force -eq $True } { $SaveInstallerArgs.SkipSslValidation = $True }
                    }
                    (Save-Installer @SaveInstallerArgs).
                    Where({ 
                        If ($IsChecksumPresent) {
                            $InstallerChecksum -ieq (Get-FileHash $_ $(
                            Switch ($InstallerChecksum.Length) { 32 { 'MD5' } 40 { 'SHA1' } 64 { 'SHA256' } 128 { 'SHA512' } })).Hash 
                        } Else { (Get-AuthenticodeSignature $_).Status -ieq 'Valid' }
                    }) |
                    Select-Object @{
                        Name = 'Path';
                        Expression = {
                            If (![string]::IsNullOrEmpty($_)) {
                                If ($IsChecksumPresent) {
                                    Write-Verbose 'Hashes match...'
                                } Else { Write-Verbose 'Signature verified...' }
                            }
                            Return $_
                        }
                    } | Move-Item -Destination (Get-InstallerPath)
                }
            }
            
            <#
            .SYNOPSIS
                Removes outdated installers.
            #>

            Function Remove-InstallerOutdated {
                [CmdletBinding()]
                [OutputType([System.Void])]
                Param ([switch] $UsePrefix)

                Try {
                    If ([string]::IsNullOrEmpty($VersionString)) { Throw }
                    $Installer = Get-Item (Get-InstallerPath) -ErrorAction Stop
                    $DescriptionCopy = !$UsePrefix ? $(
                        $InstallerDescription.Contains('*') ?
                        ${InstallerDescription}:[RegCli]::GetInstallerDescription($Installer)
                    ):$InstallerPrefix
                    Write-Verbose 'Delete outdated installers...'
                    Get-ChildItem $Installer.Directory |
                    Select-SavedInstaller -Description $DescriptionCopy |
                    Remove-Item -Exclude $Installer.Name
                }
                Catch { }
            }
    
            <#
            .SYNOPSIS
                Tests whether the current install is outdated.
            #>

            Function Test-InstallOutdated {
                [CmdletBinding()]
                [OutputType([bool])]
                Param ([switch] $UseInstaller)
                DynamicParam {
                    If (!$UseInstaller) {
                        $AttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::New()
                        $AttributeCollection.Add([System.Management.Automation.ParameterAttribute] @{ Mandatory = $False })
                        $ParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::New()
                        $ParamDictionary.Add('CompareInstalls',[System.Management.Automation.RuntimeDefinedParameter]::New('CompareInstalls',[switch],$AttributeCollection))
                        $ParamDictionary
                    }
                }

                Process {
                    If ($PSBoundParameters.CompareInstalls) { Return (Get-ExecutableVersion) -lt $VERSION_PREINSTALL }
                    (Get-InstallerVersion -UseInstaller:$UseInstaller) -gt (Get-ExecutableVersion)
                }
                End { }
            }

            Function Set-ConsoleSymlink {
                [CmdletBinding()]
                [OutputType([System.Void])]
                Param ([ref] $InstallStatus)

                $InstallDirectory = $InstallPath -replace '(\\|/)[^\\/]+$'
                If ([string]::IsNullOrEmpty($InstallDirectory)) { $InstallDirectory = $PWD }
                If ("$((Get-Item $InstallDirectory -ErrorAction SilentlyContinue).FullName)" -ieq (Get-Item $SaveTo).FullName) {
                    # If $InstallPath directory is equal to $SaveTo
                    @{
                        NewName = & {
                            [void] ($InstallPath -match '(?<ExeName>[^\\/]+$)')
                            $Matches.ExeName
                        }
                        LiteralPath = Get-InstallerPath
                        ErrorAction = 'SilentlyContinue'
                        Force = $True
                    } | ForEach-Object { Rename-Item @_ }
                    $InstallStatus.Value = Test-Path $InstallPath
                } Else {
                    New-Item $InstallDirectory -ItemType Directory -Force | Out-Null
                    @{
                        Path = $InstallPath
                        ItemType = 'SymbolicLink'
                        Value = Get-InstallerPath
                        ErrorAction = 'SilentlyContinue'
                        Force = $True
                    } | ForEach-Object { New-Item @_ | Out-Null }
                    $InstallStatus.Value = (Get-Item (Get-Item $InstallPath).Target).FullName -ieq (Get-Item (Get-InstallerPath)).FullName
                }
            }
        } -ArgumentList $ExecutablePath,$InstallerDirectory,$VersionString,$InstallerDescription,$UseSigningTime,$InstallerChecksum,$SoftwareName,$InstallerExtension
    }

    Static [string] $CommonScriptVersion = '1.1'

    Static [System.Management.Automation.PSModuleInfo] GetCommonScript(
        [string] $Name,
        [string] $CommonPath
    ) {
        # Load the Invoke-CommonScript function

        New-Item -Path $CommonPath -ItemType Directory -ErrorAction SilentlyContinue
        Return New-Module {
            Param(
                [string] $Name,
                [string] $CommonPath
            )

            $CommonScript = "$CommonPath\$Name"
            $RequestArguments = @{
                Uri = "https://github.com/sangafabrice/reg-cli/raw/main/common/$Name@$([RegCli]::CommonScriptVersion).ps1"
                Method = 'HEAD'
                Verbose = $False
            }
            Try {
                $CommonScriptEtag = (Invoke-WebRequest @RequestArguments).Headers.ETag -replace '"|\s|#'
                $LocalCommonScriptEtag = (Get-Content $CommonScript -Tail 1 -ErrorAction SilentlyContinue) -replace '"|\s|#'
                If ($LocalCommonScriptEtag -ieq $CommonScriptEtag) { Throw }
                $RequestArguments.Method = 'GET'
                ${Function:Invoke-CommonScript} = "$(Invoke-WebRequest @RequestArguments)"
                Set-Content $CommonScript -Value ${Function:Invoke-CommonScript}
                Add-Content $CommonScript -Value "# $CommonScriptEtag"
            }
            Catch {
                $ErrorActionPreference = 'SilentlyContinue'
                ${Function:Invoke-CommonScript} = (Get-Content $CommonScript -Raw)?.Substring(0,(Get-Item $CommonScript).Length - 68)
            }
        } -ArgumentList $Name,$CommonPath
    }
}