NuGet/NuGetFeedClass.ps1

#requires -Version 5.0

[NuGetFeed[]] $NuGetFeedCache = @()

# PROOF OF CONCEPT PREVIEW: This class holds the connection to a NuGet feed
class NuGetFeed {

    [string] $url
    [string] $token
    [string[]] $patterns
    [string[]] $fingerprints

    [string] $searchQueryServiceUrl
    [string] $packagePublishUrl
    [string] $packageBaseAddressUrl

    [hashtable] $orgType = @{}

    [hashtable] $searchResultsCache = @{}
    [int]       $searchResultsCacheRetentionPeriod
    [string]    $cacheFolder

    NuGetFeed([string] $nuGetServerUrl, [string] $nuGetToken, [string[]] $patterns, [string[]] $fingerprints, [int] $nuGetSearchResultsCacheRetentionPeriod, [string] $nuGetCacheFolder) {
        $this.url = $nuGetServerUrl
        $this.token = $nuGetToken
        $this.patterns = $patterns
        $this.fingerprints = $fingerprints
        $this.searchResultsCacheRetentionPeriod = $nuGetSearchResultsCacheRetentionPeriod
        $this.cacheFolder = $nugetCacheFolder

        # When trusting nuget.org, you should only trust packages signed by an author or packages matching a specific pattern (like using a registered prefix or a full name)
        if ($nuGetServerUrl -like 'https://api.nuget.org/*' -and $patterns.Contains('*') -and (!$fingerprints -or $fingerprints.Contains('*'))) {
            throw "Trusting all packages on nuget.org is not supported"
        }

        try {
            $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
            $capabilities = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $this.url
            $global:ProgressPreference = $prev
            $this.searchQueryServiceUrl = $capabilities.resources | Where-Object { $_.'@type' -eq 'SearchQueryService' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1
            if (!$this.searchQueryServiceUrl) {
                # Azure DevOps doesn't support SearchQueryService, but SearchQueryService/3.0.0-beta
                $this.searchQueryServiceUrl = $capabilities.resources | Where-Object { $_.'@type' -eq 'SearchQueryService/3.0.0-beta' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1
            }
            $this.packagePublishUrl = $capabilities.resources | Where-Object { $_."@type" -eq 'PackagePublish/2.0.0' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1
            $this.packageBaseAddressUrl = $capabilities.resources | Where-Object { $_."@type" -eq 'PackageBaseAddress/3.0.0' } | Select-Object -ExpandProperty '@id' | Select-Object -First 1
            if (!$this.searchQueryServiceUrl -or !$this.packagePublishUrl -or !$this.packageBaseAddressUrl) {
                Write-Host "Capabilities of NuGet server $($this.url) are not supported"
                $capabilities.resources | ForEach-Object { Write-Host "- $($_.'@type')"; Write-Host "-> $($_.'@id')" }
            }
            Write-Verbose "Capabilities of NuGet server $($this.url) are:"
            Write-Verbose "- SearchQueryService=$($this.searchQueryServiceUrl)"
            Write-Verbose "- PackagePublish=$($this.packagePublishUrl)"
            Write-Verbose "- PackageBaseAddress=$($this.packageBaseAddressUrl)"
        }
        catch {
            throw (GetExtendedErrorMessage $_)
        }
    }

    static [NuGetFeed] Create([string] $nuGetServerUrl, [string] $nuGetToken, [string[]] $patterns, [string[]] $fingerprints, [int] $nuGetSearchResultsCacheRetentionPeriod, [string] $nuGetCacheFolder) {
        $nuGetFeed = $script:NuGetFeedCache | Where-Object { $_.url -eq $nuGetServerUrl -and $_.token -eq $nuGetToken -and (-not (Compare-Object $_.patterns $patterns)) -and (-not (Compare-Object $_.fingerprints $fingerprints)) -and $_.searchResultsCacheRetentionPeriod -eq $nuGetSearchResultsCacheRetentionPeriod }
        if (!$nuGetFeed) {
            $nuGetFeed = [NuGetFeed]::new($nuGetServerUrl, $nuGetToken, $patterns, $fingerprints, $nuGetSearchResultsCacheRetentionPeriod, $nugetCacheFolder)
            $script:NuGetFeedCache += $nuGetFeed
        }
        return $nuGetFeed
    }

    [void] Dump([string] $message) {
        Write-Host $message
    }

    [hashtable] GetHeaders() {
        $headers = @{
            "Content-Type" = "application/json; charset=utf-8"
        }
        # nuget.org only support anonymous access
        if ($this.token -and $this.url -notlike 'https://api.nuget.org/*') {
            $headers += @{
                "Authorization" = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("user:$($this.token)")))"
            }
        }
        return $headers
    }

    [bool] IsTrusted([string] $packageId) {
        return ($packageId -and ($this.patterns | Where-Object { $packageId -like $_ }))
    }

    [hashtable[]] Search([string] $packageName) {
        $useCache = $this.searchResultsCacheRetentionPeriod -gt 0
        $hasCache = $this.searchResultsCache.ContainsKey($packageName)
        if ($hasCache) {
            if ($useCache) {
                # Clear cache older than the retention period
                $clearCache = $this.searchResultsCache[$packageName].timestamp.AddSeconds($this.searchResultsCacheRetentionPeriod) -lt (Get-Date)
            } else {
                # Clear cache if we are not using it
                $clearCache = $true
            }
            if ($clearCache) {
                $this.searchResultsCache.Remove($packageName)
                $hasCache = $false
            }
        }

        if ($useCache -and $hasCache) { 
            Write-Host "Search package using cache"
            $matching = $this.searchResultsCache[$packageName].matching
        } 
        elseif ($this.searchQueryServiceUrl -match '^https://nuget.pkg.github.com/(.*)/query$') {
            # GitHub support for SearchQueryService is unstable and is not usable
            # use GitHub API instead
            # GitHub API unfortunately doesn't support filtering, so we need to filter ourselves
            $organization = $matches[1]
            $headers = @{
                "Accept" = "application/vnd.github+json"
                "X-GitHub-Api-Version" = "2022-11-28"
            }
            if ($this.token) {
                $headers += @{
                    "Authorization" = "Bearer $($this.token)"
                }
            }
            if (-not $this.orgType.ContainsKey($organization)) {
                $orgMetadata = Invoke-RestMethod -Method GET -Headers $headers -Uri "https://api.github.com/users/$organization"
                if ($orgMetadata.type -eq 'Organization') {
                    $this.orgType[$organization] = 'orgs'
                }
                else {
                    $this.orgType[$organization] = 'users'
                }
            }
            $cacheKey = "GitHubPackages:$($this.orgType[$organization])/$organization"
            $matching = @()
            if ($this.searchResultsCacheRetentionPeriod -gt 0 -and $this.searchResultsCache.ContainsKey($cacheKey)) {
                if ($this.searchResultsCache[$cacheKey].timestamp.AddSeconds($this.searchResultsCacheRetentionPeriod) -lt (Get-Date)) {
                    Write-Host "Cache expired, removing cache $cacheKey"
                    $this.searchResultsCache.Remove($cacheKey)
                }
                else {
                    Write-Host "Search available packages using cache $cacheKey"
                    $matching = $this.searchResultsCache[$cacheKey].matching
                    Write-Host "$($matching.Count) packages found"
                }
            }
            if (-not $matching) {
                $per_page = 50
                $queryUrl = "https://api.github.com/$($this.orgType[$organization])/$organization/packages?package_type=nuget&per_page=$($per_page)&page="
                $page = 1
                $matching = @()
                while ($true) {
                    Write-Host -ForegroundColor Yellow "Search package using $queryUrl$page"
                    $result = Invoke-RestMethod -UseBasicParsing -Method GET -Headers $headers -Uri "$queryUrl$page"
                    Write-Host "$($result.Count) packages found"
                    if ($result.Count -eq 0) {
                        break
                    }
                    $matching += @($result)
                    if ($result.Count -ne $per_page) {
                        break
                    }
                    $page++
                }
                Write-Host "Total of $($matching.Count) packages found"
                $this.searchResultsCache[$cacheKey] = @{
                    matching = $matching
                    timestamp = (Get-Date)
                }
            }
            $matching = @($matching | Where-Object { $_.name -like "*$packageName*" -and $this.IsTrusted($_.name) } | Sort-Object { $_.name.replace('.symbols','') } | ForEach-Object { @{ "id" = $_.name; "versions" = @() } } )
        }
        else {
            $queryUrl = "$($this.searchQueryServiceUrl)?q=$packageName&take=50"
            try {
                Write-Host -ForegroundColor Yellow "Search package using $queryUrl"
                $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
                $searchResult = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl
                $global:ProgressPreference = $prev
            }
            catch {
                throw (GetExtendedErrorMessage $_)
            }
            # Check that the found pattern matches the package name and the trusted patterns
            $matching = @($searchResult.data | Where-Object { $_.id -like "*$($packageName)*" -and $this.IsTrusted($_.id) } | Sort-Object { $_.id.replace('.symbols','') } | ForEach-Object { @{ "id" = $_.id; "versions" = @($_.versions.version) } } )
        }
        $exact = $matching | Where-Object { $_.id -eq $packageName -or $_.id -eq "$packageName.symbols" }
        if ($exact) {
            Write-Host "Exact match found for $packageName"
            $matching = $exact
        }
        else {
            Write-Host "$($matching.count) matching packages found"
        }

        if ($useCache -and !$hasCache) {
            # Cache the search results
            $this.searchResultsCache[$packageName] = @{
                matching = $matching
                timestamp = (Get-Date)
            }
        }

        return $matching | ForEach-Object { Write-Host "- $($_.id)"; $_ }
    }

    [string[]] GetVersions([hashtable] $package, [bool] $descending, [bool] $allowPrerelease) {
        if (!$this.IsTrusted($package.id)) {
            throw "Package $($package.id) is not trusted on $($this.url)"
        }
        if ($package.versions.count -eq 0) {
            $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($package.Id.ToLowerInvariant())/index.json"
            try {
                Write-Host -ForegroundColor Yellow "Get versions using $queryUrl"
                $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
                $versions = Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl
                $global:ProgressPreference = $prev
            }
            catch {
                throw (GetExtendedErrorMessage $_)
            }
            $package.versions = @($versions.versions)
        }
        $versionsArr = $package.versions
        Write-Host "$($versionsArr.count) versions found"
        $versionsArr = @($versionsArr | Where-Object { $allowPrerelease -or !$_.Contains('-') } | Sort-Object { ($_ -replace '-.+$') -as [System.Version]  }, { "$($_)z" } -Descending:$descending | ForEach-Object { "$_" })
        Write-Host "First version is $($versionsArr[0])"
        Write-Host "Last version is $($versionsArr[$versionsArr.Count-1])"
        return $versionsArr
    }

    # Normalize name or publisher name to be used in nuget id
    static [string] Normalize([string] $name) {
        return $name -replace '[^a-zA-Z0-9_\-]',''
    }

    static [string] NormalizeVersionStr([string] $versionStr) {
        $idx = $versionStr.IndexOf('-')
        $version = [System.version]($versionStr.Split('-')[0])
        if ($version.Build -eq -1) { $version = [System.Version]::new($version.Major, $version.Minor, 0, 0) }
        if ($version.Revision -eq -1) { $version = [System.Version]::new($version.Major, $version.Minor, $version.Build, 0) }
        if ($idx -gt 0) {
            return "$version$($versionStr.Substring($idx))"
        }
        else {
            return "$version"
        }
    }

    static [Int32] CompareVersions([string] $version1, [string] $version2) {
        $version1 = [NuGetFeed]::NormalizeVersionStr($version1)
        $version2 = [NuGetFeed]::NormalizeVersionStr($version2)
        $ver1 = $version1 -replace '-.+$' -as [System.Version]
        $ver2 = $version2 -replace '-.+$' -as [System.Version]
        if ($ver1 -eq $ver2) {
            # add a 'z' to the version to make sure that 5.1.0 is greater than 5.1.0-beta
            # Tags are sorted alphabetically (alpha, beta, rc, etc.), even though this shouldn't matter
            # New prerelease versions will always have a new version number
            return [string]::Compare("$($version1)z", "$($version2)z")
        }
        elseif ($ver1 -gt $ver2) {
            return 1
        }
        else {
            return -1
        }
    }

    # Test if version is included in NuGet version range
    # https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges
    static [bool] IsVersionIncludedInRange([string] $versionStr, [string] $nuGetVersionRange) {
        $versionStr = [NuGetFeed]::NormalizeVersionStr($versionStr)
        $version = $versionStr -replace '-.+$' -as [System.Version]
        if ($nuGetVersionRange -match '^\s*([\[(]?)([\d\.]*)(,?)([\d\.]*)([\])]?)\s*$') {
            $inclFrom = $matches[1] -ne '('
            $range = $matches[3] -eq ','
            $inclTo = $matches[5] -eq ']'
            if ($matches[1] -eq '' -and $matches[5] -eq '') {
                $range = $true
            }
            if ($matches[2]) {
                $fromver = [System.Version]([NuGetFeed]::NormalizeVersionStr($matches[2]))
            }
            else {
                $fromver = [System.Version]::new(0,0,0,0)
                if ($inclFrom) {
                    Write-Host "Invalid NuGet version range $nuGetVersionRange"
                    return $false
                }
            }
            if ($matches[4]) {
                $tover = [System.Version]([NuGetFeed]::NormalizeVersionStr($matches[4]))
            }
            elseif ($range) {
                $tover = [System.Version]::new([int32]::MaxValue,[int32]::MaxValue,[int32]::MaxValue,[int32]::MaxValue)
                if ($inclTo) {
                    Write-Host "Invalid NuGet version range $nuGetVersionRange"
                    return $false
                }
            }
            else {
                $tover = $fromver
            }
            if (!$range -and (!$inclFrom -or !$inclTo)) {
                Write-Host "Invalid NuGet version range $nuGetVersionRange"
                return $false
            }
            if ($inclFrom) {
                if ($inclTo) {
                    return $version -ge $fromver -and $version -le $tover
                }
                else {
                    return $version -ge $fromver -and $version -lt $tover
                }
            }
            else {
                if ($inclTo) {
                    return $version -gt $fromver -and $version -le $tover
                }
                else {
                    return $version -gt $fromver -and $version -lt $tover
                }
            }
        }
        return $false
    }

    [string] FindPackageVersion([hashtable] $package, [string] $nuGetVersionRange, [string[]] $excludeVersions, [string] $select, [bool] $allowPrerelease) {
        $versions = $this.GetVersions($package, !($select -eq 'Earliest' -or $select -eq 'AllAscending'), $allowPrerelease)
        if ($excludeVersions) {
            Write-Host "Exclude versions: $($excludeVersions -join ', ')"
        }
        $versionList = @()
        foreach($version in $versions ) {
            if ($excludeVersions -contains $version) {
                continue
            }
            if (($select -eq 'Exact' -and [NuGetFeed]::NormalizeVersionStr($nuGetVersionRange) -eq [NuGetFeed]::NormalizeVersionStr($version)) -or ($select -ne 'Exact' -and [NuGetFeed]::IsVersionIncludedInRange($version, $nuGetVersionRange))) {
                if ($select -eq 'AllAscending' -or $select -eq 'AllDescending') {
                    Write-Host "Include $version"
                }
                elseif ($nuGetVersionRange -eq '0.0.0.0') {
                    Write-Host "$select version is $version"
                }
                else {
                    Write-Host "$select version matching '$nuGetVersionRange' is $version"
                }
                $versionList += @($version)
            }
        }
        return ($versionList -join ',')
    }

    # Download the specs for the package with id = packageId and version = version
    # The following properties are returned:
    # - id: the package id
    # - name: the package name (either title, description or id from the nuspec)
    # - version: the package version
    # - authors: the package authors
    # - dependencies: the package dependencies (id and version range)
    [PSCustomObject] DownloadPackageSpec([string] $packageId, [string] $version) {
        $nuSpecName = "$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant()).json"
        $nuSpecFileName = Join-Path $this.cacheFolder $nuSpecName
        $nuSpecMutex = New-Object System.Threading.Mutex($false, $nuSpecName.Replace('/','_').Replace('\','_'))
        try {
            try {
                if (!$nuSpecMutex.WaitOne(1000)) {
                    Write-Host "Waiting for other process downloading nuspec '$($nuSpecName)'"
                    $nuSpecMutex.WaitOne() | Out-Null
                    Write-Host "Other process completed download"
                }
            }
            catch [System.Threading.AbandonedMutexException] {
               Write-Host "Other process terminated abnormally"
            }
            if (Test-Path $nuSpecFileName) {
                Write-Host "Using cached nuspec for $packageId version $version"
                (Get-Item $nuSpecFileName).LastWriteTime = Get-Date
                return (Get-Content -Path $nuSpecFileName | ConvertFrom-Json | ConvertTo-HashTable)
            }
            if (!$this.IsTrusted($packageId)) {
                throw "Package $packageId is not trusted on $($this.url)"
            }
            if ($this.packageBaseAddressUrl -like 'https://nuget.pkg.github.com/*') {
                $queryUrl = "$($this.packageBaseAddressUrl.SubString(0,$this.packageBaseAddressUrl.LastIndexOf('/')))/$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant()).json"
                Write-Host "Download nuspec using $queryUrl"
                $response = Invoke-WebRequest -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl
                $content = $response.Content | ConvertFrom-Json
                if (!($content.PSObject.Properties.Name -eq 'catalogEntry') -or ($null -eq $content.catalogEntry)) {
                    throw "Package $packageId version $version not found on"
                }
                $returnValue = @{
                    "id" = $content.catalogEntry.id
                    "name" = $content.catalogEntry.description
                    "version" = $content.catalogEntry.version
                    "authors" = $content.catalogEntry.authors
                    "dependencies" = $content.catalogEntry.dependencyGroups | ForEach-Object { $_.dependencies | ForEach-Object { @{"id" = $_.id; "version" = $_.range.replace(' ','') } } }
                }
            }
            else {
                $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant())/$($packageId.ToLowerInvariant()).nuspec"
                try {
                    Write-Host "Download nuspec using $queryUrl"
                    $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
                    $tmpFile = Join-Path ([System.IO.Path]::GetTempPath()) "$([GUID]::NewGuid().ToString()).nuspec"
                    Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl -OutFile $tmpFile
                    $nuspec = [xml](Get-Content -Path $tmpfile -Encoding UTF8 -Raw)
                    Remove-Item -Path $tmpFile -Force
                    $global:ProgressPreference = $prev
                }
                catch {
                    throw (GetExtendedErrorMessage $_)
                }
                if ($nuspec.package.metadata.PSObject.Properties.Name -eq 'title') {
                    $appName = $nuspec.package.metadata.title
                }
                elseif ($nuspec.package.metadata.PSObject.Properties.Name -eq 'description') {
                    $appName = $nuspec.package.metadata.description
                }
                else {
                    $appName = $nuspec.package.metadata.id
                }
                if ($nuspec.package.metadata.PSObject.Properties.Name -eq 'Dependencies') {
                    $dependencies = @($nuspec.package.metadata.Dependencies.GetEnumerator() | ForEach-Object { @{"id" = $_.id; "version" = $_.version } })
                }
                else {
                    $dependencies = @()
                }
                $returnValue = @{
                    "id" = $nuspec.package.metadata.id
                    "name" = $appName
                    "version" = $nuspec.package.metadata.version
                    "authors" = $nuspec.package.metadata.authors
                    "dependencies" = $dependencies
                }
            }
            New-Item -Path $nuSpecFileName -ItemType File -Force -Value ($returnValue | ConvertTo-Json -Depth 99) | Out-Null
            return $returnValue
        }
        finally {
            $nuSpecMutex.ReleaseMutex()
        }
    }

    [string] DownloadPackage([string] $packageId, [string] $version) {
        $packageSubFolder = "$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant())"
        $packageMutex = New-Object System.Threading.Mutex($false, $packageSubFolder.Replace('/','_').Replace('\','_'))
        try {
            try {
                if (!$packageMutex.WaitOne(1000)) {
                    Write-Host "Waiting for other process downloading package '$($packageSubFolder)'"
                    $packageMutex.WaitOne() | Out-Null
                    Write-Host "Other process completed download"
                }
            }
            catch [System.Threading.AbandonedMutexException] {
               Write-Host "Other process terminated abnormally"
            }

            if (!$this.IsTrusted($packageId)) {
                throw "Package $packageId is not trusted on $($this.url)"
            }
            $queryUrl = "$($this.packageBaseAddressUrl.TrimEnd('/'))/$($packageId.ToLowerInvariant())/$($version.ToLowerInvariant())/$($packageId.ToLowerInvariant()).$($version.ToLowerInvariant()).nupkg"
            $packageCacheFolder = Join-Path $this.cacheFolder $packageSubFolder
            if (test-Path $packageCacheFolder) {
                Write-Host "Using cached package for $packageId version $version"
                (Get-Item $packageCacheFolder).LastWriteTime = Get-Date
            }
            else {
                try {
                    Write-Host -ForegroundColor Green "Download package using $queryUrl"
                    $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
                    $filename = "$($packageCacheFolder).zip"
                    New-Item -Path $packageCacheFolder -ItemType Container | Out-Null
                    Invoke-RestMethod -UseBasicParsing -Method GET -Headers ($this.GetHeaders()) -Uri $queryUrl -OutFile $filename
                    if ($this.fingerprints) {
                        $arguments = @("nuget", "verify", $filename)
                        if ($this.fingerprints.Count -eq 1 -and $this.fingerprints[0] -eq '*') {
                            Write-Host "Verifying package using any certificate"
                        }
                        else {
                            Write-Host "Verifying package using $($this.fingerprints -join ', ')"
                            $arguments += @("--certificate-fingerprint $($this.fingerprints -join ' --certificate-fingerprint ')")
                        }
                        cmddo -command 'dotnet' -arguments $arguments -silent -messageIfCmdNotFound "dotnet not found. Please install it from https://dotnet.microsoft.com/download"
                    }
                    Expand-Archive -Path $filename -DestinationPath $packageCacheFolder -Force
                    $global:ProgressPreference = $prev
                    Write-Host "Package successfully downloaded"
                }
                catch {
                    if (Test-Path $packageCacheFolder) {
                        Remove-Item $packageCacheFolder -Recurse -Force
                    }
                    throw (GetExtendedErrorMessage $_)
                }
                finally {
                    if (Test-Path $filename) {
                        Remove-Item $filename -Force
                    }
                }
            }
            $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([GUID]::NewGuid().ToString())
            New-Item -Path $tmpFolder -ItemType Container | Out-Null
            Copy-Item -Path (Join-Path $packageCacheFolder '*') -Destination $tmpFolder -Recurse -Force
            return $tmpFolder
        }
        finally {
            $packageMutex.ReleaseMutex()
        }
    }

    [void] PushPackage([string] $package) {
        if (!($this.token)) {
            throw "NuGet token is required to push packages"
        }
        Write-Host "Preparing NuGet Package for submission"
        $headers = $this.GetHeaders()
        $headers += @{
            "X-NuGet-ApiKey" = $this.token
            "X-NuGet-Client-Version" = "6.3.0"
        }
        $boundary = [System.Guid]::NewGuid().ToString();
        $LF = "`r`n";
        $tmpFile = Join-Path ([System.IO.Path]::GetTempPath()) ([GUID]::NewGuid().ToString())
        $fs = [System.IO.File]::OpenWrite($tmpFile)
        $fs | Add-Member -MemberType ScriptMethod -Name WriteBytes -Value { param($bytes) $this.Write($bytes, 0, $bytes.Length) }
        try {
            $fs.WriteBytes([System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF"))
            $fs.WriteBytes([System.Text.Encoding]::UTF8.GetBytes("Content-Type: application/octet-stream$($LF)Content-Disposition: form-data; name=package; filename=""$([System.IO.Path]::GetFileName($package))""$($LF)$($LF)"))
            $fs.WriteBytes([System.IO.File]::ReadAllBytes($package))
            $fs.WriteBytes([System.Text.Encoding]::UTF8.GetBytes("$LF--$boundary--$LF"))
        } finally {
            $fs.Close()
        }
        
        Write-Host "Submitting NuGet package"
        try {
            Invoke-RestMethod -UseBasicParsing -Uri $this.packagePublishUrl -ContentType "multipart/form-data; boundary=$boundary" -Method Put -Headers $headers -inFile $tmpFile | Out-Host
            Write-Host -ForegroundColor Green "NuGet package successfully submitted"

            # Clear matching search results caches
            @( $this.searchResultsCache.Keys ) | 
                Where-Object { $package -like "*$($_)*" -or $_ -like 'GitHubPackages:*' } | 
                ForEach-Object { $this.searchResultsCache.Remove($_) }
        }
        catch [System.Net.WebException] {
            if ($_.Exception.Status -eq "ProtocolError" -and $_.Exception.Response -is [System.Net.HttpWebResponse]) {
                $response = [System.Net.HttpWebResponse]($_.Exception.Response)
                if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Conflict) {
                    Write-Host -ForegroundColor Yellow "NuGet package already exists"
                }
                else {
                    throw (GetExtendedErrorMessage $_)
                }
            }
            else {
                throw (GetExtendedErrorMessage $_)
            }
        }
        catch {
            throw (GetExtendedErrorMessage $_)
        }
        finally {
            Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue
        }
    
    }
}

# SIG # Begin signature block
# MIIoKQYJKoZIhvcNAQcCoIIoGjCCKBYCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBdGF7Irg0X+w+2
# DVWFeXSvLHR00Sy/He2XlSdY1wVu2qCCDXYwggX0MIID3KADAgECAhMzAAAEhV6Z
# 7A5ZL83XAAAAAASFMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM3WhcNMjYwNjE3MTgyMTM3WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDASkh1cpvuUqfbqxele7LCSHEamVNBfFE4uY1FkGsAdUF/vnjpE1dnAD9vMOqy
# 5ZO49ILhP4jiP/P2Pn9ao+5TDtKmcQ+pZdzbG7t43yRXJC3nXvTGQroodPi9USQi
# 9rI+0gwuXRKBII7L+k3kMkKLmFrsWUjzgXVCLYa6ZH7BCALAcJWZTwWPoiT4HpqQ
# hJcYLB7pfetAVCeBEVZD8itKQ6QA5/LQR+9X6dlSj4Vxta4JnpxvgSrkjXCz+tlJ
# 67ABZ551lw23RWU1uyfgCfEFhBfiyPR2WSjskPl9ap6qrf8fNQ1sGYun2p4JdXxe
# UAKf1hVa/3TQXjvPTiRXCnJPAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUuCZyGiCuLYE0aU7j5TFqY05kko0w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwNTM1OTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBACjmqAp2Ci4sTHZci+qk
# tEAKsFk5HNVGKyWR2rFGXsd7cggZ04H5U4SV0fAL6fOE9dLvt4I7HBHLhpGdE5Uj
# Ly4NxLTG2bDAkeAVmxmd2uKWVGKym1aarDxXfv3GCN4mRX+Pn4c+py3S/6Kkt5eS
# DAIIsrzKw3Kh2SW1hCwXX/k1v4b+NH1Fjl+i/xPJspXCFuZB4aC5FLT5fgbRKqns
# WeAdn8DsrYQhT3QXLt6Nv3/dMzv7G/Cdpbdcoul8FYl+t3dmXM+SIClC3l2ae0wO
# lNrQ42yQEycuPU5OoqLT85jsZ7+4CaScfFINlO7l7Y7r/xauqHbSPQ1r3oIC+e71
# 5s2G3ClZa3y99aYx2lnXYe1srcrIx8NAXTViiypXVn9ZGmEkfNcfDiqGQwkml5z9
# nm3pWiBZ69adaBBbAFEjyJG4y0a76bel/4sDCVvaZzLM3TFbxVO9BQrjZRtbJZbk
# C3XArpLqZSfx53SuYdddxPX8pvcqFuEu8wcUeD05t9xNbJ4TtdAECJlEi0vvBxlm
# M5tzFXy2qZeqPMXHSQYqPgZ9jvScZ6NwznFD0+33kbzyhOSz/WuGbAu4cHZG8gKn
# lQVT4uA2Diex9DMs2WHiokNknYlLoUeWXW1QrJLpqO82TLyKTbBM/oZHAdIc0kzo
# STro9b3+vjn2809D0+SOOCVZMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgkwghoFAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAASFXpnsDlkvzdcAAAAABIUwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJUwT2St+I5QZWKlhddP3bTn
# gAGSoRapS5mKK/y05K56MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAB0S05IKFw2WbzHFVWkDapJkrbudZ97amXbfDRfD56Zb2P5QE5FU/8JzP
# HCACGuK5XCwuJ7UWclRGMkcj2dZz5Hp1s+jEdDXI94EcMHGcFzbidy6i7Nm85Zzy
# f1BpqE0sBP8SoYKIlFkLFxrDHGbKgx4wPktIncIbCi4WkB4RmL2BSObRD/g8zzgr
# he3QbIQobvfY/9OfnTUW8gFA+KBPYhDlmd+/oGrT4bzN7m4c4QUdpiz5rS+EGeTU
# HA/YSaBeBMjazvbEEriJLysvd6iQf0qJIJ+TCWQwllkd6dfvFiVt//32JzS7XWnr
# Q9ky8jjqfAg3Tt1gd2glKicPfy/wVqGCF5MwghePBgorBgEEAYI3AwMBMYIXfzCC
# F3sGCSqGSIb3DQEHAqCCF2wwghdoAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFRBgsq
# hkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCDFKm6EkF+V4rsIIIqFz6k/8IvmMWX3e2f4KkVVEb80LwIGaZRpT6cM
# GBIyMDI2MDIyNDEyMjg1NS42NFowBIACAfSggdGkgc4wgcsxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy
# aWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo5MjAwLTA1
# RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCC
# EeowggcgMIIFCKADAgECAhMzAAACCQgH4PlcjOZVAAEAAAIJMA0GCSqGSIb3DQEB
# CwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDEzMDE5NDI1
# NVoXDTI2MDQyMjE5NDI1NVowgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMx
# JzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo5MjAwLTA1RTAtRDk0NzElMCMGA1UE
# AxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAMKUSjD3Lgzd/VL3PXG00QRPBYvW8SKLDSgPtJcR2/ix
# 0/TGxXKJ2/ojauYSXw9iz0txmPOxY4cjt1CREvbwY/cJdy9jRmrqdawdjZBqYJkU
# sXYiVEfoEfHZGQ3tlEMqazsE6jggYFGUIyRS/033+3A7MCSlY2wzdv8FDFzCFWCx
# Cq1Dw0Q9S6JH4ZXmt1AdRPimOKFlOQnCtqWLPRltilRMfk6SLd3cGnH2qI+uIHqG
# E18Y+OXQ8inbcPnv2ulbpmY+o9PyPXYpfvJJnA27Gzc9i8X/DXcaxFeTMhsjIsoQ
# /OP2XOaasXbCO+9SvH0BnDsYtJeTbwOfVdJ/raFuQW5QbA8UuncRtGohWYFnjbBz
# PmZIggLLdCz+HCERiFSd2cAGA2kPlq8As5XuxR8mscNldfp/2CBuMgDqPaeFIBIi
# qXwXkuwoHDRE+0O7LePYI/G1OZmjNssrxMy3EOIwKDFOl+DmJhS/KFXhqpoMvBEG
# ygFGE7/6HDJsqdjBfEp546uw7BAudo4TkGYUlhYE4XPd3zwsEr1BEGB0QfkItWHv
# CSAwh6H3pwfn4fTES+aDq3u7O2VdfZJXvF1Rg/EDe+ONXcSRXtptIcPkcdBlOt3c
# WqwP9U5gAJRUE+vEX6RStkZfFgidlOmtgxgSrpQgbUNPikJU/0NxoIsYg5gQnWDT
# AgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUSYvo0cRdOOW98C9AzbV3MxaTytIwHwYD
# VR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZO
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIw
# VGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBc
# BggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0
# cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYD
# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMC
# B4AwDQYJKoZIhvcNAQELBQADggIBAFxefG84PCTiH+NtQGycWUW2tK4EFlvvBJl9
# rmUpExM182WZoALht3tajQjmEzGwQlTK6kfCHiQPmqRFlzMhzSMgAFBDXENQFr5Z
# PGun9QCoLXuKMUJ49kphWM2sd/8GaPPsVo4jjWTG55GHAs0hxDaCYGoNHlbhNLaG
# 1EljJkCzuN8mZsO1NxQ4yESXU5aXH8We9xBui3lU/NpTCJPo2J7yXo9mOhCy7GJq
# y5ICbEohB2wecnlCiSrB3KpeLUVkO0RNW9td8Oyh/NO1rh6fap/jyHMRnBS9uTPm
# ya3z3SdUAruTPZyuvM3eGmd8W5+2n+tctZO/E9Bx9ZeIS4hR3YaDt5HxC3Iq0kNT
# z48PAQKTOhomNsYIqrH0RKAUnPOtc3CGFfpFzyDYRT/7reaapZ4IX+Qk4WDZ4nDt
# q79psRKCrcRrPIPVWUv4dpf4wEcbNCYe286bdCXjBVM3darxfxsJHryqIXmsVqyb
# hHEXrNqNl5IcL+pLnffr/howOqxXo7zpGU88JgYk4+1/Yxso7tckl4v9RA3Rze6L
# HlExOjrp1sBPE9QUQbk+Hg8fMaNRsQ7sPfku4QGKIbxiuUxE6QaXd8FCX1tZuDD0
# IhRBvCrlxNoTGV8Skx1KjJ0miVRNAPkQsobPVMlqFOJ13bTCXCLkGTfpcibOwfhi
# zXmJdF8CMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG
# 9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO
# BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEy
# MDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw
# MTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az
# /1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V2
# 9YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oa
# ezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkN
# yjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7K
# MtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRf
# NN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SU
# HDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoY
# WmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5
# C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8
# FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TAS
# BgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1
# Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUw
# UzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNy
# b3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIB
# hjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fO
# mhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9w
# a2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggr
# BgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3
# DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEz
# tTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJW
# AAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G
# 82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/Aye
# ixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI9
# 5ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1j
# dEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZ
# KCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xB
# Zj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuP
# Ntq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvp
# e784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCA00w
# ggI1AgEBMIH5oYHRpIHOMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw
# JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0wNUUwLUQ5NDcxJTAjBgNVBAMT
# HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAHzv
# ras9NB3sicMJB1vWSAUpCQJEoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwDQYJKoZIhvcNAQELBQACBQDtR3h9MCIYDzIwMjYwMjI0MDEwODEz
# WhgPMjAyNjAyMjUwMTA4MTNaMHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAO1HeH0C
# AQAwBwIBAAICCTgwBwIBAAICEhowCgIFAO1Iyf0CAQAwNgYKKwYBBAGEWQoEAjEo
# MCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG
# 9w0BAQsFAAOCAQEArm7xPM5wE5ekbg6jkv7kTNYsZRWQ1N0gtTFu3MLhyr+k/oBf
# cYgV13+QkX/s/aMzxve0xUc82xIhv4nzQngwHpaITVrLnKVldxKGqfzNJhrYp2hk
# pYPN7PVB2twYcc0FqvP3d9AqpkWgIOYB4zCgyIh4VWJtx2KihzLf2CWC5tdwydZS
# 0afAueV9KvjLEOpcZFC1aemAtMsi4K3+dmRkTz8qSMcVv2WT/wWTuuo6suiBzFpc
# f9rW62j5Q9Bd52M9+xQvLXAjb7eCyZ014Uj6TdiaB5+6nBekn9n+APj0aQgnKgu7
# EcTeMB7HjXOPEVJhk1A0Z0+JIfkWSVg5L0fWRDGCBA0wggQJAgEBMIGTMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACCQgH4PlcjOZVAAEAAAIJMA0G
# CWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJ
# KoZIhvcNAQkEMSIEIEwZlR2iHNuUVPiAieioSmEdz2denmt3KEDbFNZC1X2MMIH6
# BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgaBssHsi99AIuZQ5RmGN1SorxuKR8
# HplVV2hOM3CFEz4wgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MAITMwAAAgkIB+D5XIzmVQABAAACCTAiBCAhxwexJH9mnhHGMiaU8NGU7dr+kdFU
# +CT8ZqlJl70xMzANBgkqhkiG9w0BAQsFAASCAgAmdV39wzg+9g3xQh40PCScGuqK
# tMzYGTfKa6Uydz3kgfAYxy9DZfc+es0jztsJARqioqfTpcm3ToU1ZXjhcWavTeHZ
# zgDbCh/aonpEHX3lA6FFQgs6P1chK3DMlggW70fV4B3HTCF/JdLLdoUoed6o4Mw6
# 9HDydsk4sPmGEQGkSliMkwwiU7xIwFfBEFScz3kS5hviVdfGJxpunu2lN8MP9U9l
# PME3mJB+emnrs1rc1cTAhnK5gBL4kO4zuzdAVVumFOAKypmhfh8XmvB1Qj9yzhBF
# n3iUkYHyWSDc9QozZewxSJ/68vpoY7g91p2wxuglT4h6lefynpB/0ueFafh+qNv/
# CEHz9kkOv6RmTpzi71DOmwy0YaM01xUkjvS75mb4v3NJun3vLW0YnEE2nznewYpt
# iFuv3z1ZdhMjnVHr9cNLDYobFjQbHHP9D4/np5hgEu1miVfl8KM9V9WimJ4yEk/L
# N+TDfbj/4cpLWjxf20abe5xlf4dURhw+eRTh4uq18Jg0qKUiqVtyyUODAedeUsBq
# G59NbMn8xAOPqLRlB6ejta+4aBNLROFMHo3k83B/2z2MX/ciE46uR2h+7TdyhU2o
# 2uBMScV6C2Qw5z9ZOFeDUFZMuWW/BR7XZ2z27V/DcsR3fufX3oB/KssGpyHtQv6B
# 9W6Gnt0MgOOaDQoNTw==
# SIG # End signature block