Microsoft.DotNet.Dsc.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true
Set-StrictMode -Version Latest

#region Functions
function Get-DotNetPath
{
    if ($IsWindows)
    {
        $dotNetPath = "$env:ProgramFiles\dotnet\dotnet.exe"
        if (-not (Test-Path $dotNetPath))
        {
            $dotNetPath = "${env:ProgramFiles(x86)}\dotnet\dotnet.exe"
            if (-not (Test-Path $dotNetPath))
            {
                throw "dotnet.exe not found in Program Files or Program Files (x86)"
            }
        }
    }
    elseif ($IsMacOS)
    {
        $dotNetPath = "/usr/local/share/dotnet/dotnet"
        if (-not (Test-Path $dotNetPath))
        {
            $dotNetPath = "/usr/local/bin/dotnet"
            if (-not (Test-Path $dotNetPath))
            {
                throw "dotnet not found in /usr/local/share/dotnet or /usr/local/bin"
            }
        }
    }
    elseif ($IsLinux)
    {
        $dotNetPath = "/usr/share/dotnet/dotnet"
        if (-not (Test-Path $dotNetPath))
        {
            $dotNetPath = "/usr/bin/dotnet"
            if (-not (Test-Path $dotNetPath))
            {
                throw "dotnet not found in /usr/share/dotnet or /usr/bin"
            }
        }
    }
    else
    {
        throw "Unsupported operating system"
    }

    Write-Verbose -Message "'dotnet' found at $dotNetPath"
    return $dotNetPath
}

function Get-DotNetToolArguments
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $PackageId,
        [Parameter(Mandatory = $false)]
        [string] $Version,
        [Parameter(Mandatory = $false)]
        [bool]   $PreRelease,
        [Parameter(Mandatory = $false)]
        [string] $ToolPathDirectory,
        [bool]   $Exist,
        [switch] $Downgrade
    )

    $arguments = @($PackageId)

    if (-not ($PSBoundParameters.ContainsKey("ToolPathDirectory")))
    {
        $arguments += "--global"
    }

    if ($PSBoundParameters.ContainsKey("Prerelease") -and $PSBoundParameters.ContainsKey("Version"))
    {
        # do it with version instead of pre
        $null = $PSBoundParameters.Remove("Prerelease")
    }

    # mapping table of command line arguments
    $mappingTable = @{
        Version           = "--version {0}"
        PreRelease        = "--prerelease"
        ToolPathDirectory = "--tool-path {0}"
        Downgrade         = '--allow-downgrade'
    }
    
    $PSBoundParameters.GetEnumerator() | ForEach-Object {
        if ($mappingTable.ContainsKey($_.Key)) 
        {
            if ($_.Value -ne $false -and -not (([string]::IsNullOrEmpty($_.Value))))
            {
                $arguments += ($mappingTable[$_.Key] -f $_.Value)
            }
        }
    }

    return ($arguments -join " ")
}

# TODO: when https://github.com/dotnet/sdk/pull/37394 is documented and version is released with option simple use --format=JSON

function Convert-DotNetToolOutput
{
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        [string[]] $Output
    )

    process
    {
        # Split the output into lines
        $lines = $Output | Select-Object -Skip 2

        # Initialize an array to hold the custom objects
        $inputObject = @()

        # Skip the header lines and process each line
        foreach ($line in $lines)
        {
            # Split the line into columns
            $columns = $line -split '\s{2,}'

            # Create a custom object for each line
            $customObject = [PSCustomObject]@{
                PackageId = $columns[0]
                Version   = $columns[1]
                Commands  = $columns[2]
            }

            # Add the custom object to the array
            $inputObject += $customObject
        }

        return $inputObject
    }
}

