PSAppDeployToolkit.WinGet.psm1

<#
 
.SYNOPSIS
PSAppDeployToolkit.WinGet - This module script a basic scaffold to use with PSAppDeployToolkit modules destined for the PowerShell Gallery.
 
.DESCRIPTION
This module can be directly imported from the command line via Import-Module, but it is usually imported by the Invoke-AppDeployToolkit.ps1 script.
 
PSAppDeployToolkit is licensed under the BSD 3-Clause License - Copyright (C) 2024 Mitch Richters. All rights reserved.
 
.NOTES
BSD 3-Clause License
 
Copyright (c) 2024, Mitch Richters
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 
1. Redistributions of source code must retain the above copyright notice, this
    list of conditions and the following disclaimer.
 
2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
 
3. Neither the name of the copyright holder nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
#>


#-----------------------------------------------------------------------------
#
# MARK: Module Initialization Code
#
#-----------------------------------------------------------------------------

# Throw if this psm1 file isn't being imported via our manifest.
if (!([System.Environment]::StackTrace.Split("`n") -like '*Microsoft.PowerShell.Commands.ModuleCmdletBase.LoadModuleManifest(*'))
{
    throw [System.Management.Automation.ErrorRecord]::new(
        [System.InvalidOperationException]::new("This module must be imported via its .psd1 file, which is recommended for all modules that supply a .psd1 file."),
        'ModuleImportError',
        [System.Management.Automation.ErrorCategory]::InvalidOperation,
        $MyInvocation.MyCommand.ScriptBlock.Module
    )
}

