Private/Logic/RuntimeKernel/Supply/Manifested.ArtifactSupply.ps1

function Get-ManifestedArtifactCacheRootFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$Layout
    )

    $artifactBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'artifact' -BlockName 'zipPackage'
    if (-not $artifactBlock) {
        $artifactBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'artifact' -BlockName 'executableInstaller'
    }
    if (-not $artifactBlock) {
        return $null
    }

    $layoutPropertyName = if ($artifactBlock.PSObject.Properties.Match('cacheRootLayoutProperty').Count -gt 0) { $artifactBlock.cacheRootLayoutProperty } else { $null }
    if ([string]::IsNullOrWhiteSpace($layoutPropertyName)) {
        return $null
    }

    if (-not $Layout.PSObject.Properties.Match($layoutPropertyName).Count) {
        throw "Definition '$($Definition.commandName)' references unknown layout property '$layoutPropertyName'."
    }

    return $Layout.$layoutPropertyName
}

function Get-ManifestedArtifactFileRegexFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [string]$Flavor
    )

    $artifactBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'artifact' -BlockName 'zipPackage'
    if (-not $artifactBlock) {
        $artifactBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'artifact' -BlockName 'executableInstaller'
    }
    if (-not $artifactBlock -or -not $artifactBlock.PSObject.Properties.Match('fileNamePattern').Count) {
        return $null
    }

    return (Expand-ManifestedDefinitionTemplate -Template $artifactBlock.fileNamePattern -Flavor ([regex]::Escape($Flavor)))
}

function Save-ManifestedArtifactMetadataFromPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$PackageInfo
    )

    if (-not $PackageInfo -or [string]::IsNullOrWhiteSpace($PackageInfo.Path)) {
        return
    }

    $metadata = [ordered]@{}
    if ($PackageInfo.PSObject.Properties['TagName'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.TagName)) { $metadata['Tag'] = $PackageInfo.TagName }
    if ($PackageInfo.PSObject.Properties['Version'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Version)) { $metadata['Version'] = $PackageInfo.Version }
    if ($PackageInfo.PSObject.Properties['Flavor'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Flavor)) { $metadata['Flavor'] = $PackageInfo.Flavor }
    if ($PackageInfo.PSObject.Properties['FileName'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.FileName)) { $metadata['AssetName'] = $PackageInfo.FileName }
    if ($PackageInfo.PSObject.Properties['DownloadUrl'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.DownloadUrl)) { $metadata['DownloadUrl'] = $PackageInfo.DownloadUrl }
    if ($PackageInfo.PSObject.Properties['Sha256'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Sha256)) { $metadata['Sha256'] = $PackageInfo.Sha256 }
    if ($PackageInfo.PSObject.Properties['ShaSource'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.ShaSource)) { $metadata['ShaSource'] = $PackageInfo.ShaSource }
    if ($PackageInfo.PSObject.Properties['ReleaseUrl'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.ReleaseUrl)) { $metadata['ReleaseUrl'] = $PackageInfo.ReleaseUrl }
    if ($PackageInfo.PSObject.Properties['ReleaseId'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.ReleaseId)) { $metadata['ReleaseId'] = $PackageInfo.ReleaseId }
    if ($PackageInfo.PSObject.Properties['Channel'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Channel)) { $metadata['Channel'] = $PackageInfo.Channel }
    if ($PackageInfo.PSObject.Properties['ShasumsUrl'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.ShasumsUrl)) { $metadata['ShasumsUrl'] = $PackageInfo.ShasumsUrl }
    if ($PackageInfo.PSObject.Properties['NpmVersion'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.NpmVersion)) { $metadata['NpmVersion'] = $PackageInfo.NpmVersion }
    if ($PackageInfo.PSObject.Properties['SignatureStatus'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.SignatureStatus)) { $metadata['SignatureStatus'] = $PackageInfo.SignatureStatus }
    if ($PackageInfo.PSObject.Properties['SignerSubject'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.SignerSubject)) { $metadata['SignerSubject'] = $PackageInfo.SignerSubject }
    $metadata['AcquiredAtUtc'] = (Get-Date).ToUniversalTime().ToString('o')

    Save-ManifestedArtifactMetadata -ArtifactPath $PackageInfo.Path -Metadata $metadata | Out-Null
}

function Get-ManifestedCachedZipArtifactsFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-ManifestedDefinitionFlavor -Definition $Definition
    }

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $cacheRoot = Get-ManifestedArtifactCacheRootFromDefinition -Definition $Definition -Layout $layout
    if ([string]::IsNullOrWhiteSpace($cacheRoot) -or -not (Test-Path -LiteralPath $cacheRoot)) {
        return @()
    }

    $pattern = Get-ManifestedArtifactFileRegexFromDefinition -Definition $Definition -Flavor $Flavor
    if ([string]::IsNullOrWhiteSpace($pattern)) {
        return @()
    }

    $items = Get-ChildItem -LiteralPath $cacheRoot -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match $pattern } |
        ForEach-Object {
            $metadata = Get-ManifestedArtifactMetadata -ArtifactPath $_.FullName
            [pscustomobject]@{
                TagName     = if ($metadata -and $metadata.PSObject.Properties['Tag']) { $metadata.Tag } else { $null }
                Version     = if ($metadata -and $metadata.PSObject.Properties['Version']) { $metadata.Version } elseif ($matches.Count -gt 1) { $matches[1] } else { $null }
                Flavor      = $Flavor
                FileName    = $_.Name
                Path        = $_.FullName
                Source      = 'cache'
                Action      = 'SelectedCache'
                DownloadUrl = if ($metadata -and $metadata.PSObject.Properties['DownloadUrl']) { $metadata.DownloadUrl } else { $null }
                Sha256      = if ($metadata -and $metadata.PSObject.Properties['Sha256']) { $metadata.Sha256 } else { $null }
                ShaSource   = if ($metadata -and $metadata.PSObject.Properties['ShaSource']) { $metadata.ShaSource } else { $null }
                ReleaseUrl  = if ($metadata -and $metadata.PSObject.Properties['ReleaseUrl']) { $metadata.ReleaseUrl } else { $null }
                ShasumsUrl  = if ($metadata -and $metadata.PSObject.Properties['ShasumsUrl']) { $metadata.ShasumsUrl } else { $null }
                NpmVersion  = if ($metadata -and $metadata.PSObject.Properties['NpmVersion']) { $metadata.NpmVersion } else { $null }
                SignatureStatus = if ($metadata -and $metadata.PSObject.Properties['SignatureStatus']) { $metadata.SignatureStatus } else { $null }
                SignerSubject = if ($metadata -and $metadata.PSObject.Properties['SignerSubject']) { $metadata.SignerSubject } else { $null }
            }
        } |
        Sort-Object -Descending -Property @{ Expression = { ConvertTo-ManifestedComparableVersion -VersionText $_.Version } }

    return @($items)
}

function Get-LatestCachedZipArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $cachedArtifacts = @(Get-ManifestedCachedZipArtifactsFromDefinition -Definition $Definition -Flavor $Flavor -LocalRoot $LocalRoot)
    if ([bool]$Definition.policies.requireTrustedArtifact) {
        $trustedArtifact = @($cachedArtifacts | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Sha256) } | Select-Object -First 1)
        if ($trustedArtifact) {
            return $trustedArtifact[0]
        }
    }

    return ($cachedArtifacts | Select-Object -First 1)
}

function Get-ManifestedCachedInstallerArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $artifacts = @(Get-ManifestedCachedZipArtifactsFromDefinition -Definition $Definition -Flavor $Flavor -LocalRoot $LocalRoot)
    if ($artifacts.Count -gt 0) {
        return $artifacts[0]
    }

    return $null
}

