private/Get-OSDeployCoreESD.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function Get-OSDeployCoreESD {
    <#
    .SYNOPSIS
        Gets verified ESD files from the OSDeployCore cache
 
    .DESCRIPTION
        Selects the newest recognized XML catalog by parsed build and release timestamp
        from the module operating systems directory, resolves its Windows release folder, and checks
        the corresponding cached en-US Enterprise ESD files for x64 and ARM64, optionally
        limited to one architecture. Each file's SHA256 hash must match its catalog entry.
        Missing catalog entries, files, malformed catalog names, and checksum mismatches
        produce warnings and are excluded. The function does not download, delete, or modify
        ESD files.
 
    .PARAMETER Architecture
        Limits cache verification to amd64 or arm64. When omitted, verifies both architectures.
 
    .EXAMPLE
        PS> Get-OSDeployCoreESD
 
        Returns cached en-US Enterprise ESD files whose SHA256 hashes match the newest
        module catalog.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.IO.FileInfo. Returns zero, one, or two verified ESD files, with at most one
        file for each of the x64 and ARM64 targets.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Run Update-OSDeployCoreESD to download missing or outdated ESD files.
 
        Dependencies:
          DotNet Classes: System.Collections.Generic.List[System.IO.FileInfo],
                          System.IO.FileInfo, System.IO.Path
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo[]])]
    param (
        [Parameter()]
        [ValidateSet('amd64', 'arm64')]
        [System.String]
        $Architecture
    )

    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Start"

    # -------------------------------------------------------------------------
    # Resolve catalog files (latest first)
    # -------------------------------------------------------------------------
    $catalogDir = Join-Path $script:OSDeployModuleBase 'core\operatingsystems'
    $catalogRecords = foreach ($catalogFile in Get-ChildItem -Path $catalogDir -Filter '26200*.xml' -File) {
        try {
            $catalogMetadata = Get-OSDeployCatalogMetadata -Name $catalogFile.Name
        }
        catch {
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Ignoring unrecognized catalog filename: $($catalogFile.FullName)"
            continue
        }

        [pscustomobject]@{
            Build = $catalogMetadata.Build
            ReleaseDateTime = $catalogMetadata.ReleaseDateTime
            OSFolderName = $catalogMetadata.OSFolderName
            File = $catalogFile
        }
    }
    $latestCatalogRecord = $catalogRecords |
        Sort-Object @{ Expression = 'Build'; Descending = $true },
                    @{ Expression = 'ReleaseDateTime'; Descending = $true } |
        Select-Object -First 1
    $latestXml = $latestCatalogRecord.File

    if (-not $latestXml) {
        Write-Warning "[$(Get-Date -Format s)] No recognized OS catalog XML files found in '$catalogDir'."
        return
    }

    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Using catalog: $($latestXml.Name)"
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Importing OSDeploy PSModule OperatingSystem Catalog"
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $($latestXml.FullName)"

    # -------------------------------------------------------------------------
    # Parse the catalog
    # -------------------------------------------------------------------------
    [xml]$catalog = Get-Content -Path $latestXml.FullName -Raw
    $allFiles     = $catalog.MCT.Catalogs.Catalog.PublishedMedia.Files.File

    # -------------------------------------------------------------------------
    # Resolve the Windows release folder from the parsed catalog build.
    # -------------------------------------------------------------------------
    $osFolderName = $latestCatalogRecord.OSFolderName
    Write-Verbose "[$($MyInvocation.MyCommand.Name)] OS folder name: $osFolderName"

    # -------------------------------------------------------------------------
    # Locate the download directory
    # -------------------------------------------------------------------------
    $downloadDir = Join-Path $script:OSDeployCorePath 'OSDCloud' 'OS' $osFolderName
    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Download directory: $downloadDir"

    # -------------------------------------------------------------------------
    # Target the requested architecture, or both when no filter is specified.
    # -------------------------------------------------------------------------
    $targets = switch ($Architecture) {
        'amd64' { [pscustomobject]@{ Architecture = 'x64';   Edition = 'Enterprise'; LanguageCode = 'en-us' } }
        'arm64' { [pscustomobject]@{ Architecture = 'ARM64'; Edition = 'Enterprise'; LanguageCode = 'en-us' } }
        default {
            [pscustomobject]@{ Architecture = 'x64';   Edition = 'Enterprise'; LanguageCode = 'en-us' }
            [pscustomobject]@{ Architecture = 'ARM64'; Edition = 'Enterprise'; LanguageCode = 'en-us' }
        }
    }

    $normalizeHash = { param([string]$hash) ($hash -replace '\s+', '').ToUpperInvariant() }
    $results       = [System.Collections.Generic.List[System.IO.FileInfo]]::new()

    foreach ($target in $targets) {
        $entry = $allFiles | Where-Object {
            $_.LanguageCode -eq $target.LanguageCode -and
            $_.Edition      -eq $target.Edition      -and
            $_.Architecture -eq $target.Architecture
        } | Select-Object -First 1

        if (-not $entry) {
            Write-Warning "[$(Get-Date -Format s)] No catalog entry found for $($target.Edition) $($target.Architecture) $($target.LanguageCode). Skipping."
            continue
        }

        $destPath = Join-Path $downloadDir $entry.FileName
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Checking: $($entry.FileName)"
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Verifying OSDeployCoreESD for Windows 11 $($target.Edition) $($target.LanguageCode) $($target.Architecture)"
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] FileName: $($entry.FileName)"

        if (-not (Test-Path -Path $destPath -PathType Leaf)) {
            Write-Warning "[$(Get-Date -Format s)] ESD not found in cache: $destPath"
            Write-Warning "[$(Get-Date -Format s)] Run Update-OSDeployCoreESD to download it."
            continue
        }

        $expectedSha256 = & $normalizeHash $entry.Sha256
        $actualHash     = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash.ToUpperInvariant()

        if ($actualHash -ne $expectedSha256) {
            Write-Warning "[$(Get-Date -Format s)] SHA256 mismatch for '$($entry.FileName)'."
            Write-Warning "[$(Get-Date -Format s)] Expected : $expectedSha256"
            Write-Warning "[$(Get-Date -Format s)] Actual : $actualHash"
            Write-Warning "[$(Get-Date -Format s)] Run Update-OSDeployCoreESD to re-download it."
            continue
        }

        Write-Verbose "[$($MyInvocation.MyCommand.Name)] SHA256 verified: $($entry.FileName)"
        # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] ESD cached and verified: $($entry.FileName)"
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hash SHA256: $actualHash"
        $results.Add((Get-Item -Path $destPath))
    }

    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Done. $($results.Count) file(s) verified."

    return $results.ToArray()
}