Classes/Public/DscConfigurationFileParser.ps1

<#
Copyright (c) 2018-2021 VMware, Inc. All rights reserved
 
The BSD-2 license (the "License") set forth below applies to all parts of the Desired State Configuration Resources for VMware project. You may not use this file except in compliance with the License.
 
BSD-2 License
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 
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.
 
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.
#>


class DscConfigurationFileParser {
    <#
    .DESCRIPTION
 
    Parses the PowerShell script file containing the DSC Configurations by performing the following steps:
 
    1. Extracts each DSC Configuration defined in the file and converts it into a DscConfigurationBlock object, which contains
    the following information: the name of the DSC Configuration, the configurationData defined as hashtable which is needed
    if the DSC Configuration retrieves data from it. Also the object contains the text information for the DSC Configuration:
    the start and end line of the DSC Configuration in the file and also the DSC Configuration as text. This whole information
    is later passed to the DscConfigurationCompiler which compiles it and produces the VmwDscConfiguration object.
 
    2. Invokes all non DSC Configuration lines in the provided file and this ways makes them available in the current scope. This way
    for example if there is configurationData defined, the hashtable will be available in memory for the DscConfigurationCompiler to
    produce the VmwDscConfiguration object.
 
    3. If the Parameters hashtable is passed, invokes all non DSC Configuration lines with the specified PowerShell script file parameters.
    #>

    [DscConfigurationBlock[]] ParseDscConfigurationFile([string] $dscConfigurationFilePath, [System.Collections.Hashtable] $parameters) {
        $dscConfigurationFileContent = Get-Content -Path $dscConfigurationFilePath
        $dscConfigurationFileContentRaw = Get-Content -Path $dscConfigurationFilePath -Raw

        $tokens = [System.Management.Automation.PSParser]::Tokenize($dscConfigurationFileContentRaw, [ref]$null)
        $dscConfigurations = $this.GetAllDscConfigurations($tokens)

        $scriptContent = [System.Text.StringBuilder]::new()
        $dscConfigurationsContent = [System.Text.StringBuilder]::new()

        $j = 0
        for ($i = 0; $i -lt $dscConfigurationFileContent.Length; $i++) {
            if ($i -lt ($dscConfigurations[$j].Extent.StartLine - 1) -or $i -gt $dscConfigurations[$j].Extent.EndLine) {
                $scriptContent.AppendLine($dscConfigurationFileContent[$i]) | Out-Null
            }
            else {
                $dscConfigurationsContent.AppendLine($dscConfigurationFileContent[$i]) | Out-Null
            }

            if ($i + 1 -gt $dscConfigurations[$j].Extent.EndLine - 1) {
                $dscConfigurationsContentAsString = $dscConfigurationsContent.ToString()

                # Only the content of the DSC Configuration is needed so we trim the Configuration keyword and the name of the DSC Configuration.
                $dscConfigurationsContentAsString = $dscConfigurationsContentAsString.TrimStart('Configuration').TrimStart()
                $dscConfigurationsContentAsString = $dscConfigurationsContentAsString.TrimStart($dscConfigurations[$j].Name).TrimStart()

                $dscConfigurationsContentAsString = $dscConfigurationsContentAsString.TrimEnd()

                # After the text of the DSC Configuration is retrieved, we clear the content, so that we can start
                # with an empty string for the next DSC Configuration in the file.
                $dscConfigurations[$j].Extent.Text = $dscConfigurationsContentAsString
                if ($j -lt ($dscConfigurations.Length - 1)) {
                    $dscConfigurationsContent = [System.Text.StringBuilder]::new()
                    $j++
                }
            }
        }

        # The ScriptBlock is invoked for all non DSC Configurations lines with the PowerShell script file parameters if specified.
        $scriptContentAsString = $scriptContent.ToString()
        $scriptBlock = [ScriptBlock]::Create($scriptContentAsString)

        if ($parameters.Count -gt 0) {
            $invokeParameters = $this.GetOrderedScriptParameters($tokens, $parameters)
            $scriptBlock.Invoke($invokeParameters)
        }
        else {
            $scriptBlock.Invoke()
        }

        <#
            In the file there should be only one configurationData hashtable for all defined DSC Configurations. If the user wants to use different
            configurationData hashtables, the DSC Configurations should be defined in separate files as there is no way to know which hashtable is for
            which DSC Configuration. So the assumption is that there should be only one hashtable defined with the AllNodes key in the file.
        #>

        $configurationData = Get-Variable |
            Where-Object -FilterScript { $null -ne $_.Value -and $_.Value.GetType() -eq [System.Collections.Hashtable] -and $_.Value.ContainsKey('AllNodes') } |
            Select-Object -First 1
        if ($null -ne $configurationData -and $scriptContentAsString -Match $configurationData.Name) {
            foreach ($dscConfiguration in $dscConfigurations) {
                $dscConfiguration.ConfigurationData = $configurationData.Value
            }
        }

        return $dscConfigurations
    }

