Public/Add-MSIXFixWinRARModernShell.ps1

function Add-MSIXFixWinRARModernShell {
<#
.SYNOPSIS
    Replaces the WinRAR classic context menu with MsixModernContextMenuHandler
    for the Windows 11 modern context menu (top section, IExplorerCommand).

.DESCRIPTION
    Replaces the classic RarExt.dll shell extension (IContextMenu via COM surrogate /
    PsfFtaCom) with MsixModernContextMenuHandler.dll (IExplorerCommand, in-process).

    Changes applied to the package:
      Removes classic shell extension entries from the manifest and replace is


.PARAMETER MsixFile
    Path to the WinRAR MSIX file to modify.

.PARAMETER MSIXFolder
    Temporary extraction folder. Defaults to a unique path under %TEMP%.

.PARAMETER OutputFilePath
    Path for the repackaged MSIX. Defaults to overwriting the source file.

.PARAMETER Subject
    Publisher subject string (CN=...). When provided, Set-MSIXPublisher is called.

.PARAMETER Version
    Package version to set (e.g. '7.20.1.6').

.PARAMETER HandlerLibsPath
    Folder containing MsixModernContextMenuHandler.dll and .json.
    Defaults to the module's Libs\Fixes\WinRarModernContextMenuReplacement folder.

.PARAMETER Force
    Overwrites existing files in the extraction folder without prompting.

.PARAMETER KeepMSIXFolder
    Keeps the temporary extraction folder after packing.

.EXAMPLE
    Add-MSIXFixWinRARModernShell -MsixFile "C:\Packages\WinRAR.msix" -Verbose

.EXAMPLE
    Add-MSIXFixWinRARModernShell `
        -MsixFile "C:\Packages\WinRAR.msix" `
        -OutputFilePath "C:\Packages\WinRAR_modern.msix" `
        -Subject "CN=Contoso, O=Contoso, C=DE" `
        -Version "7.20.1.6" `
        -Verbose

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

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [System.IO.FileInfo] $MsixFile,

        [System.IO.DirectoryInfo] $MSIXFolder = ($env:Temp + '\MSIX_TEMP_' + [System.Guid]::NewGuid().ToString()),

        [System.IO.FileInfo] $OutputFilePath = $null,

        [string] $Subject = '',
        [string] $Version = '',

        [string] $HandlerLibsPath = (Join-Path $PSScriptRoot '..\Libs\Fixes\WinRarModernContextMenuReplacement'),

        [switch] $Force,
        [switch] $KeepMSIXFolder
    )

    process {
        if ($null -eq $OutputFilePath) {
            $OutputFilePath = $MsixFile
        }

        try {
            $null = Open-MSIXPackage -MsixFile $MsixFile -Force:$Force -MSIXFolder $MSIXFolder

            if ($Subject -ne '') {
                Set-MSIXPublisher -MSIXFolder $MSIXFolder -PublisherSubject $Subject
            }

            if ($Version -ne '') {
                Set-MSIXPackageVersion -MSIXFolder $MSIXFolder -MSVersion $Version
                Write-Verbose "Package version set to $Version"
            }

            Invoke-MSIXCleanup -MSIXFolder $MSIXFolder

            # RarExt.dll and RarExt32.dll are the classic IContextMenu shell extension DLLs.
            # Remove
            foreach ($artifact in @('Uninstall.exe', 'RarExt.dll', 'RarExt32.dll')) {
                $artifactPath = Join-Path $MSIXFolder.FullName "VFS\ProgramFilesX64\WinRAR\$artifact"
                if (Test-Path $artifactPath) {
                    Remove-Item $artifactPath -Force
                    Write-Verbose "Removed WinRAR artifact: $artifact"
                }
            }

            # Remove classic shell extension manifest entries
            Remove-MSIXClassicShellExtension -MSIXFolder $MSIXFolder -Categories @(
                'windows.fileExplorerClassicContextMenuHandler',
                'windows.fileExplorerClassicDragDropContextMenuHandler',
                'windows.fileExplorerContextMenus'
            )

            # Copy MsixModernContextMenuHandler.dll + .json to the package root.
            $libsResolved = [IO.Path]::GetFullPath($HandlerLibsPath)
            $vfsWinRAR    = Join-Path $MSIXFolder.FullName 'VFS\ProgramFilesX64\WinRAR'
            $pkgRoot      = $MSIXFolder.FullName
            $dllSrc       = Join-Path $libsResolved 'MsixModernContextMenuHandler.dll'
            $jsonSrc      = Join-Path $libsResolved 'MsixModernContextMenuHandler.json'

            if (-not (Test-Path $dllSrc))  { throw "Handler DLL not found: $dllSrc" }
            if (-not (Test-Path $jsonSrc)) { throw "Handler JSON not found: $jsonSrc" }

            Copy-Item $dllSrc  $pkgRoot  -Force
            Copy-Item $jsonSrc $pkgRoot  -Force
            Write-Verbose "Copied MsixModernContextMenuHandler.dll + .json to package root"

            Copy-Item $dllSrc  $vfsWinRAR -Force
            Copy-Item $jsonSrc $vfsWinRAR -Force
            Write-Verbose "Copied MsixModernContextMenuHandler.dll + .json into VFS\ProgramFilesX64\WinRAR"

            # --- Manifest: remove old comServer, add desktop4/5 FileExplorerContextMenus ---

            $manifestPath = Join-Path $MSIXFolder.FullName 'AppxManifest.xml'
            $xml = New-Object System.Xml.XmlDocument
            $xml.Load($manifestPath)

            $comNs    = 'http://schemas.microsoft.com/appx/manifest/com/windows10'
            $d4Ns     = 'http://schemas.microsoft.com/appx/manifest/desktop/windows10/4'
            $d5Ns     = 'http://schemas.microsoft.com/appx/manifest/desktop/windows10/5'
            # Single CLSID for all ItemTypes — VS Code and Notepad++ both use one CLSID.
            # Two CLSIDs caused an Explorer crash (KERNELBASE.dll 0xc0000005) before the DLL
            # was even loaded. One CLSID, multiple ItemType registrations = proven pattern.
            $clsid    = '4A7B3C1D-E2F5-4689-ABCD-EF1234567891'

            # Remove all existing comServer extensions (old RarExt.dll SurrogateServer).
            foreach ($node in @($xml.SelectNodes(
                    "//*[local-name()='Extension' and @Category='windows.comServer']"))) {
                $null = $node.ParentNode.RemoveChild($node)
                Write-Verbose "Removed old comServer extension"
            }

            # Target the Application that runs WinRAR.exe (before PSF shimming renames it).
            # A generic first-match approach picks the wrong Application when the package
            # declares multiple Applications (e.g. a separate RarExtInstaller entry).
            $winrarAppNode = $null
            foreach ($candidateApp in @($xml.SelectNodes("//*[local-name()='Application']"))) {
                if ($candidateApp.GetAttribute('Executable') -like '*WinRAR.exe') {
                    $winrarAppNode = $candidateApp
                    break
                }
            }
            if ($null -eq $winrarAppNode) {
                $winrarAppNode = $xml.SelectSingleNode("//*[local-name()='Application']")
                Write-Warning "No Application with WinRAR.exe found - falling back to first Application."
            }

            # Locate or create the Extensions element under the target Application.
            # Use ChildNodes iteration to avoid XPath single-quote quoting issues.
            $appExtNode = $null
            foreach ($childNode in $winrarAppNode.ChildNodes) {
                if ($childNode.LocalName -eq 'Extensions') {
                    $appExtNode = $childNode
                    break
                }
            }
            if (-not $appExtNode) {
                $appExtNode = $xml.CreateElement(
                    'Extensions',
                    'http://schemas.microsoft.com/appx/manifest/foundation/windows10')
                $null = $winrarAppNode.AppendChild($appExtNode)
                Write-Verbose "Created Extensions element under WinRAR Application."
            }

            # Ensure required namespaces are declared on the root element.
            foreach ($ns in @(
                    @{ Prefix = 'xmlns:com';      Uri = $comNs },
                    @{ Prefix = 'xmlns:desktop4'; Uri = $d4Ns  },
                    @{ Prefix = 'xmlns:desktop5'; Uri = $d5Ns  })) {
                if (-not $xml.DocumentElement.HasAttribute($ns.Prefix)) {
                    $xml.DocumentElement.SetAttribute($ns.Prefix, $ns.Uri)
                    Write-Verbose "Added $($ns.Prefix) namespace to manifest root"
                }
            }

            # Add desktop4 and desktop5 to IgnorableNamespaces
            $ignorable = $xml.DocumentElement.GetAttribute('IgnorableNamespaces')
            $nsToAdd = @('desktop4', 'desktop5') | Where-Object { $ignorable -notmatch "\b$_\b" }
            if ($nsToAdd.Count -gt 0) {
                $xml.DocumentElement.SetAttribute(
                    'IgnorableNamespaces',
                    ($ignorable.TrimEnd() + ' ' + ($nsToAdd -join ' ')).Trim())
                Write-Verbose "IgnorableNamespaces updated: added $($nsToAdd -join ', ')"
            }

            $comExt       = $xml.CreateElement('com:Extension', $comNs)
            $comExt.SetAttribute('Category', 'windows.comServer')
            $comServer    = $xml.CreateElement('com:ComServer', $comNs)
            $comSurrogate = $xml.CreateElement('com:SurrogateServer', $comNs)
            $comSurrogate.SetAttribute('DisplayName', 'MsixModernContextMenuHandler')
            $comClass     = $xml.CreateElement('com:Class', $comNs)
            $comClass.SetAttribute('Id', $clsid)
            $comClass.SetAttribute('Path', 'MsixModernContextMenuHandler.dll')
            $comClass.SetAttribute('ThreadingModel', 'STA')
            $null = $comSurrogate.AppendChild($comClass)
            $null = $comServer.AppendChild($comSurrogate)
            $null = $comExt.AppendChild($comServer)
            $null = $appExtNode.AppendChild($comExt)
            Write-Verbose "Added com:SurrogateServer for MsixModernContextMenuHandler.dll (STA)"

            $d4Ext   = $xml.CreateElement('desktop4:Extension', $d4Ns)
            $d4Ext.SetAttribute('Category', 'windows.fileExplorerContextMenus')
            $d4Menus = $xml.CreateElement('desktop4:FileExplorerContextMenus', $d4Ns)

            foreach ($itemType in @('*', 'Directory')) {
                $d5Item = $xml.CreateElement('desktop5:ItemType', $d5Ns)
                $d5Item.SetAttribute('Type', $itemType)
                $d5Verb = $xml.CreateElement('desktop5:Verb', $d5Ns)
                $d5Verb.SetAttribute('Id', 'ModernMenu')
                $d5Verb.SetAttribute('Clsid', $clsid)
                $null = $d5Item.AppendChild($d5Verb)
                $null = $d4Menus.AppendChild($d5Item)
            }

            $null = $d4Ext.AppendChild($d4Menus)
            $null = $appExtNode.AppendChild($d4Ext)
            Write-Verbose "Added desktop4:FileExplorerContextMenus (*, Directory) to Application/Extensions"

            # Remove capabilities WinRAR does not need when deployed outside the Store
            foreach ($capName in @('internetClient')) {
                $capNode = $xml.SelectSingleNode(
                    "//*[local-name()='Capability' and @Name='$capName']")
                if ($capNode) {
                    $null = $capNode.ParentNode.RemoveChild($capNode)
                    Write-Verbose "Removed Capability: $capName"
                }
            }

            $xml.Save($manifestPath)

            # --- PSF for WinRAR.exe ---

            Add-MSIXInstalledLocationVirtualization -MSIXFolderPath $MSIXFolder
            Add-MSIXPsfFrameworkFiles -MSIXFolder $MSIXFolder

            $apps = Get-MSIXApplications -MSIXFolder $MSIXFolder
            if ($null -eq $apps -or $apps.Count -eq 0) {
                Write-Warning "No application entries found in AppxManifest.xml."
            }
            else {
                $shimmedIds = @{}
                foreach ($app in $apps) {
                    Write-Verbose "Adding PSF shim for application: $($app.Id)"
                    $newId = Add-MSXIXPSFShim -MSIXFolder $MSIXFolder -MISXAppID $app.Id -PSFArchitektur x64
                    $shimmedIds[$app.Id] = $newId
                }

                $winrarApp = $apps | Where-Object { $_.Executable -like '*WinRAR.exe' } | Select-Object -First 1
                if ($null -ne $winrarApp) {
                    $winrarNewId = $shimmedIds[$winrarApp.Id]
                    Add-MSIXAppExecutionAlias -MSIXFolder $MSIXFolder `
                        -MISXAppID $winrarNewId `
                        -CommandlineAlias 'WinRAR.exe' `
                        -Executable 'VFS\ProgramFilesX64\WinRAR\WinRAR.exe'

                    # PSFLauncher is intentionally NOT set in the deployed JSON.
                }
            }

            Add-MSIXPSFDefaultRegLegacy -MSIXFolder $MSIXFolder
            Add-MSIXPSFMFRFixup -MSIXFolder $MSIXFolder -IlvAware $true

            # Move IExplorerCommand extensions to a hidden Application to suppress the
            # Windows 11 Application-level grouping flyout.
            if ($null -ne $winrarNewId) {
                $xmlPost = New-Object System.Xml.XmlDocument
                $xmlPost.Load($manifestPath)

                $shimmedApp = $null
                foreach ($appNode in @($xmlPost.SelectNodes("//*[local-name()='Application']"))) {
                    if ($appNode.GetAttribute('Id') -eq $winrarNewId) {
                        $shimmedApp = $appNode
                        break
                    }
                }

                if ($null -ne $shimmedApp) {
                    $shimmedExts = $null
                    foreach ($child in $shimmedApp.ChildNodes) {
                        if ($child.LocalName -eq 'Extensions') {
                            $shimmedExts = $child
                            break
                        }
                    }

                    if ($null -ne $shimmedExts) {
                        $toMove = @()
                        foreach ($ext in @($shimmedExts.ChildNodes)) {
                            $cat = $ext.GetAttribute('Category')
                            if ($cat -eq 'windows.comServer' -or $cat -eq 'windows.fileExplorerContextMenus') {
                                $toMove += $ext
                            }
                        }

                        if ($toMove.Count -gt 0) {
                            $fNs = 'http://schemas.microsoft.com/appx/manifest/foundation/windows10'

                            $helperApp = $xmlPost.CreateElement('Application', $fNs)
                            $helperApp.SetAttribute('Id', 'WinRARMenuHelper')
                            $helperApp.SetAttribute('Executable', 'VFS\ProgramFilesX64\WinRAR\RarExtInstaller.exe')
                            $helperApp.SetAttribute('EntryPoint', 'Windows.FullTrustApplication')

                            # AppListEntry="none" is an attribute of uap:VisualElements,
                            # Suppress the Start-menu entry on the clone.
                            $sourceVe = $null
                            foreach ($child in $shimmedApp.ChildNodes) {
                                if ($child.LocalName -eq 'VisualElements') {
                                    $sourceVe = $child
                                    break
                                }
                            }
                            if ($null -ne $sourceVe) {
                                $veEl = $sourceVe.CloneNode($true)
                                $null = $veEl.SetAttribute('AppListEntry', 'none')
                                $null = $veEl.SetAttribute('DisplayName', 'WinRAR Menu Handler')
                                $null = $helperApp.AppendChild($veEl)
                            }
                            else {
                                Write-Warning "No VisualElements found on '$winrarNewId' - WinRARMenuHelper will lack VisualElements."
                            }

                            $helperExts = $xmlPost.CreateElement('Extensions', $fNs)
                            foreach ($ext in $toMove) {
                                $null = $shimmedExts.RemoveChild($ext)
                                $null = $helperExts.AppendChild($ext)
                            }
                            $null = $helperApp.AppendChild($helperExts)

                            $appsEl = $xmlPost.SelectSingleNode("//*[local-name()='Applications']")
                            $null = $appsEl.AppendChild($helperApp)

                            $xmlPost.Save($manifestPath)
                            Write-Verbose "Moved context menu extensions to hidden Application 'WinRARMenuHelper'"
                        }
                    }
                }
            }

            Close-MSIXPackage -MSIXFolder $MSIXFolder -MSIXFile $OutputFilePath -Force:$Force -KeepMSIXFolder:$KeepMSIXFolder
            "WinRAR modern shell fix applied: $OutputFilePath"
        }
        catch {
            Write-Error "Error applying WinRAR modern shell fix: $_"
        }
    }
}