Public/Add-MSIXPSFDefaultFRF.ps1

function Add-MSIXPSFDefaultFRF {
<#
.SYNOPSIS
    Adds a standard FileRedirectionFixup configuration to an MSIX package.

.DESCRIPTION
    Applies a comprehensive default FileRedirectionFixup rule set by calling
    Add-MSIXPSFFileRedirectionFixup with the following configuration:

    packageRelative exclusions (not redirected):
      - Executable types: .exe .dll .tlb .ocx .com .fon .ttc .ttf .zip*
      - VFS\FONTS folder
    packageRelative catch-all (redirected):
      - Everything else under the package root
    knownFolders (redirected):
      - ProgramFilesX64, SystemX86, System (only when -SystemFolders is true)

.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder (must contain config.json.xml).

.PARAMETER Executable
    Regex pattern for the process entry. Default: ".*" (all processes).

.PARAMETER SystemFolders
    When $true, redirects ProgramFilesX64, SystemX86 and System known folders.
    Default is $false because the packageRelative catch-all already covers
    system writes indirectly via MSIX VFS-mapping (writes to C:\Windows\System32
    are normalised to <pkg>\VFS\SystemX64\... by the runtime, then matched by
    the catch-all). Explicit knownFolder rules are mostly redundant for the
    default whole-package scope.

    However, when -PackageRelativeBase is set to a subfolder, the catch-all
    no longer covers system paths via VFS-mapping. In that case you usually
    want -SystemFolders:`$true to add explicit knownFolder rules - a warning
    is issued at runtime if you forget.

.PARAMETER PackageRelativeBase
    Optional base path inside the package for the binary-type-exclusion rule
    and the catch-all redirection rule. Default '' = applies to the whole
    package root (legacy behaviour). Set to a subfolder like
    'VFS\ProgramFilesX64\NickIT' to scope the catch-all to that app folder.
    Useful for isolating which writes really need redirection (lab tests of
    the VFS-mapping hypothesis). The FONTS exclusion and known-folder rules
    are NOT affected by this parameter.

.PARAMETER ExcludePersonalFolder
    When $true, adds isExclusion rules for the user's personal data folders
    so writes there bypass FRF and land in the real %USERPROFILE% instead of
    the package's writable container. Apps may resolve the same logical folder
    via different Windows APIs (legacy CSIDL vs. modern KNOWNFOLDERID), and
    MSIX maps each to a different VFS path - so multiple excludes per folder
    are needed to cover all routes:
      VFS\Personal - Documents (legacy CSIDL_PERSONAL)
      VFS\Profile\Documents - Documents (modern fallback)
      VFS\Profile\Pictures - Pictures
      VFS\Profile\Music - Music
      VFS\Profile\Videos - Videos
      VFS\Profile\Downloads - Downloads
      VFS\Profile\Desktop - Desktop (legacy CSIDL_DESKTOPDIRECTORY)
      VFS\ThisPCDesktopFolder - Desktop (Win10+ KNOWNFOLDERID)
    Off by default - turn on when the app is supposed to produce files the
    user can find later via Explorer (would otherwise disappear on uninstall).

.EXAMPLE
    Add-MSIXPSFDefaultFRF -MSIXFolder "C:\MSIXTemp\WinRAR"

.EXAMPLE
    Add-MSIXPSFDefaultFRF -MSIXFolder "C:\MSIXTemp\Lab" -SystemFolders:$false

.EXAMPLE
    # Office-style app: keep Documents writes in the real user profile
    Add-MSIXPSFDefaultFRF -MSIXFolder "C:\MSIXTemp\App" -ExcludePersonalFolder:$true

.NOTES
    Hintergrund / Stolpersteine zu FRF (Hypothesen, VFS-Pfad-Inkonsistenzen, Best Practices)
    sind in Test\InternalDocs\MSIX-Grundlagen-KB.md unter "File Redirection Fixup (FRF)"
    dokumentiert (Buchkandidat-Material).

    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [String] $Executable = '.*',

        [String] $PackageRelativeBase = '',

        [bool] $SystemFolders         = $false,
        [bool] $ExcludePersonalFolder = $false
    )

    process {
        # Validate MSIXFolder
        if (-not (Test-Path $MSIXFolder.FullName -PathType Container)) {
            Write-Error "MSIXFolder not found: $($MSIXFolder.FullName)"
            return
        }
        if (-not (Test-Path (Join-Path $MSIXFolder.FullName 'AppxManifest.xml'))) {
            Write-Warning "AppxManifest.xml not found in '$($MSIXFolder.FullName)'. Is this a valid expanded MSIX package?"
        }

        # Validate Executable is a compilable regex
        try {
            $null = [System.Text.RegularExpressions.Regex]::new($Executable)
        }
        catch {
            Write-Error "-Executable '$Executable' is not a valid regular expression: $_"
            return
        }

        # When the catch-all is scoped to a subfolder, the VFS-mapping side effect
        # that catches system writes via the whole-package catch-all no longer
        # applies. Warn the user if they didn't explicitly opt into SystemFolders.
        if ($PackageRelativeBase -ne '' -and -not $PSBoundParameters.ContainsKey('SystemFolders')) {
            Write-Warning ("PackageRelativeBase='$PackageRelativeBase' scopes the catch-all to that subfolder. " +
                'Writes to system paths (System32, ProgramFiles, ...) are no longer redirected via ' +
                'VFS-mapping side-effects. If the app writes to system folders, consider ' +
                "-SystemFolders:`$true to add explicit knownFolder rules.")
        }

        # Exclusion: executable and binary file types must not be redirected
        Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
            -PackageRelative -Base $PackageRelativeBase `
            -Patterns @(
                '.*\\.[eE][xX][eE]$', '.*\\.[dD][lL][lL]$', '.*\\.[tT][lL][bB]$',
                '.*\\.[oO][cC][xX]$', '.*\\.[cC][oO][mM]$', '.*\\.[fF][oO][nN]$',
                '.*\\.[tT][tT][cC]$', '.*\\.[tT][tT][fF]$', '.*\\.[zZ][iI][pP].*'
            ) -IsExclusion

        # Exclusion: VFS\FONTS is managed by the OS font subsystem
        Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
            -PackageRelative -Base 'VFS\FONTS' -Patterns @('.*') -IsExclusion

        # Optional exclusion: keep user's personal data folders in the real profile.
        # MSIX VFS naming is wildly inconsistent across legacy CSIDL paths and the
        # newer KNOWNFOLDERID names. Apps that fall back from one API to the other
        # (common in .NET libraries) hit different VFS paths sequentially, so we
        # exclude both routes per logical folder.
        if ($ExcludePersonalFolder) {
            $personalFolders = @(
                'VFS\Personal',             # Documents (legacy CSIDL_PERSONAL)
                'VFS\Profile\Documents',    # Documents (modern fallback)
                'VFS\Profile\Pictures',
                'VFS\Profile\Music',
                'VFS\Profile\Videos',
                'VFS\Profile\Downloads',
                'VFS\Profile\Desktop',      # Desktop (legacy CSIDL_DESKTOPDIRECTORY)
                'VFS\ThisPCDesktopFolder'   # Desktop (Win10+ KNOWNFOLDERID)
            )
            foreach ($pf in $personalFolders) {
                Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
                    -PackageRelative -Base $pf -Patterns @('.*') -IsExclusion
            }
        }

        # Redirect everything else under the package root (or, if -PackageRelativeBase
        # is set, only under that subfolder of the package).
        Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
            -PackageRelative -Base $PackageRelativeBase -Patterns @('.*')

        if ($SystemFolders) {
            Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
                -KnownFolder 'ProgramFilesX64' -Base '' -Patterns @('.*')

            Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
                -KnownFolder 'SystemX86' -Base '' -Patterns @('.*')

            Add-MSIXPSFFileRedirectionFixup -MSIXFolder $MSIXFolder -Executable $Executable `
                -KnownFolder 'System' -Base '' -Patterns @('.*')
        }
    }
}