Private/CommonToolUtilities.psm1

###########################################################################
# #
# Copyright (c) Microsoft Corporation. All rights reserved. #
# #
# This code is licensed under the MIT License (MIT). #
# #
###########################################################################


$ModuleParentPath = Split-Path -Parent $PSScriptRoot
Import-Module -Name "$ModuleParentPath\Private\UpdateEnvironmentPath.psm1" -Force

class ContainerTool {
    [ValidateNotNullOrEmpty()][string]$Feature
    [ValidateNotNullOrEmpty()][string]$Version
    [ValidateNotNullOrEmpty()][string]$Uri
    [string]$InstallPath
    [string]$DownloadPath
    [string]$EnvPath
}

class FileDigest {
    [string]$HashFunction
    [string]$Digest

    FileDigest([string]$hashFunction, [string]$digest) {
        $this.HashFunction = $hashFunction
        $this.Digest = $digest
    }
}

class FileDownloadParameters {
    [ValidateSet("Containerd", "Buildkit", "nerdctl", "WinCNIPlugin")]
    [string]$Feature
    [string]$Repo
    [string]$Version = "latest"
    [ValidateSet("386", "amd64", "arm64", "arm")]
    [string]$OSArchitecture = "$env:PROCESSOR_ARCHITECTURE"
    [string]$DownloadPath = "$HOME\Downloads"
    [string]$ChecksumSchemaFile
    [string]$FileFilterRegEx

    FileDownloadParameters(
        [string]$feature,
        [string]$repo,
        [string]$version = "latest",
        [string]$arch = "$env:PROCESSOR_ARCHITECTURE",
        [string]$downloadPath = "$HOME\Downloads",
        [string]$checksumSchemaFile = $null,
        [string]$fileFilterRegEx = $null
    ) {
        $this.Feature = $feature
        $this.Repo = $repo
        $this.Version = $version
        $this.OSArchitecture = $arch
        $this.DownloadPath = $downloadPath
        $this.ChecksumSchemaFile = $checksumSchemaFile
        $this.FileFilterRegEx = $fileFilterRegEx
    }
}


Add-Type @'
public enum ActionConsent {
    Yes = 0,
    No = 1
}
'@


$HASH_FUNCTIONS = @("SHA1", "SHA256", "SHA384", "SHA512", "MD5")
$HASH_FUNCTIONS_STR = $HASH_FUNCTIONS -join '|' # SHA1|SHA256|SHA384|SHA512|MD5
$NERDCTL_CHECKSUM_FILE_PATTERN = "(?<hashfunction>(?:^({0})))" -f ($HASH_FUNCTIONS -join '|')
$NERDCTL_FILTER_SCRIPTBLOCK_STR = { (("{0}" -match "$NERDCTL_CHECKSUM_FILE_PATTERN") -and "{0}" -notmatch ".*.asc$") }.ToString()


Set-Variable -Option AllScope -scope Global -Visibility Public -Name "CONTAINERD_REPO" -Value "containerd/containerd" -Force
Set-Variable -Option AllScope -scope Global -Visibility Public -Name "BUILDKIT_REPO" -Value "moby/buildkit" -Force
Set-Variable -Option AllScope -scope Global -Visibility Public -Name "NERDCTL_REPO" -Value "containerd/nerdctl" -Force
Set-Variable -Option AllScope -scope Global -Visibility Public -Name "WINCNI_PLUGIN_REPO" -Value "microsoft/windows-container-networking" -Force
Set-Variable -Option AllScope -scope Global -Visibility Public -Name "CLOUDNATIVE_CNI_REPO" -Value "containernetworking/plugins" -Force


function Get-LatestToolVersion($tool) {
    # Get the repository based on the tool
    $repository = switch ($tool.ToLower()) {
        "containerd" { $CONTAINERD_REPO }
        "buildkit" { $BUILDKIT_REPO }
        "nerdctl" { $NERDCTL_REPO }
        "wincniplugin" { $WINCNI_PLUGIN_REPO }
        "cloudnativecni" { $CLOUDNATIVE_CNI_REPO }
        Default { Throw "Couldn't get latest $tool version. Invalid tool name: '$tool'." }
    }

    # Get the latest release version URL string
    $uri = "https://api.github.com/repos/$repository/releases/latest"

    Write-Debug "Getting the latest $tool version from $uri"

    # Get the latest release version
    try {
        $response = Invoke-WebRequest -Uri $uri -UseBasicParsing
        $version = ($response.content | ConvertFrom-Json).tag_name
        return $version.TrimStart("v")
    }
    catch {
        Throw "Couldn't get $tool latest version from $uri. $($_.Exception.Message)"
    }
}

function Test-EmptyDirectory($path) {
    if (-not (Test-Path -Path $path)) {
        return $true
    }

    $pathItems = Get-ChildItem -Path $path -Recurse -ErrorAction Ignore | Where-Object {
        !$_.PSIsContainer -or ($_.PSIsContainer -and $_.GetFileSystemInfos().Count -ne 0) }
    if (!$pathItems) {
        return $true
    }

    $itemCount = $pathItems | Measure-Object
    return ($itemCount.Count -eq 0)
}

