AlkanePSF.psm1

Add-Type -AssemblyName System.IO.Compression.FileSystem

$script:applicationsModel = @()
$script:processesModel = @()
$script:capabilitiesArray = @()
$script:dependenciesModel = @()
$script:protocolsModel = @()
$script:stringReplaceModel = @()
$script:scriptArray = @()
$script:removeApplicationArray = @()
$script:removeShortcutArray = @()

$script:alkanePSFConfigured = $false

$script:MSIXInputFilePath = ""
$script:MSIXOutputFilePath = ""
$script:MSIXStagingFolderPath = ""
$script:MSIXCertificateFilePath = ""
$script:MSIXCertificatePassword = ""
$script:MSIXArchitecture = ""
$script:TimManganZipUrl = ""
$script:PSFType = ""

$fileRedirectionDllName = "FileRedirectionFixup.dll"
$regLegacyDllName = "RegLegacyFixups.dll"
$envVarDllName = "EnvVarFixup.dll"
$dynamicLibraryDllName = "DynamicLibraryFixup.dll"
$mfrDllName = "MFRFixup.dll"
$traceDllName = "TraceFixup.dll"

function Write-AlkanePSFOutput {
    Param($message)

    if ($message -like "*error*") {
        Write-Host $message -ForegroundColor Red
    } elseif ($message -like "*warning*") {
        Write-Host $message -ForegroundColor DarkYellow
    } else {
        Write-Host $message
    }
}

#*******************************************
#********************************************
#function to configure AlkanePSF
#*******************************************
#********************************************

function Set-AlkanePSFConfiguration {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [string]$MSIXInputFilePath,
        [string]$MSIXOutputFilePath,
        [string]$MSIXStagingFolderPath,
        [string]$MSIXCertificateFilePath,
        [securestring]$MSIXCertificatePassword,
        [ValidateSet("32","64")]
        [string]$MSIXArchitecture,
        [ValidateSet("MS","TM")]
        [string]$PSFType="MS",
        [string]$TimManganZipUrl
    )

    if($PSCmdlet.ShouldProcess("Should process?")){

        if ($null -eq $MSIXInputFilePath -or $MSIXInputFilePath -eq "" -or (!(test-path $MSIXInputFilePath))) {
            Write-AlkanePSFOutput "**ERROR** Could not find input MSIX path."
        }
        $script:MSIXInputFilePath = $MSIXInputFilePath
        $script:MSIXOutputFilePath = $MSIXOutputFilePath

        if ($MSIXStagingFolderPath -notlike "*\") { $MSIXStagingFolderPath = "$MSIXStagingFolderPath\" }
        $script:MSIXStagingFolderPath = $MSIXStagingFolderPath

        if ($null -eq $MSIXCertificateFilePath -or $MSIXCertificateFilePath -eq "" -or (!(test-path $MSIXCertificateFilePath))) {
            Write-AlkanePSFOutput "**ERROR** Could not find certificate path."
        }
        $script:MSIXCertificateFilePath = $MSIXCertificateFilePath
        $pword = [System.Net.NetworkCredential]::new("", $MSIXCertificatePassword).Password
        if ($null -eq $pword -or $pword -eq "") {
            Write-AlkanePSFOutput "**ERROR** Certificate password cannot be empty."
        }
        $script:MSIXCertificatePassword = $pword
        $script:MSIXArchitecture = $MSIXArchitecture
        if ($null -eq $TimManganZipUrl -or $TimManganZipUrl -eq "" -or $TimManganZipUrl -notlike "https://github.com/TimMangan/MSIX-PackageSupportFramework/blob/develop/*") {
            Write-AlkanePSFOutput "**ERROR** Tim Mangan PSF url should be similar to (change version as required) https://github.com/TimMangan/MSIX-PackageSupportFramework/blob/develop/ZipRelease.zip-v2024-10-26.zip."
        }
        $script:TimManganZipUrl = $TimManganZipUrl
        $script:PSFType = $PSFType

        Write-AlkanePSFOutput "AlkanePSF configured."
        $script:alkanePSFConfigured = $true
    }
}

#*******************************************
#********************************************
#function to start process, return exit code and output command line
#*******************************************
#********************************************