    <#
    .DESCRIPTION
 
    Extracts each DSC Configuration defined in the file and converts it into a DscConfigurationBlock object. The content of the file is passed
    as an array of PSTokens from which each DSC Configuration info is extracted.
    #>

    hidden [DscConfigurationBlock[]] GetAllDscConfigurations([System.Collections.ObjectModel.Collection[System.Management.Automation.PSToken]] $tokens) {
        $dscConfigurations = @()

        for ($i = 0; $i -lt $tokens.Count; $i++) {
            $token = $tokens[$i]

            if ($token.Type -eq 'Keyword' -and $token.Content -eq 'Configuration') {
                $dscConfiguration = [DscConfigurationBlock]::new()
                $dscConfiguration.Extent = [DscConfigurationBlockExtent]::new()
                $dscConfiguration.Extent.StartLine = $token.StartLine

                $j = $i + 1
                $dscConfigurationBlockReached = $false
                $endOfDscConfigurationReached = $false
                $bracketsCounter = 0

                while ($j -lt $tokens.Count -and !$endOfDscConfigurationReached) {
                    $dscConfigurationToken = $tokens[$j]

                    # The name of the DSC Configuration is in the token of type CommandArgument.
                    if ($null -eq $dscConfiguration.Name -and $dscConfigurationToken.Type -eq 'CommandArgument') {
                        $dscConfiguration.Name = $dscConfigurationToken.Content
                    }

                    if ($dscConfigurationToken.Type -eq 'GroupStart' -and $dscConfigurationToken.Content -eq '{') {
                        if (!$dscConfigurationBlockReached) {
                            $dscConfigurationBlockReached = $true
                        }

                        $bracketsCounter++
                    }

                    if ($dscConfigurationToken.Type -eq 'GroupEnd' -and $dscConfigurationToken.Content -eq '}') {
                        $bracketsCounter--
                    }

                    # bracketsCounter equal to zero indicates that the closing bracket of the current DSC Configuration was reached.
                    if ($dscConfigurationBlockReached -and $bracketsCounter -eq 0) {
                        $dscConfiguration.Extent.EndLine = $dscConfigurationToken.EndLine
                        $endOfDscConfigurationReached = $true
                    }

                    <#
                        The first index should be updated at each step, so after the nested loop finishes and
                        the DSC Configuration block is populated with the text of the DSC Configuration,
                        the first loop continues from the end of the current DSC Configuration.
                    #>

                    $j++
                    $i = $j
                }

                $dscConfigurations += $dscConfiguration
            }
        }

        return $dscConfigurations
    }

    <#
    .DESCRIPTION
 
    Orders the specified script parameters in the correct order.
 
    1. Extracts each script parameter in an array in the correct order when parsing the script file.
    2. Checks all passed parameters and orders the values in the correct parameter order.
    3. If a parameter is not passed, $null is added to the array for the specified parameter.
    #>