# Set up lookup table for all cmdlets used within module, using PSAppDeployToolkit's as a basis.
$CommandTable = [System.Collections.Generic.Dictionary[System.String, System.Management.Automation.CommandInfo]](& (& 'Microsoft.PowerShell.Core\Get-Command' -Name Get-ADTCommandTable -FullyQualifiedModule @{ ModuleName = 'PSAppDeployToolkit'; Guid = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.4' }))

# Expand command lookup table with cmdlets used through this module.
& {
    # Import system modules and add their commands to the command table.
    $SystemModules = [System.Collections.ObjectModel.ReadOnlyCollection[Microsoft.PowerShell.Commands.ModuleSpecification]]$(
        @{ ModuleName = 'Appx'; Guid = 'aeef2bef-eba9-4a1d-a3d2-d0b52df76deb'; ModuleVersion = '1.0' }
    )
    (& $Script:CommandTable.'Import-Module' -FullyQualifiedName $SystemModules -Global -Force -PassThru -ErrorAction Stop).ExportedCommands.Values | & { process { $CommandTable.Add($_.Name, $_) } }

    # Add commands from manifest-defined modules.
    $RequiredModules = [System.Collections.ObjectModel.ReadOnlyCollection[Microsoft.PowerShell.Commands.ModuleSpecification]]$(
        @{ ModuleName = 'psyml'; Guid = 'a88e2e67-a937-4d98-a4d3-0b03d3ade169'; ModuleVersion = '1.0.0' }
    )
    (& $Script:CommandTable.'Get-Module' -FullyQualifiedName $RequiredModules -ErrorAction Stop).ExportedCommands.Values | & { process { $CommandTable.Add($_.Name, $_) } }
}

# Set required variables to ensure module functionality.
& $Script:CommandTable.'New-Variable' -Name ErrorActionPreference -Value ([System.Management.Automation.ActionPreference]::Stop) -Option Constant -Force
& $Script:CommandTable.'New-Variable' -Name InformationPreference -Value ([System.Management.Automation.ActionPreference]::Continue) -Option Constant -Force
& $Script:CommandTable.'New-Variable' -Name ProgressPreference -Value ([System.Management.Automation.ActionPreference]::SilentlyContinue) -Option Constant -Force

# Ensure module operates under the strictest of conditions.
& $Script:CommandTable.'Set-StrictMode' -Version 3

# Store build information pertaining to this module's state.
& $Script:CommandTable.'New-Variable' -Name Module -Option Constant -Force -Value ([ordered]@{
        Manifest = & $Script:CommandTable.'Import-LocalizedData' -BaseDirectory $PSScriptRoot -FileName 'PSAppDeployToolkit.WinGet.psd1'
        Compiled = $MyInvocation.MyCommand.Name.Equals('PSAppDeployToolkit.WinGet.psm1')
    }).AsReadOnly()

# Remove any previous functions that may have been defined.
if ($Module.Compiled)
{
    & $Script:CommandTable.'New-Variable' -Name FunctionPaths -Option Constant -Value ($MyInvocation.MyCommand.ScriptBlock.Ast.EndBlock.Statements | & { process { if ($_ -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return "Microsoft.PowerShell.Core\Function::$($_.Name)" } } })
    & $Script:CommandTable.'Remove-Item' -LiteralPath $FunctionPaths -Force -ErrorAction Ignore
}

# Define enum for all known WinGet exit codes.
enum ADTWinGetExitCode
{
    INTERNAL_ERROR = -1978335231
    INVALID_CL_ARGUMENTS = -1978335230
    COMMAND_FAILED = -1978335229
    MANIFEST_FAILED = -1978335228
    CTRL_SIGNAL_RECEIVED = -1978335227
    SHELLEXEC_INSTALL_FAILED = -1978335226
    UNSUPPORTED_MANIFESTVERSION = -1978335225
    DOWNLOAD_FAILED = -1978335224
    CANNOT_WRITE_TO_UPLEVEL_INDEX = -1978335223
    INDEX_INTEGRITY_COMPROMISED = -1978335222
    SOURCES_INVALID = -1978335221
    SOURCE_NAME_ALREADY_EXISTS = -1978335220
    INVALID_SOURCE_TYPE = -1978335219
    PACKAGE_IS_BUNDLE = -1978335218
    SOURCE_DATA_MISSING = -1978335217
    NO_APPLICABLE_INSTALLER = -1978335216
    INSTALLER_HASH_MISMATCH = -1978335215
    SOURCE_NAME_DOES_NOT_EXIST = -1978335214
    SOURCE_ARG_ALREADY_EXISTS = -1978335213
    NO_APPLICATIONS_FOUND = -1978335212
    NO_SOURCES_DEFINED = -1978335211
    MULTIPLE_APPLICATIONS_FOUND = -1978335210
    NO_MANIFEST_FOUND = -1978335209
    EXTENSION_PUBLIC_FAILED = -1978335208
    COMMAND_REQUIRES_ADMIN = -1978335207
    SOURCE_NOT_SECURE = -1978335206
    MSSTORE_BLOCKED_BY_POLICY = -1978335205
    MSSTORE_APP_BLOCKED_BY_POLICY = -1978335204
    EXPERIMENTAL_FEATURE_DISABLED = -1978335203
    MSSTORE_INSTALL_FAILED = -1978335202
    COMPLETE_INPUT_BAD = -1978335201
    YAML_INIT_FAILED = -1978335200
    YAML_INVALID_MAPPING_KEY = -1978335199
    YAML_DUPLICATE_MAPPING_KEY = -1978335198
    YAML_INVALID_OPERATION = -1978335197
    YAML_DOC_BUILD_FAILED = -1978335196
    YAML_INVALID_EMITTER_STATE = -1978335195
    YAML_INVALID_DATA = -1978335194
    LIBYAML_ERROR = -1978335193
    MANIFEST_VALIDATION_WARNING = -1978335192
    MANIFEST_VALIDATION_FAILURE = -1978335191
    INVALID_MANIFEST = -1978335190
    UPDATE_NOT_APPLICABLE = -1978335189
    UPDATE_ALL_HAS_FAILURE = -1978335188
    INSTALLER_SECURITY_CHECK_FAILED = -1978335187
    DOWNLOAD_SIZE_MISMATCH = -1978335186
    NO_UNINSTALL_INFO_FOUND = -1978335185
    EXEC_UNINSTALL_COMMAND_FAILED = -1978335184
    ICU_BREAK_ITERATOR_ERROR = -1978335183
    ICU_CASEMAP_ERROR = -1978335182
    ICU_REGEX_ERROR = -1978335181
    IMPORT_INSTALL_FAILED = -1978335180
    NOT_ALL_PACKAGES_FOUND = -1978335179
    JSON_INVALID_FILE = -1978335178
    SOURCE_NOT_REMOTE = -1978335177
    UNSUPPORTED_RESTSOURCE = -1978335176
    RESTSOURCE_INVALID_DATA = -1978335175
    BLOCKED_BY_POLICY = -1978335174
    RESTAPI_INTERNAL_ERROR = -1978335173
    RESTSOURCE_INVALID_URL = -1978335172
    RESTAPI_UNSUPPORTED_MIME_TYPE = -1978335171
    RESTSOURCE_INVALID_VERSION = -1978335170
    SOURCE_DATA_INTEGRITY_FAILURE = -1978335169
    STREAM_READ_FAILURE = -1978335168
    PACKAGE_AGREEMENTS_NOT_ACCEPTED = -1978335167
    PROMPT_INPUT_ERROR = -1978335166
    UNSUPPORTED_SOURCE_REQUEST = -1978335165
    RESTAPI_ENDPOINT_NOT_FOUND = -1978335164
    SOURCE_OPEN_FAILED = -1978335163
    SOURCE_AGREEMENTS_NOT_ACCEPTED = -1978335162
    CUSTOMHEADER_EXCEEDS_MAXLENGTH = -1978335161
    MISSING_RESOURCE_FILE = -1978335160
    MSI_INSTALL_FAILED = -1978335159
    INVALID_MSIEXEC_ARGUMENT = -1978335158
    FAILED_TO_OPEN_ALL_SOURCES = -1978335157
    DEPENDENCIES_VALIDATION_FAILED = -1978335156
    MISSING_PACKAGE = -1978335155
    INVALID_TABLE_COLUMN = -1978335154
    UPGRADE_VERSION_NOT_NEWER = -1978335153
    UPGRADE_VERSION_UNKNOWN = -1978335152
    ICU_CONVERSION_ERROR = -1978335151
    PORTABLE_INSTALL_FAILED = -1978335150
    PORTABLE_REPARSE_POINT_NOT_SUPPORTED = -1978335149
    PORTABLE_PACKAGE_ALREADY_EXISTS = -1978335148
    PORTABLE_SYMLINK_PATH_IS_DIRECTORY = -1978335147
    INSTALLER_PROHIBITS_ELEVATION = -1978335146
    PORTABLE_UNINSTALL_FAILED = -1978335145
    ARP_VERSION_VALIDATION_FAILED = -1978335144
    UNSUPPORTED_ARGUMENT = -1978335143
    BIND_WITH_EMBEDDED_NULL = -1978335142
    NESTEDINSTALLER_NOT_FOUND = -1978335141
    EXTRACT_ARCHIVE_FAILED = -1978335140
    NESTEDINSTALLER_INVALID_PATH = -1978335139
    PINNED_CERTIFICATE_MISMATCH = -1978335138
    INSTALL_LOCATION_REQUIRED = -1978335137
    ARCHIVE_SCAN_FAILED = -1978335136
    PACKAGE_ALREADY_INSTALLED = -1978335135
    PIN_ALREADY_EXISTS = -1978335134
    PIN_DOES_NOT_EXIST = -1978335133
    CANNOT_OPEN_PINNING_INDEX = -1978335132
    MULTIPLE_INSTALL_FAILED = -1978335131
    MULTIPLE_UNINSTALL_FAILED = -1978335130
    NOT_ALL_QUERIES_FOUND_SINGLE = -1978335129
    PACKAGE_IS_PINNED = -1978335128
    PACKAGE_IS_STUB = -1978335127
    APPTERMINATION_RECEIVED = -1978335126
    DOWNLOAD_DEPENDENCIES = -1978335125
    DOWNLOAD_COMMAND_PROHIBITED = -1978335124
    SERVICE_UNAVAILABLE = -1978335123
    RESUME_ID_NOT_FOUND = -1978335122
    CLIENT_VERSION_MISMATCH = -1978335121
    INVALID_RESUME_STATE = -1978335120
    CANNOT_OPEN_CHECKPOINT_INDEX = -1978335119
    RESUME_LIMIT_EXCEEDED = -1978335118
    INVALID_AUTHENTICATION_INFO = -1978335117
    AUTHENTICATION_TYPE_NOT_SUPPORTED = -1978335116
    AUTHENTICATION_FAILED = -1978335115
    AUTHENTICATION_INTERACTIVE_REQUIRED = -1978335114
    AUTHENTICATION_CANCELLED_BY_USER = -1978335113
    AUTHENTICATION_INCORRECT_ACCOUNT = -1978335112
    NO_REPAIR_INFO_FOUND = -1978335111
    REPAIR_NOT_APPLICABLE = -1978335110
    EXEC_REPAIR_FAILED = -1978335109
    REPAIR_NOT_SUPPORTED = -1978335108
    ADMIN_CONTEXT_REPAIR_PROHIBITED = -1978335107
    SQLITE_CONNECTION_TERMINATED = -1978335106
    DISPLAYCATALOG_API_FAILED = -1978335105
    NO_APPLICABLE_DISPLAYCATALOG_PACKAGE = -1978335104
    SFSCLIENT_API_FAILED = -1978335103
    NO_APPLICABLE_SFSCLIENT_PACKAGE = -1978335102
    LICENSING_API_FAILED = -1978335101
    INSTALL_PACKAGE_IN_USE = -1978334975
    INSTALL_INSTALL_IN_PROGRESS = -1978334974
    INSTALL_FILE_IN_USE = -1978334973
    INSTALL_MISSING_DEPENDENCY = -1978334972
    INSTALL_DISK_FULL = -1978334971
    INSTALL_INSUFFICIENT_MEMORY = -1978334970
    INSTALL_NO_NETWORK = -1978334969
    INSTALL_CONTACT_SUPPORT = -1978334968
    INSTALL_REBOOT_REQUIRED_TO_FINISH = -1978334967
    INSTALL_REBOOT_REQUIRED_FOR_INSTALL = -1978334966
    INSTALL_REBOOT_INITIATED = -1978334965
    INSTALL_CANCELLED_BY_USER = -1978334964
    INSTALL_ALREADY_INSTALLED = -1978334963
    INSTALL_DOWNGRADE = -1978334962
    INSTALL_BLOCKED_BY_POLICY = -1978334961
    INSTALL_DEPENDENCIES = -1978334960
    INSTALL_PACKAGE_IN_USE_BY_APPLICATION = -1978334959
    INSTALL_INVALID_PARAMETER = -1978334958
    INSTALL_SYSTEM_NOT_SUPPORTED = -1978334957
    INSTALL_UPGRADE_NOT_SUPPORTED = -1978334956
    INVALID_CONFIGURATION_FILE = -1978286079
    INVALID_YAML = -1978286078
    INVALID_FIELD_TYPE = -1978286077
    UNKNOWN_CONFIGURATION_FILE_VERSION = -1978286076
    SET_APPLY_FAILED = -1978286075
    DUPLICATE_IDENTIFIER = -1978286074
    MISSING_DEPENDENCY = -1978286073
    DEPENDENCY_UNSATISFIED = -1978286072
    ASSERTION_FAILED = -1978286071
    MANUALLY_SKIPPED = -1978286070
    WARNING_NOT_ACCEPTED = -1978286069
    SET_DEPENDENCY_CYCLE = -1978286068
    INVALID_FIELD_VALUE = -1978286067
    MISSING_FIELD = -1978286066
    TEST_FAILED = -1978286065
    TEST_NOT_RUN = -1978286064
    GET_FAILED = -1978286063
    UNIT_NOT_INSTALLED = -1978285823
    UNIT_NOT_FOUND_REPOSITORY = -1978285822
    UNIT_MULTIPLE_MATCHES = -1978285821
    UNIT_INVOKE_GET = -1978285820
    UNIT_INVOKE_TEST = -1978285819
    UNIT_INVOKE_SET = -1978285818
    UNIT_MODULE_CONFLICT = -1978285817
    UNIT_IMPORT_MODULE = -1978285816
    UNIT_INVOKE_INVALID_RESULT = -1978285815
    UNIT_SETTING_CONFIG_ROOT = -1978285808
    UNIT_IMPORT_MODULE_ADMIN = -1978285807
    NOT_SUPPORTED_BY_PROCESSOR = -1978285806
}


#-----------------------------------------------------------------------------
#
# MARK: Convert-ADTArrayToRegexCaptureGroup
#
#-----------------------------------------------------------------------------

function Convert-ADTArrayToRegexCaptureGroup
{
    <#
    .SYNOPSIS
        Accepts one or more strings and converts the results into a regex capture group.
 
    .DESCRIPTION
        This function accepts one or more strings and converts the results into a regex capture group.
 
    .PARAMETER InputObject
        One or more string objects to parse and return as a regex capture group.
 
    .INPUTS
        System.String. Convert-ADTArrayToRegexCaptureGroup accepts accepts one or more string objects for returning as a regex capture group.
 
    .OUTPUTS
        System.String. Convert-ADTArrayToRegexCaptureGroup returns a string object of the concatenated input as a regex capture group.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowNull()]
        [System.String[]]$InputObject
    )

    begin
    {
        # Open collector to hold escaped and parsed values.
        $items = [System.Collections.Generic.List[System.String]]::new()
    }

    process
    {
        # Process incoming data and store in the collector.
        foreach ($item in $InputObject.Where({ ![System.String]::IsNullOrWhiteSpace($_) }))
        {
            $items.Add([System.Text.RegularExpressions.Regex]::Escape($item))
        }
    }

    end
    {
        # Return collected strings as a regex capture group.
        if ($items.Count) { return "($([System.String]::Join('|', $items)))" }
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Convert-ADTFunctionParamsToArgArray
#
#-----------------------------------------------------------------------------

function Convert-ADTFunctionParamsToArgArray
{
    <#
    .SYNOPSIS
        Converts the provided parameter metadata into an argument array for applications, with presets for MSI, Dell Command | Update, and WinGet.
 
    .DESCRIPTION
        This function accepts parameter metadata and with this, the parameter set name and a help message tag, converts the parameters into an array of arguments for applications.
 
        There are presets available for MSI, Dell Command | Update, WinGet, and PnpUtil, or a completely custom arrangement can be accomodated.
 
    .PARAMETER BoundParameters
        A hashtable of parameters to process.
 
    .PARAMETER Invocation
        The script or function's InvocationInfo ($MyInvocation) to process.
 
    .PARAMETER ParameterSetName
        The ParameterSetName to use as a filter against the Invocation's parameters.
 
    .PARAMETER HelpMessage
        The HelpMessage field to use as a filter against the Invocation's parameters.
 
    .PARAMETER Exclude
        One or more parameter names to exclude from the results.
 
    .PARAMETER Ordered
        Instructs that the returned parameters are in the exact order they're read from the BoundParameters or Invocation.
 
    .PARAMETER Preset
        The preset of which to use when generating an argument array. Current presets are MSI, Dell Command | Update, WinGet, PnpUtil, and PowerShell.
 
    .PARAMETER ArgValSeparator
        For non-preset modes, the separator between an argument's name and value.
 
    .PARAMETER ArgPrefix
        For non-preset modes, the prefix to apply to an argument's name.
 
    .PARAMETER ValueWrapper
        For non-preset modes, what, if anything, to use as characters to wrap around the value (e.g. --ArgName="Value").
 
    .PARAMETER MultiValDelimiter
        For non-preset modes, how to handle parameters where their value is an array of data.
 
    .INPUTS
        System.Collections.IDictionary. Convert-ADTFunctionParamsToArgArray can accept one or more IDictionary objects for processing.
        System.Management.Automation.InvocationInfo. Convert-ADTFunctionParamsToArgArray can accept one or more InvocationInfo objects for processing.
 
    .OUTPUTS
        System.String[]. Convert-ADTFunctionParamsToArgArray returns one or more string objects representing the converted parameters.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ParameterSetName', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'HelpMessage', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Ordered', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'MultiValDelimiter', Justification = "This parameter is used within delegates that PSScriptAnalyzer has no visibility of. See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 for more details.")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersPreset', ValueFromPipeline = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersCustom', ValueFromPipeline = $true)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary]$BoundParameters,

        [Parameter(Mandatory = $true, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter', ValueFromPipeline = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter', ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.InvocationInfo]$Invocation,

        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')]
        [ValidateNotNullOrEmpty()]
        [System.String]$ParameterSetName,

        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')]
        [ValidateNotNullOrEmpty()]
        [System.String]$HelpMessage,

        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersPreset', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Exclude,

        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset', HelpMessage = 'Primary parameter')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom', HelpMessage = 'Primary parameter')]
        [System.Management.Automation.SwitchParameter]$Ordered,

        [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersPreset')]
        [Parameter(Mandatory = $true, ParameterSetName = 'InvocationPreset')]
        [ValidateSet('MSI', 'WinGet', 'DellCommandUpdate', 'PnpUtil', 'PowerShell')]
        [System.String]$Preset,

        [Parameter(Mandatory = $true, ParameterSetName = 'BoundParametersCustom')]
        [Parameter(Mandatory = $true, ParameterSetName = 'InvocationCustom')]
        [ValidateSet(' ', '=', "`n")]
        [System.String]$ArgValSeparator,

        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')]
        [ValidateSet('-', '--', '/')]
        [System.String]$ArgPrefix,

        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')]
        [ValidateSet("'", '"')]
        [System.String]$ValueWrapper,

        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersPreset')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationPreset')]
        [Parameter(Mandatory = $false, ParameterSetName = 'BoundParametersCustom')]
        [Parameter(Mandatory = $false, ParameterSetName = 'InvocationCustom')]
        [ValidateSet(',', '|')]
        [System.String]$MultiValDelimiter = ','
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Set up regex for properly trimming lines. Yes, reflection == long lines.
        $invalidends = "$($MyInvocation.MyCommand.Parameters.Values.GetEnumerator().Where({$_.Name.Equals('ArgValSeparator')}).Attributes.Where({$_ -is [System.Management.Automation.ValidateSetAttribute]}).ValidValues | & $Script:CommandTable.'Convert-ADTArrayToRegexCaptureGroup')+$"
        $nullvalues = "\s$($MyInvocation.MyCommand.Parameters.Values.GetEnumerator().Where({$_.Name.Equals('ValueWrapper')}).Attributes.Where({$_ -is [System.Management.Automation.ValidateSetAttribute]}).ValidValues | & $Script:CommandTable.'Convert-ADTArrayToRegexCaptureGroup'){2}$"

        # Set up the string for formatting.
        $string = switch ($Preset)
        {
            MSI { "{0}=`"{1}`""; break }
            WinGet { "--{0}`n{1}"; break }
            DellCommandUpdate { "-{0}={1}"; break }
            PnpUtil { "/{0}`n{1}"; break }
            PowerShell { "-{0}`n`"{1}`""; break }
            default { "$($ArgPrefix){0}$($ArgValSeparator)$($ValueWrapper){1}$($ValueWrapper)"; break }
        }

        # Persistent scriptblocks stored in RAM for Convert-ADTFunctionParamsToArgArray.
        $script = if ($Preset -eq 'MSI')
        {
            {
                # For switches, we want to convert the $true/$false into 1/0 respectively.
                if ($_.Value -isnot [System.Management.Automation.SwitchParameter])
                {
                    [System.String]::Format($string, $_.Key.ToUpper(), $_.Value -join $MultiValDelimiter).Split("`n").Trim()
                }
                else
                {
                    [System.String]::Format($string, $_.Key.ToUpper(), [System.UInt32][System.Boolean]$_.Value).Split("`n").Trim()
                }
            }
        }
        else
        {
            {
                # For switches, we only want true switches, and we drop the $true value entirely.
                $notswitch = $_.Value -isnot [System.Management.Automation.SwitchParameter]
                if ($notswitch -or $_.Value)
                {
                    $name = if ($Preset -eq 'PowerShell') { $_.Key } else { $_.Key.ToLower() }
                    $value = if ($notswitch) { $_.Value -join $MultiValDelimiter }
                    [System.String]::Format($string, $name, $value).Split("`n").Trim() -replace $nullvalues
                }
            }
        }

        # Amend exclusions with default parameter values.
        $Exclude = $(
            $Exclude
            [System.Management.Automation.PSCmdlet]::CommonParameters
            [System.Management.Automation.PSCmdlet]::OptionalCommonParameters
        )
    }

    process
    {
        try
        {
            try
            {
                # If we're processing an invocation, get its bound parameters as required.
                if ($Invocation)
                {
                    $bpdvParams = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' -Invocation $MyInvocation -HelpMessage 'Primary parameter'
                    $BoundParameters = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' @bpdvParams
                }

                # Process the parameters into an argument array and return to the caller.
                return $BoundParameters.GetEnumerator().Where({ $Exclude -notcontains $_.Key }).ForEach($script) -replace $invalidends | & $Script:CommandTable.'Where-Object' { ![System.String]::IsNullOrWhiteSpace($_) }
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to convert the provided input to an argument array."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Convert-ADTWinGetQueryOutput
#
#-----------------------------------------------------------------------------

function Convert-ADTWinGetQueryOutput
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$WinGetOutput
    )

    # Process each collected line into an object.
    try
    {
        $WinGetOutput.Trim().TrimEnd('.') | & {
            begin
            {
                # Define variables for heading data that'll be the first line via the pipe.
                $listHeading = $headIndices = $null
            }

            process
            {
                if ($_ -notmatch '^\w+')
                {
                    return
                }

                # Use our first valid line to set up the keys for each property.
                if (!$listHeading)
                {
                    # Get all headings and the indices from the output.
                    $listHeading = $_ -split '\s+'
                    $headIndices = $($listHeading | & { process { $args[0].IndexOf($_) } } $_; 10000)
                    return
                }

                # Establish hashtable to hold contents we're converting.
                $obj = [ordered]@{}

                # Begin conversion and return object to the pipeline.
                for ($i = 0; $i -lt $listHeading.Length; $i++)
                {
                    $thisi = [System.Math]::Min($headIndices[$i], $_.Length)
                    $nexti = [System.Math]::Min($headIndices[$i + 1], $_.Length)
                    $value = $_.Substring($thisi, $nexti - $thisi).Trim()
                    $obj.Add($listHeading[$i], $(if (![System.String]::IsNullOrWhiteSpace($value)) { $value }))
                }
                return [pscustomobject]$obj
            }
        }
    }
    catch
    {
        $naerParams = @{
            Exception = [System.IO.InvalidDataException]::new("Failed to parse provided WinGet output.", $_.Exception)
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'WinGetListOutputParseFailure'
            TargetObject = $WinGetOutput
            RecommendedAction = "Please review the WinGet output manually, then try again."
        }
        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTRedirectedUri
#
#-----------------------------------------------------------------------------

function Get-ADTRedirectedUri
{
    <#
    .SYNOPSIS
        Returns the resolved URI from the provided permalink.
 
    .DESCRIPTION
        This function gets the resolved/redirected URI from the provided input and returns it to the caller.
 
    .PARAMETER Uri
        The URL that requires redirection resolution.
 
    .PARAMETER Headers
        Any headers that need to be provided for URI redirection resolution.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        System.Uri
 
        Get-ADTRedirectedUri returns a Uri of the resolved/redirected URI.
 
    .EXAMPLE
        Get-ADTRedirectedUri -Uri https://aka.ms/getwinget
 
        Returns the absolute URI for the specified short link, e.g. https://github.com/microsoft/winget-cli/releases/download/v1.8.1911/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    [OutputType([System.Uri])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute))
                {
                    $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.'))
                }
                return !!$_
            })]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.Collections.IDictionary]$Headers = @{ Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' }
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Create web request.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieving the redirected URI for [$Uri]."
                $webReq = [System.Net.WebRequest]::Create($Uri)
                $webReq.AllowAutoRedirect = $false
                $Headers.GetEnumerator() | & { process { $webReq.($_.Key) = $_.Value } }

                # Get a response and close it out.
                $reqRes = $webReq.GetResponse()
                $resLoc = $reqRes.GetResponseHeader('Location')
                $reqRes.Close()

                # If $resLoc is empty, return the provided URI so something is returned to the caller.
                if (![System.String]::IsNullOrWhiteSpace($resLoc))
                {
                    $Uri = $resLoc
                }

                # Return the redirected URI to the caller.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieved redirected URI [$Uri] from the provided input."
                return $Uri
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to determine the redirected URI for [$Uri]."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTUriFileName
#
#-----------------------------------------------------------------------------

function Get-ADTUriFileName
{
    <#
    .SYNOPSIS
        Returns the filename of the provided URI.
 
    .DESCRIPTION
        This function gets the filename of the provided URI from the provided input and returns it to the caller.
 
    .PARAMETER Uri
        The URL that to retrieve the filename from.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        System.String
 
        Get-ADTUriFileName returns a string value of the URI's filename.
 
    .EXAMPLE
        Get-ADTUriFileName -Uri https://aka.ms/getwinget
 
        Returns the filename for the specified URI, redirected or otherwise. e.g. Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute))
                {
                    $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.'))
                }
                return !!$_
            })]
        [System.Uri]$Uri
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Re-write the URI to factor in any redirections.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Retrieving the file name for URI [$Uri]."
                $Uri = & $Script:CommandTable.'Get-ADTRedirectedUri' -Uri $Uri

                # Create web request.
                $webReq = [System.Net.WebRequest]::Create($Uri)
                $webReq.AllowAutoRedirect = $false
                $webReq.Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'

                # Get a response and close it out.
                $reqRes = $webReq.GetResponse()
                $resCnt = $reqRes.GetResponseHeader('Content-Disposition')
                $reqRes.Close()

                # If $resCnt is empty, the provided URI likely has the filename in it.
                $filename = if (!$resCnt.Contains('filename'))
                {
                    & $Script:CommandTable.'Remove-ADTInvalidFileNameChars' -Name $Uri.ToString().Split('/')[-1]
                }
                else
                {
                    & $Script:CommandTable.'Remove-ADTInvalidFileNameChars' -Name $resCnt.Split(';').Trim().Where({ $_.StartsWith('filename=') }).Split('=')[-1]
                }

                # Return the determined filename to the caller.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resolved filename [$filename] from the provided URI."
                return $filename
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to determine the filename for URI [$Uri]."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchArgumentList
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetHashMismatchArgumentList
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")]
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Manifest,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Installer,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$FilePath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$LogFile
    )

    # Internal filter to process manifest install switches.
    filter Get-ADTWinGetManifestInstallSwitches
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")]
        [CmdletBinding()]
        param
        (
            [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
            [ValidateNotNullOrEmpty()]
            [pscustomobject]$InputObject,

            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [System.String]$Type
        )

        # Test whether the piped object has InstallerSwitches and it's not null.
        if (($InputObject.PSObject.Properties.Name -notcontains 'InstallerSwitches') -or ($null -eq $InputObject.InstallerSwitches))
        {
            return
        }

        # Return the requested type. This will be null if its not available.
        return $InputObject.InstallerSwitches.PSObject.Properties | & $Script:CommandTable.'Where-Object' { $_.Name -eq $Type } | & $Script:CommandTable.'Select-Object' -ExpandProperty Value
    }

    # Internal function to return default install switches based on type.
    function Get-ADTDefaultKnownSwitches
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")]
        [CmdletBinding()]
        [OutputType([System.String])]
        param
        (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [System.String]$InstallerType
        )

        # Switch on the installer type and return an array of strings for the args.
        switch -Regex ($InstallerType)
        {
            '^(Burn|Wix|Msi)$'
            {
                "/quiet"
                "/norestart"
                "/log `"$LogFile`""
                break
            }
            '^Nullsoft$'
            {
                "/S"
                break
            }
            '^Inno$'
            {
                "/VERYSILENT"
                "/NORESTART"
                "/LOG=`"$LogFile`""
                break
            }
            default
            {
                $naerParams = @{
                    Exception = [System.InvalidOperationException]::new("The installer type '$_' is unsupported.")
                    Category = [System.Management.Automation.ErrorCategory]::InvalidData
                    ErrorId = 'WinGetInstallerTypeUnknown'
                    TargetObject = $_
                    RecommendedAction = "Please report the installer type to the project's maintainer for further review."
                }
                $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
            }
        }
    }

    # Add standard msiexec.exe args.
    if ($FilePath.EndsWith('msi'))
    {
        "/i `"$FilePath`""
    }

    # If we're not overriding, get silent switches from manifest and $Custom if we can.
    if (!$Override)
    {
        # Try to get switches from the installer, then the manifest, then by what the installer is, either from the installer or the manifest.
        if ($switches = $Installer | Get-ADTWinGetManifestInstallSwitches -Type Silent)
        {
            # First check the installer array for a silent switch.
            $switches
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using Silent switches from the manifest's installer data."
        }
        elseif ($switches = $Manifest | Get-ADTWinGetManifestInstallSwitches -Type Silent)
        {
            # Fall back to the manifest itself.
            $switches
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using Silent switches from the manifest's top level."
        }
        elseif ($instType = $Installer | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType')
        {
            # We have no defined switches, try to determine switches from the installer's defined type.
            Get-ADTDefaultKnownSwitches -InstallerType $instType
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using default switches for the manifest installer's installer type ($instType)."
        }
        elseif ($instType = $Manifest | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType')
        {
            # The installer array doesn't define a type, see if the manifest itself does.
            Get-ADTDefaultKnownSwitches -InstallerType $instType
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using default switches for the manifest's installer type ($instType)."
        }
        elseif ($switches = $Installer | Get-ADTWinGetManifestInstallSwitches -Type SilentWithProgress)
        {
            # We're shit out of luck... circle back and see if we have _anything_ we can use.
            $switches
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using SilentWithProgress switches from the manifest's installer data."
        }
        elseif ($switches = $Manifest | Get-ADTWinGetManifestInstallSwitches -Type SilentWithProgress)
        {
            # Last-ditch effort. It's this or bust.
            $switches
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Using SilentWithProgress switches from the manifest's top level."
        }
        else
        {
            $naerParams = @{
                Exception = [System.InvalidOperationException]::new("Unable to determine how to silently install the application.")
                Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                ErrorId = 'WinGetInstallerTypeUnknown'
                TargetObject = $PSBoundParameters
                RecommendedAction = "Please report this issue to the project's maintainer for further review."
            }
            $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
        }

        # Append any custom switches the caller has provided.
        if ($Custom)
        {
            $Custom
        }
    }
    else
    {
        # Override replaces anything the manifest provides.
        $Override
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchDownload
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetHashMismatchDownload
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Installer
    )

    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$($Installer.InstallerUrl)], please wait..."
    try
    {
        # Download WinGet app and store path to binary.
        $wgFilePath = "$([System.IO.Directory]::CreateDirectory("$([System.IO.Path]::GetTempPath())$(& $Script:CommandTable.'Get-Random')").FullName)\$(& $Script:CommandTable.'Get-ADTUriFileName' -Uri $Installer.InstallerUrl)"
        & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $Installer.InstallerUrl -OutFile $wgFilePath

        # If downloaded file is a zip, we need to expand it and modify our file path before returning.
        if ($wgFilePath -match 'zip$')
        {
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloaded installer is a zip file, expanding its contents."
            & $Script:CommandTable.'Expand-Archive' -LiteralPath $wgFilePath -DestinationPath ([System.IO.Path]::GetTempPath()) -Force
            $wgFilePath = "$([System.IO.Path]::GetTempPath())$($Installer.NestedInstallerFiles.RelativeFilePath)"
        }
        return $wgFilePath
    }
    catch
    {
        $naerParams = @{
            Exception = [System.InvalidOperationException]::new("Failed to download [$($Installer.InstallerUrl)].", $_.Exception)
            Category = [System.Management.Automation.ErrorCategory]::InvalidOperation
            ErrorId = 'WinGetInstallerDownloadFailure'
            RecommendedAction = "Please verify the installer's URI is valid, then try again."
        }
        $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchExitCodes
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetHashMismatchExitCodes
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "This function is appropriately named and we don't need PSScriptAnalyzer telling us otherwise.")]
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Manifest,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Installer,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$FilePath
    )

    # Try to get switches from the installer, then the manifest, then by whatever known defaults we have.
    if ($Installer.PSObject.Properties.Name.Contains('InstallerSuccessCodes'))
    {
        return $Installer.InstallerSuccessCodes
    }
    elseif ($Manifest.PSObject.Properties.Name.Contains('InstallerSuccessCodes'))
    {
        return $Manifest.InstallerSuccessCodes
    }
    else
    {
        # Zero is valid for everything.
        0

        # Factor in two msiexec.exe-specific exit codes.
        if ($FilePath.EndsWith('msi'))
        {
            1641  # Machine needs immediate reboot.
            3010  # Reboot should be rebooted.
        }
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchInstaller
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetHashMismatchInstaller
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [pscustomobject]$Manifest,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Scope,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Architecture,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$InstallerType
    )

    # Get correct installation data from the manifest based on scope and system architecture.
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Processing installer metadata from the package manifest."
    $nativeArch = $Manifest.Installers.Architecture -contains $Script:ADT.SystemArchitecture
    $cultureName = [System.Globalization.CultureInfo]::CurrentUICulture.Name
    $wgInstaller = $Manifest.Installers | & $Script:CommandTable.'Where-Object' {
        (!$_.PSObject.Properties.Name.Contains('Scope') -or ($_.Scope -eq $Scope)) -and
        (!$_.PSObject.Properties.Name.Contains('InstallerLocale') -or ($_.InstallerLocale -eq $cultureName)) -and
        (!$InstallerType -or (($instType = $_ | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType') -and ($instType -eq $InstallerType))) -and
        ($_.Architecture.Equals($Architecture) -or ($haveArch = $_.Architecture -eq $Script:ADT.SystemArchitecture) -or (!$haveArch -and !$nativeArch))
    }

    # Validate the output. The yoda notation is to keep PSScriptAnalyzer happy.
    if ($null -eq $wgInstaller)
    {
        # We found nothing and therefore can't continue.
        $naerParams = @{
            Exception = [System.InvalidOperationException]::new("Error occurred while processing installer metadata from the package's manifest.")
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'WinGetManifestInstallerResultNull'
            TargetObject = $wgInstaller
            RecommendedAction = "Please review the package's installer metadata within the manifest, then try again."
        }
        $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
    }
    elseif ($wgInstaller -is [System.Collections.IEnumerable])
    {
        # We got multiple values. Get all unique installer types from the metadata and check for uniqueness.
        if (!$wgInstaller.Count.Equals((($wgInstTypes = $wgInstaller | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType' | & $Script:CommandTable.'Select-Object' -Unique) | & $Script:CommandTable.'Measure-Object').Count))
        {
            # Something's gone wrong as we've got duplicate installer types.
            $naerParams = @{
                Exception = [System.InvalidOperationException]::new("Error determining correct installer metadata from the package's manifest.")
                Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                ErrorId = 'WinGetManifestInstallerResultInconclusive'
                TargetObject = $wgInstaller
                RecommendedAction = "Please review the package's installer metadata within the manifest, then try again."
            }
            $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
        }

        # Installer types were unique, just return the first one and hope for the best.
        & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found installer types ['$([System.String]::Join("', '", $wgInstTypes))']; using [$($wgInstTypes[0])] metadata."
        $wgInstaller = $wgInstaller | & $Script:CommandTable.'Where-Object' { ($_ | & $Script:CommandTable.'Get-ADTWinGetHashMismatchInstallerType').Equals($wgInstTypes[0]) }
    }

    # Return installer metadata to the caller.
    return $wgInstaller
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchInstallerType
#
#-----------------------------------------------------------------------------

filter Get-ADTWinGetHashMismatchInstallerType
{
    if ($_.PSObject.Properties.Name.Contains('NestedInstallerType'))
    {
        return $_.NestedInstallerType
    }
    elseif ($_.PSObject.Properties.Name.Contains('InstallerType'))
    {
        return $_.InstallerType
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetHashMismatchManifest
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetHashMismatchManifest
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Version
    )

    # Set up vars and get package manifest.
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading and parsing the package manifest from GitHub."
    try
    {
        $wgUriBase = "https://raw.githubusercontent.com/microsoft/winget-pkgs/master/manifests/{0}/{1}/{2}/{3}.installer.yaml"
        $wgPkgsUri = [System.String]::Format($wgUriBase, $Id.Substring(0, 1).ToLower(), $Id.Replace('.', '/'), $Version, $Id)
        $wgPkgYaml = & $Script:CommandTable.'Invoke-RestMethod' -UseBasicParsing -Uri $wgPkgsUri -Verbose:$false
        $wgManifest = $wgPkgYaml | & $Script:CommandTable.'ConvertFrom-Yaml'
        return $wgManifest
    }
    catch
    {
        $naerParams = @{
            Exception = [System.IO.InvalidDataException]::new("Failed to download or parse the package manifest from GitHub.", $_.Exception)
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'WinGetManifestParseFailure'
            RecommendedAction = "Please review the package's manifest, then try again."
        }
        $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetPath
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetPath
{
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param
    (
    )

    # For the system user, get the path from Program Files directly. For some systems, we can't rely on the
    # output of Get-AppxPackage as it'll update, but Get-AppxPackage won't reflect the new path fast enough.
    $wingetPath = if ($Script:ADT.RunningAsSystem)
    {
        & $Script:CommandTable.'Get-ChildItem' -Path "$([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles))\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe" | & $Script:CommandTable.'Sort-Object' -Descending | & $Script:CommandTable.'Select-Object' -First 1
    }
    elseif (($wingetCommand = & $Script:CommandTable.'Get-Command' -Name winget.exe -ErrorAction Ignore))
    {
        $wingetCommand.Source
    }
    elseif ([System.IO.File]::Exists(($appxPath = "$(& $Script:CommandTable.'Get-AppxPackage' -Name Microsoft.DesktopAppInstaller -AllUsers:$Script:ADT.RunningAsSystem | & $Script:CommandTable.'Sort-Object' -Property Version -Descending | & $Script:CommandTable.'Select-Object' -ExpandProperty InstallLocation -First 1)\winget.exe")))
    {
        $appxPath
    }

    # Throw if we didn't find a WinGet path.
    if (!$wingetPath)
    {
        $naerParams = @{
            Exception = [System.IO.FileNotFoundException]::new("Failed to find a valid path to winget.exe on this system.")
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'MicrosoftDesktopAppInstallerVersionError'
            RecommendedAction = "Please invoke [Repair-ADTWinGetPackageManager], then try again."
        }
        $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
    }

    # Return the found path to the caller.
    return [System.IO.FileInfo]$wingetPath
}


#-----------------------------------------------------------------------------
#
# MARK: Invoke-ADTWebDownload
#
#-----------------------------------------------------------------------------

function Invoke-ADTWebDownload
{
    <#
    .SYNOPSIS
        Wraps around Invoke-WebRequest to provide logging and retry support.
 
    .DESCRIPTION
        This function allows callers to download files as part of a deployment with logging and retry support.
 
    .PARAMETER Uri
        The URL that to retrieve the file from.
 
    .PARAMETER OutFile
        The path of where to save the file to.
 
    .PARAMETER Headers
        Any headers that need to be provided for file transfer.
 
    .PARAMETER Sha256Hash
        An optional SHA256 reference file hash for download verification.
 
    .PARAMETER PassThru
        Returns the WebResponseObject object from Invoke-WebRequest.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        Microsoft.PowerShell.Commands.WebResponseObject
 
        Invoke-ADTWebDownload returns the results from Invoke-WebRequest if PassThru is specified.
 
    .EXAMPLE
        Invoke-ADTWebDownload -Uri https://aka.ms/getwinget -OutFile "$($adtSession.DirSupportFiles)\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
 
        Downloads the latest WinGet installer to the SupportFiles directory.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    [OutputType([Microsoft.PowerShell.Commands.WebResponseObject])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if (![System.Uri]::IsWellFormedUriString($_.AbsoluteUri, [System.UriKind]::Absolute))
                {
                    $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Uri -ProvidedValue $_ -ExceptionMessage 'The specified input is not a valid Uri.'))
                }
                return !!$_
            })]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$OutFile,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.Collections.IDictionary]$Headers = @{ Accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' },

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Sha256Hash,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$PassThru
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Commence download and return the result if passing through.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading $Uri."
                $iwrParams = & $Script:CommandTable.'Get-ADTBoundParametersAndDefaultValues' -Invocation $MyInvocation -Exclude Sha256Hash
                $iwrResult = & $Script:CommandTable.'Invoke-ADTCommandWithRetries' -Command $Script:CommandTable.'Invoke-WebRequest' -UseBasicParsing @iwrParams -Verbose:$false

                # Validate the hash if one was provided.
                if ($PSBoundParameters.ContainsKey('Sha256Hash') -and (($fileHash = & $Script:CommandTable.'Get-FileHash' -LiteralPath $OutFile).Hash -ne $Sha256Hash))
                {
                    $naerParams = @{
                        Exception = [System.BadImageFormatException]::new("The downloaded file has an invalid file hash of [$($fileHash.Hash)].", $OutFile)
                        Category = [System.Management.Automation.ErrorCategory]::InvalidData
                        ErrorId = 'DownloadedFileInvalid'
                        TargetObject = $fileHash
                        RecommendedAction = "Please compare the downloaded file's hash against the provided value and try again."
                    }
                    throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                }

                # Return any results from Invoke-WebRequest if we have any and we're passing through.
                if ($PassThru -and $iwrResult)
                {
                    return $iwrResult
                }
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Object ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Error downloading setup file(s) from the provided URL of [$Uri]."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Invoke-ADTWinGetDeploymentOperation
#
#-----------------------------------------------------------------------------

function Invoke-ADTWinGetDeploymentOperation
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('install', 'repair', 'uninstall', 'upgrade')]
        [System.String]$Action
    )

    dynamicparam
    {
        # Define parameter dictionary for returning at the end.
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

        # Add in parameters for specific modes.
        if ($Action -match '^(install|upgrade)$')
        {
            if ($Action -eq 'upgrade')
            {
                $paramDictionary.Add('Include-Unknown', [System.Management.Automation.RuntimeDefinedParameter]::new(
                        'Include-Unknown', [System.Management.Automation.SwitchParameter], $(
                            [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                            [System.Management.Automation.AliasAttribute]::new('IncludeUnknown')
                        )
                    ))
            }
            $paramDictionary.Add('Ignore-Security-Hash', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Ignore-Security-Hash', [System.Management.Automation.SwitchParameter], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.AliasAttribute]::new('AllowHashMismatch')
                    )
                ))
            $paramDictionary.Add('DebugHashMismatch', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'DebugHashMismatch', [System.Management.Automation.SwitchParameter], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    )
                ))
            $paramDictionary.Add('Architecture', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Architecture', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateSetAttribute]::new('x86', 'x64', 'arm64')
                    )
                ))
            $paramDictionary.Add('Custom', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Custom', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                    )
                ))
            $paramDictionary.Add('Header', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Header', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                    )
                ))
            $paramDictionary.Add('Installer-Type', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Installer-Type', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateSetAttribute]::new('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable')
                        [System.Management.Automation.AliasAttribute]::new('InstallerType')
                    )
                ))
            $paramDictionary.Add('Locale', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Locale', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                    )
                ))
            $paramDictionary.Add('Location', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Location', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                    )
                ))
            $paramDictionary.Add('Override', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Override', [System.String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                    )
                ))
            $paramDictionary.Add('Scope', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Scope', [String], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.ValidateSetAttribute]::new('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown')
                    )
                ))
            $paramDictionary.Add('Skip-Dependencies', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Skip-Dependencies', [System.Management.Automation.SwitchParameter], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                        [System.Management.Automation.AliasAttribute]::new('SkipDependencies')
                    )
                ))
        }
        if ($Action -ne 'repair')
        {
            $paramDictionary.Add('Force', [System.Management.Automation.RuntimeDefinedParameter]::new(
                    'Force', [System.Management.Automation.SwitchParameter], $(
                        [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    )
                ))
        }

        # Add in parameters used by all actions.
        $paramDictionary.Add('MatchOption', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'MatchOption', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateSetAttribute]::new('Equals', 'EqualsCaseInsensitive')
                )
            ))
        $paramDictionary.Add('Query', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Query', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Id', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Id', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Log', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Log', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Mode', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Mode', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateSetAttribute]::new('Silent', 'Interactive')
                )
            ))
        $paramDictionary.Add('Moniker', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Moniker', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Name', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Name', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Source', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Source', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))
        $paramDictionary.Add('Version', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'Version', [System.String], $(
                    [System.Management.Automation.ParameterAttribute]@{ Mandatory = $false }
                    [System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()
                )
            ))

        # Return the populated dictionary.
        return $paramDictionary
    }

    begin
    {
        # Internal function to generate arguments array for WinGet.
        function Out-ADTWinGetDeploymentArgumentList
        {
            [CmdletBinding()]
            param
            (
                [Parameter(Mandatory = $true)]
                [ValidateNotNullOrEmpty()]
                [System.Collections.Generic.Dictionary[System.String, System.Object]]$BoundParameters,

                [Parameter(Mandatory = $false)]
                [ValidateNotNullOrEmpty()]
                [System.String[]]$Exclude
            )

            # Ensure the action is also excluded.
            $PSBoundParameters.Exclude = $('Action'; 'MatchOption'; 'Ignore-Security-Hash'; 'DebugHashMismatch'; $(if ($Exclude) { $Exclude } ))

            # Output each item for the caller to collect.
            return $(
                $Action
                & $Script:CommandTable.'Convert-ADTFunctionParamsToArgArray' @PSBoundParameters -Preset WinGet
                if ($PSBoundParameters.ContainsKey('MatchOption') -and ($PSBoundParameters.MatchOption -eq 'Equals'))
                {
                    '--exact'
                }
                '--accept-source-agreements'
                if ($Action -ne 'Uninstall')
                {
                    '--accept-package-agreements'
                }
            )
        }

        # Define internal scriptblock for invoking WinGet. This is a
        # scriptblock so Write-ADTLogEntry uses this function's source.
        $wingetInvoker = {
            return , [System.String[]](& $wingetPath $wingetArgs 2>&1 | & {
                    begin
                    {
                        $waleParams = @{PassThru = $true }
                    }

                    process
                    {
                        if ($_ -match '^\w+')
                        {
                            $waleParams.Severity = if ($_ -match 'exit code: \d+') { 3 } else { 1 }
                            & $Script:CommandTable.'Write-ADTLogEntry' @waleParams -Message ($_.Trim() -replace '((?<![.:])|:)$', '.')
                        }
                    }
                })
        }

        # Throw if an id, name, or moniker hasn't been provided. This is done like this
        # and not via parameter sets because this is what Install-WinGetPackage does.
        if (!$PSBoundParameters.ContainsKey('Id') -and !$PSBoundParameters.ContainsKey('Name') -and !$PSBoundParameters.ContainsKey('Moniker'))
        {
            $naerParams = @{
                Exception = [System.ArgumentException]::new("Please specify a package by Id, Name, or Moniker.")
                Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
                ErrorId = "WinGet$([System.Globalization.CultureInfo]::CurrentUICulture.TextInfo.ToTitleCase($Action))FilterError"
                TargetObject = $PSBoundParameters
                RecommendedAction = "Please specify a package by Id, Name, or Moniker; then try again."
            }
            $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
        }

        # Try to get the path to WinGet before proceeding.
        try
        {
            $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }

        # Get the active ADT session object if one's in play.
        $adtSession = if (& $Script:CommandTable.'Test-ADTSessionActive')
        {
            & $Script:CommandTable.'Get-ADTSession'
        }

        # Default the scope to "Machine" for the safety of users.
        # It's super easy to install user-scoped apps into the SYSTEM
        # user's account, and it's painful to diagnose/clean up.
        if (($noScope = !$PSBoundParameters.ContainsKey('Scope')))
        {
            $PSBoundParameters.Add('Scope', 'Machine')
        }

        # Most of the time, we're only wanting a WinGet package anyway.
        # Defaulting to the winget source speeds up operations.
        if ($PSBoundParameters.ContainsKey('Source'))
        {
            try
            {
                $null = & $Script:CommandTable.'Get-ADTWinGetSource' -Name $PSBoundParameters.Source
            }
            catch
            {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
        else
        {
            $PSBoundParameters.Add('Source', 'winget')
        }

        # Add in a default log file if the caller hasn't specified one.
        if (!$PSBoundParameters.ContainsKey('Log'))
        {
            $PSBoundParameters.Log = if ($adtSession)
            {
                "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$($adtSession.InstallName)_WinGet.log"
            }
            else
            {
                "$([System.IO.Path]::GetTempPath())Invoke-ADTWinGetOperation_$([System.DateTime]::Now.ToString('O').Split('.')[0].Replace(':', $null))_WinGet.log"
            }
        }

        # Attempt to find the package to install.
        try
        {
            $fawgpParams = @{}; if ($PSBoundParameters.ContainsKey('Id'))
            {
                $fawgpParams.Add('Id', $PSBoundParameters.Id)
            }
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                $fawgpParams.Add('Name', $PSBoundParameters.Name)
            }
            if ($PSBoundParameters.ContainsKey('Moniker'))
            {
                $fawgpParams.Add('Moniker', $PSBoundParameters.Moniker)
            }
            if ($PSBoundParameters.ContainsKey('Source'))
            {
                $fawgpParams.Add('Source', $PSBoundParameters.Source)
            }
            $wgPackage = & $Script:CommandTable.'Find-ADTWinGetPackage' @fawgpParams -InformationAction SilentlyContinue
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }

        # Set up arguments array for WinGet.
        $wingetArgs = Out-ADTWinGetDeploymentArgumentList -BoundParameters $PSBoundParameters

        # Generate action lookup table for verbage.
        $actionTranslator = @{
            Install = 'Installer'
            Repair = 'Repair'
            Uninstall = 'Uninstaller'
            Upgrade = 'Installer'
        }
    }

    end
    {
        # Test whether we're debugging the AllowHashMismatch feature.
        if (($Action -notmatch '^(install|upgrade)$') -or !$PSBoundParameters.ContainsKey('DebugHashMismatch') -or !$PSBoundParameters.DebugHashMismatch)
        {
            # Invoke WinGet and print each non-null line.
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Executing [$wingetPath $wingetArgs]."
            $wingetOutput = & $wingetInvoker

            # If package isn't found, rerun again without --Scope argument.
            if (($Global:LASTEXITCODE -eq [ADTWinGetExitCode]::NO_APPLICABLE_INSTALLER) -and $noScope)
            {
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Attempting to execute WinGet again without '--scope' argument."
                $wingetArgs = Out-ADTWinGetDeploymentArgumentList -BoundParameters $PSBoundParameters -Exclude Scope
                $wingetOutput = & $wingetInvoker
            }
        }
        else
        {
            # Going into bypass mode. Simulate WinGet output for the purpose of getting the app's version later on.
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Bypassing WinGet as `-DebugHashMismatch` has been passed. This switch should only be used for debugging purposes."
            $Global:LASTEXITCODE = [ADTWinGetExitCode]::INSTALLER_HASH_MISMATCH.value__
            $PSBoundParameters.'Ignore-Security-Hash' = $true
        }

        # If we're bypassing a hash failure, process the WinGet manifest ourselves.
        if (($Global:LASTEXITCODE -eq [ADTWinGetExitCode]::INSTALLER_HASH_MISMATCH) -and $PSBoundParameters.ContainsKey('Ignore-Security-Hash') -and $PSBoundParameters.'Ignore-Security-Hash')
        {
            # The hash failed, however we're forcing an override. Set up default parameters for Get-ADTWinGetHashMismatchInstaller and get started.
            & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installation failed due to mismatched hash, attempting to override as `-IgnoreHashFailure` has been passed."
            $gawgaiParams = @{}; if ($PSBoundParameters.ContainsKey('Scope'))
            {
                $gawgaiParams.Add('Scope', $PSBoundParameters.Scope)
            }
            if ($PSBoundParameters.ContainsKey('Architecture'))
            {
                $gawgaiParams.Add('Architecture', $PSBoundParameters.Architecture)
            }
            if ($PSBoundParameters.ContainsKey('Installer-Type'))
            {
                $gawgaiParams.Add('InstallerType', $PSBoundParameters.'Installer-Type')
            }

            # Grab the manifest so we can parse out the installation info as required.
            $wgAppInfo = [ordered]@{ Manifest = & $Script:CommandTable.'Get-ADTWinGetHashMismatchManifest' -Id $wgPackage.Id -Version $wgPackage.Version }
            $wgAppInfo.Add('Installer', (& $Script:CommandTable.'Get-ADTWinGetHashMismatchInstaller' @gawgaiParams -Manifest $wgAppInfo.Manifest))
            $wgAppInfo.Add('FilePath', (& $Script:CommandTable.'Get-ADTWinGetHashMismatchDownload' -Installer $wgAppInfo.Installer))

            # Set up arguments to pass to Start-Process.
            $spParams = @{
                WorkingDirectory = $ExecutionContext.SessionState.Path.CurrentLocation.Path
                ArgumentList = & $Script:CommandTable.'Get-ADTWinGetHashMismatchArgumentList' @wgAppInfo -LogFile $PSBoundParameters.Log
                FilePath = $(if ($wgAppInfo.FilePath.EndsWith('msi')) { 'msiexec.exe' } else { $wgAppInfo.FilePath })
                PassThru = $true
                Wait = $true
            }

            # Commence installation and test the resulting exit code for success.
            $wingetOutput = $(
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Starting package install..." -PassThru
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Executing [$($spParams.FilePath) $($spParams.ArgumentList)]" -PassThru
                if ((& $Script:CommandTable.'Get-ADTWinGetHashMismatchExitCodes' @wgAppInfo) -notcontains ($Global:LASTEXITCODE = (& $Script:CommandTable.'Start-Process' @spParams).ExitCode))
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Uninstall failed with exit code: $Global:LASTEXITCODE." -PassThru
                }
                else
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully installed." -PassThru
                }
            )
        }

        # Generate an exception if we received any failure.
        $wingetException = if (($wingetErrLine = $($wingetOutput -match 'exit code: \d+')))
        {
            [System.Runtime.InteropServices.ExternalException]::new($wingetErrLine, [System.Int32]($wingetErrLine -replace '^.+:\s(\d+)\.$', '$1'))
        }
        elseif ($Global:LASTEXITCODE)
        {
            # All this bullshit is to change crap like '0x800704c7 : unknown error.' to 'Unknown error.'...
            $wgErrorDef = if ([System.Enum]::IsDefined([ADTWinGetExitCode], $Global:LASTEXITCODE)) { [ADTWinGetExitCode]$Global:LASTEXITCODE }
            $wgErrorMsg = [System.Text.RegularExpressions.Regex]::Replace($wingetOutput[-1], '^0x\w{8}\s:\s(\w)', { $args[0].Groups[1].Value.ToUpper() })
            [System.Runtime.InteropServices.ExternalException]::new("WinGet operation finished with exit code 0x$($Global:LASTEXITCODE.ToString('X'))$(if ($wgErrorDef) {" ($wgErrorDef)"}) [$($wgErrorMsg.TrimEnd('.'))].", $Global:LASTEXITCODE)
        }

        # Calculate the exit code of the deployment operation.
        $wingetExitCode = if ($wingetException)
        {
            $wingetException.ErrorCode
        }
        else
        {
            $Global:LASTEXITCODE
        }

        # Update the session's exit code if one's in play.
        if ($adtSession)
        {
            $adtSession.SetExitCode($wingetExitCode)
        }

        # The WinGet cmdlets don't throw on install failure, but we need to within PSADT. Try
        # to give as much information as we can to the caller, including installer exit code.
        if ($wingetException)
        {
            # Throw our determined exception out to the caller to handle.
            $naerParams = @{
                Exception = $wingetException
                Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                ErrorId = "WinGetPackage$([System.Globalization.CultureInfo]::CurrentUICulture.TextInfo.ToTitleCase($Action))Failure"
                TargetObject = $wingetOutput
                RecommendedAction = "Please review the exit code, then try again."
            }
            $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
        }

        # Construct the return object to the caller. We try to closely match WinGet's output as possible.
        # According to WinGet: The Windows Package Manager does not support the reboot behavior currently.
        # https://github.com/microsoft/winget-pkgs/blob/master/doc/manifest/schema/1.6.0/installer.md
        return [pscustomobject]@{
            Id = $wgPackage.Id
            Name = $wgPackage.Name
            Source = $PSBoundParameters.Source
            CorrelationData = [System.String]::Empty
            ExtendedErrorCode = $wingetException
            RebootRequired = $Global:LASTEXITCODE.Equals(1641) -or ($Global:LASTEXITCODE.Equals(3010))
            Status = if ($wingetException) { "$($Action)Error" } else { 'Ok' }
            "$($actionTranslator.$Action)ErrorCode" = $wingetExitCode
        }
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Invoke-ADTWinGetQueryOperation
#
#-----------------------------------------------------------------------------

function Invoke-ADTWinGetQueryOperation
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('list', 'search')]
        [System.String]$Action,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Command,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.UInt32]$Count,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Tag
    )

    # Try to get the path to WinGet before proceeding.
    try
    {
        $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    # Confirm the validity of the provided source.
    if ($PSBoundParameters.ContainsKey('Source'))
    {
        try
        {
            $null = & $Script:CommandTable.'Get-ADTWinGetSource' -Name $PSBoundParameters.Source
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    # Set up arguments array for WinGet.
    $wingetArgs = $(
        $Action
        $PSBoundParameters | & $Script:CommandTable.'Convert-ADTFunctionParamsToArgArray' -Preset WinGet -Exclude Action, MatchOption
        if ($MatchOption -eq 'Equals') { '--exact' }
        '--accept-source-agreements'
    )

    # Invoke WinGet and return early if we couldn't find a package.
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Finding packages matching input criteria, please wait..."
    if (($wingetOutput = & $wingetPath $wingetArgs 2>&1 | & { process { if ($_ -match '^\w+') { return $_.Trim() } } }) -match '^No.+package found matching input criteria\.$')
    {
        & $Script:CommandTable.'Write-ADTLogEntry' -Message "No package found matching input criteria."
        return
    }

    # Convert the cached output to proper PowerShell objects.
    $wingetObjects = & $Script:CommandTable.'Convert-ADTWinGetQueryOutput' -WinGetOutput $wingetOutput
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found $(($wingetObjCount = ($wingetObjects | & $Script:CommandTable.'Measure-Object').Count)) package$(if (!$wingetObjCount.Equals(1)) { 's' }) matching input criteria."
    return $wingetObjects
}


#-----------------------------------------------------------------------------
#
# MARK: Repair-ADTWinGetDesktopAppInstaller
#
#-----------------------------------------------------------------------------

function Repair-ADTWinGetDesktopAppInstaller
{
    # Update WinGet to the latest version. Don't rely in 3rd party store API services for this.
    # https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget-on-windows-sandbox
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installing/updating $(($pkgName = "Microsoft.DesktopAppInstaller")) dependency, please wait..."

    # Define installation file info.
    $packages = @(
        @{
            Name = 'C++ Desktop Bridge Runtime dependency'
            Uri = ($uri = [System.Uri]'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx')
            FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])"
        }
        @{
            Name = 'Windows UI Library dependency'
            Uri = ($uri = [System.Uri]'https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx')
            FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])"
        }
        @{
            Name = 'latest WinGet msixbundle'
            Uri = ($uri = & $Script:CommandTable.'Get-ADTRedirectedUri' -Uri 'https://aka.ms/getwinget')
            FilePath = "$([System.IO.Path]::GetTempPath())$($uri.Segments[-1])"
        }
    )

    # Download all packages.
    foreach ($package in $packages)
    {
        & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$($package.Name)], please wait..."
        & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $package.Uri -OutFile $package.FilePath
    }

    # Set the log file path.
    $logFile = if (& $Script:CommandTable.'Test-ADTSessionActive')
    {
        "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$((& $Script:CommandTable.'Get-ADTSession').InstallName)_Dism.log"
    }
    else
    {
        "$([System.IO.Path]::GetFileNameWithoutExtension($packages[(-1)].FilePath)).log"
    }

    # Pre-provision package in the system.
    $aappParams = @{
        Online = $true
        SkipLicense = $true
        PackagePath = $packages[(-1)].FilePath
        DependencyPackagePath = $packages[(0)..($packages.Count - 2)].FilePath
        LogPath = $logFile
    }
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Pre-provisioning [$pkgName] $($packages[-1].Uri.Segments[-2].Trim('/')), please wait..."
    $null = & $Script:CommandTable.'Add-AppxProvisionedPackage' @aappParams
}


#-----------------------------------------------------------------------------
#
# MARK: Repair-ADTWinGetVisualStudioRuntime
#
#-----------------------------------------------------------------------------

function Repair-ADTWinGetVisualStudioRuntime
{
    # Set required variables for install operation.
    $pkgArch = @('x86', 'x64')[[System.Environment]::Is64BitProcess]
    $pkgName = "Microsoft Visual C++ 2015-2022 Redistributable ($pkgArch)"
    $uriPath = "https://aka.ms/vs/17/release/vc_redist.$pkgArch.exe"
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Preparing $pkgName dependency, please wait..."

    # Get the active ADT session object for log naming, if available.
    $adtSession = if (& $Script:CommandTable.'Test-ADTSessionActive')
    {
        & $Script:CommandTable.'Get-ADTSession'
    }

    # Set up the filename for the download.
    $fileName = & $Script:CommandTable.'Get-Random'

    # Set the log filename.
    $logFile = if ($adtSession)
    {
        "$((& $Script:CommandTable.'Get-ADTConfig').Toolkit.LogPath)\$($adtSession.InstallName)_MSVCRT.log"
    }
    else
    {
        "$([System.IO.Path]::GetTempPath())$fileName.log"
    }

    # Define arguments for installation.
    $spParams = @{
        FilePath = "$([System.IO.Path]::GetTempPath())$fileName.exe"
        ArgumentList = "/install", "/quiet", "/norestart", "/log `"$logFile`""
    }

    # Download and extract installer.
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Downloading [$pkgName], please wait..."
    & $Script:CommandTable.'Invoke-ADTWebDownload' -Uri $uriPath -OutFile $spParams.FilePath

    # Invoke installer and throw if we failed.
    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installing [$pkgName], please wait..."
    if (($exitCode = (& $Script:CommandTable.'Start-Process' @spParams -Wait -PassThru).ExitCode))
    {
        if ($adtSession)
        {
            $adtSession.SetExitCode($exitCode)
        }
        $naerParams = @{
            Exception = [System.Runtime.InteropServices.ExternalException]::new("The installation of [$pkgName] failed with exit code [$exitCode].", $exitCode)
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'VcRedistInstallFailure'
            TargetObject = $exitCode
            RecommendedAction = "Please review the exit code, then try again."
        }
        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Assert-ADTWinGetPackageManager
#
#-----------------------------------------------------------------------------

function Assert-ADTWinGetPackageManager
{
    <#
    .SYNOPSIS
        Verifies that WinGet is installed properly.
 
    .DESCRIPTION
        Verifies that WinGet is installed properly.
 
        Note: The cmdlet doesn't ensure that the latest version of WinGet is installed. It just verifies that the installed version of Winget is supported by installed version of this module.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        None
 
        This function does not return any output.
 
    .EXAMPLE
        Assert-ADTWinGetPackageManager
 
        If the current version of WinGet is installed correctly, the command returns without error.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
    )

    # Try to get the WinGet version.
    try
    {
        [System.Version]$wingetVer = (& $Script:CommandTable.'Get-ADTWinGetVersion' -InformationAction SilentlyContinue).Trim('v')
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    # Test that the retrieved version is greater than or equal to our minimum.
    if ($wingetVer -lt $Script:ADT.WinGetMinVersion)
    {
        $naerParams = @{
            Exception = [System.Activities.VersionMismatchException]::new("The installed WinGet version of [$wingetVer] is less than [$($Script:ADT.WinGetMinVersion)].", [System.Activities.WorkflowIdentity]::new('winget.exe', $wingetVer, $wingetPath.FullName), [System.Activities.WorkflowIdentity]::new('winget.exe', $Script:ADT.WinGetMinVersion, $wingetPath.FullName))
            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
            ErrorId = 'WinGetMinimumVersionError'
            RecommendedAction = "Please run [Repair-ADTWinGetPackageManager] as an admin, then try again."
        }
        $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Find-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Find-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Searches for packages from configured sources.
 
    .DESCRIPTION
        Searches for packages from configured sources.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER Command
        Specify the name of the command defined in the package manifest.
 
    .PARAMETER Count
        Limits the number of items returned by the command.
 
    .PARAMETER Id
        Specify the package identifier for the package you want to list.
 
    .PARAMETER Moniker
        Specify the moniker of the package you want to list.
 
    .PARAMETER Name
        Specify the name of the package to list.
 
    .PARAMETER Source
        Specify the name of the WinGet source to search. The most common sources are `msstore` and `winget`.
 
    .PARAMETER Tag
        Specify a package tag to search for.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        None
 
        This function does not return any output.
 
    .EXAMPLE
        Find-WinGetPackage -Id Microsoft.PowerShell
 
        This example shows how to search for packages by package identifier. By default, the command searches all configured sources. The command performs a case-insensitive substring match against the PackageIdentifier property of the packages.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Command,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.UInt32]$Count,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Tag
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Throw if at least one filtration method isn't provided.
        if (!($PSBoundParameters.Keys -match '^(Query|Command|Id|Moniker|Name|Tag)$'))
        {
            $naerParams = @{
                Exception = [System.ArgumentException]::new("At least one search parameter must be provided to this function.")
                Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
                ErrorId = "WinGetPackageSearchError"
                TargetObject = $PSBoundParameters
                RecommendedAction = "Please review the specified parameters, then try again."
            }
            $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTErrorRecord' @naerParams))
        }
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                if (!($wgPackage = & $Script:CommandTable.'Invoke-ADTWinGetQueryOperation' -Action Search @PSBoundParameters))
                {
                    $naerParams = @{
                        Exception = [System.IO.InvalidDataException]::new("No packages matched the given input criteria.")
                        Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                        ErrorId = "WinGetPackageNotFoundError"
                        TargetObject = $PSBoundParameters
                        RecommendedAction = "Please review the specified input, then try again."
                    }
                    throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                }
                return $wgPackage
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to find the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Lists installed packages.
 
    .DESCRIPTION
        This command lists all of the packages installed on your system. The output includes packages installed from WinGet sources and packages installed by other methods. Packages that have package identifiers starting with `MSIX` or `ARP` could not be correlated to a WinGet source.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER Command
        Specify the name of the command defined in the package manifest.
 
    .PARAMETER Count
        Limits the number of items returned by the command.
 
    .PARAMETER Id
        Specify the package identifier for the package you want to list.
 
    .PARAMETER Moniker
        Specify the moniker of the package you want to list.
 
    .PARAMETER Name
        Specify the name of the package to list.
 
    .PARAMETER Source
        Specify the name of the WinGet source of the package.
 
    .PARAMETER Tag
        Specify a package tag to search for.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        None
 
        This function does not return any output.
 
    .EXAMPLE
        Get-ADTWinGetPackage
 
        This example shows how to list all packages installed on your system.
 
    .EXAMPLE
        Get-ADTWinGetPackage -Id "Microsoft.PowerShell"
 
        This example shows how to get an installed package by its package identifier.
 
    .EXAMPLE
        Get-ADTWinGetPackage -Name "PowerShell"
 
        This example shows how to get installed packages that match a name value. The command does a substring comparison of the provided name with installed package names.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Command,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.UInt32]$Count,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Tag
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                return (& $Script:CommandTable.'Invoke-ADTWinGetQueryOperation' -Action List @PSBoundParameters)
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetSource
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetSource
{
    <#
    .SYNOPSIS
        Lists configured WinGet sources.
 
    .DESCRIPTION
        Lists the configured WinGet sources.
 
    .PARAMETER Name
        Lists the configured WinGet sources.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        PSObject
 
        This function returns one or more objects for each WinGet source.
 
    .EXAMPLE
        Get-ADTWinGetSource
 
        Lists all configured WinGet sources.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Try to get the path to WinGet before proceeding.
        try
        {
            $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    process
    {
        try
        {
            try
            {
                # Get all sources, returning early if there's none (1:1 API with `Get-WinGetSource`).
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Getting list of WinGet sources, please wait..."
                if (($wgSrcRes = & $wingetPath source list 2>&1).Equals('There are no sources configured.'))
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "There are no WinGet sources configured on this system."
                    return
                }

                # Convert the results into proper PowerShell data.
                $wgSrcObjs = & $Script:CommandTable.'Convert-ADTWinGetQueryOutput' -WinGetOutput $wgSrcRes

                # Filter by the name if specified.
                if ($PSBoundParameters.ContainsKey('Name'))
                {
                    if (!($wgSrcObj = $wgSrcObjs | & { process { if ($_.Name -eq $Name) { return $_ } } } | & $Script:CommandTable.'Select-Object' -First 1))
                    {
                        $naerParams = @{
                            Exception = [System.ArgumentException]::new("No source found matching the given value [$Name].")
                            Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
                            ErrorId = 'WinGetSourceNotFoundFailure'
                            TargetObject = $wgSrcObjs
                            RecommendedAction = "Please review the configured sources, then try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found WinGet source [$Name]."
                    return $wgSrcObj
                }
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Found $(($wgSrcObjCount = ($wgSrcObjs | & $Script:CommandTable.'Measure-Object').Count)) WinGet source$(if (!$wgSrcObjCount.Equals(1)) { 's' }): ['$([System.String]::Join("', '", $wgSrcObjs.Name))']."
                return $wgSrcObjs
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the specified WinGet source(s)."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Get-ADTWinGetVersion
#
#-----------------------------------------------------------------------------

function Get-ADTWinGetVersion
{
    <#
    .SYNOPSIS
        Gets the installed version of WinGet.
 
    .DESCRIPTION
        Gets the installed version of WinGet.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        System.String
 
        This function returns the installed WinGet's version number as a string.
 
    .EXAMPLE
        Get-ADTWinGetVersion -All
 
        Gets the installed version of WinGet.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Try to get the path to WinGet before proceeding.
        try
        {
            $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    process
    {
        try
        {
            try
            {
                # Get the WinGet version and return it to the caller. The API here 1:1 matches WinGet's PowerShell module, rightly or wrongly.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Running [$wingetPath] with [--version] parameter."
                $wingetVer = & $wingetPath --version

                # If we've got a null string, we're probably missing the Visual Studio Runtime or something.
                if ([System.String]::IsNullOrWhiteSpace($wingetVer))
                {
                    $naerParams = @{
                        Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run.")
                        Category = [System.Management.Automation.ErrorCategory]::PermissionDenied
                        ErrorId = 'WinGetNullOutputError'
                        RecommendedAction = "Please run [Repair-ADTWinGetPackageManager] as an admin, then try again."
                    }
                    throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                }
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Installed WinGet version is [$($wingetVer)]."
                return $wingetVer
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to get the WinGet version."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Install-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Install-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Installs a WinGet Package.
 
    .DESCRIPTION
        This command installs a WinGet package from a configured source. The command includes parameters to specify values used to search for packages in the configured sources. By default, the command searches the winget source. All string-based searches are case-insensitive substring searches. Wildcards are not supported.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER AllowHashMismatch
        Allows you to download package even when the SHA256 hash for an installer or a dependency does not match the SHA256 hash in the WinGet package manifest.
 
    .PARAMETER Architecture
        Specify the processor architecture for the WinGet package installer.
 
    .PARAMETER Custom
        Use this parameter to pass additional arguments to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string is added to the arguments defined in the package manifest.
 
    .PARAMETER Force
        Force the installer to run even when other checks WinGet would perform would prevent this action.
 
    .PARAMETER Header
        Custom value to be passed via HTTP header to WinGet REST sources.
 
    .PARAMETER Id
        Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match.
 
    .PARAMETER InstallerType
        A package may contain multiple installer types.
 
    .PARAMETER Locale
        Specify the locale of the installer package. The locale must provided in the BCP 47 format, such as `en-US`. For more information, see Standard locale names (/globalization/locale/standard-locale-names).
 
    .PARAMETER Location
        Specify the file path where you want the packed to be installed. The installer must be able to support alternate install locations.
 
    .PARAMETER Log
        Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`.
 
    .PARAMETER Mode
        Specify the output mode for the installer.
 
    .PARAMETER Moniker
        Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`.
 
    .PARAMETER Name
        Specify the name of the package to be installed.
 
    .PARAMETER Override
        Use this parameter to override the existing arguments passed to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string overrides the arguments specified in the package manifest.
 
    .PARAMETER Scope
        Specify WinGet package installer scope.
 
    .PARAMETER SkipDependencies
        Specifies that the command shouldn't install the WinGet package dependencies.
 
    .PARAMETER Source
        Specify the name of the WinGet source from which the package should be installed.
 
    .PARAMETER Version
        Specify the version of the package.
 
    .PARAMETER DebugHashMismatch
        Forces the AllowHashMismatch for debugging purposes.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        PSObject
 
        This function returns a PSObject containing the outcome of the operation.
 
    .EXAMPLE
        Install-WinGetPackage -Id Microsoft.PowerShell
 
        This example shows how to install a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package. If more than one source is configured with the same package identifier, the user must disambiguate.
 
    .EXAMPLE
        Install-WinGetPackage -Name "PowerToys (Preview)"
 
        This example shows how to install a package by specifying the package name.
 
    .EXAMPLE
        Install-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0
 
        This example shows how to install a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$AllowHashMismatch,

        [Parameter(Mandatory = $false)]
        [ValidateSet('x86', 'x64', 'arm64')]
        [System.String]$Architecture,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Custom,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$Force,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Header,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable')]
        [System.String]$InstallerType,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Locale,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Location,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Log,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Silent', 'Interactive')]
        [System.String]$Mode,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Override,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown')]
        [System.String]$Scope,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$SkipDependencies,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Version,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$DebugHashMismatch
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Install @PSBoundParameters)
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to install the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Invoke-ADTWinGetOperation
#
#-----------------------------------------------------------------------------

function Invoke-ADTWinGetOperation
{
    <#
    .SYNOPSIS
        PSAppDeployToolkit - This script performs the installation or uninstallation of an application(s).
 
    .DESCRIPTION
        - The script is provided as a template to perform an install, uninstall, or repair of an application(s).
        - The script either performs an "Install", "Uninstall", or "Repair" deployment type.
        - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install.
 
        The script imports the PSAppDeployToolkit module which contains the logic and functions required to install or uninstall an application.
 
    .PARAMETER Id
        The WinGet package identifier for the deployment.
 
    .PARAMETER DeploymentType
        The type of deployment to perform. Default is: Install.
 
    .PARAMETER DeployMode
        Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive.
 
    .PARAMETER AllowRebootPassThru
        Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered.
 
    .EXAMPLE
        powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeployMode Silent
 
    .EXAMPLE
        powershell.exe -File Invoke-AppDeployToolkit.ps1 -AllowRebootPassThru
 
    .EXAMPLE
        powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeploymentType Uninstall
 
    .EXAMPLE
        Invoke-AppDeployToolkit.exe -DeploymentType "Install" -DeployMode "Silent"
 
    .INPUTS
        None. You cannot pipe objects to this script.
 
    .OUTPUTS
        None. This script does not generate any output.
 
    .LINK
        https://psappdeploytoolkit.com
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Install', 'Uninstall', 'Repair')]
        [System.String]$DeploymentType = 'Install',

        [Parameter(Mandatory = $false)]
        [ValidateSet('Interactive', 'Silent', 'NonInteractive')]
        [System.String]$DeployMode = 'Interactive',

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$AllowRebootPassThru
    )


    ##================================================
    ## MARK: Pre-initialization
    ##================================================

    # Set strict error handling across entire operation.
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
    & $Script:CommandTable.'Set-StrictMode' -Version 3
    $mainError = $null

    # Confirm WinGet is healthy, then try to find the specified package.
    try
    {
        & $Script:CommandTable.'Assert-ADTWinGetPackageManager'
    }
    catch
    {
        try
        {
            & $Script:CommandTable.'Invoke-ADTWinGetRepair'
            & $Script:CommandTable.'Assert-ADTWinGetPackageManager'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    # Try to find the specified package.
    try
    {
        $wgPackage = & $Script:CommandTable.'Find-ADTWinGetPackage' -Id $Id
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }


    ##================================================
    ## MARK: Variables
    ##================================================

    $adtSession = @{
        # App variables.
        AppName = ($wgPackage.Name -replace ([regex]::Escape($wgPackage.Version))).Trim()
        AppVersion = $wgPackage.Version

        # Script variables.
        DeployAppScriptFriendlyName = $MyInvocation.MyCommand.Name
        DeployAppScriptVersion = $MyInvocation.MyCommand.Module.Version
        DeployAppScriptParameters = $PSBoundParameters
    }

    function Install-ADTDeployment
    {
        ##================================================
        ## MARK: Pre-Install
        ##================================================
        $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"

        ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt.
        & $Script:CommandTable.'Show-ADTInstallationWelcome' -AllowDefer -DeferTimes 3 -CheckDiskSpace -PersistPrompt -NoMinimizeWindows
        if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break }

        ## Show Progress Message (with the default message).
        & $Script:CommandTable.'Show-ADTInstallationProgress'


        ##================================================
        ## MARK: Install
        ##================================================
        $adtSession.InstallPhase = $adtSession.DeploymentType

        ## Install our WinGet package.
        $null = & $Script:CommandTable.'Install-ADTWinGetPackage' -Id $Id


        ##================================================
        ## MARK: Post-Install
        ##================================================
        $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
    }

    function Uninstall-ADTDeployment
    {
        ##================================================
        ## MARK: Pre-Uninstall
        ##================================================
        $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"

        ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing.
        & $Script:CommandTable.'Show-ADTInstallationWelcome' -CloseProcessesCountdown 60 -NoMinimizeWindows
        if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break }

        ## Show Progress Message (with the default message).
        & $Script:CommandTable.'Show-ADTInstallationProgress'


        ##================================================
        ## MARK: Uninstall
        ##================================================
        $adtSession.InstallPhase = $adtSession.DeploymentType

        ## Uninstall our WinGet package.
        $null = & $Script:CommandTable.'Uninstall-ADTWinGetPackage' -Id $Id


        ##================================================
        ## MARK: Post-Uninstallation
        ##================================================
        $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
    }

    function Repair-ADTDeployment
    {
        ##================================================
        ## MARK: Pre-Repair
        ##================================================
        $adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"

        ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing.
        & $Script:CommandTable.'Show-ADTInstallationWelcome' -CloseProcessesCountdown 60 -NoMinimizeWindows
        if ($adtSession.GetExitCode().Equals(60012)) { $Global:LASTEXITCODE = 60012; break }

        ## Show Progress Message (with the default message).
        & $Script:CommandTable.'Show-ADTInstallationProgress'


        ##================================================
        ## MARK: Repair
        ##================================================
        $adtSession.InstallPhase = $adtSession.DeploymentType

        ## Repair our WinGet package.
        $null = & $Script:CommandTable.'Repair-ADTWinGetPackage' -Id $Id


        ##================================================
        ## MARK: Post-Repair
        ##================================================
        $adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
    }


    ##================================================
    ## MARK: Initialization
    ##================================================

    # Import the module and instantiate a new session.
    try
    {
        $adtSession = & $Script:CommandTable.'Open-ADTSession' -SessionState $ExecutionContext.SessionState @adtSession @PSBoundParameters -PassThru
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }


    ##================================================
    ## MARK: Invocation
    ##================================================

    try
    {
        & "$($adtSession.DeploymentType)-ADTDeployment"
        & $Script:CommandTable.'Close-ADTSession'
        $Global:LASTEXITCODE = $adtSession.GetExitCode()
    }
    catch
    {
        & $Script:CommandTable.'Write-ADTLogEntry' -Message ($mainErrorMessage = & $Script:CommandTable.'Resolve-ADTErrorRecord' -ErrorRecord ($mainError = $_)) -Severity 3
        & $Script:CommandTable.'Show-ADTDialogBox' -Text $mainErrorMessage -Icon Stop | & $Script:CommandTable.'Out-Null'
        & $Script:CommandTable.'Close-ADTSession' -ExitCode 60001
        $Global:LASTEXITCODE = 60001
    }
    finally
    {
        if ($mainError -and !([System.Environment]::GetCommandLineArgs() -eq '-NonInteractive'))
        {
            $PSCmdlet.ThrowTerminatingError($mainError)
        }
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Invoke-ADTWinGetRepair
#
#-----------------------------------------------------------------------------

function Invoke-ADTWinGetRepair
{
    <#
    .SYNOPSIS
        PSAppDeployToolkit - This script performs the installation or uninstallation of an application(s).
 
    .DESCRIPTION
        - The script is provided as a template to perform an install, uninstall, or repair of an application(s).
        - The script either performs an "Install", "Uninstall", or "Repair" deployment type.
        - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install.
 
        The script imports the PSAppDeployToolkit module which contains the logic and functions required to install or uninstall an application.
 
    .PARAMETER AllowRebootPassThru
        Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered.
 
    .EXAMPLE
        powershell.exe -File Invoke-AppDeployToolkit.ps1 -AllowRebootPassThru
 
    .INPUTS
        None. You cannot pipe objects to this script.
 
    .OUTPUTS
        None. This script does not generate any output.
 
    .LINK
        https://psappdeploytoolkit.com
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$AllowRebootPassThru
    )


    ##================================================
    ## MARK: Variables
    ##================================================

    $adtSession = @{
        # App variables.
        AppName = "$($MyInvocation.MyCommand.Module.Name) Repair Operation"

        # Script variables.
        DeployAppScriptFriendlyName = $MyInvocation.MyCommand.Name
        DeployAppScriptVersion = $MyInvocation.MyCommand.Module.Version
        DeployAppScriptParameters = $PSBoundParameters

        # Script parameters.
        DeploymentType = 'Repair'
        DeployMode = 'Silent'
    }


    ##================================================
    ## MARK: Initialization
    ##================================================

    # Set strict error handling across entire operation.
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
    & $Script:CommandTable.'Set-StrictMode' -Version 3
    $mainError = $null

    # Import the module and instantiate a new session.
    try
    {
        $adtSession = & $Script:CommandTable.'Open-ADTSession' -SessionState $ExecutionContext.SessionState @adtSession @PSBoundParameters -PassThru
        $adtSession.InstallPhase = $adtSession.DeploymentType
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }


    ##================================================
    ## MARK: Invocation
    ##================================================

    try
    {
        & $Script:CommandTable.'Repair-ADTWinGetPackageManager'
        & $Script:CommandTable.'Close-ADTSession'
        $Global:LASTEXITCODE = $adtSession.GetExitCode()
    }
    catch
    {
        & $Script:CommandTable.'Write-ADTLogEntry' -Message (& $Script:CommandTable.'Resolve-ADTErrorRecord' -ErrorRecord ($mainError = $_)) -Severity 3
        & $Script:CommandTable.'Close-ADTSession' -ExitCode 60001 -Force:(!(& $Script:CommandTable.'Get-PSCallStack').Command.Equals('Invoke-ADTWinGetOperation'))
        $Global:LASTEXITCODE = 60001
    }
    finally
    {
        if ($mainError -and !([System.Environment]::GetCommandLineArgs() -eq '-NonInteractive'))
        {
            $PSCmdlet.ThrowTerminatingError($mainError)
        }
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Repair-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Repair-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Repairs a WinGet Package.
 
    .DESCRIPTION
        This command repairs a WinGet package from your computer, provided the package includes repair support. The command includes parameters to specify values used to search for installed packages. By default, all string-based searches are case-insensitive substring searches. Wildcards are not supported.
 
        Note: Not all packages support repair.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER Id
        Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match.
 
    .PARAMETER Log
        Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`.
 
    .PARAMETER Mode
        Specify the output mode for the installer.
 
    .PARAMETER Moniker
        Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`.
 
    .PARAMETER Name
        Specify the name of the package to be installed.
 
    .PARAMETER Source
        Specify the name of the WinGet source from which the package should be installed.
 
    .PARAMETER Version
        Specify the version of the package.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        PSObject
 
        This function returns a PSObject containing the outcome of the operation.
 
    .EXAMPLE
        Repair-WinGetPackage -Id "Microsoft.GDK.2406"
 
        This example shows how to repair a package by specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package.
 
    .EXAMPLE
        Repair-WinGetPackage -Name "Microsoft Game Development Kit - 240602 (June 2024 Update 2)"
 
        This example shows how to repair a package using the package name. Please note that the examples mentioned above are mainly reference examples for the repair cmdlet and may not be operational as is, since many installers don't support repair as a standard functionality. For the Microsoft.GDK.2406 example, the assumption is that Microsoft.GDK.2406 supports repair capability and the author of the installer has provided the necessary repair context/switches in the Package Manifest in the Package Source referenced by the WinGet Client.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Log,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Silent', 'Interactive')]
        [System.String]$Mode,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Version
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Repair @PSBoundParameters)
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Repair-ADTWinGetPackageManager
#
#-----------------------------------------------------------------------------

function Repair-ADTWinGetPackageManager
{
    <#
    .SYNOPSIS
        Repairs the installation of the WinGet client on your computer.
 
    .DESCRIPTION
        This command repairs the installation of the WinGet client on your computer by installing the specified version or the latest version of the client. This command can also install the WinGet client if it is not already installed on your machine. It ensures that the client is installed in a working state.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        None
 
        This function does not return any output.
 
    .EXAMPLE
        Repair-ADTWinGetPackageManager
 
        This example shows how to repair they WinGet client by installing the latest version and ensuring it functions properly.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Test whether WinGet is installed and available at all.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Confirming whether [Microsoft.DesktopAppInstaller] is installed, please wait..."
                if (!($wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath') -or !$wingetPath.Exists)
                {
                    # Throw if we're not admin.
                    if (!$Script:ADT.RunningAsAdmin)
                    {
                        $naerParams = @{
                            Exception = [System.UnauthorizedAccessException]::new("WinGet is not installed. Please install [Microsoft.DesktopAppInstaller] and try again.")
                            Category = [System.Management.Automation.ErrorCategory]::PermissionDenied
                            ErrorId = 'MicrosoftDesktopAppInstallerCannotInstallFailure'
                            RecommendedAction = "Please install [Microsoft.DesktopAppInstaller] as an admin, then try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }

                    # Install Microsoft.DesktopAppInstaller.
                    & $Script:CommandTable.'Repair-ADTWinGetDesktopAppInstaller'

                    # Throw if the installation was successful but we still don't have WinGet.
                    if (!($wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath') -or !$wingetPath.Exists)
                    {
                        $naerParams = @{
                            Exception = [System.InvalidOperationException]::new("Failed to get a valid WinGet path after successfully pre-provisioning the app. Please report this issue for further analysis.")
                            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                            ErrorId = 'MicrosoftDesktopAppInstallerMissingFailure'
                            RecommendedAction = "Please report this issue to the project's maintainer for further analysis."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }
                }
                else
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed that [Microsoft.DesktopAppInstaller] is installed on system."
                }

                # Test whether we have any output from winget.exe. If this is null, it typically means the appropriate MSVC++ runtime is not installed.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Testing whether [Microsoft Visual C++ 2015-2022 Runtime] is installed, please wait..."
                if (!(& $wingetPath))
                {
                    # Throw if we're not admin.
                    if (!$Script:ADT.RunningAsAdmin)
                    {
                        $naerParams = @{
                            Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run. Please ensure the latest [Microsoft Visual C++ 2015-2022 Runtime] is installed and try again.")
                            Category = [System.Management.Automation.ErrorCategory]::PermissionDenied
                            ErrorId = 'VcRedistCannotInstallFailure'
                            RecommendedAction = "Please install the latest [Microsoft Visual C++ 2015-2022 Runtime] as an admin, then try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }

                    # Install MSVCRT onto device.
                    & $Script:CommandTable.'Repair-ADTWinGetVisualStudioRuntime'

                    # Throw if we're still not able to run WinGet.
                    if (!(& $wingetPath))
                    {
                        $naerParams = @{
                            Exception = [System.InvalidOperationException]::new("The installed version of WinGet was unable to run. This is possibly related to a missing [Microsoft Visual C++ 2015-2022 Runtime] library.")
                            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                            ErrorId = 'MicrosoftDesktopAppInstallerExecutionFailure'
                            RecommendedAction = "Please verify that WinGet.exe can run on this system, then try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }
                }
                else
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed that [Microsoft Visual C++ 2015-2022 Runtime] is installed on system."
                }

                # Ensure winget.exe is above the minimum version.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Testing whether the installed WinGet is version [$($Script:ADT.WinGetMinVersion)] or higher, please wait..."
                if (([System.Version]$wingetVer = (& $Script:CommandTable.'Get-ADTWinGetVersion' -InformationAction SilentlyContinue).Trim('v')) -lt $Script:ADT.WinGetMinVersion)
                {
                    # Throw if we're not admin.
                    if (!$Script:ADT.RunningAsAdmin)
                    {
                        $naerParams = @{
                            Exception = [System.Activities.VersionMismatchException]::new("The installed WinGet version of [$wingetVer] is less than [$($Script:ADT.WinGetMinVersion)]. Please update [Microsoft.DesktopAppInstaller] and try again.", [System.Activities.WorkflowIdentity]::new('winget.exe', $wingetVer, $wingetPath.FullName), [System.Activities.WorkflowIdentity]::new('winget.exe', $Script:ADT.WinGetMinVersion, $wingetPath.FullName))
                            Category = [System.Management.Automation.ErrorCategory]::PermissionDenied
                            ErrorId = 'VcRedistCannotInstallFailure'
                            RecommendedAction = "Please update [Microsoft.DesktopAppInstaller] as an admin, then try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }

                    # Install the missing dependency and reset variables.
                    & $Script:CommandTable.'Repair-ADTWinGetDesktopAppInstaller'
                    $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'

                    # Ensure winget.exe is above the minimum version.
                    & $Script:CommandTable.'Assert-ADTWinGetPackageManager'

                    # Reset WinGet sources after updating. Helps with a corner-case issue discovered.
                    & $Script:CommandTable.'Reset-ADTWinGetSource' -All
                }
                else
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully confirmed WinGet version [$wingetVer] is installed on system."
                }
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the WinGet package manager."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Reset-ADTWinGetSource
#
#-----------------------------------------------------------------------------

function Reset-ADTWinGetSource
{
    <#
    .SYNOPSIS
        Resets WinGet sources.
 
    .DESCRIPTION
        Resets a named WinGet source by removing the source configuration. You can reset all configured sources and add the default source configurations using the All switch parameter. This command must be executed with administrator permissions.
 
    .PARAMETER Name
        The name of the source.
 
    .PARAMETER All
        Reset all sources and add the default sources.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        None
 
        This function does not return any output.
 
    .EXAMPLE
        Reset-ADTWinGetSource -Name msstore
 
        This example resets the configured source named 'msstore' by removing it.
 
    .EXAMPLE
        Reset-ADTWinGetSource -All
 
        This example resets all configured sources and adds the default sources.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'All')]
        [System.Management.Automation.SwitchParameter]$All
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        # Try to get the path to WinGet before proceeding.
        try
        {
            $wingetPath = & $Script:CommandTable.'Get-ADTWinGetPath'
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }

    process
    {
        try
        {
            try
            {
                # Reset all sources if specified.
                if ($All)
                {
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resetting all WinGet sources, please wait..."
                    if (!($wgSrcRes = & $wingetPath source reset --force 2>&1).Equals('Resetting all sources...Done'))
                    {
                        $naerParams = @{
                            Exception = [System.Runtime.InteropServices.ExternalException]::new("Failed to reset all WinGet sources. $($wgSrcRes.TrimEnd('.')).", $Global:LASTEXITCODE)
                            Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                            ErrorId = 'WinGetSourceAllResetFailure'
                            TargetObject = $wgSrcRes
                            RecommendedAction = "Please review the result in this error's TargetObject property and try again."
                        }
                        throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                    }
                    & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully reset all WinGet sources."
                    return
                }

                # Reset the specified source.
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Resetting WinGet source [$Name], please wait..."
                if (!($wgSrcRes = & $wingetPath source reset $Name 2>&1).Equals("Resetting source: $Name...Done"))
                {
                    $naerParams = @{
                        Exception = [System.Runtime.InteropServices.ExternalException]::new("Failed to WinGet source [$Name]. $($wgSrcRes.TrimEnd('.')).", $Global:LASTEXITCODE)
                        Category = [System.Management.Automation.ErrorCategory]::InvalidResult
                        ErrorId = "WinGetNamedSourceResetFailure"
                        TargetObject = $wgSrcRes
                        RecommendedAction = "Please review the result in this error's TargetObject property and try again."
                    }
                    throw (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams)
                }
                & $Script:CommandTable.'Write-ADTLogEntry' -Message "Successfully WinGet source [$Name]."
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to repair the specified WinGet source(s)."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Uninstall-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Uninstall-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Uninstalls a WinGet Package.
 
    .DESCRIPTION
        This command uninstalls a WinGet package from your computer. The command includes parameters to specify values used to search for installed packages. By default, all string-based searches are case-insensitive substring searches. Wildcards are not supported.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER Force
        Force the installer to run even when other checks WinGet would perform would prevent this action.
 
    .PARAMETER Id
        Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match.
 
    .PARAMETER Log
        Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`.
 
    .PARAMETER Mode
        Specify the output mode for the installer.
 
    .PARAMETER Moniker
        Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`.
 
    .PARAMETER Name
        Specify the name of the package to be installed.
 
    .PARAMETER Source
        Specify the name of the WinGet source from which the package should be installed.
 
    .PARAMETER Version
        Specify the version of the package.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        PSObject
 
        This function returns a PSObject containing the outcome of the operation.
 
    .EXAMPLE
        Uninstall-WinGetPackage -Id Microsoft.PowerShell
 
        This example shows how to uninstall a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package.
 
    .EXAMPLE
        Uninstall-WinGetPackage -Name "PowerToys (Preview)"
 
        This sample uninstalls the PowerToys package by the specifying the package name.
 
    .EXAMPLE
        Uninstall-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0
 
        This example shows how to uninstall a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$Force,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Log,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Silent', 'Interactive')]
        [System.String]$Mode,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Version
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Uninstall @PSBoundParameters)
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to uninstall the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Update-ADTWinGetPackage
#
#-----------------------------------------------------------------------------

function Update-ADTWinGetPackage
{
    <#
    .SYNOPSIS
        Installs a newer version of a previously installed WinGet package.
 
    .DESCRIPTION
        This command searches the packages installed on your system and installs a newer version of the matching WinGet package. The command includes parameters to specify values used to search for packages in the configured sources. By default, the command searches the winget source. All string-based searches are case-insensitive substring searches. Wildcards are not supported.
 
    .PARAMETER Query
        Specify one or more strings to search for. By default, the command searches all configured sources.
 
    .PARAMETER MatchOption
        Specify matching logic used for search.
 
    .PARAMETER AllowHashMismatch
        Allows you to download package even when the SHA256 hash for an installer or a dependency does not match the SHA256 hash in the WinGet package manifest.
 
    .PARAMETER Architecture
        Specify the processor architecture for the WinGet package installer.
 
    .PARAMETER Custom
        Use this parameter to pass additional arguments to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string is added to the arguments defined in the package manifest.
 
    .PARAMETER Force
        Force the installer to run even when other checks WinGet would perform would prevent this action.
 
    .PARAMETER Header
        Custom value to be passed via HTTP header to WinGet REST sources.
 
    .PARAMETER Id
        Specify the package identifier to search for. The command does a case-insensitive full text match, rather than a substring match.
 
    .PARAMETER IncludeUnknown
        Use this parameter to upgrade the package when the installed version is not specified in the registry.
 
    .PARAMETER InstallerType
        A package may contain multiple installer types.
 
    .PARAMETER Locale
        Specify the locale of the installer package. The locale must provided in the BCP 47 format, such as `en-US`. For more information, see Standard locale names (/globalization/locale/standard-locale-names).
 
    .PARAMETER Location
        Specify the file path where you want the packed to be installed. The installer must be able to support alternate install locations.
 
    .PARAMETER Log
        Specify the location for the installer log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`.
 
    .PARAMETER Mode
        Specify the output mode for the installer.
 
    .PARAMETER Moniker
        Specify the moniker of the WinGet package to install. For example, the moniker for the Microsoft.PowerShell package is `pwsh`.
 
    .PARAMETER Name
        Specify the name of the package to be installed.
 
    .PARAMETER Override
        Use this parameter to override the existing arguments passed to the installer. The parameter takes a single string value. To add multiple arguments, include the arguments in the string. The arguments must be provided in the format expected by the installer. If the string contains spaces, it must be enclosed in quotes. This string overrides the arguments specified in the package manifest.
 
    .PARAMETER Scope
        Specify WinGet package installer scope.
 
    .PARAMETER SkipDependencies
        Specifies that the command shouldn't install the WinGet package dependencies.
 
    .PARAMETER Source
        Specify the name of the WinGet source from which the package should be installed.
 
    .PARAMETER Version
        Specify the version of the package.
 
    .PARAMETER DebugHashMismatch
        Forces the AllowHashMismatch for debugging purposes.
 
    .INPUTS
        None
 
        You cannot pipe objects to this function.
 
    .OUTPUTS
        PSObject
 
        This function returns a PSObject containing the outcome of the operation.
 
    .EXAMPLE
        Update-WinGetPackage -Id Microsoft.PowerShell
 
        This example shows how to update a package by the specifying the package identifier. If the package identifier is available from more than one source, you must provide additional search criteria to select a specific instance of the package.
 
    .EXAMPLE
        Update-WinGetPackage -Name "PowerToys (Preview)"
 
        This sample updates the PowerToys package by the specifying the package name.
 
    .EXAMPLE
        Update-WinGetPackage Microsoft.PowerShell -Version 7.4.4.0
 
        This example shows how to update a specific version of a package using a query. The command does a query search for packages matching `Microsoft.PowerShell`. The results of the search a limited to matches with the version of `7.4.4.0`.
 
    .LINK
        https://github.com/mjr4077au/PSAppDeployToolkit.WinGet
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Query,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Equals', 'EqualsCaseInsensitive')]
        [System.String]$MatchOption,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$AllowHashMismatch,

        [Parameter(Mandatory = $false)]
        [ValidateSet('x86', 'x64', 'arm64')]
        [System.String]$Architecture,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Custom,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$Force,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Header,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Id,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$IncludeUnknown,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Inno', 'Wix', 'Msi', 'Nullsoft', 'Zip', 'Msix', 'Exe', 'Burn', 'MSStore', 'Portable')]
        [System.String]$InstallerType,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Locale,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Location,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Log,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Silent', 'Interactive')]
        [System.String]$Mode,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Moniker,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Override,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Any', 'User', 'System', 'UserOrUnknown', 'SystemOrUnknown')]
        [System.String]$Scope,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$SkipDependencies,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Source,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Version,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter]$DebugHashMismatch
    )

    begin
    {
        # Initialize function.
        & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process
    {
        try
        {
            try
            {
                # Send this to the backend common function.
                return (& $Script:CommandTable.'Invoke-ADTWinGetDeploymentOperation' -Action Upgrade @PSBoundParameters)
            }
            catch
            {
                # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used.
                & $Script:CommandTable.'Write-Error' -ErrorRecord $_
            }
        }
        catch
        {
            # Process the caught error, log it and throw depending on the specified ErrorAction.
            & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ -LogMessage "Failed to update the specified WinGet package."
        }
    }

    end
    {
        # Finalize function.
        & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet
    }
}


#-----------------------------------------------------------------------------
#
# MARK: Module Constants and Function Exports
#
#-----------------------------------------------------------------------------

# Set all functions as read-only, export all public definitions and finalise the CommandTable.
& $Script:CommandTable.'Set-Item' -LiteralPath $FunctionPaths -Options ReadOnly
& $Script:CommandTable.'Get-Item' -LiteralPath $FunctionPaths | & { process { $CommandTable.Add($_.Name, $_) } }
& $Script:CommandTable.'New-Variable' -Name CommandTable -Value ([System.Collections.ObjectModel.ReadOnlyDictionary[System.String, System.Management.Automation.CommandInfo]]::new($CommandTable)) -Option Constant -Force -Confirm:$false
& $Script:CommandTable.'Export-ModuleMember' -Function $Module.Manifest.FunctionsToExport

# Store module globals needed for the lifetime of the module.
& $Script:CommandTable.'New-Variable' -Name ADT -Option Constant -Value ([pscustomobject]@{
        WinGetMinVersion = [System.Version]::new(1, 7, 10582)
        RunningAsSystem = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.IsWellKnown([System.Security.Principal.WellKnownSidType]::LocalSystemSid)
        RunningAsAdmin = & $Script:CommandTable.'Test-ADTCallerIsAdmin'
        SystemArchitecture = switch ([PSADT.OperatingSystem.OSHelper]::GetArchitecture())
        {
            ([PSADT.Shared.SystemArchitecture]::ARM64)
            {
                'arm64'
                break
            }
            ([PSADT.Shared.SystemArchitecture]::AMD64)
            {
                'x64'
                break
            }
            ([PSADT.Shared.SystemArchitecture]::i386)
            {
                'x86'
                break
            }
            default
            {
                throw [System.Management.Automation.ErrorRecord]::new(
                    [System.InvalidOperationException]::new("The operating system of this computer is of an unsupported architecture."),
                    'WinGetInvalidArchitectureError',
                    [System.Management.Automation.ErrorCategory]::InvalidOperation,
                    $_
                )
            }
        }
    })

# Following the successful import, set the console's output encoding to UTF8 as required by WinGet's command line.
[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# Announce successful importation of module.
& $Script:CommandTable.'Write-ADTLogEntry' -Message "Module [PSAppDeployToolkit.WinGet] imported successfully." -ScriptSection Initialization -Source 'PSAppDeployToolkit.WinGet.psm1'