GitDsc.psm1

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

using namespace System.Collections.Generic

enum Ensure {
    Absent
    Present
}

enum ConfigLocation {
    none
    global
    system
    worktree
    local
}

#region DSCResources
<#
    .SYNOPSIS
        The `GitClone` DSC resource is used to clone a Git repository to a local directory.
 
    .DESCRIPTION
        The `GitClone` DSC resource clones a remote Git repository specified by its HTTPS URL
        into a local root directory. If the repository has already been cloned, the resource
        will confirm the existing remote matches the specified URL.
 
        ## Requirements
 
        * Target machine must have Git installed.
 
    .PARAMETER HttpsUrl
        The HTTPS URL of the Git repository to clone. This is a key property.
 
    .PARAMETER Ensure
        Specifies whether the repository should be present or absent. Defaults to `Present`.
        Removing a cloned repository is not supported by this resource.
 
    .PARAMETER RemoteName
        The name of the remote. Defaults to `origin`.
 
    .PARAMETER RootDirectory
        The root directory where the repository will be cloned into. This is a mandatory property.
 
    .PARAMETER FolderName
        The folder name for the cloned repository. If not specified, it is derived from the HTTPS URL.
 
    .PARAMETER ExtraArgs
        Additional arguments to pass to `git clone`, provided as an array of strings where each
        element is a separate argument.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName GitDsc -Name GitClone -Method Set -Property @{
            HttpsUrl = 'https://github.com/microsoft/winget-dsc'
            RootDirectory = 'C:\repos'
        }
 
        This example clones the winget-dsc repository into C:\repos.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName GitDsc -Name GitClone -Method Set -Property @{
            HttpsUrl = 'https://github.com/microsoft/winget-dsc'
            RootDirectory = 'C:\repos'
            ExtraArgs = @('--depth', '1')
        }
 
        This example performs a shallow clone of the winget-dsc repository into C:\repos.
#>

[DSCResource()]
class GitClone {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$HttpsUrl

    [DscProperty()]
    [string]$RemoteName

    # The root directory where the project will be cloned to. (i.e. the directory where you expect to run `git clone`)
    [DscProperty(Mandatory)]
    [string]$RootDirectory

    # The folder name where the repository will be cloned to. If not specified, it will be derived from the HTTPS URL.
    [DscProperty()]
    [string]$FolderName

    [DscProperty()]
    [string[]]$ExtraArgs

    [GitClone] Get() {
        Assert-Git

        $currentState = [GitClone]::new()
        $currentState.HttpsUrl = $this.HttpsUrl
        $currentState.RootDirectory = $this.RootDirectory
        $currentState.FolderName = $this.FolderName
        $currentState.Ensure = [Ensure]::Absent
        $currentState.RemoteName = ($null -eq $this.RemoteName) ? 'origin' : $this.RemoteName

        if (-not(Test-Path -Path $this.RootDirectory)) {
            return $currentState
        }

        # Check if the URL is a Git repository URL
        Assert-GitUrl -HttpsUrl $this.HttpsUrl

        Set-Location $this.RootDirectory
        $projectName = $this.FolderName ? $this.FolderName : (GetGitProjectName($this.HttpsUrl))
        $expectedDirectory = Join-Path -Path $this.RootDirectory -ChildPath $projectName

        if (Test-Path $expectedDirectory) {
            Set-Location -Path $expectedDirectory
            try {
                $gitRemoteValue = Invoke-GitRemote -Arguments @('get-url', $currentState.RemoteName)
                if ($this.HttpsUrl.StartsWith($gitRemoteValue)) {
                    $currentState.Ensure = [Ensure]::Present
                }
            } catch {
                # Failed to execute `git remote`. Ensure state is `absent`
            }
        }

        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $currentState.Ensure -eq $this.Ensure
    }

    [void] Set() {
        if ($this.Ensure -eq [Ensure]::Absent) {
            throw 'This resource does not support removing a cloned repository.'
        }

        if (-not(Test-Path $this.RootDirectory)) {
            New-Item -ItemType Directory -Path $this.RootDirectory
        }

        Set-Location $this.RootDirectory

        $cloneArgs = [List[string]]::new()
        if ($this.ExtraArgs) {
            foreach ($a in ($this.ExtraArgs | Where-Object { $_ })) {
                $cloneArgs.Add($a)
            }
        }
        $cloneArgs.Add($this.HttpsUrl)
        if ($this.FolderName) {
            $cloneArgs.Add($this.FolderName)
        }

        Invoke-GitClone -Arguments $cloneArgs
    }
}

