private/core-operatingsystem/Get-ModuleCoreOperatingSystems.ps1

function Get-ModuleCoreOperatingSystems {
    <#
    .SYNOPSIS
    Gets normalized module core operating system catalog records.
 
    .DESCRIPTION
    Reads operating system catalog XML files from core\operatingsystems under the module root,
    converts each PublishedMedia file node into a PowerShell object, removes excluded metadata
    properties, and normalizes duplicate properties. Duplicate catalog rows are grouped by
    FilePath, FileName, LanguageCode, and Architecture, then the preferred row is selected
    by hash availability.
 
    .EXAMPLE
    Get-ModuleCoreOperatingSystems
 
    Returns all normalized catalog records discovered in the module core operating systems cache.
 
    .EXAMPLE
    Get-ModuleCoreOperatingSystems | Where-Object { $_.LanguageCode -eq 'en-us' }
 
    Returns only catalog records for en-us language media.
 
    .INPUTS
    None
    You cannot pipe input to this function.
 
    .OUTPUTS
    PSCustomObject[]
    Normalized raw catalog records imported from module XML metadata.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-22 - Initial help block created
    2026-08-05 - Expanded help content and examples
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param ()

    $ErrorActionPreference = 'Stop'
    $records = @()
    $mctRecords = @()

    $srcRoot = Join-Path $($MyInvocation.MyCommand.Module.ModuleBase) 'core\operatingsystems'

    foreach ($file in (Get-ChildItem -Path $srcRoot -Filter '*.xml' -Recurse -File | Sort-Object FullName)) {
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Importing $($file.FullName)"

        $xml = [xml](Get-Content -Path $file.FullName -Raw)
        $fileNodes = $xml.MCT.Catalogs.Catalog.PublishedMedia.Files.File

        if (-not $fileNodes) {
            continue
        }

        foreach ($node in ($fileNodes | Sort-Object FileName, LanguageCode, Edition)) {
            $properties = [ordered]@{
                Sha1   = $null
                Sha256 = $null
            }

            $excludedProperties = @('Edition', 'Key', 'Architecture_Loc', 'ArchitectureLoc', 'Edition_Loc', 'EditionLoc', 'IsRetailOnly')

            foreach ($child in $node.ChildNodes) {
                if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) {
                    continue
                }

                $name = $child.LocalName
                $value = $child.InnerText

                if ($name -match '^Sha1$') {
                    $name = 'Sha1'
                }
                elseif ($name -match '^Sha256$') {
                    $name = 'Sha256'
                }

                if ($excludedProperties -contains $name) {
                    continue
                }

                if ($properties.Contains($name)) {
                    if ($name -in @('Sha1', 'Sha256') -or [string]::IsNullOrWhiteSpace($properties[$name])) {
                        $properties[$name] = $value
                    }
                    else {
                        $suffix = 2
                        while ($properties.Contains("$name$suffix")) {
                            $suffix++
                        }
                        $properties["$name$suffix"] = $value
                    }
                }
                else {
                    $properties[$name] = $value
                }
            }

            $mctRecords += [pscustomobject]$properties
        }
    }

    $mctRecords = $mctRecords |
        Group-Object -Property FilePath, FileName, LanguageCode, Architecture |
        ForEach-Object {
            $_.Group |
                Sort-Object -Property @{ Expression = { [string]::IsNullOrWhiteSpace($_.Sha256) }; Ascending = $true }, @{ Expression = { [string]::IsNullOrWhiteSpace($_.Sha1) }; Ascending = $true } |
                Select-Object -First 1
        } |
        Sort-Object -Property FilePath, FileName, LanguageCode, Architecture

    if (-not $mctRecords) {
        return $records
    }

    return $mctRecords
}