Includes/Download.provider.github.ps1

<#
    Provider : github
    Dependencies : PowerShellForGitHub (Get-GithubRelease)
 
    v2 : assets are selected through the download.assets[] list (patterns
    tested in order, first match wins). The GitHub digest is verified
    automatically when present. When the matched asset declares a
    signature-file, it is downloaded from the same release and used to
    verify the asset's integrity.
#>


function Get-ReleaseFromGithub {
    [CmdletBinding()]
    [OutputType([object])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{}
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $release = $null
        if ($pkgdef.version -eq "latest") {
            $release = Get-GithubRelease -Owner $pkgdef.download.owner -RepositoryName $pkgdef.download.repo -Latest
        } else {
            $releases = Get-GithubRelease -Owner $pkgdef.download.owner -RepositoryName $pkgdef.download.repo
            $release = @($releases | Where-Object { $_.tag_name -like "*$($pkgdef.version)*" }) | Select-Object -First 1
        }
        if ($release) {
            Write-Verbose "Found release $($release.tag_name) matched requested version $($pkgdef.version)"
        } else {
            Write-Fatal "No release found for version $($pkgdef.version)"
        }
        # update mustacheValues
        if ($mustacheValues) { $mustacheValues.TAGNAME = $release.tag_name }
        return $release
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Select the asset of a GitHub release matching the ordered pattern list.
#>

function Get-AssetFromGithub {
    [CmdletBinding()]
    [OutputType([System.Object])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        # Release found with Get-ReleaseFromGithub
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][System.Object]$Release
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $assets = @($Release.assets)
        if (-not $assets.Count) {
            Write-Fatal "No assets found for release $($Release.tag_name)"
        }
        Write-Verbose "Found $($assets.Count) assets for release $($Release.tag_name)"

        $match = Select-GenericPackageAsset -Names (@($assets | ForEach-Object { $_.name })) -Assets $assets -AssetSpecs @($pkgdef.download.assets) -mustacheValues $mustacheValues
        if (-not $match) {
            Write-Fatal "No asset of release $($Release.tag_name) matches the declared patterns"
        }
        return $assets[$match.Index]
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Get asset information from a GitHub release without downloading.
#>

function Get-AssetInfoFromGithub {
    [CmdletBinding()]
    [OutputType([array])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        [Parameter(Mandatory = $false)][string]$CachePath,
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{}
    )
    Begin {
        Write-EnterFunction
        $assets = @()
    }

    Process {
        $release = Get-ReleaseFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues
        $asset = Get-AssetFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release
        if ($asset) {
            $assetInfo = @{
                name         = $asset.name
                download_url = $asset.browser_download_url
                local_path   = "$CachePath/$($asset.name)"
            }
            if ($asset.digest) { $assetInfo.digest = $asset.digest }
            $assets += $assetInfo
        }
    }

    End {
        Write-LeaveFunction
        return $assets
    }
}

<#
.SYNOPSIS
Download an asset from a GitHub release.
 
.DESCRIPTION
Resolves the release, selects the asset with the ordered pattern list,
verifies the GitHub digest when available, downloads the asset (skipping
when already cached) and verifies the signature-file when declared.
#>

function Download-AssetFromGithub {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Download is a more intuitive verb for this function because Get- is too generic.")]
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Destination path (cache directory)
        [Parameter()][String]$DestinationPath,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{}
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        # Resolve DestinationPath: parameter > pkgdef.download.destination > temp path
        if ([string]::IsNullOrEmpty($DestinationPath)) {
            if ($pkgdef.download.destination) {
                $DestinationPath = $pkgdef.download.destination
            } else {
                $DestinationPath = [io.path]::GetTempPath()
            }
        }

        $release = Get-ReleaseFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues
        $asset = Get-AssetFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release
        if (-not $asset) { return $null }

        $destFile = "$DestinationPath/$($asset.name)"
        if (Test-FileExist $destFile) {
            Write-Verbose "Asset $($asset.name) already exists in $DestinationPath, skipping download"
            return $destFile
        }

        # GitHub digest verification (when the API provides one) — checked
        # against the downloaded file, since PwSh.Fw.Web Download-File does
        # not accept digest parameters
        $DigestAlgo = $null
        $rc = [string]$asset.digest -match '(?<algo>[a-z]+[0-9]+):(?<hash>[a-fA-F0-9]+)'
        if ($rc) {
            switch ($Matches.algo) {
                "sha256" { $DigestAlgo = 'SHA256' }
                "sha1"   { $DigestAlgo = 'SHA1' }
                "md5"    { $DigestAlgo = 'MD5' }
                default  { Write-Warning "Unable to detect hash algorithm in digest : $($asset.digest)" }
            }
        }

        Write-Verbose "Downloading $($asset.name) from $($asset.browser_download_url) to $DestinationPath"
        $null = Download-File -Url $asset.browser_download_url -To $DestinationPath -PassThru
        if (-not (Test-FileExist $destFile)) {
            Write-Fatal "Error downloading asset $($asset.name)"
            return $null
        }
        if ($DigestAlgo) {
            $ActualDigest = (Get-FileHash -Algorithm $DigestAlgo -Path $destFile).Hash.ToLower()
            if ($ActualDigest -ne $Matches.hash.ToLower()) {
                Write-Fatal "Digest mismatch for $($asset.name) : expected $($Matches.hash), got $ActualDigest"
                return $null
            }
            Write-Verbose "Digest verified ($DigestAlgo) for $($asset.name)"
        }

        # Signature-file verification (companion checksum from the same release)
        $spec = $null
        foreach ($a in @($pkgdef.download.assets)) {
            if ($a.pattern -and $asset.name -match (Render-MustacheField -value $a.pattern -mustacheValues $mustacheValues)) {
                $spec = $a
                break
            }
        }
        if ($spec -and $spec.'signature-file') {
            $sigPattern = Render-MustacheField -value $spec.'signature-file'.pattern -mustacheValues $mustacheValues
            $sigAsset = @($release.assets | Where-Object { $_.name -match $sigPattern }) | Select-Object -First 1
            if (-not $sigAsset) {
                Write-Fatal "Signature file matching '$sigPattern' not found in release $($release.tag_name)"
                return $null
            }
            $sigFile = Download-File -Url $sigAsset.browser_download_url -To $DestinationPath -PassThru
            if (-not (Test-GenericPackageSignature -AssetPath $destFile -SignaturePath $sigFile -Algorithm $spec.'signature-file'.algorithm)) {
                return $null
            }
        }

        return $destFile
    }

    End {
        Write-LeaveFunction
    }
}