<#
    .SYNOPSIS
        The `GitRemote` DSC resource is used to manage remote repository references in a Git project.
 
    .DESCRIPTION
        The `GitRemote` DSC resource adds or removes a named remote URL from an existing local
        Git repository. The project directory must already exist as a valid Git repository.
 
        ## Requirements
 
        * Target machine must have Git installed.
        * The project directory must be an existing Git repository.
 
    .PARAMETER RemoteName
        The name of the Git remote. This is a key property.
 
    .PARAMETER RemoteUrl
        The URL of the Git remote. This is a key property.
 
    .PARAMETER Ensure
        Specifies whether the remote should be present or absent. Defaults to `Present`.
 
    .PARAMETER ProjectDirectory
        The path to the local Git repository. This is a mandatory property.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName GitDsc -Name GitRemote -Method Set -Property @{
            RemoteName = 'upstream'
            RemoteUrl = 'https://github.com/microsoft/winget-dsc'
            ProjectDirectory = 'C:\repos\winget-dsc'
        }
 
        This example adds an upstream remote to the specified local Git repository.
#>

[DSCResource()]
class GitRemote {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$RemoteName

    [DscProperty(Key)]
    [string]$RemoteUrl

    # The root directory where the project will be cloned to. (i.e. the directory where you expect to run `git clone`)
    [DscProperty(Mandatory)]
    [string]$ProjectDirectory

    [GitRemote] Get() {
        $currentState = [GitRemote]::new()
        $currentState.RemoteName = $this.RemoteName
        $currentState.RemoteUrl = $this.RemoteUrl
        $currentState.ProjectDirectory = $this.ProjectDirectory

        if (-not(Test-Path -Path $this.ProjectDirectory)) {
            throw 'Project directory does not exist.'
        }

        Set-Location $this.ProjectDirectory
        try {
            $gitRemoteValue = Invoke-GitRemote -Arguments @('get-url', $this.RemoteName)
            $currentState.Ensure = ($gitRemoteValue -like $this.RemoteUrl) ? [Ensure]::Present : [Ensure]::Absent
        } catch {
            $currentState.Ensure = [Ensure]::Absent
        }

        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $currentState.Ensure -eq $this.Ensure
    }

    [void] Set() {
        Set-Location $this.ProjectDirectory

        if ($this.Ensure -eq [Ensure]::Present) {
            try {
                Invoke-GitRemote -Arguments @('add', $this.RemoteName, $this.RemoteUrl)
            } catch {
                throw 'Failed to add remote repository.'
            }
        } else {
            try {
                Invoke-GitRemote -Arguments @('remove', $this.RemoteName)
            } catch {
                throw 'Failed to remove remote repository.'
            }
        }
    }
}

<#
    .SYNOPSIS
        The `GitConfigUserName` DSC resource is used to manage the Git user name configuration.
 
    .DESCRIPTION
        The `GitConfigUserName` DSC resource sets or removes the `user.name` Git configuration
        value at the specified configuration scope (local, global, system, or worktree).
 
        ## Requirements
 
        * Target machine must have Git installed.
        * For system-level configuration, the resource must be run as an Administrator.
 
    .PARAMETER UserName
        The Git user name to configure. This is a key property.
 
    .PARAMETER Ensure
        Specifies whether the user name should be present or absent. Defaults to `Present`.
 
    .PARAMETER ConfigLocation
        The Git configuration scope to apply the setting to (e.g., `global`, `system`, `local`).
 
    .PARAMETER ProjectDirectory
        The path to the Git repository. Required for non-global and non-system configurations.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName GitDsc -Name GitConfigUserName -Method Set -Property @{
            UserName = 'Demitrius Nelon'
            ConfigLocation = 'global'
        }
 
        This example sets the global Git user name to 'Demitrius Nelon'.
#>

[DSCResource()]
class GitConfigUserName {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$UserName

    [DscProperty()]
    [ConfigLocation]$ConfigLocation

    [DscProperty()]
    [string]$ProjectDirectory

