<#PSScriptInfo .VERSION 2026.8.14.0 .GUID 3a7b9c4d-2e8f-4a1b-9d6c-5e3f7a8b9c2d .AUTHOR Michael Escamilla .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI https://github.com/MichaelEscamilla/GetMSIInformation .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.0.0.0 - Initial release 2.0.0.0 - 10-4-2024 - Added file hash information, and context menu items for the installation and uninstallation of a Right-Click Option in Windows Explorer. And some other UI improvements. 2024.10.4.1 - Updated the version numbering, and a sepearator in the context menu. 2024-10.13.0 - Added an error message when the file is locked 2024-12.8.0 - Formatted Script for Publishing to PowerShell Gallery 2026-1.5.0 - Added Icon extraction and export functionality. Added context menu items to open the icon temp folder and right-click menu folder. 2026.1.5.1 - Fixed a bug when launching the script from the internet 2026.1.6.0 - Fixed a bug where the 'No Icon' label would not hide 2026.1.6.1 - Added the michaeltheadmin.com icon to the Form and Right-Click Menu 2026.1.7.0 - Simplified console output messages during script download from GitHub 2026.8.11.0 - Updated UI for less 'default' look. Added the Compressed Product Code (Compressed GUID) to the GUI. Right-Click Install will directly call pwsh.exe if available. 2026.8.12.0 - Added a check for updates feature. Manual check available under the 'About' menu. A non-blocking background check runs at startup and notifies via the status bar when a newer release is available. 2026.8.12.1 - Added Logic to Update the script depending on the way it was launched. 2026.8.12.2 - Added status bar feedback when installing or removing the right-click menu. The 'latest version' confirmation now shows in the status bar instead of a pop-up. 2026.8.13.0 - Added screenshot options under the File menu to copy the window to the clipboard or save it as a PNG. Fixed the drag-and-drop box staying highlighted gray after being clicked. Property fields now show the accent outline on hover instead of staying outlined after clicking. 2026.8.14.0 - The right-click menu now works on any file type to get hash information, not just MSI files. Fixed repeating errors when the status bar message tried to reset. The window now comes to the foreground faster on the first launch in a session. .PRIVATEDATA #> <# .SYNOPSIS This script provides a graphical user interface (GUI) for viewing and copying properties of MSI files. .DESCRIPTION The script creates a WPF-based GUI that allows users to drag and drop MSI files to view their properties such as Product Name, Manufacturer, Product Version, Product Code, and Upgrade Code. It also provides functionality to copy these properties to the clipboard and to clear the displayed information. Additionally, the script includes options to install and uninstall a context menu item for MSI files to retrieve their properties. .PARAMETER FilePath Optional parameter to specify the path of the MSI file to automatically load the information for. .NOTES #> param ( [Parameter(Mandatory = $false)] [string]$FilePath ) ############################################# ################# Variables ################# ############################################# # Script Name $Script:ScriptName = "GetMSIInformation.ps1" # Script Version [System.Version]$Script:ScriptVersion = "2026.8.14.0" $Script:RightClickMenuName = "Get MSI Information" $Script:RightClickMenuFolderPath = "$env:LOCALAPPDATA\GetMSIInformation" # Icon Temp Folder Path $Script:IconTempFolderPath = "$env:TEMP\GetMSIInformation\Icons" # GitHub Repository (used for the update check) $Script:GitHubRepo = "MichaelEscamilla/GetMSIInformation" $Script:ReleasesApiUrl = "https://api.github.com/repos/$Script:GitHubRepo/releases/latest" $Script:ReleasesPageUrl = "https://github.com/$Script:GitHubRepo/releases" # Headers required by the GitHub REST API (a User-Agent is mandatory) $Script:UpdateCheckHeaders = @{ 'User-Agent' = 'GetMSIInformation-UpdateCheck' 'Accept' = 'application/vnd.github+json' } # How this script was launched; drives which update action the 'Update Available' click takes. $Script:UpdateChannel = $null # Get the Security Principal $Script:currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) # Get PowerShell Version $Script:ScriptPSVersion = $PSVersionTable.PSVersion # Get Pwsh Path $Script:PowerShellPath = (Get-Command pwsh.exe -ErrorAction SilentlyContinue) # michaeltheadmin.com Icon $Script:WindowIconBase64 = "AAABAAEAIBwAAAEAIACYDgAAFgAAACgAAAAgAAAAOAAAAAEAIAAAAAAAcA4AAMQOAADEDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaSFoNRkZGEUtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEFRGRhFaSEgNAAAAAAAAAABUVFQLUkxPrFNOUOlRTE7tUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7VROUOhSTU+qTExMCU5KTH5lYWP/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bmxt/2RfYf9OSkx6TkhLtWtoaf94eHj/dnZ2/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3Z2dv94eHj/aWZn/05IS69OSUuxamdo/3h4eP9mZGX/Uk1P/05JS/9OSUv/VVFT/1VRUv9OSUv/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/TklL/1VRU/9VUVP/TklL/05JS/9STlD/ZmRl/3h4eP9oZWb/T0lMqk9JTLBqZ2j/eHl4/1dUVf90cHH/rqys/62rrP9rZ2j/b2tt/66srP+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+tq6z/a2do/29rbP+urKz/rays/3Bsbv9YVVb/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/U1BR/5GPkP/09PT/8/Pz/399ff+Hg4X/9PT0/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo//Pz8/9/fX3/hoOE//T09P/z9PP/i4iJ/1VRUv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9VUlP/hIGC/9PS0v/S0dH/dXJz/3t3ef/T0tL/ycjI/8rJyv/Kycr/ycjI/8nIyP/Kycr/ysnK/8nIyP/JyMj/ysnK/8vKyf/Lycj/1NLR/3dzc/98eHj/1dPS/9TT0f9/fHz/V1NU/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/19cXv9TTU//V1JU/1dSVP9TTlD/VE9R/1dSVP9XUlT/V1NV/1dTVf9XUlT/V1JU/1dTVf9XU1X/V1JU/1dSVP9XU1X/WFNU/1hSVP9ZU1T/VU9Q/1VPUP9ZU1T/WVNU/1JNTv9gXV7/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/V1NV/3x4ef/Hx8j/v76//8HAwf/BwMD/v76//8jHx/9ybnD/dnN0/8jIyP/Ix8f/cm9w/3Vxc//Ix8j/yMfI/4B2cP85VYL/LXrf/y933P8xed7/MXne/y933P8tet7/Q1p//2NaU/94eXn/aGVm/09JTKpPSUywamdo/3h5eP9UUFL/jImK//L09P/n6Oj/5+jo/+fo6P/n6Oj/8/Pz/4B9f/+Gg4T/9PT0//L08/+Afn//hICC//L09P/y9PT/k4h//zRblv8jjf//J4n//yeJ//8nif//J4n//yON//9AYJL/Y1lQ/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/1hUVv95dHP/v7u3/7izr/+3tLP/tLO0/7GwsP+5t7j/cGxu/3Rwcf+5uLj/ubi4/3Bsbv9ybnD/ubi4/7m4uP98c27/PVV9/y5zzf8xcMr/NHTO/zR0zv8xcMr/LnPN/0RXef9jWlX/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFxd/01LUv9ITVz/SE5d/0pJUP9TTU7/XFdZ/1tXWf9VUVL/VlFT/1tXWf9bV1n/VVFT/1ZRU/9cV1n/XFdZ/1VQU/9ZU1L/ZFtY/2RbV/9ZUEz/WlFN/2RbWP9kW1j/VlBQ/19cXv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9jWFH/QF+Q/ymE+f8phPn/OFiJ/4yCe//e3t7/3d3d/3p2eP9/fH3/3t7e/93e3f96d3j/fXl7/93e3v/d3t7/fXp7/4B9fv/f3t7/3t3d/3p2d/9/fH3/397e/97e3f+DgIH/VlJT/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/2NYT/8+Ypr/JI3//ySN//81WpL/lYuC//Pz8//y8vL/gHx+/4aDhP/z8/P/8vPy/4B9fv+DgIL/8vPz//Lz8/+DgYL/h4SF//Pz8//y8vL/f3x9/4aDhP/z8/P/8vPy/4qIiP9VUVL/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFhV/0ZTbv87aaj/O2mo/0NRbP9waGX/m5iZ/5uYmf9mYWP/aGRm/5uYmf+bmJn/ZmJj/2djZf+bmJn/m5iZ/2djZf9pZWb/m5iZ/5uYmf9lYWL/amZo/6Cdnv+fnZ7/a2Zo/1lVV/94eXn/aGVm/09JTKpOSUuxamdo/3d4eP9nZmj/XFVT/2BUTP9gVEz/X1hV/1dUVv9QTE7/UExO/1lVV/9ZVVb/UExO/1BMTv9ZVVf/WVVW/1BMTv9QTE7/WVVW/1hVVv9QTE7/UExO/1lVV/9ZVVb/UE1O/1FNTv9WUlT/aWdo/3d4eP9oZWb/TkhLq09JTLNraGn/eHl5/3d3d/93eHj/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4eP93d3f/eHl4/2hmZ/9OSEuuUUpMbWRfYf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/amhp/2poaf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9samv/Yl1f/1BLS2hRUVECUUtNjFNNT89QS0zTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BLTdFQS03yUEtN8FBLTdFQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEtM01NNT89RTE6IfHx8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNMT7lTTVCmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU01Qs1JNT7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABTTU1RWFNV/1NOUIhUTE9pVFBSaExMTBYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRTE9mVlFS11NOUOJWUVLdUEtQNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////wAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//n////5////+B////wf///////////8=" ############################################# ################# Functions ################# ############################################# #region Functions function Test-FileLock { param ( [Parameter(Mandatory = $true)] [string]$Path ) try { $FileStream = [System.IO.File]::Open("$($Path)", 'Open', 'Write') $FileStream.Close() $FileStream.Dispose() return $false } catch { return $true } } function Get-MsiProperties { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path ) Write-Host "Getting MSI Properties for: [$Path]" # Check if the MSI file path exists if (-not (Test-Path $Path)) { throw "The file $Path does not exist." } # Create a new Windows Installer COM object $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer # Open the MSI database in read-only mode $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) # Open a view on the Property table $MSIPropertyView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT * FROM Property")) # Execute the view query $MSIPropertyView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null # Fetch the first record from the result set $MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null) # Initialize an empty System Object to store properties [System.Object]$Properties = @{} # Loop through all records in the result set while ($null -ne $MSIRecord) { # Get the property name from the first column $property = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(1)) # Get the property value from the second column $Value = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(2)) # Add the property name and value to the hashtable $Properties[$Property] = $Value # Fetch the next record from the result set $MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null) } # Close the Property view $MSIPropertyView.GetType().InvokeMember("Close", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null # Return the System Object of properties $Properties } # Stolen from: https://github.com/codaamok # https://gist.github.com/codaamok/7ed30d01280ce28bb451621966707c1b function Convert-ProductCodeToCompressedGuid { param( [Parameter(Mandatory = $true)] [string]$ProductCode ) function Get-ReversedString ([array]$a) { [String]::Join('', $a[-1.. - ($a.Count)]) } function Get-ReversedBytes ([String]$a) { [String]::Join('', ($a -split '(..)' -ne '' -replace '(\w)(\w)', '$2$1')) } # Strip braces and dashes from the GUID $ProductCode = $ProductCode -replace '\{|\}|\-' $data1 = Get-ReversedString $ProductCode[0..7] $data2 = Get-ReversedString $ProductCode[8..11] $data3 = Get-ReversedString $ProductCode[12..15] $data4 = Get-ReversedBytes ($ProductCode[16..19] -join '') $data5 = Get-ReversedBytes ($ProductCode[20..31] -join '') return '{0}{1}{2}{3}{4}' -f $data1, $data2, $data3, $data4, $data5 } function Enable-AllButtons { param ( [Parameter(Mandatory = $false)] [string]$Exclude ) # Get all button variables $Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Button in $Buttons) { # Check that the button name does not contain the exclude variable if ($Button.Name -notlike "*$Exclude*") { # Enable Button $Button.IsEnabled = $true } } } function Disable-AllButtons { # Get all button variables $Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Button in $Buttons) { # Disable Button $Button.IsEnabled = $false } } function Clear-Textboxes { # Get all textbox variables $Textboxes = Get-Variable -Name "txt_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Textbox in $Textboxes) { # Clear textbox $Textbox.Clear() } } # Stolen from: https://github.com/PatchMyPCTeam/CustomerTroubleshooting/blob/Release/PowerShell/Get-LocalContentHashes.ps1 function Get-EncodedHash { [CmdletBinding()] Param( [Parameter(Position = 0)] [System.Object]$HashValue ) $hashBytes = $hashValue.Hash -split '(?<=\G..)(?=.)' | ForEach-Object { [byte]::Parse($_, 'HexNumber') } Return [Convert]::ToBase64String($hashBytes) } function Get-FileHashInformation { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path ) Write-Host "Getting File Hash Information for: [$Path]" # Initialize the hash object $Hashes = @{} # Get File Hash - MD5 $FileHashMD5 = Get-FileHash -Path $Path -Algorithm MD5 $Hashes["MD5"] = $FileHashMD5 # Get File Hash - SHA1 $FileHashSHA1 = Get-FileHash -Path $Path -Algorithm SHA1 $Hashes["SHA1"] = $FileHashSHA1 # Get File Hash - SHA256 $FileHashSHA256 = Get-FileHash -Path $Path -Algorithm SHA256 $Hashes["SHA256"] = $FileHashSHA256 # Get File Hash - SHA1 - Encoded $FileHashEncoded = Get-EncodedHash -HashValue $FileHashSHA1 $Hashes["Digest"] = $FileHashEncoded # Get File Hash - SHA256 - Encoded $FileHashSHA256Encoded = Get-EncodedHash -HashValue $FileHashSHA256 $Hashes["D256"] = $FileHashSHA256Encoded # Return the hash object $Hashes } function Get-MsiIcon { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path, [Parameter(Mandatory = $false)] [string]$ExportFolder = "$($Script:IconTempFolderPath)" ) Write-Host "Getting MSI Icon for: [$Path]" # Ensure the export folder exists if (-not (Test-Path -Path $ExportFolder)) { New-Item -ItemType Directory -Path $ExportFolder -Force | Out-Null } # Create a new Windows Installer COM object $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer # Open the MSI database in read-only mode $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) # Get all Icons in the Icon table try { # Open a view on the Icon table to get the icon binary data $IconView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name FROM _Tables WHERE Name='Icon'")) # Execute the view query $IconView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconView, $null) | Out-Null # Fetch Record $IconTable = $IconView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconView, $null) # Close the Icon view $IconView.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconView, $null) | Out-Null if ($IconTable) { # Open a view on the Icon table $IconData = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name,Data FROM Icon")) # Execute the view query $IconData.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconData, $null) | Out-Null # Start Building PSCustomObject [Collections.Generic.List[PSCustomObject]]$IconInformation = @() Do { # Fetch the next record $IconRecord = $IconData.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconData, $null) if ($IconRecord) { # Get the 'Name' Field $IconDataName = $IconData.GetType().InvokeMember("StringData", 'Public, Instance, GetProperty', $null, $IconRecord, 1) # Get the DataSize of the Binary Data $IconDataSize = $IconData.GetType().InvokeMember("DataSize", "GetProperty", $null, $IconRecord, 2) Write-Verbose "Found Icon: [$IconDataName] with Size: [$IconDataSize] bytes" # Read the Binary Data $IconBinaryData = $IconData.GetType().InvokeMember("ReadStream", "InvokeMethod", $null, $IconRecord, @(2, $IconDataSize, 2)) # Get Binary data as ANSI string - use Windows-1252 encoding $ByteArray = [System.Text.Encoding]::GetEncoding(1252).GetBytes($IconBinaryData) # Construct the path to save the ICO file $IconPathTemp = Join-Path -Path $ExportFolder -ChildPath "$($IconDataName)" # Check if the file already exists and delete it if necessary if (Test-Path -Path $IconPathTemp) { Remove-Item -Path $IconPathTemp -Force } # Write the byte array to an ICO file [System.IO.File]::WriteAllBytes($IconPathTemp, $ByteArray) # Create PSCustomObject for Icon Information $IconInfoObject = [PSCustomObject]@{ Name = $IconDataName Size = $IconDataSize Path = $IconPathTemp } # Add to Icon Information List $IconInformation.Add($IconInfoObject) } } While ($IconRecord) # Close the IconData view $IconData.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconData, $null) | Out-Null } } catch { Write-Verbose "Error retrieving Icons: $_" } Return $IconInformation } function Set-TextboxInformation { param ( [Parameter(Mandatory = $false)] [System.Object]$MSIPropertiesInfo, [Parameter(Mandatory = $true)] [hashtable]$FileHashInfo ) # Set the MSI file properties textboxes (only when MSI properties are provided) if ($MSIPropertiesInfo) { $txt_ProductName.Text = $MSIPropertiesInfo.ProductName $txt_Manufacture.Text = $MSIPropertiesInfo.Manufacturer $txt_ProductVersion.Text = $MSIPropertiesInfo.ProductVersion $txt_ProductCode.Text = $MSIPropertiesInfo.ProductCode $txt_UpgradeCode.Text = $MSIPropertiesInfo.UpgradeCode # Set the Compressed GUID from the Product Code if ($MSIPropertiesInfo.ProductCode) { $txt_CompressedGUID.Text = Convert-ProductCodeToCompressedGuid -ProductCode $MSIPropertiesInfo.ProductCode } } # Set the File Hash Information textboxes $txt_MD5.Text = $FileHashInfo.MD5.Hash $txt_SHA1.Text = $FileHashInfo.SHA1.Hash $txt_SHA256.Text = $FileHashInfo.SHA256.Hash $txt_Digest.Text = $FileHashInfo.Digest $txt_D256.Text = $FileHashInfo.D256 } function Build-IconImageControls { param ( [Parameter(Mandatory = $true)] [PSCustomObject[]]$IconObjects ) # Start Building PSCustomObject [Collections.Generic.List[PSCustomObject]]$Script:IconImageControlsList = @() # Loop through each Icon $IconIndex = 0 foreach ($IconObject in $IconObjects) { if ((Test-Path ($IconObject.Path))) { # IconBitmap Export File Path $IconBitmapPath = Join-Path -Path "$($IconObject.Path | Split-Path -Parent)" -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name))_Export.ico" # Extract the Binary file to a Bitmap File at 256px if ($Script:ScriptPSVersion -lt [Version]"7.4") { # This is the only method available in .NET 4.x.x $IconExtract = [System.Drawing.Icon]::ExtractAssociatedIcon($IconObject.Path) } else { # This uses newer .NET methods from 8+ and results in higher quality images $IconExtract = [System.Drawing.Icon]::ExtractIcon($IconObject.Path, 0, 256) } # Delete any existing ICO file #Remove-Item -Path "$($IconBitmapPath)" -Force -ErrorAction SilentlyContinue | Out-Null # Save the Extracted icon to a bitmap file $IconExtract.ToBitmap().Save("$($IconBitmapPath)") # Create BitmapImage from the Bitmap file $Bitmap = New-Object System.Windows.Media.Imaging.BitmapImage $Bitmap.BeginInit() $Bitmap.StreamSource = [System.IO.MemoryStream]::new([System.IO.File]::ReadAllBytes("$($IconBitmapPath)")) $Bitmap.CacheOption = [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad $Bitmap.EndInit() $Bitmap.Freeze() # Create a New Image Control in the Grid 'grid_Icon' $ImageControlIcon = New-Object System.Windows.Controls.Image $ImageControlIcon.SetValue([System.Windows.Controls.Control]::NameProperty, "img_Icon_$(([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name)) -replace '[^a-zA-Z0-9]', '_')") $ImageControlIcon.Source = $Bitmap $ImageControlIcon.SetValue([System.Windows.Controls.Grid]::RowProperty, 0) $ToolTip = New-Object System.Windows.Controls.ToolTip if ($Script:ScriptPSVersion -lt [Version]"7.4") { $ToolTip.Content = "Click to Export.`nUse PowerShell 7.4 or higher for better quality." $ToolTip.Background = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Colors]::LightGoldenrodYellow) $ToolTip.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Colors]::Red) } else { $ToolTip.Content = "Click to Export" } $ImageControlIcon.ToolTip = $ToolTip $ImageControlIcon.SetValue([System.Windows.Controls.ToolTipService]::InitialShowDelayProperty, 100) $ImageControlIcon.HorizontalAlignment = "Center" $ImageControlIcon.VerticalAlignment = "Center" $ImageControlIcon.Visibility = "Collapsed" $ImageControlIcon.add_mouseleftbuttonup($Button_ExportToPNG_Handler) # Add the Image Control to the Grid 'grid_Icon' $grid_Icon.Children.Add($ImageControlIcon) # Create PSCustomObject for the Image Control Information $ImageControlIconInfo = [PSCustomObject]@{ Index = $IconIndex Name = $ImageControlIcon.Name IconName = "$([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name))" IconBinaryPath = "$($IconObject.Path)" IconBitmapPath = "$($IconBitmapPath)" Control = $ImageControlIcon } # Add to IconImageControlsList $Script:IconImageControlsList.Add($ImageControlIconInfo) } # Increment the Icon Index $IconIndex++ } } function Set-IconImageNavigation { [CmdletBinding(DefaultParameterSetName = 'New')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'New')] [Parameter(Mandatory = $true, ParameterSetName = 'Next')] [Parameter(Mandatory = $true, ParameterSetName = 'Previous')] [PSCustomObject[]]$IconObjects, [Parameter(Mandatory = $false, ParameterSetName = 'Next')] [switch]$Next, [Parameter(Mandatory = $false, ParameterSetName = 'Previous')] [switch]$Previous ) # IconImageControlsList Count $IconCount = $Script:IconImageControlsList.Count # Get the Current Icon Index if ($null -eq $Script:CurrentIconIndex) { $Script:CurrentIconIndex = 0 } $CurrentIndex = $Script:CurrentIconIndex if ($Next) { # Increment the Index $NewIndex = $CurrentIndex + 1 } elseif ($Previous) { # Decrement the Index $NewIndex = $CurrentIndex - 1 } else { # If neither Next nor Previous is specified, keep the current index $NewIndex = $CurrentIndex } # Set GlobalIndex $Script:CurrentIconIndex = $NewIndex # Hide the 'No Icon' Label $lbl_NoIcon.Visibility = "Collapsed" # Hide the current Icon ($Script:IconImageControlsList | Where-Object { $_.Index -eq $CurrentIndex }).Control.Visibility = "Hidden" # Show the new Icon ($Script:IconImageControlsList | Where-Object { $_.Index -eq $NewIndex }).Control.Visibility = "Visible" # Set the Navigation button states if ($IconCount -gt 1) { if ($NewIndex -eq 0) { $btn_IconPrevious.IsEnabled = $false $btn_IconNext.IsEnabled = $true } elseif ($NewIndex -eq ($IconCount - 1)) { $btn_IconPrevious.IsEnabled = $true $btn_IconNext.IsEnabled = $false } else { $btn_IconPrevious.IsEnabled = $true $btn_IconNext.IsEnabled = $true } } } function Set-IconImage { param ( [Parameter(Mandatory = $true)] [PSCustomObject[]]$IconObjects ) # Build the Icon Image Controls Build-IconImageControls -IconObjects $IconObjects # Set the Icon Image Navigation Set-IconImageNavigation -IconObjects $Script:IconImageControlsList } function Clear-IconImageControls { # Clear the Script Scoped Variables Remove-Variable -Name IconImageControlsList -Scope Script -ErrorAction SilentlyContinue Remove-Variable -Name CurrentIconIndex -Scope Script -ErrorAction SilentlyContinue # Reload all Icon Image Controls on the form into variables $formMSIProperties.FindName("grid_Icon").Children | Where-Object { $_.Name -like "img_Icon*" } | ForEach-Object { Set-Variable -Name $_.Name -Value $_ -Scope Script } # Get all Icon Image Controls that start with the name 'img_Icon' and Hide them $IconImageControls = Get-Variable -Name "img_Icon*" -Scope Script -ErrorAction SilentlyContinue if ($null -ne $IconImageControls) { foreach ($IconImageControl in $IconImageControls) { $IconImageControl.Value.Visibility = "Collapsed" } } # Show the No Icon Label Visibility $lbl_NoIcon.Visibility = "Visible" # Disable the Icon Navigation buttons $btn_IconPrevious.IsEnabled = $false $btn_IconNext.IsEnabled = $false } function Invoke-FormReset { # Clear the Icon Image Controls Clear-IconImageControls # Clear the Textboxes Clear-Textboxes # Clear and Reset the listbox font style $lsbox_FilePath.Items.Clear() $lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::BackgroundProperty) $lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::ForegroundProperty) $lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::FontWeightProperty) $lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::FontSizeProperty) # Forget the loaded MSI so a relaunch (update) doesn't pass a stale path $Script:LoadedMSIPath = $null # Disable all buttons Disable-AllButtons } function Invoke-GetMSIInformation { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$MSIPath ) # Reset the form Invoke-FormReset # Get the File Hash Information for any file type $HashInfo = Get-FileHashInformation -Path $MSIPath # Check if the file is an MSI file $IsMSI = ([System.IO.Path]::GetExtension($MSIPath[0])) -eq ".msi" if ($IsMSI) { # Get the MSI file properties $FileMSIInfo = Get-MsiProperties -Path $MSIPath # Populate the textboxes with the MSI properties and hash information Set-TextboxInformation -MSIPropertiesInfo $FileMSIInfo -FileHashInfo $HashInfo # Extract and display the Icons $IconObjects = Get-MsiIcon -Path $MSIPath if ($null -ne $IconObjects) { Set-IconImage -IconObjects $IconObjects } # Enable the Copy buttons Enable-AllButtons -Exclude "Icon" } else { # Populate only the hash textboxes for non-MSI files Set-TextboxInformation -FileHashInfo $HashInfo # Enable only the Hash and FilePath Copy buttons foreach ($ButtonName in @("btn_MD5_Copy", "btn_SHA1_Copy", "btn_SHA256_Copy", "btn_Digest_Copy", "btn_D256_Copy", "btn_FilePath_Copy")) { (Get-Variable -Name $ButtonName -ValueOnly -ErrorAction SilentlyContinue).IsEnabled = $true } } # Clear the listbox and add the filename $lsbox_FilePath.Items.Clear() $lsbox_FilePath.Items.Add($MSIPath[0]) # Remember the loaded file so an update relaunch can reopen it $Script:LoadedMSIPath = $MSIPath[0].FullName # Remove lock on current file [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() } function Invoke-LaunchAsPwsh { # Are we running as PowerShell 7.4 or higher if ($Script:ScriptPSVersion -ge [Version]"7.4") { Write-Host "Running PowerShell 7.4 or higher" } else { # Check that PowerShell is installed if ($Script:PowerShellPath) { # Check that it is version 7.4 or higher if ($Script:PowerShellPath.Version -lt [Version]"7.4") { Write-Host "Installed PowerShell (pwsh.exe) version [$($Script:PowerShellPath.Version)] is lower than 7.4. Continuing with existing PowerShell version [$($Script:ScriptPSVersion)]..." } else { # Start PowerShell and run the script Write-Host "Relaunching script in PowerShell (pwsh.exe)..." if ($PSCommandPath -ne "") { Start-Process -FilePath $Script:PowerShellPath -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$PSCommandPath`" -FilePath `"$FilePath`"" } else { Start-Process -FilePath $Script:PowerShellPath -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -Command `"Invoke-Expression (Invoke-RestMethod 'https://raw.githubusercontent.com/MichaelEscamilla/GetMSIInformation/main/GetMSIInformation.ps1')`"" } Exit } } else { Write-Host "PowerShell (pwsh.exe) is not installed on this system." } } } # Authored as a scriptblock because it's passed to a runspace). $Script:TestForUpdate = { param( [string]$ApiUrl, [hashtable]$Headers, [System.Version]$CurrentVersion ) $release = Invoke-RestMethod -Uri $ApiUrl -Headers $Headers -TimeoutSec 10 -ErrorAction Stop $latestVersion = [System.Version]($release.tag_name -replace '^v', '') [PSCustomObject]@{ UpdateAvailable = ($latestVersion -gt $CurrentVersion) LatestVersion = $latestVersion HtmlUrl = $release.html_url Tag = $release.tag_name } } function Start-BackgroundUpdateCheck { [CmdletBinding()] param([switch]$Manual) # Don't start a second check while one is still running (e.g. manual click during the startup check). if ($Script:UpdateHandle -and -not $Script:UpdateHandle.IsCompleted) { return } $Script:UpdateCheckManual = $Manual.IsPresent # Detect the distribution channel once so the update click is a cheap lookup. if (-not $Script:UpdateChannel) { $Script:UpdateChannel = Get-UpdateChannel } try { $Script:UpdatePowerShell = [powershell]::Create() $Script:UpdatePowerShell.AddScript($Script:TestForUpdate). AddArgument($Script:ReleasesApiUrl). AddArgument($Script:UpdateCheckHeaders). AddArgument($Script:ScriptVersion) | Out-Null $Script:UpdateHandle = $Script:UpdatePowerShell.BeginInvoke() $Script:UpdateTimer = New-Object System.Windows.Threading.DispatcherTimer $Script:UpdateTimer.Interval = [TimeSpan]::FromMilliseconds(500) $Script:UpdateTimer.Add_Tick({ if (-not $Script:UpdateHandle.IsCompleted) { return } $Script:UpdateTimer.Stop() try { $result = $Script:UpdatePowerShell.EndInvoke($Script:UpdateHandle) | Select-Object -First 1 Write-Host "Background update check: UpdateAvailable=$($result.UpdateAvailable) | Installed [$($Script:ScriptVersion)] | Latest [$($result.LatestVersion)]" Show-UpdateAvailable -Result $result # Manual check only: confirm when already current (an available update is surfaced by the menu item). if ($Script:UpdateCheckManual -and -not $result.UpdateAvailable) { Set-StatusMessage -Message "You're running the latest version ($($Script:ScriptVersion))." -Type Success } } catch { Write-Host "Background update check failed: $($_.Exception.Message)" if ($Script:UpdateCheckManual) { [System.Windows.MessageBox]::Show("Update check failed:`n$($_.Exception.Message)", "Check for Updates", 'OK', 'Warning') | Out-Null } } finally { $Script:UpdatePowerShell.Dispose() } }) $Script:UpdateTimer.Start() } catch { Write-Host "Unable to start background update check: $($_.Exception.Message)" } } function Show-UpdateAvailable { # Reveals the 'Update Available' menu item when a newer release exists, so both the background and # manual checks surface the result the same way. Remembers the download link for the click handler. [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Result ) if (-not $Result.UpdateAvailable) { return } $Script:LatestReleaseUrl = if ($Result.HtmlUrl) { $Result.HtmlUrl } else { $Script:ReleasesPageUrl } $Script:LatestReleaseTag = $Result.Tag $MenuItem_UpdateAvailable.Header = "Update Available: $($Result.LatestVersion)" $MenuItem_UpdateAvailable.Visibility = [System.Windows.Visibility]::Visible } function Set-StatusMessage { # Flashes a temporary message in the status bar, then restores the previous text after a few seconds. [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet('Success', 'Danger', 'Accent', 'Muted')] [string]$Type = 'Success' ) if (-not $txtblk_StatusBar) { return } $brushKey = switch ($Type) { 'Success' { 'Success' } 'Danger' { 'Danger' } 'Accent' { 'Accent' } default { 'TextMuted' } } # Snapshot the resting state so the revert restores whatever was there before, not a hardcoded string. # Only capture when the bar is at rest; otherwise a rapid second flash would capture the first transient. if (-not ($Script:StatusTimer -and $Script:StatusTimer.IsEnabled)) { $Script:StatusRestoreText = $txtblk_StatusBar.Text $Script:StatusRestoreBrush = $txtblk_StatusBar.Foreground $Script:StatusRestoreWeight = $txtblk_StatusBar.FontWeight } $txtblk_StatusBar.Text = $Message $txtblk_StatusBar.Foreground = $formMSIProperties.FindResource($brushKey) $txtblk_StatusBar.FontWeight = [System.Windows.FontWeights]::SemiBold # Reuse a single timer so rapid clicks don't stack revert callbacks. if (-not $Script:StatusTimer) { $Script:StatusTimer = New-Object System.Windows.Threading.DispatcherTimer $Script:StatusTimer.Interval = [TimeSpan]::FromSeconds(4) # Stop via $sender and read the restore state off the timer's Tag: script-scoped # variables aren't reliably visible inside the Tick callback, so keep it self-contained. $Script:StatusTimer.Add_Tick({ param($timer, $e) $timer.Stop() $restore = $timer.Tag if ($restore) { $restore.Bar.Text = $restore.Text $restore.Bar.Foreground = $restore.Brush $restore.Bar.FontWeight = $restore.Weight } }) } $Script:StatusTimer.Stop() $Script:StatusTimer.Tag = [pscustomobject]@{ Bar = $txtblk_StatusBar Text = $Script:StatusRestoreText Brush = $Script:StatusRestoreBrush Weight = $Script:StatusRestoreWeight } $Script:StatusTimer.Start() } function Get-WindowBitmap { # Renders the window's visual tree to a bitmap at the current DPI so the capture is crisp on scaled displays. [CmdletBinding()] param() $source = [System.Windows.PresentationSource]::FromVisual($formMSIProperties) $scaleX = if ($source) { $source.CompositionTarget.TransformToDevice.M11 } else { 1 } $scaleY = if ($source) { $source.CompositionTarget.TransformToDevice.M22 } else { 1 } $pixelWidth = [int][Math]::Ceiling($formMSIProperties.ActualWidth * $scaleX) $pixelHeight = [int][Math]::Ceiling($formMSIProperties.ActualHeight * $scaleY) # Suppress hit-testing during the render so a control under the cursor (e.g. a textbox left # hovered after the menu closes) doesn't capture its IsMouseOver accent state. $formMSIProperties.IsHitTestVisible = $false [System.Windows.Input.Mouse]::Synchronize() $formMSIProperties.UpdateLayout() try { $rtb = New-Object System.Windows.Media.Imaging.RenderTargetBitmap($pixelWidth, $pixelHeight, (96 * $scaleX), (96 * $scaleY), [System.Windows.Media.PixelFormats]::Pbgra32) $rtb.Render($formMSIProperties) } finally { $formMSIProperties.IsHitTestVisible = $true [System.Windows.Input.Mouse]::Synchronize() } return $rtb } function Get-UpdateChannel { # Detects how the script was launched so the update action can match the channel. # Cached once in $Script:UpdateChannel; ordering matters because RightClick and LooseFile # both have a non-empty $PSCommandPath. [CmdletBinding()] param() # Web: launched via iex (irm ...); nothing on disk to update, next launch is always latest. if ([string]::IsNullOrEmpty($PSCommandPath)) { $channel = 'Web' } else { $scriptFolder = Split-Path -Path $PSCommandPath -Parent # Right-click install: running from the LOCALAPPDATA copy. if ($scriptFolder -eq $Script:RightClickMenuFolderPath) { $channel = 'RightClick' } else { # PowerShell Gallery install: Install-Script location matches where we're running from. $installed = Get-InstalledScript -Name 'GetMSIInformation' -ErrorAction SilentlyContinue if ($installed -and $installed.InstalledLocation -eq $scriptFolder) { $channel = 'PSGallery' } else { # Anything else: a loose .ps1 on disk. $channel = 'LooseFile' } } } Write-Host "Update channel: [$channel]" return $channel } function Open-ReleasePage { # Universal fallback for the update action: open the newer release (or the releases list). if ($Script:LatestReleaseUrl) { Start-Process $Script:LatestReleaseUrl } else { Start-Process $Script:ReleasesPageUrl } } function Update-ScriptFile { # Overwrites the target .ps1 with the latest release. Returns $false so the caller can fall # back to the releases page when the file can't be replaced in place (locked, read-only, offline). [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ScriptPath, [Parameter(Mandatory = $true)] [string]$Tag ) # Can't overwrite a file that's locked (e.g. open in an editor). if (Test-FileLock -Path $ScriptPath) { Write-Host "Update aborted: script file is locked [$ScriptPath]" return $false } $downloadUrl = "https://raw.githubusercontent.com/$($Script:GitHubRepo)/$Tag/$($Script:ScriptName)" $tempFile = Join-Path -Path $env:TEMP -ChildPath 'GetMSIInformation.update.ps1' try { Write-Host "Downloading update: [$downloadUrl]" Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing -ErrorAction Stop if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { throw 'Downloaded file is empty.' } Write-Host "Replacing script: [$ScriptPath]" Copy-Item -Path $tempFile -Destination $ScriptPath -Force -ErrorAction Stop return $true } catch { Write-Host "Update failed: $($_.Exception.Message)" return $false } finally { Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue } } function Restart-Script { # Relaunches the on-disk script so an applied update takes effect, then closes this instance. [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ScriptPath, [string]$FilePath ) # Prefer pwsh 7.4+ (matches the startup relaunch); fall back to Windows PowerShell. if ($Script:PowerShellPath -and $Script:PowerShellPath.Version -ge [Version]"7.4") { $CommandExe = $Script:PowerShellPath.Path } else { $CommandExe = "C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe" } $argList = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$ScriptPath`"" if (-not [string]::IsNullOrEmpty($FilePath)) { $argList += " -FilePath `"$FilePath`"" } Write-Host "Relaunching updated script: [$ScriptPath]" Start-Process -FilePath $CommandExe -ArgumentList $argList $formMSIProperties.Close() } function Install-RightClickMenu { # Installs or refreshes the LOCALAPPDATA copy, icon, and .msi right-click registry entry. # Shared by the Install menu item and the RightClick update path. [CmdletBinding()] param() # Set Script Name $SaveAsScriptName = $ScriptName # Create a new directory in the LOCALAPPDATA folder Write-Host "Creating Folder: [$($Script:RightClickMenuFolderPath)]" $DestinationFolderPath = "$($Script:RightClickMenuFolderPath)" if (-not (Test-Path $DestinationFolderPath)) { $DestinationFolder = New-Item -ItemType Directory -Path $DestinationFolderPath -ErrorAction SilentlyContinue } else { $DestinationFolder = Get-Item -Path $DestinationFolderPath } # Create an ico file from $Script:WindowIconBase64 $IconFilePath = "$($DestinationFolder.FullName)\GetMSIInformation.ico" # Delete existing Icon file if it exists Remove-Item $IconFilePath -Force -ErrorAction SilentlyContinue | Out-Null Write-Host "Creating Icon file: [$IconFilePath]" $IconByteArray = [System.Convert]::FromBase64String($Script:WindowIconBase64) [System.IO.File]::WriteAllBytes($IconFilePath, $IconByteArray) # Check if the script is being Invoked from the Internet if ($PSCommandPath -ne "") { # Copy the script to the new directory Write-Host "Creating Script: [$($DestinationFolder.FullName)\$($SaveAsScriptName)]" Copy-Item "$PSScriptRoot\$([System.IO.Path]::GetFileName($PSCommandPath))" -Destination "$($DestinationFolder.FullName)\$($SaveAsScriptName)" -ErrorAction SilentlyContinue } else { Write-Host "PSCommandPath is not available." # Script URL $ScriptURL = "https://raw.githubusercontent.com/MichaelEscamilla/GetMSIInformation/main/GetMSIInformation.ps1" Write-Host "Downloading script: [$ScriptURL]" try { Invoke-WebRequest -Uri $ScriptURL -OutFile "$($DestinationFolder.FullName)\$($SaveAsScriptName)" -ErrorAction Stop Write-Host "Script saved: [$($DestinationFolder.FullName)\$($SaveAsScriptName)]" } catch { Write-Host "Failed to download the script: $_" } } # Remove the legacy .msi-only entry so upgraders don't keep a stale duplicate on MSI files. if ((Test-Path -LiteralPath "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName") -eq $true) { Remove-Item "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName" -Force -Recurse -ErrorAction SilentlyContinue } # Prefer pwsh 7.4+ so the menu launches directly and skips the slow relaunch. # Fall back to Windows PowerShell (always present) when pwsh isn't installed. if ($Script:PowerShellPath -and $Script:PowerShellPath.Version -ge [Version]"7.4") { $CommandExe = $Script:PowerShellPath.Path } else { $CommandExe = "C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe" } # Pick the icon: prefer the extracted .ico, then pwsh, then Windows PowerShell. if (Test-Path -LiteralPath $IconFilePath) { $IconValue = $IconFilePath } elseif ($Script:PowerShellPath) { $IconValue = $Script:PowerShellPath.Path } else { $IconValue = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" } # Register under the '*' key so the entry appears on the right-click menu of every file type. # '*' is a literal registry key but a wildcard to the PowerShell registry provider, so use the # .NET registry API to create/set it reliably. # TODO: Revisit the menu label - "Get MSI Information" now shows on all file types, not just .msi. $MenuKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Software\Classes\*\shell\$RightClickMenuName") $MenuKey.SetValue('', $RightClickMenuName) $MenuKey.SetValue('icon', $IconValue) $CommandKey = $MenuKey.CreateSubKey('command') $CommandKey.SetValue('', "`"$CommandExe`" -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -Command `"$($DestinationFolder.FullName)\$($SaveAsScriptName)`" -FilePath '%1'") $CommandKey.Close() $MenuKey.Close() Write-Host "Registry Modified: [HKCU:\Software\Classes\*\shell\$($RightClickMenuName)]" Write-Host "Installation Complete" } #endregion Functions ############################################# ################# Main Script ################ ############################################# # Relaunch the script in PowerShell 7.4 or higher if available. Invoke-LaunchAsPwsh # Load Assemblies Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName System.Windows.Forms # Build the GUI [xml]$XAMLformMSIProperties = @"