function Get-ReleaseAssets {
    [OutputType([PSCustomObject])]
    param (
        [string]$repo, # containers repo-owner/$repo-name
        [string]$version,
        [string]$OSArch
    )

    function Invoke-GitHubApi {
        param($uri)
        try {
            Write-Debug "Invoking GitHub API. URI: $uri"
            $response = Invoke-RestMethod -Uri "$uri" -Headers @{ "User-Agent" = "PowerShell" }
            return $response
        }
        catch {
            Throw "GitHub API error. URL: `"$uri`". Error: $($_.Exception.Message)"
        }
    }

    Write-Debug "Getting release assets:`n`trepo: $repo`n`trelease version: $version`n`trelease architecture: $OSArch "
    $baseApiUrl = "https://api.github.com/repos/$repo"
    if ($version -eq "latest") {
        $apiUrl = "$baseApiUrl/releases/latest"
    }
    else {
        # We use this method to get the release assets for a specific version
        # because creating a string for the version tag is not always consistent
        # e.g. "v1.0.0" vs "1.0.0"
        # The q parameter used for searching is not available in the GitHub API's /tags endpoint.
        # GitHub's API for listing tags (/repos/{owner}/{repo}/tags) does not support querying or filtering
        # directly through a q parameter like some other endpoints might (e.g., the search API).

        # Get all releases tags
        $response = Invoke-GitHubApi -Uri "$baseApiUrl/tags"
        $releaseTag = $response | Where-Object { ($_.name.TrimStart("v")) -eq ($version.TrimStart("v")) }

        if (-not $releaseTag) {
            Throw "Couldn't find release tags for the provided version: '$version'"
        }

        if ($releaseTag.Count -gt 1) {
            Write-Warning "Found multiple release tags for the provided version: '$version'. Using the first tag."
        }

        $releaseTagName = $releaseTag | Select-Object -First 1 -ExpandProperty name

        # Get the release with the specified tag
        Write-Debug "Release tag: $releaseTagName"
        $apiUrl = "$baseApiUrl/releases/tags/$releaseTagName"
    }
    $response = Invoke-GitHubApi -Uri "$apiUrl"

    # Filter list of assets by architecture and file name
    $releaseAssets = $response | Select-Object -Property name, url, created_at, published_at, `
    @{ l = "version"; e = { $_.tag_name } }, `
    @{ l = "assets_url"; e = { $_.assets[0].url } }, `
    @{ l = "release_assets"; e = {
            $_.assets |
            Where-Object {
                (
                    # Filter assets by OS (windows) and architecture
                    # In the "zip|tar.gz" regex, we do not add the "$" at the end to allow for checksum files to be included
                    # The checksum files end with eg: ".tar.gz.sha256sum"
                    ($_.name -match "(windows(.+)$OSArch)") -or

                    # nerdctl checksum files are named "SHA256SUMS".
                    (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.name)))
                )
            } |
            ForEach-Object {
                Write-Debug ("Asset name: {0}" -f $_.Name)
                [PSCustomObject]@{
                    "asset_name"         = $_.name
                    "asset_download_url" = $_.browser_download_url
                    "asset_size"         = $_.size
                }
            }
        }
    }

    # Check if any release assets were found for the specified architecture
    $archReleaseAssets = $releaseAssets.release_assets | Where-Object { ($_.asset_name -match "windows(.+)$OSArch") }
    if ($archReleaseAssets.Count -eq 0) {
        Throw "Couldn't find release assets for the provided architecture: '$OSArch'"
    }

    if ($archReleaseAssets.Count -lt 2) {
        Write-Warning "Some assets may be missing for the release. Expected at least 2 assets, found $($archReleaseAssets.Count)."
    }

    # Return the assets for the release. Includes the archive file for the binaries and the checksum files.
    return $releaseAssets
}