    [GitConfigUserName] Get() {
        $currentState = [GitConfigUserName]::new()
        $currentState.UserName = $this.UserName
        $currentState.ConfigLocation = $this.ConfigLocation
        $currentState.ProjectDirectory = $this.ProjectDirectory

        if ($this.ConfigLocation -ne [ConfigLocation]::global -and $this.ConfigLocation -ne [ConfigLocation]::system) {
            # Project directory is not required for --global or --system configurations
            if ($this.ProjectDirectory) {
                if (Test-Path -Path $this.ProjectDirectory) {
                    Set-Location $this.ProjectDirectory
                } else {
                    throw 'Project directory does not exist.'
                }
            } else {
                throw 'Project directory parameter must be specified for non-system and non-global configurations.'
            }
        }

        $configArgs = ConstructGitConfigUserArguments -Arguments @('user.name') -ConfigLocation $this.ConfigLocation
        $result = Invoke-GitConfig -Arguments $configArgs
        $currentState.Ensure = ($currentState.UserName -eq $result) ? [Ensure]::Present : [Ensure]::Absent
        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $currentState.Ensure -eq $this.Ensure
    }

    [void] Set() {
        if ($this.ConfigLocation -eq [ConfigLocation]::system) {
            Assert-IsAdministrator
        }

        if ($this.ConfigLocation -ne [ConfigLocation]::global -and $this.ConfigLocation -ne [ConfigLocation]::system) {
            Set-Location $this.ProjectDirectory
        }

        if ($this.Ensure -eq [Ensure]::Present) {
            $configArgs = ConstructGitConfigUserArguments -Arguments @('user.name', $this.UserName) -ConfigLocation $this.ConfigLocation
        } else {
            $configArgs = ConstructGitConfigUserArguments -Arguments @('--unset', 'user.name') -ConfigLocation $this.ConfigLocation
        }

        Invoke-GitConfig -Arguments $configArgs
    }
}

<#
    .SYNOPSIS
        The `GitConfigUserEmail` DSC resource is used to manage the Git user email configuration.
 
    .DESCRIPTION
        The `GitConfigUserEmail` DSC resource sets or removes the `user.email` Git configuration
        value at the specified configuration scope (local, global, system, or worktree).
 
        ## Requirements
 
        * Target machine must have Git installed.
        * For system-level configuration, the resource must be run as an Administrator.
 
    .PARAMETER UserEmail
        The Git user email to configure. This is a key property.
 
    .PARAMETER Ensure
        Specifies whether the user email should be present or absent. Defaults to `Present`.
 
    .PARAMETER ConfigLocation
        The Git configuration scope to apply the setting to (e.g., `global`, `system`, `local`).
 
    .PARAMETER ProjectDirectory
        The path to the Git repository. Required for non-global and non-system configurations.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName GitDsc -Name GitConfigUserEmail -Method Set -Property @{
            UserEmail = 'demitrius.nelon@example.com'
            ConfigLocation = 'global'
        }
 
        This example sets the global Git user email to 'demitrius.nelon@example.com'.
#>

[DSCResource()]
class GitConfigUserEmail {
    [DscProperty()]
    [Ensure]$Ensure = [Ensure]::Present

    [DscProperty(Key)]
    [string]$UserEmail

    [DscProperty()]
    [ConfigLocation]$ConfigLocation

    [DscProperty()]
    [string]$ProjectDirectory

    [GitConfigUserEmail] Get() {
        $currentState = [GitConfigUserEmail]::new()
        $currentState.UserEmail = $this.UserEmail
        $currentState.ConfigLocation = $this.ConfigLocation
        $currentState.ProjectDirectory = $this.ProjectDirectory

        if ($this.ConfigLocation -ne [ConfigLocation]::global -and $this.ConfigLocation -ne [ConfigLocation]::system) {
            # Project directory is not required for --global or --system configurations
            if ($this.ProjectDirectory) {
                if (Test-Path -Path $this.ProjectDirectory) {
                    Set-Location $this.ProjectDirectory
                } else {
                    throw 'Project directory does not exist.'
                }
            } else {
                throw 'Project directory parameter must be specified for non-system and non-global configurations.'
            }
        }

        $configArgs = ConstructGitConfigUserArguments -Arguments @('user.email') -ConfigLocation $this.ConfigLocation
        $result = Invoke-GitConfig -Arguments $configArgs
        $currentState.Ensure = ($currentState.UserEmail -eq $result) ? [Ensure]::Present : [Ensure]::Absent
        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $currentState.Ensure -eq $this.Ensure
    }