function Start-AlkanePSFProcess {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [string]$Exe,
        [array]$ExeArgs
    )

    if($PSCmdlet.ShouldProcess("Should process?")){

        $fullcommand = @($Exe,$ExeArgs)

        # Process each element in the array and add quotes if it contains spaces
        $quotedArgs = $fullcommand | ForEach-Object {
            if ($_ -match '\s') {
                "`"$_`""  # Add quotes around the argument if it contains spaces
            } else {
                $_  # Leave the argument as is if it doesn't contain spaces
            }
        }

        # Join the arguments into a single string
        $joinedArgs = $quotedArgs -join " "
        Write-AlkanePSFOutput "Command: $joinedArgs"

        #run command
        return (Start-Process -FilePath $Exe -ArgumentList $ExeArgs -Wait -Passthru).ExitCode
    }
}

#*******************************************
#********************************************
#function to remove directories with long paths
#*******************************************
#********************************************

function Remove-LongPathDirectory
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [string]$FullPath
    )
    if($PSCmdlet.ShouldProcess("Should process?")){
        & cmd /c rmdir "$FullPath" /s /q
    }
}

#*******************************************
#********************************************
#function to find an exe in the Windows SDK
#*******************************************
#********************************************

function Get-WindowsSDKExe {
    Param(
        [string]$ExeName
    )

    try {
        $sdkPath = "${env:ProgramFiles(x86)}\Windows Kits\10\Bin\"
        if (!(test-path $sdkPath)) {
            Write-AlkanePSFOutput "Could not find $sdkPath"
            return ""
        }

        if($env:PROCESSOR_ARCHITECTURE -eq "x86") {
            $pathToExe = (Get-ChildItem $sdkPath -recurse -include $ExeName -ErrorAction SilentlyContinue | Where-Object FullName -like "*\x86\*" | Select-Object -First 1 -ExpandProperty FullName)
        } else {
            $pathToExe = (Get-ChildItem $sdkPath -recurse -include $ExeName -ErrorAction SilentlyContinue | Where-Object FullName -like "*\x64\*" | Select-Object -First 1 -ExpandProperty FullName)
        }

        return $pathToExe

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#********************************************
#********************************************
#function to install PSF prereqs
#********************************************
#********************************************

function Install-AlkanePSFPrerequisite() {
    Param(
        [bool]$ForceReinstall=$false
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {

        #installs
        #https://developer.microsoft.com/en-gb/windows/downloads/windows-sdk/
        #with
        #Windows SDK Signing Tools for Desktop Apps
        #Windows SDK for UWP Managed Apps
        #Windows SDK for UWP C++ Apps
        #Windows SDK for UWP Apps Localization

        $makeAppxPath = Get-WindowsSDKExe -ExeName "makeappx.exe"

        if (($null -eq $makeAppxPath -or $makeAppxPath -eq "") -or $ForceReinstall) {
            Write-AlkanePSFOutput "Installing Windows SDK tools for MSIX."

            $downloadExePath = "$env:temp\wdksetup.exe"
            $url = "https://go.microsoft.com/fwlink/?linkid=2083338"

            if (test-path $downloadExePath) {
                #remove before downloading
                Write-AlkanePSFOutput "Removing $downloadExePath."
                remove-item -Path $downloadExePath -Force -Recurse -ErrorAction SilentlyContinue
            }

            Write-AlkanePSFOutput "Downloading Windows SDK from $url."

            (New-Object Net.WebClient).DownloadFile($url, $downloadExePath)

            if (test-path $downloadExePath) {
                Write-AlkanePSFOutput "Installing Windows SDK."

                $processArgs = @("/features","OptionId.SigningTools","OptionId.UWPManaged","OptionId.UWPCPP","OptionId.UWPLocalized","/quiet","/norestart")
                $exitcode = (Start-AlkanePSFProcess -Exe $downloadExePath -ExeArgs $processArgs)

                if ($exitcode -eq 0) {
                    Write-AlkanePSFOutput "Windows SDK installed with exit code $exitcode."
                } else {
                    Write-AlkanePSFOutput "**WARNING** Windows SDK installed with exit code $exitcode."
                }

                #remove download
                Write-AlkanePSFOutput "Removing $downloadExePath."
                Remove-Item $downloadExePath -Force -Recurse -ErrorAction SilentlyContinue
            } else {
                Write-AlkanePSFOutput "**WARNING** Could not download Windows SDK from $url"
            }
        } else {
            Write-AlkanePSFOutput "Windows SDK already installed."
        }

        $nupkg = Get-Package | Where-Object Name -eq "Microsoft.PackageSupportFramework" | Select-Object -ExpandProperty Source

        if (($null -eq $nupkg) -or $ForceReinstall) {
            Write-AlkanePSFOutput "Installing Microsoft's Package Support Framework."

            $nuget = get-packagesource | Where-Object ProviderName -eq "Nuget"
            if ($null -eq $nuget) {
                Register-PackageSource -Name nuget.org -Location https://www.nuget.org/api/v2 -ProviderName NuGet -Trusted
                Install-Package -Name Microsoft.PackageSupportFramework -ProviderName Nuget -Force
                Write-AlkanePSFOutput "Microsoft's Package Support Framework installed."
            } else {
                $package = Get-Package | Where-Object Name -eq "Microsoft.PackageSupportFramework"
                if ($null -eq $package) {
                    Install-Package -Name Microsoft.PackageSupportFramework -ProviderName Nuget -Force
                    Write-AlkanePSFOutput "Microsoft's Package Support Framework installed."
                } else {
                    if ($ForceReinstall) {
                        Install-Package -Name Microsoft.PackageSupportFramework -ProviderName Nuget -Force
                        Write-AlkanePSFOutput "Microsoft's Package Support Framework installed."
                    }
                }
            }
        } else {
            Write-AlkanePSFOutput "Microsoft's Package Support Framework already installed."
        }


        $tmPsfLocation = "$env:temp\TMPSF"

        if ((!(test-path $tmPsfLocation)) -or $ForceReinstall) {
            Write-AlkanePSFOutput "Installing Tim Mangan's Package Support Framework."

            $extractFolder = "$env:temp\TMPSF"
            $downloadZipPath = "$env:temp\TMPSF.zip"

            if (test-path $downloadZipPath) {
                #remove before downloading
                Write-AlkanePSFOutput "Removing $downloadZipPath."
                remove-item -Path $downloadZipPath -Force -Recurse -ErrorAction SilentlyContinue
            }

            if (test-path $extractFolder) {
                #remove before extracting
                Write-AlkanePSFOutput "Removing $extractFolder."
                Remove-LongPathDirectory $extractFolder
            }

            Write-AlkanePSFOutput "Downloading Tim Mangan's PSF from $script:TimManganZipUrl."

            #download zip
            (New-Object Net.WebClient).DownloadFile($script:TimManganZipUrl, $downloadZipPath)

            #if zip downloaded
            if (test-path $downloadZipPath) {

                #extract it
                [IO.Compression.Zipfile]::ExtractToDirectory($downloadZipPath,$extractFolder);

                #if extracted ok, extract the release folder
                if (test-path "$extractFolder\ReleasePsf.zip") {
                    [IO.Compression.Zipfile]::ExtractToDirectory("$extractFolder\ReleasePsf.zip",$extractFolder);
                    Write-AlkanePSFOutput "Tim Mangen's Package Support Framework installed."
                }

                #remove download
                Write-AlkanePSFOutput "Removing $downloadZipPath."
                Remove-Item $downloadZipPath -Force -Recurse -ErrorAction SilentlyContinue
            }
        } else {
             Write-AlkanePSFOutput "Tim Mangan's Package Support Framework already installed."
        }

        Write-AlkanePSFOutput "Finished installing prerequisites."
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#********************************************
#********************************************
#function to extract/stage an MSIX package
#********************************************
#********************************************



function Get-AlkanePSFApplicationId {

    $appxManifest = "$($script:MSIXStagingFolderPath)AppxManifest.xml"

    if (test-path $appxManifest) {
        [xml]$appInfo = Get-Content -Path $appxManifest
        $applications = $appInfo.Package.Applications.Application
        if ($null -ne $applications) {
            $availableApps = ($applications.id -join " ")
            Write-AlkanePSFOutput "Available App Id's are: $availableApps"
        }
    }
}

#********************************************
#********************************************
#function to extract/stage an MSIX package
#********************************************
#********************************************

function New-AlkanePSFStagedPackage {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param()

    if($PSCmdlet.ShouldProcess("Should process?")){
        try {
            if (!(test-path $script:MSIXInputFilePath)) {
                Write-AlkanePSFOutput "**WARNING** Could not find $script:MSIXInputFilePath"
                return
            }

            Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)"
            if (Test-Path $script:MSIXStagingFolderPath) {
                Remove-LongPathDirectory $script:MSIXStagingFolderPath
            }

            New-Item -Path $script:MSIXStagingFolderPath -ItemType Directory -Force | Out-Null

            Write-AlkanePSFOutput "Finding MakeAppx.exe."
            $makeAppxPath = Get-WindowsSDKExe -ExeName "makeappx.exe"



            if ($makeAppxPath -ne "") {

                Write-AlkanePSFOutput "Extracting (staging) package to $($script:MSIXStagingFolderPath)."
                $processArgs = @("unpack","/p","$script:MSIXInputFilePath","/d","$($script:MSIXStagingFolderPath)")
                $exitcode = (Start-AlkanePSFProcess -Exe $makeAppxPath -ExeArgs $processArgs)

                if ($exitcode -eq 0) {
                    Write-AlkanePSFOutput "Package extracted (staged) to $($script:MSIXStagingFolderPath) with exit code $exitcode."
                } else {
                    Write-AlkanePSFOutput "**WARNING** Package extracted (staged) to $($script:MSIXStagingFolderPath) with exit code $exitcode."
                }

            } else {
                Write-AlkanePSFOutput "Cannot find MakeAppx.exe"
            }

        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }
}


#********************************************
#********************************************
#function to get the name of a filename without the extension
#********************************************
#********************************************

function Get-ExeNameForFixup {
    Param(
        [string]$PathToEXE
    )

    try {
        if ($PathToEXE -like "*/*" -or $PathToEXE -like "*\*") {
            $PathToEXE = $PathToEXE.replace('\','/').split('/')[-1]
        }

        #keep full file name
        #if ($PathToEXE -like "*.*") {
        # $PathToEXE = $PathToEXE.split('.')[0]
        #}

        return $PathToEXE
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#function to get a PSF fixup
#*******************************************
#********************************************

function Get-AlkanePSFFixup {

    try {
        $allFixups = @()

        foreach ($fixup in $script:processesModel.fixups) {
            $allFixups += $fixup
        }

        return ($allFixups)
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


Function Get-OriginalExe {
    Param(
        [string]$configJson,
        [string]$applicationId
    )

    if (test-path $configJson) {
        # Load JSON file
        $json = Get-Content -Path $configJson -Raw | ConvertFrom-Json

        # Loop through the objects in the JSON data
        foreach ($object in $json.PSObject.Properties | Where-Object Name -eq "applications") {

            $app = $($object.Value)

            if ($app.id -eq $applicationId) {
                return $app.executable
            }
        }
    }
    return ""

}

#*******************************************
#********************************************
#Function to add a supported Fixup application
#*******************************************
#********************************************

Function Add-AlkanePSFApplication {
    Param(
        [string]$ApplicationId,
        [string]$WorkingDirectory,
        [array]$Arguments,
        [bool]$InPackageContext=$true,
        [string]$ScriptExecutionMode="-ExecutionPolicy Bypass"
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {

        if (!(test-path $script:MSIXInputFilePath)) {
            Write-AlkanePSFOutput "**WARNING** Could not find $script:MSIXInputFilePath"
            return
        }

        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -ne $targetExe -and $targetExe -ne "") {
            Write-AlkanePSFOutput "Application $ApplicationId already exists. Will not add again."
            return
        }

        $appxManifest = "$($script:MSIXStagingFolderPath)AppxManifest.xml"

        if (!(test-path $appxManifest)) {
            Write-AlkanePSFOutput "**WARNING** Could not find $appxManifest"
            return
        }

        $exe=""

        [xml]$appInfo = Get-Content -Path $appxManifest

        $applications = $appInfo.Package.Applications.Application
        if ($null -ne $applications) {

            $exe = $applications | Where-Object id -eq "$ApplicationId" | Select-Object -ExpandProperty executable

            if ($exe -like "*psflauncher*") {
                $configJSON = "$($script:MSIXStagingFolderPath)config.json"
                $origExe = Get-OriginalExe $configJSON $ApplicationId

                if ($origExe -ne "") {
                    #if manifest exe is psflauncher, we try to reinstate the original from the config.json
                    $exe = $origExe
                }

            }

            $availableApps = ($applications.id -join " ")
            if ($null -eq $exe -or $exe -eq "") {
                Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId in AppxManifest.xml. Available app id's are: $availableApps. Please change the application ID."
                return
            } else {
                $processArgs=""
                if($null -ne $Arguments -or $Arguments.Count -gt 0){
                    $processArgs = ($Arguments | ForEach-Object {"`"" + $_ + "`""}) -join " "
                }

                $script:applicationsModel += ,([ordered]@{
                    id=$ApplicationId;
                    executable=$exe;
                    workingDirectory=$WorkingDirectory;
                    inPackageContext=$InPackageContext;
                    arguments=$processArgs;
                    ScriptExecutionMode=$ScriptExecutionMode;
                })
            }
        }

        $exe = Get-ExeNameForFixup $exe

        $script:processesModel += ,([ordered]@{
            executable=$exe
        })
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


