Obs/bin/ObsDep/content/Powershell/Roles/Common/Servicing/Scripts/Modules/FolderHash.psm1

#region C# code
$FolderHashSource = @"
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
 
namespace Microsoft.Solutions.IO
{
    [Serializable]
    public class FolderHashEntry : IEquatable<FolderHashEntry>
    {
        public string Path { get; private set; }
        public long LastWriteTimeUTCTicks { get; private set; }
        public int Size { get; private set; }
        public string Hash { get; private set; }
 
        public FolderHashEntry(string path, long lastWriteTimeUTCTicks, int size, string hash)
        {
            Path = path;
            LastWriteTimeUTCTicks = lastWriteTimeUTCTicks;
            Size = size;
            Hash = hash;
        }
 
        public void UpdateHash(string hash)
        {
            Hash = hash;
        }
 
        public override int GetHashCode() { return Path.GetHashCode(); }
 
        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            FolderHashEntry fhe = obj as FolderHashEntry;
            if (fhe == null) return false;
            else return Equals(fhe);
        }
 
        public bool Equals(FolderHashEntry other)
        {
            if (other == null) { return false; }
            return (this.Path.Equals(other.Path));
        }
    }
 
    [Serializable]
    public class FolderHash
    {
        public string Algorithm { get; private set; }
        public List<FolderHashEntry> Entries { get; private set; }
 
        public FolderHash(string algorithm)
        {
            Algorithm = algorithm;
            Entries = new List<FolderHashEntry>();
        }
    }
}
"@


Add-Type -TypeDefinition $FolderHashSource -Language CSharp
#endregion C# code


<#
.Synopsis
    Perform a deep copy on an object
#>

function Invoke-DeepCopy ($object)
{
    $ms = $null
    $newObject = $null

    try
    {
        # Serialize and Deserialize data using BinaryFormatter
        $ms = New-Object System.IO.MemoryStream
        $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
        $bf.Serialize($ms, $object)
        $ms.Position = 0
        $newObject = $bf.Deserialize($ms)
    }
    finally
    {
        if ($ms)
        {
            $ms.Dispose()
            $ms = $null
        }
    }

    $newObject
}


function Get-HashProviderFromString
{
    [CmdletBinding()]
    param (
        [ValidateSet('SHA1','SHA256', 'SHA384', 'SHA512')]
        [string]
        $Algorithm = 'SHA256'
    )

    $sha = $null

    switch ($Algorithm)
    {
        'SHA1'
        {
            $sha = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider
        }

        'SHA256'
        {
            $sha = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider
        }

        'SHA384'
        {
            $sha = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider
        }

        'SHA512'
        {
            $sha = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider
        }
    }
    
    return $sha
}

<#
.Synopsis
    For a given Path and Algorithm, returns a hash for the file.
    We write our own function because build machines are v3.0 and Get-FileHash in PowerShell is v4.0.
#>

function Get-FileHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string[]]
        $Path,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')]
        [string]
        $Algorithm = 'SHA256'
    )

    $sha = Get-HashProviderFromString -Algorithm $Algorithm

    $file = [System.IO.File]::Open($Path,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
    [System.BitConverter]::ToString($sha.ComputeHash($file)) -replace "-",""
    $file.Close()
}

<#
.Synopsis
    For a given path, returns a Microsoft.Solutions.IO.FolderHash object with an entry for each file found.
#>

function Get-FolderHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string[]]
        $Path,

        [string]
        $Filter,

        [string[]]
        $Exclude,

        [string[]]
        $Include,

        [switch]
        $Recurse,

        [switch]
        $NoHash,

        [switch]
        $AllowSkippedFiles,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')]
        [string]
        $Algorithm = 'SHA256'
    )

    $folderHash = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $Algorithm

    if (-not (Test-Path $Path))
    {
        throw "Path $Path does not exist or access denied."
    }

    $args = @{
        'Path' = $Path
        'Filter' = $Filter
        'Exclude' = $Exclude
        'Include' = $Include
        'Recurse' = $Recurse
        'ErrorAction' = 'SilentlyContinue'
    }

    $files = Get-ChildItem @args -File

    foreach ($file in $files)
    {
        $fullName = $file.FullName
        Write-Verbose "Processing: $fullName."

        try 
        {
            # remove $Path from name so we store only the relative path
            $relativePath = $fullName -replace [regex]::Escape($($Path))
            $lastWriteTimeUTCTicks = $file.LastWriteTimeUtc.Ticks
            $size = $file.Length
            if ($NoHash)
            {
                $hash = [Guid]::Empty
            }
            else
            {
                $hash = Get-FileHash -Path $fullName -Algorithm $Algorithm -ErrorAction Stop
            }
            $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $relativePath.TrimStart("\"), $lastWriteTimeUTCTicks, $size, $hash.Hash

            $folderHash.Entries.Add($entry)
        }
        catch [Microsoft.PowerShell.Commands.WriteErrorException]
        {
            if (-not $AllowSkippedFiles)
            {
                Write-Warning "WriteErrorException: $fullName."
                $ex = $_ | Select-Object *
                Write-Warning $ex
                throw
            }

            Write-Verbose "Skipping $fullName."
        }
        catch
        {
            Write-Warning "Error processing: $fullName."
            $ex = $_ | Select-Object *
            Write-Warning $ex
            throw
        }
    }

    $folderHash
}