    [void] Set() {
        if ($this.ConfigLocation -eq [ConfigLocation]::system) {
            Assert-IsAdministrator
        }

        if ($this.ConfigLocation -ne [ConfigLocation]::global -and $this.ConfigLocation -ne [ConfigLocation]::system) {
            Set-Location $this.ProjectDirectory
        }

        if ($this.Ensure -eq [Ensure]::Present) {
            $configArgs = ConstructGitConfigUserArguments -Arguments @('user.email', $this.UserEmail) -ConfigLocation $this.ConfigLocation
        } else {
            $configArgs = ConstructGitConfigUserArguments -Arguments @('--unset', 'user.email') -ConfigLocation $this.ConfigLocation
        }

        Invoke-GitConfig -Arguments $configArgs
    }
}

#endregion DSCResources

#region Functions
function Assert-Git {
    # Refresh session $path value before invoking 'git'
    $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
    try {
        Invoke-Git -Command 'help'
        return
    } catch {
        throw 'Git is not installed'
    }
}

function GetGitProjectName {
    param(
        [Parameter()]
        [string]$HttpsUrl
    )

    $projectName = ($HttpsUrl.split('/')[-1]).split('.')[0]
    return $projectName
}

function Invoke-GitConfig {
    param(
        [Parameter()]
        [string[]]$Arguments = @()
    )

    return Invoke-Git -Command (@('config') + $Arguments)
}

function Invoke-GitRemote {
    param(
        [Parameter()]
        [string[]]$Arguments = @()
    )

    return Invoke-Git -Command (@('remote') + $Arguments)
}

function Invoke-GitClone {
    param(
        [Parameter()]
        [string[]]$Arguments = @()
    )

    return Invoke-Git -Command (@('clone') + $Arguments)
}

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

    $argList = @($Command | Where-Object { -not [string]::IsNullOrEmpty($_) })
    return & git @argList
}

function ConstructGitConfigUserArguments {
    param(
        [Parameter(Mandatory)]
        [string[]]$Arguments,

        [Parameter(Mandatory)]
        [ConfigLocation]$ConfigLocation
    )

    if ([ConfigLocation]::None -ne $ConfigLocation) {
        return @("--$ConfigLocation") + $Arguments
    }
    return $Arguments
}

function Assert-IsAdministrator {
    $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity )

    $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator

    if (-not $windowsPrincipal.IsInRole($adminRole)) {
        throw 'This resource must be run as an Administrator to modify system settings.'
    }
}

function Assert-GitUrl {
    param(
        [Parameter(Mandatory)]
        [string]$HttpsUrl
    )

    $out = Invoke-Git -Command @('ls-remote', $HttpsUrl, '*') 2>&1

    if ($LASTEXITCODE -ne 0) {
        throw "Invalid Git URL: $HttpsUrl. Error: $out"
    }
}
#endregion Functions