#*******************************************
#********************************************
#Function to add file redirection fixup
#*******************************************
#********************************************

Function Add-AlkanePSFFileRedirectionFixup {
    Param(
        [string]$ApplicationId,
        [ValidateSet("packageRelative","packageDriveRelative","knownFolders")]
        [string]$FixupType,
        [string]$FixupBase,
        [array]$FixupPatterns,
        [string]$FixupId
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding File Redirection Fixup for $ApplicationId"

    try {
        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $FileRedirectionFixupModel = [ordered]@{
            dll=$fileRedirectionDllName;
            config=@{
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $fileRedirectionDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($FileRedirectionFixupModel)
        }

        $fixupstype = (($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $fileRedirectionDllName).config.redirectedPaths.$FixupType
        if ($null -eq $fixupstype -or $fixupstype.Count -eq 0) {
            ($script:processesModel.fixups | Where-Object dll -eq $fileRedirectionDllName).config.redirectedPaths += @{$FixupType = $null }
        }

        switch ($FixupType) {
            "packageRelative" {
                ($script:processesModel.fixups | Where-Object dll -eq $fileRedirectionDllName).config.redirectedPaths.$FixupType += ,@{
                    base=$FixupBase;
                    patterns=$FixupPatterns;
                }
            }
            "packageDriveRelative" {
                ($script:processesModel.fixups | Where-Object dll -eq $fileRedirectionDllName).config.redirectedPaths.$FixupType += ,@{
                    base=$FixupBase;
                    patterns=$FixupPatterns;
                }
            }
            "knownFolders" {
                ($script:processesModel.fixups | Where-Object dll -eq $fileRedirectionDllName).config.redirectedPaths.$FixupType += ,@{
                    id=$FixupId;
                    relativePaths = ,@{
                        base=$FixupBase;
                        patterns=$FixupPatterns;
                    }
                }
            }
        }


    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#Function to add reg legacy fixup
#*******************************************
#********************************************

Function Add-AlkanePSFRegLegacyFixup {
    Param(
        [string]$ApplicationId,
        [ValidateSet("ModifyKeyAccess","FakeDelete")]
        [string]$FixupType,
        [ValidateSet("HKCU","HKLM")]
        [string]$FixupHive,
        [ValidateSet("FULL2RW","FULL2R","Full2MaxAllowed","RW2R","RW2MaxAllowed")]
        [string]$FixupAccess,
        [array]$FixupPatterns
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding Reg Legacy Fixup for $ApplicationId"

    try {
        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $RegLegacyFixupModel = [ordered]@{
            dll=$regLegacyDllName;
            config=@{
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $regLegacyDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($RegLegacyFixupModel)
        }


        switch ($FixupType) {
            "ModifyKeyAccess" {
                ($script:processesModel.fixups | Where-Object dll -eq $regLegacyDllName).config.remediation += ,@{
                    type=$FixupType;
                    hive=$FixupHive;
                    access=$FixupAccess;
                    patterns=$FixupPatterns;
                }
            }
           "FakeDelete" {
                ($script:processesModel.fixups | Where-Object dll -eq $regLegacyDllName).config.remediation += ,@{
                    type=$FixupType;
                    hive=$FixupHive;
                    access=$FixupAccess;
                    patterns=$FixupPatterns;
                }
            }
        }

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#function to add env var fixup
#*******************************************
#********************************************

Function Add-AlkanePSFEnvVarFixup {
    Param(
        [string]$ApplicationId,
        [string]$FixupVarName,
        [string]$FixupVarValue,
        [bool]$FixupVarUseRegistry=$true
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding Env Var Fixup for $ApplicationId"

    try {

        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $EnvVarFixupModel = [ordered]@{
            dll=$envVarDllName;
            config=@{
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $envVarDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($EnvVarFixupModel)
        }

        ($script:processesModel.fixups | Where-Object dll -eq $envVarDllName).config.envVars += ,@{
            name=$FixupVarName;
            value=$FixupVarValue;
            useRegistry=$FixupVarUseRegistry;
        }

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#Function to add dynamic library fixup
#*******************************************
#********************************************

Function Add-AlkanePSFDynamicLibraryFixup {
    Param(
        [string]$ApplicationId,
        [string]$FixupDllName,
        [string]$FixupDllFilepath,
        [bool]$FixupForcePackageDllUse=$true
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding Dynamic Library Fixup for $ApplicationId"

    try {
        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $DynamicLibraryFixupModel = [ordered]@{
            dll=$dynamicLibraryDllName;
            config=@{
                forcePackageDllUse=$FixupForcePackageDllUse;
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $dynamicLibraryDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($DynamicLibraryFixupModel)
        }

        ($script:processesModel.fixups | Where-Object dll -eq $dynamicLibraryDllName).config.relativeDllPaths += ,@{
            name=$FixupDllName;
            filepath=$FixupDllFilepath;
        }

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#Function to add MFR fixup
#*******************************************
#********************************************

Function Add-AlkanePSFMFRFixup {
    Param(
        [string]$ApplicationId,
        [ValidateSet("OverrideLocalRedirections","OverrideTraditionalRedirections")]
        [string]$FixupType,
        [string]$FixupName,
        [string]$FixupMode,
        [bool]$FixupIlvAware=$false,
        [ValidateSet("disableAll","enablePe","default")]
        [string]$FixupOverrideCOW="default"
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding MFR Fixup for $ApplicationId"

    try {
        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $MFRFixupModel = [ordered]@{
            dll=$mfrDllName;
            config=@{
                overrideCOW=$FixupOverrideCOW;
                ilvAware=$FixupIlvAware;
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $mfrDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($MFRFixupModel)
        }

        switch ($FixupType) {
            "OverrideLocalRedirections" {
                ($script:processesModel.fixups | Where-Object dll -eq $mfrDllName).config.overrideLocalRedirections += ,[ordered]@{
                    name=$FixupName;
                    mode=$FixupMode;
                }
            }
            "OverrideTraditionalRedirections" {
                ($script:processesModel.fixups | Where-Object dll -eq $mfrDllName).config.overrideTraditionalRedirections += ,[ordered]@{
                    name=$FixupName;
                    mode=$FixupMode;
                }
            }
        }


    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


#*******************************************
#********************************************
#function to add trace fixup
#*******************************************
#********************************************

Function Add-AlkanePSFTraceFixup {
    Param(
        [string]$ApplicationId,
        [ValidateSet("printf","eventlog","outputDebugString")]
        [string]$FixupTraceMethod,
        [bool]$FixupWaitForDebugger=$false,
        [bool]$FixupTraceFunctionEntry=$false,
        [bool]$FixupTraceCallingModule=$true,
        [bool]$FixupIgnoreDllLoad=$true,
        [ValidateSet("default","filesystem","registry","processAndThread","dynamicLinkLibrary")]
        [string]$FixupTraceLevelProperty,
        [ValidateSet("always","ignoreSuccess","allFailures","unexpectedFailures","ignore")]
        [string]$FixupTraceLevelValue,
        [ValidateSet("default","filesystem","registry","processAndThread","dynamicLinkLibrary")]
        [string]$FixupBreakOnProperty,
        [ValidateSet("always","ignoreSuccess","allFailures","unexpectedFailures","ignore")]
        [string]$FixupBreakOnValue
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding Trace Fixup for $ApplicationId"

    try {

        #check if we have an 'application' in our dynamic config.json
        $targetExe = ($script:applicationsModel | Where-Object id -eq "$ApplicationId").executable
        if ($null -eq $targetExe -or $targetExe -eq "") {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
            return
        }
        #extract the process name without the extension
        $targetExe = Get-ExeNameForFixup -PathToEXE $targetExe

        $TraceFixupModel = [ordered]@{
            dll=$traceDllName;
            config=@{
                traceMethod=$FixupTraceMethod
                waitForDebugger=$FixupWaitForDebugger
                traceFunctionEntry=$FixupTraceFunctionEntry
                traceCallingModule=$FixupTraceCallingModule
                ignoreDllLoad=$FixupIgnoreDllLoad
                traceLevels=@{
                    $FixupTraceLevelProperty=$FixupTraceLevelValue
                }
                breakOn=@{
                    $FixupBreakOnProperty=$FixupBreakOnValue
                }
            }
        }

        #check if fixup has been added previously
        $fixups = ($script:processesModel | Where-Object executable -eq $targetExe).fixups | Where-Object dll -eq $traceDllName
        if ($null -eq $fixups -or $fixups.Count -eq 0) {
            ($script:processesModel | Where-Object executable -eq $targetExe).fixups += @($TraceFixupModel)
        }

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#function to add start script
#*******************************************
#********************************************

Function Add-AlkanePSFStartScript {
    Param(
        [string]$ApplicationId,
        [string]$ScriptSourcePath,
        [array]$ScriptArguments,
        [bool]$RunInVirtualEnvironment=$true,
        [bool]$ShowWindow=$false,
        [bool]$StopOnScriptError=$false,
        [bool]$WaitForScriptToFinish=$true,
        [int]$Timeout,
        [bool]$RunOnce=$true
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding Start Script for $ApplicationId"

    try {
        $foundApp = $false;

        if (!(test-path $ScriptSourcePath)) {
            Write-AlkanePSFOutput "**ERROR** Cannot find script $ScriptSourcePath"
        }

        #script found - add to global array for copying
        $script:scriptArray += $ScriptSourcePath
        $fileName = Split-Path -Path $ScriptSourcePath -Leaf

        foreach ($app in $script:applicationsModel) {

            if ($app.id -eq $ApplicationId) {
                $foundApp = $true

                $processArgs=""
                if($null -ne $ScriptArguments -or $ScriptArguments.Count -gt 0){
                    $processArgs = ($ScriptArguments | ForEach-Object {"`"" + $_ + "`""}) -join " "
                }

                $app.startScript = @{
                    scriptPath=$fileName;
                    scriptArguments=$processArgs;
                    runInVirtualEnvironment=$RunInVirtualEnvironment;
                    showWindow=$ShowWindow;
                    stopOnScriptError=$StopOnScriptError;
                    waitForScriptToFinish=$WaitForScriptToFinish;
                    timeout=$Timeout;
                    runOnce=$RunOnce;
                }
            }
        }

        if (!$foundApp) {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
        }
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}

#*******************************************
#********************************************
#function to add end script
#*******************************************
#********************************************

Function Add-AlkanePSFEndScript {
    Param(
        [string]$ApplicationId,
        [string]$ScriptSourcePath,
        [array]$ScriptArguments,
        [bool]$RunInVirtualEnvironment=$true,
        [bool]$ShowWindow=$false,
        [bool]$StopOnScriptError=$false,
        [bool]$WaitForScriptToFinish=$true,
        [int]$Timeout,
        [bool]$RunOnce=$true
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Adding End Script for $ApplicationId"

    try {
        $foundApp = $false;

        if (!(test-path $ScriptSourcePath)) {
            Write-AlkanePSFOutput "**ERROR** Cannot find script $ScriptSourcePath"
        }

        #script found - add to global array for copying
        $script:scriptArray += $ScriptSourcePath
        $fileName = Split-Path -Path $ScriptSourcePath -Leaf

        foreach ($app in $script:applicationsModel) {
            if ($app.id -eq $ApplicationId) {
                $foundApp = $true

                $processArgs=""
                if($null -ne $ScriptArguments -or $ScriptArguments.Count -gt 0){
                    $processArgs = ($ScriptArguments | ForEach-Object {"`"" + $_ + "`""}) -join " "
                }

                $app.endScript = @{
                    scriptPath=$fileName;
                    scriptArguments=$processArgs;
                    runInVirtualEnvironment=$RunInVirtualEnvironment;
                    showWindow=$ShowWindow;
                    stopOnScriptError=$StopOnScriptError;
                    waitForScriptToFinish=$WaitForScriptToFinish;
                    timeout=$Timeout;
                    runOnce=$RunOnce;
                }
            }
        }

        if (!$foundApp) {
            Write-AlkanePSFOutput "**WARNING** Could not find application ID: $ApplicationId. Please change the application ID or run: Add-AlkanePSFApplication -ApplicationId `"$ApplicationId`"."
        }
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}



#*******************************************
#********************************************
#function to remove application
#*******************************************
#********************************************

function Remove-AlkanePSFApplication
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [string]$ApplicationId
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    if($PSCmdlet.ShouldProcess("Should process?")){
        try {
            $script:removeApplicationArray += $ApplicationId
        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }
}

#*******************************************
#********************************************
#function to remove shortcut
#*******************************************
#********************************************

function Remove-AlkanePSFShortcut
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [string]$ShortcutName
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    if($PSCmdlet.ShouldProcess("Should process?")){
        try {
            $script:removeShortcutArray += $ShortcutName
        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }
}

#*******************************************
#********************************************
#function to add manifest when using PSFLauncher to elevate
#*******************************************
#********************************************

Function Add-AlkanePSFManifest {
    Param(
        [string]$ManifestPath
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    Write-AlkanePSFOutput "Generating PSF manifest for elevation"

    $xmlContent = "<?xml version=`"1.0`" encoding=`"UTF-8`" standalone=`"yes`"?>" + "`r`n"
    $xmlContent += "<assembly xmlns=`"urn:schemas-microsoft-com:asm.v1`" manifestVersion=`"1.0`">" + "`r`n"
    $xmlContent += " <trustInfo xmlns=`"urn:schemas-microsoft-com:asm.v3`">" + "`r`n"
    $xmlContent += " <security>" + "`r`n"
    $xmlContent += " <requestedPrivileges>" + "`r`n"
    $xmlContent += " <requestedExecutionLevel level=`"requireAdministrator`" uiAccess=`"false`" />" + "`r`n"
    $xmlContent += " </requestedPrivileges>" + "`r`n"
    $xmlContent += " </security>" + "`r`n"
    $xmlContent += " </trustInfo>" + "`r`n"
    $xmlContent += "</assembly>"

    $xmlContent | Out-File -FilePath $ManifestPath -Encoding UTF8

}


#*******************************************
#********************************************
#function to add capability
#*******************************************
#********************************************

Function Add-AlkanePSFCapability {
    Param(
        [string]$CapabilityName
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {
        $script:capabilitiesArray += $CapabilityName
    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


#*******************************************
#********************************************
#function to add dependency
#*******************************************
#********************************************

Function Add-AlkanePSFDependency {
    Param(
        [string]$DependencyName,
        [string]$DependencyMinVersion,
        [string]$DependencyPublisher
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {

        $script:dependenciesModel += ,([ordered]@{
            name=$DependencyName;
            minversion=$DependencyMinVersion;
            publisher=$DependencyPublisher;
        })

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


#*******************************************
#********************************************
#function to add protocol
#*******************************************
#********************************************

Function Add-AlkanePSFProtocol {
    Param(
        [string]$ApplicationId,
        [string]$ProtocolName,
        [string]$ProtocolParameters,
        [string]$ProtocolDisplayName,
        [string]$ProtocolLogo
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {

        $script:protocolsModel += ,([ordered]@{
            applicationid=$ApplicationId;
            name=$ProtocolName;
            parameters=$ProtocolParameters;
            displayName=$ProtocolDisplayName;
            logo=$ProtocolLogo
        })

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}


#*******************************************
#********************************************
#function to add protocol
#*******************************************
#********************************************

Function Add-AlkanePSFStringReplace {
    Param(
        [string]$FindRegex,
        [string]$ReplaceString
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    try {

        $script:stringReplaceModel += ,([ordered]@{
            find=$FindRegex;
            replace=$ReplaceString;
        })

    } catch {
        Write-AlkanePSFOutput "**ERROR** $($_)"
    }
}



#*******************************************
#********************************************
#Function to apply fixups by updating AppxManifest.xml and generating config.json
#*******************************************
#********************************************

function Set-AlkanePSF {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param(
        [bool]$OverwriteConfigJson=$true,
        [bool]$OpenConfigJson=$false
    )

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    if($PSCmdlet.ShouldProcess("Should process?")){

        try {

            Write-AlkanePSFOutput "Applying fixups to AppxManifest.xml and config.json."

            if (!(test-path $script:MSIXInputFilePath)) {
                Write-AlkanePSFOutput "**WARNING** Could not find $script:MSIXInputFilePath"
                return
            }

            if (!(test-path $script:MSIXCertificateFilePath)) {
                Write-AlkanePSFOutput "**WARNING** Could not find $script:MSIXCertificateFilePath"
                return
            }

            if (!(test-path $script:MSIXStagingFolderPath)) {
                Write-AlkanePSFOutput "**WARNING** Could not find $($script:MSIXStagingFolderPath)"
                return
            }

            #set the location of config.json and appxmanifest.xml to the staging folder
            $configJSON = "$($script:MSIXStagingFolderPath)config.json"
            $appxManifest    = "$($script:MSIXStagingFolderPath)AppxManifest.xml"

            #locate the bin folder containing PSF DLLs

            if ($PSFType -eq "MS") {
                Write-AlkanePSFOutput "Using Microsoft's PSF."
                $nupkg = Get-Package | Where-Object Name -eq "Microsoft.PackageSupportFramework" | Select-Object -ExpandProperty Source
                $psfLocation = (get-item $nupkg).Directory.FullName + "\bin"
            } else {
                Write-AlkanePSFOutput "Using Tim Mangan's PSF."
                $psfLocation = "$env:temp\TMPSF"
            }

            if (!(test-path $psfLocation)) {
                Write-AlkanePSFOutput "**ERROR** Could not find $psfLocation"
                return
            }

            #store all fixups
            $allfixups = Get-AlkanePSFFixup

            #psfLauncher files
            $psfLauncherExe = "PsfLauncher$script:MSIXArchitecture.exe"
            $psfLauncherDll = "PsfRuntime$script:MSIXArchitecture.dll"
            $psfRunExe = "PsfRunDll$script:MSIXArchitecture.exe"

            #fixup files
            $dynamicLibraryFixupDll = "DynamicLibraryFixup$script:MSIXArchitecture.dll"
            $fileRedirectionFixupDll = "FileRedirectionFixup$script:MSIXArchitecture.dll"
            $regLegacyFixupDll = "RegLegacyFixups$script:MSIXArchitecture.dll"
            $envVarFixupDll = "EnvVarFixup$script:MSIXArchitecture.dll"
            $mfrFixupDll = "MFRFixup$script:MSIXArchitecture.dll"
            $traceFixupDll = "TraceFixup$script:MSIXArchitecture.dll"

            #copy psfLauncher
            Copy-Item -Path "$psfLocation\$psfLauncherExe" -Destination "$($script:MSIXStagingFolderPath)" -Force
            Copy-Item -Path "$psfLocation\$psfLauncherDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            Copy-Item -Path "$psfLocation\$psfRunExe" -Destination "$($script:MSIXStagingFolderPath)" -Force

            #only used if we need elevation
            $psfManifest = "$($script:MSIXStagingFolderPath)PsfLauncher$script:MSIXArchitecture.exe.manifest"

            foreach($tempScript in $script:scriptArray) {
                if (test-path $tempScript) {
                    Write-AlkanePSFOutput "Copying $tempScript to $($script:MSIXStagingFolderPath)."
                    Copy-Item -Path "$tempScript" -Destination "$($script:MSIXStagingFolderPath)" -Force
                }
            }

            $pathToAppxManifest = "$($script:MSIXStagingFolderPath)AppxManifest.xml"

            #get a fresh AppxManifest in case we're running this multiple times
            try {
                Write-AlkanePSFOutput "Extracting a fresh AppxManifest from MSIX."

                $zip = [IO.Compression.ZipFile]::OpenRead($script:MSIXInputFilePath)
                $zip.Entries | Where-Object {$_.Name -eq 'AppxManifest.xml'} | ForEach-Object {
                    [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $pathToAppxManifest, $true)
                }
                $zip.Dispose()
            } catch {
                Write-AlkanePSFOutput "**ERROR** $($_)"
            }

            #Need to make sure manifest publisher is the same as our certificate
            $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
            $cert.Import($script:MSIXCertificateFilePath,$script:MSIXCertificatePassword,'DefaultKeySet')
            $certificateSubject = $cert.Subject

            #read from manifest
            [xml]$appInfo = Get-Content -Path $appxManifest
            $manifestChangesMade = $false

            #add capabilities

            #Define the namespace URI by retrieving it from the existing XML
            $namespaceURI = $appInfo.DocumentElement.GetNamespaceOfPrefix("rescap")

            foreach ($fixupCapability in $script:capabilitiesArray) {

                $existingCapability = $appInfo.Package.Capabilities.Capability | where-object Name -eq $fixupCapability

                if ($existingCapability) {
                    Write-AlkanePSFOutput "Capability $fixupCapability already exists."
                    break
                }

                # Create a new <rescap:Capability Name="xxx" /> element using the existing namespace
                $capabilityNode = $appInfo.CreateElement("rescap", "Capability", $namespaceURI)
                $capabilityNode.SetAttribute("Name", $fixupCapability)

                # Locate the <Capabilities> node, or create it if it doesn't exist
                $capabilitiesNode = $appInfo.DocumentElement.Capabilities
                if (-not $capabilitiesNode) {
                    $capabilitiesNode = $appInfo.CreateElement("Capabilities")
                }

                $capabilitiesNode.AppendChild($capabilityNode) | Out-Null
                $appInfo.DocumentElement.AppendChild($capabilitiesNode) | Out-Null
                Write-AlkanePSFOutput "Added $fixupCapability Capability"

                if ($fixupCapability -eq "allowElevation") {
                    Add-AlkanePSFManifest -ManifestPath $psfManifest
                }

                $manifestChangesMade = $true
            }


            #add dependencies
            $namespaceURI = $appInfo.DocumentElement.NamespaceURI

            foreach ($fixupdependency in $script:dependenciesModel) {

                $depName = $fixupdependency.name
                $depMinVersion = $fixupdependency.minversion
                $depPublisher = $fixupdependency.publisher

                $existingDependency = $appInfo.Package.Dependencies.PackageDependency | where-object Name -eq $depName
                if ($existingDependency) {
                    Write-AlkanePSFOutput "Dependency $depName already exists."
                    break
                }

                # Create a new <PackageDependency Name="xxx" /> element
                $dependencyNode = $appInfo.CreateElement("PackageDependency", $namespaceURI)
                $dependencyNode.SetAttribute("Name", $depName)
                $dependencyNode.SetAttribute("MinVersion", $depMinVersion)
                $dependencyNode.SetAttribute("Publisher", $depPublisher)

                # Locate the <Dependencies> node, or create it if it doesn't exist
                $dependenciesNode = $appInfo.DocumentElement.Dependencies
                if (-not $dependenciesNode) {
                    $dependenciesNode = $appInfo.CreateElement("Dependencies")
                }

                $dependenciesNode.AppendChild($dependencyNode) | Out-Null
                $appInfo.DocumentElement.AppendChild($dependenciesNode) | Out-Null
                Write-AlkanePSFOutput "Added $depName Dependency"

                $manifestChangesMade = $true
            }

            #add protocols

             #Define the namespace URI by retrieving it from the existing XML
            $namespaceURIUAP3 = $appInfo.DocumentElement.GetNamespaceOfPrefix("uap3")
            $namespaceURIUAP = $appInfo.DocumentElement.GetNamespaceOfPrefix("uap")

            foreach ($fixupprotocol in $script:protocolsModel) {

                $proAppId = $fixupprotocol.applicationid
                $proName = $fixupprotocol.name
                $proParameters = $fixupprotocol.parameters
                $proDisplayName = $fixupprotocol.displayName
                $proLogo = $fixupprotocol.logo

                $existingApp = $appInfo.Package.Applications.Application | where-object Id -eq $proAppId

                if ($null -ne $existingApp) {
                    #found app

                    $existingProtocol = $existingApp.Extensions.Extension.Protocol | where-object Name -eq $proName
                    if ($null -ne $existingProtocol) {
                        Write-AlkanePSFOutput "Protocol $proName already exists."
                        break
                    }

                    #not found protocol
                    $extensionsNode = $existingApp.Extensions
                    if (-not $extensionsNode) {
                        $extensionsNode = $appInfo.CreateElement("Extensions",$existingApp.NamespaceURI)
                        $existingApp.AppendChild($extensionsNode) | Out-Null
                    }

                    $extensionNode = $appInfo.CreateElement("uap3", "Extension", $namespaceURIUAP3)
                    $extensionNode.SetAttribute("Category", "windows.protocol") | Out-Null
                    $extensionsNode.AppendChild($extensionNode) | Out-Null

                    $protocolNode = $appInfo.CreateElement("uap3", "Protocol", $namespaceURIUAP3)
                    $protocolNode.SetAttribute("Name", $proName) | Out-Null

                    if ($null -ne $proParameters -and $proParameters -ne "") {
                        #cannot add if blank
                        $protocolNode.SetAttribute("Parameters", $proParameters) | Out-Null
                    }
                    $extensionNode.AppendChild($protocolNode) | Out-Null

                    $displayNameNode = $appInfo.CreateElement("uap", "DisplayName", $namespaceURIUAP)
                    $displayNameNode.InnerText = $proDisplayName
                    $protocolNode.AppendChild($displayNameNode) | Out-Null

                    $logoNode = $appInfo.CreateElement("uap", "Logo", $namespaceURIUAP)
                    $logoNode.InnerText = $proLogo
                    $protocolNode.AppendChild($logoNode) | Out-Null

                    Write-AlkanePSFOutput "Added $proName Protocol"

                    $manifestChangesMade = $true

                }


            }

            #remove apps

            foreach ($appid in $script:removeApplicationArray) {

                $applicationNode = $appInfo.Package.Applications.Application | where-object Id -eq $appid

                if ($applicationNode) {
                    # Get the parent node (Applications)
                    $parentNode = $applicationNode.ParentNode

                    # Remove the child node (the Application node)
                    $parentNode.RemoveChild($applicationNode) | Out-Null

                    Write-AlkanePSFOutput "Removed Application with Id $appid"

                    $manifestChangesMade = $true
                }
            }


            foreach ($shortcutName in $script:removeShortcutArray) {

                #escape special chars.
                $shortcutName = $shortcutName.Replace("[", "``[").Replace("]", "``]").Replace("*", "``*").Replace("?", "``?")

                $venodes = $appInfo.Package.Applications.Application.VisualElements | Where-Object DisplayName -like "*$shortcutName*"

                if ($venodes) {
                    foreach ($element in $venodes) {
                        Write-AlkanePSFOutput "Removing visual element $($element.DisplayName)"
                        $parentNode = $element.ParentNode
                        $parentNode.RemoveChild($element) | Out-Null

                        $manifestChangesMade = $true
                    }
                }

                $enodes = $appInfo.Package.Applications.Application.Extensions.Extension.Shortcut | Where-Object File -like "*$shortcutName*"

                if ($enodes) {
                    foreach ($element in $enodes) {
                        Write-AlkanePSFOutput "Removing shortcut $($element.File)"
                        $parentNode = $element.ParentNode
                        $parentNode.RemoveChild($element) | Out-Null

                        $manifestChangesMade = $true
                    }
                }

            }

            $identity = $appInfo.Package.Identity
            $identity.Publisher = $certificateSubject

            #get app list for config.json from Appxmanifest
            $applications = $appInfo.Package.Applications.Application

            if ($null -ne $applications) {

                #change executable in manifest to point to the PsfLauncher

                foreach ($fixupapp in $script:applicationsModel) {
                    $foundApp = $false
                    foreach ($manifestapp in $applications){
                        if ($manifestapp.id -eq $fixupapp.id) {
                            $foundApp = $true
                            $manifestapp.Executable = "$psfLauncherExe"
                            break
                        }
                    }

                    if (!$foundApp) {
                        $availableApps = ($applications.id -join " ")
                        Write-AlkanePSFOutput "**WARNING** Could not find application ID: $($fixupapp.id). When using Add-AlkanePSFApplication, your app id must be one of: $availableApps"
                    }
                }

                $manifestChangesMade = $true

            }

            if ($manifestChangesMade) {
                #save the appxmanifest.xml
                $appInfo.Save($appxManifest)
            }


            $manifestChangesMade = $false

            # Read the content of the file
            $fileContent = Get-Content -Path $appxManifest -Raw

            #string replacements
            foreach ($stringReplacement in $script:stringReplaceModel) {
                $findpattern = $stringReplacement.find
                $replace = $stringReplacement.replace

                try {
                    # Perform the regex replace
                    $fileContent = $fileContent -replace $findpattern, $replace
                    Write-AlkanePSFOutput "Replaced $findpattern with $replace"
                    $manifestChangesMade = $true
                } catch {
                    Write-AlkanePSFOutput "**ERROR** Replacing $findpattern with $replace. $_.Message"
                }

            }

            if ($manifestChangesMade) {
                #save the appxmanifest.xml
                Set-Content -Path $appxManifest -Value $fileContent
            }

            #generate config.json

            #copy required fixup DLLs, and removed DLLs not required
            $ApplyDynamicLibraryFixup = $false
            $ApplyFileRedirectionFixup = $false
            $ApplyEnvVarFixup = $false
            $ApplyRegLegacyFixup = $false
            $ApplyMFRFixup = $false
            $ApplyTraceFixup = $false

            foreach($fu in $allfixups) {
                 switch ($fu.dll) {
                    $dynamicLibraryDllName {$fu.dll="$dynamicLibraryFixupDll";$ApplyDynamicLibraryFixup=$true;}
                    $fileRedirectionDllName {$fu.dll="$fileRedirectionFixupDll";$ApplyFileRedirectionFixup=$true;}
                    $envVarDllName {$fu.dll="$envVarFixupDll";$ApplyEnvVarFixup=$true;}
                    $regLegacyDllName {$fu.dll="$regLegacyFixupDll";$ApplyRegLegacyFixup=$true;}
                    $mfrDllName {$fu.dll="$mfrFixupDll";$ApplyMFRFixup=$true;}
                    $traceDllName {$fu.dll="$traceFixupDll";$ApplyTraceFixup=$true;}
                }
            }


            if ($ApplyTraceFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$traceFixupDll."
                Copy-Item -Path "$psfLocation\$traceFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$traceFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$traceFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            if ($ApplyMFRFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$mfrFixupDll."
                Copy-Item -Path "$psfLocation\$mfrFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$mfrFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$mfrFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            if ($ApplyFileRedirectionFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$fileRedirectionFixupDll."
                Copy-Item -Path "$psfLocation\$fileRedirectionFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$fileRedirectionFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$fileRedirectionFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            if ($ApplyRegLegacyFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$regLegacyFixupDll."
                Copy-Item -Path "$psfLocation\$regLegacyFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$regLegacyFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$regLegacyFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            if ($ApplyEnvVarFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$EnvVarFixupDll."
                Copy-Item -Path "$psfLocation\$EnvVarFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$EnvVarFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$EnvVarFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            if ($ApplyDynamicLibraryFixup) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)$dynamicLibraryFixupDll."
                Copy-Item -Path "$psfLocation\$dynamicLibraryFixupDll" -Destination "$($script:MSIXStagingFolderPath)" -Force
            } else {
                Write-AlkanePSFOutput "Removing $($script:MSIXStagingFolderPath)$dynamicLibraryFixupDll."
                Remove-Item -Path "$($script:MSIXStagingFolderPath)$dynamicLibraryFixupDll" -Force -Recurse -ErrorAction SilentlyContinue
            }

            #copy script wrapper
            $scriptWrapper = "$psfLocation\StartingScriptWrapper.ps1"

            if (test-path($scriptWrapper)) {
                Write-AlkanePSFOutput "Adding $($script:MSIXStagingFolderPath)StartingScriptWrapper.ps1"
                Copy-Item -Path $scriptWrapper -Destination "$($script:MSIXStagingFolderPath)" -Force
            }

            $json = @{
                'applications' = $script:applicationsModel
                'processes' = $script:processesModel
            }

            #delete config.json if exists
            if ($OverwriteConfigJson -and (test-path $configJSON)) {
                Write-AlkanePSFOutput "Removing $configJSON."
                Remove-Item $configJSON -Force -Recurse -ErrorAction SilentlyContinue

                #write config.json
                 Write-AlkanePSFOutput "Creating $configJSON."
                $json | ConvertTo-Json -Depth 15 | Out-File -FilePath $configJSON
            } else {
                Write-AlkanePSFOutput "Will not overwrite $configJSON."
            }

            if ($OpenConfigJson) {
                Start-Process "notepad.exe" -ArgumentList $configJSON
            }

        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }
}

#*******************************************
#********************************************
#Function to create Resources.pri (when we manually add new PNG icons etc)
#*******************************************
#********************************************

function New-AlkanePSFResourcesPRI {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param()

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    if($PSCmdlet.ShouldProcess("Should process?")){
        try {
            Write-AlkanePSFOutput "Generating new Resources.pri."

            Write-AlkanePSFOutput "Finding MakePri.exe."
            $makePriPath = Get-WindowsSDKExe -ExeName "makepri.exe"

            if ($makePriPath -ne "") {
                Write-AlkanePSFOutput "Found at $makePriPath."

                Write-AlkanePSFOutput "Generating PriConfig.xml..."

                #Language in <Resource> node: en-US
                #Build for Win 10

                #make pri config
                $processArgs = @("createconfig","/cf","$($script:MSIXStagingFolderPath)PriConfig.xml","/dq","en-US","/pv","10.0.0","/o")
                $exitcode = (Start-AlkanePSFProcess -Exe $makePriPath -ExeArgs $processArgs)

                if ($exitcode -eq 0) {
                    Write-AlkanePSFOutput "Generated $($script:MSIXStagingFolderPath)PriConfig.xml with exit code $exitcode."
                } else {
                    Write-AlkanePSFOutput "**WARNING** Generating $($script:MSIXStagingFolderPath)PriConfig.xml with exit code $exitcode."
                }

                Write-AlkanePSFOutput "Generating Resources.pri..."

                #make pri config
                $processArgs = @("new","/pr","$($script:MSIXStagingFolderPath)","/cf","$($script:MSIXStagingFolderPath)PriConfig.xml","/mn","$($script:MSIXStagingFolderPath)AppxManifest.xml","/of","$($script:MSIXStagingFolderPath)Resources.pri","/o")
                $exitcode = (Start-AlkanePSFProcess -Exe $makePriPath -ExeArgs $processArgs)

                if ($exitcode -eq 0) {
                    Write-AlkanePSFOutput "Generated $($script:MSIXStagingFolderPath)Resources.pri with exit code $exitcode."
                } else {
                    Write-AlkanePSFOutput "**WARNING** Generating $($script:MSIXStagingFolderPath)Resources.pri with exit code $exitcode."
                }

            } else {
                Write-AlkanePSFOutput "Cannot find MakePri.exe."
            }
        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }

}

#*******************************************
#********************************************
#Function to create and sign a new MSIX
#*******************************************
#********************************************

function New-AlkanePSFMSIX {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    Param()

    if (!($script:alkanePSFConfigured)) {
        Write-AlkanePSFOutput "**ERROR** AlkanePSF not configured. You must first call Set-AlkanePSFConfiguration."
        return
    }

    if($PSCmdlet.ShouldProcess("Should process?")){
        try {
            Write-AlkanePSFOutput "Compiling fixed MSIX package."

            if (!(test-path $script:MSIXCertificateFilePath)) {
                Write-AlkanePSFOutput "**WARNING** Could not find $script:MSIXCertificateFilePath"
                return
            }

            if (test-path $script:MSIXOutputFilePath) {
                Write-AlkanePSFOutput "Removing $script:MSIXOutputFilePath."
                Remove-Item $script:MSIXOutputFilePath -Force -Recurse -ErrorAction SilentlyContinue
            }

            Write-AlkanePSFOutput "Finding MakeAppx.exe."
            $makeAppxPath = Get-WindowsSDKExe -ExeName "makeappx.exe"

            if ($makeAppxPath -ne "") {
                Write-AlkanePSFOutput "Found at $makeAppxPath."

                Write-AlkanePSFOutput "Finding Signtool.exe."
                $signToolPath = Get-WindowsSDKExe -ExeName "signtool.exe"

                if ($signToolPath -ne "") {
                    Write-AlkanePSFOutput "Found at $signToolPath."

                    Write-AlkanePSFOutput "Compiling package to $script:MSIXOutputFilePath."

                    # Extract the directory path from the file path
                    $directoryPath = [System.IO.Path]::GetDirectoryName($script:MSIXOutputFilePath)

                    # Check if the directory exists, and create it if it doesn’t
                    if (!(Test-Path -Path $directoryPath)) {
                        New-Item -ItemType Directory -Path $directoryPath -Force | Out-Null
                    }

                    $processArgs = @("pack","/p","$script:MSIXOutputFilePath","/d","$($script:MSIXStagingFolderPath)")
                    $exitcode = (Start-AlkanePSFProcess -Exe $makeAppxPath -ExeArgs $processArgs)

                    if ($exitcode -eq 0) {
                        Write-AlkanePSFOutput "Package compiled to $script:MSIXOutputFilePath with exit code $exitcode."
                    } else {
                        Write-AlkanePSFOutput "**WARNING** Package compiled to $script:MSIXOutputFilePath with exit code $exitcode."
                    }

                    Write-AlkanePSFOutput "Signing $script:MSIXOutputFilePath."
                    $processArgs = @("sign","/tr","""http://timestamp.digicert.com""","/v","/fd","sha256","/td","sha256","/f","""$script:MSIXCertificateFilePath""","/p","""$script:MSIXCertificatePassword""","""$script:MSIXOutputFilePath""")
                    $exitcode = (Start-AlkanePSFProcess -Exe $signToolPath -ExeArgs $processArgs)

                    if ($exitcode -eq 0) {
                        Write-AlkanePSFOutput "Signed $script:MSIXOutputFilePath with exit code $exitcode."
                    } else {
                        Write-AlkanePSFOutput "**WARNING** Signed $script:MSIXOutputFilePath with exit code $exitcode."
                    }

                } else {
                    Write-AlkanePSFOutput "Cannot find Signtool.exe."
                }
            } else {
                Write-AlkanePSFOutput "Cannot find MakeAppx.exe."
            }

        } catch {
            Write-AlkanePSFOutput "**ERROR** $($_)"
        }
    }

}