MSIX.Limitations.ps1

# =============================================================================
# MSIX Limitations knowledge base
# -----------------------------------------------------------------------------
# Curated list of what MSIX cannot do (or where it requires PSF / app workarounds).
# Sourced from current Microsoft Learn documentation; vendor-specific opinions
# are tagged separately so you can filter them out.
#
# References (all checked against current MS Learn — date stamped per item):
# - https://learn.microsoft.com/windows/msix/desktop/desktop-to-uwp-known-issues
# - https://learn.microsoft.com/windows/msix/packaging-tool/know-your-installer
# - https://learn.microsoft.com/windows/msix/packaging-tool/convert-an-installer-with-services
# - https://learn.microsoft.com/windows/msix/desktop/desktop-to-uwp-behind-the-scenes
# =============================================================================

$script:MsixLimitations = @(
    @{
        Id          = 'no-drivers'
        Title       = 'Drivers are not supported'
        Source      = 'msft-docs'
        Severity    = 'blocker'
        Description = 'MSIX cannot install kernel-mode drivers, file-system filter drivers, or any signed-driver components. Apps that bundle their own drivers must split them into a separate non-MSIX installer.'
        Workaround  = 'Ship the driver via a separate signed MSI/INF and depend on it as an OS prereq.'
    },
    @{
        Id          = 'install-dir-readonly'
        Title       = 'Installation directory is read-only at runtime'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = 'The package install location under C:\Program Files\WindowsApps is mounted read-only. Apps that write log/config files alongside their executable will fail.'
        Workaround  = 'Preferred (Win11 25H2+): Set-MsixFileSystemWriteVirtualization. Alternative: Use PSF FileRedirectionFixup or MFRFixup, or change the app to write to %LocalAppData%.'
    },
    @{
        Id          = 'cwd-system32'
        Title       = 'Working directory defaults to System32'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = 'Packaged apps launch with CWD=C:\Windows\System32 (or SysWOW64), not their install folder. Apps that read companion files via relative paths break.'
        Workaround  = 'Set workingDirectory in PSF config.json (Add-MsixPsfV2 -WorkingDirectory).'
    },
    @{
        Id          = 'hklm-redirected'
        Title       = 'HKLM writes are redirected to a private hive'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = 'Writes to HKLM are diverted to an isolated binary file per package. Other apps cannot see them. Reads merge through the virtual registry.'
        Workaround  = 'For genuine per-machine state, ship a separate config script. Otherwise, RegLegacyFixups can grant write access to specific keys.'
    },
    @{
        Id          = 'appdata-private'
        Title       = 'AppData is private per package'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = '%AppData% is redirected to %LocalAppData%\Packages\<PFN>\LocalCache\Roaming. Two apps cannot share data via AppData unless they ship in the same package.'
        Workaround  = 'Use a known shared location (Documents, ProgramData with explicit ACL) for cross-app data.'
    },
    @{
        Id          = 'inproc-shellext'
        Title       = 'In-process shell extensions cannot be registered'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = 'Classic IContextMenu / drop handlers normally load in-process into explorer.exe and are blocked by MSIX. Use the desktop9 surrogate-server pattern for legacy COM, or desktop4 IExplorerCommand for new extensions.'
        Workaround  = 'Add-MsixLegacyContextMenu (Win11 21H2+) or Add-MsixFileExplorerContextMenu.'
    },
    @{
        Id          = 'service-elevation'
        Title       = 'Packages with services need admin elevation to install'
        Source      = 'msft-docs'
        Severity    = 'low'
        Description = 'Services are supported (since MPT 1.2019.1220.0) but require admin to install and per-machine deployment.'
        Workaround  = 'Deploy via Intune/SCCM with admin context.'
    },
    @{
        Id          = 'sxs-assemblies'
        Title       = 'WinSxS shared assemblies cannot be loaded'
        Source      = 'msft-docs'
        Severity    = 'medium'
        Description = 'Apps dynamically linking to C:\Windows\WinSxS DLLs (older C runtimes, etc.) will not find them inside the package.'
        Workaround  = 'Statically link the redistributable, or ship the DLLs alongside the executable inside the package.'
    },
    @{
        Id          = 'shortcut-args'
        Title       = 'Start-menu shortcuts cannot pass arguments natively'
        Source      = 'mixed'
        Severity    = 'medium'
        Description = 'The MSIX-installed shortcut points at the Application entry, with no native way to inject command-line arguments.'
        Workaround  = "PSF arguments field (New-MsixPsfArgument + Add-MsixPsfV2)."
    },
    @{
        Id          = 'multi-pkg-fileassoc'
        Title       = 'Multiple installed packages cannot register the same file extension'
        Source      = 'msft-docs'
        Severity    = 'low'
        Description = 'File-type associations are exclusive per family. Last-write-wins or both apps register but only one is the default handler.'
        Workaround  = 'Ensure only the intended package owns the extension.'
    },
    @{
        Id          = 'dotnet-pre-462'
        Title       = '.NET Framework pre-4.6.2 requires extra validation'
        Source      = 'msft-docs'
        Severity    = 'low'
        Description = 'Apps targeting .NET 2.0/3.5 generally work but may show performance issues; .NET 3.5 feature must be installed on the target machine.'
        Workaround  = 'Retarget to 4.6.2+ where possible.'
    },
    @{
        Id          = 'com-discovery'
        Title       = 'External processes may not see in-package COM servers'
        Source      = 'mixed'
        Severity    = 'medium'
        Description = 'COM servers registered via the package manifest are visible inside the container, but classic out-of-process discovery from non-packaged callers can fail without explicit OutOfProcessServer + RuntimeBehavior tuning.'
        Workaround  = 'Use windows.comServer extension with appropriate OutOfProcessServer config; expose only intended classes.'
    },
    @{
        Id          = 'no-windows-services-deps'
        Title       = 'Cannot depend on services that live outside the package'
        Source      = 'msft-docs'
        Severity    = 'low'
        Description = 'Service dependencies must resolve to services included in the package; cross-package service dependencies are not supported.'
        Workaround  = 'Bundle the dependent service in the same package, or run it as a separate non-MSIX install.'
    },
    @{
        Id          = 'protocol-handler-private'
        Title       = 'Custom URL/protocol handlers are scoped to the package'
        Source      = 'mixed'
        Severity    = 'low'
        Description = 'A protocol handler registered by the manifest is visible to the OS but the launching app/browser must support packaged-app activation. Some legacy callers do not.'
        Workaround  = 'Test from edge/non-packaged callers; some require the handler to be registered for both URL and FileType activation.'
    },
    @{
        Id          = 'signing-publisher-mismatch'
        Title       = 'Manifest Publisher must match the signing certificate Subject'
        Source      = 'msft-docs'
        Severity    = 'low'
        Description = 'signtool fails with 0x8007000B if the AppxManifest Publisher and the cert Subject differ exactly (case-sensitive, including spaces).'
        Workaround  = 'Update-MsixSigner -Publisher … (this module already handles re-stamping the publisher).'
    }
)


