Private/Get-OSDAppCatalogPackages.ps1

function Get-OSDAppCatalogPackages {
    [CmdletBinding(DefaultParameterSetName = 'Uri')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Uri')]
        [uri]$CatalogUri,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [string]$CatalogPath,

        [string[]]$Name
    )

    if ($PSCmdlet.ParameterSetName -eq 'Uri') {
        $catalog = Get-OSDAppManifest -Uri $CatalogUri
        $catalogBaseUri = [uri]::new($CatalogUri, '.')
        $catalogRoot = $null
    }
    else {
        $resolvedCatalogPath = (Resolve-Path -LiteralPath $CatalogPath -ErrorAction Stop).Path
        $catalog = Get-OSDAppManifest -Path $resolvedCatalogPath
        $catalogBaseUri = $null
        $catalogRoot = Split-Path -Path $resolvedCatalogPath -Parent
    }

    if (-not ($catalog.PSObject.Properties.Name -contains 'Applications')) {
        throw 'Invalid OSD Apps catalog. Root catalog must contain Applications.'
    }

    $applications = @($catalog.Applications)
    $canonicalIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $aliasOwners = @{}

    # Application Ids and aliases become cache directory names. Validate the
    # complete identity graph before resolving a subset or reading manifests.
    foreach ($application in $applications) {
        $applicationId = [string]$application.Id
        if ([string]::IsNullOrWhiteSpace($applicationId) -or
            $applicationId -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$' -or
            $applicationId -match '\.\.') {
            throw "Invalid application Id '$applicationId' in repository catalog."
        }
        if (-not $canonicalIds.Add($applicationId)) {
            throw "Duplicate application Id '$applicationId' in repository catalog."
        }
    }

    foreach ($application in $applications) {
        $applicationId = [string]$application.Id
        $aliases = @(
            if ($application.PSObject.Properties.Name -contains 'Aliases') {
                @($application.Aliases)
            }
        )
        foreach ($aliasValue in $aliases) {
            $alias = ([string]$aliasValue).Trim()
            if ([string]::IsNullOrWhiteSpace($alias) -or
                $alias -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$' -or
                $alias -match '\.\.') {
                throw "Invalid alias '$alias' for '$applicationId' in repository catalog."
            }
            if ($canonicalIds.Contains($alias)) {
                throw "Alias '$alias' for '$applicationId' conflicts with a canonical application Id."
            }
            $aliasKey = $alias.ToLowerInvariant()
            if ($aliasOwners.ContainsKey($aliasKey)) {
                throw "Alias '$alias' is assigned to both '$($aliasOwners[$aliasKey])' and '$applicationId'."
            }
            $aliasOwners[$aliasKey] = $applicationId
        }
    }

    if ($Name) {
        $resolvedNames = @(
            foreach ($requestedName in $Name) {
                $requestedKey = ([string]$requestedName).ToLowerInvariant()
                if ($aliasOwners.ContainsKey($requestedKey)) {
                    [string]$aliasOwners[$requestedKey]
                }
                else {
                    [string]$requestedName
                }
            }
        )
        $applications = @($applications | Where-Object { $_.Id -in $resolvedNames })
        $missing = @($resolvedNames | Where-Object { $_ -notin @($applications.Id) })
        if ($missing.Count -gt 0) {
            throw "Requested application(s) not found in catalog: $($missing -join ', ')"
        }
    }

    $packages = [System.Collections.Generic.List[object]]::new()

    foreach ($application in $applications) {
        if (-not $application.Id) {
            throw 'Catalog application entry must contain Id.'
        }

        foreach ($packageRef in @($application.Packages)) {
            if (-not $packageRef.Architecture -or -not $packageRef.Manifest) {
                throw "Catalog package reference for '$($application.Id)' must contain Architecture and Manifest."
            }

            if ($PSCmdlet.ParameterSetName -eq 'Uri') {
                $packageManifestUri = [uri]::new($catalogBaseUri, ([string]$packageRef.Manifest).Replace('\','/'))
                $packageManifest = Get-OSDAppManifest -Uri $packageManifestUri
                $packageBaseUri = [uri]::new($packageManifestUri, '.')
            }
            else {
                $packageManifestPath = Join-Path $catalogRoot (([string]$packageRef.Manifest) -replace '/', [IO.Path]::DirectorySeparatorChar)
                $packageManifest = Get-OSDAppManifest -Path $packageManifestPath
                $packageRoot = Split-Path -Path $packageManifestPath -Parent
            }

            if ([string]$packageManifest.Id -ne [string]$application.Id) {
                throw "Package manifest Id '$($packageManifest.Id)' does not match catalog Id '$($application.Id)'."
            }

            if (([string]$packageManifest.Architecture).ToLowerInvariant() -ne ([string]$packageRef.Architecture).ToLowerInvariant()) {
                throw "Package manifest architecture '$($packageManifest.Architecture)' does not match catalog architecture '$($packageRef.Architecture)' for '$($application.Id)'."
            }

            if ([string]::IsNullOrWhiteSpace([string]$packageManifest.Version)) {
                throw "Package manifest for '$($application.Id)' must contain Version."
            }

            $packageType = if ($packageManifest.PSObject.Properties.Name -contains 'PackageType' -and
                -not [string]::IsNullOrWhiteSpace([string]$packageManifest.PackageType)) {
                [string]$packageManifest.PackageType
            }
            else {
                'Application'
            }
            if ($packageType -notin @('Application','Script')) {
                throw "Package '$($application.Id)' has unsupported PackageType '$packageType'. Expected Application or Script."
            }
            $packageType = if ($packageType -ieq 'Script') { 'Script' } else { 'Application' }

            if (-not $packageManifest.Archive -or
                [string]$packageManifest.Archive.Sha256 -notmatch '^[A-Fa-f0-9]{64}$') {
                throw "Package '$($application.Id)' has an invalid SHA-256 archive definition."
            }

            $fileName = if ($packageManifest.Archive.FileName) { [string]$packageManifest.Archive.FileName } else { 'Package.zip' }
            if ($fileName -ne 'Package.zip') {
                throw "Package '$($application.Id)' must use the fixed archive name Package.zip."
            }

            $archive = [ordered]@{
                FileName = $fileName
                Sha256   = [string]$packageManifest.Archive.Sha256
            }

            if ($PSCmdlet.ParameterSetName -eq 'Uri') {
                $archive.Uri = ([uri]::new($packageBaseUri, $fileName)).AbsoluteUri
            }
            else {
                $archive.SourcePath = Join-Path $packageRoot $fileName
            }

            $packages.Add([pscustomobject]@{
                Id           = [string]$packageManifest.Id
                Aliases      = @(
                    if ($application.PSObject.Properties.Name -contains 'Aliases') {
                        @($application.Aliases | ForEach-Object { ([string]$_).Trim() })
                    }
                )
                DisplayName  = if ($packageManifest.DisplayName) { [string]$packageManifest.DisplayName } else { [string]$packageManifest.Id }
                Vendor       = if ($packageManifest.Vendor) { ([string]$packageManifest.Vendor).Trim() } else { $null }
                Version      = [string]$packageManifest.Version
                Architecture = [string]$packageManifest.Architecture
                PackageType  = $packageType
                SuccessCodes = @($packageManifest.SuccessCodes)
                Archive      = [pscustomobject]$archive
            })
        }
    }

    [pscustomobject]@{
        SchemaVersion = $catalog.SchemaVersion
        GeneratedAt   = $catalog.GeneratedAt
        Packages      = @($packages)
    }
}