function Get-InstallationFile {
    [OutputType([string[]])]
    param(
        [parameter(Mandatory, HelpMessage = "Information (parameters) of the file to download")]
        [PSCustomObject] $fileParameters
    )

    begin {
        function Receive-File {
            param($params)

            $MaximumRetryCount = 3
            $RetryIntervalSec = 60
            $lastError = $null  # Store the last exception

            do {
                try {
                    Invoke-WebRequest -Uri $params.Uri -OutFile $params.DownloadPath -UseBasicParsing
                    return
                }
                catch {
                    $lastError = $_  # Store the last error for proper exception handling
                    Write-Warning "Failed to download `"$($params.Feature)`" release assets. Retrying... ($MaximumRetryCount retries left)"
                    Start-Sleep -Seconds $RetryIntervalSec
                    $MaximumRetryCount -= 1
                }
            } until ($MaximumRetryCount -eq 0)

            # Throw the last encountered error after all retries fail
            Throw "Couldn't download `"$($params.Feature)`" release assets from `"$($params.Uri)`".`n$($lastError.Exception.Message)"
        }

        function DownloadAssets {
            param(
                [string]$featureName,
                [string]$version,
                [string]$downloadPath = "$HOME\Downloads",
                [PSCustomObject]$releaseAssets,
                [PSCustomObject]$file
            )

            $archiveFile = $checksumFile = $null
            foreach ($asset in $releaseAssets.PSObject.Properties) {
                $key = $asset.Name
                $asset_params = $asset.Value

                $destPath = Join-Path -Path $downloadPath -ChildPath $asset_params.asset_name
                switch ($key) {
                    "ArchiveFileAsset" { $archiveFile = $destPath }
                    "ChecksumFileAsset" { $checksumFile = $destPath }
                    default { Throw "Invalid input: $key" }
                }

                $downloadParams = [PSCustomObject]@{
                    Feature      = $featureName
                    Version      = $version
                    Uri          = $asset_params.asset_download_url
                    DownloadPath = $destPath
                }

                Write-Debug "Downloading asset $($asset_params.asset_name)...`n`tVersion: $version`n`tURI: $($downloadParams.Uri)`n`tDestination path: $($downloadParams.DownloadPath)"
                try {
                    Receive-File -Params $downloadParams
                }
                catch {
                    Write-Error "Failed to download $($downloadParams.Feature) release assets. $($_.Exception.Message)"
                    Throw $_  # Re-throw the exception to halt execution if a download fails
                }
            }

            # Verify that both the archive and checksum files were downloaded
            if (-not (Test-Path $archiveFile)) {
                Throw "Archive file not found in the release assets: `'$archiveFile`""
            }
            if (-not (Test-Path $checksumFile)) {
                Throw "Checksum file not found in the release assets: `'$checksumFile`""
            }

            # Verify checksum
            try {
                $isValidChecksum = if ([System.IO.Path]::GetExtension($checksumFile) -eq ".json") {
                    Test-Checksum -JSON -DownloadedFile $archiveFile -ChecksumFile $checksumFile -SchemaFile $file.ChecksumSchemaFile
                }
                else {
                    Test-Checksum -DownloadedFile $archiveFile -ChecksumFile $checksumFile
                }
            }
            catch {
                Write-Error "Checksum verification process failed: $($_.Exception.Message)"
                Throw $_  # Re-throw the exception if checksum verification fails
            }

            # Remove the checksum file after verification
            if (Test-Path -Path $checksumFile) {
                Remove-Item -Path $checksumFile -Force -ErrorAction SilentlyContinue
            }

            if (-not $isValidChecksum) {
                Write-Error "Checksum verification failed for $archiveFile. The file will be deleted."

                # Remove the checksum file after verification
                if (Test-Path -Path $archiveFile) {
                    Remove-Item -Path $archiveFile -Force -ErrorAction SilentlyContinue
                }
                Throw "Checksum verification failed. One or more files are corrupted."
            }

            return $archiveFile
        }
    }

    process {
        # Fetch the release assets based on the provided parameters
        $releaseAssets = Get-ReleaseAssets -repo $fileParameters.Repo -version $fileParameters.Version -OSArch $fileParameters.OSArchitecture

        # Filter file names based on the provided regex or default logic
        if ([string]::IsNullOrWhiteSpace($fileParameters.FileFilterRegEx)) {
            # Default logic to filter the archive and checksum files
            $filteredAssets = $releaseAssets.release_assets | Where-Object {
                # In the "zip|tar.gz" regex, we do not add the "$" at the end to allow for checksum files to be included
                # The checksum files end with eg: ".sha256sum"
                ($_.asset_name -match ".*(.zip|.tar.gz)") -or

                # Buildkit checksum files are named ending with ".provenance.json" or ".sbom.json"
                # We only need the ".sbom.json" file
                ($_.asset_name -match ".sbom.json$") -or

                # nerdctl checksum files are named "SHA256SUMS". Check file names that have such a format.
                (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.asset_name)))
            }
        }
        else {
            # Use the provided regex to filter the archive and checksum files
            $fileFilterRegEx = $fileParameters.FileFilterRegEx -replace "<__VERSION__>", "v?$($releaseAssets.version.TrimStart('v'))"
            Write-Debug "File filter: `"$fileFilterRegEx`""
            $filteredAssets = $releaseAssets.release_assets | Where-Object { $_.asset_name -match $fileFilterRegEx }
        }

        # Pair archive and checksum files
        $assetsToDownload = @()
        $archiveExtensionStr = @(".zip", ".tar.gz", ".tgz") -join "|"
        $failedDownloads = @()
        foreach ($asset in $filteredAssets) {
            if ($asset.asset_name -notmatch "(?<extension>$archiveExtensionStr)$") {
                continue
            }

            $fileExtension = $matches.extension

            # Remove the trailing archive file extension to get the checksum file name
            $assetFileName = $asset.asset_name -replace "$fileExtension", ""

            # Find the checksum file that matches the archive
            $checksumAsset = $filteredAssets | Where-Object {
                ($_.asset_name -match "(?:(^$assetFileName).*($HASH_FUNCTIONS_STR))") -or

                # Buildkit checksum is in .sbom.json
                ($_.asset_name -match ".sbom.json$") -or

                (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.asset_name)))
            }

            if (-not $checksumAsset) {
                Write-Error "Checksum file for $assetFileName not found. Skipping download."
                $failedDownloads += $asset.asset_name
                continue
            }

            $assetsToDownload += @{
                FeatureName   = $releaseAssets.name
                Version       = $releaseAssets.version
                DownloadPath  = $fileParameters.DownloadPath
                File          = $fileParameters
                ReleaseAssets = [PSCustomObject]@{
                    ArchiveFileAsset  = $asset
                    ChecksumFileAsset = $checksumAsset
                }
            }
        }

        if ($failedDownloads) {
            $errorMsg = "Failed to find checksum files for $($failedDownloads -join ', ')."
        }

        # Download the archive and verify checksum
        $archiveFiles = $failedDownloads = @()
        Write-Debug "Assets to download count: $($assetsToDownload.ReleaseAssets.Count)"
        foreach ($asset in $assetsToDownload) {
            try {
                Write-Debug "Downloading $($asset.FeatureName) assets..."
                $archiveFile = DownloadAssets @asset

                Write-Debug "Downloaded archive file: `"$archiveFile`""
                $archiveFiles += $archiveFile
            }
            catch {
                Write-Error "Failed to download assets for `"$($asset.FeatureName)`". $_"
                $failedDownloads += $asset.FeatureName
            }
        }

        if ($errorMsg) {
            Throw "Some files were not downloaded. $errorMsg"
        }

        if ($failedDownloads) {
            Throw "Failed to download assets for $($failedDownloads -join ', '). See logs for detailed error information."
        }

        # Return the archive file path. May be multiple files.
        # eg. containerd may contain containerd, cri-containerd, and cri-containerd-cni
        return $archiveFiles
    }

    end {
        Write-Information "File download and verification process completed."
    }
}

function Test-CheckSum {
    param(
        [Parameter(Mandatory, ParameterSetName = 'Default')]
        [Parameter(Mandatory, ParameterSetName = 'JSON')]
        [ValidateNotNullOrEmpty()]
        [string] $DownloadedFile,

        [Parameter(Mandatory, ParameterSetName = 'Default')]
        [Parameter(Mandatory, ParameterSetName = 'JSON')]
        [ValidateNotNullOrEmpty()]
        [string] $ChecksumFile,

        [Parameter(Mandatory, ParameterSetName = 'JSON')]
        [switch]$JSON,

        [Parameter(Mandatory, ParameterSetName = 'JSON')]
        [ValidateNotNullOrEmpty()]
        [string]$SchemaFile,

        [Parameter(ParameterSetName = 'JSON')]
        [ScriptBlock]$ExtractDigestScriptBlock,

        [Parameter(ParameterSetName = 'JSON')]
        [System.Array]$ExtractDigestArguments
    )

    Write-Debug "Checksum verification...`n`tSource file: $DownloadedFile`n`tChecksum file: $ChecksumFile"

    if (-not (Test-Path -Path $downloadedFile)) {
        Throw "Couldn't find source file: `"$downloadedFile`"."
    }

    if (-not (Test-Path -Path $ChecksumFile)) {
        Throw "Couldn't find checksum file: `"$ChecksumFile`"."
    }

    if ($JSON) {
        Write-Debug "Checksum file format: JSON"
        Write-Debug "SchemaFile: $SchemaFile"
        return (
            Test-JSONChecksum `
                -DownloadedFile $downloadedFile `
                -ChecksumFile $ChecksumFile `
                -SchemaFile $SchemaFile `
                -ExtractDigestScriptBlock $ExtractDigestScriptBlock `
                -ExtractDigestArguments $ExtractDigestArguments
        )
    }

    Write-Debug "Checksum file format: Text"
    return Test-FileChecksum -DownloadedFile $DownloadedFile -ChecksumFile $ChecksumFile
}