    hidden [array] GetOrderedScriptParameters(
        [System.Collections.ObjectModel.Collection[System.Management.Automation.PSToken]] $tokens,
        [System.Collections.Hashtable] $parameters) {
        $scriptParameters = @()
        $invokeParameters = @()

        $i = 0
        $endOfParamsReached = $false

        while ($i -lt $tokens.Count -and !$endOfParamsReached) {
            $token = $tokens[$i]

            if ($token.Type -eq 'Keyword' -and $token.Content -eq 'Param') {
                $j = $i + 1
                $paramsBlockReached = $false
                $bracketsCounter = 0
                $attributeBracketsCounter = 0

                while ($j -lt $tokens.Count -and !$endOfParamsReached) {
                    $paramsToken = $tokens[$j]

                    if ($paramsToken.Type -eq 'GroupStart' -and $paramsToken.Content -eq '(') {
                        if (!$paramsBlockReached) {
                            $paramsBlockReached = $true
                        }

                        $bracketsCounter++
                    }

                    if ($paramsToken.Type -eq 'GroupEnd' -and $paramsToken.Content -eq ')') {
                        $bracketsCounter--
                    }

                    if ($paramsToken.Type -eq 'Operator' -and $paramsToken.Content -eq '[') {
                        $attributeBracketsCounter++
                    }

                    if ($paramsToken.Type -eq 'Operator' -and $paramsToken.Content -eq ']') {
                        $attributeBracketsCounter--
                    }

                    <#
                        The additional check is needed to ignore the Parameter Attributes, for example:
                        [Parameter(Mandatory = $true)] where true is also a variable.
                    #>

                    if ($paramsToken.Type -eq 'Variable' -and $attributeBracketsCounter -eq 0) {
                        $scriptParameters += $paramsToken.Content
                    }

                    # bracketsCounter equal to zero indicates that the closing bracket of the Param was reached.
                    if ($paramsBlockReached -and $bracketsCounter -eq 0) {
                        $endOfParamsReached = $true
                    }

                    $j++
                    $i = $j
                }
            }

            $i++
        }

        foreach ($scriptParameter in $scriptParameters) {
            $parameterName = $parameters.Keys | Where-Object -FilterScript { $_ -eq $scriptParameter }
            if ($null -ne $parameterName) {
                if ($parameters.$parameterName -is [array]) {
                    <#
                        Arrays should be passed with the below syntax, otherwise
                        only the first element of the array is passed to the script.
                    #>

                    $invokeParameters += (, $parameters.$parameterName)
                }
                else {
                    $invokeParameters += $parameters.$parameterName
                }
            }
            else {
                <#
                    If not specified, the parameter shoule be added with $null value
                    to keep the order of the parameters.
                #>

                $invokeParameters += $null
            }
        }

        return $invokeParameters
    }
}