function Get-InstalledDotNetToolPackages
{
    [CmdletBinding()]
    param (
        [string] $PackageId,
        [string] $Version,
        [bool]   $PreRelease,
        [Parameter(Mandatory = $false)]
        [ValidateScript({
                if (-Not ($_ | Test-Path -PathType Container) )
                {
                    throw "Directory does not exist" 
                }
                return $true
            })]
        [string] $ToolPathDirectory,
        [bool]   $Exist
    )

    $resultSet = [System.Collections.Generic.List[DotNetToolPackage]]::new()
    $listCommand = "tool list --global"
    $installDir = Join-Path -Path $env:USERPROFILE '.dotnet' 'tools'

    if ($PSBoundParameters.ContainsKey('ToolPathDirectory'))
    {
        $listCommand = "tool list --tool-path $ToolPathDirectory"
        $installDir = $ToolPathDirectory
    }

    $result = Invoke-DotNet -Command $listCommand
    $packages = Convert-DotNetToolOutput -Output $result

    if ($null -eq $packages)
    {
        Write-Debug -Message "No packages found."
        return
    }

    if (-not [string]::IsNullOrEmpty($PackageId))
    {
        $packages = $packages | Where-Object { $_.PackageId -eq $PackageId }
    }

    foreach ($package in $packages)
    {
        # flags to determine the existence of the package
        $isPrerelease = $false
        $preReleasePackage = $package.Version -Split "-"
        if ($preReleasePackage.Count -gt 1)
        {
            # set the pre-release flag to true to build the object
            $isPrerelease = $true
        }

        $resultSet.Add([DotNetToolPackage]::new(
                $package.PackageId, $package.Version, $package.Commands, $isPrerelease, $installDir, $true
            ))
    }

    return $resultSet
}

function Get-SemVer($version)
{
    $version -match "^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(\-(?<pre>[0-9A-Za-z\-\.]+))?(\+(?<build>[0-9A-Za-z\-\.]+))?$" | Out-Null
    $major = [int]$matches['major']
    $minor = [int]$matches['minor']
    $patch = [int]$matches['patch']
    
    if ($null -eq $matches['pre']) { $pre = @() }
    else { $pre = $matches['pre'].Split(".") }

    $revision = 0
    if ($pre.Length -gt 1)
    {
        $revision = Get-HighestRevision -InputArray $pre
    }

    return [version]$version = "$major.$minor.$patch.$revision" 
}

function Get-HighestRevision
{
    param (
        [Parameter(Mandatory = $true)]
        [array]$InputArray
    )

    # Filter the array to keep only integers
    $integers = $InputArray | ForEach-Object {
        $_ -as [int]
    }

    # Return the highest integer
    if ($integers.Count -gt 0)
    {
        return ($integers | Measure-Object -Maximum).Maximum
    }
    else
    {
        return $null
    }
}

function Install-DotNetToolPackage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $PackageId,
        [string] $Version,
        [bool]   $PreRelease,
        [string] $ToolPathDirectory,
        [bool]   $Exist
    )

    $installArgument = Get-DotNetToolArguments @PSBoundParameters
    $arguments = "tool install $installArgument --ignore-failed-sources"
    Write-Verbose -Message "Installing dotnet tool package with arguments: $arguments"

    Invoke-DotNet -Command $arguments
}

function Update-DotNetToolPackage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $PackageId,
        [string] $Version,
        [bool]   $PreRelease,
        [string] $ToolPathDirectory,
        [bool]   $Exist
    )

    $installArgument = Get-DotNetToolArguments @PSBoundParameters
    $arguments = "tool update $installArgument --ignore-failed-sources"
    Write-Verbose -Message "update dotnet tool package with arguments: $arguments"

    Invoke-DotNet -Command $arguments
}

function Assert-DotNetToolDowngrade
{
    [version]$version = Invoke-DotNet -Command '--version'

    if ($version.Build -lt 200)
    {
        return $false
    }

    return $true
}

function Uninstall-DotNetToolPackage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $PackageId,
        [string] $ToolPathDirectory
    )

    $installArgument = Get-DotNetToolArguments @PSBoundParameters
    $arguments = "tool uninstall $installArgument" 
    Write-Verbose -Message "Uninstalling dotnet tool package with arguments: $arguments"
        
    Invoke-DotNet -Command $arguments
}

