private/Test-OSDeployWinPEStartupProfile.ps1

#Requires -PSEdition Core

function Test-OSDeployWinPEStartupProfile {
    <#
    .SYNOPSIS
        Validates a WinPEStartup profile JSON file
 
    .DESCRIPTION
        Parses one WinPEStartup profile and verifies that it contains one non-empty JSON
        object using only supported properties and value types. Stored commands are not
        executed. Invalid content produces a terminating error that identifies the file.
 
    .PARAMETER Path
        Specifies the WinPEStartup profile JSON file to validate.
 
    .EXAMPLE
        PS> Test-OSDeployWinPEStartupProfile -Path 'C:\ProgramData\OSDeployCore\boot-assets\winpestartup-profiles\Deploy.json'
 
        Validates the selected WinPEStartup profile without executing its commands.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-09-11
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.String]
        $Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "WinPEStartup profile was not found: $Path"
    }

    try {
        $Profile = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop |
            ConvertFrom-Json -NoEnumerate -ErrorAction Stop
    }
    catch {
        throw "Invalid WinPEStartup profile JSON '$Path': $($_.Exception.Message) Backslashes in JSON strings must be escaped as '\\'."
    }

    $BooleanKeys = @(
        'Invoke-WinPEStartup:SkipOnScreenKeyboard'
        'Invoke-WinPEStartup:ShowPnpDevices'
        'Invoke-WinPEStartup:ShowPnpErrors'
        'Invoke-WinPEStartup:SkipWiFi'
        'Invoke-WinPEStartup:SkipIPConfig'
        'Invoke-WinPEStartup:SkipUpdateOSDCloud'
        'Invoke-WinPEStartup:InvokeStartupCommandNoExit'
        'Invoke-WinPEStartup:InvokeMainCommandNoExit'
        'Invoke-WinPEStartup:InvokeShutdownCommandNoExit'
    )
    $ArrayKeys = @(
        'Invoke-WinPEStartup:InstallModule'
        'Invoke-WinPEStartup:InvokeStartupCommand'
        'Invoke-WinPEStartup:InvokeMainCommand'
        'Invoke-WinPEStartup:InvokeShutdownCommand'
    )
    $ErrorActionKeys = @(
        'Invoke-WinPEStartup:InvokeStartupCommandEA'
        'Invoke-WinPEStartup:InvokeMainCommandEA'
        'Invoke-WinPEStartup:InvokeShutdownCommandEA'
    )
    $SupportedKeys = @($BooleanKeys + $ArrayKeys + $ErrorActionKeys)

    if ($Profile -isnot [System.Management.Automation.PSCustomObject] -or $Profile -is [System.Array]) {
        throw "Invalid WinPEStartup profile '$Path': The profile must contain one JSON object."
    }
    if (@($Profile.PSObject.Properties).Count -eq 0) {
        throw "Invalid WinPEStartup profile '$Path': The profile must contain at least one explicit setting."
    }

    foreach ($Property in $Profile.PSObject.Properties) {
        if ($Property.Name -notin $SupportedKeys) {
            throw "Invalid WinPEStartup profile '$Path': Unsupported property '$($Property.Name)'."
        }
        if ($Property.Name -in $BooleanKeys -and $Property.Value -isnot [System.Boolean]) {
            throw "Invalid WinPEStartup profile '$Path': '$($Property.Name)' must be a JSON boolean."
        }
        if ($Property.Name -in $ArrayKeys) {
            if ($Property.Value -isnot [System.Array]) {
                throw "Invalid WinPEStartup profile '$Path': '$($Property.Name)' must be a JSON array."
            }
            foreach ($Item in $Property.Value) {
                if ($Item -isnot [System.String] -or [System.String]::IsNullOrWhiteSpace($Item)) {
                    throw "Invalid WinPEStartup profile '$Path': '$($Property.Name)' must contain only non-empty strings."
                }
            }
        }
        if ($Property.Name -in $ErrorActionKeys -and $Property.Value -notin @('Continue', 'Stop')) {
            throw "Invalid WinPEStartup profile '$Path': '$($Property.Name)' must be 'Continue' or 'Stop'."
        }
    }
}