function Get-ManifestedOnlineGitHubReleaseArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-ManifestedDefinitionFlavor -Definition $Definition
    }

    $versionSpec = Get-ManifestedVersionSpec -Definition $Definition
    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'githubRelease'
    if (-not $supplyBlock) {
        return $null
    }

    $repositoryParts = @($supplyBlock.repository -split '/', 2)
    if ($repositoryParts.Count -ne 2) {
        throw "Command definition '$($Definition.commandName)' has invalid githubRelease.repository value '$($supplyBlock.repository)'."
    }

    $owner = $repositoryParts[0]
    $repository = $repositoryParts[1]

    try {
        $release = Get-ManifestedGitHubLatestRelease -Owner $owner -Repository $repository
        if ($release.Draft -or $release.Prerelease) {
            throw "The latest release for '$($supplyBlock.repository)' is not stable."
        }

        $tagName = $release.TagName
        $version = ConvertTo-ManifestedVersionTextFromRule -VersionText $tagName -Rule $versionSpec.ReleaseVersionRule
        if ([string]::IsNullOrWhiteSpace($version)) {
            $version = (ConvertTo-ManifestedComparableVersion -VersionText $tagName).ToString()
        }
        $fileName = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.assetNamePattern -Version $version -TagName $tagName -Flavor $Flavor
        $asset = Get-ManifestedGitHubReleaseAsset -Release $release -AssetName $fileName
        if (-not $asset) {
            throw "Could not find the expected GitHub release asset '$fileName'."
        }

        $checksumAssetName = $null
        if ($supplyBlock.PSObject.Properties.Match('checksum').Count -gt 0 -and $supplyBlock.checksum) {
            if ($supplyBlock.checksum.PSObject.Properties.Match('assetNamePattern').Count -gt 0) {
                $checksumAssetName = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.checksum.assetNamePattern -Version $version -TagName $tagName -Flavor $Flavor
            }
            elseif ($supplyBlock.checksum.PSObject.Properties.Match('assetName').Count -gt 0) {
                $checksumAssetName = $supplyBlock.checksum.assetName
            }
        }

        $checksum = if ($checksumAssetName) {
            Get-ManifestedGitHubReleaseAssetChecksum -Release $release -Owner $owner -Repository $repository -TagName $tagName -AssetName $fileName -FallbackSource $(if ($supplyBlock.checksum.PSObject.Properties.Match('fallbackSource').Count -gt 0) { $supplyBlock.checksum.fallbackSource } else { 'ChecksumAsset' }) -ChecksumAssetName $checksumAssetName
        }
        else {
            $null
        }

        return [pscustomobject]@{
            TagName     = $tagName
            Version     = $version
            Flavor      = $Flavor
            FileName    = $fileName
            Path        = $null
            Source      = 'online'
            Action      = 'SelectedOnline'
            DownloadUrl = $asset.BrowserDownloadUrl
            Sha256      = if ($checksum) { $checksum.Sha256 } else { $null }
            ShaSource   = if ($checksum) { $checksum.Source } else { $null }
            ReleaseUrl  = $release.HtmlUrl
        }
    }
    catch {
        $tagInfo = Get-ManifestedGitHubLatestReleaseTag -Owner $owner -Repository $repository
        if (-not $tagInfo) {
            throw
        }

        $tagName = $tagInfo.TagName
        $version = ConvertTo-ManifestedVersionTextFromRule -VersionText $tagName -Rule $versionSpec.ReleaseVersionRule
        if ([string]::IsNullOrWhiteSpace($version)) {
            $version = (ConvertTo-ManifestedComparableVersion -VersionText $tagName).ToString()
        }
        $fileName = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.assetNamePattern -Version $version -TagName $tagName -Flavor $Flavor

        $checksumAssetName = $null
        if ($supplyBlock.PSObject.Properties.Match('checksum').Count -gt 0 -and $supplyBlock.checksum) {
            if ($supplyBlock.checksum.PSObject.Properties.Match('assetNamePattern').Count -gt 0) {
                $checksumAssetName = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.checksum.assetNamePattern -Version $version -TagName $tagName -Flavor $Flavor
            }
            elseif ($supplyBlock.checksum.PSObject.Properties.Match('assetName').Count -gt 0) {
                $checksumAssetName = $supplyBlock.checksum.assetName
            }
        }

        $checksum = if ($checksumAssetName) {
            Get-ManifestedGitHubReleaseAssetChecksum -Owner $owner -Repository $repository -TagName $tagName -AssetName $fileName -FallbackSource $(if ($supplyBlock.checksum.PSObject.Properties.Match('fallbackSource').Count -gt 0) { $supplyBlock.checksum.fallbackSource } else { 'ChecksumAsset' }) -ChecksumAssetName $checksumAssetName
        }
        else {
            $null
        }

        return [pscustomobject]@{
            TagName     = $tagName
            Version     = $version
            Flavor      = $Flavor
            FileName    = $fileName
            Path        = $null
            Source      = 'online'
            Action      = 'SelectedOnline'
            DownloadUrl = New-ManifestedGitHubReleaseAssetUrl -Owner $owner -Repository $repository -TagName $tagName -AssetName $fileName
            Sha256      = if ($checksum) { $checksum.Sha256 } else { $null }
            ShaSource   = if ($checksum) { $checksum.Source } else { $null }
            ReleaseUrl  = $tagInfo.HtmlUrl
        }
    }
}

function Get-ManifestedOnlineNodeDistArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-ManifestedDefinitionFlavor -Definition $Definition
    }

    $versionSpec = Get-ManifestedVersionSpec -Definition $Definition
    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'nodeDist'
    if (-not $supplyBlock) {
        return $null
    }

    $response = Invoke-WebRequestEx -Uri $supplyBlock.indexUrl -UseBasicParsing
    $items = $response.Content | ConvertFrom-Json
    $release = $items |
        Where-Object {
            if ($supplyBlock.PSObject.Properties.Match('releaseChannel').Count -gt 0 -and $supplyBlock.releaseChannel -eq 'lts') {
                return ($_.lts -and $_.lts -ne $false)
            }

            return $true
        } |
        Sort-Object -Descending -Property @{ Expression = { ConvertTo-ManifestedComparableVersion -VersionText $_.version } } |
        Select-Object -First 1

    if (-not $release) {
        throw "Unable to determine the requested Node.js release for '$($Definition.commandName)'."
    }

    $tagName = $release.version
    $fileName = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.assetNamePattern -Version $tagName -TagName $tagName -Flavor $Flavor
    $baseUrl = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.baseUrlPattern -Version $tagName -TagName $tagName -Flavor $Flavor

    return [pscustomobject]@{
        TagName     = $tagName
        Version     = $(if ($tagName) { ConvertTo-ManifestedVersionTextFromRule -VersionText $tagName -Rule $versionSpec.ReleaseVersionRule } else { $null })
        Flavor      = $Flavor
        FileName    = $fileName
        Path        = $null
        Source      = 'online'
        Action      = 'SelectedOnline'
        NpmVersion  = $release.npm
        DownloadUrl = ($baseUrl.TrimEnd('/') + '/' + $fileName)
        ShasumsUrl  = ($baseUrl.TrimEnd('/') + '/' + $supplyBlock.shasumsAssetName)
    }
}

function Get-ManifestedOnlineVSCodeArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-ManifestedDefinitionFlavor -Definition $Definition
    }

    $versionSpec = Get-ManifestedVersionSpec -Definition $Definition
    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'vsCodeUpdate'
    if (-not $supplyBlock) {
        return $null
    }

    $channel = if ($supplyBlock.PSObject.Properties.Match('channel').Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($supplyBlock.channel)) { $supplyBlock.channel } else { 'stable' }
    $updateTarget = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.updateTargetPattern -Flavor $Flavor
    $latestUri = Expand-ManifestedDefinitionTemplate -Template $supplyBlock.latestUrlPattern -Flavor $Flavor -Version $channel -TagName $channel
    $latestUri = $latestUri.Replace('{updateTarget}', $updateTarget).Replace('{channel}', $channel)
    $headResult = Invoke-ManifestedVSCodeHeadRequest -Uri $latestUri

    if ($headResult.StatusCode -notin @(301, 302, 303, 307, 308)) {
        throw "Unexpected VS Code update response status code $($headResult.StatusCode)."
    }
    if ([string]::IsNullOrWhiteSpace($headResult.Location)) {
        throw 'The VS Code update service did not return a redirect location.'
    }
    if ([string]::IsNullOrWhiteSpace($headResult.Sha256)) {
        throw 'The VS Code update service did not return an X-SHA256 header.'
    }

    $resolvedUri = [uri]$headResult.Location
    $fileName = Split-Path -Leaf $resolvedUri.AbsolutePath
    $match = [regex]::Match($fileName, (Expand-ManifestedDefinitionTemplate -Template $supplyBlock.fileNamePattern -Flavor ([regex]::Escape($Flavor))))
    if (-not $match.Success) {
        throw "Could not parse the VS Code archive name '$fileName'."
    }

    return [pscustomobject]@{
        TagName     = $channel
        Version     = $(ConvertTo-ManifestedVersionTextFromRule -VersionText $match.Groups[1].Value -Rule $versionSpec.ReleaseVersionRule)
        Flavor      = $Flavor
        Channel     = $channel
        FileName    = $fileName
        Path        = $null
        Source      = 'online'
        Action      = 'SelectedOnline'
        DownloadUrl = $resolvedUri.AbsoluteUri
        Sha256      = $headResult.Sha256
        ShaSource   = 'X-SHA256'
        ReleaseUrl  = $latestUri
    }
}

function Get-ManifestedOnlinePythonEmbedArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$Flavor
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-ManifestedDefinitionFlavor -Definition $Definition
    }

    $versionSpec = Get-ManifestedVersionSpec -Definition $Definition
    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'pythonEmbed'
    if (-not $supplyBlock) {
        return $null
    }

    foreach ($candidate in @(Get-PythonReleaseCandidates)) {
        try {
            $assetDetails = Get-PythonReleaseAssetDetails -ReleaseUrl $candidate.ReleaseUrl -Flavor $Flavor
            return [pscustomobject]@{
                ReleaseId   = $candidate.ReleaseId
                Version     = $(ConvertTo-ManifestedVersionTextFromRule -VersionText $candidate.Version -Rule $versionSpec.ReleaseVersionRule)
                Flavor      = $Flavor
                FileName    = $assetDetails.FileName
                Path        = $null
                Source      = 'online'
                Action      = 'SelectedOnline'
                DownloadUrl = $assetDetails.DownloadUrl
                Sha256      = $assetDetails.Sha256
                ShaSource   = $assetDetails.ShaSource
                ReleaseUrl  = $candidate.ReleaseUrl
                ReleaseDate = $candidate.ReleaseDate
            }
        }
        catch {
            continue
        }
    }

    throw "Unable to determine the requested Python embeddable release for '$($Definition.commandName)'."
}

function Get-ManifestedDirectDownloadArtifactFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition
    )

    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'directDownload'
    if (-not $supplyBlock) {
        return $null
    }

    return [pscustomobject]@{
        Version     = if ($supplyBlock.PSObject.Properties.Match('version').Count -gt 0) { $supplyBlock.version } else { $null }
        FileName    = $supplyBlock.fileName
        Path        = $null
        Source      = 'online'
        Action      = 'SelectedOnline'
        DownloadUrl = $supplyBlock.downloadUrl
    }
}