<#
.Synopsis
    For a given path and Microsoft.Solutions.IO.FolderHash.Entries object or JSON file containing Entries,
    test each file and report files that are same, different, missing, extra files from the Entries list.
    Errors will be reported as skipped unless critical which will throw.
#>

function Test-FolderHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [string]
        $Filter,

        [string[]]
        $Exclude,

        [string[]]
        $Include,

        [switch]
        $Recurse,

        [switch]
        $NoHash,

        [switch]
        $AllowSkippedFiles,

        [Parameter(ParameterSetName='Entry', Mandatory=$true)]
        [Microsoft.Solutions.IO.FolderHash]
        $FolderHash,

        [Parameter(ParameterSetName='JSON', Mandatory=$true)]
        [string]
        $JsonFile
    )

    if ($JsonFile)
    {
        Write-Verbose "Processing JSON: $JsonFile."
        $fh = Get-Content $JsonFile -Raw | ConvertFrom-Json
        $folderHashSrc = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $fh.Algorithm
        $entries = $fh.Entries

        foreach ($entry in $entries) {
            $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $entry.Path, $entry.LastWriteTimeUTCTicks, $entry.Size, $entry.Hash
            $folderHashSrc.Entries.Add($entry)
        }
    }
    else
    {
        Write-Verbose "Processing Microsoft.Solutions.IO.FolderHash object."
        # use a clone so we do not change the original object
        $folderHashSrc = Invoke-DeepCopy $FolderHash
    }

    Write-Verbose "Found $($folderHashSrc.Entries.Count) entries."

    Write-Verbose "Reading files from: $Path"
    # always call with -NoHash first for performance reasons, so we don't hash files with different sizes or LastWriteTimeUTCTicks
    $args = @{
        'Path' = $Path
        'Filter' = $Filter
        'Exclude' = $Exclude
        'Include' = $Include
        'Recurse' = $Recurse
        'NoHash' = $true
        'Algorithm' = $folderHashSrc.Algorithm
    }
    $folderHashDst = Get-FolderHash @args

    Write-Verbose "Found $($folderHashDst.Entries.Count) entries."
    
    Write-Verbose "Starting test against $Path."
    # create new lists for each category we are tracking by moving entries from the original lists
    $same = New-Object System.Collections.Generic.List[Object]
    $different = New-Object System.Collections.Generic.List[Object]
    # could not process
    $skipped = New-Object System.Collections.Generic.List[Object]
    # in src, not in dst
    $missing = New-Object System.Collections.Generic.List[Object]
    # not in src, in dst
    $extra = New-Object System.Collections.Generic.List[Object]

    while ($folderHashSrc.Entries.Count)
    {
        $srcEntry = $folderHashSrc.Entries[0]
        Write-Verbose "Finding: $($srcEntry.Path)."
        $dstEntry = $folderHashDst.Entries | Where-Object {$_.Path -eq $srcEntry.Path}

        if ($dstEntry)
        {
            Write-Verbose "Found: $($dstEntry.Path)."

            # we save ticks which are in 100-ns units. we are not guaranteed this granularity when storing files
            # on unc paths, locally, etc. Use granularity to the second to optimize not calculating the hash.
            $srcSeconds = [long]($srcEntry.LastWriteTimeUTCTicks / 10000000) * 10000000
            $dstSeconds = [long]($dstEntry.LastWriteTimeUTCTicks / 10000000) * 10000000
            if ($srcSeconds -ne $dstSeconds)
            {
                # ok to report LastWriteTimeUTCTicks here
                Write-Verbose "LastWriteTimeUTCTicks mismatch: $($srcEntry.LastWriteTimeUTCTicks) -ne $($dstEntry.LastWriteTimeUTCTicks)."
                # different
                $different.Add($srcEntry)
            }
            elseif ($srcEntry.Size -ne $dstEntry.Size)
            {
                Write-Verbose "Size mismatch: $($srcEntry.Size) -ne $($dstEntry.Size)."
                # different
                $different.Add($srcEntry)
            }
            else {
                # try to get hash now
                $fullName = Join-Path $Path $dstEntry.Path
                try
                {
                    $hash = (Get-FileHash -Path $fullName -Algorithm $folderHashSrc.Algorithm -ErrorAction Stop).Hash
                    $dstEntry.UpdateHash($hash)

                    if ($srcEntry.Hash -ne $dstEntry.Hash)
                    {
                        Write-Verbose "Hash mismatch: $($srcEntry.Hash) -ne $($dstEntry.Hash)."
                        # different
                        $different.Add($srcEntry)
                    }
                    else
                    {
                        Write-Verbose "Same: $($srcEntry.Path)."
                        # same
                        $same.Add($srcEntry)
                    }
                }
                catch [Microsoft.PowerShell.Commands.WriteErrorException]
                {
                    if (-not $AllowSkippedFiles)
                    {
                        Write-Warning "WriteErrorException: $fullName."
                        $ex = $_ | Select-Object *
                        Write-Warning $ex
                        throw
                    }

                    Write-Verbose "Skipping $fullName."
                    # skipped
                    $skipped.Add($srcEntry)
                }
                catch
                {
                    Write-Warning "Error processing: $fullName."
                    $ex = $_ | Select-Object *
                    Write-Warning $ex
                    throw
                }
            }

            if (-not ($folderHashDst.Entries.Remove($dstEntry)))
            {
                throw "Error removing dstEntry from folderHashDst.Entries."
            }
        }
        else
        {
            Write-Verbose "Missing $($srcEntry.Path)."
            # missing
            $missing.Add($srcEntry)
        }

        if (-not ($folderHashSrc.Entries.Remove($srcEntry)))
        {
            throw "Error removing srcEntry from folderHashSrc.Entries."
        }
    }

    # anything left in the dst list is extra
    $extra.AddRange($folderHashDst.Entries)

    $results = [ordered]@{}
    $results.Add('NumFailures', ($different.Count + $skipped.Count + $missing.Count + $extra.Count))
    $results.Add('Same', $same)
    $results.Add('Different', $different)
    $results.Add('Skipped', $skipped)
    $results.Add('Missing', $missing)
    $results.Add('Extra', $extra)

    Write-Verbose "Test-FolderHash results:"
    Write-Verbose " Same: $($same.Count)"
    Write-Verbose " Different: $($different.Count)"
    Write-Verbose " Skipped: $($skipped.Count)"
    Write-Verbose " Missing: $($missing.Count)"
    Write-Verbose " Extra: $($extra.Count)"

    $results
}

