Public/Resolve-HDTImageIndex.ps1
|
function Resolve-HDTImageIndex { <# .SYNOPSIS Chooses the one image to apply, or refuses to choose. .DESCRIPTION Index selectable by number, name, or edition. This is that selection, and its refusal: TWO IMAGES MATCHING ONE REQUEST IS A REFUSAL, NOT A COIN TOSS. It is the same rule HDT makes about disks, applied to what gets applied to them - and it matters on real media, where the staged Server 2025 WIM carries two images whose names differ only by "(Desktop Experience)" and two more that share the edition id ServerStandard. EACH CRITERION IS MATCHED INDEPENDENTLY AND THE RESULTS ARE INTERSECTED. That is what makes -Index 2 -Edition ServerStandard work: the edition alone is ambiguous, the pair is not. A filter-in-sequence implementation would have had to decide which criterion narrows first, and would refuse a request that is perfectly unambiguous. EXACT BEFORE WILDCARD, for -Name. 'Windows Server 2025 Standard' is index 1 exactly AND is contained in index 2's name. A containment-first implementation refuses a request an administrator meant unambiguously. A -Name carrying * or ? is taken as a wildcard as written; one without is tried exactly first and as a containment match second. With nothing asked for: a single-image file resolves to that image, a declared -DefaultIndex is used next, and anything else is an HDTAmbiguousImageError listing every index and name - which is what an administrator needs to see to write the request that would have worked. HDTAmbiguousImageError is classified Configuration by Get-HDTFailureClass, so a refusal ends the run rather than being retried three times. .PARAMETER Image The image rows, as IImageService.GetImageInfo returns them or as Get-HDTOperatingSystem reads them back from os.yaml. .PARAMETER Index The index to apply. Must exist. .PARAMETER Name The image name, matched case-insensitively: exactly first, then as a wildcard. .PARAMETER Edition The edition id - EnterpriseS, ServerStandard - matched case-insensitively and exactly. .PARAMETER DefaultIndex The index to use when nothing was asked for. Must exist. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject - the single image row. .EXAMPLE Resolve-HDTImageIndex -Image $catalog.Images -Edition EnterpriseS The Windows 11 Enterprise LTSC index, without depending on the marketing name staying the same across media revisions. .EXAMPLE Resolve-HDTImageIndex -Image $catalog.Images -Name 'Windows Server 2025 Standard (Desktop Experience)' The index the Server 2025 media puts at 2, named rather than numbered. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyCollection()] [object[]] $Image, [Parameter()] [int] $Index, [Parameter()] [string] $Name, [Parameter()] [string] $Edition, [Parameter()] [int] $DefaultIndex ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $row = @($Image) if ($row.Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord ` -Message 'the image file declares no image, so there is no index to apply.')) } $presentIndex = @($row | ForEach-Object { [int] $_.Index }) $requested = New-Object -TypeName System.Collections.ArrayList # -- each criterion, matched independently -------------------------------- $candidate = $row if ($PSBoundParameters.ContainsKey('Index')) { [void] $requested.Add(('index {0}' -f $Index)) $match = @($row | Where-Object { [int] $_.Index -eq $Index }) if ($match.Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Index ` -Message ("index {0} names no image in this image file. The indices it carries are {1}." -f $Index, ($presentIndex -join ', ')))) } $candidate = @($candidate | Where-Object { [int] $_.Index -eq $Index }) } if (-not [string]::IsNullOrWhiteSpace($Name)) { [void] $requested.Add(("name '{0}'" -f $Name)) # Exact first. The LTSC / LTSC-N and Standard / Standard (Desktop # Experience) pairs on the real staged media are exactly this case. $match = @($row | Where-Object { ([string] $_.Name) -eq $Name }) if ($match.Count -eq 0) { $pattern = $Name if ($Name -notmatch '[\*\?]') { $pattern = '*{0}*' -f $Name } $match = @($row | Where-Object { ([string] $_.Name) -like $pattern }) } if ($match.Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name ` -Message ("no image in this image file is named '{0}'. The images it carries are: {1}." -f $Name, (@($row | ForEach-Object { '{0} = {1}' -f $_.Index, $_.Name }) -join '; ')))) } $matchIndex = @($match | ForEach-Object { [int] $_.Index }) $candidate = @($candidate | Where-Object { $matchIndex -contains [int] $_.Index }) } if (-not [string]::IsNullOrWhiteSpace($Edition)) { [void] $requested.Add(("edition '{0}'" -f $Edition)) $match = @($row | Where-Object { ([string] $_.Edition) -eq $Edition }) if ($match.Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Edition ` -Message ("no image in this image file carries the edition '{0}'. The editions it carries are {1}." -f $Edition, (@($row | ForEach-Object { [string] $_.Edition } | Sort-Object -Unique) -join ', ')))) } $matchIndex = @($match | ForEach-Object { [int] $_.Index }) $candidate = @($candidate | Where-Object { $matchIndex -contains [int] $_.Index }) } # -- nothing was asked for ------------------------------------------------ if ($requested.Count -eq 0) { if ($row.Count -eq 1) { return $row[0] } if ($PSBoundParameters.ContainsKey('DefaultIndex')) { $match = @($row | Where-Object { [int] $_.Index -eq $DefaultIndex }) if ($match.Count -eq 1) { return $match[0] } $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $DefaultIndex ` -Message ("the default index {0} names no image in this image file. The indices it carries are {1}." -f $DefaultIndex, ($presentIndex -join ', ')))) } $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTAmbiguousImageError' ` -TargetObject ([int[]] $presentIndex) -Category InvalidResult ` -Message ("this image file carries {0} images and the step named none of them, so HDT will not guess which to apply. The images are: {1}. Set index:, name: or edition: on the step." -f $row.Count, (@($row | ForEach-Object { '{0} = {1}' -f $_.Index, $_.Name }) -join '; ')))) } # -- the intersection ----------------------------------------------------- if ($candidate.Count -eq 1) { return $candidate[0] } if ($candidate.Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject (@($requested) -join ' and ') ` -Message ("no image in this image file matches {0} together. The images it carries are: {1}." -f (@($requested) -join ' and '), (@($row | ForEach-Object { '{0} = {1}' -f $_.Index, $_.Name }) -join '; ')))) } $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTAmbiguousImageError' ` -TargetObject ([int[]] @($candidate | ForEach-Object { [int] $_.Index })) -Category InvalidResult ` -Message ("{0} images match {1}, and HDT will not guess which to apply: {2}. Name an index, or a name that matches one image." -f $candidate.Count, (@($requested) -join ' and '), (@($candidate | ForEach-Object { '{0} = {1}' -f $_.Index, $_.Name }) -join '; ')))) } |