function Test-FileChecksum {
    # https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/usedeclaredvarsmorethanassignments?view=ps-modules#special-cases
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "isValid", Justification = "Special case")]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "found", Justification = "Special case")]

    [OutputType([bool])]
    param(
        [parameter(Mandatory)]
        [string] $DownloadedFile,

        [Parameter(Mandatory)]
        [string] $checksumFile
    )

    # Get download file name from downloaded file
    $downloadedFileName = Split-Path -Leaf $DownloadedFile

    $isValid = $false
    try {
        # Extract algorithm from checksum file name
        if ($checksumFile -notmatch "(?<hashfunction>$HASH_FUNCTIONS_STR)") {
            Throw "Invalid hash function. Supported hash functions: $($HASH_FUNCTIONS -join ', ')"
        }
        $algorithm = $matches.hashfunction
        Write-Debug "Algorithm: $algorithm"
        $downloadedChecksum = Get-FileHash -Path $DownloadedFile -Algorithm $algorithm

        # checksum is stored in a file named SHA256SUMS
        # checksum file content format: <checksum> <filename>
        # separate by two spaces
        $found = $false
        Get-Content -Path $checksumFile | ForEach-Object {
            # Split the read line to extract checksum and filename
            if ($_ -notmatch "^([\d\w]+)(\s){1,2}([\S]+)$") {
                Throw "Invalid checksum file content format in $checksumFile. Expected format: <checksum> <filename>."
            }

            # 0: full match, 1: checksum, 2: space, 3: filename
            $checksum = $matches[1]
            $filename = $matches[3]

            # Check if the downloaded file name matches any of the file names in the checksum file
            if ($filename -match "^(?:\.\/release\/)?($downloadedFileName)$") {
                $isValid = $downloadedChecksum.Hash -eq $checksum
                $found = $true
                return
            }
        }

        if (-not $found) {
            Throw "Checksum not found for `"$downloadedFileName`" in $checksumFile"
        }
    }
    catch {
        Throw "Checksum verification failed for $DownloadedFile. $_"
    }
    finally {
        # Delete checksum file
        Write-Debug "Deleting checksum file $checksumFile"
        if (Test-Path -Path $checksumFile) {
            Remove-Item -Path $checksumFile -Force -ErrorAction Ignore
        }
    }

    Write-Debug "Checksum verification status. {success: $isValid}"
    return $isValid
}

function Test-JSONChecksum {
    [OutputType([bool])]
    param(
        [parameter(Mandatory)]
        [string] $DownloadedFile,

        [parameter(Mandatory)]
        [string] $checksumFile,

        [Parameter(Mandatory)]
        [string] $SchemaFile,

        [Parameter(HelpMessage = "Script block to extract checksum from JSON file. If the ScriptBlock takes any parameters, pass them as an object with ``ExtractDigestArguments``. The function must return a FileDigest object with HashFunction (string) and Digest (string).")]
        [ScriptBlock]$ExtractDigestScriptBlock,

        [Parameter(HelpMessage = "Parameters to pass to the script block.")]
        [System.Array]$ExtractDigestArguments
    )

    # Validate the checksum file
    $isJsonValid = ValidateJSONChecksumFile -ChecksumFilePath $checksumFile -SchemaFile $SchemaFile
    Write-Debug "Checksum JSON file validation status. {success: $isJsonValid}"

    if ($null -eq $ExtractDigestScriptBlock) {
        Write-Debug "Using default JSON checksum extraction script block"
        $ExtractDigestScriptBlock = ${function:GenericExtractDigest}
        $ExtractDigestArguments = @($DownloadedFile, $checksumFile)
    }

    # Invoke the script block to extract the file digest
    Write-Debug "Extracting file digest from $checksumFile"
    $extractedFileDigest = & $ExtractDigestScriptBlock @ExtractDigestArguments

    # Since Invoke() returns a collection, we need to extract the first item
    if ( ($null -eq $extractedFileDigest) -or
        ($extractedFileDigest.Count -ne 1) -or
        ($extractedFileDigest[0].GetType().Name -ne "FileDigest")
    ) {
        Throw 'Invalid value. Requires a value with type "FileDigest".'
    }

    # Validate the hash function and checksum
    $isValid = $false
    try {
        $algorithm = $extractedFileDigest[0].HashFunction.ToUpper()
        $digest = $extractedFileDigest[0].Digest

        # Validate the hash function
        if ($HASH_FUNCTIONS -notcontains $algorithm) {
            Throw "Invalid hash function, `"$algorithm`". Supported algorithms are: $($HASH_FUNCTIONS -join ', ')"
        }

        # Validate the checksum
        $hash = Get-FileHash -Path $DownloadedFile -Algorithm $algorithm
        $isValid = ($digest -eq $hash.Hash)
    }
    catch {
        Throw "Checksum verification failed for $downloadedFile. $_"
    }
    finally {
        # Delete checksum checksumFile
        Write-Debug "Deleting checksum file $checksumFile"
        if (Test-Path -Path $checksumFile) {
            Remove-Item -Path $checksumFile -Force -ErrorAction Ignore
        }
    }

    Write-Debug "Checksum verification status. {success: $isValid}"
    return $isValid
}