function Get-FolderContentSummaryHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')]
        [string]
        $Algorithm = 'SHA256'
    )

    $folderHash = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $Algorithm

    if (-not (Test-Path $Path))
    {
        throw "Path $Path does not exist or access denied."
    }

    $args = @{
        'Path' = $Path
        'Recurse' = $true
        'ErrorAction' = 'SilentlyContinue'
    }

    $files = Get-ChildItem @args -File

    foreach ($file in $files)
    {
        $fullName = $file.FullName
        Write-Verbose "Processing: $fullName."

        try 
        {
            # Remove $Path from name so we store only the relative path
            $relativePath = $fullName -replace [regex]::Escape($($Path))
            $lastWriteTimeUTCTicks = $file.LastWriteTimeUtc.Ticks
            $size = $file.Length
            $hash = Get-FileHash -Path $fullName -Algorithm $Algorithm -ErrorAction Stop
            $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $relativePath.TrimStart("\"), $lastWriteTimeUTCTicks, $size, $hash.Hash

            $folderHash.Entries.Add($entry)
        }
        catch [Microsoft.PowerShell.Commands.WriteErrorException]
        {
            Write-Warning "WriteErrorException: $fullName."
            $ex = $_ | Select-Object *
            Write-Warning $ex
            throw
        }
        catch
        {
            Write-Warning "Error processing: $fullName."
            $ex = $_ | Select-Object *
            Write-Warning $ex
            throw
        }
    }

    # Sort the file list by name and then create a byte stream for hashing based on the names and hashes
    $fileNameAndHashBytes = new-object System.Collections.Generic.List[byte]
    foreach ($entry in $folderHash.Entries)
    {
        $fileNameAndHashBytes.AddRange([System.Text.Encoding]::UTF8.GetBytes($entry.Path))
        $fileNameAndHashBytes.AddRange([System.Text.Encoding]::UTF8.GetBytes($entry.Hash))
    }

    $sha = Get-HashProviderFromString -Algorithm $Algorithm

    return [System.BitConverter]::ToString($sha.ComputeHash($fileNameAndHashBytes.ToArray())) -replace "-",""
}

