Private/Get-FoPipelineExecutableInventory.ps1

function Get-FoPipelineInventorySettings {
    [CmdletBinding()]
    param(
        [ValidateSet('32', '64')]
        [string]$Architecture = '64'
    )

    $s = Get-FoModuleDefaults
    $s.Level = 9
    $s.PNGAllowLossy = $true
    $s.JPEGAllowLossy = $true
    $s.JPEGCopyMetadata = $false
    $s.GIFAllowLossy = $true
    $s.WEBPAllowLossy = $true
    $s.EXEEnableUPX = $true
    $s.EXEDisablePETrim = $false
    $s.HTMLEnableTidy = $true
    $s.CSSEnableTidy = $true
    $s.JSEnableJSMin = $true
    $s.XMLEnableLeanify = $true
    $s.LUAEnableLeanify = $true
    $s.MiscDisable = $false
    $s.PDFSkipLayered = $false
    $s.PDFProfile = 'none'
    $s.ZIPRecurse = $true
    $s.GZCopyMetadata = $false
    $s.ZIPCopyMetadata = $false

    if ($script:FoModuleRoot) {
        $folder = if ($Architecture -eq '64') { 'Plugins64' } else { 'Plugins32' }
        $s.PluginPath = Join-Path $script:FoModuleRoot $folder
    }

    return $s
}

function Get-FoPipelineGroupPrimaryExtensions {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    if ($script:FoPipelineGroupPrimaryExtensions) {
        return $script:FoPipelineGroupPrimaryExtensions
    }

    # Prefer an extension that uniquely maps to the group. Shared mappings (e.g. .ico →
    # ICO + PNG) skip PNG-only steps such as TruePNG/PNGOut when used as the inventory
    # sample, which would omit those tools from Install-FoPlugins FullPortable.
    $extsByGroup = @{}
    $map = Get-FoExtensionMap
    foreach ($ext in $map.Keys) {
        foreach ($group in @($map[$ext])) {
            if (-not $extsByGroup.ContainsKey($group)) {
                $extsByGroup[$group] = [System.Collections.Generic.List[string]]::new()
            }
            $extsByGroup[$group].Add($ext)
        }
    }

    $byGroup = @{}
    foreach ($group in $extsByGroup.Keys) {
        $exts = @($extsByGroup[$group])
        $unique = @(
            $exts | Where-Object { @($map[$_]).Count -eq 1 } | Sort-Object
        )
        $candidates = if ($unique.Count -gt 0) { $unique } else { @($exts | Sort-Object) }

        $canonical = '.' + $group.ToLowerInvariant()
        if ($candidates -contains $canonical) {
            $byGroup[$group] = $canonical
        }
        else {
            $byGroup[$group] = $candidates[0]
        }
    }

    $script:FoPipelineGroupPrimaryExtensions = $byGroup
    return $byGroup
}

function New-FoPipelineInventoryContext {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [string]$Extension = '.bin',
        [hashtable]$Flags = @{},
        [ValidateSet('32', '64')]
        [string]$Architecture = '64'
    )

    $settings = Get-FoPipelineInventorySettings -Architecture $Architecture
    $ctx = @{
        InputFile     = "C:\FoInventory\sample$Extension"
        Extension     = $Extension
        Settings      = $settings
        IsAPNG        = $false
        IsPNG9Patch   = $false
        IsZipSFX      = $false
        IsEXESFX      = $false
        IsJPEGCMYK    = $false
        IsPDFLayered  = $false
    }

    foreach ($key in $Flags.Keys) {
        $ctx[$key] = $Flags[$key]
    }

    if ($Flags.ContainsKey('Extension')) {
        $ctx.Extension = $Flags.Extension
        $ctx.InputFile = "C:\FoInventory\sample$($Flags.Extension)"
    }

    return $ctx
}

function Get-FoPipelineInventoryFlagVariants {
    [CmdletBinding()]
    [OutputType([object[]])]
    param()

    # Inventory must exercise every context flag path that conditionally adds
    # plugin steps. Keep this list in sync when adding pipelines with new
    # `if ($Context.Flag)` step additions:
    # - default: normal/lossy-enabled settings for most pipelines
    # - IsAPNG: PNG APNG-only `apngopt.exe`
    # - IsPNG9Patch: PNG 9-patch-safe branch coverage
    # - IsPDFLayered: layered-PDF gates and warning-safe enumeration
    # - IsJPEGCMYK: JPEG CMYK branch coverage
    return @(
        @{}
        @{ IsAPNG = $true }
        @{ IsPNG9Patch = $true }
        @{ IsPDFLayered = $true }
        @{ IsJPEGCMYK = $true }
    )
}

function Get-FoPipelineDeclaredExecutables {
    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [ValidateSet('32', '64')]
        [string]$Architecture = $(if ([Environment]::Is64BitProcess) { '64' } else { '32' })
    )

    $exes = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    $groupExtensions = Get-FoPipelineGroupPrimaryExtensions
    $flagVariants = Get-FoPipelineInventoryFlagVariants

    $pipelineCommands = @(Get-Command -Name 'Get-Fo*Pipeline' -CommandType Function -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match '^Get-Fo.+Pipeline$' })

    foreach ($cmd in $pipelineCommands) {
        if ($cmd.Name -notmatch '^Get-Fo(.+)Pipeline$') { continue }
        $group = $Matches[1]
        $ext = if ($groupExtensions.ContainsKey($group)) { $groupExtensions[$group] } else { '.bin' }

        foreach ($flags in $flagVariants) {
            $ctx = New-FoPipelineInventoryContext -Extension $ext -Flags $flags -Architecture $Architecture
            try {
                $steps = @(& $cmd.Name $ctx)
            }
            catch {
                Write-Warning "Pipeline inventory skipped $($cmd.Name): $($_.Exception.Message)"
                Write-Verbose "Pipeline inventory skipped $($cmd.Name) for variant: $($_.Exception.Message)"
                continue
            }

            foreach ($step in $steps) {
                foreach ($exe in (Get-FoStepRequiredExecutables -Step $step)) {
                    if ($exe) { [void]$exes.Add($exe) }
                }
            }
        }
    }

    return @($exes | Sort-Object)
}