function ValidateJSONChecksumFile {
    param(
        [parameter(Mandatory = $true, HelpMessage = "Downloaded checksum file path")]
        [String]$ChecksumFilePath,
        [parameter(Mandatory = $true, HelpMessage = "JSON schema file path")]
        [String]$SchemaFile
    )

    Write-Debug "Validating JSON checksum file...`n`tChecksum file path: $ChecksumFilePath`n`tSchema file: $SchemaFile"

    # Check if the schema file exists
    if (-not (Test-Path -Path $SchemaFile)) {
        Throw "Couldn't find the JSON schema file: `"$SchemaFile`"."
    }

    $schemaFileContent = Get-Content -Path $SchemaFile -Raw
    if ([string]::IsNullOrWhiteSpace($schemaFileContent)) {
        Throw "Invalid schema file: $SchemaFile. Schema file is empty."
    }

    # Test JSON checksum file is valid
    try {
        $isValidJSON = Test-Json -Json "$(Get-Content -Path $ChecksumFilePath -Raw)" -Schema "$schemaFileContent"
        return $isValidJSON
    }
    catch {
        Throw "Invalid JSON format in checksum file. $_"
    }
}

function GenericExtractDigest {
    param(
        [parameter(Mandatory = $true, HelpMessage = "Downloaded tool file path")]
        [String]$DownloadedFile,

        [parameter(Mandatory = $true, HelpMessage = "Downloaded checksum file path")]
        [String]$ChecksumFile
    )

    Write-Debug "Extracting digest from $checksumFile using default script block for in-toto SBOM format"

    # Read the JSON file and get the checksum
    $jsonContent = Get-Content -Path $ChecksumFile -Raw | ConvertFrom-Json

    # Check if using in-toto SBOM format: https://github.com/in-toto/attestation/tree/v0.1.0/spec#statement
    if ($jsonContent._type -notlike "https://in-toto.io/*") {
        Throw( -join (
                "Invalid checksum JSON format. Expected in-toto SBOM format: $($jsonContent._type). ",
                "Please provide an appropriate script block to extract the digest for $($jsonContent._type) format."))
        return
    }

    # Check if the downloaded filename is the same as subject.name
    $downloadedFileName = Split-Path "$DownloadedFile" -Leaf
    for ($i = 0; $i -lt $jsonContent.subject.Count; $i++) {
        $subject = $jsonContent.subject[$i]

        if ($subject.name -ne $downloadedFileName) {
            continue
        }

        $digest = $subject.digest
        $algorithm = ($digest | Get-Member -MemberType NoteProperty).Name
        $checksum = $digest.$algorithm

        return ([FileDigest]::new($algorithm, $checksum))
    }

    Throw "Downloaded file name does not match the subject name ($($subject.name)) in the JSON file."
}

function Get-DefaultInstallPath($tool) {
    switch ($tool) {
        "buildkit" {
            $executable = "build*.exe"
        }
        Default {
            $executable = "$tool.exe"
        }
    }

    $source = Get-Command -Name $executable -ErrorAction Ignore | `
        Where-Object { $_.Source -like "*$tool*" } | `
        Select-Object Source -Unique
    if ($source) {
        return (Split-Path -Parent $source[0].Source) -replace '(\\bin)$', ''
    }
    return "$Env:ProgramFiles\$tool"
}

function Install-RequiredFeature {
    param(
        [string] $Feature,
        [string] $InstallPath,
        [string[]] $SourceFile,
        [string] $EnvPath,
        [boolean] $cleanup,

        # Use by WinCNI plugin to avoid updating the environment path
        [boolean] $UpdateEnvPath = $true
    )
    # Create the directory to untar to
    Write-Information -InformationAction Continue -MessageData "Extracting $Feature to $InstallPath"
    if (!(Test-Path $InstallPath)) {
        New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null
    }

    # Untar file
    $failed = @()
    foreach ($file in $SourceFile) {
        if (-not (Test-Path -Path $file)) {
            Throw "Couldn't find source file: `"$file`"."
        }

        Write-Debug "Expand archive:`n`tSource file: $file`n`tDestination Path: $InstallPath"
        $cmdOutput = Invoke-ExecutableCommand -executable "tar.exe" -arguments "-xf `"$file`" -C `"$InstallPath`"" -timeout  (60 * 2)
        if ($cmdOutput.ExitCode -ne 0) {
            Write-Error "Failed to expand archive `"$file`" at `"$InstallPath`". Exit code: $($cmdOutput.ExitCode). $($cmdOutput.StandardError.ReadToEnd())"
            $failed += $file
        }
    }

    if ($failed) {
        Throw "Couldn't expand archive file(s) $($failed -join ','). See logs for detailed error information."
    }

    # Add to env path
    if ($UpdateEnvPath -and -not [string]::IsNullOrWhiteSpace($envPath)) {
        Add-FeatureToPath -Feature $feature -Path $envPath
    }

    # Clean up
    if ($CleanUp) {
        Write-Output "Cleanup to remove downloaded files"
        if (Test-Path -Path $SourceFile -ErrorAction SilentlyContinue) {
            Remove-Item -Path $SourceFile -Force -ErrorAction Ignore
        }
    }
}

function Add-FeatureToPath ($Path, $Feature) {
    @("User", "System") | ForEach-Object { Update-EnvironmentPath -Tool $Feature -Path $Path -Action 'Add' -PathType $_ }
}

