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 XML catalog by file name from the module operating systems
        directory, derives the Windows release folder from that catalog name, 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'
    $latestXml  = Get-ChildItem -Path $catalogDir -Filter '*.xml' -File |
        Sort-Object Name -Descending |
        Select-Object -First 1

    if (-not $latestXml) {
        Write-Warning "[$(Get-Date -Format s)] No 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

    # -------------------------------------------------------------------------
    # Derive OS folder name from the catalog filename
    # e.g. '26200.8457-win11-25h2.xml' → 'Windows 11 25H2'
    # -------------------------------------------------------------------------
    $catalogBase = [System.IO.Path]::GetFileNameWithoutExtension($latestXml.Name)
    if ($catalogBase -notmatch '^\d+\.\d+-win(\d+)-(.+)$') {
        Write-Warning "[$(Get-Date -Format s)] Cannot parse OS version from catalog name '$($latestXml.Name)'. Expected format: '<build>-win<version>-<release>.xml' (e.g. '26200.8457-win11-25h2.xml')."
        return
    }
    $osFolderName = "Windows $($Matches[1]) $($Matches[2].ToUpper())"
    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()
}