# SIG # Begin signature block
# MIIgFgYJKoZIhvcNAQcCoIIgBzCCIAMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCn7rp1VVymnTRP
# JsLIR7druOaThTKQixOro80WrfuPKqCCD8swggTMMIIDtKADAgECAhBdqtQcwalQ
# C13tonk09GI7MA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNVBAYTAlVTMR0wGwYDVQQK
# ExRTeW1hbnRlYyBDb3Jwb3JhdGlvbjEfMB0GA1UECxMWU3ltYW50ZWMgVHJ1c3Qg
# TmV0d29yazEwMC4GA1UEAxMnU3ltYW50ZWMgQ2xhc3MgMyBTSEEyNTYgQ29kZSBT
# aWduaW5nIENBMB4XDTE4MDgxMzAwMDAwMFoXDTIxMDkxMTIzNTk1OVowZDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0
# bzEVMBMGA1UECgwMVk13YXJlLCBJbmMuMRUwEwYDVQQDDAxWTXdhcmUsIEluYy4w
# ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuswYfqnKot0mNu9VhCCCR
# vVcCrxoSdB6G30MlukAVxgQ8qTyJwr7IVBJXEKJYpzv63/iDYiNAY3MOW+Pb4qGI
# bNpafqxc2WLW17vtQO3QZwscIVRapLV1xFpwuxJ4LYdsxHPZaGq9rOPBOKqTP7Jy
# KQxE/1ysjzacA4NXHORf2iars70VpZRksBzkniDmurvwCkjtof+5krxXd9XSDEFZ
# 9oxeUGUOBCvSLwOOuBkWPlvCnzEqMUeSoXJavl1QSJvUOOQeoKUHRycc54S6Lern
# 2ddmdUDPwjD2cQ3PL8cgVqTsjRGDrCgOT7GwShW3EsRsOwc7o5nsiqg/x7ZmFpSJ
# AgMBAAGjggFdMIIBWTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDArBgNVHR8E
# JDAiMCCgHqAchhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNybDBhBgNVHSAEWjBY
# MFYGBmeBDAEEATBMMCMGCCsGAQUFBwIBFhdodHRwczovL2Quc3ltY2IuY29tL2Nw
# czAlBggrBgEFBQcCAjAZDBdodHRwczovL2Quc3ltY2IuY29tL3JwYTATBgNVHSUE
# DDAKBggrBgEFBQcDAzBXBggrBgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0dHA6
# Ly9zdi5zeW1jZC5jb20wJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdi5zeW1jYi5jb20v
# c3YuY3J0MB8GA1UdIwQYMBaAFJY7U/B5M5evfYPvLivMyreGHnJmMB0GA1UdDgQW
# BBTVp9RQKpAUKYYLZ70Ta983qBUJ1TANBgkqhkiG9w0BAQsFAAOCAQEAlnsx3io+
# W/9i0QtDDhosvG+zTubTNCPtyYpv59Nhi81M0GbGOPNO3kVavCpBA11Enf0CZuEq
# f/ctbzYlMRONwQtGZ0GexfD/RhaORSKib/ACt70siKYBHyTL1jmHfIfi2yajKkMx
# UrPM9nHjKeagXTCGthD/kYW6o7YKKcD7kQUyBhofimeSgumQlm12KSmkW0cHwSSX
# TUNWtshVz+74EcnZtGFI6bwYmhvnTp05hWJ8EU2Y1LdBwgTaRTxlSDP9JK+e63vm
# SXElMqnn1DDXABT5RW8lNt6g9P09a2J8p63JGgwMBhmnatw7yrMm5EAo+K6gVliJ
# LUMlTW3O09MbDTCCBVkwggRBoAMCAQICED141/l2SWCyYX308B7KhiowDQYJKoZI
# hvcNAQELBQAwgcoxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5j
# LjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMp
# IDIwMDYgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFF
# MEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZp
# Y2F0aW9uIEF1dGhvcml0eSAtIEc1MB4XDTEzMTIxMDAwMDAwMFoXDTIzMTIwOTIz
# NTk1OVowfzELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0
# aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTAwLgYDVQQDEydT
# eW1hbnRlYyBDbGFzcyAzIFNIQTI1NiBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXgx4AFq8ssdIIxNdok1FgHnH24ke021hN
# I2JqtL9aG1H3ow0Yd2i72DarLyFQ2p7z518nTgvCl8gJcJOp2lwNTqQNkaC07BTO
# kXJULs6j20TpUhs/QTzKSuSqwOg5q1PMIdDMz3+b5sLMWGqCFe49Ns8cxZcHJI7x
# e74xLT1u3LWZQp9LYZVfHHDuF33bi+VhiXjHaBuvEXgamK7EVUdT2bMy1qEORkDF
# l5KK0VOnmVuFNVfT6pNiYSAKxzB3JBFNYoO2untogjHuZcrf+dWNsjXcjCtvanJc
# YISc8gyUXsBWUgBIzNP4pX3eL9cT5DiohNVGuBOGwhud6lo43ZvbAgMBAAGjggGD
# MIIBfzAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9zMi5zeW1j
# Yi5jb20wEgYDVR0TAQH/BAgwBgEB/wIBADBsBgNVHSAEZTBjMGEGC2CGSAGG+EUB
# BxcDMFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc3ltYXV0aC5jb20vY3BzMCgG
# CCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0aC5jb20vcnBhMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9zMS5zeW1jYi5jb20vcGNhMy1nNS5jcmwwHQYDVR0l
# BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIBBjApBgNVHREE
# IjAgpB4wHDEaMBgGA1UEAxMRU3ltYW50ZWNQS0ktMS01NjcwHQYDVR0OBBYEFJY7
# U/B5M5evfYPvLivMyreGHnJmMB8GA1UdIwQYMBaAFH/TZafC3ey78DAJ80M5+gKv
# MzEzMA0GCSqGSIb3DQEBCwUAA4IBAQAThRoeaak396C9pK9+HWFT/p2MXgymdR54
# FyPd/ewaA1U5+3GVx2Vap44w0kRaYdtwb9ohBcIuc7pJ8dGT/l3JzV4D4ImeP3Qe
# 1/c4i6nWz7s1LzNYqJJW0chNO4LmeYQW/CiwsUfzHaI+7ofZpn+kVqU/rYQuKd58
# vKiqoz0EAeq6k6IOUCIpF0yH5DoRX9akJYmbBWsvtMkBTCd7C6wZBSKgYBU/2sn7
# TUyP+3Jnd/0nlMe6NQ6ISf6N/SivShK9DbOXBd5EDBX6NisD3MFQAfGhEV0U5eK9
# J0tUviuEXg+mw3QFCu+Xw4kisR93873NQ9TxTKk/tYuEr2Ty0BQhMIIFmjCCA4Kg
# AwIBAgIKYRmT5AAAAAAAHDANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQDEyBNaWNyb3NvZnQgQ29kZSBW
# ZXJpZmljYXRpb24gUm9vdDAeFw0xMTAyMjIxOTI1MTdaFw0yMTAyMjIxOTM1MTda
# MIHKMQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNV
# BAsTFlZlcmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA2IFZl
# cmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMT
# PFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkgLSBHNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8k
# CAgpejWeYAyq50s7Ttx8vDxFHLsr4P4pAvlXCKNkhRUn9fGtyDGJXSLoKqqmQrOP
# +LlVt7G3S7P+j34HV+zvQ9tmYhVhz2ANpNje+ODDYgg9VBPrScpZVIUm5SuPG5/r
# 9aGRwjNJ2ENjalJL0o/ocFFN0Ylpe8dw9rPcEnTbe11LVtOWvxV3obD0oiXyrxyS
# Zxjl9AYE75C55ADk3Tq1Gf8CuvQ87uCL6zeL7PTXrPL28D2v3XWRMxkdHEDLdCQZ
# IZPZFP6sKlLHj9UESeSNY0eIPGmDy/5HvSt+T8WVrg6d1NFDwGdz4xQIfuU/n3O4
# MwrPXT80h5aK7lPoJRUCAwEAAaOByzCByDARBgNVHSAECjAIMAYGBFUdIAAwDwYD
# VR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAYYwHQYDVR0OBBYEFH/TZafC3ey78DAJ
# 80M5+gKvMzEzMB8GA1UdIwQYMBaAFGL7CiFbf0NuEdoJVFBr9dKWcfGeMFUGA1Ud
# HwROMEwwSqBIoEaGRGh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By
# b2R1Y3RzL01pY3Jvc29mdENvZGVWZXJpZlJvb3QuY3JsMA0GCSqGSIb3DQEBBQUA
# A4ICAQCBKoIWjDRnK+UD6zR7jKKjUIr0VYbxHoyOrn3uAxnOcpUYSK1iEf0g/T9H
# BgFa4uBvjBUsTjxqUGwLNqPPeg2cQrxc+BnVYONp5uIjQWeMaIN2K4+Toyq1f75Z
# +6nJsiaPyqLzghuYPpGVJ5eGYe5bXQdrzYao4mWAqOIV4rK+IwVqugzzR5NNrKSM
# B3k5wGESOgUNiaPsn1eJhPvsynxHZhSR2LYPGV3muEqsvEfIcUOW5jIgpdx3hv08
# 44tx23ubA/y3HTJk6xZSoEOj+i6tWZJOfMfyM0JIOFE6fDjHGyQiKEAeGkYfF9sY
# 9/AnNWy4Y9nNuWRdK6Ve78YptPLH+CHMBLpX/QG2q8Zn+efTmX/09SL6cvX9/zoc
# Qjqh+YAYpe6NHNRmnkUB/qru//sXjzD38c0pxZ3stdVJAD2FuMu7kzonaknAMK5m
# yfcjKDJ2+aSDVshIzlqWqqDMDMR/tI6Xr23jVCfDn4bA1uRzCJcF29BUYl4DSMLV
# n3+nZozQnbBP1NOYX0t6yX+yKVLQEoDHD1S2HmfNxqBsEQOE00h15yr+sDtuCjqm
# a3aZBaPxd2hhMxRHBvxTf1K9khRcSiRqZ4yvjZCq0PZ5IRuTJnzDzh69iDiSrkXG
# GWpJULMF+K5ZN4pqJQOUsVmBUOi6g4C3IzX0drlnHVkYrSCNlDGCD6Ewgg+dAgEB
# MIGTMH8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3JhdGlv
# bjEfMB0GA1UECxMWU3ltYW50ZWMgVHJ1c3QgTmV0d29yazEwMC4GA1UEAxMnU3lt
# YW50ZWMgQ2xhc3MgMyBTSEEyNTYgQ29kZSBTaWduaW5nIENBAhBdqtQcwalQC13t
# onk09GI7MA0GCWCGSAFlAwQCAQUAoIGWMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCoGCisGAQQBgjcCAQwx
# HDAaoRiAFmh0dHA6Ly93d3cudm13YXJlLmNvbS8wLwYJKoZIhvcNAQkEMSIEIJqs
# bMII43fUM6w95DuZ0bbvqa/n4ZgkeSMOt9kVzFTdMA0GCSqGSIb3DQEBAQUABIIB
# AHXWNBHLm1khsofHZmj6uyYgh1toRUX3MlrP1uWEKGJ0X6hLc7LheSuAXdUpgBAx
# ca1NqEfXQ2gtovVQQg1ZM5uQVuWGvcC8bTUgCfs3Zwxes9aQnN0NhNlNuusypL2F
# FkhwWd/lb+YNUrNTOPw9CgqGpaiYDjR+xMHsBj745tDc1HlUlDwQi7qw/SN/mVgB
# b9+fE8rXnIElknF52zS6jh5UJsgO3rBPtFFNu0EOzuzjtalqbIJJCQm3oCU8f/7o
# LvkAHT2MZQ6zMBcRpROqrWP2zCXT6WRPgZRSshHdOpsh0d3IKxkIrp7WnGzb+r/W
# tUdfIGOCHvpZgKO4+Huxygihgg1FMIINQQYKKwYBBAGCNwMDATGCDTEwgg0tBgkq
# hkiG9w0BBwKggg0eMIINGgIBAzEPMA0GCWCGSAFlAwQCAQUAMHgGCyqGSIb3DQEJ
# EAEEoGkEZzBlAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgCJeGJCe3
# x506yP/ExHHW/7ZDKbdkQg9ev5/kzI5nStUCEQCJrSZ3OtYJQeHbnerEHbXPGA8y
# MDIxMDIyNDA5MDA1NVqgggo3MIIE/jCCA+agAwIBAgIQDUJK4L46iP9gQCHOFADw
# 3TANBgkqhkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl
# cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdp
# Q2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBMB4XDTIxMDEwMTAw
# MDAwMFoXDTMxMDEwNjAwMDAwMFowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMTCC
# ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMLmYYRnxYr1DQikRcpja1HX
# OhFCvQp1dU2UtAxQtSYQ/h3Ib5FrDJbnGlxI70Tlv5thzRWRYlq4/2cLnGP9NmqB
# +in43Stwhd4CGPN4bbx9+cdtCT2+anaH6Yq9+IRdHnbJ5MZ2djpT0dHTWjaPxqPh
# Lxs6t2HWc+xObTOKfF1FLUuxUOZBOjdWhtyTI433UCXoZObd048vV7WHIOsOjizV
# I9r0TXhG4wODMSlKXAwxikqMiMX3MFr5FK8VX2xDSQn9JiNT9o1j6BqrW7EdMMKb
# aYK02/xWVLwfoYervnpbCiAvSwnJlaeNsvrWY4tOpXIc7p96AXP4Gdb+DUmEvQEC
# AwEAAaOCAbgwggG0MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1Ud
# JQEB/wQMMAoGCCsGAQUFBwMIMEEGA1UdIAQ6MDgwNgYJYIZIAYb9bAcBMCkwJwYI
# KwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAfBgNVHSMEGDAW
# gBT0tuEgHf4prtLkYaWyoiWyyBc1bjAdBgNVHQ4EFgQUNkSGjqS6sGa+vCgtHUQ2
# 3eNqerwwcQYDVR0fBGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
# L3NoYTItYXNzdXJlZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0
# LmNvbS9zaGEyLWFzc3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYB
# BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0
# cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRp
# bWVzdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAQEASBzctemaI7znGucg
# Do5nRv1CclF0CiNHo6uS0iXEcFm+FKDlJ4GlTRQVGQd58NEEw4bZO73+RAJmTe1p
# pA/2uHDPYuj1UUp4eTZ6J7fz51Kfk6ftQ55757TdQSKJ+4eiRgNO/PT+t2R3Y18j
# UmmDgvoaU+2QzI2hF3MN9PNlOXBL85zWenvaDLw9MtAby/Vh/HUIAHa8gQ74wOFc
# z8QRcucbZEnYIpp1FUL1LTI4gdr0YKK6tFL7XOBhJCVPst/JKahzQ1HavWPWH1ub
# 9y4bTxMd90oNcX6Xt/Q/hOvB46NJofrOp79Wz7pZdmGJX36ntI5nePk2mOHLKNpb
# h6aKLzCCBTEwggQZoAMCAQICEAqhJdbWMht+QeQF2jaXwhUwDQYJKoZIhvcNAQEL
# BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ
# RCBSb290IENBMB4XDTE2MDEwNzEyMDAwMFoXDTMxMDEwNzEyMDAwMFowcjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRp
# bWVzdGFtcGluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3Q
# Mu5LzY9/3am6gpnFOVQoV7YjSsQOB0UzURB90Pl9TWh+57ag9I2ziOSXv2MhkJi/
# E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/
# Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPuXciaC1TjqAlxa+DPIhAPdc9xck4Krd9A
# Oly3UeGheRTGTSQjMF287DxgaqwvB8z98OpH2YhQXv1mblZhJymJhFHmgudGUP2U
# Kiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQhBlyF/EXBu89zdZN7wZC/aJTKk+FHcQd
# PK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4wggHKMB0GA1UdDgQWBBT0tuEgHf4prtLk
# YaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzASBgNV
# HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF
# BQcDCDB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
# Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDig
# NoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# QXNzdXJlZElEUm9vdENBLmNybDBQBgNVHSAESTBHMDgGCmCGSAGG/WwAAgQwKjAo
# BggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzALBglghkgB
# hv1sBwEwDQYJKoZIhvcNAQELBQADggEBAHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE
# 70uEv8rPAwL9xafDDiBCLK938ysfDCFaKrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy
# 0dh8GWLoXoIlHsS6HHssIeLWWywUNUMEaLLbdQLgcseY1jxk5R9IEBhfiThhTWJG
# JIdjjJFSLK8pieV4H9YLFKWA1xJHcLN11ZOFk362kmf7U2GJqPVrlsD0WGkNfMgB
# sbkodbeZY4UijGHKeZR+WfyMD+NvtQEmtmyl7odRIeRYYJu6DC0rbaLEfrvEJStH
# Agh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR79VYzIi8iNrJLokqV2PWmjlIxggJNMIIC
# SQIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw
# FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy
# IEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBAhANQkrgvjqI/2BAIc4UAPDdMA0G
# CWCGSAFlAwQCAQUAoIGYMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkq
# hkiG9w0BCQUxDxcNMjEwMjI0MDkwMDU1WjArBgsqhkiG9w0BCRACDDEcMBowGDAW
# BBTh14Ko4ZG+72vKFpG1qrSUpiSb8zAvBgkqhkiG9w0BCQQxIgQgqoALo+9l2YW7
# /eFbnxFQ9qyCP7AGTiQAFET3OrxBqpEwDQYJKoZIhvcNAQEBBQAEggEAc/gJdcK1
# zknqC6DFS0xEQBYBHT6qMmPnuvSGSw0kSasXM5U8XcGvZEK7f/kQ+sYP2dZDOlC0
# VSoY3ACh0RwZqRurKbqWjfoZzLRRSKCLSd5MQsRO/glXcEq4Lm/CMG0yzSsizv3p
# jRyrC6S9NjwJOaabLP8y/S6zs4c5yMUSdZlp2oYsmQRRj+A1EOqpfP5zRkAcuJim
# RCwM4wEPSsrblT8+IfjupZWtjImYtr/zPsCnJm8cvPDdg5HHErXy7dLGOx8uhNkl
# nt5sfMjnjDBS3J9rSN5Tihe9l/QmfM3riDYfGbWXp0W3oi+/4BgGlDrd1+5oPQyx
# DmmD8HU5OMRJMA==
# SIG # End signature block