Private/Assets/Resolve-AvmPinnedAsset.ps1
|
function Resolve-AvmPinnedAsset { <# .SYNOPSIS Materialise a single pinned asset descriptor into the per-user cache. .DESCRIPTION Given an asset descriptor (the shape Resolve-AvmPolicyBundle projects from the pin manifest returns) plus its asset Name, ensures the referenced archive is downloaded, SHA-verified, extracted, and available under <Get-AvmFolder Cache>/assets/<name>/<sha256-prefix12>/ The 12-hex prefix (first 12 chars of the lowercase SHA256) keeps cache paths short on Windows per spec section 6 (260-char budget). The full 64-char SHA is still pinned by the descriptor, verified by Invoke-AvmHttp, and preserved in .meta.json -- only the on-disk directory name is truncated. The install pipeline mirrors Install-AvmToolFromPins: cache-hit short-circuit -> cross-process lock -> stage under .staging/<uuid>/ -> Invoke-AvmHttp (download + SHA verify) -> Expand-AvmToolArchive -> optional Path subdir verification -> atomic Move-Item to final dir -> .meta.json + .verified marker -> unlock. Archive type is inferred from the source URL extension: *.zip -> zip *.tar.gz | *.tgz -> tar.gz This slice (2/2 of the pinned-asset feature) only supports sha256-pinned archives. Ref-only assets and type=git assets throw AvmConfigurationException so callers see a clean "not yet supported" message; both are tracked for follow-up slices. .PARAMETER Name The asset name. Used for the cache subdirectory and diagnostics. .PARAMETER Asset The pscustomobject descriptor built by the caller (see Resolve-AvmPolicyBundle). Required properties: Source. Conditionally required: Sha256. Optional: Ref, Path, Type. .PARAMETER AllowFileUrls Forwarded to Invoke-AvmHttp. Permits file:// sources for fixtures. .PARAMETER Force If set, blow away any cached materialisation and re-install. .OUTPUTS [pscustomobject] with members: Name : asset name Sha256 : the cache key Ref : descriptor Ref (may be $null) Path : absolute on-disk root of the materialised asset. If the descriptor specifies a sub-Path, that subdir is appended. Action : 'cache-hit' | 'installed' | 'race-loss' #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [pscustomobject] $Asset, [switch] $AllowFileUrls, [switch] $Force ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($Name)) { throw [System.ArgumentException]::new( 'Resolve-AvmPinnedAsset: Name must be a non-empty string.', 'Name') } $source = if ($Asset.PSObject.Properties['Source']) { [string]$Asset.Source } else { $null } if ([string]::IsNullOrWhiteSpace($source)) { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' is missing 'Source'.", 'AVM1004') } $type = if ($Asset.PSObject.Properties['Type']) { [string]$Asset.Type } else { $null } if ($type -eq 'git') { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' uses type='git'; only archive assets are supported in this slice (slice 2/2 of the pinned-asset feature).", 'AVM1004') } $sha = if ($Asset.PSObject.Properties['Sha256']) { [string]$Asset.Sha256 } else { $null } if ([string]::IsNullOrWhiteSpace($sha)) { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' has no 'Sha256'. Ref-only materialisation is not yet supported; pin a sha256 (slice 2/2 of the pinned-asset feature).", 'AVM1004') } if ($sha -cnotmatch '^[0-9a-f]{64}$') { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' Sha256 must be 64-char lowercase hex.", 'AVM1004') } $archiveKind = $null $extSuffix = $null $lower = $source.ToLowerInvariant() if ($lower.EndsWith('.zip')) { $archiveKind = 'zip' $extSuffix = '.zip' } elseif ($lower.EndsWith('.tar.gz')) { $archiveKind = 'tar.gz' $extSuffix = '.tar.gz' } elseif ($lower.EndsWith('.tgz')) { $archiveKind = 'tar.gz' $extSuffix = '.tgz' } else { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' source URL '$source' has an unsupported archive extension; expected .zip, .tar.gz, or .tgz.", 'AVM1004') } $ref = if ($Asset.PSObject.Properties['Ref']) { [string]$Asset.Ref } else { $null } $subPath = if ($Asset.PSObject.Properties['Path']) { [string]$Asset.Path } else { $null } $cacheRoot = Get-AvmFolder -Kind Cache $assetsRoot = Join-Path $cacheRoot 'assets' $assetDir = Join-Path $assetsRoot $Name # Spec section 6 line 220: use a 12-hex prefix of the SHA256 as the content-addressed # segment, not the full 64-char hash. Keeps Windows paths within budget; the # full SHA is still validated by Invoke-AvmHttp and recorded in .meta.json. $shaPrefix = $sha.Substring(0, 12) $versionDir = Join-Path $assetDir $shaPrefix $verified = Join-Path $versionDir '.verified' $resolvedPath = if ([string]::IsNullOrWhiteSpace($subPath)) { $versionDir } else { Join-Path $versionDir $subPath } if ((Test-Path -LiteralPath $verified) -and (Test-Path -LiteralPath $resolvedPath) -and -not $Force) { Write-AvmLog ("asset: cache hit for {0}; sha256-prefix={1}" -f $Name, $shaPrefix) -Level Verbose | Out-Null return [pscustomobject][ordered]@{ Name = $Name Sha256 = $sha Ref = $ref Path = $resolvedPath Action = 'cache-hit' } } if (-not $PSCmdlet.ShouldProcess($versionDir, "Resolve pinned asset '$Name'")) { return $null } if ($Force -and (Test-Path -LiteralPath $versionDir)) { Write-AvmLog ("asset: force removing {0}" -f $versionDir) -Level Verbose | Out-Null Remove-Item ` -LiteralPath $versionDir ` -Recurse ` -Force ` -ProgressAction SilentlyContinue } if (-not (Test-Path -LiteralPath $assetDir)) { New-Item -ItemType Directory -Path $assetDir -Force | Out-Null } $lockFile = Join-Path $assetDir '.lock' Write-AvmLog ("asset: acquiring cache lock {0}" -f $lockFile) -Level Verbose | Out-Null $lock = Lock-AvmToolCache -LockFile $lockFile try { if ((Test-Path -LiteralPath $verified) -and (Test-Path -LiteralPath $resolvedPath) -and -not $Force) { Write-AvmLog ("asset: post-lock cache hit for {0}; sha256-prefix={1}" -f $Name, $shaPrefix) -Level Verbose | Out-Null return [pscustomobject][ordered]@{ Name = $Name Sha256 = $sha Ref = $ref Path = $resolvedPath Action = 'cache-hit' } } $stagingRoot = Join-Path $assetDir '.staging' if (-not (Test-Path -LiteralPath $stagingRoot)) { New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null } $stagingDir = Join-Path $stagingRoot ([Guid]::NewGuid().ToString('N').Substring(0, 12)) New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null try { $archivePath = Join-Path $stagingDir ("download" + $extSuffix) $httpParams = @{ Url = $source Destination = $archivePath ExpectedSha256 = $sha } if ($AllowFileUrls -and $source.StartsWith('file://')) { # Invoke-AvmHttp accepts file:// natively; no extra flag needed, # but we still gate it via -AllowFileUrls on this function so # callers must opt in explicitly (matches Test-AvmPins). } elseif ($source.StartsWith('file://') -and -not $AllowFileUrls) { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' uses file:// source; pass -AllowFileUrls to permit it.", 'AVM1004') } Write-AvmLog ("asset: downloading {0} from {1}" -f $Name, $source) -Level Verbose | Out-Null Invoke-AvmHttp @httpParams | Out-Null Write-AvmLog ("asset: expanding {0} archive for {1}" -f $archiveKind, $Name) -Level Verbose | Out-Null Expand-AvmToolArchive -ArchivePath $archivePath -Archive $archiveKind -TargetDir $stagingDir -EntrypointBasename $Name Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue if (-not [string]::IsNullOrWhiteSpace($subPath)) { $stagedSub = Join-Path $stagingDir $subPath Write-AvmLog ("asset: verifying declared subpath {0}" -f $subPath) -Level Verbose | Out-Null if (-not (Test-Path -LiteralPath $stagedSub)) { throw [AvmConfigurationException]::new( "Resolve-AvmPinnedAsset: asset '$Name' declared Path '$subPath' but it does not exist after extraction.", 'AVM1004') } } $meta = [pscustomobject][ordered]@{ name = $Name source = $source sha256 = $sha ref = $ref path = $subPath archive = $archiveKind installedAt = [DateTime]::UtcNow.ToString('o') } $metaPath = Join-Path $stagingDir '.meta.json' $meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $metaPath -Encoding utf8 try { Move-Item -LiteralPath $stagingDir -Destination $versionDir -Force } catch [System.IO.IOException] { if (Test-Path -LiteralPath $verified) { Write-AvmLog ("asset: rename race lost for {0}; using completed cache entry" -f $Name) -Level Verbose | Out-Null Remove-Item ` -LiteralPath $stagingDir ` -Recurse ` -Force ` -ErrorAction SilentlyContinue ` -ProgressAction SilentlyContinue return [pscustomobject][ordered]@{ Name = $Name Sha256 = $sha Ref = $ref Path = $resolvedPath Action = 'race-loss' } } throw } New-Item -ItemType File -Path $verified -Force | Out-Null Write-AvmLog ("asset: installed {0}; verified marker={1}" -f $Name, $verified) -Level Verbose | Out-Null return [pscustomobject][ordered]@{ Name = $Name Sha256 = $sha Ref = $ref Path = $resolvedPath Action = 'installed' } } finally { if (Test-Path -LiteralPath $stagingDir) { Remove-Item ` -LiteralPath $stagingDir ` -Recurse ` -Force ` -ErrorAction SilentlyContinue ` -ProgressAction SilentlyContinue } } } finally { $lock.Dispose() } } # SIG # Begin signature block # MIInOgYJKoZIhvcNAQcCoIInKzCCJycCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDlZRwDanhiDblf # V+LICTsCJjcdkK7k9AUpfJyFOrq9cKCCDMkwggYEMIID7KADAgECAhMzAAACHPrN # xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1 # OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP # oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC # /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf # rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j # qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT # xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL # EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT # DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw # YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl # MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC # AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN # rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK # 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK # Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY # BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu # uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE # msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz # 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6 # U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO # 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD # 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC # EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS # b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX # DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ # Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq # lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo # 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv # QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a # 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1 # FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO # GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7 # ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ # uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS # CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm # VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3 # SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E # BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX # LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB # Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP # oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw # TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC # AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D # 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY # nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI # vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6 # aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w # PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7 # RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK # /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK # YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw # YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT # Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghnHMIIZwwIBATBu # MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc # +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIF3zu0wKvM2vMdADPIg1wOucwL85 # U5Kk7NgEabvs53k6MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv # AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE # ggEArX1M4isrFNnoX1PGQeh2wQC3RKcoLsFl/McQ7ZSFLLHiN+wAqD5lqOQcBrdE # 1CU3TR9gUcALJ1vlHyB3uEc8Z7C3XhPyuEBP8efogfW1LfbxYZhfUnX2FlUxzV3C # nXjc7Cp3FubtuBAtw1I3FwG1mvyjYpYgV3jZYGzVabesdYltcU18tzWVF4tyYw8h # S7e3YW7JLp6pPMWlPNjYc3Y6jBQ1KXiHtDGk7I6rO49ye2WYXtMvoj8+bZCTcnI8 # RL8NNccpE74YEH732XQkQ/cLnx1/dnOqQmTgxAfddMrh1HT6qGMQ89QyVO0tmYix # +6H4RYCyNRYD+9a9eaGUd1b+TKGCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCCF38G # CSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG # 9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC # AQUABCBrcbouYHUhlhLbaFazVCc6maw8rwz4qrxVLvS2RYjcjQIGaoVaWunqGBMy # MDI2MDkwNDE0MDAzOC4yOTZaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj # YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0wNUUw # LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHt # MIIHIDCCBQigAwIBAgITMwAAAiY1tD5nQ5P2HwABAAACJjANBgkqhkiG9w0BAQsF # ADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTQwMDJa # Fw0yNzA1MTcxOTQwMDJaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw # JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0wNUUwLUQ5NDcxJTAjBgNVBAMT # HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQC//w+ZZIL5RFFpVI8D3ZyuNu8IzcAEOD30OLYjh337rXjc # rIlOSzpJc4ZeUxEyli6x6F6zm4NR8dbPb9diDp/hOUzHWGxiA1Z3RXKBb/4F/ojy # vN43SEGWqSfVc3I3BlsYT35ecVAJ9kVf90YOv29tFjJBBZkYvrT/DwwyRLscOyP4 # p+9/lyJjD+ULs3YXBhVrfZ+MbQB+BYKLqRvBKbj/wR9akNrMxQINoGaD5jZO/N/n # SsmG2P1zv/cv4gSoMBnWeQIBkjd2I5w1DeXupp2vSiNmR5sA2ZkBK3yiQWaJvRxO # DlkfiyHk9Mkk/TrYTjmjPCbhe+uqhHNRy8UlbOvWsCq0tRtUykHv39DgqAfJNrE8 # OSt835rBzDprrcAhwmgfhoVi4AKeqwikY0nUa48K0Qy80XT4fiEA3ExEZNaRFo9N # q/GwbfgqKqGmc9xhKuRFcjtua4KHZvnAvpWgEFSOCkovXs/BcLnkEHM9xZ8iUag5 # CyhNqXYYE/z0pcXdYaNIkQ68EWmuvLm7g9oofV2vOm5GVNoghnkWG6nGPo/JwEgm # A9oSS0EfvFRMWPA/gpSvF3shArKHnaEpVSSi3DNbyiuYiEs9Ko0IkZc8xKFeQRaq # GRxrB+2r/7B3X81Tps99KhFwg+wD87od22F2MUg1x7twt3gaVnFk0IZIwUPCGwID # AQABo4IBSTCCAUUwHQYDVR0OBBYEFF3hn9fYJN2Y/Z9LVbBPIxAzXHsQMB8GA1Ud # IwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRp # bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYI # KwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMv # TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1Ud # EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeA # MA0GCSqGSIb3DQEBCwUAA4ICAQA2Ux0tr9sYCjsq0FRyiVpx15OurNXv6Qk7iX+A # rVPlz3w4tqjcTNm1dt3tTua2wJMpJhPH8n7UXhmT98d5Du44Ll4adnse4SQfVg3Q # L6aRkXHnJUn8y9iftB/Py22n9xnwPFfj3QlDOSgLuHleu97U0iH2ZaluYabWXJih # diYpK8cPHFlqZOAiot0+GD8dP+RMuvpxt/F2LmYelpoZwriiFOUmlxEUV7xJHyZZ # lDquskeyuq01DTv91N4qM8cfPPhl/2pc4HeMf/nd2HouifJbDQFNd4WPhLzn0Sy3 # u1Zh3+S3tjQdqN+dyw60RaV+RXCoOLgFZ3MAg/GoDl+fvb5hy/1a71ctX8wEad1P # f6def2pqfl3wFc++hkF8DXXTZofJN4YVaN3InwbAGQDDkNK4lqecCixxmSKwidPy # nGeE5OtvNoK1pkLsm/i8F1RjGczZ/kSF2VDkqG866iQ+jVbGOQ6Du3eyyFcFKZoD # J4B5mEAS9aT2SKqllLeybOboH6r67siR5B/2Hnu7+KYuYZy0BEadtA6ngG4cnSR9 # JsrkhhsKmb11ujqwgJyNx92MsoGGwNgN1aI0QID8CsjCFwpfmMzlA44xHKYv3hmj # xeqBS4uU5rQeiAnVgpJeaVGKm/lzPDtnppGV+7XhRp5b1ZxT/Z7Xxc+I7H7/jCtQ # DZoaZTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcN # AQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAw # BgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEw # MB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk # 4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9c # T8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWG # UNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6Gnsz # rYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2 # LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLV # wIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTd # EonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0 # gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFph # AXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJ # YfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXb # GjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJ # KwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnP # EP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMw # UQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggr # BgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw # DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoY # xDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp # L2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYB # BQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v # cGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0B # AQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U5 # 18JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgAD # sAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo # 32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZ # iefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZK # PmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RI # LLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgk # ujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9 # af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzba # ukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/ # OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQMIIC # OAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUG # A1UECxMeblNoaWVsZCBUU1MgRVNOOjk2MDAtMDVFMC1EOTQ3MSUwIwYDVQQDExxN # aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCi/fMx # Ftkqr7XMXdsRyWU0lSKHZ6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD # QSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7kTuKjAiGA8yMDI2MDkwNDA3MTM0NloY # DzIwMjYwOTA1MDcxMzQ2WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDuRO4qAgEA # MAoCAQACAi+FAgH/MAcCAQACAhLvMAoCBQDuRj+qAgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBACRlQolrlMTtJFsSlJkRw+SeQTBg58sBjnAVerBOk8Kt8g06 # WMkTLnPZE0hs0pNjsf371VB9tej7KmS26w8ow9epjcY12TnlKjMY1UCwqnrtRMzx # zUYUNjK4EprJ/gNTNBQ7panDETKHo/q2Rn5zK658ulSoUqbKZzJ60JDX656gkUTv # VYd4KTO+L1GVFAhTsC8NE7YF57iE9QNrhc9qVsev9IFJHLkUUs2kLGU4l8zbglgp # fvkR2ZGmDc3DZtbyIXdZ1kr00pcWnMewBYR99hGyV1TWMD7B1gNbNxdDRptF/osR # 1wrvFl+UBBqlpIWuyV16MvGi7hivzdMqi520PSQxggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiY1tD5nQ5P2HwABAAACJjAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCCAQW30Refw6xdXh04yMz2OC0rw1RulZMNla8LX1RyxjzCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIMwyXGFnTNsZRBrs6GN/BbV0okaN # P3VBYqLFjUsFnbgqMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAImNbQ+Z0OT9h8AAQAAAiYwIgQgi+Og7yRbNb89rFwwJsbuyG/BxvCc # LNWRFC4h7axrO38wDQYJKoZIhvcNAQELBQAEggIAVmkkMCo5nkl003tlsaGXdUqI # POnGNsEdPxerTvbna3PsXOLgXw8+VZEYvufA3Qm9R8DZlyW9t6NDS+bC+URshBS8 # NUyGPIDOa7VP1vgb6bq70FMgxHEWYvMW4YyesdfKLbNgD+r4abwn0Bup3CDzMkl8 # g0dSrTVZIhQ18yb8DNPMhPAmRrDB+NBdIzn3aWotv/rzqXn3uZCJfj1uiotH7IpX # b97SQqwc6Aq7OV5IDHisH8pNFf9WPMpft6i1FR3kvHFuJ6oKk5ejHjO0RpvEP991 # Y8UGxVN7SBqF6E/ei/5/EhqY647ZBpPrRHvtGlSBmfevHGBZgezG+Rl1ISv7nres # 52JIHXGJl8TUiVgmrBYfe81z2VtUlcsSB5aD/hl1clLSnhNleBclqSbOwnZNT3wb # c5iCJ/19z4bOm1nonZ29VmSatJF+GP9mFKiSIJ4vYkmI7YPho3CZ1CCSTwdG2+84 # 5XS5l4OrI0VM2bGEK2HYzN9cF/uyGq5ksY6UlGK841MU4ot0PFkncfRUL2TeryM5 # 79JySEkjxYBumuaIXvmlp/exuCyepWRi3dN9c/Z/PhYoS5ulciU26RKZRvQliMCz # q6ADgvBs6vnR+wGu9Cjvm5upe1y8nhSVAuT/GCWYLxvqzfZ85JkXbs6ijaFThsUe # +Mzw10DY+o4qJcf2ehQ= # SIG # End signature block |