function Invoke-DotNet
{
    param (
        [Parameter(Mandatory = $true)]
        [string] $Command
    )

    try
    {
        Invoke-Expression "& `"$DotNetCliPath`" $Command"
    }
    catch
    {
        throw "Executing dotnet.exe with {$Command} failed."
    }
}

# Keeps the path of the code.exe CLI path.
$DotNetCliPath = Get-DotNetPath

#endregion Functions

#region Classes
<#
.SYNOPSIS
    This class is used to install and uninstall .NET SDK tools globally or use the tool path directory.
#>

[DSCResource()]
class DotNetToolPackage
{
    [DscProperty(Key)]
    [string] $PackageId

    [DscProperty()]
    [string] $Version

    [DscProperty()]
    [string[]] $Commands

    [DscProperty()]
    [bool] $Prerelease = $false

    [DscProperty()]
    [string] $ToolPathDirectory

    [DscProperty()]
    [bool] $Exist = $true

    static [hashtable] $InstalledPackages

    DotNetToolPackage()
    {
        [DotNetToolPackage]::GetInstalledPackages()
    }

    DotNetToolPackage([string] $PackageId, [string] $Version, [string[]] $Commands, [bool] $PreRelease, [string] $ToolPathDirectory, [bool] $Exist)
    {
        $this.PackageId = $PackageId
        $this.Version = $Version
        $this.Commands = $Commands
        $this.PreRelease = $PreRelease
        $this.ToolPathDirectory = $ToolPathDirectory
        $this.Exist = $Exist
    }

    [DotNetToolPackage] Get()
    {
        # get the properties of the object currently set
        $properties = $this.ToHashTable()

        # refresh installed packages
        [DotNetToolPackage]::GetInstalledPackages($properties)

        # current state
        $currentState = [DotNetToolPackage]::InstalledPackages[$this.PackageId]

        if ($null -ne $currentState)
        {
            if ($this.Version -and ($this.Version -ne $currentState.Version))
            {
                # See treatment: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#normalized-version-numbers
                # in this case, we misuse revision if beta,alpha, rc are present and grab the highest revision
                $installedVersion = Get-Semver -Version $currentState.Version
                $currentVersion = Get-Semver -Version $this.Version
                if ($currentVersion -ne $installedVersion)
                {
                    $currentState.Exist = $false
                }
            }

            return $currentState
        }
        
        return [DotNetToolPackage]@{
            PackageId         = $this.PackageId
            Version           = $this.Version
            Commands          = $this.Commands
            PreRelease        = $this.PreRelease
            ToolPathDirectory = $this.ToolPathDirectory
            Exist             = $false
        }
    }

    Set()
    {
        if ($this.Test())
        {
            return
        }

        $currentPackage = [DotNetToolPackage]::InstalledPackages[$this.PackageId]
        if ($currentPackage -and $this.Exist)
        {
            if ($this.Version -lt $currentPackage.Version)
            {
                $this.ReInstall($false)
            }
            else
            {
                $this.Upgrade($false)
            }
        }
        elseif ($this.Exist)
        {
            $this.Install($false)
        }
        else
        {
            $this.Uninstall($false)
        }
    }

    [bool] Test()
    {
        $currentState = $this.Get()
        if ($currentState.Exist -ne $this.Exist)
        {
            return $false
        }

        if ($null -ne $this.Version -or $this.Version -ne $currentState.Version -and $this.PreRelease -ne $currentState.PreRelease)
        {
            return $false
        }
        return $true
    }

    static [DotNetToolPackage[]] Export()
    {
        return [DotNetToolPackage]::Export(@{})
    }

    static [DotNetToolPackage[]] Export([hashtable] $filterProperties)
    {
        $packages = Get-InstalledDotNetToolPackages @filterProperties

        return $packages
    }

    #region DotNetToolPackage helper functions
    static [void] GetInstalledPackages()
    {   
        [DotNetToolPackage]::InstalledPackages = @{}

        foreach ($extension in [DotNetToolPackage]::Export())
        {
            [DotNetToolPackage]::InstalledPackages[$extension.PackageId] = $extension
        }
    }

    static [void] GetInstalledPackages([hashtable] $filterProperties)
    {   
        [DotNetToolPackage]::InstalledPackages = @{}

        foreach ($extension in [DotNetToolPackage]::Export($filterProperties))
        {
            [DotNetToolPackage]::InstalledPackages[$extension.PackageId] = $extension
        }
    }

    [void] Upgrade([bool] $preTest)
    {
        if ($preTest -and $this.Test())
        {
            return
        }

        $params = $this.ToHashTable()   

        Update-DotNetToolpackage @params
        [DotNetToolPackage]::GetInstalledPackages()
    }

    [void] ReInstall([bool] $preTest)
    {
        if ($preTest -and $this.Test())
        {
            return
        }
        
        $this.Uninstall($false)
        $this.Install($false)
        [DotNetToolPackage]::GetInstalledPackages()
    }

    [void] Install([bool] $preTest)
    {
        if ($preTest -and $this.Test())
        {
            return
        }

        $params = $this.ToHashTable()   

        Install-DotNetToolpackage @params
        [DotNetToolPackage]::GetInstalledPackages()
    }

    [void] Install()
    {
        $this.Install($true)
    }

    [void] Uninstall([bool] $preTest)
    {
        $params = $this.ToHashTable()

        $uninstallParams = @{
            PackageId = $this.PackageId
        }

        if ($params.ContainsKey('ToolPathDirectory'))
        {
            $uninstallParams.Add('ToolPathDirectory', $params['ToolPathDirectory'])
        }

        Uninstall-DotNetToolpackage @uninstallParams
        [DotNetToolPackage]::GetInstalledPackages()
    }

    [void] Uninstall()
    {
        $this.Uninstall($true)
    }

    [hashtable] ToHashTable()
    {
        $parameters = @{}
        foreach ($property in $this.PSObject.Properties)
        {
            if (-not ([string]::IsNullOrEmpty($property.Value)))
            {
                $parameters[$property.Name] = $property.Value
            }
        }

        return $parameters
    }
    #endregion DotNetToolPackage helper functions
}
#endregion Classes
# SIG # Begin signature block
# MIIoPAYJKoZIhvcNAQcCoIIoLTCCKCkCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBbIHdIp5YpzD8Y
# AokWfxoa9rRjeQE2dRpM3HZmwyR+mqCCDYUwggYDMIID66ADAgECAhMzAAAEA73V
# lV0POxitAAAAAAQDMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTEzWhcNMjUwOTExMjAxMTEzWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCfdGddwIOnbRYUyg03O3iz19XXZPmuhEmW/5uyEN+8mgxl+HJGeLGBR8YButGV
# LVK38RxcVcPYyFGQXcKcxgih4w4y4zJi3GvawLYHlsNExQwz+v0jgY/aejBS2EJY
# oUhLVE+UzRihV8ooxoftsmKLb2xb7BoFS6UAo3Zz4afnOdqI7FGoi7g4vx/0MIdi
# kwTn5N56TdIv3mwfkZCFmrsKpN0zR8HD8WYsvH3xKkG7u/xdqmhPPqMmnI2jOFw/
# /n2aL8W7i1Pasja8PnRXH/QaVH0M1nanL+LI9TsMb/enWfXOW65Gne5cqMN9Uofv
# ENtdwwEmJ3bZrcI9u4LZAkujAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU6m4qAkpz4641iK2irF8eWsSBcBkw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMjkyNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AFFo/6E4LX51IqFuoKvUsi80QytGI5ASQ9zsPpBa0z78hutiJd6w154JkcIx/f7r
# EBK4NhD4DIFNfRiVdI7EacEs7OAS6QHF7Nt+eFRNOTtgHb9PExRy4EI/jnMwzQJV
# NokTxu2WgHr/fBsWs6G9AcIgvHjWNN3qRSrhsgEdqHc0bRDUf8UILAdEZOMBvKLC
# rmf+kJPEvPldgK7hFO/L9kmcVe67BnKejDKO73Sa56AJOhM7CkeATrJFxO9GLXos
# oKvrwBvynxAg18W+pagTAkJefzneuWSmniTurPCUE2JnvW7DalvONDOtG01sIVAB
# +ahO2wcUPa2Zm9AiDVBWTMz9XUoKMcvngi2oqbsDLhbK+pYrRUgRpNt0y1sxZsXO
# raGRF8lM2cWvtEkV5UL+TQM1ppv5unDHkW8JS+QnfPbB8dZVRyRmMQ4aY/tx5x5+
# sX6semJ//FbiclSMxSI+zINu1jYerdUwuCi+P6p7SmQmClhDM+6Q+btE2FtpsU0W
# +r6RdYFf/P+nK6j2otl9Nvr3tWLu+WXmz8MGM+18ynJ+lYbSmFWcAj7SYziAfT0s
# IwlQRFkyC71tsIZUhBHtxPliGUu362lIO0Lpe0DOrg8lspnEWOkHnCT5JEnWCbzu
# iVt8RX1IV07uIveNZuOBWLVCzWJjEGa+HhaEtavjy6i7MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAQDvdWVXQ87GK0AAAAA
# BAMwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIO1q
# Bg4AgJ3LU8RNNC9LXjZ30ohwfDZI7xmcSxeqECNTMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAYTvbgq4Y8YZdkpJiA2ECVd/pS9Vdelp6m0R+
# k09LJUOjoIuZ3peF6HXr2Qr15hUfqpBDrET8LlET0v2DED/BQzMvUzcIGNI8ogC0
# oJdc6EgeAxImuxsPpLWvEkr3HFlkIF1stEpQqg9uXq63K48zXhtKzyfIGoX83Ycm
# MU6f/keuldDl033mV0a/SHJjIv97G4z4qxM0g+PUc/3gdqxKvHgu1QccLnBc1b1S
# 2wxOKL9ePLGEvAse6pl6VE12TS+hfKkhj1MM5I0kzTCw8cJAzQCGxrx697NWFvp+
# oizlyI3OU+uX3IWcaljqxVgX8pvCVU+AxsMGARkBZ/ADsg7kvKGCF5cwgheTBgor
# BgEEAYI3AwMBMYIXgzCCF38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCD7Enra1ScS1Ffq+liXjZ2HD8W54uD080HW
# kXoO9jBe0QIGZwfzLC8yGBMyMDI0MTAyMjEzMjgxOC40NjdaMASAAgH0oIHRpIHO
# MIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxk
# IFRTUyBFU046ODkwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFNlcnZpY2WgghHtMIIHIDCCBQigAwIBAgITMwAAAe3hX8vV96VdcwAB
# AAAB7TANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MDAeFw0yMzEyMDYxODQ1NDFaFw0yNTAzMDUxODQ1NDFaMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODkwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Uw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCoMMJskrrqapycLxPC1H7z
# D7g88NpbEaQ6SjcTIRbzCVyYQNsz8TaL1pqFTEAPL1X7ojL4/EaEW+UjNqZs/ayM
# yW4YIpFPZP2x4FBMVCddseF2i+aMMjDHi0LcTQZxM2s3mFMrCZAWSfLYXYDIimFB
# z8j0oLWGy3VgLmBTKM4xLqv7DZUz8B2SoAmbEtp62ngSl0hOoN73SFwE+Y24SvGQ
# MWhykpG+vXDwcpWvwDe+TgnrLR7ATRFXN5JS26dm2yy6SYFMRYnME3dMHCQ/UQIQ
# QNC8nLmIvdKkAoWEMXtJsGEo3QrM2S2SBv4PpHRzRukzTtP+UAceGxM9JyrwUQP5
# OCEmW6YchEyRDSwP4hU9f7B0Ayh14Pw9vJo7jewNjeMPIkmneyLSi0ruv2ox/xRG
# tcJ9yBNC5BaRktjz7stPaojR+PDA2fuBtCo8xKlkt53mUb7AY+CZHHqhLm76pdMF
# 6BHv2TvwlVBeQRN22XjaVVRwCgjgJnNewt7PejcrpUn0qHLgLq+1BN1DzYukWkTr
# 7wT0zl0iXr+NtqUkWSOnWRfe8N21tB6uv3VkW8nFdChtbbZZz24peLtJEZuNrN8X
# f9PTPMzZXDJBI1EciR/91QcGoZFmVbFVb2rUIAs01+ZkewvbhmGVDefX9oZG4/K4
# gGUsTvTW+r1JZMxUT2MwqQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFM4b8Oz33hAq
# BEfKlAZf0NKh4CIZMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8G
# A1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBs
# BggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
# AwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCd1gK2Rd+eGL0e
# Hi+iE6/qDY8sbbsO4emancp6KPN+xq5ZAatiBR4jmRRhm+9Vik0Fo0DLWi/N28bF
# I7dXYw09p3vCipbjy4Eoifm0Nud7/4U30i9+7RvW7XOQ3rx37+U7vq9lk6yYpGCN
# p0jlJ188/CuRPgqJnfq5EdeafH2AoG46hKWTeB7DuXasGt6spJOenGedSre34MWZ
# qeTIQ0raOItZnFuGDy4+xoD1qRz2QW+u2gCHaG8AQjhYUM4uTi9t6kttj6c7Xamr
# 2zrWuceDhz7sKLttLTJ7ws5YrA2I8cTlbMAf2KW0GVjKbYGd+LZGduEK7/7fs4GU
# kMqc51FsNdG1n+zgc7zHu2oGGeCBg4s8ZR0ZFyx7jsgm9sSFCKQ5CsbAvlr/60Nd
# k5TeMR8Js2kNUicu2CqZ03833TsvTgk7iD1KLgfS16HEvjN6m4VKJKgjJ7OJJzab
# tS4JQgUnJrIZfyosk4D18rZni9pUwN03WgTmd10WTwiZOu4g8Un6iKcPMY/iFqTu
# 4ntkzFUxBBpbFG6k1CINZmoirEWmCtG3lyZ2IddmjtIefTkIvGWb4Jxzz7l2m/E2
# kGOixDJHsahZVmwsoNvhy5ku/inU++dXHzw+hlvqTSFT89rIFVhcmsWPDJPNRSSp
# MhoJ33V2Za/lkKcbkUM0SbQgS9qsdzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKb
# SZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj
# YXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIy
# NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXI
# yjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjo
# YH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1y
# aa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v
# 3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pG
# ve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viS
# kR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYr
# bqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlM
# jgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSL
# W6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AF
# emzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIu
# rQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIE
# FgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWn
# G1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW
# M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5
# Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi
# AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV
# 9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3Js
# Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAx
# MC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2
# LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv
# 6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZn
# OlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1
# bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4
# rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU
# 6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDF
# NLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/
# HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdU
# CbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKi
# excdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTm
# dHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZq
# ELQdVTNYs6FwZvKhggNQMIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp
# Y2EgT3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg5MDAtMDVF
# MC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMK
# AQEwBwYFKw4DAhoDFQDuHayKTCaYsYxJh+oWTx6uVPFw+aCBgzCBgKR+MHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6sGZ5TAi
# GA8yMDI0MTAyMjAzMjYyOVoYDzIwMjQxMDIzMDMyNjI5WjB3MD0GCisGAQQBhFkK
# BAExLzAtMAoCBQDqwZnlAgEAMAoCAQACAjOXAgH/MAcCAQACAhLmMAoCBQDqwutl
# AgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSCh
# CjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBABveEfWZaVwZ+OlFZ9kQtpqo
# Ffz92o3jQxZGqJotswekcNytSoTUCe2H4C2ik0DWqxkPmBoUSlA1YLpyiWu7bnoO
# GzMIteq79YoMg4cJEffK29YSRBKJc2bumA8YrFpNfOeDXHs2cebM4/kXmBegVP3K
# HYDKz9Tz1OCwpERHiHXPy7kY5miXbOOFxv4ksewAggcYFfZzr0bclY3OYgHQ6CXG
# LKUD2vMFoH1p5EudsxQCoegDQ5b6cnJ+LPr33YCWQnNQuZDAVt7K/gm+N3CUppms
# jCpJarOXz2QL1NdmckLx5gLeeHaCL0D7KdQaYNwhr62Vd4BKrLox1n8Sr33t6ZMx
# ggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA
# Ae3hX8vV96VdcwABAAAB7TANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBBqZJUhmdRxuEVGdLpFW4z
# L0Osyjt7ElpBhru5fabthzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EII0u
# DWg0CFseKxK3A16l1wrIwrsSDrXZ6xSf0F4xbMo5MIGYMIGApH4wfDELMAkGA1UE
# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHt4V/L1felXXMAAQAAAe0wIgQgPzUO
# mS933Rxkby0kSWrJlGP4Qaiob+zZLFDJ9fArZ8QwDQYJKoZIhvcNAQELBQAEggIA
# P3lptC//Woc5BP6m3SzezmNrSYZ/AnrJVSu49SUjJ6sQ1RuvHTmeleBAVM4MwMDn
# MLyXyrvcnN4hQOxIVlyijHRneXSY6wQe3z31c8UPhRBgpV+bu6LjQJTaZGx8WEFH
# 8PPELcJyxmnxwvEsKUX8V++Sh4Lf2ME5xxfNA9A3POl0meUQQhfj2YgARq925m1P
# VXxx4cxyBSKOO8kEkiKO2NyP9F8kKBX+tvpXwUJ5BEBseOdh00uer3SIXZcZNDUE
# ymIJ4uZZazznRL3/ROpQ5WxIGmK4jbMClOtdKWdR22+dl5KsNp9g1gS6AbAQJyL0
# FZrVW4s7ylf7DF7Jx9aeeupe0Tm5c66sQydJ0qUYLsigRXKAKJZ1BC17AhTYMUxu
# ypL3XOQp0c3N4SzIA7vldD5607tH8anrEAe9yydEu8+u1yhphTD/3uO5moJBrL50
# 4FZy4pMjxSFEn98LgOUhcaZymj7R0ymxVJivbwCQent2kqyMvvwxbrFGyDvSnjPP
# WQ9ePZy9kMyJHhpiztREmdObLRwNqo0wAvVcAzYa5IabCnegnhSxv76nv13YMvqN
# p4nUeSMWxhhoBJMpjBsmejFI2ZA5uc59G5rob+OGJZM2BObpsuOYZoDElocOC1su
# KVI9xlvlGbhdpin+rx3qanS+kwyBTp3QDVN5As3z6RE=
# SIG # End signature block