function Get-MsixLimitation {
    <#
    .SYNOPSIS
        Lists known MSIX limitations and their workarounds.
 
    .DESCRIPTION
        Returns entries from the module's curated MSIX-limitation knowledge
        base. Each entry describes a documented scenario where the MSIX
        runtime cannot host an application as-is, along with the recommended
        workaround (PSF fixup, manifest extension, or out-of-band install).
 
        Most entries are sourced directly from Microsoft Learn and tagged
        'msft-docs'; a smaller set comes from community/vendor practice and
        is tagged 'mixed'. Use -ExcludeVendor to limit the output to the
        documented-by-Microsoft subset.
 
    .PARAMETER Id
        Filter to one limitation by id.
 
    .PARAMETER Severity
        Filter by severity: blocker, medium, low.
 
    .PARAMETER ExcludeVendor
        Exclude entries where Source != 'msft-docs' (i.e. drop vendor-flavoured
        items that are not directly documented by Microsoft).
 
    .OUTPUTS
        [pscustomobject] one per limitation with properties Id, Title, Source,
        Severity, Description, Workaround.
 
    .EXAMPLE
        Get-MsixLimitation -Severity blocker
 
    .EXAMPLE
        Get-MsixLimitation -ExcludeVendor | Format-Table Id, Severity, Title
 
    .EXAMPLE
        Get-MsixLimitation -Id 'install-dir-readonly' | Select-Object -ExpandProperty Workaround
    #>

    [CmdletBinding()]
    param(
        [string]$Id,
        [ValidateSet('blocker','medium','low')]
        [string]$Severity,
        [switch]$ExcludeVendor
    )

    $list = $script:MsixLimitations | ForEach-Object { [pscustomobject]$_ }

    if ($Id)            { $list = $list | Where-Object Id -eq $Id }
    if ($Severity)      { $list = $list | Where-Object Severity -eq $Severity }
    if ($ExcludeVendor) { $list = $list | Where-Object Source -eq 'msft-docs' }

    return $list
}