#region Exports
Export-ModuleMember Get-FolderContentSummaryHash
Export-ModuleMember Get-FolderHash
Export-ModuleMember Test-FolderHash
#endregion Exports

# SIG # Begin signature block
# MIInwgYJKoZIhvcNAQcCoIInszCCJ68CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBzh2j+pjx1Jn5O
# 2VuDyNN5fl8LCWpODT0gGlEJPNFZyqCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGaIwghmeAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEICOU4zc8snhm8wfpMSot7Zq3
# zUgclC+dSX4dQPrpBkjNMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAbx0cWnQr8ILejMQXAUeGB0MKGVct4OzPIOwfpU7slXQWyl+6+dXX8zlZ
# 4Q/jKGJ1NdzfhuZvyMnhfx24qvhXkIE44G0EPe7dd5c1VhC3atYrlbwCgaCL/0Oh
# VytrHMPboTBJsnd7SuUoMI+OTXo6FxAiR7BCJXLkefiA9ez0ZeJHPZ3FGaLMB0mc
# 239COHG5sMw5Y+JhfenZ/4nch1QHNhhZO89dI8FCu4tOow3BqEjRO7e957G+XtJN
# Zo2UBOajZxI9y+6763DxUXwXmNXW2KNforiO0N+tdjZIdsujDOiPBta1ymgbFszS
# zN3LT5QkvKv5SaiJHTZ/yhJJ9rxXKqGCFywwghcoBgorBgEEAYI3AwMBMYIXGDCC
# FxQGCSqGSIb3DQEHAqCCFwUwghcBAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCCAUi/OXIb3dDQuVm0OXPrRQoVrbYcWdRzXgH2mk0EisgIGZbqeulad
# GBMyMDI0MDIxMjE0MDYyOS4wNzlaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OjA4NDItNEJFNi1DMjlBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIRezCCBycwggUPoAMCAQICEzMAAAHajtXJWgDREbEAAQAAAdowDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNjU5WhcNMjUwMTEwMTkwNjU5WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowODQyLTRC
# RTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJOQBgh2tVFR1j8jQA4NDf8b
# cVrXSN080CNKPSQo7S57sCnPU0FKF47w2L6qHtwm4EnClF2cruXFp/l7PpMQg25E
# 7X8xDmvxr8BBE6iASAPCfrTebuvAsZWcJYhy7prgCuBf7OidXpgsW1y8p6Vs7sD2
# aup/0uveYxeXlKtsPjMCplHkk0ba+HgLho0J68Kdji3DM2K59wHy9xrtsYK+X9er
# bDGZ2mmX3765aS5Q7/ugDxMVgzyj80yJn6ULnknD9i4kUQxVhqV1dc/DF6UBeuzf
# ukkMed7trzUEZMRyla7qhvwUeQlgzCQhpZjz+zsQgpXlPczvGd0iqr7lACwfVGog
# 5plIzdExvt1TA8Jmef819aTKwH1IVEIwYLA6uvS8kRdA6RxvMcb//ulNjIuGceyy
# kMAXEynVrLG9VvK4rfrCsGL3j30Lmidug+owrcCjQagYmrGk1hBykXilo9YB8Qyy
# 5Q1KhGuH65V3zFy8a0kwbKBRs8VR4HtoPYw9z1DdcJfZBO2dhzX3yAMipCGm6Smv
# mvavRsXhy805jiApDyN+s0/b7os2z8iRWGJk6M9uuT2493gFV/9JLGg5YJJCJXI+
# yxkO/OXnZJsuGt0+zWLdHS4XIXBG17oPu5KsFfRTHREloR2dI6GwaaxIyDySHYOt
# vIydla7u4lfnfCjY/qKTAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUoXyNyVE9ZhOV
# izEUVwhNgL8PX0UwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBALmDVdTtuI0jAEt4
# 1O2OM8CU237TGMyhrGr7FzKCEFaXxtoqk/IObQriq1caHVh2vyuQ24nz3TdOBv7r
# cs/qnPjOxnXFLyZPeaWLsNuARVmUViyVYXjXYB5DwzaWZgScY8GKL7yGjyWrh78W
# JUgh7rE1+5VD5h0/6rs9dBRqAzI9fhZz7spsjt8vnx50WExbBSSH7rfabHendpeq
# bTmW/RfcaT+GFIsT+g2ej7wRKIq/QhnsoF8mpFNPHV1q/WK/rF/ChovkhJMDvlqt
# ETWi97GolOSKamZC9bYgcPKfz28ed25WJy10VtQ9P5+C/2dOfDaz1RmeOb27Kbeg
# ha0SfPcriTfORVvqPDSa3n9N7dhTY7+49I8evoad9hdZ8CfIOPftwt3xTX2RhMZJ
# CVoFlabHcvfb84raFM6cz5EYk+x1aVEiXtgK6R0xn1wjMXHf0AWlSjqRkzvSnRKz
# FsZwEl74VahlKVhI+Ci9RT9+6Gc0xWzJ7zQIUFE3Jiix5+7KL8ArHfBY9UFLz4sn
# boJ7Qip3IADbkU4ZL0iQ8j8Ixra7aSYfToUefmct3dM69ff4Eeh2Kh9NsKiiph58
# 9Ap/xS1jESlrfjL/g/ZboaS5d9a2fA598mubDvLD5x5PP37700vm/Y+PIhmp2fTv
# uS2sndeZBmyTqcUNHRNmCk+njV3nMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjow
# ODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAQqIfIYljHUbNoY0/wjhXRn/sSA2ggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOl0RNYwIhgPMjAyNDAyMTIxNTE4NDZaGA8yMDI0MDIxMzE1MTg0NlowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA6XRE1gIBADAKAgEAAgIBRwIB/zAHAgEAAgISkjAK
# AgUA6XWWVgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAEHMH8eDwQ5UEvYI
# N/9Kq6PkMHM1J4YpUps7G5vN9ACEiNOyRzo1u51vINRVtW35RkXd/FwSJxwy2FsB
# v/D0WmPV4Zm4NoWqDnB/Dpb4oBYpgmXmXTcFHbPKZldyI3/XMMJxnSLiJ6zmzbCt
# mhPIujUMoFKQpSrn7wOflyWjp5aaMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAHajtXJWgDREbEAAQAAAdowDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgvjsf5B4uJKHqdAgmEnBhfzL+moJoRqaj7/7ln5B+tYAwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCAipaNpYsDvnqTe95Dj1C09020I5ljibrW/ndIC
# Oxg9xjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# 2o7VyVoA0RGxAAEAAAHaMCIEIDnRr0iTxo7N39RYkJQjNKgSUzMlqo106/A0RY2l
# 63UJMA0GCSqGSIb3DQEBCwUABIICAIzSukqLMqjfb2mNIi8B9JWEErpL9bnR8eLo
# 2Q1zfeKjiiw/auNUTku1lf0ZOxzvTkay3CBwdsESGJwH/kg1tRn+XmyScnK9edNr
# yRgIdpKm6cnwN2bpIgzVLQ5yzDzvlPU/IGq4sHHotx24RNtSnOMYK9uvn8yDCadO
# iCpprBd9bu14s5YovXHK5hMyW+BndTYYm/DuVOBs37CiXROKYIGlOkXU86dR/b+O
# 29i43BVmFa3+5pIh9Z2PEnzEBk4anISwuhdannIkDnlWiz4oL3QsdJtRGDnpDnrZ
# I8XdfrJapHhr0X2vhAKAsZ4P6Umvkj78V5NmlYWxsTLiQyhK04M6H2xLbad9XaMl
# zLUhnV89m/ypOHCrLinNKrOPf36NkZzX1yBlD58jPCWi4cJ6KWlf6fh4Rp2Rshv9
# jak4iYhYfUdrglHn0N4vviNV5Sl3TuKHQy4TBeQ9Xb2/ctvy+Bi0zBhgeqWBBPZ/
# kT6Bdw/4BUQZj0JL6rwEAFn62MI5k9uVLmYn9pwrGir9wZOSbFYuaS81UkKGLaXq
# xq8BVuVMr1pWe01WYleaOY+HapPTzLjx1/lM4Zc+X5kvvPOulQ0IXptZKxtIHDxM
# nSSmz9SQhPtrtCfJtzQ3BWOrNQC3k9qo95lth3fqbqr8qbVK/lCKVMQ0JxfUjbHK
# 5r0u3z4m
# SIG # End signature block