# SIG # Begin signature block
# MIInbgYJKoZIhvcNAQcCoIInXzCCJ1sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBBSkHauk5/cCV2
# LPOHBxcDJOsMrRRwCe6kcCJA/z7mCaCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn7MIIZ9wIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI
# hvcNAQkEMSIEIGjYbqM6lxQ5NocMzqve44b7Kjj4Ht0Mf+nHOrCjo3OcMEIGCisG
# AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAQmQKl3BumJQluAFxfoEN
# V36jL7JLap2hVLXUSoIyIx0n469Wwd2PXE9vy9JMfaQQOavxppVxhEAQPHSzQvf8
# /FNSdAuEDKmGA8aDvclIgVp6NzErf0Qj+xMN1mLtCQWvZThzIsSlosdzmzMZHkSt
# xIHrjGABKOxMp3ZO9WpOwsUk4egfqVigQa0npWD6VxhAMmcNLNVQhh/07MHYjBqb
# ajrIe468OBtiv1MboQNM4TRLHUCJsglIA1qX2EcEhextXbTUNo1lJMtRgfkDiA81
# n92hdT2SpPklrgIZynjvWfaq0HDaTjtulpn4yhoyzpmNGGFiyD9uBHV0Xn7D9dFi
# haGCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UGCSqGSIb3DQEHAqCCF4YwgheC
# AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB
# QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCCJeVLMslc+IQSNGjn0
# ZET254pMwlkLZ31shX+QWHZKoAIGahBh/FQUGBMyMDI2MDYwOTIzNDIxNy45NjRa
# MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozNjA1LTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEfswggcoMIIFEKAD
# AgECAhMzAAACE7BDNWbPr5XoAAEAAAITMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxN1oXDTI2MTExMzE4
# NDgxN1owgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr
# BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG
# A1UECxMeblNoaWVsZCBUU1MgRVNOOjM2MDUtMDVFMC1EOTQ3MSUwIwYDVQQDExxN
# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEA9Jl64LoZxDINSFgz+9KS5Ozv5m548ePVzc9RXWe4T4/Mplfg
# a4eq12RGdp5cVvnjde5vxfq2ax/jnu7vUW4rZN4mOUm5vh+kcYsQlYQ53FwgIB3n
# EjcQHomrG3mZe/ozjFSAr6JbglKtIeAySPzAcFzyAer5lLNUHBEvQMM8BOjMyapC
# vh0xsg4xKFcVEJQLKEfCGBffMZI/amutHFb3CUTZ7aVpG2KHEFUNlZ1vwMKvxXTP
# RDnbwPGzyyqJJznfsLNHQ4vXt2ttS1PeCoGI0hN1Peq8yGsIXM9oocwC06DGNSM/
# 4LAx2uKvwmUn6NwLc0+tmvny6w28rZLejskRfnVWofEv1mWY0jHUnHrwSGBS8gVP
# 9gcBs6P5g0OpJPMfxdUkHXRkcMPPW0hIP8NbW8W5Sup8HuwnSKbjpyAlGBUdM/V5
# rZb0sZmkn714r6ULGK+cLLAN6R3FhX6N0nj64F27LTK2BbS0pJZaXjo0eDNz1Qcx
# eIFLUgF+RBsLYDn8E8cCkexK8Nlt3Gi9zJf55w6UfTZ+kwTMxMqFxh7+Tfx7+aBO
# bZ+nx961AtiqAy7zVV69o/LWRdKPZdvZn9ESyGbTnPfjkBERv22prSlETlRwzP6b
# mEVOKWLWVwxuwh7bUWUuUb1cj93zvttQYGQat5E9ALLJNmlvLKCskB7raLsCAwEA
# AaOCAUkwggFFMB0GA1UdDgQWBBQTnhBKx+FryphQWMRipH49sMFAOjAfBgNVHSME
# GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG
# AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p
# Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB
# Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN
# BgkqhkiG9w0BAQsFAAOCAgEAgmxaJrGqQ2D6UJhZ6Ql2SZFOaNuGbW3LzB+ES+l2
# BB1MJtBRSFdi/hVY33NpxsJQhQ5TLVp0DXYOkIoPQc17rH+IVhemO8jCt+U6I1TI
# w6cR7c+tEo/Jjp6EqEU1c4/mraMjgHhQ+raC/OUAm98A1r4bIPHtsBmLROGmeE5X
# LIFaBIZWHvh2COXITKObXVd5wGtJ1dZZdwaHACXF506jta+uoUdyzAeuNlTPLTrZ
# 8nyhxGwk9Vh6eiDQ7CQMWSSa8DJS9PUXjeoi9vTdS7ZMXqu+tv6Qz3xtoBF5+YFK
# 4uE+miGs90Fxm0VK2lWrmFhjkRl5zyoHOdwG7spNYkDomCPNWIudUQmQYKpt/Hss
# pfcb+xpnWIDQdMzgE8pj1vpwLgWEnH7LtT4dZCeoDo9PK40RxBD8kKJ769ngkEwf
# wCD2EX/MQk79eIvOhpnH12GuVByvaKZk5XZvqtPONNwr8q/qA3877IuWwWgnaeX+
# prpw0dZ/QLtbGGVrgP+TRQjt+2dcZA5P3X4LwANhiPsy0Ol4XCdj7OxBLFvOzsCP
# DPaVnkp+dfDFG+NOBir7aqTJ68622pymg1V+6gc/1RvxC/wgvYyG033ecJqv0On0
# ZRNYr+i/OkwgA3HP1aLD0aHrEpw6lt0263iRkCvrcdcOW8w3jC8TJuaGWyC2S9jE
# jzgwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB
# CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe
# Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm
# TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H
# ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc
# wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A
# W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w
# jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG
# MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ
# 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP
# 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz
# ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz
# NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3
# xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG
# AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/
# LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG
# DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB
# BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G
# A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw
# VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j
# cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF
# BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC
# cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF
# vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l
# 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn
# 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m
# O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx
# TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4
# S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9
# y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM
# +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw
# RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDVjCCAj4C
# AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozNjA1LTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa
# AxUAmBE8SCjxgjacmy8/VEdk7NxpR6aggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T
# dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO3SmeEwIhgPMjAyNjA2MDkx
# MzU1NDVaGA8yMDI2MDYxMDEzNTU0NVowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA
# 7dKZ4QIBADAHAgEAAgIBZTAHAgEAAgITWTAKAgUA7dPrYQIBADA2BgorBgEEAYRZ
# CgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0G
# CSqGSIb3DQEBCwUAA4IBAQBKVpbNP8KKgXqOW9UQ5dUSP2p2lJAWPxAnwkhW5osO
# W0LsLnUWgKsYoAePKges0uN7mzoNRIUqlquGVCjJ8K4vqJdpaXE/knv5l116Cine
# kuppJD41Aq8rRYALDDjKffyr6ArlbMsjWlxFN4jpjBzMOM/1AXsSBQ+xgpBkonrS
# noyIX80B7R0j8b5pG1Z12Low8M6wQE12AlsUgk6Qb5mEhv4OYjL/dEEByhP6kX98
# ZunGrbVi369fEdJ45tX4ue8D2dUUncD+x2Qf8RCIRtdTsltkR7m09w1BQ/CwSuG6
# 6GIy37ITgWXqVh3thlRKR+XMt4tVPLqnWy1B5Cdm8vGzMYIEDTCCBAkCAQEwgZMw
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAITsEM1Zs+vlegAAQAA
# AhMwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRAB
# BDAvBgkqhkiG9w0BCQQxIgQgHQt5tn2gbeWt6l669LBX6qas7aUzVEQ1eNEJgB0y
# uqEwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDM4QltFIUz8J4DjAzP4nVo
# dZvQxYGleUIfp86Oa5xYaDCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwAhMzAAACE7BDNWbPr5XoAAEAAAITMCIEINHs5aLatkZiOBLnf5sb6Xka
# TL4tJASeQ72ULXa/epmmMA0GCSqGSIb3DQEBCwUABIICAKENdat7Ym4n1zLE1m6j
# YAlGO5qpho60uqx866moRGxCfKmf/lHzGBIyVpvlFlAj7l07rUvXc/Jn/LkYLCqI
# HvO3u/7iZBbhm1+vGKYv7VKbGqxuHc0JFW1wQ35b37fSRmdU/OUvq1GndvhNmHyj
# s+mpSzBXrHnjuyAh2ETCZJEjqdFTx7F6Vs7CH5wMi6JX7VLV3ZckXB7GaslHUnY8
# kCEayv4md3a03bvYa2kClERhJYYwQ4imNYl3iBHdG6Zblb03PcdN+zIhJCUosyU7
# HEW/jd6MIFja5twe8oFBHEvy2eqgQaiMtS1JUJfQgAiQlF1XL1BmS9zdpJvD9I/s
# fFQJ3w/al2UzA+Hd18PyMEEADdLgmEwa2hUkCc+97QVW7zUxti88EjmhrQlBeJbk
# 3i0mHylltTNCoCim3UAlLTCx2KaXNLsOl5sWXKAPEFR57f+PnatzOlX5Gb/z3VCc
# fQ3d+mfDGZ/vo+q7d/5PAha2jkeMC8G0QOUlifBUIUKPEy63d4+i5YFw0uFGi2ep
# Zy4O2XVI7OMERPIXcGGiSZ+Kh5txFF8HRk59+jL1e44EhGE2fy0d3Xe1ukxzBdvB
# 0jhJ3eoXHZ0vbIDxWdUZ40zw0q8W6H1JVVU7vpsRqcbkZZiaiuwLCYqWsEjwrA94
# ptRtYF2TH3QyECal1edBhrLm
# SIG # End signature block