function Remove-FeatureFromPath {
    [CmdletBinding(
        SupportsShouldProcess = $true
    )]
    param(
        [string]$Feature
    )

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "$feature will be removed from Environment paths")) {
            @("User", "System") | ForEach-Object { Update-EnvironmentPath -Tool $Feature -Action 'Remove' -PathType $_ }
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Test-ServiceRegistered ($service) {
    $scQueryResult = (sc.exe query $service) | Select-String -Pattern "SERVICE_NAME: $service"
    return ($null -ne $scQueryResult)
}

function Invoke-ServiceAction ($Action, $service) {
    if (!(Test-ServiceRegistered -Service $service)) {
        throw "$service service does not exist as an installed service."
    }

    $serviceInfo = Get-Service $service -ErrorAction Ignore
    if (!$serviceInfo) {
        Throw "$service service does not exist as an installed service."
    }

    switch ($Action) {
        'Start' {
            Invoke-StartService -Service $service
        }
        'Stop' {
            Invoke-StopService -Service $service
        }
        Default {
            Throw 'Not implemented'
        }
    }
}

function Invoke-StartService($service) {
    process {
        try {
            Start-Service -Name $service

            # Waiting for the service to come to a steady state
            (Get-Service -Name $service -ErrorAction Ignore).WaitForStatus('Running', '00:00:30')

            Write-Debug "Success: { Service: $service, Action: 'Start' }"
        }
        catch {
            Throw "Couldn't start $service service. $_"
        }
    }
}

function Invoke-StopService($service) {
    process {
        try {
            Stop-Service -Name $service -NoWait -Force

            # Waiting for the service to come to a steady state
            (Get-Service -Name $service -ErrorAction Ignore).WaitForStatus('Stopped', '00:00:30')

            Write-Debug "Success: { Service: $service, Action: 'Stop' }"
        }
        catch {
            Throw "Couldn't stop $service service. $_"
        }
    }
}

function Test-ConfFileEmpty($Path) {
    if (!(Test-Path -LiteralPath $Path)) {
        return $true
    }

    $isFileNotEmpty = (([System.IO.File]::ReadAllText($Path)) -match '\S')
    return (-not $isFileNotEmpty )
}

function Uninstall-ProgramFiles($path) {
    try {
        Get-Item -Path "$path" -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
    }
    catch {
        $errMsg = $_
        if ($errMsg -match "denied") {
            Write-Error "Failed to delete directory: '$path'. Access to path denied. To resolve this issue, see https://github.com/microsoft/containers-toolkit/blob/main/docs/docs/FAQs.md#resolving-uninstallation-error-access-to-path-denied"
        }
        else {
            Write-Error "Failed to delete directory: '$path'. $_"
        }
    }
}

function Invoke-ExecutableCommand {
    [OutputType([System.Diagnostics.Process])]
    param (
        [parameter(Mandatory)]
        [String] $executable,
        [parameter(Mandatory)]
        [String] $arguments,
        [Parameter(Mandatory = $false, HelpMessage = "Period of time to wait (in seconds) for the associated process to exit. Default is 15 seconds.")]
        [Int32] $timeout = 15
    )

    Write-Debug "Executing '$executable $arguments'"

    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $executable
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $arguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    # Blocks the current thread of execution until the time has elapsed or the process has exited.
    $p.WaitForExit($timeout * 1000) | Out-Null

    if (-not $p.HasExited) {
        Write-Debug "Execution did not complete in $timeout seconds."
    }
    else {
        Write-Debug "Command execution completed. Exit code: $($p.ExitCode)"
    }

    return $p
}

Export-ModuleMember -Variable CONTAINERD_REPO, BUILDKIT_REPO, NERDCTL_REPO, WINCNI_PLUGIN_REPO, CLOUDNATIVE_CNI_REPO
Export-ModuleMember -Function Get-LatestToolVersion
Export-ModuleMember -Function Get-DefaultInstallPath
Export-ModuleMember -Function Test-EmptyDirectory
Export-ModuleMember -Function Get-InstallationFile
Export-ModuleMember -Function Install-RequiredFeature
Export-ModuleMember -Function Invoke-ExecutableCommand
Export-ModuleMember -Function Test-ServiceRegistered
Export-ModuleMember -Function Add-FeatureToPath
Export-ModuleMember -Function Remove-FeatureFromPath
Export-ModuleMember -Function Invoke-ServiceAction
Export-ModuleMember -Function Test-ConfFileEmpty
Export-ModuleMember -Function Uninstall-ProgramFiles
Export-ModuleMember -Function Test-CheckSum

# SIG # Begin signature block
# MIIoRAYJKoZIhvcNAQcCoIIoNTCCKDECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD5E3ayxuJhyOJ0
# 23s8KKLrxNEslJs3zqI3EZxMp+wlR6CCDYswggYJMIID8aADAgECAhMzAAAD9LjE
# XeFOcLZ+AAAAAAP0MA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwNzE3MjEwMjM1WhcNMjUwOTE1MjEwMjM1WjCBiDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWlj
# cm9zb2Z0IDNyZCBQYXJ0eSBBcHBsaWNhdGlvbiBDb21wb25lbnQwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv3P8bL08GKolFW7QNDVOF0aM4iqMxVvAW
# VM124/82xbjAraJkKxieMrQa1Fc95LVGgxmJIi5R6QKMz2MO9bnwC7kSkPqoZJil
# 26bRLY6jinjbwPpK3TzbW7z9bXfWw5bPFlt72NVIdXJ3xtHoYa+AOi++CF2Ry7+7
# o1AzvotJwG6lQSiCMKeMt8apqEF1f+QkDFEUv5tezw9748DeHW9orvo4IPzWa7vW
# QgljB08LKSnzTN9/Jot2coWpFv4YuEoJZmR2ofPJMnDUUruDORTXnxwhfvd/wUmI
# SoEysSqobkNV+qFuUmSShYrx8R1zHm7P6G/iRMIKYmSrIYBKUvndAgMBAAGjggFz
# MIIBbzAfBgNVHSUEGDAWBgorBgEEAYI3TBEBBggrBgEFBQcDAzAdBgNVHQ4EFgQU
# Dz4uMjS8YCSZaU0449GJYQ1ufyowRQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEWMBQGA1UEBRMNMjMxNTIyKzUwMjUxODAfBgNV
# HSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0Ey
# MDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZF
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQ
# Q0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL
# BQADggIBABUCAiEn4g8i5T3VCP8160IY4ERdvZi5QZ2pSnBPW1dswVhLxkNTiCTV
# XKDjTQ4EwDBNSZZGJePz4+t86pKhlBON3S7wswf5fCovJLlIiKbw+E4TZeY6xAxd
# +5zV7Q2lsQhPHxiOY0PIGUE0KJfv/DQUulD8DrE0rru7yOO+DJI0muoK0BbHhRfd
# mAJhp2gbYRkarEIkhML9m3gR12mCBb69Vocm4IyOBivUPMjjvQMkERF7cR07k2uP
# 6dmpR8wtof9la0/K0wgiP5XuQUsAqgzhXrljH7dK7nqGrBDjJtrRdYfvVL+Rcz9i
# YZO280g2uNtac5em3HOEsactAL7XKqZ4o7s9sRyp/bTNLLRmhFMB729IL+Hi0YM7
# C8th3HZ5nP+77L46KUGip6QgRIJs+EO0YNW+AwgMxPfKpTx/Ggh8Z85kP7HLDZJk
# ZdPO/3cgVOTO4ax21vO2yMPCdfoGGr2ZLZw4SjEbGuOZJ22iGMV7tBvHk8nWAt3q
# +j/icAq99GA1nIPnw3jK3K9OwGqwA9eiWsO8/bHMm6s50UKIFupMKm6qObosaVBy
# R58rf8Cxumka7hPy1eSJSzQyA4UqYNTWuChsTfqgRLmLomS6yAu7t4r/bM4mGl+2
# Ki+avhQ4COm3jWWd0V6UGIP3T4zaKNs2GWFBIYsb/6XVvvi7pz/JMIIHejCCBWKg
# AwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYw
# NzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGf
# Qhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRg
# JGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NE
# t13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnn
# Db6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+E
# GvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+t
# GSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxh
# H2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AV
# s70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f
# 7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3D
# KI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9Jaw
# vEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1Ud
# DgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi
# AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRy
# LToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3Js
# Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDEx
# XzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDEx
# XzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/
# BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2Nz
# L3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAA
# bwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUA
# A4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+
# vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4H
# Limb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6
# aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiX
# mE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn
# +N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAq
# ZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5h
# YbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/
# RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXm
# r/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMyk
# XcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGg8wghoLAgEB
# MIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNV
# BAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAP0uMRd4U5w
# tn4AAAAAA/QwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEIAih5WVYQw/XLv11645meHVV+wvFHu+NW4WZf85+zxBeMEQGCisGAQQBgjcC
# AQwxNjA0oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNy
# b3NvZnQuY29tIDANBgkqhkiG9w0BAQEFAASCAQArvapeCiUzYgJ0S36TLJB+YA5k
# p+/oZSUlqjK4lsZYh2XB1ch8HDlh5ZoX7BpGgWr2pUNiOn/ee6NspMDDNcjGyBgv
# LO+Y8HY4+sMVn9FuCKLrTtzFr/0BpbZubtYIyhC6KaGFjjYRHSD8FfBxRyS/vB2S
# lOX+dfOfqXzD6c4FFfmHkFHUnakRXNV7/8+bvfAlo2wLeRaJuXGAWeXkWLH4eI4V
# h/re0pQabqNbmLhod3m/+6rHolrH0tsz7u6mJm5NXxiLeSAJZ+yeT/vbggz6ZUBo
# EOEySr1MCRoIP7Qn5mY3ZzV/dKclUGIqSBfG7BlyhLtQKbW3AKMyFVVxthoIoYIX
# lzCCF5MGCisGAQQBgjcDAwExgheDMIIXfwYJKoZIhvcNAQcCoIIXcDCCF2wCAQMx
# DzANBglghkgBZQMEAgEFADCCAVIGCyqGSIb3DQEJEAEEoIIBQQSCAT0wggE5AgEB
# BgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEICge72xIxQq7QqX7AYmRSro+
# tz//K79xTiMr2uBYkJQeAgZoJdLn0t0YEzIwMjUwNTE1MTYxOTQ5LjYyMlowBIAC
# AfSggdGkgc4wgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsT
# Hm5TaGllbGQgVFNTIEVTTjpBNDAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEe0wggcgMIIFCKADAgECAhMzAAACAnlQ
# dCEUfbihAAEAAAICMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMB4XDTI1MDEzMDE5NDI0NFoXDTI2MDQyMjE5NDI0NFowgcsxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jv
# c29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# TjpBNDAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALd5Knpy5xQY
# 6Rw+Di8pYol8RB6yErZkGxhTW0Na9C7ov2Wn52eqtqMh014fUc3ejPeKIagla43Y
# dU1mRw63fxpYZ5szSBRQ60+O4uG47l3rtilCwcEkBaFy978xV2hA+PWeOICNKI6s
# vzEVqsUsjjpEfw14OEA9dwmlafsAjMLIiNk5onYNYD7pDA3PCqMGAil/WFYXCoe8
# 8R53LSei1du1Z9P28JIv2x0Mror8cf0expjnAuZRQHtJ+4sajU5YSbownIbaOLGq
# L03JGjKl0Xx1HKNbEpGXYnHC9t62UNOKjrpeWJM5ySrZGAz5mhxkRvoSg5213Rcq
# HcvPHb0CEfGWT7p4jBq+Udi44tkMqh085U3qPUgn1uuiVjqZluhDnU6p7mcQzmH9
# YlfbwYtmKgSQk3yo57k/k/ZjH0eg6ou6BfTSoLPGrgEObzEfzkcrG8oI7kqKSilp
# EYa1CVeMPK6wxaWsdzJK3noOEvh1xWeft0W8vnTO9CUVkyFWh6FZJCSRa5SUIKog
# 6tN7tFuadt0miwf7uUL6fneCcrLg6hnO5R6rMKdIHUk1c8qcmiM/cN7nHCymLm1S
# 9AU1+V8ZOyNmBACAMF2D8M7RMaAtEMq9lAJnmoi5elBHKDfvJznV73nPxTabKxTR
# edKlZ6KAeqTI4C0N9wimrka/sdX51rZHAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQU
# 2ga5tQ+M/Z/yJ+Qgq/DLWuVIdNkwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAo
# MSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1w
# JTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAK
# BggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAIPz
# doVBTE3fseQ6gkMzWZocVlVQZypNBw+c4PpShhEyYMq/QZpseUTzYBiAs+5WW6Sf
# se0k8XbPSOdOAB9EyfbokUs8bs79dsorbmGsE8nfSUG7CMBNW3nxQDUFajuWyafK
# u6v/qHwAXOtfKte2W/NBippFhj2TRQVjkYz6f1hoQQrYPbrx75r4cOZZ761gvYf7
# 07hDUxAtqD5yI3AuSP/5CXGleJai70q8A/S0iT58fwXfDDlU5OL1pn36o+OzPDfU
# fid22K8FlofmzlugmYfYlu0y5/bLuFJ0l0TRRbYHQURk8siZ6aUqGyUk1WoQ7tE+
# CXtzzVC5VI7nx9+mZvC1LGFisRLdWw+CVef04MXsOqY8wb8bKwHij9CSk1Sr7BLt
# s5FM3Oocy0f6it3ZhKZr7VvJYGv+LMgqCA4J0TNpkN/KbXYYzprhL4jLoBQinv8o
# ikCZ9Z9etwwrtXsQHPGh7OQtEQRYjhe0/CkQGe05rWgMfdn/51HGzEvS+DJruM1+
# s7uiLNMCWf/ZkFgH2KhR6huPkAYvjmbaZwpKTscTnNRF5WQgulgoFDn5f/yMU7X+
# lnKrNB4jX+gn9EuiJzVKJ4td8RP0RZkgGNkxnzjqYNunXKcr1Rs2IKNLCZMXnT1i
# f0zjtVCzGy/WiVC7nWtVUeRI2b6tOsvArW2+G/SZMIIHcTCCBVmgAwIBAgITMwAA
# ABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAw
# OTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6c
# BwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWN
# E893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8
# OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6O
# U8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6
# BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75x
# qRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrb
# qn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XY
# cz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK
# 12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJR
# XRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnG
# rnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsG
# AQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBe
# Yl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/Bggr
# BgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1Jl
# cG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQM
# HgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1Ud
# IwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0
# dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0Nl
# ckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG
# Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0
# XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEk
# W+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zR
# oZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1
# AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthIS
# EV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4s
# a3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32
# THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMB
# V0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5P
# ndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUx
# UYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi
# 6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6C
# baUFEMFxBmoQtB1VM1izoXBm8qGCA1AwggI4AgEBMIH5oYHRpIHOMIHLMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3Nv
# ZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046
# QTQwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl
# cnZpY2WiIwoBATAHBgUrDgMCGgMVAEmJSGkJYD/df+NnIjLTJ7pEnAvOoIGDMIGA
# pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQELBQAC
# BQDr0FFkMCIYDzIwMjUwNTE1MTE0MTI0WhgPMjAyNTA1MTYxMTQxMjRaMHcwPQYK
# KwYBBAGEWQoEATEvMC0wCgIFAOvQUWQCAQAwCgIBAAICAIwCAf8wBwIBAAICEfYw
# CgIFAOvRouQCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgC
# AQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsFAAOCAQEAqqUjDKJUQO5S
# 5oWMRobJDrdADBYfc7j4mRkYrEUPK4jrYzGKExolvKc4XPg4FTbNNC26hewAakPw
# Yzv/W7lVUvT9bC/SDlDbHIBlJ8fn5W9xERKAGJW/RsAO419HsXOWuRUHfPDKxRXk
# 3keFzUKCWZ/W5lvKPbcuWsOBEgeRopyjtt5WL8HJrI7utXSEMRg5EF4rza6a+mmK
# 5o93iGgHcfSdYw6JxD6/BIwyu7Ym0wGrlfTGjC7b3lVvnD2ToFSmLdZdpJtg4AtH
# PF/IEvV0O8gIFtuncGwmu844MkSqa17rc80SsUDnYDOOdyhkBJwAW26ikitsppQr
# 3ny6MonygTGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy
# MDEwAhMzAAACAnlQdCEUfbihAAEAAAICMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkq
# hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEILzs/8/YUM0O
# mO1cxt9gYvT4CVahk6gxjEGEZoXWd4uJMIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB
# 5DCBvQQg843qARgHlsvNcta5SYvxl3zFcCypeSx50XKiV8yUX+wwgZgwgYCkfjB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAgJ5UHQhFH24oQABAAAC
# AjAiBCCneRkQYbjGMp3Wxu2U5Dh+xgPqhLWg/GvX2xjkcWuAyzANBgkqhkiG9w0B
# AQsFAASCAgAVQ+WIex7CZY6l04dRn8ZUVYJWWODaw66PWE55yYlxUqHRJ+UyKGCJ
# pEEfWV93kVygr8np/z2YYiX+ZmGrdhXQpFaCwfxfZOpPeKqf1qypozccMW+ijAZu
# BLT6cWh5zzecJIEmip4++GL9Gc/vYt06TaFLb8ZNQp1/yKidUx3nfYR+gaT3MeaN
# qxrJ6Ow3afzDnZfXi+nRe3yiVhSuTPgchmOfZJ3DJshAR96pCD9UgGJO6a8ioyCT
# IXteJFrWVq2d8Kf40xonGGeoaL67ADNDW3dTyE6TzAfOv2NWWonDVEYHByL4UrVZ
# CRZFAku71nHoQHanF8hHLpTDy/JkhNroGqfeqIn9Q5sb99Vq7oXkpMEWNyk3Jqga
# lHGewSjWXTqqJmLSlIuisV8D4TMxq3TMIqZTiVScQHmaLEo8eDQd0D15UMM3BAZZ
# IlykJn+Ng+GKUmVF0IfEJWp8zOHZwkTwEpe/WNmGNlKBkvz5c+ie1EZvjhH/6B1q
# 4yNnmfwMymX1Xp0o+/ExszsbFQr3gcwIDV21PIFBBQE7I3bfKhGr6iunze8m2ne8
# 7df/tpSJJp0T8caA0fxX3FCkkalNCGi09Qea645cwWFI0ZcyY9wHESwrhn+ZQbvb
# yFCouvOnyCJAbg4n1VvVPMF84Xd5oscKE/k/KLTqtF0UPgwKx3vk1g==
# SIG # End signature block