function Test-MsixAgainstLimitation {
    <#
    .SYNOPSIS
        Inspects an MSIX file and reports which documented limitations are
        likely to apply, based on heuristics on the manifest and unpacked
        content.
 
    .DESCRIPTION
        Unpacks the supplied .msix into a temporary workspace, parses
        AppxManifest.xml, and walks the Applications / Extensions tree to
        flag scenarios that are known to hit MSIX limitations (e.g. an
        executable nested under a subfolder triggers the CWD=System32 and
        install-dir-readonly limitations; a windows.service extension flags
        elevation requirements). Limitations that always apply to a packaged
        Win32 app (HKLM redirection, private AppData) are appended to the
        result for completeness.
 
        Complements Get-MsixStaticAnalysis. The workspace is removed
        afterwards.
 
    .PARAMETER PackagePath
        .msix file to analyse.
 
    .OUTPUTS
        [pscustomobject] one per matched limitation, deduplicated by Id. Same
        shape as Get-MsixLimitation.
 
    .EXAMPLE
        Test-MsixAgainstLimitation -PackagePath .\app.msix |
            Format-Table Id, Severity, Title
 
    .EXAMPLE
        # Merge with a full static-analysis run
        $hits = Test-MsixAgainstLimitation -PackagePath .\app.msix
        $static = Get-MsixStaticAnalysis -PackagePath .\app.msix
        $hits, $static.Findings
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$PackagePath
    )

    $toolsRoot = Get-MsixToolsRoot
    $fileinfo  = Get-Item $PackagePath
    $workspace = New-MsixWorkspace "$($fileinfo.BaseName)-limits"
    $hits = @()

    try {
        $r = Invoke-MsixProcess "$toolsRoot\Tools\MakeAppx.exe" -ArgumentList @('unpack', '/p', $fileinfo.FullName, '/d', $workspace, '/o')
        Assert-MsixProcessSuccess $r 'MakeAppx unpack'

        [xml]$manifest = Get-MsixManifest "$workspace\AppxManifest.xml"

        # cwd-system32 / install-dir-readonly: any executable in a subfolder + writable companions
        foreach ($app in @($manifest.Package.Applications.Application)) {
            if ($app.Executable -and $app.Executable.Contains('\')) {
                $hits += (Get-MsixLimitation -Id 'cwd-system32')
                $hits += (Get-MsixLimitation -Id 'install-dir-readonly')
                break
            }
        }

        # com-discovery: any com:Extension
        if ($manifest.Package.Extensions.Extension -or
            ($manifest.Package.Applications.Application.Extensions.Extension |
                Where-Object { $_.Category -eq 'windows.comServer' })) {
            $hits += (Get-MsixLimitation -Id 'com-discovery')
        }

        # no-windows-services-deps: any windows.service extension
        if ($manifest.Package.Applications.Application.Extensions.Extension |
            Where-Object { $_.Category -eq 'windows.service' }) {
            $hits += (Get-MsixLimitation -Id 'no-windows-services-deps')
            $hits += (Get-MsixLimitation -Id 'service-elevation')
        }

        # protocol handlers
        if ($manifest.Package.Applications.Application.Extensions.Extension |
            Where-Object { $_.Category -eq 'windows.protocol' }) {
            $hits += (Get-MsixLimitation -Id 'protocol-handler-private')
        }

        # multi-package file association
        if ($manifest.Package.Applications.Application.Extensions.Extension |
            Where-Object { $_.Category -eq 'windows.fileTypeAssociation' }) {
            $hits += (Get-MsixLimitation -Id 'multi-pkg-fileassoc')
        }

        # always applicable for any packaged Win32 app
        $hits += (Get-MsixLimitation -Id 'hklm-redirected')
        $hits += (Get-MsixLimitation -Id 'appdata-private')

        return $hits | Sort-Object Id -Unique

    } finally {
        Remove-Item $workspace -Recurse -Force -ErrorAction SilentlyContinue
    }
}


# Backward-compatible plural aliases
Set-Alias Get-MsixLimitations Get-MsixLimitation
Set-Alias Test-MsixAgainstLimitations Test-MsixAgainstLimitation