Client/Start-PsFCIV.ps1

function Start-PsFCIV {
<#
.ExternalHelp PSPKI.Help.xml
#>

[CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [IO.DirectoryInfo]$Path,
        [Parameter(Mandatory = $true, Position = 1, ParameterSetName = '__xml')]
        [string]$XML,
        [Parameter(Position = 2)]
        [string]$Include = "*",
        [Parameter(Position = 3)]
        [string[]]$Exclude,
        [ValidateSet("Rename", "Delete")]
        [string]$Action,
        [ValidateSet("Bad", "Locked", "Missed", "New", "Ok", "Unknown", "All")]
        [String[]]$Show,
        [ValidateSet("MD5", "SHA1", "SHA256", "SHA384", "SHA512")]
        [AllowEmptyCollection()]
        [String[]]$HashAlgorithm = "SHA1",
        [switch]$Recurse,
        [switch]$Rebuild,
        [switch]$Quiet,
        [switch]$NoStatistic,
        [Parameter(ParameterSetName = '__online')]
        [switch]$Online
    )

#region C# wrappers
Add-Type @"
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace PsFCIV {
    public class StatTable {
        public List<String> Total = new List<String>();
        public List<String> New = new List<String>();
        public List<String> Ok = new List<String>();
        public List<String> Bad = new List<String>();
        public List<String> Missed = new List<String>();
        public List<String> Locked = new List<String>();
        public List<String> Unknown = new List<String>();
        public int Del;
    }
    public class IntStatTable {
        public Int32 Total;
        public Int32 New;
        public Int32 Ok;
        public Int32 Bad;
        public Int32 Missed;
        public Int32 Locked;
        public Int32 Unknown;
        public Int32 Del;
    }
    [XmlType(AnonymousType = true)]
    [XmlRoot(Namespace = "", IsNullable = false)]
    public class FCIV {
        public FCIV() { FILE_ENTRY = new List<FCIVFILE_ENTRY>(); }
         
        [XmlElement("FILE_ENTRY")]
        public List<FCIVFILE_ENTRY> FILE_ENTRY { get; set; }
    }
    [XmlType(AnonymousType = true)]
    public class FCIVFILE_ENTRY {
        public FCIVFILE_ENTRY() { }
        public FCIVFILE_ENTRY(string path) { name = path; }
 
        public String name { get; set; }
        public UInt64 Size { get; set; }
        public String TimeStamp { get; set; }
        public String MD5 { get; set; }
        public String SHA1 { get; set; }
        public String SHA256 { get; set; }
        public String SHA384 { get; set; }
        public String SHA512 { get; set; }
 
        public override Int32 GetHashCode() { return name.GetHashCode(); }
        public override Boolean Equals(Object other) {
            if (ReferenceEquals(null, other) || other.GetType() != GetType()) { return false; }
            return other.GetType() == GetType() && String.Equals(name, ((FCIVFILE_ENTRY)other).name);
        }
    }
}
"@
 -Debug:$false -Verbose:$false -ReferencedAssemblies "System.Xml"
Add-Type -AssemblyName System.Xml
#endregion
    
    if ($PSBoundParameters.Verbose) {$VerbosePreference = "continue"}
    if ($PSBoundParameters.Debug) {$DebugPreference = "continue"}
    # preserving current path
    $oldpath = $pwd.Path
    $Exclude += $XML

    if (Test-Path -LiteralPath $path) {
        Set-Location -LiteralPath $path
        if ($pwd.Provider.Name -ne "FileSystem") {
            Set-Location $oldpath
            throw "Specified path is not filesystem path. Try again!"
        }
    } else {throw "Specified path not found."}
    
    # statistic variables
    $sum = $new = New-Object PsFCIV.FCIV
    # creating statistics variable with properties. Each property will contain file names (and paths) with corresponding status.
    $global:stats = New-Object PsFCIV.StatTable
    $script:statcount = New-Object PsFCIV.IntStatTable
    
    # lightweight proxy function for Get-ChildItem cmdlet
    function dirx ([string]$Path, [string]$Filter, [string[]]$Exclude, $Recurse, [switch]$Force) {
        Get-ChildItem @PSBoundParameters -ErrorAction SilentlyContinue | Where-Object {!$_.psiscontainer}
    }    
    # internal function that will check whether the file is locked. All locked files are added to a group with 'Unknown' status.
    function __filelock ($file) {
        $locked = $false
        trap {Set-Variable -name locked -value $true -scope 1; continue}
        $inputStream = New-Object IO.StreamReader $file.FullName
        if ($inputStream) {$inputStream.Close()}
        if ($locked) {
            Write-Verbose "File $($file.Name) is locked. Skipping this file.."
            Write-Debug "File $($file.Name) is locked. Skipping this file.."
            __statcounter $filename Locked
        }
        $locked
    }    
    # internal function to generate UI window with results by using Out-GridView cmdlet.
    function __formatter ($props, $max) {
        $total = @($input)
        foreach ($property in $props) {
            $(for ($n = 0; $n -lt $max; $n++) {
                $total[0] | Select-Object @{n = $property; e = {$_.$property[$n]}}
            }) | Out-GridView -Title "File list by category: $property"
        }
    }
    # internal hasher
    function __hashbytes ($type, $file) {
        $hasher = [Security.Cryptography.HashAlgorithm]::Create($type)
        $inputStream = New-Object IO.StreamReader $file.FullName
        $hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
        $hasher.Clear()
        $inputStream.Close()
        $hashBytes
    }
    # internal function which reads the XML file (if exist).
    function __fromxml ($xml) {
    # reading existing XML file and selecting required properties
        if (!(Test-Path -LiteralPath $XML)) {return New-Object PsFCIV.FCIV}
        try {
            $fs = New-Object IO.FileStream $XML, "Open"
            $xmlser = New-Object System.Xml.Serialization.XmlSerializer ([Type][PsFCIV.FCIV])
            $sum = $xmlser.Deserialize($fs)
            $fs.Close()
            $sum
        } catch {
            Write-Error -Category InvalidData -Message "Input XML file is not valid FCIV XML file."
        } finally {
            if ($fs -ne $null) {$fs.Close()}
        }
        
    }
    # internal xml writer
    function __writexml ($sum) {
        if ($sum.FILE_ENTRY.Count -eq 0) {
            Write-Verbose "There is no data to write to XML database."
            Write-Debug "There is no data to write to XML database."
        } else {
            Write-Debug "Preparing to DataBase file creation..."
            try {
                $fs = New-Object IO.FileStream $XML, "Create"
                $xmlser = New-Object System.Xml.Serialization.XmlSerializer ([Type][PsFCIV.FCIV])
                $xmlser.Serialize($fs,$sum)
            } finally {
                if ($fs -ne $null) {$fs.Close()}
            }
            Write-Debug "DataBase file created..."
        }
    }
    # internal function to create XML entry object for a file.
    function __makeobject ($file, [switch]$NoHash, [switch]$hex) {
        Write-Debug "Starting object creation for '$($file.FullName)'..."
        $object = New-Object PsFCIV.FCIVFILE_ENTRY
        $object.name = $file.FullName -replace [regex]::Escape($($pwd.ProviderPath + "\"))
        $object.Size = $file.Length
        # use culture-invariant date/time format.
        $object.TimeStamp = "$($file.LastWriteTime.ToUniversalTime())"
        if (!$NoHash) {
        # calculating appropriate hash and convert resulting byte array to a Base64 string
            foreach ($hash in "MD5", "SHA1", "SHA256", "SHA384", "SHA512") {
                if ($HashAlgorithm -contains $hash) {
                    Write-Debug "Calculating '$hash' hash..."
                    $hashBytes = __hashbytes $hash $file
                    if ($hex) {
                        $object.$hash = -join ($hashBytes | Foreach-Object {"{0:X2}" -f $_})
                    } else {
                        Write-Debug ("Calculated hash value: " + (-join ($hashBytes | Foreach-Object {"{0:X2}" -f $_})))
                        $object.$hash = [System.Convert]::ToBase64String($hashBytes)
                    }
                }
            }
        }
        Write-Debug "Object created!"
        $object
    }    
    # internal function that calculates current file hash and formats it to an octet string (for example, B926D7416E8235E6F94F756E9F3AE2F33A92B2C4).
    function __precheck ($entry, $file, $HashAlgorithm) {
        if ($HashAlgorithm.Length -gt 0) {
            $SelectedHash = $HashAlgorithm
        } else {
            :outer foreach ($hash in "SHA512", "SHA384", "SHA256", "SHA1", "MD5") {
                if ($entry.$hash) {$SelectedHash = $hash; break outer}
            }
        }
        Write-Debug "Selected hash: $hash"
        -join ($(__hashbytes $SelectedHash $file) | ForEach-Object {"{0:X2}" -f $_})
        $SelectedHash
    }
    # process -Action parameter to perform an action against bad file (if actual file properties do not match the record in XML).
    function __takeaction ($file, $Action) {
        switch ($Action) {
            "Rename" {Rename-Item $file $($file.FullName + ".bad")}
            "Delete" {Remove-Item $file -Force}
        }
    }    
    # core file verification function.
    function __checkfiles ($entry, $file, $Action) {
        if (($file.Length -eq $entry.Size) -and ("$($file.LastWriteTime.ToUniversalTime())" -eq $entry.TimeStamp)) {
            $hexhash = __precheck $entry $file $HashAlgorithm
            $ActualHash = -join ([Convert]::FromBase64String($entry.($hexhash[1])) | ForEach-Object {"{0:X2}" -f $_})
            if (!$ActualHash) {
                Write-Verbose "XML database entry does not contains '$($hexhash[1])' hash value for the entry '$($entry.name)'."
                __statcounter $entry.name Unknown
                return
            } elseif ($ActualHash -eq $hexhash[0]) {
                Write-Debug "File hash: $ActualHash"
                Write-Verbose "File '$($file.name)' is ok."
                __statcounter $entry.name Ok
                return
            } else {
                Write-Debug "File '$($file.name)' failed hash verification.
                    Expected hash: $hexhash
                    Actual hash: $ActualHash"

                __statcounter $entry.name Bad
                if ($Action) {__takeaction $file $Action}
            }
        } else {
            Write-Verbose "File '$($file.FullName)' size or Modified Date/Time mismatch."
            Write-Debug "Expected file size is: $($entry.Size) byte(s), actual size is: $($file.Length) byte(s)."
            Write-Debug "Expected file modification time is: $($entry.TimeStamp), actual file modification time is: $($file.LastWriteTime.ToUniversalTime())"
            __statcounter $entry.name Bad
            if ($Action) {__takeaction $file $Action}
        }
    }
    # internal function to calculate resulting statistics and show if if necessary.
    function __stats {
    # if -Show parameter is presented we display selected groups (Total, New, Ok, Bad, Missed, Unknown)
        if ($show -and !$NoStatistic) {
            if ($Show -eq "All" -or $Show.Contains("All")) {
                $global:stats | __formatter "Bad", "Locked", "Missed", "New", "Ok", "Unknown" $script:statcount.Total
            } else {
                $global:stats | Select-Object $show | __formatter $show $script:statcount.Total
            }            
        }
        # script work in numbers
        if (!$Quiet) {
            Write-Host ----------------------------------- -ForegroundColor Green
            if ($Rebuild) {
                Write-Host Total entries processed: $script:statcount.Total -ForegroundColor Cyan
                Write-Host Total removed unused entries: $script:statcount.Del -ForegroundColor Yellow
            } else {Write-Host Total files processed: $script:statcount.Total -ForegroundColor Cyan}
            Write-Host Total new added files: $script:statcount.New -ForegroundColor Green
            Write-Host Total good files: $script:statcount.Ok -ForegroundColor Green
            Write-Host Total bad files: $script:statcount.Bad -ForegroundColor Red
            Write-Host Total unknown status files: $script:statcount.Unknown -ForegroundColor Yellow
            Write-Host Total missing files: $script:statcount.Missed -ForegroundColor Yellow
            Write-Host Total locked files: $script:statcount.Locked -ForegroundColor Yellow
            Write-Host ----------------------------------- -ForegroundColor Green
        }
        # restore original variables
        Set-Location -LiteralPath $oldpath
        $exit = 0
        # create exit code depending on check status
        if ($Rebuild) {$exit = [int]::MaxValue} else {
            if ($script:statcount.Bad -ne 0) {$exit += 1}
            if ($script:statcount.Missed -ne 0) {$exit += 2}
            if ($script:statcount.Unknown -ne 0) {$exit += 4}
            if ($script:statcount.Locked -ne 0) {$exit += 8}
        }
        if ($Quiet) {exit $exit}
    }
    # internal function to update statistic counters.
    function __statcounter ($filename, $status) {
        $script:statcount.$status++
        $script:statcount.Total++
        if (!$NoStatistic) {
            $global:stats.$status.Add($filename)
        }
    }
    if ($Online) {
        Write-Debug "Online mode ON"
        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            Write-Verbose "Perform file '$($_.fullName)' checking."
            $file = Get-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
            if (__filelock $file) {return}
            __makeobject $file -hex
        }
        return
    }

    <#
    in this part we perform XML file update by removing entries for non-exist files and
    adding new entries for files that are not in the database.
    #>

    if ($Rebuild) {
        Write-Debug "Rebuild mode ON"
        if (Test-Path -LiteralPath $xml) {
            $old = __fromxml $xml
        } else {
            Set-Location $oldpath
            throw "Unable to find XML file. Please, run the command without '-Rebuild' switch."
        }
        $interm = New-Object PsFCIV.FCIV
        # use foreach-object instead of where-object to keep original types.
        Write-Verbose "Perform DB file cleanup from non-existent items."
        $old.FILE_ENTRY | ForEach-Object {
            if ((Test-Path -LiteralPath $_.name)) {
                if ($_.name -eq $xml) {
                    Write-Debug "File '$($_.name)' is DB file. Removed."
                } else {
                    $interm.FILE_ENTRY.Add($_)
                }
            } else {
                Write-Debug "File '$($_.name)' does not exist. Removed."
            }
        }
        $script:statcount.Del = $interm.Length
        $script:statcount.Total = $old.FILE_ENTRY.Count - $interm.Length
        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            Write-Verbose "Perform file '$($_.FullName)' checking."
            $file = Get-Item -LiteralPath $_.FullName -Force
            if (__filelock $file) {return}
            $filename = $file.FullName -replace [regex]::Escape($($pwd.providerpath + "\"))
            if ($interm.FILE_ENTRY.Contains((New-Object PsFCIV.FCIVFILE_ENTRY $filename))) {
                Write-Verbose "File '$filename' already exist in XML database. Skipping."
                return
            } else {
                $new.FILE_ENTRY.Add((__makeobject $file))
                Write-Verbose "File '$filename' is added."
                __statcounter $filename New
            }
        }
        $interm.FILE_ENTRY.AddRange($new.FILE_ENTRY)
        __writexml $interm
        __stats
        return
    }
    
    # this part contains main routine
    $sum = __fromxml $xml
    <#
    check XML file format. If Size property of the first element is zero, then the file was generated by
    original FCIV.exe tool. In this case we transform existing XML to a new PsFCIV format by adding new
    properties. Each record is checked against hashes stored in the source XML file. If hash check fails,
    an item is removed from final XML.
    #>

    if ($sum.FILE_ENTRY.Count -gt 0 -and $sum.FILE_ENTRY[0].Size -eq 0) {
        #
        if ($PSBoundParameters.ContainsKey("HashAlgorithm")) {
            $HashAlgorithm = $HashAlgorithm[0].ToUpper()
        } else {
            $HashAlgorithm = @()
        }
        Write-Debug "FCIV (compatibility) mode ON"
        if ($HashAlgorithm -and $HashAlgorithm -notcontains "sha1" -and $HashAlgorithm -notcontains "md5") {
            throw "Specified hash algorithm (or algorithms) is not supported. For native FCIV source, use MD5 and/or SHA1."
        }
        for ($index = 0; $index -lt $sum.FILE_ENTRY.Count; $index++) {
            Write-Verbose "Perform file '$($sum.FILE_ENTRY[$index].name)' checking."
            $filename = $sum.FILE_ENTRY[$index].name
            # check if the path is absolute and matches current path. If the path is absolute and does not belong to
            # current path -- skip this entry.
            if ($filename.Contains(":") -and $filename -notmatch [regex]::Escape($pwd.ProviderPath)) {return}
            # if source file name record contains absolute path, and belongs to the current pathe,
            # just strip base path. New XML format uses relative paths only.
            if ($filename.Contains(":")) {$filename = $filename -replace ([regex]::Escape($($pwd.ProviderPath + "\")))}
            # Test if the file exist. If the file does not exist, skip the current entry and process another record.
            if (!(Test-Path -LiteralPath $filename)) {
                Write-Verbose "File '$filename' not found. Skipping."
                __statcounter $filename Missed
                return
            }
            # get file item and test if it is not locked by another application
            $file = Get-Item -LiteralPath $filename -Force -ErrorAction SilentlyContinue
            if (__filelock $file) {return}
            # create new-style entry record that stores additional data: file length and last modification timestamp.
            $entry = __makeobject $file -NoHash
            $entry.name = $filename
            # process current hash entries and copy required hash values to a new entry object.
            "SHA1", "MD5" | ForEach-Object {$entry.$_ = $sum.FILE_ENTRY[$index].$_}
            $sum.FILE_ENTRY[$index] = $entry
            __checkfiles $newentry $file $Action
        }
        # we are done. Overwrite XML, display stats and exit.
        __writexml $sum
        # display statistics and exit right now.
        __stats
    }
    # if XML file exist, proccess and check all records. XML file will not be modified.
    if ($sum.FILE_ENTRY.Count -gt 0) {
        Write-Debug "Native PsFCIV mode ON"
        # this part is executed only when we want to process certain file. Wildcards are not allowed.
        if ($Include -ne "*") {
            $sum.FILE_ENTRY | Where-Object {$_.name -like $Include} | ForEach-Object {
                Write-Verbose "Perform file '$($_.name)' checking."
                $entry = $_
                # calculate the hash if the file exist.
                if (Test-Path -LiteralPath $entry.name) {
                    # and check file integrity
                    $file = Get-Item -LiteralPath $entry.name -Force -ErrorAction SilentlyContinue
                    __checkfiles $entry $file $Action
                } else {
                    # if there is no record for the file, skip it and display appropriate message
                    Write-Verbose "File '$filename' not found. Skipping."
                    __statcounter $entry.name Missed
                }
            }
        } else {
            $sum.FILE_ENTRY | ForEach-Object {
                <#
                to process files only in the current directory (without subfolders), we remove items
                that contain slashes from the process list and continue regular file checking.
                #>

                if (!$Recurse -and $_.name -match "\\") {return}
                Write-Verbose "Perform file '$($_.name)' checking."
                $entry = $_
                if (Test-Path -LiteralPath $entry.name) {
                    $file = Get-Item -LiteralPath $entry.name -Force -ErrorAction SilentlyContinue
                    __checkfiles $entry $file $Action
                } else {
                    Write-Verbose "File '$($entry.name)' not found. Skipping."
                    __statcounter $entry.name Missed
                }
            }
        }
    } else {
        # if there is no existing XML DB file, start from scratch and create a new one.
        Write-Debug "New XML mode ON"

        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            $_
            # Write-Verbose "Perform file '$($_.fullName)' checking."
            # $file = Get-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
            # if (__filelock $file) {return}
            # $entry = __makeobject $file
            # $sum.FILE_ENTRY.Add($entry)
            # __statcounter $entry.name New
        }
        __writexml $sum
    }
    __stats
}
# SIG # Begin signature block
# MIIfhgYJKoZIhvcNAQcCoIIfdzCCH3MCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDyiOQ9k5O1jC7L
# 65ADm7KbGxbYomcT8rhMn+L93frsTKCCGYYwggX1MIID3aADAgECAhAdokgwb5sm
# GNCC4JZ9M9NqMA0GCSqGSIb3DQEBDAUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRo
# ZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xODExMDIwMDAwMDBaFw0zMDEyMzEyMzU5
# NTlaMHwxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIx
# EDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEkMCIG
# A1UEAxMbU2VjdGlnbyBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEAhiKNMoV6GJ9J8JYvYwgeLdx8nxTP4ya2JWYpQIZU
# RnQxYsUQ7bKHJ6aZy5UwwFb1pHXGqQ5QYqVRkRBq4Etirv3w+Bisp//uLjMg+gwZ
# iahse60Aw2Gh3GllbR9uJ5bXl1GGpvQn5Xxqi5UeW2DVftcWkpwAL2j3l+1qcr44
# O2Pej79uTEFdEiAIWeg5zY/S1s8GtFcFtk6hPldrH5i8xGLWGwuNx2YbSp+dgcRy
# QLXiX+8LRf+jzhemLVWwt7C8VGqdvI1WU8bwunlQSSz3A7n+L2U18iLqLAevRtn5
# RhzcjHxxKPP+p8YU3VWRbooRDd8GJJV9D6ehfDrahjVh0wIDAQABo4IBZDCCAWAw
# HwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFA7hOqhT
# OjHVir7Bu61nGgOFrTQOMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/
# AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMDBggrBgEFBQcDCDARBgNVHSAECjAIMAYG
# BFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29t
# L1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUF
# BwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VT
# RVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2Nz
# cC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQBNY1DtRzRKYaTb3moq
# jJvxAAAeHWJ7Otcywvaz4GOz+2EAiJobbRAHBE++uOqJeCLrD0bs80ZeQEaJEvQL
# d1qcKkE6/Nb06+f3FZUzw6GDKLfeL+SU94Uzgy1KQEi/msJPSrGPJPSzgTfTt2Sw
# piNqWWhSQl//BOvhdGV5CPWpk95rcUCZlrp48bnI4sMIFrGrY1rIFYBtdF5KdX6l
# uMNstc/fSnmHXMdATWM19jDTz7UKDgsEf6BLrrujpdCEAJM+U100pQA1aWy+nyAl
# EA0Z+1CQYb45j3qOTfafDh7+B1ESZoMmGUiVzkrJwX/zOgWb+W/fiH/AI57SHkN6
# RTHBnE2p8FmyWRnoao0pBAJ3fEtLzXC+OrJVWng+vLtvAxAldxU0ivk2zEOS5LpP
# 8WKTKCVXKftRGcehJUBqhFfGsp2xvBwK2nxnfn0u6ShMGH7EezFBcZpLKewLPVdQ
# 0srd/Z4FUeVEeN0B3rF1mA1UJP3wTuPi+IO9crrLPTru8F4XkmhtyGH5pvEqCgul
# ufSe7pgyBYWe6/mDKdPGLH29OncuizdCoGqC7TtKqpQQpOEN+BfFtlp5MxiS47V1
# +KHpjgolHuQe8Z9ahyP/n6RRnvs5gBHN27XEp6iAb+VT1ODjosLSWxr6MiYtaldw
# HDykWC6j81tLB9wyWfOHpxptWDCCBkowggUyoAMCAQICEBdBS6OH2/E/xEs3Bf5c
# krcwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0
# ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMSQwIgYDVQQDExtTZWN0aWdvIFJTQSBDb2RlIFNpZ25pbmcgQ0Ew
# HhcNMTkwODEzMDAwMDAwWhcNMjIwODEyMjM1OTU5WjCBmTELMAkGA1UEBhMCVVMx
# DjAMBgNVBBEMBTk3MjE5MQ8wDQYDVQQIDAZPcmVnb24xETAPBgNVBAcMCFBvcnRs
# YW5kMRwwGgYDVQQJDBMxNzEwIFNXIE1pbGl0YXJ5IFJkMRswGQYDVQQKDBJQS0kg
# U29sdXRpb25zIEluYy4xGzAZBgNVBAMMElBLSSBTb2x1dGlvbnMgSW5jLjCCAiIw
# DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANC9ao+Uw7Owaxi+v5FF1+eKGIpv
# QnKBFu61VsoHFyotJ8yoeC8tiRjmHggRbmQm0sTAdAXw23Rj5ZW6ndMWgA258car
# a6+oWB071e3ctsHoavc7NkDoCkKS2uh5tTmqclNMg6xaU1IIp9IWFq00K1jkeXex
# HIFLjTF2AA2SEteJO6VY08EiN6ktAOa1P4NbB0fTRUmca0j3W552hvU5Ig8G0DJt
# b4IDMMnu6WllNuxfqyNJiUOYkDET1p52XzvhMFMFnhbsH9JPcR4IA7Pp4xc1mRhe
# D9uE+KVx1astA/GvWtkpeZy/efbaMOxY4VuTW9kdgc8tB4VPamQQpoVmD3ULsaPz
# iv8cOum0CMrTtwKA/meas20A69u3xg8KeuDwxE0rysT4a68lXjFZViyHQQQzeZi4
# wAifk3URIABuKy6DQdQ4FJRjIvAXh5PD2WatY7aJJw9nc0biEB7bEjDNYufJ4OL9
# M9ibVqQxpLz0Vm9D+aCD1CJFySCcIOg7VRWCNyTqtDxDlWd6I7H1s2QwsiEWIOCE
# MtOlve+rZi9RgJhtrdoINgmgSPNH+lITexCMrNDvpEzYxggsTLcEs4jq6XzoD/bR
# G9gvSv/d5Di8Js0gjaqpwDZbLsProdRFX0AlAROarTVW0m9nqVHcP4o0Lc/jKCJ6
# 8073khO+aMOJKW/9AgMBAAGjggGoMIIBpDAfBgNVHSMEGDAWgBQO4TqoUzox1Yq+
# wbutZxoDha00DjAdBgNVHQ4EFgQUd9YCgc1i67qdUtY6jeRnT0YzsVAwDgYDVR0P
# AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEQYJ
# YIZIAYb4QgEBBAQDAgQQMEAGA1UdIAQ5MDcwNQYMKwYBBAGyMQECAQMCMCUwIwYI
# KwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMEMGA1UdHwQ8MDowOKA2
# oDSGMmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1JTQUNvZGVTaWduaW5n
# Q0EuY3JsMHMGCCsGAQUFBwEBBGcwZTA+BggrBgEFBQcwAoYyaHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUlNBQ29kZVNpZ25pbmdDQS5jcnQwIwYIKwYBBQUH
# MAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCAGA1UdEQQZMBeBFWluZm9AcGtp
# c29sdXRpb25zLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAa4IZBlHU1V6Dy+atjrwS
# YugL+ryvzR1eGH5+nzbwxAi4h3IaknQBIuWzoamR+hRUga9/Rd4jrBbXGTgkqM7A
# tnzXP7P5NZOmxOdFOl1UfgNIv5MfJNPzsvn54bnx9rgKWJlpmKPCr1xtfj2ERlhA
# f6ADOfUyCcTnSwlBi1Bai60wqqDPuj1zcDaD2XGddVmqVrplx1zNoX7vhyErA7V9
# psRWQYIflYY0L58gposEUVMKM6TJRRjndibRnO2CI9plXDBz4j3cTni3fXGM3UuB
# VInKSeC+mTsvJVYTHjBowWohhxMBdqD0xFVbysoRKGtWSJwErdAomjMCrY2q6oYc
# xzCCBmowggVSoAMCAQICEAMBmgI6/1ixa9bV6uYX8GYwDQYJKoZIhvcNAQEFBQAw
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBD
# QS0xMB4XDTE0MTAyMjAwMDAwMFoXDTI0MTAyMjAwMDAwMFowRzELMAkGA1UEBhMC
# VVMxETAPBgNVBAoTCERpZ2lDZXJ0MSUwIwYDVQQDExxEaWdpQ2VydCBUaW1lc3Rh
# bXAgUmVzcG9uZGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2Rd
# /Hyz4II14OD2xirmSXU7zG7gU6mfH2RZ5nxrf2uMnVX4kuOe1VpjWwJJUNmDzm9m
# 7t3LhelfpfnUh3SIRDsZyeX1kZ/GFDmsJOqoSyyRicxeKPRktlC39RKzc5YKZ6O+
# YZ+u8/0SeHUOplsU/UUjjoZEVX0YhgWMVYd5SEb3yg6Np95OX+Koti1ZAmGIYXIY
# aLm4fO7m5zQvMXeBMB+7NgGN7yfj95rwTDFkjePr+hmHqH7P7IwMNlt6wXq4eMfJ
# Bi5GEMiN6ARg27xzdPpO2P6qQPGyznBGg+naQKFZOtkVCVeZVjCT88lhzNAIzGvs
# YkKRrALA76TwiRGPdwIDAQABo4IDNTCCAzEwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwggG/BgNVHSAEggG2MIIB
# sjCCAaEGCWCGSAGG/WwHATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRp
# Z2ljZXJ0LmNvbS9DUFMwggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBz
# AGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBv
# AG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAg
# AHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAg
# AHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBt
# AGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0
# AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABo
# AGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9
# bAMVMB8GA1UdIwQYMBaAFBUAEisTmLKZB+0e36K+Vw0rZwLNMB0GA1UdDgQWBBRh
# Wk0ktkkynUoqeRqDS/QeicHKfTB9BgNVHR8EdjB0MDigNqA0hjJodHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURDQS0xLmNybDA4oDagNIYy
# aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5j
# cmwwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCd
# JX4bM02yJoFcm4bOIyAPgIfliP//sdRqLDHtOhcZcRfNqRu8WhY5AJ3jbITkWkD7
# 3gYBjDf6m7GdJH7+IKRXrVu3mrBgJuppVyFdNC8fcbCDlBkFazWQEKB7l8f2P+fi
# EUGmvWLZ8Cc9OB0obzpSCfDscGLTYkuw4HOmksDTjjHYL+NtFxMG7uQDthSr849D
# p3GdId0UyhVdkkHa+Q+B0Zl0DSbEDn8btfWg8cZ3BigV6diT5VUW8LsKqxzbXEgn
# Zsijiwoc5ZXarsQuWaBh3drzbaJh6YoLbewSGL33VVRAA5Ira8JRwgpIr7DUbuD0
# FAo6G+OPPcqvao173NhEMIIGzTCCBbWgAwIBAgIQBv35A5YDreoACus/J7u6GzAN
# BgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg
# SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2Vy
# dCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMjExMTEwMDAw
# MDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVk
# IElEIENBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDogi2Z+crC
# QpWlgHNAcNKeVlRcqcTSQQaPyTP8TUWRXIGf7Syc+BZZ3561JBXCmLm0d0ncicQK
# 2q/LXmvtrbBxMevPOkAMRk2T7It6NggDqww0/hhJgv7HxzFIgHweog+SDlDJxofr
# Nj/YMMP/pvf7os1vcyP+rFYFkPAyIRaJxnCI+QWXfaPHQ90C6Ds97bFBo+0/vtuV
# SMTuHrPyvAwrmdDGXRJCgeGDboJzPyZLFJCuWWYKxI2+0s4Grq2Eb0iEm09AufFM
# 8q+Y+/bOQF1c9qjxL6/siSLyaxhlscFzrdfx2M8eCnRcQrhofrfVdwonVnwPYqQ/
# MhRglf0HBKIJAgMBAAGjggN6MIIDdjAOBgNVHQ8BAf8EBAMCAYYwOwYDVR0lBDQw
# MgYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUF
# BwMIMIIB0gYDVR0gBIIByTCCAcUwggG0BgpghkgBhv1sAAEEMIIBpDA6BggrBgEF
# BQcCARYuaHR0cDovL3d3dy5kaWdpY2VydC5jb20vc3NsLWNwcy1yZXBvc2l0b3J5
# Lmh0bTCCAWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAg
# AHQAaABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0
# AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUAIABE
# AGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEAbgBkACAAdABoAGUAIABS
# AGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3
# AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkAYQBiAGkAbABpAHQAeQAgAGEAbgBk
# ACAAYQByAGUAIABpAG4AYwBvAHIAcABvAHIAYQB0AGUAZAAgAGgAZQByAGUAaQBu
# ACAAYgB5ACAAcgBlAGYAZQByAGUAbgBjAGUALjALBglghkgBhv1sAxUwEgYDVR0T
# AQH/BAgwBgEB/wIBADB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0f
# BHoweDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz
# dXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDAdBgNVHQ4EFgQUFQASKxOYspkH
# 7R7for5XDStnAs0wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJ
# KoZIhvcNAQEFBQADggEBAEZQPsm3KCSnOB22WymvUs9S6TFHq1Zce9UNC0Gz7+x1
# H3Q48rJcYaKclcNQ5IK5I9G6OoZyrTh4rHVdFxc0ckeFlFbR67s2hHfMJKXzBBlV
# qefj56tizfuLLZDCwNK1lL1eT7EF0g49GqkUW6aGMWKoqDPkmzmnxPXOHXh2lCVz
# 5Cqrz5x2S+1fwksW5EtwTACJHvzFebxMElf+X+EevAJdqP77BzhPDcZdkbkPZ0XN
# 1oPt55INjbFpjE/7WeAjD9KqrgB87pxCDs+R1ye3Fu4Pw718CqDuLAhVhSK46xga
# TfwqIa1JMYNHlXdx3LEbS0scEJx3FMGdTy9alQgpECYxggVWMIIFUgIBATCBkDB8
# MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
# VQQHEwdTYWxmb3JkMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxJDAiBgNVBAMT
# G1NlY3RpZ28gUlNBIENvZGUgU2lnbmluZyBDQQIQF0FLo4fb8T/ESzcF/lyStzAN
# BglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqG
# SIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3
# AgEVMC8GCSqGSIb3DQEJBDEiBCA63cuPOq4qhiGqn3stsxzHBnWw2b2Mc2um2b/L
# sDm76TANBgkqhkiG9w0BAQEFAASCAgBvpeZgLCURMeK25TC2UibNQqRPLLyXNosm
# NOPyZRZmD+5xp8S+JX003xtfhhwzhoiOND/iI8UpkfS0MoZJllZgCV19H7cqTe8G
# n5PaR69GlzUaazNHMa2mzMH3jaLVLQWcqx4k+Gpevof+StKdGci4uTo/mTpsh7WU
# BtXB38evuRdbrjmETZRx+pCZx+2yKxEipSSquv4BFfqX5h1w/Ds7+lRfMhQtAU/8
# wYKXy1mLA0HMj6kN08/wIEKgIB0tFN8WyvbKsumYWbXxfge+PszvnjK/Fj6eHhmK
# 4nY8O2JYRmkKuJlTa6acSG8KjW/J1volaH7EdfjiK5Q9X4Fb1Zpw0oi1j5KmZq0P
# wKi+KSowkOYQ5lDSIf5wIoyg0tcitq6Ykak092zgRk7nSEZwOFWkYWUVXfeSPkmj
# hzrF6zh2hfS7Qn130Fx0GAjv3S2Ek5NYrdSMLS9lSsfviKv1wb1shwvedbjUQpoh
# ly4Go49pdd6eJZcFszXNI4hTp00znlfgGfbsftMVF4oIgm3LpCF4B1Y6hGngd57B
# vsbgq394NbaaRbJaln154tPrRvLRt7zhU0XTSMjMK/euQlbrfsxrzrAAF8OrFkbs
# XgiZpRykkT5ncUa0i+yA1/dIUUMtTl8flUGOfqeItiNCmpcAC4cUJXtwQ9ITdJ6y
# +xad2r/qtKGCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNV
# BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp
# Y2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGa
# Ajr/WLFr1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3
# DQEHATAcBgkqhkiG9w0BCQUxDxcNMTkwODI2MTAxOTUzWjAjBgkqhkiG9w0BCQQx
# FgQURzS7eSl2kYcVWfZrUZKhHt95878wDQYJKoZIhvcNAQEBBQAEggEAkJbYdndx
# wFasp7qqjrONlmwYMpBa41gc9LgFuDdSfAkXUyBDUBC63L5hJvZLxhkzACx44zGv
# bBzNk+lyke13y5ZWZB2CrblJd/vClfGGv4Od9ne8OxA6fkGdBTOR4JobBAWo1Oug
# HAzQmpyR6pOIn5MlClMrnbcIIkWdKcuKmwwD3XIRdXiIenBALv5usM6pw5Okgw66
# d1Z0Hx+tXMA0J4yXqcoN0xirysf6ScYoCOLv8bcEgYlrkzjg+uTqEHBYAHnV1YRi
# IUzUHvoX4b/ReB4ZzpIIhAbCcZgrMe1Nwf7qXd4Zjyh9clTxIArCTrP6d63R+i9h
# rCJhMDe3ufjktw==
# SIG # End signature block