MSIX.Validation.ps1

function Test-MsixManifest {
    <#
    .SYNOPSIS
        Validates that an AppxManifest.xml is well-formed and contains the
        Identity + Application elements MSIX requires.
 
    .DESCRIPTION
        Parses the file with secure XML settings (DTD prohibited, entities
        capped) and asserts that Identity.Name, Identity.Publisher,
        Identity.Version, and at least one Applications/Application element
        are present. Throws on the first missing field; returns $true on
        success so the call site can use it in boolean contexts.
 
        See Get-Help Test-MsixPsfConfig for the matching PSF config check.
 
    .PARAMETER Path
        Absolute path to an AppxManifest.xml file. Typically the manifest
        inside an unpacked workspace.
 
    .OUTPUTS
        [bool] $true when validation succeeds; otherwise throws.
 
    .EXAMPLE
        Test-MsixManifest -Path 'C:\workspace\AppxManifest.xml'
    #>

    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) {
        throw "Manifest not found: $Path"
    }

    try {
        [xml]$xml = _MsixLoadXmlSecure -Path $Path
    } catch {
        throw "Manifest is not valid XML: $_"
    }

    foreach ($field in @('Name', 'Publisher', 'Version')) {
        if (-not $xml.Package.Identity.$field) {
            throw "Manifest missing Identity.$field"
        }
    }

    if (-not $xml.Package.Applications.Application) {
        throw "Manifest contains no Application elements"
    }

    Write-MsixLog Debug "Manifest OK: $Path"
    return $true
}

function Test-MsixPsfConfig {
    <#
    .SYNOPSIS
        Validates that a PSF config.json parses as JSON and contains the
        top-level applications and processes arrays PSFLauncher requires.
 
    .DESCRIPTION
        Add-MsixPsfV2 calls this after generating or merging config.json so
        a malformed config fails the pipeline before repack. Throws on the
        first missing structural element; returns $true on success.
 
    .PARAMETER Path
        Absolute path to a config.json file.
 
    .OUTPUTS
        [bool] $true when validation succeeds; otherwise throws.
 
    .EXAMPLE
        Test-MsixPsfConfig -Path 'C:\workspace\App\config.json'
    #>

    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) {
        throw "PSF config not found: $Path"
    }

    try {
        $json = Get-Content $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
    } catch {
        throw "PSF config is not valid JSON: $_"
    }

    if (-not $json.applications) {
        throw "PSF config missing 'applications' array"
    }
    if (-not $json.processes) {
        throw "PSF config missing 'processes' array"
    }

    Write-MsixLog Debug "PSF config OK: $Path"
    return $true
}

function Assert-MsixProcessSuccess {
    <#
    .SYNOPSIS
        Throws when an Invoke-MsixProcess result indicates failure, surfacing
        both stdout and stderr in the error message.
 
    .DESCRIPTION
        External tools like MakeAppx and signtool routinely split diagnostic
        output across stderr (one line) and stdout (the multi-line detail).
        This helper concatenates both streams so the operator sees the full
        failure, then throws when ExitCode is non-zero. No-op on success.
 
    .PARAMETER Result
        The object returned by Invoke-MsixProcess: ExitCode, StdOut, StdErr.
 
    .PARAMETER Operation
        Short label used in the thrown error (e.g. 'MakeAppx unpack').
 
    .EXAMPLE
        $r = Invoke-MsixProcess $makeAppx -ArgumentList @('unpack','/p',$pkg,'/d',$ws,'/o')
        Assert-MsixProcessSuccess $r 'MakeAppx unpack'
    #>

    param(
        [Parameter(Mandatory)]
        [pscustomobject]$Result,
        [string]$Operation = 'Process'
    )
    if ($Result.ExitCode -eq 0) { return }

    # Include BOTH streams in the error message when present. Tools like
    # MakeAppx and SignTool sometimes split the useful diagnostic across
    # stderr (one line) and stdout (the full multi-line failure detail);
    # picking only one drops information the operator needs to act on.
    $parts = @()
    if ($Result.StdErr) { $parts += "stderr: $($Result.StdErr.Trim())" }
    if ($Result.StdOut) { $parts += "stdout: $($Result.StdOut.Trim())" }
    if ($parts.Count -eq 0) { $parts += '(no output)' }
    throw "$Operation failed (exit $($Result.ExitCode)). $([string]::Join(' | ', $parts))"
}